Working Draft 02: digest encoding, VAC parent digests, and the VDC - #20
Merged
Conversation
Three of these are semver-major. `sha2` 0.10 -> 0.11 is the one worth noting: `affinidi-data-integrity` already pulls `sha2` 0.11 through `affinidi-crypto`, so the library was linking two copies of it and hashing with the older one. The library graph now carries a single `sha2`. Digest output is unchanged, which the tests pinning known digests confirm. The rest are dev-dependencies, so they affect the examples rather than the crate: `affinidi-tdk` 0.10 -> 0.12, `chacha20poly1305` 0.10 -> 0.11, and `rand` 0.8 -> 0.10. The `data_room` example moves to the `rand` 0.10 API (`rand::rng()`, `rand::Rng`) and off the now-deprecated `Key::from_slice` / `Nonce::from_slice`. Everything else moves within its existing range via `cargo update`. Only `generic-array` stays behind latest, held there by a transitive constraint. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Both drafts 0.6.0 tracked have merged upstream - the VAC as PR #29 and the VDC as PR #19 - and the digest encoding changed underneath them. Breaking on the wire as well as in the API. The digest encoding. Working Draft 02 replaced `sha256:<lowercase hex>` with a base58btc multibase multihash and renamed the property from `digest` to `digestMultibase`. `digest_multibase()` becomes the conformant digest and now excludes the top-level `proof`, which is what it was deprecated for; `digest()` and `digest_json()` are deprecated but emit the old form unchanged, so a caller migrating can recompute a stored digest to compare. The old property name is still accepted when parsing, so credentials issued against WD01 deserialize - their values then fail with `InvalidDigest` rather than as a silent mismatch. Digests are compared as decoded bytes, never strings. The specification requires it because one digest has more than one spelling, and a string comparison would report a mismatch where the two credentials agree. An unimplemented hash algorithm is rejected rather than treated as a mismatch: a governing party may require a stronger hash, and conflating the two would silently downgrade that choice into a failed comparison. `authority.parent` is now a digest rather than an `id`. A digest names nothing that can be fetched, so verification cannot come to depend on network availability, a verifier cannot be induced to request an address of the holder's choosing, and nobody hosting an identifier learns when a credential is used. It also binds a link to the exact claims its issuer narrowed from: re-issuing a parent orphans its children, while re-proofing leaves them alone. Adds `attenuate_from_json` for a parent that arrived from a counterparty, and an `AuthorityError::Digest` kept distinct from `BrokenLink` - a malformed chain and a widening one are different findings. The VDC, which was previously a type string over a bare subject. Now carries a `delegation` object, forms a grant/acceptance edge, and has chain verification. The acceptance is required: a grant alone establishes what the delegator appointed, not what the delegate agreed to, and a delegator cannot produce the countersignature. Re-delegation is opt-in, the opposite default from attenuation, because a delegate speaks in the principal's name and the principal keeps the register of who may do so. `delegation::verify_chain` returns what the chain appoints for and deliberately not whether the act is permitted - a VDC moves the permission question, it does not answer it. `validUntil` becomes REQUIRED on a VAC and a VDC. For a VAC the reasoning is sharper: nothing about the subject's current standing is consulted at verification, so authority that does not expire is authority nobody can withdraw by waiting. `credentialStatus` is modelled and `DTGCommon::extra` preserves unmodelled top-level members, both because a parse-then-re-serialise used to drop them and silently change a credential's digest. That narrows rather than closes the hazard - a timestamp is still normalized on the way out - so the wire-form constructors remain the safe habit, with a test pinning exactly that. Status is modelled but not resolved; no revocation checking happens anywhere in this crate. Fixes one real bug: `new_member_vmc` probed `credentialSubject` for `digest` to tell a grant from an acknowledgement, and after the rename would have accepted an acknowledgement as a grant. Deliberately not implemented: the three VAC changes in flight upstream - revocation (PR #39), a `maxAttenuation` ceiling (PR #40), and key control at invocation, which removes `audience` (PR #41). `audience` stays until that lands rather than removing a shipped field twice. Correlation scope (PR #30) retired the R-DID/M-DID/C-DID/P-DID types, but the specification has not yet named the property carrying the declaration, so only the retired names leave the docs. 115 tests, up from 73. `tests/delegation_chain.rs` is new and, like `authority_chain.rs`, is mostly attacks: what makes either credential safe is a verifier refusing a chain that widens. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review🔎 A manual security review is recommended before merging. Please contact the Security team for specifics and remediation guidance.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the library up to Working Draft 02 of DTG Core Credentials. Both drafts 0.6.0 tracked have merged upstream — the VAC as PR #29 and the VDC as PR #19 — and the digest encoding changed underneath them.
Breaking on the wire as well as in the API. Version bumped to 0.7.0 and the changelog cut with today's date, so this is ready to tag on merge.
Two commits, reviewable in order:
chore(deps): update to current releasesfeat!: bring the library up to Working Draft 02The digest encoding
WD02 replaced
sha256:<lowercase hex>with a base58btc multibase multihash, and renamed the property fromdigesttodigestMultibase. Every cross-credential reference now uses that one encoding: the member-issued VMC's digest of the grant it acknowledges, the VWC's of the edge credential it attests, a VAC'sauthority.parent, and a VDC'sdelegation.parent/delegation.accepts.digest_multibase()is the conformant digest and now excludes the top-levelproof— which is what it was deprecated for. Un-deprecated.digest_multibase_json()is its wire-form counterpart.digest()/digest_json()are deprecated but emit the old form unchanged, so a caller migrating can recompute a stored digest to compare against.digestis still accepted when parsing, so WD01 credentials deserialize. Their values then fail withInvalidDigestrather than as a silent mismatch — the intended outcome.UnsupportedDigestAlgorithm), not treated as a mismatch. A governing party may require a stronger hash; conflating the two would silently downgrade that choice into a failed comparison.authority.parentis a digest, not anidThe change with the most reasoning behind it upstream. A digest names nothing that can be fetched — so verification never depends on network availability, a verifier cannot be induced to make a request against an address of the holder's choosing, and nobody hosting an identifier learns when a credential is used. It also binds a link to the exact claims its issuer narrowed from: re-issuing a parent orphans its children (each must be re-derived, which for a chain of narrowing authority is intended), while re-proofing leaves them undisturbed.
AttenuationParentHasNoIdis deprecated and never returned — not needing a top-level identifier merely to be referenced is precisely what the change was for. Addsattenuate_from_json(), and anAuthorityError::Digestkept distinct fromBrokenLink, since a malformed chain and a widening one are different findings.The VDC
Previously a
DelegationCredentialtype string over a bare subject: nodelegationobject, no edge, no chain verification. Now all three.DelegationGrant—scope,parent,maxDepth,accepts.new_delegate_vdc()andaccepts()— the grant/acceptance edge. The acceptance is required: a grant alone establishes what the delegator appointed, not what the delegate agreed to, and a delegator cannot produce the countersignature. Same consent rule as a membership edge.redelegate()— opt-in, the opposite default from a VAC's attenuation. A delegate speaks in the principal's name, so the principal keeps the register of who may do so.delegation::verify_chain— returns what the chain appoints for and deliberately not whether the act is permitted. A VDC moves the permission question; it does not answer it. Whether the principal may perform the act is the caller's check, and the reach of a delegated act is the intersection of the two.Also
validUntilis REQUIRED on a VAC and a VDC. For a VAC the reasoning is sharper: nothing about the subject's standing is consulted at verification, so authority that never expires is authority nobody can withdraw by waiting.credentialStatusis modelled, andDTGCommon::extrapreserves unmodelled top-level members. Both because a parse-then-re-serialise used to drop them and silently change a credential's digest. This narrows rather than closes the hazard — a timestamp is still normalized on the way out — so the wire-form constructors stay the safe habit, with a test pinning exactly that. Status is modelled but not resolved; no revocation checking happens anywhere in this crate.One real bug
new_member_vmcprobedcredentialSubjectfordigestto tell a grant from an acknowledgement. After the rename it would have accepted an acknowledgement as a grant — and acknowledging one forms no edge.Deliberately not implemented
Three VAC changes are in flight upstream and are not here:
credentialStatus, cascading to everything attenuated belowmaxAttenuationceiling, bounding depth per-ancestor rather than only globallyaudienceas redundantaudienceis kept until #41 lands rather than removing a shipped field twice. Neither chain verifier establishes that the party presenting a chain controls the leaf's subject — both credentials are non-bearer, and that demonstration belongs to the trust task in which they are exercised.Correlation scope (#30) retired the R-DID / M-DID / C-DID / P-DID types for a holder-declared
pairwise|directed|public. Nothing to implement yet — the spec has not named the property that carries the declaration — so this only drops the retired names from the docs.Dependencies
sha20.10 → 0.11 is the one worth noting:affinidi-data-integrityalready pulledsha20.11 throughaffinidi-crypto, so the library was linking two copies and hashing with the older one. The library graph now carries a singlesha2, and digest output is unchanged. Dev-only:affinidi-tdk0.10 → 0.12,chacha20poly13050.10 → 0.11,rand0.8 → 0.10 (the example moves torand::rng()/rand::Rng).Verification
115 tests pass, up from 73.
tests/delegation_chain.rsis new at 28 cases andauthority_chain.rsgained 8 for the digest-parent change; like the existing suite they are mostly attacks, since what makes either credential safe is a verifier refusing a chain that widens.Clippy clean at zero warnings,
cargo fmtclean, rustdoc clean,--no-default-featuresbuilds,cargo packagesucceeds.The
data_roomexample now runs both credentials side by side — the agent acting as itself under a VAC, and a scheduler acting in Bob's name under a VDC, with the VDC explicitly refused when offered as authority. That contrast is the whole reason they are separate credentials: a verifier can always tell which it was shown.