Skip to content

Commit 771a003

Browse files
atergaclaudegithub-actions[bot]
authored
feat(be): add implicit:origin and implicit:issued_at_timestamp_ns to ICRC-3 message (dfinity#3730)
## Summary - Add two new implicit attributes to the ICRC-3 certified message map produced by `prepare_icrc3_attributes`: - `implicit:origin` — the origin of the relying party app (UTF-8 blob), enabling verifiers to confirm which app the attributes were issued for - `implicit:issued_at_timestamp_ns` — the canister time at the moment of issuance as a UTF-8 decimal string of nanoseconds since Unix epoch, enabling verifiers to check freshness - These join the existing `implicit:nonce` field, bringing the total implicit fields to three - The timestamp is passed as a parameter to `Anchor::prepare_icrc3_attributes` (sourced from `ic_cdk::api::time()` in the handler) to keep the method testable ## Test plan - [x] Unit tests pass (4 prepare_icrc3 error path tests updated with new parameters) - [x] Integration test `should_certify_icrc3_attributes_mixed_omit_scope` updated to assert on all 5 map entries (email, name, nonce, origin, timestamp) and verify the signature - [x] `cargo check --tests` passes < [Previous PR](dfinity#3730) | --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 8d71a1d commit 771a003

3 files changed

Lines changed: 62 additions & 4 deletions

File tree

src/internet_identity/src/attributes.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ impl Anchor {
261261
&self,
262262
attribute_specs: Vec<ValidatedAttributeSpec>,
263263
nonce: Vec<u8>,
264+
origin: String,
265+
issued_at_timestamp_ns: u64,
264266
account: Account,
265267
) -> Result<Vec<u8>, PrepareIcrc3AttributeError> {
266268
let mut certified_pairs = BTreeMap::new();
@@ -339,6 +341,11 @@ impl Anchor {
339341
}
340342

341343
certified_pairs.insert("implicit:nonce".to_string(), nonce);
344+
certified_pairs.insert("implicit:origin".to_string(), origin.into_bytes());
345+
certified_pairs.insert(
346+
"implicit:issued_at_timestamp_ns".to_string(),
347+
issued_at_timestamp_ns.to_string().into_bytes(),
348+
);
342349

343350
let message = icrc3_attribute_message(&certified_pairs);
344351

@@ -1611,6 +1618,8 @@ mod tests {
16111618
google_spec(AttributeName::Name, Some(b"Wrong Name"), false), // wrong
16121619
],
16131620
vec![0u8; 32],
1621+
"https://dapp.com".to_string(),
1622+
1_000_000_000,
16141623
account,
16151624
);
16161625

@@ -1646,6 +1655,8 @@ mod tests {
16461655
omit_scope: false,
16471656
}],
16481657
vec![0u8; 32],
1658+
"https://dapp.com".to_string(),
1659+
1_000_000_000,
16491660
account,
16501661
);
16511662

@@ -1677,6 +1688,8 @@ mod tests {
16771688
omit_scope: false,
16781689
}],
16791690
vec![0u8; 32],
1691+
"https://dapp.com".to_string(),
1692+
1_000_000_000,
16801693
account,
16811694
);
16821695

@@ -1705,6 +1718,8 @@ mod tests {
17051718
google_spec(AttributeName::Email, None, true),
17061719
],
17071720
vec![0u8; 32],
1721+
"https://dapp.com".to_string(),
1722+
1_000_000_000,
17081723
account,
17091724
);
17101725

src/internet_identity/src/main.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,12 +1345,20 @@ mod attribute_sharing {
13451345
PrepareIcrc3AttributeError::AuthorizationError(principal)
13461346
})?;
13471347

1348-
let account = get_account_for_origin(anchor.anchor_number(), origin, account_number)
1349-
.map_err(PrepareIcrc3AttributeError::GetAccountError)?;
1348+
let account =
1349+
get_account_for_origin(anchor.anchor_number(), origin.clone(), account_number)
1350+
.map_err(PrepareIcrc3AttributeError::GetAccountError)?;
13501351

13511352
state::ensure_salt_set().await;
13521353

1353-
let message = anchor.prepare_icrc3_attributes(attributes, nonce, account)?;
1354+
let issued_at_timestamp_ns = ic_cdk::api::time();
1355+
let message = anchor.prepare_icrc3_attributes(
1356+
attributes,
1357+
nonce,
1358+
origin,
1359+
issued_at_timestamp_ns,
1360+
account,
1361+
)?;
13541362

13551363
Ok(PrepareIcrc3AttributeResponse { message })
13561364
}

src/internet_identity/tests/integration/attributes.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,17 +658,21 @@ fn should_certify_icrc3_attributes_mixed_omit_scope() {
658658
nonce: nonce.clone(),
659659
};
660660

661+
let time_before = env.get_time().as_nanos_since_unix_epoch();
662+
661663
let prepare_response =
662664
api::prepare_icrc3_attributes(&env, canister_id, principal, prepare_request)
663665
.expect("failed to call prepare_icrc3_attributes")
664666
.expect("prepare_icrc3_attributes error");
665667

668+
let time_after = env.get_time().as_nanos_since_unix_epoch();
669+
666670
let icrc3_value: Icrc3Value = candid::Decode!(&prepare_response.message, Icrc3Value)
667671
.expect("failed to decode ICRC-3 value");
668672

669673
match &icrc3_value {
670674
Icrc3Value::Map(entries) => {
671-
assert_eq!(entries.len(), 3);
675+
assert_eq!(entries.len(), 5);
672676
let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
673677
// email should have scope omitted
674678
assert!(
@@ -692,6 +696,37 @@ fn should_certify_icrc3_attributes_mixed_omit_scope() {
692696
Icrc3Value::Blob(nonce.clone()),
693697
"Nonce value in certified message does not match the provided nonce"
694698
);
699+
// origin should be included
700+
let origin_entry = entries
701+
.iter()
702+
.find(|(k, _)| k == "implicit:origin")
703+
.expect("Expected 'implicit:origin' key in message map");
704+
assert_eq!(
705+
origin_entry.1,
706+
Icrc3Value::Blob(origin.as_bytes().to_vec()),
707+
"Origin value does not match"
708+
);
709+
// issued_at_timestamp_ns should fall within the time window of the call
710+
let timestamp_entry = entries
711+
.iter()
712+
.find(|(k, _)| k == "implicit:issued_at_timestamp_ns")
713+
.expect("Expected 'implicit:issued_at_timestamp_ns' key in message map");
714+
match &timestamp_entry.1 {
715+
Icrc3Value::Blob(bytes) => {
716+
let ts: u64 = std::str::from_utf8(bytes)
717+
.expect("timestamp should be valid UTF-8")
718+
.parse()
719+
.expect("timestamp should be a valid u64");
720+
assert!(
721+
ts >= time_before && ts <= time_after,
722+
"Timestamp {} should be between {} and {}",
723+
ts,
724+
time_before,
725+
time_after
726+
);
727+
}
728+
other => panic!("Expected Blob for timestamp, got {:?}", other),
729+
}
695730
}
696731
other => panic!("Expected Map, got {:?}", other),
697732
}

0 commit comments

Comments
 (0)