Skip to content

test: add ICRC-3 certified attribute test vectors#3787

Closed
aterga wants to merge 21 commits intodfinity:mainfrom
aterga:arshavir/icrc3-test-vectors
Closed

test: add ICRC-3 certified attribute test vectors#3787
aterga wants to merge 21 commits intodfinity:mainfrom
aterga:arshavir/icrc3-test-vectors

Conversation

@aterga
Copy link
Copy Markdown
Collaborator

@aterga aterga commented Apr 16, 2026

Summary

Adds 10 integration test vectors for ICRC-3 certified attributes, covering every combination of attribute selection and encoding that the II backend canister can produce. Depends on #3770.

Each vector exercises the full pipeline — prepare_icrc3_attributesget_icrc3_attributes → signature verification — and captures:

  • Candid-encoded ICRC-3 message bytes
  • Domain-separated signed message ([0x0e] || "ic-sender-info" || message)
  • Full CBOR certificate from the canister signature
  • Human-readable ICRC-3 map representation

Scenarios covered:

# Label
1 Single email, scoped key
2 Single email, unscoped key (omit_scope = true)
3 Single name, scoped key
4 Email + name, both scoped
5 Email + name, both unscoped
6 Mixed scoping (email unscoped, name scoped)
7 Email with value validation
8 Email with specific nonce
9 Mixed scoping with specific nonce
10 No user attributes (only implicit entries)

Tests

  • icrc3_test_vectors integration test in attributes.rs — generates all 10 vectors, verifies every CBOR signature, and prints results as JSON
  • docs/icrc3-test-vectors.md — reference doc explaining the encoding pipeline and how to regenerate
  • docs/icrc3-test-vectors.json — machine-readable snapshot from a single test run

🤖 Generated with Claude Code

sea-snake and others added 21 commits April 10, 2026 10:34
…ent attribute flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…alues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
prepare_icrc3_attributes returns AttributeMismatch when requested
attributes don't exist, unlike legacy prepare_attributes which silently
omits them. The test app now resolves to undefined on error instead of
throwing, so the delegation flow can still complete.
Instead of returning AttributeMismatch when requested attributes are
unavailable, silently skip them — matching the legacy prepare_attributes
behavior. This lets the delegation flow complete with whatever attributes
are available.
- Attribute values (email, name, verified_email): Blob → Text
- implicit:origin: Blob → Text
- implicit:issued_at_timestamp_ns: Blob (string bytes) → Nat
- implicit:nonce: stays Blob (raw 32 bytes)
@aterga aterga requested a review from a team as a code owner April 16, 2026 19:48
Copilot AI review requested due to automatic review settings April 16, 2026 19:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds and exercises ICRC-3 attribute sharing paths across backend, frontend, and tests, updating the encoding to semantically-typed ICRC-3 values and extending Playwright coverage (while preserving legacy attribute tests).

Changes:

  • Backend: switch ICRC-3 attribute message construction to use Icrc3Value variants (Text/Nat/Blob) and adopt “silent omit” behavior when resolving requested attributes.
  • Frontend: add a new JSON-RPC method handler (ii-icrc3-attributes) and request schema for ICRC-3 attribute retrieval.
  • Tests/demo: extend Playwright tests for ICRC-3 attributes (and add a legacy-spec file), plus demo test-app UI + plumbing for ICRC-3 attributes and app-supplied nonce.

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/internet_identity/tests/integration/attributes.rs Updates assertions for new ICRC-3 value types and adds an integration test for silent omission of unavailable attributes.
src/internet_identity/src/attributes.rs Changes ICRC-3 message building to accept typed Icrc3Values, switches implicit values to Text/Nat, and removes old unit tests in favor of integration coverage.
src/frontend/tests/e2e-playwright/routes/authorize/openid.spec.ts Migrates OpenID e2e assertions to ICRC-3 attributes, adds ICRC-3 decoding, and verifies implicit entries + app-supplied nonce.
src/frontend/tests/e2e-playwright/routes/authorize/openid-legacy.spec.ts Adds a parallel legacy-attributes e2e suite to preserve previous behavior coverage.
src/frontend/tests/e2e-playwright/fixtures/authorize.ts Extends fixture config to toggle ICRC-3 attributes and inject a nonce; adds authorizedIcrc3Attributes extraction.
src/frontend/src/routes/(new-styling)/(resuming-channel)/resume-openid-authorize/+page.svelte Adds JSON-RPC handling for ii-icrc3-attributes, wiring prepare/get ICRC-3 canister calls into the channel.
src/frontend/src/lib/utils/transport/utils.ts Introduces request params schema for ii-icrc3-attributes (keys + nonce).
src/frontend/src/lib/generated/internet_identity_types.d.ts Updates generated canister types to include ICRC-3-related APIs/types.
src/frontend/src/lib/generated/internet_identity_idl.js Updates generated candid IDL factory with ICRC-3-related APIs/types.
demos/test-app/src/index.tsx Adds UI wiring for ICRC-3 attributes display and nonce input; passes new options into auth flow.
demos/test-app/src/index.html Adds checkbox + nonce input + output area for ICRC-3 attributes.
demos/test-app/src/auth.ts Adds ICRC-3 attribute request flow (ii-icrc3-attributes) and returns icrc3Attributes to the caller.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Parameters schema for "ii-icrc3-attributes" request
export const Icrc3AttributesParamsSchema = z.object({
keys: z.array(z.string()),
nonce: z.base64(),
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Icrc3AttributesParamsSchema validates that nonce is base64, but it doesn't enforce the protocol requirement that the decoded nonce must be exactly 32 bytes. Since the backend rejects non-32-byte nonces, consider refining this schema (or adding an additional refinement after decoding) so invalid nonces get a proper INVALID_PARAMS response instead of failing later as an internal/canister error.

Suggested change
nonce: z.base64(),
nonce: z
.base64()
.refine(
(nonce) => z.util.base64ToUint8Array(nonce).byteLength === 32,
{
message: "Nonce must decode to exactly 32 bytes",
},
),

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +145
const nonce = icrc3Nonce ?? crypto.getRandomValues(new Uint8Array(32));
const icrc3AttributesPromise =
hasAttributes && useIcrc3Attributes
? signer
.sendRequest({
jsonrpc: "2.0",
method: "ii-icrc3-attributes",
id: window.crypto.randomUUID(),
params: {
keys: requestAttributes,
// @ts-ignore Not known in TS types yet but supported in all browsers
nonce: nonce.toBase64(),
},
})
.then((response) => {
if (
!("result" in response) ||
typeof response.result !== "object" ||
response.result === null ||
!("data" in response.result) ||
!("signature" in response.result)
) {
return undefined;
}
return {
// @ts-ignore Not known in TS types yet but supported in all browsers
data: Uint8Array.fromBase64(response.result.data),
// @ts-ignore Not known in TS types yet but supported in all browsers
signature: Uint8Array.fromBase64(response.result.signature),
};
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The demo relies on the nonstandard Uint8Array.prototype.toBase64() / Uint8Array.fromBase64() APIs (suppressed via @ts-ignore) when sending/receiving ICRC-3 attribute data. This can break in environments where those proposed APIs aren’t available. Prefer using a local base64 helper (e.g. btoa/atob-based conversion) or a shared utility so the demo (and Playwright tests that depend on it) don’t hinge on runtime support for those methods.

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +219
icrc3AttributesEl.innerText = JSON.stringify({
// @ts-ignore Not known in TS types yet but supported in all browsers
data: icrc3Attributes.data.toBase64(),
// @ts-ignore Not known in TS types yet but supported in all browsers
signature: icrc3Attributes.signature.toBase64(),
});
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

icrc3Attributes.data.toBase64() / signature.toBase64() are used without a fallback, and the surrounding comment claims browser-wide support (via @ts-ignore). To avoid the demo UI failing on browsers without these proposed APIs, consider switching to an explicit base64 encoding helper (or feature-detecting and falling back) when populating #icrc3Attributes.

Copilot uses AI. Check for mistakes.
Comment on lines +271 to +283
const blobEntries = decodeIcrc3TextEntries(
authorizedIcrc3Attributes.data,
);
// Only the name from the default provider should be present via implicit consent.
expect(
blobEntries[`openid:http://localhost:${DEFAULT_OPENID_PORT}:name`],
).toBe(defaultName);
expect(
blobEntries[`openid:http://localhost:${DEFAULT_OPENID_PORT}:email`],
).toBeUndefined();
expect(blobEntries["favorite_food"]).toBeUndefined();
expect(
blobEntries[`openid:http://localhost:${ALTERNATE_OPENID_PORT}:name`],
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name blobEntries is misleading here because decodeIcrc3TextEntries returns only Text-variant entries. Renaming to something like textEntries would make the assertions easier to follow and avoid confusion with the Blob variant used for implicit:nonce.

Suggested change
const blobEntries = decodeIcrc3TextEntries(
authorizedIcrc3Attributes.data,
);
// Only the name from the default provider should be present via implicit consent.
expect(
blobEntries[`openid:http://localhost:${DEFAULT_OPENID_PORT}:name`],
).toBe(defaultName);
expect(
blobEntries[`openid:http://localhost:${DEFAULT_OPENID_PORT}:email`],
).toBeUndefined();
expect(blobEntries["favorite_food"]).toBeUndefined();
expect(
blobEntries[`openid:http://localhost:${ALTERNATE_OPENID_PORT}:name`],
const textEntries = decodeIcrc3TextEntries(
authorizedIcrc3Attributes.data,
);
// Only the name from the default provider should be present via implicit consent.
expect(
textEntries[`openid:http://localhost:${DEFAULT_OPENID_PORT}:name`],
).toBe(defaultName);
expect(
textEntries[`openid:http://localhost:${DEFAULT_OPENID_PORT}:email`],
).toBeUndefined();
expect(textEntries["favorite_food"]).toBeUndefined();
expect(
textEntries[`openid:http://localhost:${ALTERNATE_OPENID_PORT}:name`],

Copilot uses AI. Check for mistakes.
Comment on lines 252 to +314
impl Anchor {
/// Resolves attribute specs against stored credentials, builds the Candid-encoded
/// ICRC-3 message, signs it, and returns the message blob.
///
/// For each `ValidatedAttributeSpec`:
/// - Looks up the stored value from the matching OpenID credential.
/// - If `spec.value` is `Some`, validates it matches the stored value.
/// - Computes the certified key: if `omit_scope` is true, uses just the attribute name
/// (e.g., `"email"`); otherwise uses the full key (e.g., `"openid:https://...:email"`).
pub fn prepare_icrc3_attributes(
&self,
attribute_specs: Vec<ValidatedAttributeSpec>,
nonce: Vec<u8>,
origin: String,
issued_at_timestamp_ns: u64,
account: Account,
) -> Result<Vec<u8>, PrepareIcrc3AttributeError> {
let mut certified_pairs = BTreeMap::new();
let mut problems = Vec::new();
let mut certified_pairs: BTreeMap<String, Icrc3Value> = BTreeMap::new();

for spec in &attribute_specs {
match &spec.key.scope {
Some(AttributeScope::OpenId { issuer }) => {
let credential = self
.openid_credentials
.iter()
.find(|c| c.config_issuer().as_deref() == Some(issuer.as_str()));

let Some(credential) = credential else {
problems.push(format!("No credential found for issuer: {}", issuer));
continue;
};
let Some(AttributeScope::OpenId { issuer }) = &spec.key.scope else {
// Only scoped attributes are supported; silently skip unscoped ones.
continue;
};

let stored_value = match spec.key.attribute_name {
AttributeName::Email => credential.get_email(),
AttributeName::Name => credential.get_name(),
AttributeName::VerifiedEmail => credential.get_verified_email(),
};
let Some(credential) = self
.openid_credentials
.iter()
.find(|c| c.config_issuer().as_deref() == Some(issuer.as_str()))
else {
continue;
};

let Some(stored) = stored_value else {
problems.push(format!(
"Attribute {} not available for issuer {}",
spec.key.attribute_name, issuer
));
continue;
};
let stored_value = match spec.key.attribute_name {
AttributeName::Email => credential.get_email(),
AttributeName::Name => credential.get_name(),
AttributeName::VerifiedEmail => credential.get_verified_email(),
};

// If a value was provided, validate it matches.
if let Some(ref expected_value) = spec.value {
if expected_value.as_slice() != stored.as_bytes() {
problems.push(format!(
"Attribute value mismatch for {}: provided value does not match stored value (stored {} bytes)",
spec.key,
stored.len()
));
continue;
}
}

// Compute the certified key.
let certified_key = if spec.omit_scope {
spec.key.attribute_name.to_string()
} else {
spec.key.to_string()
};
let Some(stored) = stored_value else {
continue;
};

match certified_pairs.entry(certified_key) {
std::collections::btree_map::Entry::Occupied(entry) => {
problems.push(format!(
"Duplicate certified attribute key '{}' derived from spec {}",
entry.key(),
spec.key
));
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(stored.into_bytes());
}
}
}
None => {
problems.push(format!(
"Attribute {} has no scope; only scoped attributes are supported",
spec.key
));
// If a value was provided, validate it matches.
if let Some(ref expected_value) = spec.value {
if expected_value.as_slice() != stored.as_bytes() {
continue;
}
}
}

if !problems.is_empty() {
return Err(PrepareIcrc3AttributeError::AttributeMismatch { problems });
// Compute the certified key.
let certified_key = if spec.omit_scope {
spec.key.attribute_name.to_string()
} else {
spec.key.to_string()
};

// Skip duplicates silently.
if let std::collections::btree_map::Entry::Vacant(entry) =
certified_pairs.entry(certified_key)
{
entry.insert(Icrc3Value::Text(stored));
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepare_icrc3_attributes now silently skips unscoped specs, unknown issuers, unavailable attributes, value mismatches, and duplicate certified keys, which means it no longer surfaces any of those situations as PrepareIcrc3AttributeError::AttributeMismatch. However, the public API still returns Result<_, PrepareIcrc3AttributeError> and the interface types still advertise AttributeMismatch, which is now effectively unreachable. Either restore AttributeMismatch for the relevant mismatch cases, or update the interface/docs to reflect the intentional silent-omit semantics (and consider removing/deprecating the error variant to avoid misleading API consumers).

Copilot uses AI. Check for mistakes.
Comment on lines +1523 to +1525
// Silent omit behavior for prepare_icrc3_attributes (unavailable attributes,
// unknown issuers, value mismatches, scopeless attributes, duplicate keys) is
// verified by the integration test should_silently_omit_unavailable_icrc3_attributes.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment claims the integration test should_silently_omit_unavailable_icrc3_attributes verifies silent-omit behavior for value mismatches, scopeless attributes, and duplicate keys, but the integration test currently only covers unavailable attributes and an unknown issuer. Please either expand the integration test to cover the additional cases listed here, or narrow this comment so it matches what is actually being tested.

Suggested change
// Silent omit behavior for prepare_icrc3_attributes (unavailable attributes,
// unknown issuers, value mismatches, scopeless attributes, duplicate keys) is
// verified by the integration test should_silently_omit_unavailable_icrc3_attributes.
// Silent omit behavior for prepare_icrc3_attributes for unavailable attributes
// and unknown issuers is verified by the integration test
// should_silently_omit_unavailable_icrc3_attributes.

Copilot uses AI. Check for mistakes.
@timothyaterton
Copy link
Copy Markdown
Contributor

Closing in favour of a branch pushed directly to the upstream repo.

1 similar comment
@aterga
Copy link
Copy Markdown
Collaborator Author

aterga commented Apr 16, 2026

Closing in favour of a branch pushed directly to the upstream repo.

@aterga aterga closed this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants