Skip to content

fix(notary): bind credential issuance to Relay provenance#410

Merged
jeremi merged 1 commit into
release/1.0from
codex/1.0-384b-runtime-provenance
Jul 17, 2026
Merged

fix(notary): bind credential issuance to Relay provenance#410
jeremi merged 1 commit into
release/1.0from
codex/1.0-384b-runtime-provenance

Conversation

@jeremi

@jeremi jeremi commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

  • require every direct and OID4VCI credential fact to come from a registry-backed claim evaluated through its exact compiler-pinned Relay consultation closure
  • persist bounded private per-claim execution provenance only for credential-capable evaluations, including normalized shared dependency executions
  • reject legacy, missing, duplicated, extra, partially modified, or mismatched provenance before signer, credential ID, status, and direct holder-replay side effects
  • retire delegated self-attested credential issuance while preserving delegated evaluation and rendering
  • replace public self-attested credential claims with registry-backed-only capability, migration, OpenAPI, specification, and operator guidance

Runtime and public-truth portion of #384. The authoring portion is already integrated through #390. Keep #384 open until the final Notary schema closure under #227 confirms the same narrowed contract.

Why

A signed declaration, holder proof, authentication result, or relationship proof is not authoritative registry evidence. Previously, a hand-authored Notary configuration or stored evaluation could still reach direct or OID4VCI signing from a source-free claim even though generated project authoring had removed that capability.

The implementation also preserves usability: source-free and delegated evaluation-only journeys remain supported, invalid credential bindings fail during configuration with actionable remediation, and registry-backed claim dependencies work end to end instead of failing late at issuance.

Developer and operator impact

  • Credential profiles and OID4VCI configurations must select mutually bound registry-backed claims.
  • Registry-backed roots may use registry-backed dependency closures, including shared Relay executions.
  • Delegated self-attestation remains evaluation/rendering-only. Operators can keep that journey or move issuance to a non-delegated registry-backed claim.
  • Evaluations written before private execution bindings remain readable and renderable but must be re-evaluated before issuance.
  • Registry-backed evaluation-only records retain no private Relay identifiers.
  • A new migration guide gives field-level remediation and recovery steps.

Security boundary

Each credential-capable evaluation stores one exact compiler pin per claim and a normalized set of successful Relay executions. A versioned canonical SHA-256 binding covers claim/version/profile/contract/purpose, consultation ULID/acquisition time, evaluation/result timing, and exact ClaimProvenance. Issuance reconstructs the active dependency closure and rejects partial modification, ID swaps, missing, duplicate, or extra records before signing.

This unkeyed digest detects partial corruption or mutation. It is not an authenticity mechanism against an operator able to rewrite every stored field and recompute the digest; database authorization and audit controls remain that boundary.

OID4VCI preserves its existing nonce-before-evaluation ordering. Static source-free configuration denial occurs before nonce access; stored-provenance verification occurs after evaluation but before issuer resolution, status, or signing. Broader transaction semantics remain #57.

Validation

  • registry-notary-core: 356 tests
  • registry-notary-server default: 432 unit plus 10, 94, and 1 integration tests
  • registry-notary-server all features: 447 unit tests, 6 expected external-state ignores, plus 18, 7, 10, 112, and 7 integration tests
  • real standalone HTTP evaluate-to-direct-issue journey with a registry-backed root and dependency
  • direct and OID4VCI dependency-tamper tests with zero signer calls
  • strict all-feature Clippy, formatting, locked preflight, OpenAPI generation/contract, exposure and security assurance
  • 53 product Python tests and dependency policy
  • documentation build and 421,142 link/asset checks
  • two independent exact-head reviews: security and developer experience, no findings
  • one DCO-signed commit on exact parent da3c7c7dcc8898503aa9439b42e05fe8e3d63c06; Cargo.lock unchanged

The optional OpenAPI breaking comparison was not run locally because OPENAPI_CONTRACT_BASE_REF was unset; remote CI is authoritative.

The only overlap with draft #407 is removal of the obsolete held hosted-credential-tour link from the shared spreadsheet tutorial. #407 must preserve that deletion when rebased.

Merge only after the full remote matrix is green on exact head b6b3fdf578fa6a72b96595839d174a6b388a97d2.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi
jeremi force-pushed the codex/1.0-384b-runtime-provenance branch from b6b3fdf to 42b6441 Compare July 17, 2026 20:58

jeremi commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Exact-head CI update:

The first matrix correctly rejected two stale registry-notary doctor fixtures that attached a credential profile to a self-attested claim. Runtime issuance tests were not failing.

The focused repair changes only that optional diagnostic fixture to a complete registry-backed Relay consultation, so the tests again reach their intended outcomes:

  • local: warn for explicit holder_binding.mode: none;
  • production: fail readiness for unapproved signer custody.

Amended exact head: 42b6441892c2b7b8b56eebcbaaf13aeda6feea69
Exact parent remains: da3c7c7dcc8898503aa9439b42e05fe8e3d63c06

Verification:

  • full doctor_cli target: 25/25 passed;
  • format and diff checks passed;
  • Cargo.lock unchanged;
  • independent exact-delta review: no findings, approved.

The full amended exact-head remote matrix is running. The PR remains draft until every applicable check is green.

@jeremi
jeremi marked this pull request as ready for review July 17, 2026 21:26
@jeremi
jeremi merged commit 0aef9cf into release/1.0 Jul 17, 2026
16 checks passed
@jeremi
jeremi deleted the codex/1.0-384b-runtime-provenance branch July 17, 2026 21:27

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42b6441892

ℹ️ 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".

Comment on lines +790 to 792
for claim_id in levels.into_iter().flatten() {
allowed_claim_ids
.insert(BoundedClaimId::new(claim_id).map_err(|_| EvidenceError::InvalidRequest)?);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate subject-bound inputs for dependency closures

When a subject-access request selects an allowed root that has registry-backed dependencies, this loop now grants SubjectBound capability to every claim in the dependency closure. Startup still runs validate_subject_bound_registry_inputs only over subject_access.allowed_claims, and credential dependencies are intentionally not listed there because they do not carry the credential profile. That means a registry-backed dependency can map its Relay input from caller-controlled or otherwise non-bound paths such as target.attributes.*, pass config validation, and then execute under a subject token during evaluation/OID4VCI issuance for data not bound to the authenticated subject. Please validate the registry input bindings for the full closure before adding dependencies to the subject-bound capability.

Useful? React with 👍 / 👎.

validate_relay_activation_shape(&self.evidence.claims)?;
self.subject_access.validate(&self.auth, &self.evidence)?;
self.validate_oid4vci_cross_block()?;
validate_credential_claim_bindings(&self.evidence)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsupported delegated-proof dependencies

After the global registry_backed dependency ban was removed, this replacement validation only checks credential claim closures. A delegated relationship can now configure its registry-backed proof_claim with a registry-backed depends_on and still pass startup, but delegated runtime capability only permits Relay consultation for the proof claim itself (require_relay_consultation_capability), so every delegated evaluation using that relationship fails at preflight instead of being rejected as an invalid config. Please either validate delegated proof closures here or explicitly reject proof-claim dependencies.

Useful? React with 👍 / 👎.

Comment on lines +221 to +225
"result": {
"evaluation_id": evaluation_id,
"issued_at": issued_at,
"provenance": provenance,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind the signed result fields to the Relay execution

This new execution binding commits the Relay pin and public provenance, but not the claim result fields that are actually disclosed in the SD-JWT (value, satisfied, and related result metadata). If a stored evaluation is partially corrupted or modified after evaluation by changing only results[0].value from false to true, require_issuable_evaluation_provenance still recomputes the same binding and issuance signs the altered value because sd_jwt::issue reads directly from evaluation.results. Include the signed result payload (at least value/satisfied and subject fields used for issuance) in the binding so partial stored-record mutation is caught before signing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant