fix: accept delegation chains issued by a non-mainnet auth provider - #767
fix: accept delegation chains issued by a non-mainnet auth provider#767marc0olo wants to merge 3 commits into
Conversation
86359a3 to
c1e982d
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Canister-signature corruption is incorrectly treated as a root-key mismatch and accepted.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates delegated identity validation to support chains issued by non-mainnet authentication providers while still validating the remaining chain.
Changes:
- Adds suffix-based delegation verification with network-root fallback.
- Reuses validation during identity loading and linking.
- Adds unit tests for valid, skipped, broken, and mismatched chains.
File summaries
| File | Description |
|---|---|
crates/icp/src/identity/key.rs |
Implements shared delegation-chain verification and tests. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
c1e982d to
d9ded81
Compare
|
Copilot is right, and the fix is now different.
Rather than distinguish by error text, the peeling is gone. The chain is now accepted unverified only where no root key could be resolved at all ( So corruption is no longer misclassified as a root-key mismatch: on any network-resolving path it is caught, and on the no-root-key path the warning no longer claims a mismatch, only that nothing was available to check against. A typed |
There was a problem hiding this comment.
🟡 Changes recommended
Supplied network roots can be bypassed through mainnet validation or cached unchecked identities, and the core fallback lacks direct tests.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 3
- Review effort level: Balanced
d9ded81 to
7dd2c0f
Compare
|
All three are valid; fixed. Mainnet-first ordering. Correct, and it contradicted the rule the PR itself stated. A resolved network root key is now authoritative and the only key consulted — Loader cache. Real and reachable: Test coverage. Also correct — For the record on round 1: the malformed-CBOR fixture is no longer load-bearing. Nothing peels links any more, so the corruption-versus-root-mismatch distinction only decides whether an error is fatal, and on the no-root-key path both are treated the same and named honestly — nothing was available to check against. Also confirmed against the live local replica that issued the chain: the canister signature verifies under its root key and fails only under mainnet's. |
There was a problem hiding this comment.
🟡 Changes recommended
Cache entries remain root-key agnostic, and unchecked multi-link chains can bypass validation of signatures after the canister-signature link.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
crates/icp/src/identity/key.rs:1806
- This endpoint check does not complete the validation that stopped at the first canister signature. If that link is followed by another delegation with a forged ordinary signature but the expected session public key, the check succeeds and
create_identity/link_webauth_identitypersist a structurally invalid chain. Validate every suffix signature after the unverifiable canister link (or reject multi-link fallback chains) before returning success.
ensure!(
chain_ends_at_session(&from_key, &delegations, &**session)
.map_err(|message| ValidateDelegationChainError::SessionPrincipal { message })?,
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Balanced
7dd2c0f to
9ce0add
Compare
|
All three valid; fixed together, and the root cause of the churn addressed rather than patched. Unverified links behind the canister signature. Correct — and it is the property the round-1 fix removed. The resolution is to identify unverifiable links structurally rather than by error variant: Cache still root-key agnostic. Correct, and my previous Network variant over-applied. Correct. Only Test matrix is now 11 cases covering both regressions above rather than the happy path. Remaining known limit, for the record: with no root key resolved, a corrupt canister signature is still indistinguishable from one issued by an unknown network. Closing that means either matching error text or reimplementing the root-key-independent half of IC certificate verification here. The right fix is a typed variant upstream in ic-agent; not in this PR. |
There was a problem hiding this comment.
🟡 Changes recommended
The fallback can persist an expired non-mainnet delegation, and the network-mismatch error is overly definitive.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Balanced
9ce0add to
f0ffaf7
Compare
|
Two of three are correct; one is right about the gap but wrong about the cause. Error message overstates. Correct, and it is the round-1 point applied to wording rather than control flow — I reasoned about that ambiguity and then wrote a message asserting certainty anyway. Now: "could not verify the canister signature ... against the selected network's root key; this identity may have been issued for a different network." Cache-key regression untested. Correct. Expired chain persisted by the link path. The gap is real, but it is not introduced here — Fixed regardless, since it lands in the validator this PR already rewrites, and the import path's own comment argues exactly why it should exist. Added |
There was a problem hiding this comment.
🟡 Changes recommended
Root-independent corruption in skipped canister-signature links can currently pass validation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Balanced
f0ffaf7 to
a75dbc0
Compare
|
Correct, and taken the thorough way rather than the documentation way. You are right on both halves: the OID identifies the signing algorithm, not the cause of the failure, and my doc comment claimed this function "verifies everything that does not depend on the root key" when it verified none of the discarded link. Worth being precise about the cost of that: on
Validation that matters most here is that a genuine signature still passes. Three checks:
Test count 12 to 14. New direct dependencies The upstream point still stands and I have not tried to work around it here: a typed error separating trust-root failure from structural corruption would make this reimplementation unnecessary. |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes identity/delegation verification behavior (including cryptographic validation paths), which is security-sensitive and warrants final human review.
Review details
Suppressed comments (1)
crates/icp/src/identity/key.rs:723
DelegationError::InvalidCanisterSignatureis not only caused by a trust-root mismatch; it can also indicate malformed/tampered canister-signature material (as noted later in this file). The inline comment here currently claims the root key is the only deciding factor, which is inaccurate and risks misleading future maintainers.
// A canister signature is the only failure the root key decides; anything else is a
// broken chain, which a different root key would not have saved.
Err(e @ DelegationError::InvalidCanisterSignature(_)) => {
Err(e).context(ValidateDelegationChainNetworkSnafu { path: chain_path })
}
- Files reviewed: 6/7 changed files
- Comments generated: 1
- Review effort level: Lite
68a8edc to
a5969f7
Compare
|
Both halves correct, and they are the same defect:
Deliberately verifying from the last canister-signed link rather than past the leading run: the leading-run notion belongs to the no-root-key path, where a mid-chain canister signature is left as an error on purpose. Reusing it here would have re-broken the mid-chain classification fixed in the previous round. Added |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes security-sensitive identity/delegation verification behavior (including canister-signature handling), which warrants final human review despite strong unit test coverage.
Review details
Suppressed comments (1)
crates/icp/src/identity/key.rs:137
- The error message mentions a “malformed certificate delegation”, which reads like a typo/unclear phrasing (it’s not obvious what a “certificate delegation” is). Consider rewording to explicitly say “malformed certificate or delegation” (or similar) so the alternative failure mode is clear.
#[snafu(display(
"could not verify the canister signature in the delegation chain loaded from `{path}` \
against the selected network's root key; this identity was most likely issued for a \
different network, though a malformed certificate delegation would also fail here"
))]
- Files reviewed: 6/7 changed files
- Comments generated: 1
- Review effort level: Lite
a5969f7 to
41de02e
Compare
|
Correct, and easy to confirm on the wire: So every canister-signature fixture in the suite was decoding an input shape that no auth provider actually produces. The production path handled the tagged form — I checked a genuine chain against it earlier in this PR — but nothing in the committed tests pinned that, so a future change to the CBOR layer could break real signatures with the suite still green. That is exactly the regression the comment describes. The fixture builder now writes the tag via Added |
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces new security-sensitive delegation validation logic (certificate/canister-signature handling) that warrants final human review despite good test coverage.
Review details
Suppressed comments (1)
crates/icp/src/identity/key.rs:723
- This comment implies you can distinguish “untrusted root key” vs “damaged signature”, but the follow-up checks don’t (and can’t) validate the certificate’s own BLS signature bytes without a trusted root key. Clarifying that the fallback only distinguishes structural corruption from certificate BLS verification failure will keep the rationale accurate.
// `InvalidCanisterSignature` covers both a signature this root key does not trust and
// one that is simply damaged. ic-agent stops at the link it could not verify, so the
// rest of the chain is still unexamined. Check everything that needs no root key: if
// all of it holds, the root key is the only thing that turned this chain down.
- Files reviewed: 6/7 changed files
- Comments generated: 1
- Review effort level: Lite
41de02e to
c05fc31
Compare
|
The conclusion here is not right, but it is pointing at real wording that misled it.
These sit behind the BLS step, so they cannot be reached without a trusted root key, and dropping the hedge would state a certainty the code has not established. Mainnet II certificates always carry a subnet delegation, so this is the live case, not a theoretical one. That said, the phrase "a malformed certificate delegation" was evidently readable as "a malformed certificate", which the structural check does cover — so the wording was doing the opposite of its job. Both the message and the code comment now say what is actually true:
The old comment claimed "the root key is the only thing that turned this chain down", which was the overstatement that prompted this. That is the part that was genuinely wrong, and it is fixed. Eliminating the hedge entirely would mean following the subnet delegation ourselves — parsing the inner certificate and re-deriving canister ranges — which is more reimplementation than a message nuance justifies. The upstream typed-error change remains the real fix. |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core identity/delegation validation behavior (including certificate/hash-tree parsing) in a security-sensitive area and warrants final human review.
Review details
Suppressed comments (1)
crates/icp/src/identity/key.rs:137
- The
ValidateDelegationChainNetworkerror message currently implies the chain was “most likely issued for a different network”, but after the structural re-checks the remaining causes also include an invalid certificate BLS signature or invalid subnet delegation (both are behind the root-key verification step and aren’t distinguished here). Consider wording this as an ambiguous verification failure rather than primarily a network mismatch.
"the canister signature in the delegation chain loaded from `{path}` is well formed, but \
the selected network's root key does not accept it; this identity was most likely issued \
for a different network — the one part not checked here is the certificate's subnet \
delegation"
- Files reviewed: 6/7 changed files
- Comments generated: 1
- Review effort level: Lite
c05fc31 to
ff91162
Compare
|
Correct. Worth recording why this is safe to tighten, since ic-agent parses the same key leniently and a divergence would be the thing to fear. Well-formed keys decode identically under both, so nothing legitimate changes. They differ only on a key with trailing bytes, and there the strict behaviour is the one we want:
So the malformed key is rejected instead of quietly accepted, and no well-formed input moves. Added |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes security-sensitive delegation validation semantics and introduces new certificate/parsing logic where a human review of correctness and edge cases is warranted despite the added tests.
Review details
- Files reviewed: 6/7 changed files
- Comments generated: 0 new
- Review effort level: Lite
ff91162 to
334c1ba
Compare
A chain from a local Internet Identity carries a canister signature whose certificate only BLS-verifies against that replica's root key. Two things made such an identity unusable: `icp identity principal`, `account-id` and `delegation sign` pass no network root key and have no flag to supply one, so the chain was always checked against mainnet and always failed. `DelegatedIdentity::new` stops at the first link it cannot verify, so the links behind the canister signature went unchecked, at load time and at link time — including in `create_identity`, which documents its session-key check as running before anything is written. A resolved network root key is now authoritative and the only key consulted; with no network resolved, mainnet is the assumption. Where that leaves a canister signature unverifiable, verify each link as far as it can be verified without a root key. For a canister signature that is everything but the certificate's own BLS signature: that the CBOR decodes, that the signing canister's certified data matches the signature tree, and that the tree carries a signature over exactly this delegation. ic-agent verifies canister signatures only as a whole, so those checks are repeated here rather than skipped with the trust check. Key the identity cache by the root key as well as the selection: the same identity validates differently against different networks, so an entry cached for one must not be handed to a load that resolved another. `canister create` and `canister settings update` now take the caller principal from the agent rather than loading the same identity a second time without a root key. Also check expiry before `icp identity link web` writes a chain, as the import and load paths already do.
334c1ba to
b8bf1ea
Compare
Every other canister-signature fixture is encoded by the same types the production code decodes with, so it is self-consistent by construction: a change that shifts encoding and decoding together would keep those tests green while rejecting every signature a real provider issues. Add a chain a local Internet Identity actually issued, with the root key of the replica that issued it, and check that it verifies against that root key, is accepted unverified when none is available, and is rejected under the mainnet key. Verification only asks the session identity for its principal, so a stub carrying it stands in and no key material is stored.
There was a problem hiding this comment.
🔵 Needs a closer look
It changes delegation-chain validation behavior in security-sensitive identity code (including new verification logic and dependencies) and warrants final human review despite good test coverage.
Review details
- Files reviewed: 8/9 changed files
- Comments generated: 1
- Review effort level: Lite
|
Deliberate, and declining. The suggested shape is what // main, build path fallback:
// re-deserialize as ::new just ate the old values (better than an up-front clone since this path should be rare)
let (from_key, signed_delegations) = delegation::to_agent_types(&stored_chain)
.expect("same conversion already succeeded");That trades one clone for a second hex-decode pass plus an The fallback needs owned values regardless: On the cost itself — the chain in Cloning only when |
icp identity principal(andaccount-id,delegation sign) fails outright for an identitylinked against a local Internet Identity:
The chain's first link is a canister signature that only BLS-verifies against the issuing replica's
root key. Those commands pass no network root key and have no
--networkflag to supply one, sothe chain is always checked against mainnet. And
DelegatedIdentity::newstops at the first linkit cannot verify, so the links behind it — including the session-key check
create_identitydocuments as running before anything is written — never ran.
Change
resolved, mainnet is the assumption.
can be without a root key — for a canister signature, everything but the certificate's own BLS
signature. ic-agent verifies canister signatures only as a whole, so those checks are
reimplemented here.
ic-agent reports corruption through the same variant as a trust-root mismatch.
for another. It is now keyed by
(selection, root_key), andcanister create/canister settings updatetake the caller principal from the agent instead of loading the same identity asecond time without a root key.
icp identity link webnow checks expiry before writing, as the import and load paths alreadydo. Pre-existing; fixed here because it lands in the same validator.
ic-certificationandserde_bytes, both already in the lockfile viaic-agent.Stricter than main
A mainnet II identity used against a local network now fails at load rather than at ingress.
Delegation-based identities only — PEM, keyring, HSM and Pbes2 session chains are untouched.
Tests
17 unit tests, including a chain a local Internet Identity actually issued together with that
replica's root key. The synthetic fixtures are encoded by the same types the production code
decodes with, so only real bytes catch a change that shifts encoding and decoding together.
Worth discussing separately
DelegationError::InvalidCanisterSignaturecollapses a trust-root failure and structuralcorruption into one variant, which is why those checks are reimplemented here. Filed as
dfinity/agent-rs#742 and referenced from the code; that landing deletes the reimplementation and
both new dependencies.
Nothing records which network a linked identity belongs to, so local-II and mainnet-II
delegations are indistinguishable on disk and
icp identity listcannot show it. Storing a rootkey at link time isn't obviously right either: managed networks regenerate theirs on recreate.