diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8614b..7a6c0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The "resource-bound authority" release: delegations can now name *where* a scope ### Added — specification (implementation follows in the same release) -- **`resource_path` constraint** (SPEC §5.7.2, §5.7.3): binds a delegation to an opaque `resource_id` (exact byte equality, never dereferenced or normalized) and an optional `path_prefix` under segment-boundary matching — deliberately a prefix, not a glob. Absolute logical POSIX-style path model with dot-segments, backslashes, and empty interior segments rejected outright; percent-encoding does not exist in the path model, and post-verification path transformation is forbidden. NFC pre-normalization is the issuer's obligation; the verifier compares bytes exactly (a mixed-form pair fails closed; byte identity, not visual identity, is the boundary). Chain evaluation is conjunctive: a downstream cert can only narrow the upstream bound, and jointly unsatisfiable constraint sets — different resources, or same-resource prefixes that don't nest — must be rejected at issuance (decoders still accept them; verification fails closed). +- **`resource_path` constraint** (SPEC §5.7.2, §5.7.3): binds a delegation to an opaque `resource_id` (exact byte equality, never dereferenced or normalized) and an optional `path_prefix` under segment-boundary matching — deliberately a prefix, not a glob. Absolute logical POSIX-style path model with dot-segments, backslashes, and empty interior segments rejected outright; percent-encoding does not exist in the path model, and post-verification path transformation is forbidden. NFC pre-normalization is the issuer's obligation; the verifier compares bytes exactly (a mixed-form pair fails closed; byte identity, not visual identity, is the boundary). Chain evaluation is conjunctive: effective authority can stay the same or narrow but never widen (a child may carry a broader prefix on the same resource and still verify, gaining nothing because every upstream constraint still applies), and jointly unsatisfiable constraint sets — different resources, or same-resource prefixes that don't nest — must be rejected at issuance (decoders still accept them; verification fails closed). - **Resource-identifier profiles** (SPEC §5.7.4, `docs/RESOURCE_PROFILES.md`): the shared recipes that make an opaque `resource_id` interoperable. Git profile v1 (repository identity — never a branch, commit, or checkout; renames and transfers fail closed) with known-answer and negative vectors. Profiles for platform-owned resources are authored by the platforms themselves and linked when published. - **Extension-constraint `params`** (SPEC §5.7.1, §17.7): parameterized extension constraints are now representable in signed certificates under a restricted, cross-language-deterministic value model. Type-only extension constraints serialize exactly as before; existing signed certs remain byte-stable. Closes the wire-format limitation documented in alpha.15. - **Input bounds** (SPEC §5.1): `MAX_PROOF_BUNDLE_BYTES` (128 KiB, enforced before parsing), `MAX_JSON_NESTING_DEPTH`, and per-cert scope/constraint count and length limits. Violations route to the existing `invalid` status. @@ -102,7 +102,7 @@ The "integration readiness" release: everything an integrator needs to adopt the ### Changed — release process: no more direct pushes to main - The single-step `make release` (which committed the version bump directly to main via a ruleset bypass) is removed. Releases are now two-phase: `make release-prepare VERSION=…` creates a `release/` branch, bumps versions, runs the full cross-SDK gate, and opens a PR; after it merges through the normal path (CI + DCO), `make release-tag VERSION=…` verifies main carries the bump and pushes the coordinated tags. See `docs/RELEASES.md` §4. -- `release.sh` pushes the protocol tag on its own before the `sdk-*` tags: GitHub creates no push event when more than three tags arrive in one push, which had silently prevented the tag-triggered Release workflow from ever firing (§5.3.1). +- `release.sh` pushes the protocol tag on its own before the `sdk-*` tags: GitHub creates no push event when more than three tags arrive in one push, which had silently prevented the tag-triggered Release workflow from ever firing (see `docs/RELEASES.md` §5.3.1). - `release-prepare` now stamps the `(unreleased)` changelog entry with the release date. --- @@ -247,7 +247,7 @@ Total alpha.7 test additions: **134 tests**, all green; **59/59 canonical fixtur ### Spec changes -- **§5.7.2 VerifyOptions** — table extended with `Revocation`, `Policy`, `Audit`, `ConstraintEvaluators`, `PolicyVerdict`, `PolicySecret`, and `AnchorResolver` fields, with precedence rules between the legacy `IsRevoked` closure and the new `Revocation` provider. +- **§5.17 VerifyOptions** — table extended with `Revocation`, `Policy`, `Audit`, `ConstraintEvaluators`, `PolicyVerdict`, `PolicySecret`, and `AnchorResolver` fields, with precedence rules between the legacy `IsRevoked` closure and the new `Revocation` provider. - **§17 (new section)** — Provider Interfaces, including: - §17.0 conformance and wire-format invariance - §17.1–§17.3 the three core providers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f645677..999eb9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ The flow: • Go tests • Test vectors regenerate byte-identical • Release metadata stays in sync - • TypeScript / Python / Rust SDK conformance (59 fixtures each) + • SDK conformance across Go, TypeScript, Python, Rust, and C (79 fixtures each) • DCO sign-off 6. Address review feedback. Every inline comment thread must be resolved. 7. If `main` moved while the PR was open: click "Update branch" — strict @@ -83,7 +83,7 @@ npm install npm test ``` -All 59 fixtures must pass in every SDK. If a change breaks the TS conformance but passes Go, or vice versa, the implementations have drifted — fix the divergence before the PR is merged. +All 79 fixtures must pass in every SDK (Go, TypeScript, Python, Rust, C). If a change breaks the TS conformance but passes Go, or vice versa, the implementations have drifted — fix the divergence before the PR is merged. ## Canonical serialization @@ -96,7 +96,7 @@ The canonicalizer in `crypto.go` (Go) and `sdks/typescript/src/canonical.ts` (TS ## Style - **Go:** standard `go fmt`, idiomatic Go. No framework-style abstraction layers. Prefer plain structs and functions. -- **TypeScript:** strict mode (already enforced by `tsconfig.json`). No runtime dependencies beyond `@noble/ed25519` and `@noble/hashes`. +- **TypeScript:** strict mode (already enforced by `tsconfig.json`). No runtime dependencies beyond `@noble/ed25519`, `@noble/hashes`, and `@noble/post-quantum` (ML-DSA-65). - **Documentation:** if a change affects protocol behavior, update `SPEC.md` and `docs/EXPLAINED.md` in the same PR. - **Commit messages:** conventional commit prefixes preferred (`fix:`, `feat:`, `spec:`, `docs:`). The first line under 72 characters. Body wrapped to 72. A new commit, never `--amend --force-push` to shared branches. diff --git a/README.md b/README.md index 42f23a6..3feaef5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Keep your IAM, OAuth, MCP, A2A, and policy engines — Ratify adds the missing p **Quantum-safe by design.** Every signature is hybrid: Ed25519 + ML-DSA-65 (NIST FIPS 204). Both must verify. Bundles signed today remain unforgeable even when cryptographically-relevant quantum computers exist. -JSON wire format. No blockchain. No tokens. No central issuer. Open spec under CC-BY-4.0. +JSON wire format. No blockchain. No bearer tokens. No central issuer. Open spec under CC-BY-4.0. **Status:** alpha — fixture bytes may change between pre-releases · reference implementation complete · 79 canonical test vectors · cross-language interop proven (Go + TypeScript + Python + Rust + C/C++) · Patent Pending. @@ -347,6 +347,7 @@ Everything above describes a single delegate → present → verify round trip. | **Transaction receipts** | A canonical `TransactionReceipt` where every party signs the same bytes (terms + sorted party set + transaction ID). Adding, removing, or altering any party invalidates every signature — no partial-valid state. | [§5.14](SPEC.md#514-transactionreceipt), [§6.4.7](SPEC.md#647-transactionreceiptsignable) | | **Witness append-only log** | Signed `WitnessEntry` hash chain for tamper-evident audit logs. Any party can operate a witness. | [§5.12](SPEC.md#512-witnessentry), [§6.4.6](SPEC.md#646-witnessentrysignable) | | **Key rotation statements** | `KeyRotationStatement` signed by both the old and new root keys, so auditors and registries can verify identity continuity across rotations. | [§5.15](SPEC.md#515-keyrotationstatement), [§6.4.4](SPEC.md#644-keyrotationsignable) | +| **Resource-bound authority** | The `resource_path` constraint scopes a delegation to one resource (an opaque `resource_id` compared byte-for-byte) and, optionally, to a `path_prefix` within it under segment-boundary matching, so a broad grant like `files:write` can be pinned to `/docs` of a single repository. Down a sub-delegation chain, effective authority can stay the same or narrow but never widen: a child may carry a broader prefix on the same resource and still verify, yet gains no authority because every upstream constraint still applies, so downstream escape is impossible by construction. This is the eighth canonical constraint type, alongside `geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, and `max_rate`. | [§5.7.3](SPEC.md#573-the-resource_path-constraint), [§5.16](SPEC.md#516-verifiercontext) | The [`docs/AGENT_TO_AGENT.md`](docs/AGENT_TO_AGENT.md) guide shows how these compose for agent-to-agent patterns (mutual authorization, sub-delegation, receipts), and [`docs/TRANSACTION_RECEIPTS.md`](docs/TRANSACTION_RECEIPTS.md) has the receipt envelope design rationale. @@ -408,7 +409,7 @@ ratify-protocol/ ├── types.go Data structures (DelegationCert, ProofBundle, …) ├── crypto.go Hybrid Ed25519 + ML-DSA-65 primitives + canonical JSON ├── scope.go Canonical 54-scope vocabulary + intersect/expand -├── constraints.go Geo, time, version constraints +├── constraints.go Geo, time, speed, amount, rate, and resource-path constraints ├── verify.go The verifier algorithm ├── streamed_verify.go SessionToken fast path — multi-turn verification (§5.13) ├── receipt_verify.go TransactionReceipt verification (§5.14) diff --git a/SECURITY.md b/SECURITY.md index d430490..122b324 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -43,7 +43,7 @@ Once v1.0.0 stable is released, the support policy will specify which major/mino - Vulnerabilities in third-party dependencies that are already known and patched upstream. Report to the dependency, then open a PR here to bump the version. - Operational issues at any Identities AI-hosted service (Registry, Verify API). Those are reported via `security@identities.ai` with the service name in the subject line — a separate handling path. -- Attacks against out-of-protocol concerns listed in `docs/EXPLAINED.md` §5.2 (endpoint malware, social engineering of delegators, etc.). These are real risks but are not protocol defects. +- Attacks against out-of-protocol concerns listed in `SPEC.md` §15.0 under "Out of scope" (private key theft at endpoints, social engineering of principals, agent-platform compromise, etc.). These are real risks but are not protocol defects. ## Responsible disclosure examples diff --git a/SPEC.md b/SPEC.md index 9360cae..f3a0d6f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -75,7 +75,7 @@ A verifier accepts a `ProofBundle` if and only if every one of the following che 5. **Revocation** — none of the certs appear in the signed revocation list. 6. **Constraint evaluation** — every `Constraint` on every cert in the chain evaluates true against the caller-supplied `VerifierContext`. A constraint whose required context field is absent from the context fails with `constraint_unverifiable`. An unknown constraint `type` fails with `constraint_unknown`. -Any failure causes immediate rejection. The failure type (`expired`, `revoked`, `invalid`, `unauthorized`, `constraint_denied`, `constraint_unverifiable`, `constraint_unknown`) is returned deterministically. A single component-signature failure (e.g. Ed25519 passes but ML-DSA-65 fails, or vice versa) fails the whole signature — fail-closed is the v1 semantics. +Any failure causes immediate rejection. The failure type (`expired`, `revoked`, `scope_denied`, `constraint_denied`, `constraint_unverifiable`, `constraint_unknown`, `invalid_scope`, `delegation_not_authorized`, `invalid`) is returned deterministically. A single component-signature failure (e.g. Ed25519 passes but ML-DSA-65 fails, or vice versa) fails the whole signature — fail-closed is the v1 semantics. --- @@ -367,7 +367,7 @@ For failures with their own status, `error_reason` begins with the status name f } ``` -`RevocationPush` is a v1.1 optional signed notification that a revocation-list issuer sends to subscribed verifiers in real time (ROADMAP §2.4). The payload carries a delta — cert IDs added to the revocation list since the previous push — and is hybrid-signed by the issuer so a verifier can trust it without re-fetching the full list. +`RevocationPush` is a v1.1 optional signed notification that a revocation-list issuer sends to subscribed verifiers in real time (ROADMAP, Continuous real-time interactions). The payload carries a delta — cert IDs added to the revocation list since the previous push — and is hybrid-signed by the issuer so a verifier can trust it without re-fetching the full list. `seq_no` is monotonically increasing per issuer (first push is 1). Receivers MUST detect gaps (missed pushes) and fall back to a full revocation-list fetch when a gap is detected. `entries` is always serialized as an array (`[]` when empty), never as `null`. @@ -385,7 +385,7 @@ The push subscription endpoint (WebSocket, SSE, gRPC stream, etc.) is an operato } ``` -`WitnessEntry` is a v1.1 spec-defined element in a hash-chain append-only log (ROADMAP §3.2). Any party may operate a Witness: Identities AI, an enterprise's own audit system, a third-party notary, or a blockchain-anchored system. Multiple witnesses MAY independently log the same events (redundancy). +`WitnessEntry` is a v1.1 spec-defined element in a hash-chain append-only log (ROADMAP, Tamper-evident transaction streams). Any party may operate a Witness: Identities AI, an enterprise's own audit system, a third-party notary, or a blockchain-anchored system. Multiple witnesses MAY independently log the same events (redundancy). v1.1 defines the shape and the signing semantics only. Operating a scalable witness is an implementation/product concern; the spec does not mandate deployment topology, storage backend, or consistency model. The fixture `witness_entry_valid` proves cross-SDK byte-identicality of the signable and signature. @@ -406,7 +406,7 @@ v1.1 defines the shape and the signing semantics only. Operating a scalable witn } ``` -`SessionToken` is a v1.1 backward-compatible signed credential that caches the result of a full chain verification. After a successful `Verify(ProofBundle)`, the verifier MAY issue a SessionToken binding the verified chain to its session. Subsequent turns in the same session present the token plus a fresh `ChallengeSig`; the verifier checks the token's HMAC and challenge signature without re-verifying the delegation chain. This is the session cert cache of ROADMAP §2.3. +`SessionToken` is a v1.1 backward-compatible signed credential that caches the result of a full chain verification. After a successful `Verify(ProofBundle)`, the verifier MAY issue a SessionToken binding the verified chain to its session. Subsequent turns in the same session present the token plus a fresh `ChallengeSig`; the verifier checks the token's HMAC and challenge signature without re-verifying the delegation chain. This is the session cert cache of ROADMAP's Continuous real-time interactions. `chain_hash` is the 32-byte SHA-256 of the concatenated `DelegationSignBytes` of each cert in the verified chain. Any change to any cert in the chain changes `chain_hash`, invalidating tokens issued against the old chain. @@ -466,7 +466,7 @@ ReceiptPartySignature: "signature": ``` -`TransactionReceipt` is a v1.1 canonical multi-party transaction envelope (ROADMAP §3.1 / §3.3). Ratify does not interpret `terms_canonical_json` — the application owns the business terms schema. `terms_schema_uri` identifies which schema a specialized verifier dispatches on. Ratify guarantees envelope atomicity and party signatures. +`TransactionReceipt` is a v1.1 canonical multi-party transaction envelope (ROADMAP, Tamper-evident transaction streams). Ratify does not interpret `terms_canonical_json` — the application owns the business terms schema. `terms_schema_uri` identifies which schema a specialized verifier dispatches on. Ratify guarantees envelope atomicity and party signatures. The signable bytes for every party's signature (§6.4.7) include `version`, `transaction_id`, `created_at`, `terms_schema_uri`, `terms_canonical_json`, and the full sorted `parties` set (sorted lex by `party_id`; each party projected to `{agent_id, agent_pub_key, party_id, role}` — `proof_bundle` is excluded since it is verified independently). Because every party's signature covers the same sorted party set, adding, removing, or altering any party invalidates every existing signature — there is no partial-valid receipt state. @@ -893,7 +893,7 @@ Pure ML-DSA-65 would be a single point of cryptographic failure against future a 54 canonical scope strings organized by domain, plus 14 wildcards, plus one extension pattern (`custom:…`) for application-specific scopes outside the canonical vocabulary. Implementations MUST reject scopes that are not canonical, not a wildcard, and not a `custom:` extension — at issuance via `ValidateScopes`, and at verification: the verifier rejects any cert granting invalid vocabulary with `invalid_scope` (§5.9, §10 step 7.a2) before any effective-scope arithmetic. -The vocabulary covers both software agents (meetings, comms, files, transactions, execution, generation) and embodied agents (physical actions, robots, drones, vehicles, infrastructure, generic actuation). Ratify is channel-agnostic by construction (§3.5, §3.6) — the same cert/bundle/verify semantics authorize a software agent in a video meeting and a drone at a delivery address. +The vocabulary covers both software agents (meetings, comms, files, transactions, execution, generation) and embodied agents (physical actions, robots, drones, vehicles, infrastructure, generic actuation). Ratify is channel-agnostic by construction (§3 principle 6) — the same cert/bundle/verify semantics authorize a software agent in a video meeting and a drone at a delivery address. ### 9.1 Canonical scopes @@ -1701,7 +1701,7 @@ The verify function: - Returns `"policy_verdict_denied: ..."` on cached **deny** (MAC valid but `allow=false`). - Returns any other error if the verdict is unusable (bad MAC, expired, scope mismatch, etc). -**Verifier fast-path semantics (§5.7.2):** when `VerifyOptions.PolicyVerdict` and `VerifyOptions.PolicySecret` are both set, the verifier consults the verdict BEFORE the `Policy` provider: +**Verifier fast-path semantics (§5.17):** when `VerifyOptions.PolicyVerdict` and `VerifyOptions.PolicySecret` are both set, the verifier consults the verdict BEFORE the `Policy` provider: - Cached allow → live policy is **not called**; return success. - Cached deny → live policy is **not called**; return `scope_denied`. - Verdict unusable (expired / wrong MAC / scope mismatch) → fall through to live `Policy` provider. A stale verdict MUST NOT cause a verification failure on its own. @@ -1716,7 +1716,7 @@ The `ConstraintEvaluator` interface is the pluggable layer: callers register eva 1. Built-in evaluators handle the universal types (always, by the SDK directly). 2. For any type the built-in evaluators do not recognize, the registry is consulted. -3. If no entry matches, the verifier fails closed with `identity_status="constraint_unknown"` (per §5.16). +3. If no entry matches, the verifier fails closed with `identity_status="constraint_unknown"` (per §5.9). **Interface:** diff --git a/demos/README.md b/demos/README.md index 8f085d1..71c8471 100644 --- a/demos/README.md +++ b/demos/README.md @@ -1,6 +1,6 @@ # Ratify Protocol — Runnable Demos -**End-to-end narrative demos in every supported language. Run one, see the full protocol lifecycle happen in front of you.** +**End-to-end narrative demos in four languages (Go, Python, Rust, TypeScript). Run one, see the full protocol lifecycle happen in front of you.** Each demo walks through: @@ -101,20 +101,22 @@ If any two languages disagree on *what* happens (identity_status values, whether ## What this proves -- Every SDK implements the same verifier algorithm (per `SPEC.md` §10). -- Every SDK produces canonical sign bytes that match byte-for-byte. -- Hybrid Ed25519 + ML-DSA-65 signing works correctly in every language. -- The attack-rejection paths are consistent across implementations. +- Each of the four narrative demos (Go, TypeScript, Python, Rust) runs the same verifier algorithm (per `SPEC.md` §10). +- Each demo successfully signs and verifies protocol objects using its SDK's canonical serialization. +- Hybrid Ed25519 + ML-DSA-65 signing works correctly in each of the four demo languages. +- The attack-rejection paths are consistent across those implementations. - You can demo the protocol to a skeptical audience in ~15 seconds of scrolling. +- Full five-SDK conformance, including C, is established by the canonical fixture suites below, not by these demos. ## What this does NOT test These demos are narrative, not exhaustive. For the rigorous validation: -- `testvectors/v1/` — 20 canonical fixtures, every SDK passes byte-identical. +- `testvectors/v1/` — 79 canonical fixtures, every SDK passes byte-identical. - Go unit tests: `go test ./...` - TS conformance: `cd sdks/typescript && npm test` - Python conformance: `cd sdks/python && pytest` - Rust conformance: `cd sdks/rust && cargo test` +- C conformance: `cd sdks/c && cargo test --test conformance` See `docs/TEST_PLAN.md` for the full testing methodology. diff --git a/demos/go/demo.go b/demos/go/demo.go index 59037cf..65573b2 100644 --- a/demos/go/demo.go +++ b/demos/go/demo.go @@ -23,6 +23,13 @@ func kv(label, value string) { fmt.Printf(" %-20s %s\n", label, value) } +// demoRevocation is a minimal RevocationProvider (SPEC §17.1) for the demo. +type demoRevocation struct{ revokedID string } + +func (d demoRevocation) IsRevoked(certID string) (bool, error) { + return certID == d.revokedID, nil +} + func main() { // Step 1 banner("STEP 1 Alice generates a hybrid root identity") @@ -125,7 +132,7 @@ func main() { banner("REVOCATION Alice revokes the cert") r = ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: ratify.ScopeMeetingAttend, - IsRevoked: func(certID string) bool { return certID == cert.CertID }, + Revocation: demoRevocation{revokedID: cert.CertID}, }) fmt.Printf(" ❌ REJECTED as expected: %s: %s\n", r.IdentityStatus, r.ErrorReason) kv("Why:", "Verifier's revocation list now contains this cert_id.") diff --git a/demos/python/demo.py b/demos/python/demo.py index f3d0ed6..ee2baaf 100644 --- a/demos/python/demo.py +++ b/demos/python/demo.py @@ -50,6 +50,16 @@ def kv(label: str, value: str) -> None: print(f" {label:20s} {value}") +class _DemoRevocation: + """Minimal RevocationProvider (SPEC §17.1) for the demo.""" + + def __init__(self, revoked_id: str) -> None: + self._revoked_id = revoked_id + + def is_revoked(self, cert_id: str) -> tuple[bool, str | None]: + return (cert_id == self._revoked_id, None) + + def run() -> None: # ----------------------------------------------------------------- # Step 1: Alice creates her root identity @@ -185,7 +195,7 @@ def run() -> None: bundle, VerifyOptions( required_scope=SCOPE_MEETING_ATTEND, - is_revoked=lambda cid: cid == cert.cert_id, + revocation=_DemoRevocation(cert.cert_id), ), ) print(f" ❌ REJECTED as expected: {r.identity_status}: {r.error_reason}") diff --git a/demos/rust/Cargo.lock b/demos/rust/Cargo.lock index b96f573..383c070 100644 --- a/demos/rust/Cargo.lock +++ b/demos/rust/Cargo.lock @@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "ratify-protocol" -version = "1.0.0-alpha.10" +version = "1.0.0-alpha.15" dependencies = [ "base64", "chrono", diff --git a/demos/rust/demo.rs b/demos/rust/demo.rs index 8e7feec..80324e1 100644 --- a/demos/rust/demo.rs +++ b/demos/rust/demo.rs @@ -45,7 +45,8 @@ fn main() { // Step 2 banner("STEP 2 Agent (Alice's scheduler) generates its own hybrid keypair"); - let (agent, agent_priv) = generate_agent("Alice's Scheduler", "voice_agent"); + let (agent, agent_priv) = + generate_agent("Alice's Scheduler", "voice_agent").expect("agent generation"); kv("Agent ID:", &agent.id); kv("Agent type:", &agent.agent_type); kv( @@ -69,7 +70,7 @@ fn main() { expires_at: now + 7 * 24 * 3600, signature: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, }; - issue_delegation(&mut cert, &alice_priv); + issue_delegation(&mut cert, &alice_priv).expect("delegation issuance"); kv("Cert ID:", &cert.cert_id); kv("Scope:", &cert.scope.join(", ")); kv("Expires:", &format!("unix {}", cert.expires_at)); diff --git a/demos/typescript/demo.ts b/demos/typescript/demo.ts index 3f96b87..bc9236d 100644 --- a/demos/typescript/demo.ts +++ b/demos/typescript/demo.ts @@ -136,7 +136,12 @@ async function main() { banner("REVOCATION Alice revokes the cert"); result = await verifyBundle(bundle, { required_scope: SCOPE_MEETING_ATTEND, - is_revoked: (cid: string) => cid === cert.cert_id, + revocation: { + // Minimal RevocationProvider (SPEC §17.1) for the demo. + async isRevoked(cid: string): Promise<[boolean, Error | null]> { + return [cid === cert.cert_id, null]; + }, + }, }); console.log(` ❌ REJECTED as expected: ${result.identity_status}: ${result.error_reason}`); kv("Why:", "Verifier's revocation list now contains this cert_id."); diff --git a/docs/AGENT_TO_AGENT.md b/docs/AGENT_TO_AGENT.md index 4b17fef..27464d0 100644 --- a/docs/AGENT_TO_AGENT.md +++ b/docs/AGENT_TO_AGENT.md @@ -6,7 +6,7 @@ Ratify is symmetric in both directions: the same data structures and verifier al **Prerequisites:** familiarity with [`SPEC.md`](../SPEC.md) §5 (data structures), §8 (hybrid cryptography), §9 (scope vocabulary), and §10 (verifier algorithm). -**Companion reference:** [`EXPLAINED.md`](EXPLAINED.md) §11 covers the performance envelope and when to apply each pattern in real-time vs. continuous workflows. +**Companion reference:** [`BENCHMARKS.md`](BENCHMARKS.md) covers the performance envelope (per-SDK verify latency, session-token fast path) that informs when to apply each pattern in real-time vs. continuous workflows. --- @@ -151,7 +151,7 @@ Verifier runs Verify(): ### Chain depth limit -`MAX_DELEGATION_CHAIN_DEPTH = 8` (raised from 3 in v1.0.0-alpha.16 for multi-hop agent topologies). This bounds the chain length and the verify-time cost (each level adds two hybrid signature verifications); the byte and count limits in SPEC §5.1 bound the work that depth alone does not. The ceiling is a wire-determinism and denial-of-service bound, not cryptography. Organizations that want shorter chains than the ceiling should flatten (e.g., have the tenant admin directly delegate to leaves rather than passing through intermediate tiers); a per-delegation `max_delegation_depth` constraint is planned so principals can bound their own chains tighter. +`MAX_DELEGATION_CHAIN_DEPTH = 8` (raised from 3 in v1.0.0-alpha.16 for multi-hop agent topologies). This bounds the chain length and the verify-time cost (each additional cert adds one hybrid signature verification, itself an Ed25519 check plus an ML-DSA-65 check); the byte and count limits in SPEC §5.1 bound the work that depth alone does not. The ceiling is a wire-determinism and denial-of-service bound, not cryptography. Organizations that want shorter chains than the ceiling should flatten (e.g., have the tenant admin directly delegate to leaves rather than passing through intermediate tiers); a per-delegation `max_delegation_depth` constraint is planned so principals can bound their own chains tighter. --- @@ -237,7 +237,7 @@ Six months after the transaction, either party can produce this artifact and a t - **Defined:** `HybridSignature` is the authoritative signature primitive for any receipt. `CanonicalJSON(terms)` gives deterministic signable bytes regardless of which language produced the `terms` object. Applications that hash the terms will get the same bytes across implementations. - **v1.1 envelope:** [`TRANSACTION_RECEIPTS.md`](TRANSACTION_RECEIPTS.md) defines a canonical receipt envelope that binds the transaction ID, schema URI, terms bytes, party set, and every party signature. -- **NOT defined in tagged v1:** the schema of `terms` itself. That's application-specific — a compute-purchase receipt looks different from a calendar-booking receipt. v1 deliberately leaves this open until receipt fixtures and SDK APIs ship. +- **NOT defined in tagged v1:** the schema of `terms` itself. That's application-specific (a compute-purchase receipt looks different from a calendar-booking receipt). The receipt envelope, its canonical fixtures, and the SDK sign/verify APIs have shipped (v1.0.0-alpha.6); only the `terms` schema stays application-defined. ### When to use @@ -264,7 +264,7 @@ If Alice rotates her root key while Agent A is mid-interaction with Agent B, Age ### Multi-party agreements -Ratify v1 is pairwise. For three-party agreements (A, B, and C all commit to the same terms), the v1 approach is to produce three pairwise receipts (A-B, A-C, B-C) sharing the same `terms` bytes. A v2 extension may add a native *group signature* construct; until then, three pairwise receipts are the canonical pattern. +Ratify v1 supports N-party agreements natively. A single `TransactionReceipt` (SPEC §5.14) binds the full sorted party set, and every party signs the same signable bytes (transaction ID, terms, schema URI, and the complete party set). Because the whole party set is inside each signature, adding, removing, or altering any party invalidates every existing party signature (party-set atomicity). For a three-party agreement (A, B, and C all committing to the same terms), one envelope lists all three parties and carries all three signatures: there is no need to fan out into pairwise receipts, and no v2 group-signature construct is required for basic multi-party agreement. See [`TRANSACTION_RECEIPTS.md`](TRANSACTION_RECEIPTS.md) for the envelope and its fixtures. ### Privacy of counterparty identities diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md index 07ab2a4..b0255ca 100644 --- a/docs/ATTRIBUTION.md +++ b/docs/ATTRIBUTION.md @@ -13,7 +13,7 @@ Signal Protocol." **In your docs or security page:** -> Agent authorization powered by [Ratify Protocol](https://ratifyprotocol.com) +> Delegated-authority proofs powered by [Ratify Protocol](https://ratifyprotocol.com) **In your product UI** (next to verification results): diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index ca80754..d5f7673 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -29,7 +29,7 @@ Numbers below are the median of 3 runs per benchmark. Actual latency on commodit | `BenchmarkVerifyDepth3` | 831 972 | **832 µs** (0.83 ms) | 199 621 | 592 | | `BenchmarkVerifyDepth1_WithConstraint` | 397 836 | **398 µs** (0.40 ms) | 90 547 | 270 | -> Numbers refreshed 2026-07-25 (same M2 Pro baseline). The increase over the previously committed table (~0.34/0.70 ms at depth 1/3) is the cost of verifier-path correctness features added since: scope-vocabulary validation (§9, alpha.12) and the additional in-verifier checks landed through alpha.15. (Strict wire acceptance also landed in this window but is NOT a contributor here — the benchmark constructs a typed bundle once and never exercises JSON decoding.) Still under a millisecond at every legal depth. +> Numbers refreshed 2026-07-25 (same M2 Pro baseline). The increase over the previously committed table (~0.34/0.70 ms at depth 1/3) is the cost of verifier-path correctness features added since: scope-vocabulary validation (§9, alpha.12) and the additional in-verifier checks landed through alpha.15. (Strict wire acceptance also landed in this window but is NOT a contributor here — the benchmark constructs a typed bundle once and never exercises JSON decoding.) Under a millisecond at every depth measured (1 through 3); depths 4 through 8 are projected, see below. Verification exercises the full §4 trust equation: structural checks, agent binding, per-cert signature validation (Ed25519 + ML-DSA-65), chain linkage, sub-delegation gate, constraint evaluation, challenge-signature validation, revocation lookup (nil callback here), scope intersection. diff --git a/docs/EXPLAINED.md b/docs/EXPLAINED.md index 25ee29a..728b7dc 100644 --- a/docs/EXPLAINED.md +++ b/docs/EXPLAINED.md @@ -56,6 +56,11 @@ Alice's agent needs to hire a specialized "Travel Agent" to book a flight. Alice ### Multi-hop Verifiability When the Travel Agent presents the proof to the Airline, the Airline can see the entire chain. They don't just know that Alice's agent authorized the Travel Agent; they can cryptographically verify that *Alice herself* authorized the root agent to begin with. +### Resource-bound authority (the `resource_path` constraint) +Scopes answer *what* an agent may do; the `resource_path` constraint answers *where*. It binds a delegation to one resource (a repository, workspace, channel, or device) named by an opaque `resource_id`, and optionally to a `path_prefix` inside that resource. A scope of `files:write` grants file writes anywhere; `files:write` paired with `resource_path{repo R, /docs}` grants writes only under `/docs` of repository R. + +Two properties make it safe to hand down a sub-delegation chain. The `resource_id` is compared **byte-for-byte** (the verifier never fetches, decodes, or normalizes it), and `path_prefix` matches only on **segment boundaries**, so `/src` covers `/src` and `/src/a.ts` but never `/src-old`. Because verification evaluates every cert's constraints conjunctively against the same requested resource, effective authority can stay the same or narrow down the chain but never widen: a child may carry a broader prefix on the same resource and still verify, yet gains nothing because the request must still satisfy every hop upstream, while a child naming a different resource or a same-resource prefix that does not nest makes the chain jointly unsatisfiable and fails closed. Downstream escape is impossible by construction, not by a special rule. `resource_path` is one of the eight canonical constraint types (`geo_circle`, `geo_polygon`, `geo_bbox`, `time_window`, `max_speed_mps`, `max_amount`, `max_rate`, `resource_path`); the verifier receives the requested resource and path through its `VerifierContext`. + --- ## 5. Threat Model & Defenses diff --git a/docs/RELEASES.md b/docs/RELEASES.md index 3a9a7de..7737586 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -57,16 +57,20 @@ v1.0.0-alpha.12 → no-expiry sentinel, presence:represent (54th scope), v1.0.0-alpha.13 → SPEC §13.1 registry read binding + reference resolver (registry-mode key discovery, pin-plus-registry); two-phase release flow, ratify-c publish pipeline fixed +v1.0.0-alpha.14 → canonical tagline in all registry metadata, corrected + npm install pin, clippy-clean Rust crates, hardened + release path (checksum gate, convergent break-glass + publish); 63 fixtures, byte-identical to alpha.13 v1.0.0-alpha.15 → integration readiness: public wire codecs (TS/Python) + strict acceptance, ChallengeStore + normative single-use (§10), streamed-turn options verification (§5.13), operation/session-context constructions + Middleware Custody Profile (§6.4.9, §15.2.1), per-SDK benchmark and wire-size docs; 63 fixtures, byte-identical to alpha.13 -v1.0.0-alpha.14 → canonical tagline in all registry metadata, corrected - npm install pin, clippy-clean Rust crates, hardened - release path (checksum gate, convergent break-glass - publish); 63 fixtures, byte-identical to alpha.13 +v1.0.0-alpha.16 → resource-bound authority (resource_path constraint), + extension constraint params, new input bounds, and + MAX_DELEGATION_CHAIN_DEPTH raised 3→8 (SPEC §5.1, §5.7.3); + 79 fixtures (merged, unpublished) … 1.0.0-beta.1 → after first external security audit of Go reference 1.0.0-rc.1 → when Python + Rust + TS all pass + external audit of at least 2 SDKs @@ -208,9 +212,9 @@ The release logic lives in: ## 6. Continuous integration -Every push and every pull request runs a matrix across all five SDKs (Go, TypeScript, Python, Rust, C/C++). `.github/workflows/ci.yml` already defines the Go + determinism + TS jobs; Python and Rust are added now. +Every push and every pull request runs `.github/workflows/ci.yml`, which fans out into a per-SDK conformance job for each of the five implementations (Go, TypeScript, Python, Rust, C/C++), plus a determinism job (regenerate the fixtures and diff against the committed `testvectors/v1/`) and a release-metadata sync check. -Every PR must pass the full conformance grid before merge. The NxN interop matrix (see [`SDKS.md`](SDKS.md) §5) is enforced — a bundle produced by any implementation must verify in every implementation. +Every PR must pass the full conformance grid before merge. Cross-language agreement is hub-and-spoke, not a full N×N matrix: the Go reference generates the canonical fixture bytes, the determinism job proves they regenerate byte-identical, and the TypeScript, Python, Rust, and C/C++ SDKs each assert against those same committed reference vectors. Because every implementation is checked against the Go reference bytes, they transitively agree with one another, which is what keeps signatures verifying across languages. ## 7. Fixture versioning diff --git a/docs/RESOURCE_PROFILES.md b/docs/RESOURCE_PROFILES.md index b9da9ad..5f56c1e 100644 --- a/docs/RESOURCE_PROFILES.md +++ b/docs/RESOURCE_PROFILES.md @@ -14,7 +14,7 @@ Every profile must define: one canonical byte-identical representation per resou | Profile | Version | Resource kinds | Status | |---|---|---|---| -| Git | v1 (this document) | Git repositories | Draft — under adversarial review for v1.0.0-alpha.16 | +| Git | v1 (this document) | Git repositories | v1, merged in v1.0.0-alpha.16 (release unpublished) | | Platform-authored profiles | — | Defined by the authoring platform (e.g. workspaces, channels, conversations, compute nodes) | Linked here when published | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f2cd2b2..a5e84be 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,7 +10,7 @@ This roadmap has three buckets: **shipped** (in the current release), **planned* ## Shipped -All v1.1 features below are backward-compatible with v1.0 and shipped in v1.0.0-alpha.5; the C/C++ SDK and its full conformance landed across alpha.8–alpha.10. Legacy v1.0 bundles continue to verify in v1.1 verifiers. The canonical test-vector suite (`testvectors/v1/`) proves cross-SDK conformance across Go, TypeScript, Python, Rust, and C/C++ — see [`CHANGELOG.md`](../CHANGELOG.md) for the count at each release. +All v1.1 features below are backward-compatible with v1.0 and shipped in v1.0.0-alpha.4; the C/C++ SDK and its full conformance landed across alpha.8–alpha.10. Legacy v1.0 bundles continue to verify in v1.1 verifiers. The canonical test-vector suite (`testvectors/v1/`) proves cross-SDK conformance across Go, TypeScript, Python, Rust, and C/C++ — see [`CHANGELOG.md`](../CHANGELOG.md) for the count at each release. ### Continuous real-time interactions @@ -31,39 +31,20 @@ All v1.1 features below are backward-compatible with v1.0 and shipped in v1.0.0- | **Witness append-only log** | §5.12, §6.4.6 | Signed `WitnessEntry` defines the hash-chain shape for append-only audit logs. Any party can operate a witness. v1.1 defines the shape; operating a scalable witness is a deployment concern. | 1 | | **Key rotation statement** | §5.15, §6.4.4 | `KeyRotationStatement` signed by both old and new root keys. Auditors and registries can verify identity continuity across key rotations. | 2 | ---- - -## What v1.0 already guarantees (baseline) - -- **Hybrid quantum-safe signatures** (Ed25519 + ML-DSA-65). Harvest-now-decrypt-later attacks don't work. -- **Per-interaction liveness** via challenge-response (≤5 minute window, single-use challenge). -- **Chain authorization** with scope intersection. An intermediate cannot grant what it did not receive. -- **Explicit revocation** with signed revocation lists. Verifiers cache and fail-closed on unreachability. -- **Cryptographic tamper-evidence** per object. Every `DelegationCert`, `ProofBundle`, and `RevocationList` is signed; byte-level modification is detected. -- **54 canonical scopes** organized by domain, plus wildcards and a `custom:` extension pattern. -- **First-class constraints** (geo, time-window, speed, amount, rate) evaluated at verify time against caller-supplied context. -- **Three key-custody modes** — self-custody (device-held keys), custodial (server-side envelope encryption), and delegated custody (enterprise IdP as root). Self-custody is the strongest mode; custodial users can upgrade to self-custody via `KeyRotationStatement` at any time. See `SPEC.md` §15.2. - ---- - -## Planned — next releases (backward-compatible) - -### v1.0.0-alpha.11 — docs & spec hardening (RELEASED 2026-07-06; no wire change, no protocol or SDK code change) +### v1.0.0-alpha.11: docs and spec hardening (released 2026-07-06; no wire change, no protocol or SDK code change) - README truth pass: representative demo transcript, surface the shipped v1.1 feature set, accurate repository layout. - SPEC additions: §15.4 trust anchors and public-key discovery, §15.5 revocation freshness, §15.6 verifier clock discipline, §15.7 constraint attestation limits, threat T12 (key substitution), SessionToken lifetime and multi-instance guidance (§5.13), crypto-agility note (§12). - Local test gate (`scripts/test-all.sh`) now runs the C/C++ SDK, matching what CI and `docs/RELEASES.md` already claimed. - All 59 canonical fixtures (the count at the time) byte-identical to alpha.10. -### v1.0.0-alpha.12 — protocol additions (below) - -The two items below are scopes and features identified through production adapter design. No wire format change required — new `scope.go` entries, updated SPEC §9/§4, and new test fixtures. Adding fixtures changes the canonical fixture count; the release includes a full sweep of the documented count plus a fixture-count check in `scripts/check-release-sync.sh`. +### v1.0.0-alpha.12: protocol additions (released 2026-07-06) ---- +The two items below are scopes and features identified through production adapter design. No wire format change required: new `scope.go` entries, updated SPEC §9/§4, and new test fixtures. Adding fixtures changes the canonical fixture count; the release includes a full sweep of the documented count plus a fixture-count check in `scripts/check-release-sync.sh`. -### No-expiry sentinel — `ExpiresAt = 4070908799` +#### No-expiry sentinel: `ExpiresAt = 4070908799` -**Status:** Implemented — ships in v1.0.0-alpha.12. Normative in SPEC §5.1 + §5.7; fixture `no_expiry_cert`; no-expiry sentinel constant + helper in every SDK (Go `NoExpirySentinel`/`IsNoExpiry()`, TS `NO_EXPIRY_SENTINEL`/`isNoExpiry()`, Python/Rust `NO_EXPIRY_SENTINEL`/`is_no_expiry()`, C `ratify_no_expiry_sentinel()`/`ratify_expires_at_is_no_expiry()`). +**Status:** Shipped in v1.0.0-alpha.12. Normative in SPEC §5.1 + §5.7; fixture `no_expiry_cert`; no-expiry sentinel constant + helper in every SDK (Go `NoExpirySentinel`/`IsNoExpiry()`, TS `NO_EXPIRY_SENTINEL`/`isNoExpiry()`, Python/Rust `NO_EXPIRY_SENTINEL`/`is_no_expiry()`, C `ratify_no_expiry_sentinel()`/`ratify_expires_at_is_no_expiry()`). **Problem:** `DelegationCert.ExpiresAt` is `int64` (Unix timestamp). The struct has no null/optional representation. Users of the Ratify Verify managed platform can grant delegations with "no expiry (until revoked)," which the platform stores as `NULL` in the database. The cert that gets signed must still have a finite `ExpiresAt` value for protocol compliance. @@ -75,33 +56,31 @@ The two items below are scopes and features identified through production adapte **Problem for SDK consumers:** Offline verifiers using only the protocol SDK see `ExpiresAt = 4070908799` and have no way to distinguish "no expiry" from a cert that legitimately expires in 2099. They may apply organizational policy caps incorrectly. **Normalization (shipped in v1.0.0-alpha.12):** -1. `4070908799` is a normative sentinel — `NO_EXPIRY_SENTINEL` in SPEC §5.1, display/policy behavior in §5.7. -2. Conformant SDKs MUST treat `ExpiresAt == NO_EXPIRY_SENTINEL` as "no expiry (until revoked)" in display and policy evaluation — not as a literal 2099 expiry. Constant + helper in every SDK (Go `IsNoExpiry()`, TS `isNoExpiry()`, Python/Rust `is_no_expiry()`, C `ratify_expires_at_is_no_expiry()`). +1. `4070908799` is a normative sentinel: `NO_EXPIRY_SENTINEL` in SPEC §5.1, display/policy behavior in §5.7. +2. Conformant SDKs MUST treat `ExpiresAt == NO_EXPIRY_SENTINEL` as "no expiry (until revoked)" in display and policy evaluation, not as a literal 2099 expiry. Constant + helper in every SDK (Go `IsNoExpiry()`, TS `isNoExpiry()`, Python/Rust `is_no_expiry()`, C `ratify_expires_at_is_no_expiry()`). 3. Conformance fixture `no_expiry_cert.json` pins the verify behavior. **Alternative not taken:** `NoExpiry bool` or `ExpiresAt *int64` on `DelegationCert` would be wire-breaking; if the sentinel ever proves insufficient it belongs in v2.0. ---- +#### `presence:represent`: agent representation of a human -### `presence:represent` — agent representation of a human +**Status:** Shipped in v1.0.0-alpha.12. SPEC §9.1; fixtures `presence_represent_allowed` + `reject_presence_sensitive_wildcard`. -**Status:** Implemented — ships in v1.0.0-alpha.12. SPEC §9.1; fixtures `presence_represent_allowed` + `reject_presence_sensitive_wildcard`. +**Problem it solved:** -**Problem it solves:** - -The current scope vocabulary covers what an agent *does* (attend a meeting, speak, record). It does not cover what an agent *is* in a given context — specifically, an agent that is attending and interacting *as a proxy for* a named human principal, not merely alongside them. +The current scope vocabulary covers what an agent *does* (attend a meeting, speak, record). It does not cover what an agent *is* in a given context, specifically an agent that is attending and interacting *as a proxy for* a named human principal, not merely alongside them. Three scenarios, all requiring a distinct scope: | Scenario | Current scopes | Gap | |---|---|---| -| **Attendee bot** — Otter joins Marcus's meeting, takes notes | `meeting:attend`, `meeting:speak` | No gap — covered | -| **Representative agent** — Marcus's AI agent attends on his behalf, speaks and acts as his representative (does not look like him) | `meeting:attend` + `meeting:speak` | No scope asserts "this agent IS Marcus's representative" | -| **Likeness agent** — Tavus agent that looks, sounds, and responds like Marcus, trained on his knowledge | `generate:deepfake` + `meeting:video` + `meeting:speak` | `generate:deepfake` covers content generation, not real-time identity representation. A verifier cannot tell from scopes alone that this agent is presenting as Marcus. | +| **Attendee bot** (Otter joins Marcus's meeting, takes notes) | `meeting:attend`, `meeting:speak` | No gap (covered) | +| **Representative agent** (Marcus's AI agent attends on his behalf, speaks and acts as his representative; does not look like him) | `meeting:attend` + `meeting:speak` | No scope asserts "this agent IS Marcus's representative" | +| **Likeness agent** (Tavus agent that looks, sounds, and responds like Marcus, trained on his knowledge) | `generate:deepfake` + `meeting:video` + `meeting:speak` | `generate:deepfake` covers content generation, not real-time identity representation. A verifier cannot tell from scopes alone that this agent is presenting as Marcus. | **Why `generate:deepfake` is not sufficient:** -`generate:deepfake` means "generate content imitating a real person." It is a content-creation scope. Representation is a presence and identity scope — it describes the agent's relationship to the principal in a real-time interaction, not the content it generates. An agent could hold `generate:deepfake` without representing the principal in a meeting, and could represent the principal without generating likeness content. +`generate:deepfake` means "generate content imitating a real person." It is a content-creation scope. Representation is a presence and identity scope: it describes the agent's relationship to the principal in a real-time interaction, not the content it generates. An agent could hold `generate:deepfake` without representing the principal in a meeting, and could represent the principal without generating likeness content. **Proposed scope:** @@ -111,21 +90,44 @@ presence:represent (sensitive) Semantics: "This agent is authorized to attend and interact as a direct representative of the principal. Other parties in the interaction may be interacting with this agent as if it were the principal." -Sensitive by design — requires explicit human confirmation beyond standard delegation, because the scope asserts identity representation, not just task execution. +Sensitive by design: requires explicit human confirmation beyond standard delegation, because the scope asserts identity representation, not just task execution. -**Companion disclosure flag — considered and rejected (2026-07-06):** +**Companion disclosure flag, considered and rejected (2026-07-06):** -An earlier draft proposed a boolean `requires_disclosure` constraint (default `true`) on certs carrying this scope, with the protocol defining the constraint and applications enforcing it. Rejected — see the locked decisions below: a verify-time constraint cannot verify that disclosure actually happened, so it would assert an obligation the protocol is structurally unable to check. Disclosure is platform policy, carried in the SPEC as a non-normative expectation. +An earlier draft proposed a boolean `requires_disclosure` constraint (default `true`) on certs carrying this scope, with the protocol defining the constraint and applications enforcing it. Rejected (see the locked decisions below): a verify-time constraint cannot verify that disclosure actually happened, so it would assert an obligation the protocol is structurally unable to check. Disclosure is platform policy, carried in the SPEC as a non-normative expectation. **Wire impact:** None. New scope string + `sensitiveScopes` entry + `validScopes` entry. Fully backward-compatible. v1.0 verifiers that don't know this scope treat it as unknown and may reject it (correct fail-closed behavior for unknown sensitive scopes). **Design decisions (locked 2026-07-06):** -- **No implication.** `presence:represent` does NOT imply `identity:prove`. Issuers grant both explicitly when both are needed. Scope lists stay literal — effective authority is exactly the chain intersection, with no hidden expansion table for verifiers or auditors to consult. +- **No implication.** `presence:represent` does NOT imply `identity:prove`. Issuers grant both explicitly when both are needed. Scope lists stay literal: effective authority is exactly the chain intersection, with no hidden expansion table for verifiers or auditors to consult. - **One scope, no sub-qualifiers.** `presence:represent:voice` / `presence:represent:likeness` are deferred until real adapter pressure proves the distinction is needed at the protocol layer. Fidelity distinctions live in platform-layer constraints for now. Adding sub-qualifier scopes later is a backward-compatible minor version; retiring a wrongly guessed one is not, and no scope-deprecation process exists yet. -- **Disclosure is platform policy, not a protocol constraint.** A `requires_disclosure` constraint would assert an obligation the verifier cannot verify at verify time — disclosure happens in the application UI after verification. The SPEC scope entry will carry a non-normative note that verifiers accepting this scope are expected to surface the representation relationship to other participants. If disclosure ever needs protocol-level teeth, the right mechanism is a disclosure attestation in the receipt/audit layer, designed against an actual compliance requirement. +- **Disclosure is platform policy, not a protocol constraint.** A `requires_disclosure` constraint would assert an obligation the verifier cannot verify at verify time: disclosure happens in the application UI after verification. The SPEC scope entry will carry a non-normative note that verifiers accepting this scope are expected to surface the representation relationship to other participants. If disclosure ever needs protocol-level teeth, the right mechanism is a disclosure attestation in the receipt/audit layer, designed against an actual compliance requirement. + +### Resource-bound authority and deeper chains (v1.0.0-alpha.16, spec merged, release unpublished) + +| Feature | Spec | What it solves | Fixtures | +|---------|------|---------------|----------| +| **Resource-bound authority** | §5.7.3 | `resource_path` constraint (the 8th constraint type) binds a delegation to a specific resource plus an optional segment-boundary path prefix. Effective authority narrows or stays the same down the chain, never widens; a child may carry a broader prefix but gains no authority because every upstream constraint still applies. | 14 | +| **Extension constraint params** | §5.7 | Constraints may carry an extension `params` object inside the canonical signed bytes. A verifier with no registered evaluator for the type still fails closed with `constraint_unknown`. | 1 | +| **Deeper delegation chains** | §5.1 | `MAX_DELEGATION_CHAIN_DEPTH` raised from 3 to 8, driven by real multi-hop agent-platform topologies, alongside new byte/count/length input bounds. | 1 | + +--- + +## What v1.0 already guarantees (baseline) + +- **Hybrid quantum-safe signatures** (Ed25519 + ML-DSA-65). Harvest-now-decrypt-later attacks don't work. +- **Per-interaction liveness** via challenge-response (≤5 minute window, single-use challenge). +- **Chain authorization** with scope intersection. An intermediate cannot grant what it did not receive. +- **Explicit revocation** with signed revocation lists. Verifiers cache and fail-closed on unreachability. +- **Cryptographic tamper-evidence** per object. Every `DelegationCert`, `ProofBundle`, and `RevocationList` is signed; byte-level modification is detected. +- **54 canonical scopes** organized by domain, plus wildcards and a `custom:` extension pattern. +- **First-class constraints** (geo, time-window, speed, amount, rate) evaluated at verify time against caller-supplied context. +- **Three key-custody modes** — self-custody (device-held keys), custodial (server-side envelope encryption), and delegated custody (enterprise IdP as root). Self-custody is the strongest mode; custodial users can upgrade to self-custody via `KeyRotationStatement` at any time. See `SPEC.md` §15.2. --- +## Planned — next releases (backward-compatible) + ### Agent-to-agent in real-time meeting surfaces **Status:** Design decision recorded 2026-04-27. Not yet implemented. @@ -149,7 +151,6 @@ These features require changes to the wire format or new cryptographic primitive | **Session-key derivation** | Requires X25519/ML-KEM hybrid key exchange — a new crypto primitive not in v1 | Pair Ratify proof-of-authorization with a hybrid KEX so both sides derive a shared session key in the same round-trip as verification. Analogous to TLS 1.3 combining certs + ECDHE. | | **Multi-sig / threshold delegation** | Changes `DelegationCert` wire format — breaks all v1 SDKs | `MultiSigDelegationCert` with a signer list + threshold. At least `threshold` distinct signers must produce valid hybrid signatures. For enterprise 2-of-3 quorum on high-value delegations. | | **Transparency log** | Significant operational scope — Certificate-Transparency-style infrastructure | Append-only log of revocation-list updates, operated by the issuer + external witnesses. Clients subscribe to the log and detect divergent views (selective suppression). | -| **Deeper chains** | Landed in v1.0.0-alpha.16 (spec) | `MAX_DELEGATION_CHAIN_DEPTH` raised from 3 to 8, driven by real multi-hop agent-platform topologies, alongside new byte/count/length input bounds (SPEC §5.1). | | **CBOR wire format** | Bandwidth optimization | For mobile, IoT, and bandwidth-sensitive paths. JSON remains canonical for v1. | v2 will ship with `testvectors/v2/` alongside v1 fixtures. A migration window of at least 12 months will let implementers support both. diff --git a/docs/SDKS.md b/docs/SDKS.md index 54ae9d5..b538651 100644 --- a/docs/SDKS.md +++ b/docs/SDKS.md @@ -41,12 +41,14 @@ The C SDK wraps the Rust SDK via a stable C ABI (`cbindgen`-generated header). I | x86-64 | `x86_64-unknown-linux-gnu` | Intel/AMD server, Linux PC | | ARM64 | `aarch64-unknown-linux-gnu` | Raspberry Pi 4, embedded Linux, Apple Silicon | | ARM32 | `armv7-unknown-linux-gnueabihf` | Raspberry Pi 2/3, older embedded Linux | -| ARM Cortex-M4/M7 | `thumbv7em-none-eabihf` | STM32, NXP — FreeRTOS, Zephyr | +| ARM Cortex-M4/M7 (with RTOS) | `thumbv7em-none-eabihf` + std shim | STM32, NXP running FreeRTOS or Zephyr | | RISC-V 64 | `riscv64gc-unknown-linux-gnu` | SiFive, emerging IoT | | macOS ARM64 | `aarch64-apple-darwin` | Apple Silicon Mac | | Windows x86-64 | `x86_64-pc-windows-msvc` | Native Windows | -**Conformance:** All 79 canonical fixtures pass through the C ABI across every fixture kind (verify, scope, revocation, revocation_push, key_rotation, session_token, transaction_receipt, witness_entry), plus the API test suite (44 tests) and 33 advanced-surface tests. Full parity with Go, TypeScript, Python, and Rust. +**std requirement:** the C SDK wraps the Rust SDK, whose JSON wire codec (`serde_json`) requires Rust `std` and a heap. It therefore targets hosted platforms (embedded Linux on any architecture, macOS, Windows) and RTOS environments that supply a std shim (FreeRTOS via an `embedded-std` shim, Zephyr's std support). Bare-metal Cortex-M with no OS and no heap is out of scope for the C SDK: use the Rust SDK directly (`#[no_std]` + `alloc`) for that. + +**Conformance:** All 79 canonical fixtures pass through the C ABI across every fixture kind (verify, scope, revocation, revocation_push, key_rotation, session_token, transaction_receipt, witness_entry), plus the API test suite (44 tests), 33 advanced-surface tests, and 7 input-bound boundary tests. The C SDK proves conformance through this shared 79-fixture set; the cross-SDK byte-equivalence corpus (`testvectors/v1/cross_sdk_vectors.json`, checked hub-and-spoke against the Go reference) is consumed by Go, TypeScript, Python, and Rust, not by C. **FFI languages:** any language that can link a C shared library (`libratify_c.so`) can use the C SDK as its Ratify integration — Swift (via bridging header), Zig, Lua, Julia, Ruby, Elixir, and others. @@ -64,31 +66,7 @@ Five SDKs are now shipped. The next ports expand platform coverage. **Why:** Android, JVM agent services, and enterprise middleware. A Kotlin-first SDK covers Android wallet work and Java backends without forcing those deployments through FFI. -**Target:** Maven Central. Crypto via mainstream Ed25519 and ML-DSA-65 libraries or a tightly-audited native binding. Must pass all 79 fixtures. - -### Completed: Python - -**Why:** the AI/agent ecosystem is Python-heavy. LangChain, AutoGen, CrewAI, every major agent framework has Python bindings. Voice-agent platforms run Python on their backends. MCP server reference impls exist in both Python and TypeScript. A Python SDK unlocks the largest single ecosystem of agent authors. - -**Status:** Implemented in `sdks/python/` and passing all 79 fixtures. Note: the `pqcrypto` ML-DSA-65 library does not support deterministic keygen from seeds, so Python is a verification-only SDK for fixture conformance — it cannot regenerate the canonical test fixtures. See `sdks/python/README.md` for details. - -### Completed: Rust - -**Why:** edge verifiers. Cloudflare Workers, Fastly, Vercel Edge all run WebAssembly workloads. A Rust implementation compiles to WASM and lets enterprises drop Ratify verification into their edge gateway config. Rust also covers embedded, IoT, and systems programming use cases where Go/Python aren't appropriate. - -**Status:** Implemented in `sdks/rust/` and passing all 79 fixtures. - -### Enterprise-pulled: Java / Kotlin - -**Why:** Android wallet depends on Kotlin. Large enterprise shops run on JVM. Salesforce, Oracle, SAP, many large banks — if they embed Ratify server-side, they want a JVM SDK. - -**Target:** Maven Central + Kotlin Multiplatform for mobile. Crypto via Bouncy Castle (has Ed25519 and is getting ML-DSA support) or a direct Java port. - -### C / C++ via C ABI — shipped in v1.0.0-alpha.8, full conformance in v1.0.0-alpha.10 - -**Why:** any language that does not have a native SDK can link against a C shared library via FFI. Elixir, Ruby, Lua, Swift, Zig, embedded environments, and vendor firmware all benefit. - -**Implementation:** wraps the Rust SDK via `cbindgen`-generated C ABI. Ships as `libratify_c.a` (static) and `libratify_c.so`/`.dylib`/`.dll` (shared) with a committed `ratify.h` header. Pre-built archives for common targets are published as GitHub Release assets — no Rust toolchain required to consume the SDK. See `sdks/c/` for full details. +**Target:** Maven Central + Kotlin Multiplatform for mobile. Crypto via Bouncy Castle (Ed25519, plus ML-DSA support as of BC 1.78+) or a direct Java port. Must pass all 79 fixtures. Rationale is enterprise pull: Android wallet depends on Kotlin, and large JVM shops (Salesforce, Oracle, SAP, many banks) want a JVM SDK if they embed Ratify server-side. ## 4. The conformance contract @@ -141,10 +119,15 @@ Every implementation MUST export these primitives with equivalent semantics: | `DeriveID(HybridPublicKey) -> string` | 16-byte hex ID from SHA-256(ed25519 \|\| ml_dsa_65). | | `HybridKeypairFromSeeds(edSeed, mlSeed) -> (pub, priv)` | Deterministic keygen from two 32-byte seeds. | | `GenerateHybridKeypair() -> (pub, priv)` | Random hybrid keypair from OS RNG. | +| `GenerateChallenge() -> []byte` | Cryptographically random 32-byte challenge. | | `DelegationSignBytes(cert) -> []byte` | Canonical signable bytes for a cert. | | `ChallengeSignBytes(challenge, ts) -> []byte` | Raw binary `challenge \|\| BE u64(ts)`. | | `ChallengeSignBytesWithSessionContext(challenge, ts, sessionContext) -> []byte` | v1.1 session-bound `challenge \|\| BE u64(ts) \|\| session_context`; SDKs may expose this as an optional argument where idiomatic. | | `ChallengeSignBytesWithStream(challenge, ts, sessionContext, streamID, streamSeq) -> []byte` | v1.1 stream-bound challenge bytes with optional session context plus `stream_id` and `stream_seq`. | +| `OperationContextBytes(ctx) -> []byte` | alpha.16 operation-context preimage (§6.4.9): required scope, operation, resource ID, requested path, payload digest. | +| `OperationContextHash(ctx) -> []byte` | 32-byte `request_hash` over the operation-context bytes. | +| `SessionContextBytes(inputs) -> []byte` | alpha.16 session-context preimage (§6.4.9): verifier/workspace/agent/session/invocation IDs plus the 32-byte `request_hash`. | +| `BuildSessionContext(inputs) -> []byte` | 32-byte `session_context` over the session-context bytes, ready for `VerifyOptions.SessionContext` and challenge signing. | | `RevocationSignBytes(list) -> []byte` | Canonical signable bytes for a revocation list. | | `KeyRotationSignBytes(statement) -> []byte` | Canonical signable bytes for root-key rotation statements. | | `RevocationPushSignBytes(push) -> []byte` | Canonical signable bytes for revocation push notifications. | @@ -169,9 +152,11 @@ Every implementation MUST export these primitives with equivalent semantics: | `VerifySessionToken(token, secret, now) -> bool/error` | Verifies verifier-local token MAC and validity window. | | `SignTransactionReceiptParty(receipt, partyID, priv) -> ReceiptPartySignature` | Produces one party signature over the canonical receipt signable. | | `VerifyTransactionReceipt(receipt, options) -> TransactionReceiptResult` | Verifies receipt envelope atomicity, party proofs, and party signatures. | +| `VerifyStreamedTurnWithOptions(token, secret, turn, options) -> VerifyResult` | Options-object streamed-turn verification against a verifier-local session token (§5.13). | | `ExpandScopes([]string) -> []string` | Sort the deduplicated expansion. | | `IntersectScopes(lists...) -> []string` | Chain intersection, sorted. | | `HasScope(granted, required) -> bool` | Membership after expansion. | +| `IsSensitive(scope) -> bool` | True if a scope requires explicit grant (never introduced by wildcard expansion). | | `ValidateScopes([]string) -> error?` | Reject unknown. | | `Verify(bundle, options) -> VerifyResult` | The full verifier algorithm (§10 of SPEC). | @@ -183,31 +168,23 @@ Naming conventions and capitalization follow the idioms of each language (`camel |---|---|---| | Go | stdlib `crypto/ed25519` | `github.com/cloudflare/circl/sign/mldsa/mldsa65` | | TypeScript | `@noble/ed25519` | `@noble/post-quantum` (ml-dsa-65) | -| Python | `cryptography` or `pynacl` | `dilithium-py`, `pqcrypto`, or liboqs-python | -| Rust | `ed25519-dalek` | `pqcrypto-mldsa` or `oqs-rs` | +| Python | `cryptography` (shipped SDK) | `pqcrypto` (shipped SDK) | +| Rust | `ed25519-dalek` (shipped SDK) | `fips204` (shipped SDK) | | Swift | Apple `CryptoKit` | liboqs-swift wrapper (or port) | | Java / Kotlin | Bouncy Castle | Bouncy Castle (ML-DSA support is current as of BC 1.78+) | -| C | libsodium | liboqs | +| C | `ed25519-dalek` via the Rust SDK | `fips204` via the Rust SDK | SDK authors MUST use audited, mainstream implementations. Rolling your own Ed25519 or ML-DSA-65 is not acceptable for a Ratify SDK. -## 5. Interop matrix +## 5. Interop -As more implementations ship, we maintain a cross-implementation interop matrix in CI. Every (signer, verifier) pair runs the full fixture suite: +Interop is proven through a hub-and-spoke corpus, not an N×N grid. -``` - verifier → -signer ↓ Go TS Python Rust ... - Go ✅ ✅ [soon] [soon] - TS ✅ ✅ [soon] [soon] - Python [soon] [soon] ✅ [soon] - Rust [soon] [soon] [soon] ✅ - ... -``` +The Go reference implementation generates a byte-equivalence corpus (`testvectors/v1/cross_sdk_vectors.json`) covering the canonical hashing and signable-bytes constructions (`verifier_context_hash`, `bundle_hash`, `policy_verdict_sign_bytes`, `verification_receipt_sign_bytes`). The TypeScript, Python, and Rust SDKs each load that corpus and assert byte-identical output against the Go reference. Because all three match the same reference bytes, they are transitively byte-identical to Go and to one another, without maintaining a quadratic set of pairwise assertions. -Any red cell means two implementations have drifted. Drift is always a bug in at least one of them, not a spec ambiguity — the test vectors are the spec. +On top of the corpus, all five SDKs (Go, TypeScript, Python, Rust, and C) load the 79 canonical fixtures at `testvectors/v1/` and execute each fixture through the API appropriate to its kind, checking the expected result, which gives 79 × 5 fixture executions across the five SDKs. Of the 79, 62 exercise bundle verification; the rest exercise the scope, session-token, transaction-receipt, key-rotation, revocation, and witness APIs. The C SDK proves conformance through those 79 fixtures; it does not consume the cross-SDK byte-equivalence corpus. -When a new SDK PR is opened, CI runs all existing implementations as verifiers against bundles produced by the new one, and the new one as a verifier against all existing implementations' bundles. 79 × (signer_count) × (verifier_count) total assertions per CI run at full matrix. +Any divergence from the Go reference bytes is canonical-serialization drift: a bug in the diverging implementation, not a spec ambiguity. The reference bytes are the spec in runnable form. ## 6. Contributing a new SDK @@ -227,7 +204,7 @@ The `sdks/typescript/` directory is the reference template for what a mature SDK SDKs MAY live in this monorepo under `sdks//` (the recommended path for actively-maintained implementations), OR in their own repositories (if the maintainer prefers independent release cadence). Either is conformant as long as the fixture contract is met on every release. -Package names SHOULD follow the pattern `@identities-ai/ratify-protocol` (JS scope), `identitiesai-ratify-protocol` (Python/PyPI), `ratify-protocol` (Rust crate), etc. Namespace squatting or confusingly-similar names on public registries are not acceptable. +Package names SHOULD follow the pattern `@identities-ai/ratify-protocol` (JS scope), `ratify-protocol` (Python/PyPI), `ratify-protocol` (Rust crate), etc. Namespace squatting or confusingly-similar names on public registries are not acceptable. When transfer to a foundation (Linux Foundation, OpenSSF, etc.) happens in the future, SDK trademarks follow the protocol's naming convention and ownership moves accordingly. diff --git a/docs/TESTING.md b/docs/TESTING.md index a9ffd2b..63ced6f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -34,7 +34,7 @@ cd sdks/python && python -m venv .venv && source .venv/bin/activate && \ cd sdks/rust && cargo test --quiet && cd ../.. # C / C++ -cd sdks/c && cargo test --test conformance -- --nocapture && cargo test --test api && cd ../.. +cd sdks/c && cargo test --test conformance -- --nocapture && cargo test --test api && cargo test --test advanced && cargo test --test bounds && cd ../.. ``` **Pass criterion:** all five print green, all 79 fixtures pass in every language. @@ -60,7 +60,7 @@ cd demos/typescript && npm install && npm run demo && cd ../.. cd demos/rust && cargo run && cd ../.. ``` -**Pass criterion:** `STEP 5` prints `✅ VALID` with the correct human/agent IDs and granted scope. Each `ATTACK N` prints `❌ REJECTED` with the specific deterministic reason. Output is essentially identical across all five languages (only random IDs differ). +**Pass criterion:** `STEP 5` prints `✅ VALID` with the correct human/agent IDs and granted scope. Each `ATTACK N` prints `❌ REJECTED` with the specific deterministic reason. Output is essentially identical across all four narrative demos (Go, TypeScript, Python, Rust; only random IDs differ). C is exercised through the conformance fixtures, not a narrative demo. --- @@ -196,7 +196,7 @@ curl -s -X POST http://localhost:8080/v1/ratify/verify \ - **Deployment / scale.** No production load testing yet. Allocated for `TEST_PLAN.md` §8 with k6/vegeta; gated on first production deployment. - **Adversarial security audit.** External audit (Trail of Bits / NCC / Cure53) scheduled before v1.0.0 stable — see `RELEASES.md`. - **Interop with third-party agent platforms.** Third-party platform integrations are gated on design partner engagement; meanwhile, cross-language interop across our five SDKs is a reasonable proxy. -- **Long-term session / streaming properties.** See `ROADMAP.md` §2 for v1.1 gaps (session binding, sequence numbers, session cert cache). +- **Long-term session / streaming properties.** Session binding, sequence numbers, and the session cert cache are shipped v1.1 capabilities (see `ROADMAP.md`, Shipped); production-scale streaming behavior over long-lived sessions is exercised in pilots, not here. --- @@ -209,7 +209,7 @@ go test ./... && \ cd sdks/typescript && npm test && cd ../.. && \ cd sdks/python && source .venv/bin/activate && pytest -q && deactivate && cd ../.. && \ cd sdks/rust && cargo test --quiet && cd ../.. && \ - cd sdks/c && cargo test --test conformance -- --nocapture && cargo test --test api && cd ../.. + cd sdks/c && cargo test --test conformance -- --nocapture && cargo test --test api && cargo test --test advanced && cargo test --test bounds && cd ../.. ``` If all five are green, your change doesn't drift the protocol. That's the contract. diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md index 8ad2d32..ae4cb6d 100644 --- a/docs/TEST_PLAN.md +++ b/docs/TEST_PLAN.md @@ -2,7 +2,7 @@ **Companion to [`SPEC.md`](../SPEC.md) and [`SDKS.md`](SDKS.md). Defines how Ratify v1 is validated — from unit tests through external audit, and how every new language SDK proves it is conformant with the reference.** -**Last updated:** 2026-04-18 +**Last updated:** 2026-07-31 **Scope:** Ratify Protocol v1 (hybrid Ed25519 + ML-DSA-65 delegation, JSON wire format) --- @@ -20,7 +20,7 @@ ## Layer 1 — Unit Tests (Go) -Location: `ratify/ratify_test.go` (6 tests shipped; this plan expands to ~35). +Location: `ratify_test.go` (at the repo root; the "6 tests" figure below is planning-era, and this plan expands to ~35). ### 1.1 Happy path — extend current `TestDelegationRoundTrip` @@ -74,7 +74,7 @@ Location: `ratify/ratify_test.go` (6 tests shipped; this plan expands to ~35). - `TestScopeRejection` (current) - `TestScopeWildcard` (current — meeting:* expansion) -- All four wildcard expansions (`meeting:*`, `comms:*`, `comms:message:*`, `comms:email:*`) +- The four comms/meeting wildcard expansions (`meeting:*`, `comms:*`, `comms:message:*`, `comms:email:*`), a subset of the 14 wildcards in the vocabulary - Sensitive scope in a wildcard (must be rejected — `meeting:record` must not ride `meeting:*`) - Unknown scope string (`ValidateScopes` rejects) - Empty scope list with non-empty required scope (reject) @@ -88,8 +88,10 @@ Location: `ratify/ratify_test.go` (6 tests shipped; this plan expands to ~35). ### 1.9 DeriveID +`DeriveID(HybridPublicKey) string` returns `hex(SHA-256(ed25519_pub || ml_dsa_65_pub)[:16])` (SPEC §7). + - Same pubkey → same ID -- 32-byte input → 32-char hex output (16 bytes of SHA-256) +- `HybridPublicKey` input (32-byte Ed25519 || 1952-byte ML-DSA-65) → 32-char hex output (first 16 bytes of the SHA-256 digest) - Different pubkeys → different IDs (basic collision sanity) --- @@ -132,7 +134,7 @@ Every output of `ExpandScopes` must satisfy `ValidateScopes` without error. This is the single highest-leverage test artifact for the open-source launch. Without cross-language test vectors, no JS or Python implementation can be verified correct. -**Status:** ✅ Implemented on `main` — **79 fixtures** generated and committed at `testvectors/v1/*.json`. Generator: `cmd/ratify-testvectors/main.go`. Conformance test: `TestConformanceVectors` in `ratify_test.go` loads every fixture and validates `Verify()` output; mirrored in each SDK's conformance harness (TS / Python / Rust). The v1.1 fixtures are not part of a public protocol tag until the next release. +**Status:** ✅ Implemented on `main` — **79 fixtures** generated and committed at `testvectors/v1/*.json`. Generator: `cmd/ratify-testvectors/main.go`. Conformance test: `TestConformanceVectors` in `ratify_test.go` loads every fixture and validates `Verify()` output; mirrored in each SDK's conformance harness (TS / Python / Rust / C). The v1.1 fixtures are not part of a public protocol tag until the next release. ### 3.1 Location @@ -270,6 +272,48 @@ All **79 fixtures** present, generated deterministically, and passing conformanc | `witness_entry_valid` | Signed witness log entry verifies against witness key. | | `reject_challenge_forwarding` | Session-context verifier binding rejects forwarded challenges. | +**Hybrid single-component and scope-poisoning (3 fixtures, all verify kind)** + +| Name | Purpose | +|---|---| +| `reject_ed25519_only_corrupted` | Valid ML-DSA-65 but corrupted Ed25519 component; both components MUST verify, so rejected with `bad_signature`. | +| `reject_mldsa65_only_corrupted` | Valid Ed25519 but corrupted ML-DSA-65 component; the post-quantum half must also verify, so rejected with `bad_signature`. | +| `reject_unknown_scope_cert` | Signed cert grants a scope outside the vocabulary; rejected as malformed with `invalid_scope` before any effective-scope arithmetic (verify-layer counterpart of `reject_unknown_scope`). | + +**Alpha.12 additions (3 fixtures, all verify kind)** + +| Name | Purpose | +|---|---| +| `no_expiry_cert` | No-expiry sentinel `expires_at = 4070908799` verifies normally; MUST NOT be displayed or policy-evaluated as a real 2099 expiry (SPEC §5.7). | +| `presence_represent_allowed` | `presence:represent` (sensitive) granted explicitly alongside `identity:prove`; no scope implication (SPEC §9.1). | +| `reject_presence_sensitive_wildcard` | No `presence:*` wildcard exists; a sensitive scope is never introduced by wildcard expansion, so `presence:*` is rejected as `invalid_scope`. | + +**Resource-bound authority (`resource_path`), 14 fixtures (alpha.16, all verify kind)** + +| Name | Purpose | +|---|---| +| `constraint_resource_path_exact_accept` | Byte-equal prefix match on the same resource accepts. | +| `constraint_resource_path_child_accept` | Prefix `/docs` authorizes any path at or below it under segment-boundary matching. | +| `constraint_resource_path_child_broader_accept` | A broader child prefix cannot widen authority, but the pair is satisfiable; request under both prefixes accepts. | +| `constraint_resource_path_root_prefix_accept` | Root prefix `/` matches every valid path on the named resource. | +| `constraint_resource_path_whole_resource_accept` | Absent `path_prefix` authorizes the entire named resource (absence, not empty string). | +| `constraint_resource_path_trailing_slash_accept` | Trailing slash trimmed before comparison (except root); `/docs/` and `/docs` match. | +| `constraint_resource_path_percent_literal_accept` | No percent-decoding; `%2e%2e` is a literal segment, at or below the prefix, accepts. | +| `constraint_resource_path_chain_narrowing_accept` | Conjunctive across the chain; nested prefixes reduce to the narrowest under AND. | +| `constraint_resource_path_traversal_denied` | Dot-segment in the requested path is rejected outright as `constraint_denied`. | +| `constraint_resource_path_textual_prefix_denied` | `/docs-old` is a different segment from `/docs`; segment-boundary matching, `constraint_denied`. | +| `constraint_resource_path_wrong_repo_denied` | Different `resource_id` (exact byte equality); `constraint_denied`. | +| `constraint_resource_path_downstream_escape_denied` | Child claims a broader prefix; parent constraint still evaluates, so escape fails `constraint_denied`. | +| `constraint_resource_path_unsatisfiable_pair_denied` | Two constraints naming different resources are jointly unsatisfiable; fails closed as `constraint_denied`. | +| `constraint_resource_path_missing_context` | Constraint present but no resource context supplied; `constraint_unverifiable` (distinct from `constraint_denied`). | + +**Deeper chains and extension constraints (2 fixtures, alpha.16, all verify kind)** + +| Name | Purpose | +|---|---| +| `chain_depth_8_accept` | Well-formed eight-cert chain at exactly `MaxDelegationChainDepth=8` (raised from 3); verifies as `authorized_agent`. | +| `constraint_ext_params_unknown_denied` | Extension constraint carrying a `params` object inside the signed bytes; verifier with no evaluator fails closed with `constraint_unknown`. | + ### 3.4 Test vector generator `cmd/ratify-testvectors/main.go` — regenerates all vectors from fixed 32-byte seeds (`0x01…` for human root, `0x02…` for agent, etc.). Timestamps are fixed (`1800000000` = 2027-01-15 UTC). Challenges are SHA-256 of the fixture name. **Determinism is a required property:** `go run ./cmd/ratify-testvectors` produces byte-identical output to committed fixtures; any drift fails the conformance test. @@ -283,40 +327,41 @@ go test -run TestConformanceVectors ./... ### 3.5 Cross-language harness -`testvectors/run.sh` accepts a language binary (go, js, py) and runs every vector through it, comparing outputs. Part of the open-source repo. +There is no single driver script. Each SDK owns its conformance harness and loads the fixtures directly from `testvectors/v1/`: + +- Go: `TestConformanceVectors` in `ratify_test.go`. +- TypeScript: `sdks/typescript/test/conformance.test.ts`. +- Python: `sdks/python/tests/test_conformance.py`. +- Rust: `sdks/rust/tests/conformance.rs`. +- C: `sdks/c/tests/conformance.rs` (through the C ABI). + +Byte-level cross-language equivalence is proven separately by the hub-and-spoke corpus `testvectors/v1/cross_sdk_vectors.json`: Go generates the reference bytes, and TypeScript, Python, and Rust each assert byte-identity against them (`test/cross_sdk.test.ts`, `tests/test_cross_sdk.py`, `tests/cross_sdk.rs`). See §4. --- ## Layer 4 — Cross-language interop -**Status:** Go ↔ TypeScript ↔ Python ↔ Rust ↔ C all proven. All **79 fixtures** byte-identical across every pairing. - -### 4.1 The NxN conformance matrix +**Status:** All five SDKs (Go, TypeScript, Python, Rust, C) pass the 79 canonical fixtures. Byte-level equivalence is proven for Go, TypeScript, Python, and Rust through the hub-and-spoke corpus (§4.1). -Every SDK must pass the **79 canonical fixtures** when acting as a verifier against bundles produced by every other SDK (including itself). For N implementations the matrix is NxN: +### 4.1 Cross-language conformance (hub-and-spoke) -| | Go verifier | TS verifier | Python verifier | Rust verifier | C verifier | -|---|---|---|---|---|---| -| **Go signer** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **TS signer** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Python signer** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Rust signer** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **C signer** | ✅ | ✅ | ✅ | ✅ | ✅ | +Two mechanisms together give cross-language assurance: -All five SDKs produce byte-identical canonical JSON and parse each other's fixtures without drift. The fixture count of 79 breaks down by kind as: 62 verify + 2 scope + 5 session-token + 5 transaction-receipt + 2 key-rotation + 1 revocation-list + 1 revocation-push + 1 witness-entry. Alpha.16 added 16 verify-kind fixtures (14 resource_path, 1 extension-params, 1 depth-8). +1. **Shared fixtures (all five SDKs).** Go, TypeScript, Python, Rust, and C each load the **79 canonical fixtures** at `testvectors/v1/` and assert that, for every one of the 79 fixtures, executing it through the API appropriate to its kind yields the expected result (62 of the 79 exercise bundle verification; the rest exercise the scope, session-token, transaction-receipt, key-rotation, revocation, and witness APIs). The fixture count of 79 breaks down by kind as: 62 verify + 2 scope + 5 session-token + 5 transaction-receipt + 2 key-rotation + 1 revocation-list + 1 revocation-push + 1 witness-entry. Alpha.16 added 16 verify-kind fixtures (14 resource_path, 1 extension-params, 1 depth-8). +2. **Byte-equivalence corpus (hub-and-spoke, four SDKs).** The Go reference generates `testvectors/v1/cross_sdk_vectors.json` (canonical hashing and signable-bytes constructions). TypeScript, Python, and Rust each assert byte-identical output against the Go reference. Matching a single reference transitively proves the four are pairwise byte-identical without an N×N grid of assertions. C validates through the shared fixtures only; it does not consume this corpus. -Each cell assertion: *given a signer in language A and a verifier in language B, for every one of the 79 fixtures, the verifier's `VerifyResult` matches the fixture's expected result byte-for-byte.* Any failure is canonical-serialization drift — the fix is always to make the two implementations produce identical signable bytes. +Any divergence from the Go reference bytes is canonical-serialization drift: a bug in the diverging implementation, and the fix is always to make it produce identical signable bytes. ### 4.2 The single-component tamper test Hybrid signatures introduce a new failure mode: a bundle where the Ed25519 component is valid but the ML-DSA-65 component is tampered (or vice versa). The fixture `reject_bad_challenge_sig` flips the last byte of both components; the verifier rejects with `bad_challenge_sig`. Every SDK MUST also pass targeted tests where: -- Only the Ed25519 component of `cert.signature` is tampered → verifier rejects with "Ed25519 signature invalid". -- Only the ML-DSA-65 component of `cert.signature` is tampered → verifier rejects with "ML-DSA-65 signature invalid". +- Only the Ed25519 component of `cert.signature` is tampered → verifier rejects with `bad_signature: cert 0: Ed25519 signature invalid`. +- Only the ML-DSA-65 component of `cert.signature` is tampered → verifier rejects with `bad_signature: cert 0: ML-DSA-65 signature invalid`. - Only the Ed25519 component of `challenge_sig` is tampered → verifier rejects. - Only the ML-DSA-65 component of `challenge_sig` is tampered → verifier rejects. -These tests are not yet canonical fixtures but SHOULD be added to each SDK's local test suite. A future v1.x fixture expansion should add these as shipped fixtures. +The two `cert.signature` cases now ship as canonical fixtures (`reject_ed25519_only_corrupted`, `reject_mldsa65_only_corrupted`), so every SDK exercises them through the shared vector set. The two `challenge_sig` cases remain SDK-local tests; a future fixture expansion should promote them to shipped fixtures. ### 4.3 Determinism regression test @@ -331,12 +376,16 @@ diff -rq testvectors/v1/ /tmp/regen/ # MUST be empty The `.github/workflows/ci.yml` in this repo runs the following on every push and PR: -- Go vet + go test. -- Determinism check (generator rerun + diff). -- TypeScript typecheck + conformance suite. -- DCO sign-off enforcement on all commits. +- Go: `go vet` + `go test -race` + `go mod tidy` cleanliness. +- Test-vector determinism (generator rerun + `diff` against committed fixtures). +- Release-metadata sync check. +- TypeScript: typecheck + full suite (conformance + cross-SDK corpus + levers + providers). +- Python: clean-venv install + `pqcrypto` import check + pytest (79 fixtures + cross-SDK corpus + levers). +- Rust: build + `clippy -D warnings` + `cargo test` (conformance + providers + levers + cross-SDK corpus). +- C: build + `clippy -D warnings` + conformance (79) + api (44) + advanced (33) + bounds (7). +- DCO sign-off enforcement on all non-merge commits (pull requests). -When Python / Rust / other SDKs land, their CI jobs append to the same workflow, and cross-implementation assertions expand to fill the NxN matrix above. +New SDK jobs append to the same workflow and adopt the same two-mechanism check: the shared fixtures for every SDK, plus the hub-and-spoke byte-equivalence corpus where the SDK consumes it. --- @@ -520,7 +569,7 @@ The `ratify_verification_log` table already exists. Build dashboards from it. - **Cert age distribution:** IssuedAt → verification time - **Revocation hit rate:** % of verifies that hit a revoked cert - **Challenge-to-verify latency:** time from challenge issuance to verified bundle -- **Chain depth distribution:** % at depth 1, 2, 3 +- **Chain depth distribution:** % by depth bucket (1, 2, 3, 4 through 8) ### 11.2 Alerts diff --git a/docs/TRANSACTION_RECEIPTS.md b/docs/TRANSACTION_RECEIPTS.md index f59fc2b..f913585 100644 --- a/docs/TRANSACTION_RECEIPTS.md +++ b/docs/TRANSACTION_RECEIPTS.md @@ -1,6 +1,6 @@ # Transaction Receipt Envelope -**Status:** Normative as of v1.0.0-alpha.5 (SPEC §5.14, §6.4.7). Implemented in Go, TypeScript, Python, Rust, and C/C++. Five canonical fixtures prove the envelope and tamper cases. +**Status:** Normative as of v1.0.0-alpha.6 (SPEC §5.14, §6.4.7). Implemented in Go, TypeScript, Python, Rust, and C/C++. Five canonical fixtures prove the envelope and tamper cases. Ratify v1 already gives each party a `ProofBundle` that proves identity, authorization, and freshness. A transaction receipt adds a durable, multi-party artifact that says: these parties committed to the same application-defined terms at the same time, under these Ratify proofs. diff --git a/scripts/test-all.sh b/scripts/test-all.sh index efec572..c23d298 100755 --- a/scripts/test-all.sh +++ b/scripts/test-all.sh @@ -29,6 +29,9 @@ echo "==> Wire transport (TS <-> Python)" echo "==> Rust SDK" (cd sdks/rust && cargo build --all-targets && cargo test) +echo "==> Rust narrative demo (standalone cargo project, not in the SDK workspace)" +(cd demos/rust && RUSTFLAGS="-D warnings" cargo build) + echo "==> C/C++ SDK" (cd sdks/c && cargo test --test conformance -- --nocapture && cargo test --test api && cargo test --test advanced && cargo test --test bounds) diff --git a/sdks/c/README.body.md b/sdks/c/README.body.md index 0f96e78..3046306 100644 --- a/sdks/c/README.body.md +++ b/sdks/c/README.body.md @@ -9,9 +9,10 @@ | A Python script, ML pipeline, data tool | [Python SDK](https://github.com/identities-ai/ratify-protocol/tree/main/sdks/python) | | A Rust service or high-performance binary | [Rust SDK](https://github.com/identities-ai/ratify-protocol/tree/main/sdks/rust) — use directly, no FFI overhead | | **C or C++ code** | **This SDK** | -| **Firmware, RTOS, hardware driver** | **This SDK** (static library, `libratify_c.a`) | +| **RTOS firmware with a std shim** (FreeRTOS, Zephyr) | **This SDK** (requires std/heap; see Embedded deployment guide) | | **A language that FFIs to C** (Swift, Zig, Julia, Lua, etc.) | **This SDK** | -| **Air-gapped embedded device** (no OS, no runtime) | **This SDK** (static library, no OS dependencies) | +| **Air-gapped Linux or RTOS device** | **This SDK** (static library `libratify_c.a`, verifies offline) | +| **Bare-metal / no-OS firmware** (no heap, no std) | [Rust SDK](https://github.com/identities-ai/ratify-protocol/tree/main/sdks/rust) with `no_std` + `alloc` (the C SDK requires std/heap) | The C SDK wraps the Rust SDK via a C ABI. If you're writing Rust, use the Rust SDK directly — the C SDK adds an FFI boundary with no benefit in a Rust context. @@ -86,12 +87,11 @@ library cannot link into bare-metal firmware with no heap at all. Targets suppor | Target | Works? | Notes | |---|---|---| -| Linux (any arch) | ✅ | Raspberry Pi, BeagleBone, embedded Linux SBCs | -| FreeRTOS + std shim | ✅ | Use `cargo-embassy` or `embedded-std` shim | -| Zephyr RTOS | ✅ | Zephyr's Rust support includes std | -| Bare-metal Cortex-M (no OS) | ❌ | No heap, no std — use Rust SDK directly | +| Linux (any arch) | ✅ | Raspberry Pi, BeagleBone, embedded Linux SBCs; built and tested in CI | +| FreeRTOS / Zephyr with a std shim | ⚠️ Unverified | Requires a custom toolchain and sysroot that provide a Rust `std` distribution (the crate imports `std` unconditionally). No reproducible RTOS build is gated yet, so treat this as unverified until you complete a target integration build | +| Bare-metal (no OS, no heap) | ❌ | No heap, no std; use the Rust SDK directly with `#[no_std]` + `alloc` | -For bare-metal Cortex-M with no OS, use the Rust SDK directly with `#[no_std]` + `alloc`. +RTOS targets are not covered by CI. Before relying on one, stand up the custom toolchain and run a full integration build; the tables below list the target triples but do not imply a gated, reproducible build for the non-Linux entries. ## Supported targets @@ -100,7 +100,7 @@ For bare-metal Cortex-M with no OS, use the Rust SDK directly with `#[no_std]` + | x86-64 | `x86_64-unknown-linux-gnu` | Intel/AMD server, Linux PC | | ARM64 | `aarch64-unknown-linux-gnu` | Raspberry Pi 4, embedded Linux, Apple Silicon | | ARM32 | `armv7-unknown-linux-gnueabihf` | Raspberry Pi 2/3, older embedded Linux | -| ARM Cortex-M4/M7 | `thumbv7em-none-eabihf` | STM32, NXP — FreeRTOS, Zephyr | +| ARM Cortex-M4/M7 (RTOS, unverified) | `thumbv7em-none-eabihf` is a bare `none` triple with no upstream Rust `std`; an RTOS build needs a custom `std`-providing toolchain and is not gated in CI | STM32, NXP running FreeRTOS/Zephyr | | x86-32 | `i686-unknown-linux-gnu` | Legacy industrial, 32-bit Linux | | RISC-V 64 | `riscv64gc-unknown-linux-gnu` | SiFive, emerging IoT | | macOS ARM64 | `aarch64-apple-darwin` | Apple Silicon Mac | @@ -135,10 +135,6 @@ cross build --release --target aarch64-unknown-linux-gnu # ARM32 (Raspberry Pi 2/3) cross build --release --target armv7-unknown-linux-gnueabihf -# ARM Cortex-M4 bare metal (no OS) -rustup target add thumbv7em-none-eabihf -cargo build --release --target thumbv7em-none-eabihf - # RISC-V 64 cross build --release --target riscv64gc-unknown-linux-gnu ``` @@ -239,7 +235,13 @@ public: ## API overview -### Key generation (infallible) +### Key generation + +Key generation returns a `RatifyStatus` (check it for null-pointer and argument +errors). On a standard OS target it has no recoverable failure mode, because the +operating system supplies entropy. It is not truly infallible on embedded targets: +if no entropy source is configured, the library halts (panics) rather than +returning an error. See the Embedded deployment guide. ```c // Generate a HumanRoot keypair (the delegating principal) @@ -263,8 +265,8 @@ ratify_string_free(agent_id); RatifyDelegationCert *cert = NULL; char *err = NULL; -// Delegate "physical:enter" to the agent; 0 = no expiry (signs as 2099-12-31) -ratify_delegation_issue(root, agent, "[\"physical:enter\"]", 0, &cert, &err); +// Delegate "physical:enter" to the agent; expires_at 0 = no expiry (until revoked) +ratify_delegation_issue(root, agent, "[\"physical:enter\"]", 0 /* issued_at */, 0 /* expires_at: 0 = no-expiry sentinel */, &cert, &err); char *cert_json = ratify_delegation_cert_to_json(cert, &err); // Send cert_json to the agent over your transport layer @@ -276,11 +278,11 @@ ratify_string_free(cert_json); ```c // Agent side: receive the cert JSON and a fresh challenge uint8_t challenge[32]; -ratify_challenge_generate(challenge); +ratify_challenge_generate(challenge, 32); RatifyProofBundle *bundle = NULL; int64_t now = (int64_t)time(NULL); -ratify_proof_bundle_create(agent, cert_json, challenge, now, &bundle, &err); +ratify_proof_bundle_create(agent, cert_json, challenge, 32, now, &bundle, &err); char *bundle_json = ratify_proof_bundle_to_json(bundle, &err); // Send bundle_json to the verifier @@ -338,6 +340,7 @@ The core paths above cover one-shot verification. The full v1.1 surface is expor | Family | Symbols | What it does | |---|---|---| +| Resource-path verification | `ratify_verify_bundle_opts_v2` (`RatifyResourceContext`) | Carries the `requested_resource_id` / `requested_path` needed to evaluate a `resource_path` constraint (SPEC §5.16); `ratify_verify_bundle_opts` remains the non-resource path | | Session tokens | `ratify_session_token_issue` / `_verify` / `_to_json` / `_from_json` | The streamed fast path: verify the chain once, verify each turn against an HMAC token | | Streamed verification | `ratify_verify_streamed_turn_opts` | Per-turn verification with required scope, single-use challenges, and session/stream binding enforcement (`RatifyStreamedVerifyOptions`) | | Challenge store | `ratify_challenge_store_new` / `_issue` / `_check` / `_consume` | Single-use challenge acceptance (SPEC §10); `ratify_verify_bundle_opts_with_challenge_store` wires it into full verification | @@ -347,6 +350,49 @@ The core paths above cover one-shot verification. The full v1.1 surface is expor | Multi-party + audit | `ratify_transaction_receipt_*`, `ratify_witness_entry_*`, `ratify_key_rotation_*` | Transaction receipts (§5.14), witness logs (§5.12), key rotation (§5.15) | | Scope utilities | `ratify_scope_*`, `ratify_scopes_*`, `ratify_scope_vocabulary` | Expansion, intersection, validation, and vocabulary discovery | +### Available on main; ships in alpha.16 (release unpublished) + +The following are merged to `main` and covered by the test suites now. They ship +in alpha.16, which is not yet published. Every symbol below is declared +in `include/ratify.h`. The call snippets in this section are abbreviated and omit +the `RatifyStatus` error checking shown in the complete example above; check every +returned status in real code. + +- **Resource-bound verification.** `ratify_verify_bundle_opts_v2` takes a + `const RatifyResourceContext *` (fields `requested_resource_id` and + `requested_path`) alongside `RatifyVerifyOptions`, supplying the context a + `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) needs; the + verifier context fields are defined in SPEC §5.16. + Passing `NULL` for `resource` is equivalent to `ratify_verify_bundle_opts`; a + bundle whose cert bears a `resource_path` constraint verified without this + context fails closed as `constraint_unverifiable`. +- **Streamed-turn verify options.** `ratify_verify_streamed_turn_opts` with + `RatifyStreamedVerifyOptions` (`required_scope`, `now_unix`, `session_context` + / `session_context_len`, `stream`) enforces required scope, single-use + challenges (via a `RatifyChallengeStore`), and session/stream binding on each + turn. It supersedes the deprecated positional `ratify_verify_streamed_turn`. +- **Operation and session context.** `ratify_operation_context_hash` and + `ratify_session_context_build` produce the canonical SPEC §6.4.9 32-byte + hashes for operation and session binding (and the Middleware Custody Profile, + SPEC §15.2.1). +- **VerificationReceipt wire codecs.** `ratify_receipt_to_json` and + `ratify_receipt_from_json` round-trip a `RatifyReceipt` through the strict JSON + wire form (a decoded receipt carrying an unknown `identity_status` is + rejected); `ratify_receipt_hash` computes its canonical 32-byte chain hash. +- **Extension-constraint params.** Certs may carry extension constraints with + typed parameters. These evaluate through the normal verify path and route to + `constraint_unknown` when the constraint type is unrecognized (no dedicated C + symbol). +- **Chain-depth ceiling.** The maximum delegation-chain depth is now 8 (raised + from 3); a deeper chain fails closed as `invalid`, with `error_reason` + beginning `chain_too_deep`. +- **Input bounds (SPEC §5.1).** Enforced before signature work. + `MAX_PROOF_BUNDLE_BYTES` (131072, i.e. 128 KiB) is checked before parsing, and + an oversized payload yields an `invalid` result rather than being parsed. + Further limits cap scopes per cert (128), constraints per cert (32), scope + length (256 bytes), identifier length (512 bytes), agent-name length (256 + bytes), and JSON nesting depth (16). + ### Memory management Every function that returns a heap-allocated value documents which `_free` @@ -378,16 +424,19 @@ ratify_error_free(err); // for err_out parameters | `identity_status` | Meaning | |---|---| -| `authorized_agent` | Valid — agent is authorized | +| `authorized_agent` | Valid: agent is authorized | | `expired` | Delegation cert has expired | -| `revoked` | Cert was revoked by the RevocationProvider | +| `revoked` | Cert was revoked (revocation callback or signed revocation list) | | `scope_denied` | Required scope not in the effective delegation | -| `constraint_denied` | A constraint (geo, speed, amount) was violated | +| `constraint_denied` | A constraint (geo, time, speed, amount, rate, resource path) was violated | | `constraint_unverifiable` | Constraint present but no context to evaluate it | | `constraint_unknown` | Unknown constraint type | -| `delegation_not_authorized` | Chain depth / signing authority violation | -| `invalid` | Generic failure (tampered bundle, bad signature, lookup error) | -| `unauthorized` | Challenge freshness or session binding failure | +| `delegation_not_authorized` | A non-root issuer lacks `identity:delegate` (signing-authority violation) | +| `invalid_scope` | A cert grants a scope that is not canonical, not a wildcard, and not a `custom:` extension | +| `invalid` | Generic failure: tampered bundle, bad signature, a chain deeper than the depth ceiling (`error_reason` begins `chain_too_deep`), stale or unknown challenge, session/stream binding failure, or a revocation lookup error | + +`verified_human` and `unauthorized` are part of the status vocabulary but are not +emitted by proof-bundle verification through this SDK. --- @@ -413,9 +462,16 @@ All canonical fixture kinds pass through the C ABI: ## Testing ```bash -# Unit tests (all functions, null pointers, malformed JSON, round-trips) +# Unit tests (all functions, null pointers, malformed JSON, round-trips; 44 tests) cargo test --test api +# Advanced surface (session tokens, streaming, challenge store, receipts, +# revocation, witness/key-rotation, scopes, policy verdicts, context builders; 33 tests) +cargo test --test advanced + +# Protocol input bounds (max scopes, constraints, lengths, JSON nesting, bundle bytes; 7 tests) +cargo test --test bounds + # Conformance tests (79/79 fixtures) cargo test --test conformance diff --git a/sdks/c/README.md b/sdks/c/README.md index c60dfac..0f23cc3 100644 --- a/sdks/c/README.md +++ b/sdks/c/README.md @@ -32,9 +32,10 @@ Beyond the one-shot delegate → present → verify round trip, this SDK impleme | A Python script, ML pipeline, data tool | [Python SDK](https://github.com/identities-ai/ratify-protocol/tree/main/sdks/python) | | A Rust service or high-performance binary | [Rust SDK](https://github.com/identities-ai/ratify-protocol/tree/main/sdks/rust) — use directly, no FFI overhead | | **C or C++ code** | **This SDK** | -| **Firmware, RTOS, hardware driver** | **This SDK** (static library, `libratify_c.a`) | +| **RTOS firmware with a std shim** (FreeRTOS, Zephyr) | **This SDK** (requires std/heap; see Embedded deployment guide) | | **A language that FFIs to C** (Swift, Zig, Julia, Lua, etc.) | **This SDK** | -| **Air-gapped embedded device** (no OS, no runtime) | **This SDK** (static library, no OS dependencies) | +| **Air-gapped Linux or RTOS device** | **This SDK** (static library `libratify_c.a`, verifies offline) | +| **Bare-metal / no-OS firmware** (no heap, no std) | [Rust SDK](https://github.com/identities-ai/ratify-protocol/tree/main/sdks/rust) with `no_std` + `alloc` (the C SDK requires std/heap) | The C SDK wraps the Rust SDK via a C ABI. If you're writing Rust, use the Rust SDK directly — the C SDK adds an FFI boundary with no benefit in a Rust context. @@ -109,12 +110,11 @@ library cannot link into bare-metal firmware with no heap at all. Targets suppor | Target | Works? | Notes | |---|---|---| -| Linux (any arch) | ✅ | Raspberry Pi, BeagleBone, embedded Linux SBCs | -| FreeRTOS + std shim | ✅ | Use `cargo-embassy` or `embedded-std` shim | -| Zephyr RTOS | ✅ | Zephyr's Rust support includes std | -| Bare-metal Cortex-M (no OS) | ❌ | No heap, no std — use Rust SDK directly | +| Linux (any arch) | ✅ | Raspberry Pi, BeagleBone, embedded Linux SBCs; built and tested in CI | +| FreeRTOS / Zephyr with a std shim | ⚠️ Unverified | Requires a custom toolchain and sysroot that provide a Rust `std` distribution (the crate imports `std` unconditionally). No reproducible RTOS build is gated yet, so treat this as unverified until you complete a target integration build | +| Bare-metal (no OS, no heap) | ❌ | No heap, no std; use the Rust SDK directly with `#[no_std]` + `alloc` | -For bare-metal Cortex-M with no OS, use the Rust SDK directly with `#[no_std]` + `alloc`. +RTOS targets are not covered by CI. Before relying on one, stand up the custom toolchain and run a full integration build; the tables below list the target triples but do not imply a gated, reproducible build for the non-Linux entries. ## Supported targets @@ -123,7 +123,7 @@ For bare-metal Cortex-M with no OS, use the Rust SDK directly with `#[no_std]` + | x86-64 | `x86_64-unknown-linux-gnu` | Intel/AMD server, Linux PC | | ARM64 | `aarch64-unknown-linux-gnu` | Raspberry Pi 4, embedded Linux, Apple Silicon | | ARM32 | `armv7-unknown-linux-gnueabihf` | Raspberry Pi 2/3, older embedded Linux | -| ARM Cortex-M4/M7 | `thumbv7em-none-eabihf` | STM32, NXP — FreeRTOS, Zephyr | +| ARM Cortex-M4/M7 (RTOS, unverified) | `thumbv7em-none-eabihf` is a bare `none` triple with no upstream Rust `std`; an RTOS build needs a custom `std`-providing toolchain and is not gated in CI | STM32, NXP running FreeRTOS/Zephyr | | x86-32 | `i686-unknown-linux-gnu` | Legacy industrial, 32-bit Linux | | RISC-V 64 | `riscv64gc-unknown-linux-gnu` | SiFive, emerging IoT | | macOS ARM64 | `aarch64-apple-darwin` | Apple Silicon Mac | @@ -158,10 +158,6 @@ cross build --release --target aarch64-unknown-linux-gnu # ARM32 (Raspberry Pi 2/3) cross build --release --target armv7-unknown-linux-gnueabihf -# ARM Cortex-M4 bare metal (no OS) -rustup target add thumbv7em-none-eabihf -cargo build --release --target thumbv7em-none-eabihf - # RISC-V 64 cross build --release --target riscv64gc-unknown-linux-gnu ``` @@ -262,7 +258,13 @@ public: ## API overview -### Key generation (infallible) +### Key generation + +Key generation returns a `RatifyStatus` (check it for null-pointer and argument +errors). On a standard OS target it has no recoverable failure mode, because the +operating system supplies entropy. It is not truly infallible on embedded targets: +if no entropy source is configured, the library halts (panics) rather than +returning an error. See the Embedded deployment guide. ```c // Generate a HumanRoot keypair (the delegating principal) @@ -286,8 +288,8 @@ ratify_string_free(agent_id); RatifyDelegationCert *cert = NULL; char *err = NULL; -// Delegate "physical:enter" to the agent; 0 = no expiry (signs as 2099-12-31) -ratify_delegation_issue(root, agent, "[\"physical:enter\"]", 0, &cert, &err); +// Delegate "physical:enter" to the agent; expires_at 0 = no expiry (until revoked) +ratify_delegation_issue(root, agent, "[\"physical:enter\"]", 0 /* issued_at */, 0 /* expires_at: 0 = no-expiry sentinel */, &cert, &err); char *cert_json = ratify_delegation_cert_to_json(cert, &err); // Send cert_json to the agent over your transport layer @@ -299,11 +301,11 @@ ratify_string_free(cert_json); ```c // Agent side: receive the cert JSON and a fresh challenge uint8_t challenge[32]; -ratify_challenge_generate(challenge); +ratify_challenge_generate(challenge, 32); RatifyProofBundle *bundle = NULL; int64_t now = (int64_t)time(NULL); -ratify_proof_bundle_create(agent, cert_json, challenge, now, &bundle, &err); +ratify_proof_bundle_create(agent, cert_json, challenge, 32, now, &bundle, &err); char *bundle_json = ratify_proof_bundle_to_json(bundle, &err); // Send bundle_json to the verifier @@ -361,6 +363,7 @@ The core paths above cover one-shot verification. The full v1.1 surface is expor | Family | Symbols | What it does | |---|---|---| +| Resource-path verification | `ratify_verify_bundle_opts_v2` (`RatifyResourceContext`) | Carries the `requested_resource_id` / `requested_path` needed to evaluate a `resource_path` constraint (SPEC §5.16); `ratify_verify_bundle_opts` remains the non-resource path | | Session tokens | `ratify_session_token_issue` / `_verify` / `_to_json` / `_from_json` | The streamed fast path: verify the chain once, verify each turn against an HMAC token | | Streamed verification | `ratify_verify_streamed_turn_opts` | Per-turn verification with required scope, single-use challenges, and session/stream binding enforcement (`RatifyStreamedVerifyOptions`) | | Challenge store | `ratify_challenge_store_new` / `_issue` / `_check` / `_consume` | Single-use challenge acceptance (SPEC §10); `ratify_verify_bundle_opts_with_challenge_store` wires it into full verification | @@ -370,6 +373,49 @@ The core paths above cover one-shot verification. The full v1.1 surface is expor | Multi-party + audit | `ratify_transaction_receipt_*`, `ratify_witness_entry_*`, `ratify_key_rotation_*` | Transaction receipts (§5.14), witness logs (§5.12), key rotation (§5.15) | | Scope utilities | `ratify_scope_*`, `ratify_scopes_*`, `ratify_scope_vocabulary` | Expansion, intersection, validation, and vocabulary discovery | +### Available on main; ships in alpha.16 (release unpublished) + +The following are merged to `main` and covered by the test suites now. They ship +in alpha.16, which is not yet published. Every symbol below is declared +in `include/ratify.h`. The call snippets in this section are abbreviated and omit +the `RatifyStatus` error checking shown in the complete example above; check every +returned status in real code. + +- **Resource-bound verification.** `ratify_verify_bundle_opts_v2` takes a + `const RatifyResourceContext *` (fields `requested_resource_id` and + `requested_path`) alongside `RatifyVerifyOptions`, supplying the context a + `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) needs; the + verifier context fields are defined in SPEC §5.16. + Passing `NULL` for `resource` is equivalent to `ratify_verify_bundle_opts`; a + bundle whose cert bears a `resource_path` constraint verified without this + context fails closed as `constraint_unverifiable`. +- **Streamed-turn verify options.** `ratify_verify_streamed_turn_opts` with + `RatifyStreamedVerifyOptions` (`required_scope`, `now_unix`, `session_context` + / `session_context_len`, `stream`) enforces required scope, single-use + challenges (via a `RatifyChallengeStore`), and session/stream binding on each + turn. It supersedes the deprecated positional `ratify_verify_streamed_turn`. +- **Operation and session context.** `ratify_operation_context_hash` and + `ratify_session_context_build` produce the canonical SPEC §6.4.9 32-byte + hashes for operation and session binding (and the Middleware Custody Profile, + SPEC §15.2.1). +- **VerificationReceipt wire codecs.** `ratify_receipt_to_json` and + `ratify_receipt_from_json` round-trip a `RatifyReceipt` through the strict JSON + wire form (a decoded receipt carrying an unknown `identity_status` is + rejected); `ratify_receipt_hash` computes its canonical 32-byte chain hash. +- **Extension-constraint params.** Certs may carry extension constraints with + typed parameters. These evaluate through the normal verify path and route to + `constraint_unknown` when the constraint type is unrecognized (no dedicated C + symbol). +- **Chain-depth ceiling.** The maximum delegation-chain depth is now 8 (raised + from 3); a deeper chain fails closed as `invalid`, with `error_reason` + beginning `chain_too_deep`. +- **Input bounds (SPEC §5.1).** Enforced before signature work. + `MAX_PROOF_BUNDLE_BYTES` (131072, i.e. 128 KiB) is checked before parsing, and + an oversized payload yields an `invalid` result rather than being parsed. + Further limits cap scopes per cert (128), constraints per cert (32), scope + length (256 bytes), identifier length (512 bytes), agent-name length (256 + bytes), and JSON nesting depth (16). + ### Memory management Every function that returns a heap-allocated value documents which `_free` @@ -401,16 +447,19 @@ ratify_error_free(err); // for err_out parameters | `identity_status` | Meaning | |---|---| -| `authorized_agent` | Valid — agent is authorized | +| `authorized_agent` | Valid: agent is authorized | | `expired` | Delegation cert has expired | -| `revoked` | Cert was revoked by the RevocationProvider | +| `revoked` | Cert was revoked (revocation callback or signed revocation list) | | `scope_denied` | Required scope not in the effective delegation | -| `constraint_denied` | A constraint (geo, speed, amount) was violated | +| `constraint_denied` | A constraint (geo, time, speed, amount, rate, resource path) was violated | | `constraint_unverifiable` | Constraint present but no context to evaluate it | | `constraint_unknown` | Unknown constraint type | -| `delegation_not_authorized` | Chain depth / signing authority violation | -| `invalid` | Generic failure (tampered bundle, bad signature, lookup error) | -| `unauthorized` | Challenge freshness or session binding failure | +| `delegation_not_authorized` | A non-root issuer lacks `identity:delegate` (signing-authority violation) | +| `invalid_scope` | A cert grants a scope that is not canonical, not a wildcard, and not a `custom:` extension | +| `invalid` | Generic failure: tampered bundle, bad signature, a chain deeper than the depth ceiling (`error_reason` begins `chain_too_deep`), stale or unknown challenge, session/stream binding failure, or a revocation lookup error | + +`verified_human` and `unauthorized` are part of the status vocabulary but are not +emitted by proof-bundle verification through this SDK. --- @@ -436,9 +485,16 @@ All canonical fixture kinds pass through the C ABI: ## Testing ```bash -# Unit tests (all functions, null pointers, malformed JSON, round-trips) +# Unit tests (all functions, null pointers, malformed JSON, round-trips; 44 tests) cargo test --test api +# Advanced surface (session tokens, streaming, challenge store, receipts, +# revocation, witness/key-rotation, scopes, policy verdicts, context builders; 33 tests) +cargo test --test advanced + +# Protocol input bounds (max scopes, constraints, lengths, JSON nesting, bundle bytes; 7 tests) +cargo test --test bounds + # Conformance tests (79/79 fixtures) cargo test --test conformance diff --git a/sdks/go/README.body.md b/sdks/go/README.body.md index 8206945..30c5a81 100644 --- a/sdks/go/README.body.md +++ b/sdks/go/README.body.md @@ -17,3 +17,15 @@ result := ratify.Verify(&bundle, ratify.VerifyOptions{ **Source:** [`types.go`](https://github.com/identities-ai/ratify-protocol/blob/main/types.go), [`crypto.go`](https://github.com/identities-ai/ratify-protocol/blob/main/crypto.go), [`verify.go`](https://github.com/identities-ai/ratify-protocol/blob/main/verify.go), [`scope.go`](https://github.com/identities-ai/ratify-protocol/blob/main/scope.go), [`constraints.go`](https://github.com/identities-ai/ratify-protocol/blob/main/constraints.go) **Why it's at the root:** Go modules are imported by their module path. Placing the Go code at the root means the import path is simply `github.com/identities-ai/ratify-protocol` — clean and standard. The other SDKs live in `sdks/` because they are independent language implementations with their own package managers (npm, PyPI, crates.io). + +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; to use it ahead of the release, pin to a specific commit (for example `go get github.com/identities-ai/ratify-protocol@`) rather than a bare `main`, which is not a reproducible pin. + +- **Resource-bound verification.** The `resource_path` constraint (`ConstraintResourcePath`, the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via `Constraint.ResourceID` and an optional `Constraint.PathPrefix`. At verify time the application supplies `VerifierContext.RequestedResourceID`, `VerifierContext.RequestedPath`, and `VerifierContext.HasResource` (SPEC §5.16); `Verify(bundle, VerifyOptions{Context: ...})` evaluates them. Helpers: `NormalizeResourcePath`, `ResourcePathMatches`, `ValidateResourceConstraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `OperationContextBytes`, `OperationContextHash`, `SessionContextBytes`, and `BuildSessionContext`; `VerifierContextHash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `EncodeVerificationReceipt` and `DecodeVerificationReceipt`. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `VerifyStreamedTurnWithOptions` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** `Constraint.Params` carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `ValidateParamsValue`. +- **Deeper delegation chains.** `MaxDelegationChainDepth` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MaxProofBundleBytes`, `MaxScopesPerCert`, `MaxConstraintsPerCert`, `MaxScopeLengthBytes`, `MaxIdentifierLengthBytes`, `MaxAgentNameLengthBytes`, `MaxJSONNestingDepth`. diff --git a/sdks/go/README.md b/sdks/go/README.md index 53404c0..cc29589 100644 --- a/sdks/go/README.md +++ b/sdks/go/README.md @@ -40,3 +40,15 @@ result := ratify.Verify(&bundle, ratify.VerifyOptions{ **Source:** [`types.go`](https://github.com/identities-ai/ratify-protocol/blob/main/types.go), [`crypto.go`](https://github.com/identities-ai/ratify-protocol/blob/main/crypto.go), [`verify.go`](https://github.com/identities-ai/ratify-protocol/blob/main/verify.go), [`scope.go`](https://github.com/identities-ai/ratify-protocol/blob/main/scope.go), [`constraints.go`](https://github.com/identities-ai/ratify-protocol/blob/main/constraints.go) **Why it's at the root:** Go modules are imported by their module path. Placing the Go code at the root means the import path is simply `github.com/identities-ai/ratify-protocol` — clean and standard. The other SDKs live in `sdks/` because they are independent language implementations with their own package managers (npm, PyPI, crates.io). + +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; to use it ahead of the release, pin to a specific commit (for example `go get github.com/identities-ai/ratify-protocol@`) rather than a bare `main`, which is not a reproducible pin. + +- **Resource-bound verification.** The `resource_path` constraint (`ConstraintResourcePath`, the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via `Constraint.ResourceID` and an optional `Constraint.PathPrefix`. At verify time the application supplies `VerifierContext.RequestedResourceID`, `VerifierContext.RequestedPath`, and `VerifierContext.HasResource` (SPEC §5.16); `Verify(bundle, VerifyOptions{Context: ...})` evaluates them. Helpers: `NormalizeResourcePath`, `ResourcePathMatches`, `ValidateResourceConstraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `OperationContextBytes`, `OperationContextHash`, `SessionContextBytes`, and `BuildSessionContext`; `VerifierContextHash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `EncodeVerificationReceipt` and `DecodeVerificationReceipt`. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `VerifyStreamedTurnWithOptions` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** `Constraint.Params` carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `ValidateParamsValue`. +- **Deeper delegation chains.** `MaxDelegationChainDepth` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MaxProofBundleBytes`, `MaxScopesPerCert`, `MaxConstraintsPerCert`, `MaxScopeLengthBytes`, `MaxIdentifierLengthBytes`, `MaxAgentNameLengthBytes`, `MaxJSONNestingDepth`. diff --git a/sdks/python/README.body.md b/sdks/python/README.body.md index 8f06678..53ac75c 100644 --- a/sdks/python/README.body.md +++ b/sdks/python/README.body.md @@ -34,7 +34,7 @@ from ratify_protocol import ( DelegationCert, ProofBundle, VerifyOptions, PROTOCOL_VERSION, SCOPE_MEETING_ATTEND, issue_delegation, sign_challenge, generate_challenge, - derive_id, verify_bundle, HybridSignature, + verify_bundle, HybridSignature, ) import time @@ -152,7 +152,7 @@ requests.post("https://verifier.example.com/verify", data=body, ### Receiving a proof bundle ```python -from ratify_protocol import decode_proof_bundle, verify_bundle, VerifyOptions +from ratify_protocol import decode_proof_bundle, verify_bundle, VerifyOptions, SCOPE_MEETING_ATTEND bundle = decode_proof_bundle(request_body) # str or bytes result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_MEETING_ATTEND)) @@ -243,6 +243,18 @@ This SDK uses `pqcrypto` which wraps PQClean's ML-DSA-65 implementation. Two thi **Non-deterministic keygen from seeds.** `pqcrypto` does not expose seed-based ML-DSA-65 key generation through its public API — `crypto_sign_keypair` reads from the OS RNG internally. This means `hybrid_keypair_from_seeds()` is NOT truly deterministic on the ML-DSA side in Python. The practical consequence: **Python cannot regenerate the canonical test fixtures** (the Go reference does that). Python's conformance contract is verification-only — it verifies Go-generated fixtures byte-for-byte but does not regenerate them. This is a known limitation of the `pqcrypto` library, not a protocol limitation. +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; install from `main` to use it ahead of the release. + +- **Resource-bound verification.** The `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via a `Constraint`'s `resource_id` and optional `path_prefix`. At verify time the application supplies `requested_resource_id`, `requested_path`, and `has_resource` on `VerifierContext` (SPEC §5.16); `verify_bundle(bundle, VerifyOptions(context=...))` evaluates them. Helpers: `normalize_resource_path`, `resource_path_matches`, `validate_resource_constraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `operation_context_bytes`, `operation_context_hash`, `session_context_bytes`, and `build_session_context`; `verifier_context_hash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `encode_verification_receipt` and `decode_verification_receipt`. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `verify_streamed_turn_with_options` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** A `Constraint`'s `params` carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `validate_params_value`; `is_canonical_constraint_type` guards which types may carry them. +- **Deeper delegation chains.** `MAX_DELEGATION_CHAIN_DEPTH` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MAX_PROOF_BUNDLE_BYTES`, `MAX_SCOPES_PER_CERT`, `MAX_CONSTRAINTS_PER_CERT`, `MAX_SCOPE_LENGTH_BYTES`, `MAX_IDENTIFIER_LENGTH_BYTES`, `MAX_AGENT_NAME_LENGTH_BYTES`, `MAX_JSON_NESTING_DEPTH`. + ## License Apache-2.0. See the project-level LICENSE. diff --git a/sdks/python/README.md b/sdks/python/README.md index 0caa6d2..4666f5a 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -57,7 +57,7 @@ from ratify_protocol import ( DelegationCert, ProofBundle, VerifyOptions, PROTOCOL_VERSION, SCOPE_MEETING_ATTEND, issue_delegation, sign_challenge, generate_challenge, - derive_id, verify_bundle, HybridSignature, + verify_bundle, HybridSignature, ) import time @@ -175,7 +175,7 @@ requests.post("https://verifier.example.com/verify", data=body, ### Receiving a proof bundle ```python -from ratify_protocol import decode_proof_bundle, verify_bundle, VerifyOptions +from ratify_protocol import decode_proof_bundle, verify_bundle, VerifyOptions, SCOPE_MEETING_ATTEND bundle = decode_proof_bundle(request_body) # str or bytes result = verify_bundle(bundle, VerifyOptions(required_scope=SCOPE_MEETING_ATTEND)) @@ -266,6 +266,18 @@ This SDK uses `pqcrypto` which wraps PQClean's ML-DSA-65 implementation. Two thi **Non-deterministic keygen from seeds.** `pqcrypto` does not expose seed-based ML-DSA-65 key generation through its public API — `crypto_sign_keypair` reads from the OS RNG internally. This means `hybrid_keypair_from_seeds()` is NOT truly deterministic on the ML-DSA side in Python. The practical consequence: **Python cannot regenerate the canonical test fixtures** (the Go reference does that). Python's conformance contract is verification-only — it verifies Go-generated fixtures byte-for-byte but does not regenerate them. This is a known limitation of the `pqcrypto` library, not a protocol limitation. +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; install from `main` to use it ahead of the release. + +- **Resource-bound verification.** The `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via a `Constraint`'s `resource_id` and optional `path_prefix`. At verify time the application supplies `requested_resource_id`, `requested_path`, and `has_resource` on `VerifierContext` (SPEC §5.16); `verify_bundle(bundle, VerifyOptions(context=...))` evaluates them. Helpers: `normalize_resource_path`, `resource_path_matches`, `validate_resource_constraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `operation_context_bytes`, `operation_context_hash`, `session_context_bytes`, and `build_session_context`; `verifier_context_hash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `encode_verification_receipt` and `decode_verification_receipt`. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `verify_streamed_turn_with_options` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** A `Constraint`'s `params` carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `validate_params_value`; `is_canonical_constraint_type` guards which types may carry them. +- **Deeper delegation chains.** `MAX_DELEGATION_CHAIN_DEPTH` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MAX_PROOF_BUNDLE_BYTES`, `MAX_SCOPES_PER_CERT`, `MAX_CONSTRAINTS_PER_CERT`, `MAX_SCOPE_LENGTH_BYTES`, `MAX_IDENTIFIER_LENGTH_BYTES`, `MAX_AGENT_NAME_LENGTH_BYTES`, `MAX_JSON_NESTING_DEPTH`. + ## License Apache-2.0. See the project-level LICENSE. diff --git a/sdks/rust/README.body.md b/sdks/rust/README.body.md index c5d1c4e..43d3ef4 100644 --- a/sdks/rust/README.body.md +++ b/sdks/rust/README.body.md @@ -20,7 +20,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; fn main() { // 1. DELEGATE let (root, root_priv) = generate_human_root(); - let (agent, agent_priv) = generate_agent("Alice's Assistant", "voice_agent"); + let (agent, agent_priv) = + generate_agent("Alice's Assistant", "voice_agent").expect("agent generation"); let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; let mut cert = DelegationCert { @@ -36,7 +37,7 @@ fn main() { expires_at: now + 7 * 24 * 3600, signature: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, }; - issue_delegation(&mut cert, &root_priv); + issue_delegation(&mut cert, &root_priv).expect("delegation issuance"); // 2. PRESENT let challenge = generate_challenge(); @@ -110,7 +111,7 @@ let mut stmt = KeyRotationStatement { old_pub_key: old_root.public_key.clone(), new_id: new_root.id.clone(), new_pub_key: new_root.public_key.clone(), - rotated_at: now_unix(), + rotated_at: 1_700_000_000, // current unix seconds reason: "routine".into(), signature_old: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, signature_new: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, @@ -128,7 +129,7 @@ Ratify v1 ships 54 canonical scopes plus 14 wildcards and a `custom:` extension For app-specific needs not covered by the canonical vocabulary, use the `custom:` prefix: ```rust -use ratify_protocol::{validate_scopes, CUSTOM_SCOPE_PREFIX}; +use ratify_protocol::validate_scopes; assert!(validate_scopes(&["custom:acme:inventory:read".into()]).is_none()); ``` @@ -143,6 +144,18 @@ cargo test The suite loads every fixture from the [canonical test vectors](https://github.com/identities-ai/ratify-protocol/tree/main/testvectors/v1) and runs it through the Rust implementation. All 79 must pass; any failure means this SDK has drifted from the Go reference. +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; depend on `main` (git) to use it ahead of the release. + +- **Resource-bound verification.** The `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via a `Constraint`'s `resource_id` and optional `path_prefix`. At verify time the application supplies `requested_resource_id` and `requested_path` on `VerifierContext` (SPEC §5.16); `verify_bundle(&bundle, &VerifyOptions { context, ..Default::default() })` evaluates them. Helpers: `normalize_resource_path`, `resource_path_matches`, `validate_resource_constraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `operation_context_bytes`, `operation_context_hash`, `session_context_bytes`, and `build_session_context`; `verifier_context_hash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `encode_verification_receipt`; `decode_verification_receipt` requires the `std` feature. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `verify_streamed_turn_with_options` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** A `Constraint`'s `params` (typed `ParamsValue`) carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `validate_params_value` and `validate_constraint_params`; `is_canonical_constraint_type` guards which types may carry them. +- **Deeper delegation chains.** `MAX_DELEGATION_CHAIN_DEPTH` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MAX_PROOF_BUNDLE_BYTES`, `MAX_SCOPES_PER_CERT`, `MAX_CONSTRAINTS_PER_CERT`, `MAX_SCOPE_LENGTH_BYTES`, `MAX_IDENTIFIER_LENGTH_BYTES`, `MAX_AGENT_NAME_LENGTH_BYTES`, `MAX_JSON_NESTING_DEPTH`. + ## License Apache-2.0. See the project-level LICENSE. diff --git a/sdks/rust/README.md b/sdks/rust/README.md index e382432..c1c228e 100644 --- a/sdks/rust/README.md +++ b/sdks/rust/README.md @@ -43,7 +43,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; fn main() { // 1. DELEGATE let (root, root_priv) = generate_human_root(); - let (agent, agent_priv) = generate_agent("Alice's Assistant", "voice_agent"); + let (agent, agent_priv) = + generate_agent("Alice's Assistant", "voice_agent").expect("agent generation"); let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; let mut cert = DelegationCert { @@ -59,7 +60,7 @@ fn main() { expires_at: now + 7 * 24 * 3600, signature: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, }; - issue_delegation(&mut cert, &root_priv); + issue_delegation(&mut cert, &root_priv).expect("delegation issuance"); // 2. PRESENT let challenge = generate_challenge(); @@ -133,7 +134,7 @@ let mut stmt = KeyRotationStatement { old_pub_key: old_root.public_key.clone(), new_id: new_root.id.clone(), new_pub_key: new_root.public_key.clone(), - rotated_at: now_unix(), + rotated_at: 1_700_000_000, // current unix seconds reason: "routine".into(), signature_old: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, signature_new: HybridSignature { ed25519: vec![], ml_dsa_65: vec![] }, @@ -151,7 +152,7 @@ Ratify v1 ships 54 canonical scopes plus 14 wildcards and a `custom:` extension For app-specific needs not covered by the canonical vocabulary, use the `custom:` prefix: ```rust -use ratify_protocol::{validate_scopes, CUSTOM_SCOPE_PREFIX}; +use ratify_protocol::validate_scopes; assert!(validate_scopes(&["custom:acme:inventory:read".into()]).is_none()); ``` @@ -166,6 +167,18 @@ cargo test The suite loads every fixture from the [canonical test vectors](https://github.com/identities-ai/ratify-protocol/tree/main/testvectors/v1) and runs it through the Rust implementation. All 79 must pass; any failure means this SDK has drifted from the Go reference. +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; depend on `main` (git) to use it ahead of the release. + +- **Resource-bound verification.** The `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via a `Constraint`'s `resource_id` and optional `path_prefix`. At verify time the application supplies `requested_resource_id` and `requested_path` on `VerifierContext` (SPEC §5.16); `verify_bundle(&bundle, &VerifyOptions { context, ..Default::default() })` evaluates them. Helpers: `normalize_resource_path`, `resource_path_matches`, `validate_resource_constraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `operation_context_bytes`, `operation_context_hash`, `session_context_bytes`, and `build_session_context`; `verifier_context_hash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `encode_verification_receipt`; `decode_verification_receipt` requires the `std` feature. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `verify_streamed_turn_with_options` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** A `Constraint`'s `params` (typed `ParamsValue`) carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `validate_params_value` and `validate_constraint_params`; `is_canonical_constraint_type` guards which types may carry them. +- **Deeper delegation chains.** `MAX_DELEGATION_CHAIN_DEPTH` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MAX_PROOF_BUNDLE_BYTES`, `MAX_SCOPES_PER_CERT`, `MAX_CONSTRAINTS_PER_CERT`, `MAX_SCOPE_LENGTH_BYTES`, `MAX_IDENTIFIER_LENGTH_BYTES`, `MAX_AGENT_NAME_LENGTH_BYTES`, `MAX_JSON_NESTING_DEPTH`. + ## License Apache-2.0. See the project-level LICENSE. diff --git a/sdks/typescript/README.body.md b/sdks/typescript/README.body.md index d220320..678a8fa 100644 --- a/sdks/typescript/README.body.md +++ b/sdks/typescript/README.body.md @@ -47,7 +47,6 @@ await issueDelegation(cert, alicePriv); ```ts import { signChallenge, - generateChallenge, type ProofBundle, } from "@identities-ai/ratify-protocol"; @@ -142,7 +141,7 @@ await issueKeyRotationStatement(stmt, oldCustodialPrivateKey, newPrivateKey); ## Canonical serialization -Signed payloads follow Ratify's canonical JSON rules (see [SPEC.md §6.3.1](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md)). The SDK exposes: +Signed payloads follow Ratify's canonical JSON rules (see [SPEC.md §6](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md)). The SDK exposes: ```ts import { canonicalJSON, delegationSignBytes, challengeSignBytes } from "@identities-ai/ratify-protocol"; @@ -266,6 +265,18 @@ A single failure means TypeScript and the Go reference have drifted. No network code in this package. HTTP concerns (challenge issuance, revocation list fetching, API auth) live one layer up. +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; install from `main` to use it ahead of the release. + +- **Resource-bound verification.** The `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via a constraint's `resource_id` and optional `path_prefix`. At verify time the application supplies `requested_resource_id`, `requested_path`, and `has_resource` on `VerifierContext` (SPEC §5.16); `verifyBundle(bundle, { context })` evaluates them. Helpers: `normalizeResourcePath`, `resourcePathMatches`, `validateResourceConstraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `operationContextBytes`, `operationContextHash`, `sessionContextBytes`, and `buildSessionContext`; `verifierContextHash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `encodeVerificationReceipt` and `decodeVerificationReceipt`. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `verifyStreamedTurnWithOptions` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** A constraint's `params` carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `validateParamsValue`; `isCanonicalConstraintType` guards which types may carry them. +- **Deeper delegation chains.** `MAX_DELEGATION_CHAIN_DEPTH` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MAX_PROOF_BUNDLE_BYTES`, `MAX_SCOPES_PER_CERT`, `MAX_CONSTRAINTS_PER_CERT`, `MAX_SCOPE_LENGTH_BYTES`, `MAX_IDENTIFIER_LENGTH_BYTES`, `MAX_AGENT_NAME_LENGTH_BYTES`, `MAX_JSON_NESTING_DEPTH`. + ## License Apache-2.0 diff --git a/sdks/typescript/README.md b/sdks/typescript/README.md index 90e27e5..6f3b603 100644 --- a/sdks/typescript/README.md +++ b/sdks/typescript/README.md @@ -70,7 +70,6 @@ await issueDelegation(cert, alicePriv); ```ts import { signChallenge, - generateChallenge, type ProofBundle, } from "@identities-ai/ratify-protocol"; @@ -165,7 +164,7 @@ await issueKeyRotationStatement(stmt, oldCustodialPrivateKey, newPrivateKey); ## Canonical serialization -Signed payloads follow Ratify's canonical JSON rules (see [SPEC.md §6.3.1](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md)). The SDK exposes: +Signed payloads follow Ratify's canonical JSON rules (see [SPEC.md §6](https://github.com/identities-ai/ratify-protocol/blob/main/SPEC.md)). The SDK exposes: ```ts import { canonicalJSON, delegationSignBytes, challengeSignBytes } from "@identities-ai/ratify-protocol"; @@ -289,6 +288,18 @@ A single failure means TypeScript and the Go reference have drifted. No network code in this package. HTTP concerns (challenge issuance, revocation list fetching, API auth) live one layer up. +## API added on main (ships in alpha.16, release unpublished) + +The following surface is merged to `main` and ships in alpha.16. The tag is not yet published; install from `main` to use it ahead of the release. + +- **Resource-bound verification.** The `resource_path` constraint (the 8th constraint type, SPEC §5.7.3) binds authority to a named resource via a constraint's `resource_id` and optional `path_prefix`. At verify time the application supplies `requested_resource_id`, `requested_path`, and `has_resource` on `VerifierContext` (SPEC §5.16); `verifyBundle(bundle, { context })` evaluates them. Helpers: `normalizeResourcePath`, `resourcePathMatches`, `validateResourceConstraints`. +- **Operation / session verifier context.** `OperationContext` and `SessionContextInputs`, with `operationContextBytes`, `operationContextHash`, `sessionContextBytes`, and `buildSessionContext`; `verifierContextHash` produces the canonical hash bound into a `VerificationReceipt`. +- **VerificationReceipt wire codecs.** `encodeVerificationReceipt` and `decodeVerificationReceipt`. +- **Streamed-turn verify options.** `StreamedTurn` and `StreamedVerifyOptions`, with `verifyStreamedTurnWithOptions` (the options-object streamed fast path, SPEC §5.13). +- **Extension-constraint params.** A constraint's `params` carries parameters for non-canonical constraint types (SPEC §5.7.1), validated by `validateParamsValue`; `isCanonicalConstraintType` guards which types may carry them. +- **Deeper delegation chains.** `MAX_DELEGATION_CHAIN_DEPTH` is raised from 3 to 8 (SPEC §5.1). +- **Input bounds constants.** `MAX_PROOF_BUNDLE_BYTES`, `MAX_SCOPES_PER_CERT`, `MAX_CONSTRAINTS_PER_CERT`, `MAX_SCOPE_LENGTH_BYTES`, `MAX_IDENTIFIER_LENGTH_BYTES`, `MAX_AGENT_NAME_LENGTH_BYTES`, `MAX_JSON_NESTING_DEPTH`. + ## License Apache-2.0