Problem
verifyProof accepts a document when any one proof in its proof array verifies. Nothing binds a set together, so a proof can be removed without trace.
The consequence is that adding a strong proof beside a weak one raises nothing. An attacker holding a credential that carries both an archon-ecdsa-jcs-2019 proof and a legacy EcdsaSecp256k1Signature2019 proof can drop the former and keep the latter, and the credential still verifies — under the legacy proof's weaker rules, where created and proofPurpose sit outside the signature and can be altered freely.
So a credential's effective strength is that of its weakest proof, not its strongest.
Why the rule exists
It is deliberate, and the reason is sound: a document may carry a cryptosuite this build does not implement alongside one it does, and rejecting the document for the former would make Archon unable to read a credential that is perfectly valid. Any change has to preserve that.
Current state
Documented in docs/scheme.md under Proof sets, not addressed in code. Credentials issued now carry archon-ecdsa-jcs-2019 (which binds its proof configuration, #1085), so this concerns credentials issued before that and any document carrying a mixture.
Related
The stated attack needs a document nothing emits
Credentials are signed by archonEcdsaJcs2019Proof and eddsaJcs2022Proof
together (packages/keymaster/src/keymaster.ts:1381-1382). The legacy
EcdsaSecp256k1Signature2019 proof is emitted only by addOperationProof
(:1272), which serves DID operations. No credential therefore carries a legacy
proof beside a Data Integrity one, and credentials issued before #1085 carry a
single legacy proof, where there is nothing to drop. Attaching a legacy proof to
a current credential requires the issuer's key.
What is reachable
Resolution keys off the proof's own created — resolveDID(signerDid, { versionTime: proof.created }) (:1632) — which selects the version of the
issuer's document that supplies the verification key.
For a Data Integrity proof created sits inside the signed configuration, so
altering it only makes that proof fail, and the remaining proofs in the set are
still tried. For a legacy proof it sits outside the signature and can be moved
freely while the signature still verifies. Legacy proofs are accepted
indefinitely (supportedProof, :1555), so a holder of an issuer key that has
since been rotated out can sign a legacy-labelled credential and date it before
the rotation, and it resolves to the document version that still lists that key.
This is the same shape as the created-selects-the-authorizing-version problem
recorded on #1087, on the credential path rather than the operation path.
Removal is silent but not a weakening for current credentials
Both emitted proofs bind their own configuration, so stripping the
eddsa-jcs-2022 proof leaves a credential that verifies under
archon-ecdsa-jcs-2019 alone — an unregistered suite no outside implementation
reads. The loss is third-party verifiability, without any signal that it
happened. Herald compares every proof's signer against the issuer
(services/herald/server/src/routes.ts:124-135), which stops a proof being
added but cannot observe one being removed.
Problem
verifyProofaccepts a document when any one proof in itsproofarray verifies. Nothing binds a set together, so a proof can be removed without trace.The consequence is that adding a strong proof beside a weak one raises nothing. An attacker holding a credential that carries both an
archon-ecdsa-jcs-2019proof and a legacyEcdsaSecp256k1Signature2019proof can drop the former and keep the latter, and the credential still verifies — under the legacy proof's weaker rules, wherecreatedandproofPurposesit outside the signature and can be altered freely.So a credential's effective strength is that of its weakest proof, not its strongest.
Why the rule exists
It is deliberate, and the reason is sound: a document may carry a cryptosuite this build does not implement alongside one it does, and rejecting the document for the former would make Archon unable to read a credential that is perfectly valid. Any change has to preserve that.
Current state
Documented in
docs/scheme.mdunder Proof sets, not addressed in code. Credentials issued now carryarchon-ecdsa-jcs-2019(which binds its proof configuration, #1085), so this concerns credentials issued before that and any document carrying a mixture.Related
The stated attack needs a document nothing emits
Credentials are signed by
archonEcdsaJcs2019ProofandeddsaJcs2022Prooftogether (
packages/keymaster/src/keymaster.ts:1381-1382). The legacyEcdsaSecp256k1Signature2019proof is emitted only byaddOperationProof(
:1272), which serves DID operations. No credential therefore carries a legacyproof beside a Data Integrity one, and credentials issued before #1085 carry a
single legacy proof, where there is nothing to drop. Attaching a legacy proof to
a current credential requires the issuer's key.
What is reachable
Resolution keys off the proof's own
created—resolveDID(signerDid, { versionTime: proof.created })(:1632) — which selects the version of theissuer's document that supplies the verification key.
For a Data Integrity proof
createdsits inside the signed configuration, soaltering it only makes that proof fail, and the remaining proofs in the set are
still tried. For a legacy proof it sits outside the signature and can be moved
freely while the signature still verifies. Legacy proofs are accepted
indefinitely (
supportedProof,:1555), so a holder of an issuer key that hassince been rotated out can sign a legacy-labelled credential and date it before
the rotation, and it resolves to the document version that still lists that key.
This is the same shape as the
created-selects-the-authorizing-version problemrecorded on #1087, on the credential path rather than the operation path.
Removal is silent but not a weakening for current credentials
Both emitted proofs bind their own configuration, so stripping the
eddsa-jcs-2022proof leaves a credential that verifies underarchon-ecdsa-jcs-2019alone — an unregistered suite no outside implementationreads. The loss is third-party verifiability, without any signal that it
happened. Herald compares every proof's signer against the issuer
(
services/herald/server/src/routes.ts:124-135), which stops a proof beingadded but cannot observe one being removed.