chore: retire Registry Notary and make Evidence the documented product - #644
chore: retire Registry Notary and make Evidence the documented product#644jeremi wants to merge 146 commits into
Conversation
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Deployments with many callers and no identity provider had no safe way to get a signed, expiring, audience-bound token to a resource server such as Evidence. Handing every client a key from one pooled JWK set does not work: key selection is by `kid`, which the signer chooses, so every key in the pool is equally authoritative for every claim. Any client could name any principal, any requester tags, and any evidence audience. Mint separates the two questions. A server-side client registry binds a client id to that client's own public keys and to the authority Mint will assert for it. The token endpoint selects the key set by the asserted client id before verifying the signature, then reads authority from the registry and never from the assertion payload. A client therefore holds its own key and signs for itself, but possession of a key no longer decides what may be said. Security-sensitive review notes: - RFC 7523 `private_key_jwt` only. Assertions are bound to this endpoint by audience, bounded in lifetime, and single use by `jti`. The replay record outlives the assertion by the same clock skew the freshness check tolerates, so there is no window where an assertion is still accepted but no longer recorded as spent. - Strict compact-JWS preflight with duplicate-JSON-member rejection runs before any cryptographic work, and every segment is size-bounded. - The replay cache fails closed when saturated rather than evicting a live entry, since eviction is the move an attacker would use to make room for a replay. - OAuth errors collapse to `invalid_client` so the endpoint cannot be used to probe which client ids are registered. - Issuer identity, signing keys, listener, and token policy are startup-only. Only the client registry reloads, on SIGHUP, keeping the previous registry if the new one fails to load. Caller lifecycle changes therefore never restart a resource server. - Registered client authority is redacted from Debug, and the signing key never appears in Debug output or in startup diagnostics. `tests/evidence_compatibility.rs` drives the real router over a real on-disk deployment and feeds the minted token to Evidence's own authenticator. The dependency runs one way only: Evidence does not depend on Mint. The CI shard inventory gains a `mint` shard. Evidence changes now also select it, because the compatibility test dev-depends on registry-evidence and must run when Evidence's auth surface moves. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Accepted additions and must-close blockers landed so far: required request nonce end to end, governed response formats with strict Accept negotiation and the unsigned envelope, serialize-before-release-audit ordering, format-aware audit, strict verifier expectations, subject role order by declaration, Rhai containment/indexing/numeric/required fixes, transport pinning, existence-channel collapse, JWKS parity, operation exhaustion, acceptance traceability rows 1-62 with checker, contract and documentation reconciliation. Work in progress: bundle diagnostics, SIGTERM lifecycle and audit rotation procedure, reserved-header aliases, and the evidence verify subcommand are mid-implementation; independent reviews and the final same-revision gates have not run. Includes the separately authored evidence-definitions discovery endpoint that was staged in this worktree. Not a completion claim. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A registered client may now ask Mint for a token that is valid only for evidence about one named person. The delegation request rides inside the client's own signed assertion, in an `on_behalf_of` member, so the actor and the subject are covered by the client's signature. Mint validates both against the client's registration and mints them as claims. The containment is the resource server's: with the subject role's `valueOrigin` declared as `authenticated-context`, Evidence reads the selector from the token and refuses any request carrying selector values of its own. A client bug that puts the wrong person in the request body therefore cannot reach that person. Security review notes. This is authentication and authorization surface. - Delegation is opt-in per registration. A client with no `delegation` block cannot obtain a delegated token, and Mint refuses an actor outside the registered set. Every delegation failure collapses to `invalid_client`, like the existing failures, so the endpoint stays unusable as a probe. - The subject must carry the registration's subject fields exactly. A missing or extra field is refused rather than silently dropped. - `on_behalf_of` is Mint's own member, not RFC 8693 `act`: token exchange presents a subject's own credential, which a deployment without an IdP does not have. - Threat model. This defends against a buggy client, not a compromised one: a client holding its own signing key can ask for a token naming a different subject within the fields its registration permits. Closing that would mean resolving the subject from a server-side grant record. - Evidence confines an actor-bearing token to `kind: delegated` authority profiles but does not conversely require an actor to reach one. An undelegated token therefore matches such a grant and is stopped at selector resolution. Asserted in the tests and stated in both READMEs. `tests/delegated_subject_binding.rs` proves the property against Evidence's own entitlement match and selector resolution, over the same bundle the demonstration uses. `demo/` runs the whole thing against the real binaries with every request printed; its keys, certificates, and subjects are generated per run and synthetic. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Evidence's request model gained a required caller correlation nonce. The delegation test uses the offline constant, since the nonce never reaches authorization, which is what that test exercises. The demo generates a fresh one per request. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`mint token` signs a client assertion with the caller's own key and presents it to a running token endpoint. Getting an assertion right by hand is fiddly and getting it wrong yields an opaque `invalid_client`. Security review notes: - The subcommand authenticates; it does not decide. There is deliberately no path that signs an access token with Mint's signing key, which would be a way to obtain authority without authenticating inside the binary whose purpose is to make authority depend on authentication. Anything it obtains, the same client could have obtained over the wire. - The caller's key file gets the same guarantees as Mint's own signing key, through `secretfile::read_owner_only`. - Delegation subjects come from `--subject-file`, never from flags: they are a person's identifying details, and command lines are visible to every process on the host and land in shell history. - Logs move to stderr for this subcommand so stdout carries the access token alone. Also closes a claim-shadowing gap in `ClaimNames::validate`. Minting writes the configured claim names last, so a principal claim named `aud` produced a token whose audience was the principal and which still verified; the same held for any non-principal claim named `sub`. Both are now refused at configuration load, with tests over every reserved name. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Adds a sequence diagram to the demonstration README, and corrects the walkthrough's summary, which counted four requests for a script that sends nine across six steps. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Version 1 generated the public contract as a release artifact but never published it from the running service. Add GET /openapi.json, returning the same bytes the contract gate reproduces, so an adopter can point tooling at a deployment instead of tracking the repository artifact. Security review: the route is unauthenticated, like /health, /ready, and the public JWKS. Its payload is a process-constant built by the same generator as products/evidence/generated/registry-evidence.openapi.json, so it names no definition, bundle revision, authority, principal, or source, and it reaches no runtime state or dependency. It is therefore not a discovery oracle, and the requester-scoped catalog stays authenticated at GET /v1/evidence-definitions. Enforcement is the handler itself, which reads no token and holds no state, and is pinned by openapi_route_serves_the_generated_contract_without_authentication_or_source_access: it asserts no source request, an empty audit, and no bundle revision in the served bytes. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…file Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The only conflict was the workspace dependency block: the release train rewrote every registry-* pin to 0.16.3 while this side added registry-evidence and registry-mint at 0.16.2. Resolved to the 0.16.3 pins with both new members reinserted; `cargo update --workspace` moved the same two packages in the lockfile and nothing else. Neither crate appears in the beta-26 artifact list, which is correct: neither ships as a released binary yet. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…inition Rename the reference deployment project dhis2-adult-status -> dhis2-tracker-evidence and add the professional-licence-status acceptance definition beside adult-status, both served from the same DHIS2 tracker source. Bring in the version one runtime: request nonce end-to-end, response-format negotiation with unsigned envelope, strict offline verifier, source transport pinning, and observability. Security-sensitive: touches authentication (auth.rs), audit (audit.rs), source transport pinning (source.rs), and verifier policy. Reviewed for data minimization; reference deployment-project names stay test-only and the production code path remains source-neutral. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Serialize the stateless Evidence assertion as an SD-JWT VC (application/dc+sd-jwt, typ dc+sd-jwt) beside the JWS form, using the registry-platform-sdjwt serialization primitive. Amend the version one boundary to permit SD-JWT serialization solely for this response format. Security-sensitive: touches signing (signing.rs) and verification (verifier.rs). Response-format only; no credential issuance, OID4VCI, PDP, holder binding, or federation. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Landing the Evidence SD-JWT VC response format added the optional holder_key field to EvidenceRequest. Mint's tests drive Evidence's authenticator and build the struct directly, so the delegated subject-binding fixture stopped compiling. Security review: no behavior change. holder_key is meaningful only to the SD-JWT VC response format; it never reaches authorization, selectors, Rhai, source requests, or audit, and never appears in the signed-JWS payload. Setting it to None keeps this fixture exercising authorization alone, which is what the test asserts. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The demo performed every request with curl but never showed the invocation, so a reader could not reproduce a step outside the script. Each step now prints the command in copy-pasteable flag form before running it, with paths relative to the repository root. The two verify steps build one array that is both printed and executed, so the shown command cannot drift from the one that runs. Security review: the bearer token is still never rendered. curl keeps receiving its configuration on standard input, so the token reaches no command line and no process listing; the printed form substitutes the literal text $EVIDENCE_ACCESS_TOKEN for any Authorization header value. Verified by running the demo and confirming the 458-character token appears in neither the transcript nor harness.log. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
RFC 6749 section 5.1 makes expires_in recommended rather than required, so a compliant provider may answer with only access_token and token_type. The token parser rejected that response, which made such a provider unreachable. Bundles may now state assumedLifetimeSeconds: a governed positive value bounded by one day, applied only when the provider omits expires_in, and still clamped by maximumCacheSeconds. A present but zero or non-numeric expires_in stays a credential failure that no assumed lifetime rescues. Version 1 also drops query-string credential placement. RFC 6749 section 2.3.1 requires the client identifier and secret to travel in the Authorization header or the request body and never in the request URI. The OpenCRVS reference, its deployment project, and its frozen shape fixture move to form-body placement, which the provider accepts, so no shipped artifact needs the removed option. Security review notes: - No placement can now put a client identifier or secret in a token URL, which removes the documented exposure through authorization-server, proxy, and ingress URL logs. Narrowing the enum is a breaking bundle change; restoring the value later would be additive. - The assumed lifetime is a governed bundle value and is never inferred from the token itself, so no unverified token claim influences cache duration. - The redaction surface is unchanged in strength and narrower in scope: the token URL now carries nothing to redact, and the request body, response, and debug output remain fully redacted. - The opt-in live check now parses token responses exactly as the product does, so it can no longer report a passing profile for a provider the runtime would reject. Acceptance row 19 loses its query-string clause because the placement no longer exists. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The operator contract described the telemetry policy but never named the series, labels, or histogram bounds, so an operator writing a scrape config or a dashboard had to read the Rust source. Record the published contract alongside the limits that make it safe to expose. Also states two things the policy paragraph left implicit: the listener performs no authentication of its own, so the private binding is the only access control, and the series cover the HTTP boundary only, with source, signing, credential, and audit health reported by /ready instead. No behavior change. Reference only; the metric names, label sets, bucket bounds, route templates, and problem codes are the ones the runtime already emits and already asserts in tests. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…oundary The startup check rejects the mistake that actually exposes telemetry, a public or unspecified bindHost, and an operator can reasonably read the accepted range as meaning only they can reach the endpoint. On a flat pod network or a shared VPC every workload holds an RFC 1918 or unique-local address, so binding one there leaves the unauthenticated endpoint scrapable by every neighbour. Name loopback with a same-pod or same-host collector as the shape that keeps the intended boundary, and place the network policy for any wider binding with the operator. Security-sensitive documentation only: no behavior, validation, or default changes. The accepted address range and the absence of authentication on the telemetry listener are unchanged. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Add the registry-evidencectl crate and evidencectl binary: adopter tooling beside the frozen Version 1 runtime, like registryctl for the rest of the stack. It generates signing, holder, and HMAC key material, assembles public JWKS documents, scaffolds a neutral deployment project that passes `evidence check` and `evidence evaluate` with zero edits after one keygen pass (optionally paired with a Mint configuration), and drives bundle fixtures through the evidence binary. Every Evidence semantic decision is shelled out to `evidence`; nothing is re-implemented. The only workspace dependency edge is registry-platform-crypto. The source-neutrality gate now also sweeps evidencectl production Rust, templates, and Cargo metadata, and the CI evidence shard includes the new crate so evidencectl-only changes run the Evidence contract jobs. Security review notes (key generation and key handling): - Entropy: getrandom OS randomness into Zeroizing buffers; the private scalar's only non-zeroized copy is the short-lived serde_json string inside the rendered JWK, documented in code. Negative test: jwks rejects a private JWK input without echoing its contents. - File modes: private files 0600 and secret directories 0700, applied at O_CREAT so no window exists with looser permissions; public outputs 0644. Negative tests pin the modes and the 0700 normalization of a pre-created --out-dir. - Overwrite safety: --force removes the existing path first and then creates with O_EXCL, so a symlink at the target is replaced, never followed or written through, including in the remove-to-open race window. Negative tests in keygen and jwks pin this. - Redaction: private key material never reaches stdout, stderr, or process arguments; scaffolds ship no secrets and the scaffolded README instructs operators to place source bearer tokens as 0600 files, never on a command line. - Batch semantics: all target paths are collision-checked before any write, so a refused batch leaves nothing behind. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Add the registry-evidencectl row to the repository map, state its boundary beside the runtime (outside the frozen Version 1 contract, shells out to the evidence binary for every semantic decision, no registry-notary* dependency, covered by the same neutrality checks), and extend the Evidence reproducible gate commands to build and test both crates. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Convert flow-style mappings in the reference deployment projects to block style so an adopter copying these files can edit one field per line: selector profile fields, fixture case documents, and schema property maps. Short scalar lists and single-key const properties stay inline. Every file is proven byte-for-byte semantically identical to its previous parse (yq JSON round-trip diff), and the registry-evidence suite that loads these bundles passes unchanged. The opencrvs-family-evidence bundle/evidence.yaml carries the same reformat but is left uncommitted here because the working tree also holds unrelated in-progress edits to it. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The runtime refuses a file-provided secret containing a NUL byte, which a uniform 32-byte draw carries about 11.8% of the time. A scaffolded project therefore had roughly a one in eight chance of failing at `evidence serve`, long after `evidence check` and the fixtures had passed, for a reason that pointed at the secret file rather than at the tool that wrote it. Draw by rejection sampling instead. The value stays uniform over the accepted set, 255^32 or about 255.8 bits, which is not a meaningful reduction in strength for an HMAC key. Also state in the scaffold report that the source bearer token has to be obtained from the source system and written by hand: check, fixtures, and startup all pass without it, so a missing token is otherwise discovered by the first live request. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Finishes the reformatting the reference deployment bundle already started: the last flow-style mappings become block mappings, so selector fields, fixed headers, and selector alternatives are edited and diffed a line at a time. Verified value-identical: the parsed document is unchanged. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Measured audit throughput was about 270 appends/second and flat from 1 to 128 concurrent appenders: every append took one fsync under a held mutex, at 3.70 ms each. At two audit records per request that capped the service near 135 requests/second regardless of hardware or concurrency. Security review note, required for a change to audit integrity. What changed Chain-head ownership moves from registry_platform_audit::ChainState into Evidence's DurableJsonlSink. ChainState::append held its mutex across sink.write(), which serialized every append whatever the sink did internally. The on-disk chain is byte-identical: same keyed hasher, same envelope. A two-lock split replaces the single mutex. A short state lock claims a chain position and advances the head, performing no I/O; a separate flush lock is held across the blocking fsync. Appends arriving during a write form the next batch. Group commit is leader-follower with no timer and no configured window: the first caller to arrive while no write is in flight writes everything queued so far, and the others wait on the flush lock to find their records already durable. Batch size is 1 at idle and grows by itself under load. Segments rotate at auditStorage.maximumFileBytes with chain continuity across the seam, and startup verification is now bounded to the active segment, so restart time no longer grows with retained history. Properties preserved Chain positions are handed out in enqueue order under one lock, so concurrent appends extend one chain rather than forking it. Nothing is reported durable before its own bytes are synced. A failed durable write leaves the in-memory head ahead of the disk, so the sink poisons itself and refuses every later append for the life of the process rather than chaining onto a record the disk never received; concurrent waiters on a poisoned batch each receive the failure instead of hanging. A batch crossing a segment bound flushes the buffered run into the outgoing segment before sealing. Pinned-writer identity is still validated before the append and again after the sync, for both the segment and the lock file. Defect found and fixed during review The first version of the split read the recorded fingerprint under the state lock but compared it after releasing the lock, and acquired that lock with a non-blocking try_lock. Both were wrong under load: the service's own appends changed the file between snapshot and comparison, so readiness read its own traffic as external mutation, and try_lock lost to writer contention, which is busyness rather than ill health. Measured 195 of 200 probes unready while the service wrote its own audit records, which would have taken a healthy service out of a load balancer rotation under sustained load. The fingerprint is now compared under the same lock the writer advances it under, and only while the sink is quiescent. Tamper detection is unchanged. Accepted risk Startup verification is bounded to the active segment, so tampering inside an already sealed segment is not caught at boot. The new `evidence verify-audit` command is the out-of-band counterpart that does catch it. It reads the audit path and hash secret from the deployment's own runtime document and takes no path or secret flags, so it can neither be aimed at a foreign chain nor take a secret on a command line. It reports a missing sealed segment as archived history rather than as corruption, so deliberate archival stays distinguishable from tampering. Cross-crate change AuditEnvelope::new_with_hasher in registry-platform-audit becomes public. Visibility only, no behavior change. The alternative was a second implementation of the chain hash inside Evidence, and two implementations of a security-critical hash chain diverge eventually. Notary shares the crate and is unaffected; its tests pass unchanged. Result Audit appends, same host, before and after: 270/s to 322/s at 1 concurrent appender, ~270/s to 1,253/s at 8, to 4,245/s at 32, and to 12,864/s at 128. End to end over real sockets through the whole request path, including token verification, rate limiting, Rhai preparation, one source call, Rhai extraction, evidence construction, Ed25519 signing, and both audit appends: 6,976 to 7,057 requests/second across two runs at 128 in flight, p50 17.8 ms, zero non-2xx. The in-process source's own ceiling is measured in the same run and the check reports the run inconclusive below a 5x margin; it measured 20.8x. The request path also gains structured request logging, a correlation header, and an opt-in metrics listener, and the audit and rate-limiter capacity gauges that make the ceilings above observable. They land here rather than separately because the gauges read storage state this change introduces. The traceability checker now accepts #[tokio::test(flavor = "multi_thread")] as a test item. A concurrency invariant cannot be proven by a single-threaded test, so the stricter form meant leaving these invariants untraced. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
evidence check accepted a deployment whose mounted secrets the server refuses at startup, so a signing key whose kid does not match the bundle's signing.activeKeyId passed check and then failed every start. Check now resolves and validates the audit, subject-binding, and signing material exactly as startup does, without opening the audit chain; source credentials stay unresolved because readiness owns them. Security review notes: startup validation is extracted into validate_secret_material, not altered; check gains read-only secret resolution with the same fixed value-free operator messages; no secret bytes reach output; startup error-class precedence now reports subject-binding and signing faults before audit chain faults when both are broken. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A static reference docker-compose for the evidence service and the optional mint pairing: static private addresses on a user-defined network because the listener refuses wildcard binds, the deployment project mounted read-only with a container-shaped runtime file overlaid on the project's host-shaped one, and the audit chain in a named volume. The files are adopter-owned after copying; there is no generation contract. Verified end to end against the locally built images: check, both health endpoints, and audit persistence across a restart. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ence One multi-stage Dockerfile built from the repository root, with a cargo-chef recipe per binary so dependency compilation is a cacheable layer and an unrelated workspace manifest change invalidates neither image. Both images run as the distroless nonroot user with no shell. These are not release evidence; released images keep following the release/docker reproducible-build path. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The retired external integration runner took the only release-owned proof of an end-to-end source read with it. The `relay-oidc` harness looked like it covered this already: it mounts `records.csv`, materializes `people_table`, and declares `read_scope: smoke_registry:rows` on the `person` entity. But every assertion targeted `/v1/datasets`, so the read scope was never granted to the Zitadel role and no row was ever read. Map the role through a parameterized `$mapped_scope` and add three required checks. A metadata-scoped token is refused at the entity records route with `403 auth.scope_denied`; the same token remapped to the read scope returns `200`; and the record it returns is exactly the authored projection, so the source column `person_id` is absent. Relay restarts between the two scope stages deliberately, because one token carrying both scopes could not show that the read scope is independently enforced. `source_read_result` is pure so the projection rule is unit-tested offline, and it reports field names only, never the row, keeping a failing report inside the same evidence boundary as a passing one. A new test derives the expected record from the pinned fixture so the CSV, the template projection, and the expectation cannot drift apart. Review note (data minimization): these assertions are the release's only end-to-end check that a published Relay image discloses the authored projection rather than the source row. They are source-ready and unrun; a live run needs a published digest-pinned candidate. READINESS records them as unrun so no release cites them yet. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ence Remove the `federation` block and the `registry-notary` access kind from the portable manifest schema, and validate `registry-evidence` access in their place. Relay runtime binding accepts only that kind, so before this change a Relay deployment could advertise offerings that only ever pointed at a retired product. The `access.kind` vocabulary is now open. Registry Manifest checks endpoint shape for `registry-evidence` alone, because that kind names a service whose shape this repository defines: an assertion endpoint and the discovery document a relying party reads to verify the response. Any other kind falls through to generic optional-URI validation and is left to the consuming runtime. `registry-evidence-gateway-pdp/v1` is deliberately unchanged. It is live surface, consumed by the governed evidence pack policy path and required at Relay startup, and it does not name Notary. The schema version stays `registry-manifest/v1`. No adopter has published a manifest, `federation` was optional, and serde's unknown-field rejection fails closed with a readable error that names the removed key. Bumping would churn the published JSON-LD namespace IRI and the DCAT/SHACL goldens for nobody. Review note (security-sensitive: published schema and deployment contract): this narrows what a Relay deployment may advertise and changes a published discovery vocabulary. It does not touch authentication, authorization, assertion evaluation, signing, or audit. `conforms_to` is checked for presence only, not pinned to an Evidence contract version, so the portable layer stays independent of any one product's contract. Both `endpoint_url` and `discovery_url` are now required and must be HTTPS for `registry-evidence` access; validation checks shape, never reachability. The OpenAPI document is regenerated by its documented generator, not hand-edited, and `just openapi-contract` passes. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Follows the schema change that removed the `federation` block and the `registry-notary` access kind. The reference's federation section becomes an evidence-offering access section stating what the validator now enforces for `registry-evidence`, and that every other `access.kind` is left to the consuming runtime. Changelog and release-notes entries keep their historical wording. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
RS-DM-MANIFEST 0.4.0. Section 7 becomes evidence-offering access discovery: the `access.kind` vocabulary is open, and Registry Manifest checks endpoint shape for `registry-evidence` alone. REQ-DM-MANIFEST-010 now requires a non-blank `conforms_to` naming the response profile plus HTTPS `endpoint_url` and `discovery_url`, and a `ruleset` that names a declared evaluation profile when present. The Relay API reference stops telling readers a new deployment should declare no evidence offering; runtime binding accepts `registry-evidence`, so an offering now points somewhere maintained. Version-history rows for the retirement keep their original wording. Closes C10 in the Notary retirement plan. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The two C10 endpoint tests were committed with hand-wrapped comparisons that `cargo fmt --check` rejects. C10 was verified with check, clippy, tests and the docs gates but not with fmt, so the drift reached main. The change is whitespace only. Ticks G1: the full verification suite is green, with every gate's exit status recorded rather than inferred from a pipeline. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The mirrored crate docs are published at /products/registry-relay/ and are as current as any authored page, but the surface gate never read them. They still sent adopters to Registry Notary for the consultation caller, credential issuance, and signing-key operations. The consultation profile is now documented by what it does, an authenticated workload sending a purpose and a pinned contract, rather than by a caller product; the offering handoff is the access.kind registry-evidence that registry-manifest-core actually binds. The shipped wire identifiers stay: the Registry-Notary-Evaluation-Id header and the notary_evaluation_id envelope field are unchanged. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Evidence's README no longer defines itself against Registry Notary, and the two dependency clauses it carried are vacuous now that no registry-notary crate exists. The manifest reference loses its federation row outright, because FederationManifest is gone from registry-manifest-core. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The gate skipped everything under src/content/docs/products/, which is exactly where sync-repo-docs mirrors the crate docs, so the pages an adopter reaches from the product menus were never read. Removing the skip found 55 blocks across 13 mirrored pages. Two exemptions come with it. A non-mermaid fenced block is code for the same reason an inline span is, so an http example may spell a shipped header, while a mermaid fence stays subject to the rule because a diagram's participant labels are prose. And a page whose editUrl names a frozen Evidence Version 1 contract is skipped, since editing one needs a recorded re-approval rather than a docs pass. Closes G2. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Both executing tutorial gates pass from this worktree: six registryctl reader journeys and five Evidence tutorials, exit status 0 each. Six Evidence tutorial pages have no executing gate here; that extension is owned outside this plan and is recorded as a coverage caveat rather than closed. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Audited the 100 commits since the plan's starting revision. Thirty are security-sensitive; fifteen already carried a note in their own commit message, and the fifteen that did not now have one, keyed by what changed rather than by revision. Two are removals of enforcement and are argued rather than waved through. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The local was named `secret` and its value carries enough entropy to match the repository secret scan's generic-api-key rule, so a fixture the test asserts never reaches a rejection message was reported as a leak. The value, the behaviour and the assertion are unchanged. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The gate emitted a `node` call into every reader journey that edits a file, so those journeys stopped at `node: command not found` in CI's clean Debian userland. Mounting an interpreter would clear the red without fixing anything: the container exists to prove a reader needs only the documented toolset. The edit step is ported to a shell and awk helper with the same fence semantics, and a test asserts the gate reaches for no interpreter. All 22 fences named by the 11 edit steps are byte-identical between the Node helper and the shell helper, on macOS awk and on Debian trixie's mawk. G3 is unticked until the pull request's tutorial job is green. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The pull request's Evidence tutorial job passed with every step green, including the clean-container replay that failed before the edit step was ported off an interpreter. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Retargeting an evidence offering from Notary to Evidence made `access.conforms_to` name a response profile, `registry.assertion-evidence/v1`, rather than a document location, so that the portable manifest layer stays independent of any one product's contract version. Dropping `format: uri` was part of that change, not collateral damage: the manifest core validates the member as a URI for every other access kind, Relay refuses at startup to serve an offering of any other kind, and so every value these two routes can return would fail the annotation the baseline still carries. Recorded as a dated accepted diff beside the issue #362 entry, and pinned with a test that a profile identifier validates. The guard was checked against a deliberately reinstated URI requirement and failed as it should. Security review note. Threat: a future change reinstates the URI requirement and the manifest layer starts rejecting what the Evidence runtime returns. Enforcement point: `validate_registry_evidence_access` in registry-manifest-core, plus Relay's startup refusal of any offering whose `access.kind` is not `registry-evidence`. Negative test: `validation_rejects_blank_registry_evidence_conforms_to` keeps the member mandatory, so relaxing the URI rule did not relax presence. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The front forwarded whatever request line it was handed and relayed whatever headers came back. In a private repository that would be plumbing; in a public one it is an example, and it was modelling an ingress that publishes every upstream route and passes headers it cannot write intact. Code scanning read it the same way. It now serves only the two routes this deployment declares, Mint's key set and its token endpoint, forwarding a path literal from this file rather than the caller's; refuses a request or upstream header carrying CR, LF or NUL rather than splitting a message around it; sets its own TLS 1.2 floor instead of inheriting the runtime's; and closes the upstream connection on the error path. Provisioning creates secret files at their final mode. The old order wrote the file and then narrowed it, leaving a freshly generated signing key world readable for the length of a write. Tests are stdlib only, so they run under the same bare python3 the rest of the repository's checks use. `openssl` supplies the throwaway certificate, since nothing in CI installs `cryptography`; provision.py needs it and its three tests skip where it is absent, which is why they are not the gate for the route and header properties. The full demonstration was run end to end and all six steps behaved as described. Security review note. Threat: an ingress that forwards an attacker-chosen path reaches administrative routes the deployment never meant to publish, and one that relays an unvalidated header lets an upstream response split into two. Enforcement point: `route_for`, which can only return one of this file's own literals, and `well_formed`, applied to both directions before anything is written. Negative tests: `test_an_undeclared_route_never_reaches_the_upstream` asserts the stub upstream saw nothing, and `test_an_upstream_header_that_cannot_be_written_safely_is_not_relayed` asserts the injected header does not appear in the response. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
`load_baseline` carried the whole v3 shape check inline beside a one-line v2 branch and returned from three places, so the two versions read as if they were different kinds of thing. Lifting v3 out puts it beside `validate_v2_baseline` and leaves one return. No behaviour change: the same checks run in the same order and fail with the same messages. `crates/registry-relay/tests/advisory_baseline_check_test.py` passes unchanged, 17 tests. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ertions `check-debian13-images.py` built a `binary_recipe` handle and never used it, so the release workflow's builder pin was checked while the recipe's own default was not. Both ends now have to carry the same image. Two assertions were saying less than they meant. `assertTrue(a <= b)` on two sets reports only `False` when it fails; `assertLessEqual` names the artifacts that were missing. And a helper whose every other branch returns a string ended on `self.fail`, which reads as falling off the end and returning `None` into the comprehension that calls it; `raise AssertionError` says plainly that the branch does not return. Verified: `check-debian13-images.py` passes, `test_registry_release.py` 71 tests, `test_first_country_release_form.py` 54 tests. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…tion Retiring Notary took `GeneratorRecipeId::AuthorizationBeforeSource` with it, and with the recipe went the one code it emitted, `authorization.denied` under `registryctl_relay_offline_harness`. The harness is maintained and thirteen of its codes still stand, so the check was right to complain and wrong only about the remedy available to it. `RETIRED_DIAGNOSTIC_CODES` records the exemption against the exact (family, product, code) and carries the reason beside it. Widening `HISTORICAL_DIAGNOSTIC_PRODUCTS` to the product would have blinded the check to those thirteen live codes; skipping unreleased entries would have disabled the removal check altogether, since all seventy-four entries across the three catalogs are unreleased. Generated catalogs untouched. Security review note, release provenance. Threat: a broad exemption stops the check noticing that a maintained product dropped a code its released catalog promised. Enforcement point: the membership test in `compare_diagnostic_contracts` is on the full three-part key and cannot match a code the map does not name. Negative test: `test_a_retired_code_is_skipped_and_its_siblings_stay_protected` removes the retired code and a sibling together and asserts only the sibling is reported. Verified: `check-stable-surface-compatibility.py` exits 0, and its unit tests pass 14. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The two accepted lines were written against whatever oasdiff was on my PATH, which renders the same error differently from the 1.23.0 CI downloads, so they matched nothing and the check stayed red for a diff already reasoned about. The lines now reproduce 1.23.0's rendering, backticks and all, and the file says where the wording has to come from. Reproduced by installing the pinned 1.23.0 and running the CI command against the same base: 4 changes 2 error before, 2 changes 0 error 2 warning after, the two remaining warnings being the intended `registry-evidence` enum addition. Two follow-ups on the demonstration, from what code scanning made of the previous commit. `write` kept `os.open` with a mode argument that its ordinary callers set to 0644 and 0444, which reads as opening a file permissively no matter what a given call passes; the narrow creation now lives in `write_secret` with a literal 0600 and nothing else, and `write` is back to a plain write plus chmod. Refusal in the TLS front returned the value of a function that returns nothing, so `forward` mixed bare and valued returns. Verified: the demonstration runs end to end and all six steps behave as described, `.run` shows tls.key and signing.jwk at 0600 with ca.pem at 0644 and the Evidence runtime file at 0444, the support tests pass 13 under `uv` with `cryptography` and 10 with 3 skipped under bare python3. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
bcab9a3 to
0b9f4a2
Compare
…s a path to Code scanning read the local `secret_root` as sensitive data and followed it into the world-readable `runtime.yaml`, which is a true flow over a false premise: the value is the path `evidence/secrets`, not anything under it. The directory is created 0700 and every secret inside it goes through `write_secret` at 0600. Naming the local `provider_root`, after the `secretProviders.file.root` member it renders, leaves the query no sensitive-looking source and keeps the check green by construction rather than by a dismissal a later reader would never see. The rest of the stack keeps saying `secret_root`, so the reason for the divergence sits at the declaration. Verified: the support suite passes 13 tests with `cryptography` and 10 with 3 skipped under bare python3; provisioning a throwaway deployment renders the same `secretProviders.file.root` and the same modes, runtime.yaml 0444, secrets/ 0700, signing-key, source-token and tls.key 0600, ca.pem 0644; and `evidence check` accepts the result. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e4b7b7dd71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,29 @@ | |||
| stack: | |||
| release: beta-27 | |||
| version: 0.17.0 | |||
There was a problem hiding this comment.
Align beta-27 with the workspace package version
When the release-candidate workflow is dispatched for this new 0.17.0 manifest, build-release-binaries.sh still compiles workspace version 0.16.3 from Cargo.toml, so the explicit verify-registryctl-binary-version --version 0.17.0 step in .github/workflows/release-candidate.yml rejects the resulting registryctl 0.16.3 binary. Either defer this manifest or update the workspace dependencies and lockfile to 0.17.0 before making beta-27 selectable.
Useful? React with 👍 / 👎.
| - Keep a change scoped to one owning area (`crates/`, `products/`, | ||
| `docs/site/`, `release/`). |
There was a problem hiding this comment.
Split the change along owning-area boundaries
This commit changes all four independently owned areas—crates/, products/, docs/site/, and release/—including new security-sensitive runtimes and release machinery, even though the repository requires one owning area per change. At this size, ownership review, gate attribution, and safe rollback cannot be isolated; split the retirement, Evidence runtime/tooling, documentation, and release changes into separately reviewable commits.
AGENTS.md reference: AGENTS.md:L140-L145
Useful? React with 👍 / 👎.
| let expires_at = Instant::now() | ||
| .checked_add(cache_lifetime) | ||
| .ok_or(SourceError::Credential)?; |
There was a problem hiding this comment.
Expire cached OAuth tokens before the issuer does
When a source uses OAuth client credentials, expires_in is measured from issuance, but the cache deadline is calculated from a fresh Instant::now() only after the token response has arrived and been read. The cached token therefore remains eligible for at least the response latency beyond its real expiry, and without any safety margin a request near that boundary can send an already-expired bearer token and intermittently fail with 401; anchor the deadline before the token request and subtract a bounded refresh margin.
Useful? React with 👍 / 👎.
| if !self.signing.jwks_path.starts_with('/') { | ||
| return Err(ConfigError::Invalid("jwks path must be absolute")); | ||
| } |
There was a problem hiding this comment.
Reject JWKS paths that collide with Mint routes
A configuration such as jwksPath: /ready, /health, or the metadata path passes this absolute-path-only check, but build_app then registers two GET handlers for the same route and Axum rejects the overlap during router construction, turning an accepted configuration into a startup panic. Validate the path as a closed route path and reject collisions with every built-in Mint endpoint.
Useful? React with 👍 / 👎.
| validate_https_issuer(&self.issuer)?; | ||
| self.listener.bind_address()?; |
There was a problem hiding this comment.
Reject unusable Mint listener limits at startup
The listener validation checks only that the address parses, so maximumRequestBytes: 0 and requestTimeoutMilliseconds: 0 are accepted. With registered clients /ready still returns 200, but every nonempty token form is rejected by the zero body limit or immediately times out, leaving a deployment that advertises readiness while it cannot issue any token; enforce nonzero bounded listener values during configuration loading.
Useful? React with 👍 / 👎.
| asset directly: | ||
|
|
||
| ```sh | ||
| curl -fsSL https://github.com/registrystack/registry-stack/releases/download/<tag>/evidencectl-<tag>-install.sh | bash |
There was a problem hiding this comment.
Download the versioned installer before executing it
This documented curl | bash path cannot infer the release tag: when Bash reads the script from stdin, BASH_SOURCE[0] does not contain the downloaded evidencectl-<tag>-install.sh filename, so default_version remains empty and the installer exits with “No release is pinned.” Download and execute the named asset as the tutorial does, or pass EVIDENCECTL_VERSION explicitly in the pipeline.
Useful? React with 👍 / 👎.
| stage_dir="$(mktemp -d "$install_dir/.evidencectl-install.XXXXXX")" | ||
| install_started=0 | ||
| install_complete=0 | ||
| declare -A had_previous=() |
There was a problem hiding this comment.
Avoid Bash 4 associative arrays in the macOS installer
On the advertised macOS arm64 platform, /usr/bin/env bash normally resolves to the stock Bash 3.2, which does not support declare -A; the installer therefore aborts at this line after downloading and verifying the assets, before installing anything. Use Bash 3-compatible rollback state or explicitly provision and require a newer Bash before claiming macOS support.
Useful? React with 👍 / 👎.
The rollback bookkeeping used an associative array, so `declare -A` aborted the installer under the bash 3.2 that macOS ships. It reached that line only after downloading all three binaries, verifying them against SHA256SUMS, and creating a staging directory inside the install directory, and it aborted one line before the EXIT trap that would have removed the staging directory. A Mac without Homebrew bash therefore got a failed install and a stray .evidencectl-install.XXXXXX directory, on a platform the README advertises. The saved copy under the temporary directory is already the record that a binary was previously installed, so rollback reads that instead and the parallel bookkeeping goes away. Two tests cover this. One asserts the source text carries no bash 4 construct, since the runners that execute this suite all have bash 5 and would never notice a regression. The other runs the full install, and the rollback path, under /bin/bash when that is a 3.2, which is the case on macOS. Verified: 14 tests pass, /bin/bash 3.2.57 parses and runs the installer end to end, shellcheck and shfmt -d are clean. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The advertised one-liner pipes the installer into bash, which is the one way of running it that cannot work. The installer derives the release it belongs to from its own asset filename, and a piped script has no filename: BASH_SOURCE[0] is unset, so the pin is empty and the installer exits 1 with "No release is pinned for this installer copy." Anyone following the README got that error and nothing else. Document the download-then-run form the registryctl README and the first assertion tutorial already use, say why the file has to reach the shell as a file, and give the piped form its working shape for anyone who does not want to keep the script. Both behaviors verified against the installer: piped with no EVIDENCECTL_VERSION refuses to guess a release, and piped with one gets past the pin gate to asset download. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43a1449526
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for path in retired { | ||
| keys.push(load_retired_public_key(path)?); |
There was a problem hiding this comment.
Reject duplicate key IDs in the Mint JWKS
When retiredPublicJwkFiles repeats the active key ID—such as when an operator accidentally retains the active public key during rotation—this loop publishes both entries without validation. registry-platform-oidc::JwksFetcher::refresh collects keys into a map by kid, so the later retired entry replaces the active key; Mint continues reporting ready and issuing tokens, but Evidence rejects every newly minted token because it verifies against the wrong public key. Enforce unique IDs across the active and retired keys before startup succeeds.
AGENTS.md reference: AGENTS.md:L20-L23
Useful? React with 👍 / 👎.
| if !object.is_empty() { | ||
| return Err(SourceError::Credential); | ||
| } |
There was a problem hiding this comment.
Ignore extensions in OAuth token responses
When an otherwise valid OAuth token response includes a provider-specific extension, this closed-object check rejects the credential and consequently makes readiness and every request using that source fail. RFC 6749 section 5.1 requires clients to ignore unrecognized response value names, so interoperable providers are allowed to add such members; consume the recognized fields while ignoring bounded unknown members instead of rejecting the entire response.
AGENTS.md reference: AGENTS.md:L80-L84
Useful? React with 👍 / 👎.
| let id = string_of(claims, "id")?; | ||
| if string_of(claims, "jti")? != id { | ||
| return Err(SdJwtVcClaimError::ClaimShape); |
There was a problem hiding this comment.
Bind the SD-JWT issuer to the provider claim
When an SD-JWT signed by a trusted key omits iss or sets it to a value different from providedBy, this reconstruction never reads iss, so verify_sd_jwt_vc_report can still return an authentic, policy-conformant Evidence payload built from providedBy. The frozen SD-JWT VC profile defines iss as the exact service.providerId, and this encoding is supposed to represent the same assertion as the JWS form; require iss and reject it unless it equals providedBy.
AGENTS.md reference: AGENTS.md:L43-L48
Useful? React with 👍 / 👎.
Retires Registry Notary, keeps Registry Relay, and makes Evidence the
documented product. This is the accumulated work of the plan in
plans/notary-retirement-and-evidence-onboarding.md: 134 commits that havenever run against CI, because nothing had been pushed since the plan began.
Opened as a draft to get a CI signal, not to request review. 1252 files
changed is not a reviewable unit. The intent is to find out what the required
checks say about this state, fix what they surface, and then decide how to
split the change for actual review.
What is in scope
documentation removed. The retirement itself stays on the history pages
(decision records and changelog), which the docs-site checker exempts by
design.
(
registry-platform-cache,-oid4vci,-replay,-sts).(
registry-evidencectl), tutorials, reference deployment projects, andEvidence-specific verification gates.
Known-red on arrival
823c93e5("Revert "wip(evidence): ..."") has noSigned-off-bytrailer. It is the only one of the 134. Fixing it rewrites the 78 commits
after it, which also moves the base under an in-flight documentation branch,
so the fix is being sequenced deliberately rather than rushed into this push.
STABLE_SURFACE_BASE_REF,OPENAPI_CONTRACT_BASE_REF, andARCHIVE_LOCK_BASE_REFall resolve topull_request.base.sha, which here is 134 commits stale. Intentionalremovals will read as stable-surface breaks. Each failure needs reading on
its merits before it is treated as a defect.
Plan state
Global gates G1, G2, G3, and G5 are ticked in the plan file. G4 (frozen
Evidence Version 1 contracts byte-identical, or a recorded re-approval) is
reserved for maintainer decision and is provably not byte-identical: three
frozen files changed since the plan base. F3 is blocked on cutting
v0.17.0.Not in this branch
Evidence tutorial and UX documentation work in progress on
docs/evidence-ux-reset, which will land as its own reviewed change.