Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dotnet run --project examples/ZcapLd.Examples # Run con
- **Spec-exact `capabilityChain` (#50)**: root referenced by id only (never embedded); first-level chain is exactly `[rootId]`; deeper chains are `[rootId, …ancestorIds, {immediateParent}]`. The verifier rejects non-spec shapes (embedded root, duplicate ids, parent both id+embedded, wrong/missing embedded parent)
- **Root resolution (#50)**: spec-exact chains reference the root by id, so the verifier obtains it via an explicit-root verify/revoke overload, else an `IRootCapabilityResolver` (`InMemoryRootCapabilityResolver` for dev; an `IDidResolver` that also implements it is auto-detected), else fails closed. **Breaking (3.0.0)**: wire format no longer embeds the root, so pre-3.0.0 capabilities must be re-delegated
- **Invocation `capability` shape (#51)**: `InvocationCapability` (union of root-id **string** | full embedded delegated **zcap object**) backs both `Invocation.Capability` and the invocation `Proof.Capability`, with `InvocationCapabilityJsonConverter` preserving the wire shape. Per ZCAP-LD v0.3 a delegated DI invocation MUST embed the full delegated zcap (`InvocationCapability.FromCapability(delegated)`); a root invocation uses the id string (implicit `string` conversion). Verification is **strict**: a delegated invocation supplying only an id string is rejected. Revocation requests still reference the capability by id string. **Breaking**: `Invocation.Capability` is no longer `string`; `Proof.Capability` is no longer `object?`
- **DI invocation interop — Path A (#117)**: in addition to the self-contained `Invocation` envelope (above; kept for in-stack use + revocation), there is an **additive** `@digitalbazaar/zcap`-compatible mode. `SigningService.SignCapabilityInvocationAsync(capability, action, target, signerDid, appDoc?)` signs an **application document** (default carries `@context=[zcap/v1, ed25519-2020/v1]` + an `id` + an absolute-IRI `type` — both required so the doc expands to a non-empty RDF dataset under JSON-LD safe mode) with a `capabilityInvocation` proof whose proof object ALONE holds `capability`/`capabilityAction`/`invocationTarget` (no envelope, no `capabilityChain`). Signed input = `SHA-256(RDFC(proofOptions)) || SHA-256(RDFC(document))` via `LegacyProofCrypto` (proof-options `@context` spliced from the document by DataProofs). `VerificationService.VerifyCapabilityInvocationAsync(JsonObject securedDocument, string expectedAction, IReadOnlyCollection<string> expectedTargets[, root])` (+ `…DetailedAsync` with optional `expectedRootCapabilityIds`) verifies the signature over the application doc, then reuses the chain/attenuation/caveat/controller/freshness/replay checks. **Two security gates an adversarial review added (both required for soundness):** (1) the verifier validates the **application document `@context`** (array, `[0]==zcap/v1`, includes the suite context) BEFORE trusting the signature — otherwise RDFC drops the proof's invocation terms (`capability`/`capabilityAction`/`invocationTarget`, which are zcap/v1 terms) from the signed N-Quads and an attacker could rewrite them (forgery); (2) the relying party MUST supply `expectedAction`/`expectedTargets` (and may pin `expectedRootCapabilityIds`) and the verifier fails closed unless the proof matches — mirrors `@digitalbazaar/zcap`'s required `expectedAction`/`expectedTarget`/`expectedRootCapability` and prevents a confused-deputy replay of a valid invocation over a different capability. There is **no no-expectation overload** (it was the footgun). Proven live both directions (root + delegated) by `interop/run-interop.sh` checks 7–12. HTTP-Signature invocations (ezcap, "Path B") are deferred (#119).
- **Revocation**: `IRevocationService` / `IRevocationStore` abstractions; `InMemoryRevocationStore` for dev; ASP.NET endpoints via `ZcapLd.AspNetCore`
- **Replay protection**: `INonceStore` interface; `InMemoryNonceStore` (default), `NullNonceStore` (opt-out)
- **Proof `created` freshness (#71)**: both the invocation (`VerifyInvocationCoreAsync`) and signed-revocation (`RevokeCapabilityInternalAsync`) paths reject a proof whose signed `created` is missing/unparseable, future-dated beyond a configurable clock skew (`_freshnessClockSkew`, default `DefaultFreshnessClockSkew` = 1 min, set via the `freshnessClockSkew` ctor param), or older than `_nonceWindow` — via the shared `GetFreshProofCreatedUtc` helper (staleness bound reuses the nonce window, so anything still evictable is already too stale; widening `nonceWindow` widens both replay window and acceptable signing age). Bounds replay independently of nonce-store eviction (and even under `NullNonceStore`). The invocation path feeds the validated `created` into `InvocationContext.InvocationTime` (caveats evaluate against signed time). Detailed reason: `VerificationOutcome.StaleProof`. Test seam: determinism overloads `SignInvocationAsync(.., createdOverride)` / `SignRevocationAsync(.., createdOverride)` on the concrete `SigningService` (not on `ISigningService`). **Delegation-proof** `created` freshness is a separate concern (durable proofs, no staleness bound) — see #99 below
Expand Down
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ Delegates zcap's cryptography and canonicalization to the composable foundation
makes the wire format interoperable with the `@digitalbazaar/zcap` reference implementation.
Includes everything in the (unreleased) 3.0.0 section below.

### Added

- **`@digitalbazaar/zcap`-compatible Data Integrity invocations ("Path A", #117).** New
`SigningService.SignCapabilityInvocationAsync(...)` produces a secured application document with a
`capabilityInvocation` proof whose proof object alone carries `capability`/`capabilityAction`/
`invocationTarget` (no self-contained envelope), and
`VerificationService.VerifyCapabilityInvocationAsync(securedDocument, expectedAction, expectedTargets[, …])`
verifies one (signature over the application document, then chain/attenuation/caveats/controller/
freshness/replay). The verifier has **two security gates** (added after an adversarial red-team of the
initial implementation): (a) the relying party MUST declare `expectedAction` + `expectedTargets` (and
may pin `expectedRootCapabilityIds`) — the verifier fails closed unless the proof matches, mirroring
`@digitalbazaar/zcap`'s required `expectedAction`/`expectedTarget`/`expectedRootCapability` and
preventing a confused-deputy replay of an invocation signed over a different capability; (b) the
application document's `@context` is validated (array, `[0]==zcap/v1`, includes the suite context)
before the signature is trusted, so the invocation terms are actually inside the signed RDFC N-Quads
(without it, stripping `zcap/v1` lets an attacker rewrite them — a forgery). Round-trips live against
the real `@digitalbazaar/zcap` `CapabilityInvocation` purpose — root **and** delegated, both
directions — in `interop/run-interop.sh` (checks 7–12). **Additive**: the self-contained `Invocation`
envelope (in-stack + revocation) is unchanged. HTTP-Signature invocations ("Path B") remain a follow-up (#119).

### Breaking Changes

- **Canonicalization is RDFC-1.0 only — JCS support removed.** RDFC-1.0 (W3C RDF Dataset Canonicalization) is now the sole canonicalization, which is what makes proofs verify under `@digitalbazaar/zcap` (proven live by `interop/run-interop.sh`). `SigningService` and `VerificationService` no longer accept a `canonicalizationMethod` argument; the `AddZcapRdfcCanonicalization()` DI toggle and the `JcsDocumentCanonicalizer` / `JsonCanonicalizer` types are removed. **No backwards compatibility:** JCS-signed capabilities issued by ≤3.x do not verify and must be re-signed under RDFC-1.0. (See the revocation-integrity entry below for one behavioral consequence.)
Expand All @@ -19,7 +39,7 @@ Includes everything in the (unreleased) 3.0.0 section below.
- **Root id encoding matches `encodeURIComponent` exactly.** The root capability id is `urn:zcap:root:{encodeURIComponent(invocationTarget)}`; the new `UriEncoding.EncodeUriComponent` un-escapes `! ' ( ) *` that `Uri.EscapeDataString` over-escapes, so for invocation targets containing those characters the id is now byte-identical to what `@digitalbazaar/zcap` derives (previously divergent). Plain http(s) targets were already identical.
- **Verify-path `@context` and root-invariant enforcement (W3C MUST closeouts).** The crypto verification paths now enforce spec invariants the create path already checked but `VerificationService` never called: (a) a **root** `@context` MUST be the single string `https://w3id.org/zcap/v1`; (b) a **delegated** `@context` MUST be an array whose first entry is `https://w3id.org/zcap/v1` and which includes the signing suite context; (c) a **root** MUST NOT carry `expires`/`allowedAction`/`caveat` (rejected unconditionally on verify, matching `@digitalbazaar/zcap`, which rejects root `expires`). **Unknown/unmodeled** root fields (`Capability.AdditionalProperties`) are rejected only under the new opt-in `VerificationPolicy.RejectUnknownRootFields` (default `false`): `@digitalbazaar/zcap`'s `checkCapability` *ignores* unknown root fields and `AdditionalProperties` (`[JsonExtensionData]`) exists to round-trip them, so rejecting them by default would be stricter than the reference impl. Conformant capabilities (including everything zcap-dotnet's create path emits) are unaffected; only malformed/non-conformant documents that previously slipped through the crypto verify path are now rejected with `MalformedCapability`.

- **Dependencies / source — crypto + canonicalization delegated to the shared stack.** `NetDid.Core` / `NetDid.Method.Key` move **1.3.1 → 2.0.1** (NetDid 2.0 relocated its cryptographic primitives to **NetCrypto**), `NetCrypto` **1.1.0** is added as a direct reference, the direct `dotNetRdf.Core` reference is replaced by **DataProofsDotnet.Core / .Rdfc / .Legacy 1.0.0** (which own dotNetRDF + the proof cryptosuites transitively), and **NetCid** rises to 1.6.0. Consumers **recompile**: the crypto types that lived under `NetDid.Core.Crypto` (`DefaultCryptoProvider`, `KeyType`, `EcdsaSignatureFormat`, `DefaultKeyGenerator`) are now `NetCrypto.*`. **The proof wire format is unchanged** (see below).
- **Dependencies / source — crypto + canonicalization delegated to the shared stack.** `NetDid.Core` / `NetDid.Method.Key` move **1.3.1 → 2.0.1** (NetDid 2.0 relocated its cryptographic primitives to **NetCrypto**), `NetCrypto` **1.1.0** is added as a direct reference, the direct `dotNetRdf.Core` reference is replaced by **DataProofsDotnet.Core / .Rdfc / .Legacy 1.0.1** (which own dotNetRDF + the proof cryptosuites transitively), and **NetCid** rises to 1.6.0. Consumers **recompile**: the crypto types that lived under `NetDid.Core.Crypto` (`DefaultCryptoProvider`, `KeyType`, `EcdsaSignatureFormat`, `DefaultKeyGenerator`) are now `NetCrypto.*`. **The proof wire format is unchanged** (see below).
- **API — the crypto-suite extension surface is removed.** `ICryptoSuite`, `ICryptoSuiteProvider`, `CryptoSuite`, `CryptoSuiteProvider`, `IDocumentCanonicalizerProvider`, `DocumentCanonicalizerProvider`, `SignatureVerifier`, and `AddZcapCryptoSuite<T>()` are all removed. zcap is no longer a crypto-extension point: it supports a fixed set of suites (`Ed25519Signature2020`, `EcdsaSecp256r1Signature2019`) via the internal `ZcapSuiteCatalog`, with sign/verify delegated to DataProofs' legacy cryptosuites. `SigningService` / `VerificationService` drop their suite-provider and canonicalizer-provider constructor parameters; canonicalization is RDFC-1.0 only (see the RDFC-only entry above — the earlier `canonicalizationMethod` / `AddZcapRdfcCanonicalization()` selection was itself removed). New curves are added in NetCrypto + DataProofs and wired into `ZcapSuiteCatalog`, never via a zcap API (see `docs/crypto-suite-extensibility-decision.md`). In-policy under SemVer (a major 4.0.0 bump).

#### From the security + compliance scan remediation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,4 @@ Capability **delegation** interoperates with `@digitalbazaar/zcap` (proven live

- **Use `Ed25519Signature2020`.** P-256 (`EcdsaSecp256r1Signature2019`) has no `@digitalbazaar/zcap` counterpart — it ships/tests Ed25519 only and serves no `ecdsa-2019/v1` context, so a P-256 capability will not verify there.
- **Custom caveats are not enforced cross-stack.** `@digitalbazaar/zcap` has no generic caveat engine, so a capability whose security depends on `UsageCountCaveat`/`ValidWhileTrueCaveat` verifies there with the caveat **silently un-enforced**. For cross-stack security guarantees, rely only on `expires`/`allowedAction`/`invocationTarget`.
- **Invocation** interop is not yet supported (tracked in the roadmap / issue #117) — delegation only, today.
- **Invocation** interop works via Data Integrity `capabilityInvocation` proofs (`SignCapabilityInvocationAsync` / `VerifyCapabilityInvocationAsync`), proven cross-stack for root + delegated. The verifier requires the relying party to declare the **expected action and target(s)** it authorizes (as the reference impl does) and validates the application document's `@context`, so a valid invocation can't be replayed against a different endpoint or have its terms rewritten. HTTP-Signature invocations (the `ezcap` transport) are not yet supported (tracked in #119).
4 changes: 2 additions & 2 deletions docs/ZCAP-LD-ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ All four are reachable from the invocation path via chain dereference and are ex
| **1** | **Verify-path `@context` value + order + suite-presence** (R-CTX-1/2 + NEW) | **S** | — | Unit tests: a hand-crafted root with `@context != zcap/v1` and a delegated with `@context[0] != zcap/v1` (or missing suite context) are rejected with `MalformedCapability`; all existing delegation interop cases still pass. |
| **2** | **Verify-path root extra-field rejection** (R-ROOT-NOEXTRA) | **S** | — (independent of #1) | A wire/resolver root carrying `expires`/`allowedAction`/`caveat`/extra `AdditionalProperties` is rejected on the crypto verify path; parity with CapabilityService.cs:262-271 proven by test. |
| **3** | **Doc-only divergence callouts** (P-256, custom caveats) | **S** | — | README.md + XML doc-comments carry the cross-stack warnings; `docs/ZCAP-LD-INTEROP-COMPATIBILITY-ANALYSIS.md` items 7-style notes marked done. |
| **4** | **Invocation DI envelope — sign + verify** (#117 / R-INV-ENVELOPE + R-INV-RDFC-CONTEXT) | **L** | #1 (reuses the `@context` value check on the proof) | zcap-dotnet emits a `{applicationDocument, proof}` DI invocation; `interop/js/invoke-verify.mjs` `jsigs.verify` returns `verified: true`; reverse (`jsigs.sign` → dotnet verify) passes; tamper rejects. |
| **5** | **Invocation interop wired into harness** | **M** | #4 | `interop/run-interop.sh` round-trips an invocation **both directions** (dotnet→db, db→dotnet) plus tamper-reject, alongside the existing delegation cases; CI green. |
| **4** | ✅ **DONE (#117)** — **Invocation DI envelope — sign + verify** | **L** | #1 | `SigningService.SignCapabilityInvocationAsync` emits a `{applicationDocument, proof}` DI invocation (metadata only in the proof; doc carries `@context=[zcap/v1, ed25519-2020/v1]` + `id` + absolute-IRI `type`); `VerificationService.VerifyCapabilityInvocationAsync` verifies one. Proven against the real `@digitalbazaar/zcap` `CapabilityInvocation` purpose, root + delegated, both directions, + tamper reject. |
| **5** | ✅ **DONE (#117)** — **Invocation interop wired into harness** | **M** | #4 | `interop/run-interop.sh` checks 7–12 round-trip a DI invocation both directions (root + delegated) + tamper-reject; all 12 checks green. |
| **6** | **HTTP-Signature invocation envelope** (`ZcapHttpSignature` builder/verifier — Path B) | **L** | #4 (shares the `CapabilityInvocation` validation semantics; needs new npm deps) | New harness case verifies dotnet-emitted `capability-invocation`+`authorization` headers via `@digitalbazaar/http-signature-zcap-verify`; root + delegated (gzip+base64url) + base64 signature + digest header round-trip. |

**Rationale:** #1-#3 are quick, independent spec/doc closeouts that can land immediately and raise compliance without touching the envelope. #4 is the keystone (the actual interop blocker) and depends on the proof-`@context` value check from #1. #5 proves #4 end-to-end. #6 delivers the dominant real-world transport but is a clean follow-up requiring no crypto-core changes.
Expand Down
31 changes: 17 additions & 14 deletions interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

This harness **proves at runtime** that zcap-dotnet's RDFC-1.0 path is wire-compatible
with the [`@digitalbazaar/zcap`](https://github.com/digitalbazaar/zcap) v9 reference
implementation (the spec authors' library) for **capability delegation proofs** — by
signing a real capability on one stack and verifying it on the other, in both
directions, including a tamper-detection negative control and a two-level chain.
implementation (the spec authors' library) for **capability delegation AND Data Integrity
invocation** — by signing a real capability/invocation on one stack and verifying it on the
other, in both directions, including tamper-detection negative controls and a two-level chain.

It is the empirical counterpart to [`docs/ZCAP-LD-INTEROP-COMPATIBILITY-ANALYSIS.md`](../docs/ZCAP-LD-INTEROP-COMPATIBILITY-ANALYSIS.md),
which inferred RDFC byte-compatibility from decompiled internals + golden-vector
Expand All @@ -21,20 +21,23 @@ hashes. This harness replaces inference with an actual `@digitalbazaar/zcap`
| 4 | **Negative (inbound)** — tampered `allowedAction` on a db zcap → dotnet rejects | FAIL |
| 5 | **Multi-level outbound** — dotnet 2-level chain `[rootId, {parent}]` → db verifies | PASS |
| 6 | **Multi-level inbound** — db 2-level chain → dotnet verifies | PASS |
| 7–10 | **Invocation** (DI `capabilityInvocation`) — root + delegated, both directions | PASS |
| 11–12 | **Invocation negatives** — tampered `capabilityAction`, both directions | FAIL |

## Scope

- **In scope:** capability **delegation** proofs (`proofPurpose: capabilityDelegation`),
Ed25519Signature2020 over RDFC-1.0, did:key controllers. This is the dominant
interop path (the canonicalization blocker, #1 in the analysis report).
- **Out of scope:** **invocations**. zcap-dotnet signs a self-contained
`{id, capability, capabilityAction, invocationTarget}` envelope as the document,
whereas digitalbazaar signs an arbitrary application payload with the invocation
metadata only in the proof — a structural mismatch (blocker #2) independent of
canonicalization. Not exercised here.
- The default zcap-dotnet canonicalization is **JCS**, which is *not* interoperable;
this harness explicitly uses **RDFC-1.0** on both the signer and verifier. The
takeaway: RDFC-1.0 is the interop mode.
- **Delegation** proofs (`proofPurpose: capabilityDelegation`), Ed25519Signature2020 over
RDFC-1.0, did:key controllers — the canonicalization path (#1 in the analysis report).
- **Invocation** via **Data Integrity `capabilityInvocation` proofs** ("Path A", #117): a
signed application document whose proof alone carries `capability`/`capabilityAction`/
`invocationTarget`. zcap-dotnet's `SignCapabilityInvocationAsync` / `VerifyCapabilityInvocationAsync`
round-trip against the real `@digitalbazaar/zcap` `CapabilityInvocation` purpose, root + delegated.
The verifier requires the relying party to supply `expectedAction` + `expectedTargets` (as the
reference impl does) and validates the application document's `@context` — both are security gates an
adversarial review added (confused-deputy + RDFC term-dropping forgery defenses). The legacy
self-contained `Invocation` envelope remains for in-stack use + revocation.
- **Not yet covered:** HTTP-Signature invocations (the deployed `ezcap` transport — Path B). The
whole stack is **RDFC-1.0 only** (JCS was removed in 4.0.0); RDFC-1.0 is the interop format.

## Run it

Expand Down
Loading
Loading