Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
05d1a1c
Add implementation plan for DataIntegrity Proof support (Ticket #64)
Sep 28, 2026
a6002d0
Address review feedback on PR #12
Sep 28, 2026
48783ed
Merge pull request 'Ticket #64: Implementation Plan' (#12) from ticke…
Sep 28, 2026
e12d21f
Extract multibase/multicodec key decoding into shared did/multikey.go
Sep 28, 2026
2e51c2a
Address review feedback on PR #13
Sep 28, 2026
2d80231
Merge pull request 'Ticket #64 - Step 1: DataIntegrity Proof in VCVer…
Sep 28, 2026
dd3dc66
Decode publicKeyMultibase into JWKs in DID document parser
Sep 28, 2026
c6c880a
Address review feedback on PR #14
Sep 28, 2026
76437ad
Address review feedback on PR #14
Sep 28, 2026
74217a8
Merge pull request 'Ticket #64 - Step 2: DataIntegrity Proof in VCVer…
Sep 28, 2026
2fcf5a7
Implement Data Integrity proof verification core (ecdsa-rdfc-2019, ed…
Sep 28, 2026
bb3d056
Address review feedback on PR #15
Sep 28, 2026
03b271f
Merge pull request 'Ticket #64 - Step 3: Implement Data Integrity pro…
Sep 28, 2026
70c0f94
Integrate Data Integrity verification into the LD proof dispatch chain
Sep 28, 2026
67152c8
Address review feedback on PR #16
Sep 28, 2026
84a8b6e
Merge pull request 'Ticket #64 - Step 4: DataIntegrity Proof in VCVer…
Sep 28, 2026
bd28e14
Add end-to-end tests and documentation for Data Integrity proofs
Sep 28, 2026
5026558
Address review feedback on PR #17
Sep 28, 2026
7bdce9e
Merge pull request 'Ticket #64 - Step 5: End-to-end tests and documen…
Sep 28, 2026
093f6c9
cleanup
wistefan Sep 28, 2026
51ef2e0
Correct the Data Integrity section of the JSON-LD proof docs
wistefan Sep 28, 2026
4dfa942
Update CLAUDE.md for Data Integrity proof verification
wistefan Sep 28, 2026
190fb56
Verify the published W3C Data Integrity test vectors
wistefan Sep 28, 2026
c4075d5
Cover P-384 Data Integrity proofs above the common package
wistefan Sep 28, 2026
e595fdb
Add release notes for Data Integrity proof support
wistefan Sep 28, 2026
e5fe87e
Verify a proof against the proof it actually carries
wistefan Sep 28, 2026
e3dd328
Accept a Data Integrity proof without a created timestamp
wistefan Sep 28, 2026
13c0a92
Reject a Linked Data Proof past its expires timestamp
wistefan Sep 28, 2026
ac9eed1
Pin proofValue to base58-btc and tighten two rough edges
wistefan Sep 28, 2026
7250791
Describe the shipped behaviour in the Data Integrity release notes
wistefan Sep 28, 2026
2601809
Verify the JCS Data Integrity cryptosuites
wistefan Sep 28, 2026
122f4c9
Fix the staticcheck findings
wistefan Sep 28, 2026
8dc90c9
Bind the document to the proof's @context on the JCS suites
wistefan Sep 29, 2026
c5df494
Assert that expires is covered by the signature
wistefan Sep 29, 2026
943461e
Log unsupported key codecs at Debug
wistefan Sep 29, 2026
d9c9a1f
Drop the redundant RuneError branch and correct its comment
wistefan Sep 29, 2026
6d39386
List all four cryptosuites in the docs
wistefan Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,15 @@ Key config sections: `server` (port, timeouts, template/static dirs), `logging`,

JSON-LD (`ldp_vc`) presentations and credentials are cryptographically verified — see `docs/json-ld-proof-verification.md` for the full design. In short:

- `common/ldproof.go` implements `JsonWebSignature2020` signing and verification (URDNA2015 canonicalization, detached JWS with `b64=false`).
- Proof options are canonicalized under the document context **plus** `https://w3id.org/security/suites/jws-2020/v1`, so `created`, `verificationMethod`, `proofPurpose`, `challenge` and `domain` are covered by the signature. `assertProofOptionsCovered` fails closed if any of them does not survive canonicalization; it compares parsed N-Quads predicates (`common/nquads.go`), not raw text, so an IRI inside a literal cannot fake coverage.
- `common/ldproof.go` implements `JsonWebSignature2020` signing and verification (URDNA2015 canonicalization, detached JWS with `b64=false`) and `DataIntegrityProof` verification for `ecdsa-rdfc-2019` / `ecdsa-jcs-2019` (P-256, P-384) and `eddsa-rdfc-2022` / `eddsa-jcs-2022` (Ed25519). `selectProofVerifier` (`verifier/ld_proof_checker.go`) dispatches on `proof.type` and **rejects** anything else with `ErrorLDProofUnsupportedType`; it never falls back to the JWS verifier.
- A cryptosuite identifier selects a canonicalization **and** a signature algorithm, modelled as such in `dataIntegritySuites`: the `-rdfc-` suites canonicalize with URDNA2015, the `-jcs-` suites with JCS (RFC 8785, `common/jcs.go`). A JCS proof configuration is the proof verbatim, **including the `@context` the proof itself carries** (VC-DI-ECDSA §3.3.5), and needs no coverage assertion — JCS expands nothing, so nothing can drop out. That `@context` also governs the document (VC-DI-ECDSA §3.3.2 / VC-DI-EDDSA §3.3.2 step 4): the presented `@context` must **start with** every entry of the proof's, in order (`assertContextPrefix`, else `ErrorLDProofContextMismatch`), and the document is canonicalized under the proof's. A context extended after issuance therefore still verifies, while a reordered or replaced entry does not — so the base entry `DetectVCDataModelVersion` reads cannot be swapped out. A proof with no `@context` leaves the document's own in place.
- Both families sign `hash(canonical proof options) || hash(canonical document)`. The hash is curve-conditional: SHA-384 for P-384 with either ECDSA suite (`shouldUseSHA384` branches on the signature algorithm, not the suite name), SHA-256 otherwise. ECDSA hashes that concatenation once more (the signature is over `hashData`, IEEE P1363 `r||s`); Ed25519 signs it directly. The implementation verifies the published W3C test vectors (`common/data_integrity_vectors_test.go`).
- `did/multikey.go` decodes `publicKeyMultibase` (multibase + multicodec) into JWKs for both `did:key` and DID-document `Multikey` verification methods. Without it a Data Integrity credential fails at key resolution with `ErrorNoVerificationKey` before any signature is checked, since a `Multikey` verification method carries no `publicKeyJwk`.
- The proof options that are canonicalized are the proof minus its `jws`/`proofValue` member (`buildVerificationProofOptions`), so a member `LDProof` does not model — `expires`, `nonce`, `id` — is covered too; rebuilding them from a fixed field set would reject a conformant proof. The `@context` is the document's own, extended with `https://w3id.org/security/suites/jws-2020/v1` for `JsonWebSignature2020` and verbatim for `DataIntegrityProof` (VC-DI-ECDSA §3.2.5). So `created`, `expires`, `verificationMethod`, `proofPurpose`, `challenge`, `domain` and `cryptosuite` are covered by the signature, which is what makes a cryptosuite substitution impossible. `assertProofOptionsCovered` fails closed if any of them does not survive canonicalization; it compares parsed N-Quads predicates (`common/nquads.go`), not raw text, so an IRI inside a literal cannot fake coverage.
- `verifier/ld_proof_checker.go` binds the proof key to the credential's `issuer` / the presentation's `holder`, requires the matching proof purpose, and requires the key to be authorized for the corresponding verification relationship.
- `VerifyLDVPProofBinding` requires one and the same proof to carry every expected binding (challenge + domain); a split across two proofs is rejected.
- `VerifyLDVPProofFreshness` bounds `proof.created` by `verifier.ldProofMaxAge` (default 300s) on the `vp_token` and token-exchange grants, which have no server-issued nonce. Missing, unparseable and future-dated timestamps are rejected.
- `LDProofChecker.assertProofNotExpired` rejects a proof past its `expires` (credentials and presentations alike, `ldProofClockSkew` tolerated, unparseable values rejected). The timestamp is in `assertProofOptionsCovered`, so enforcing it is sound: a context that drops the term fails closed rather than leaving an unsigned expiry to be enforced.
- `VerifyLDVPProofFreshness` bounds `proof.created` by `verifier.ldProofMaxAge` (default 300s) on the `vp_token` and token-exchange grants, which have no server-issued nonce. Missing, unparseable and future-dated timestamps are rejected. `created` itself is **optional** on a `DataIntegrityProof` (VC-DATA-INTEGRITY §2.1) and only has to parse when present (`ErrorLDProofMalformedCreated`); `JsonWebSignature2020` still requires it. A presentation that omits it therefore fails the freshness check, not the signature check.
- `verifyJSONLDHolderBinding` requires an identified `credentialSubject` to be the presentation's `holder` — the JSON-LD counterpart of the JWT `cnf` binding.
- Status lists (W3C and IETF) are bound to the issuer of the referencing credential; a credential with no issuer is rejected (`ErrorStatusListIssuerUnknown`).
- The security-relevant contexts are vendored in `common/contexts/` and served by `common.NewEmbeddedContextLoader`, so verification never depends on the network.
Expand Down Expand Up @@ -147,7 +151,7 @@ Credential issuers may be identified by an HTTPS URL instead of a DID — see `d
- **An HTTPS issuer inside the verifier's own network** is unresolvable until `verifier.httpsIssuerAllowPrivateNetworks` is set — the address guard refuses non-routable targets for every issuer, not per issuer.
- **`validationMode: combined` and `jsonLd`** do not perform real JSON-LD validation — they only check that issuer and type fields are present. They are deprecated but still accepted.
- **Verification relationships are only enforced when the DID document declares them.** A `did:web` document that lists `verificationMethod` but neither `authentication` nor `assertionMethod` falls back to the flat method list with a warning.
- **Data Integrity suites other than `JsonWebSignature2020`** (`proofValue`-based cryptosuites) are parsed but not verified. VC 2.0 issuers are more likely to use these than `JsonWebSignature2020`.
- **Data Integrity selective-disclosure suites** (`bbs-2023`, `ecdsa-sd-2023`), **proof sets and chains** (`previousProof`) and **Data Integrity on VCDM 1.1** are not supported.
- **VCDM 2.0's `confirmationMethod` is not implemented.** Holder binding on every JWT path, `vp+jwt` included, uses RFC 7800 `cnf`, which VC-JOSE-COSE §4.1.3 registers for exactly that. `confirmationMethod` is a *reserved* property in VCDM 2.0 with no defined semantics, so there is nothing to implement against yet; the choice is recorded in `docs/vc-jose-cose.md`.
- **A `vp+jwt` may carry bare JWT strings in `verifiableCredential`.** VCDM 2.0 §4.13 expects credentials in a 2.0 presentation to be `EnvelopedVerifiableCredential` objects. Accepting bare strings is a deliberate leniency, and it is what lets a v1.1 `jwt_vc` ride inside a 2.0 presentation.
- **COSE (`vc+cose`) is not supported.** Only the JOSE half of VC-JOSE-COSE is implemented.
Expand Down
125 changes: 125 additions & 0 deletions RELEASE_NOTES_DATA_INTEGRITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Release Notes — W3C Data Integrity Proof Support

This release adds verification of [W3C Data Integrity](https://www.w3.org/TR/vc-data-integrity/)
proofs (`DataIntegrityProof`) on JSON-LD credentials and presentations, for all four
non-selective-disclosure cryptosuites:

| Cryptosuite | Canonicalization | Specification | Keys |
| --- | --- | --- | --- |
| `ecdsa-rdfc-2019` | RDFC-1.0 | [VC-DI-ECDSA](https://www.w3.org/TR/vc-di-ecdsa/) | EC P-256, P-384 |
| `ecdsa-jcs-2019` | JCS (RFC 8785) | [VC-DI-ECDSA](https://www.w3.org/TR/vc-di-ecdsa/) | EC P-256, P-384 |
| `eddsa-rdfc-2022` | RDFC-1.0 | [VC-DI-EDDSA](https://www.w3.org/TR/vc-di-eddsa/) | Ed25519 |
| `eddsa-jcs-2022` | JCS (RFC 8785) | [VC-DI-EDDSA](https://www.w3.org/TR/vc-di-eddsa/) | Ed25519 |

Until now `JsonWebSignature2020` was the only suite VCVerifier verified. `DataIntegrityProof` is
the securing mechanism VCDM 2.0 defines for JSON-LD, so an issuer following the current W3C
recommendations is more likely to emit one of the suites above than `JsonWebSignature2020` — the
VC 2.0 support shipped previously did not reach the credentials VC 2.0 issuers actually produce.

See [docs/json-ld-proof-verification.md](docs/json-ld-proof-verification.md) for the full design,
including the limitations below.

## Not a security fix

The previous behaviour was already fail-closed. `ParseLDProof` accepted a `proofValue`-based
proof, but `common.VerifyLinkedDataProof` rejected every proof type other than
`JsonWebSignature2020`, so a `DataIntegrityProof` credential was **rejected, not accepted
unverified**. This release closes an interoperability gap, not a hole.

## New Features

### Data Integrity proof verification

`common.VerifyDataIntegrityProof` canonicalizes the document and the proof options with
URDNA2015, computes `hash(canonical proof options) || hash(canonical document)` and verifies the
multibase-encoded raw signature carried in `proofValue`.

The hash is curve-conditional, as the specifications require: SHA-384 for P-384 with the ECDSA
suites, SHA-256 for P-256 and Ed25519. ECDSA signatures are IEEE P1363 (`r || s`),
Ed25519 signatures are the raw 64 bytes.

The implementation is verified against the **published W3C test vectors** for all six
suite/curve combinations (`common/data_integrity_vectors_test.go`), not only against fixtures
this codebase signs itself.

### JCS canonicalization

`common/jcs.go` implements the JSON Canonicalization Scheme (RFC 8785) that the `-jcs-` suites
use in place of RDF canonicalization: ECMAScript number serialization, the RFC's string escaping,
and property names sorted by UTF-16 code units. It is checked against the RFC's own test data,
including the appendix B number samples and the sorting vector where UTF-8 and UTF-16 order
disagree.

A JCS proof configuration is the proof verbatim, including the `@context` the proof itself
carries — a conforming issuer copies the document's context into the proof before signing
(VC-DI-ECDSA §3.3.5). Rewriting it in a signed document breaks the signature, as does swapping
a cryptosuite for the other canonicalization's variant of the same algorithm.

### `Multikey` verification methods

A Data Integrity verification method normally carries `publicKeyMultibase` rather than
`publicKeyJwk`. The DID document parser previously stored that string without decoding it, so
`JSONWebKey()` returned nil and key resolution failed with `ErrorNoVerificationKey` before any
signature was checked.

`did/multikey.go` now decodes multibase + multicodec public keys (`0xed` Ed25519, `0x1200` P-256,
`0x1201` P-384) for both `did:key` resolution and DID document verification methods. A
verification method whose `publicKeyMultibase` cannot be decoded is skipped rather than aborting
the parse of the whole document.

`secp256k1` (`0xe7`) is recognized and rejected with an explicit error — the curve is not
available in Go's standard library.

### Proof type dispatch

`selectProofVerifier` routes by `proof.type`: `JsonWebSignature2020` to
`common.VerifyLinkedDataProof`, `DataIntegrityProof` to `common.VerifyDataIntegrityProof`, and
**anything else to a rejection** (`ErrorLDProofUnsupportedType`). The dispatch is exhaustive; an
unrecognized proof type is never reinterpreted as a supported one.

Every other check applies to Data Integrity proofs unchanged, because the dispatch sits below
them: issuer/holder binding of the verification method, the required proof purpose
(`assertionMethod` for credentials, `authentication` for presentations), verification
relationship enforcement, `challenge`/`domain` binding, proof freshness and holder binding. The
`cryptosuite` is part of the signed proof options and is asserted to be covered by the signature,
so one suite's signature cannot be replayed as another's.

Both proof families can appear in the same presentation: a `JsonWebSignature2020` presentation may
carry `DataIntegrityProof` credentials and vice versa.

## Configuration

None. No new configuration keys; Data Integrity proofs are verified wherever
`JsonWebSignature2020` proofs already were.

## Scope and Limitations

Out of scope for this release:

- **The selective-disclosure suites** (`bbs-2023`, `ecdsa-sd-2023`), which involve derived proofs.
- **Proof sets and proof chains** (`previousProof`).
- **Data Integrity on VCDM 1.1 documents**, which would additionally require vendoring the
`https://w3id.org/security/data-integrity/v2` context.

### Proof members beyond the modelled ones

The proof configuration that is canonicalized and hashed is the proof itself, minus its
`proofValue`/`jws` member, under the document's own `@context` — as VC-DI-ECDSA §3.2.5
specifies, rather than a document rebuilt from the fields VCVerifier models. A proof carrying
`expires`, `nonce`, `id` or a vendor extension therefore verifies, and those members are covered
by the signature: rewriting one in a captured document invalidates the proof.

`expires` is also enforced. A proof past it is rejected (`ld_proof_expired`), with the same clock
skew the freshness check tolerates, on credentials and presentations alike.

### Optional `created`

`created` is optional on a `DataIntegrityProof`, per VC-DATA-INTEGRITY §2.1, and only has to be a
valid RFC 3339 date-time when present. Presentations still need one to pass the freshness check
on the grants that have no server-issued nonce, and `JsonWebSignature2020` still requires it
outright.

### Encoding

`proofValue` must be base58-btc, as both cryptosuites require. A signature in another multibase
alphabet is rejected even when the bytes it carries would verify.
Loading
Loading