diff --git a/crates/registry-notary-core/src/config.rs b/crates/registry-notary-core/src/config.rs index 61cd29494..3c52bfc3d 100644 --- a/crates/registry-notary-core/src/config.rs +++ b/crates/registry-notary-core/src/config.rs @@ -23,8 +23,8 @@ use serde::{Deserialize, Serialize}; use crate::deployment::DeploymentConfig; use crate::model::{ - DisclosureProfile, EvidenceAuthorizationDetails, FORMAT_SD_JWT_VC, - SD_JWT_VC_HOLDER_BINDING_METHOD, SD_JWT_VC_SIGNING_ALG, + DisclosureProfile, EvidenceAuthorizationDetails, FORMAT_CCCEV_JSONLD, FORMAT_CLAIM_RESULT_JSON, + FORMAT_SD_JWT_VC, SD_JWT_VC_HOLDER_BINDING_METHOD, SD_JWT_VC_SIGNING_ALG, }; mod audit; diff --git a/crates/registry-notary-core/src/config/errors.rs b/crates/registry-notary-core/src/config/errors.rs index 66b965b3f..c1d6cd701 100644 --- a/crates/registry-notary-core/src/config/errors.rs +++ b/crates/registry-notary-core/src/config/errors.rs @@ -54,6 +54,20 @@ pub enum EvidenceConfigError { DuplicateClaimId { claim: String }, #[error("claim '{claim}' has invalid semantics config: {reason}")] InvalidClaimSemantics { claim: String, reason: String }, + #[error( + "claim '{claim}' formats must list at least one evaluation response format; \ + omit formats to use the default claim-result JSON format" + )] + EmptyClaimFormats { claim: String }, + #[error( + "claim '{claim}' formats contains unsupported format '{format}'; supported formats are \ + claim-result JSON, CCCEV JSON-LD, and SD-JWT VC" + )] + UnsupportedClaimFormat { claim: String, format: String }, + #[error( + "claim '{claim}' formats must include application/vnd.registry-notary.claim-result+json" + )] + MissingClaimResultFormat { claim: String }, /// REQ-DM-CLAIM-008 requires a claim's `disclosure.default` to be a /// member of `disclosure.allowed`; RS-DM-CLAIM Section 10 previously /// documented this as unchecked at load, surfacing only when a result diff --git a/crates/registry-notary-core/src/config/evidence/claims.rs b/crates/registry-notary-core/src/config/evidence/claims.rs index 894d86faa..a919f0fdd 100644 --- a/crates/registry-notary-core/src/config/evidence/claims.rs +++ b/crates/registry-notary-core/src/config/evidence/claims.rs @@ -3,6 +3,10 @@ use super::*; +fn default_claim_formats() -> Vec { + vec![FORMAT_CLAIM_RESULT_JSON.to_string()] +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct ClaimDefinition { @@ -27,7 +31,7 @@ pub struct ClaimDefinition { pub operations: ClaimOperationsConfig, #[serde(default)] pub disclosure: DisclosureConfig, - #[serde(default)] + #[serde(default = "default_claim_formats")] pub formats: Vec, #[serde(default)] pub credential_profiles: Vec, diff --git a/crates/registry-notary-core/src/config/root.rs b/crates/registry-notary-core/src/config/root.rs index dd652050e..4cc030434 100644 --- a/crates/registry-notary-core/src/config/root.rs +++ b/crates/registry-notary-core/src/config/root.rs @@ -285,6 +285,31 @@ impl StandaloneRegistryNotaryConfig { }); } validate_claim_semantics(claim)?; + if claim.formats.is_empty() { + return Err(EvidenceConfigError::EmptyClaimFormats { + claim: claim.id.clone(), + }); + } + if let Some(format) = claim.formats.iter().find(|format| { + !matches!( + format.as_str(), + FORMAT_CLAIM_RESULT_JSON | FORMAT_CCCEV_JSONLD | FORMAT_SD_JWT_VC + ) + }) { + return Err(EvidenceConfigError::UnsupportedClaimFormat { + claim: claim.id.clone(), + format: format.clone(), + }); + } + if !claim + .formats + .iter() + .any(|format| format == FORMAT_CLAIM_RESULT_JSON) + { + return Err(EvidenceConfigError::MissingClaimResultFormat { + claim: claim.id.clone(), + }); + } // REQ-DM-CLAIM-008: reject a disclosure default outside the // allowed set at load; this is the most consequential of the // three RS-DM-CLAIM Section 10 gaps because a privacy-sensitive diff --git a/crates/registry-notary-core/src/config/tests/credentials.rs b/crates/registry-notary-core/src/config/tests/credentials.rs index 032817927..7b38cc5db 100644 --- a/crates/registry-notary-core/src/config/tests/credentials.rs +++ b/crates/registry-notary-core/src/config/tests/credentials.rs @@ -776,6 +776,81 @@ pub(super) fn unknown_depends_on_is_rejected() { // the loader previously deferred to request/evaluation time. // ----------------------------------------------------------------------- +#[test] +pub(super) fn omitted_claim_formats_default_to_claim_result_json() { + let claim = minimal_claim("default-format"); + assert_eq!(claim.formats, vec![FORMAT_CLAIM_RESULT_JSON.to_string()]); +} + +#[test] +pub(super) fn empty_claim_formats_are_rejected() { + let mut config = minimal_config(); + let mut claim = minimal_claim("unrenderable-claim"); + claim.formats.clear(); + config.evidence.claims = vec![claim]; + + let err = config + .validate() + .expect_err("an explicitly empty formats list must fail validation"); + assert!(matches!( + err, + EvidenceConfigError::EmptyClaimFormats { ref claim } + if claim == "unrenderable-claim" + )); +} + +#[test] +pub(super) fn claim_formats_must_include_claim_result_json() { + let mut config = minimal_config(); + let mut claim = minimal_claim("cccev-only"); + claim.formats = vec![FORMAT_CCCEV_JSONLD.to_string()]; + config.evidence.claims = vec![claim]; + + let err = config + .validate() + .expect_err("claim-result JSON must be supported by every claim"); + assert!(matches!( + err, + EvidenceConfigError::MissingClaimResultFormat { ref claim } + if claim == "cccev-only" + )); +} + +#[test] +pub(super) fn unsupported_claim_formats_are_rejected() { + let mut config = minimal_config(); + let mut claim = minimal_claim("unknown-renderer"); + claim.formats.push("application/example+json".to_string()); + config.evidence.claims = vec![claim]; + + let err = config + .validate() + .expect_err("an unimplemented renderer must fail configuration load"); + assert!(matches!( + err, + EvidenceConfigError::UnsupportedClaimFormat { + ref claim, + ref format, + } if claim == "unknown-renderer" && format == "application/example+json" + )); +} + +#[test] +pub(super) fn implemented_claim_formats_are_accepted() { + let mut config = minimal_config(); + let mut claim = minimal_claim("supported-formats"); + claim.formats = vec![ + FORMAT_CLAIM_RESULT_JSON.to_string(), + FORMAT_CCCEV_JSONLD.to_string(), + FORMAT_SD_JWT_VC.to_string(), + ]; + config.evidence.claims = vec![claim]; + + config + .validate() + .expect("all implemented claim formats must pass validation"); +} + #[test] pub(super) fn duplicate_claim_id_is_rejected() { // REQ-DM-CLAIM-001: two claims sharing an id previously loaded diff --git a/crates/registry-notary/tests/doctor_cli.rs b/crates/registry-notary/tests/doctor_cli.rs index 3063b2eeb..b66af3c01 100644 --- a/crates/registry-notary/tests/doctor_cli.rs +++ b/crates/registry-notary/tests/doctor_cli.rs @@ -160,6 +160,7 @@ fn write_config_with_options(tmp: &TempDir, options: TestConfigOptions<'_>) -> P type: cel expression: "true" formats: + - application/vnd.registry-notary.claim-result+json - application/dc+sd-jwt credential_profiles: - unbound_sd_jwt diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 52e84d6de..0b302794e 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -182,8 +182,7 @@ Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manife model](../../spec/rs-dm-manifest/). - The `plugin` rule type is unimplemented: The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. -- Load-time invariants are not all enforced: The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. A disclosure default outside the allowed set and a dangling rule source surface as request or evaluation errors at runtime, not as startup failures. Duplicate claim ids are different: they are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. -- Identifier-only matching has no further gating: A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. +- Identifier-only matching has no binding-level policy: A source binding with no matching policy resolves on identifiers alone. Claim-level scope and purpose gates still apply, but the binding adds no relationship or input-minimization policy. - Manifests describe; they do not enforce: The portable metadata layer only describes: publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. - Rendered standards artifacts are well-formed, not certified: Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. - Vocabulary influence is not vocabulary emission: PROV-O is a design influence only: provenance-shaped concepts appear in audit fields and the claim provenance struct, but no PROV-O vocabulary terms are emitted as JSON-LD. No standalone SKOS artifact is published yet (only embedded SKOS-shaped nodes), and CPSV-AP has no Relay runtime metadata route. diff --git a/docs/site/src/content/docs/spec/rs-dm-claim.mdx b/docs/site/src/content/docs/spec/rs-dm-claim.mdx index e72277ef9..46dd60b56 100644 --- a/docs/site/src/content/docs/spec/rs-dm-claim.mdx +++ b/docs/site/src/content/docs/spec/rs-dm-claim.mdx @@ -6,11 +6,11 @@ owner: registry-docs source_repos: - registry-notary - registry-platform -last_reviewed: "2026-07-07" +last_reviewed: "2026-07-11" doc_type: specification doc_id: RS-DM-CLAIM category: normative -evidence: partial +evidence: verified locale: en standards_referenced: - openapi @@ -25,7 +25,7 @@ audience: - specification editor --- -This document defines the target data model of a Registry Notary claim definition: the configuration artifact that declares what Notary can evaluate, what it reads to evaluate it, what it discloses, and what it can issue. It is the structural counterpart to the [Registry Notary protocol](../rs-pr-notary/): RS-PR-NOTARY Section 1 defers the claim-definition structure (source bindings, rule, disclosure policy, and formats) to this document. The Evidence section distinguishes shipped behavior from the implementation gaps tracked in Section 10. +This document defines the data model of a Registry Notary claim definition: the configuration artifact that declares what Notary can evaluate, what it reads to evaluate it, what it discloses, and what it can issue. It is the structural counterpart to the [Registry Notary protocol](../rs-pr-notary/): RS-PR-NOTARY Section 1 defers the claim-definition structure (source bindings, rule, disclosure policy, and formats) to this document. Where this document and [RS-PR-NOTARY](../rs-pr-notary/) state the same constraint, RS-PR-NOTARY is the protocol-level (wire) form and this document is the data-model form. Both refine the Registry Notary component of [RS-ARC-G](../rs-arc-g/) Section 3 (REQ-ARC-G-007) one level of detail down, from architectural boundary to the structure a claim takes in configuration. Security-model concerns (the scope and authentication machinery a claim relies on) belong to [RS-SEC-G](../rs-sec-g/); this document refers to them only where they shape the model. @@ -37,6 +37,7 @@ The key words in this document are interpreted per [RS-DOC](../rs-doc/) Section | --- | --- | --- | --- | | 0.1.0 | 2026-06-13 | draft | Initial claim-definition data model, distilled from the Registry Notary source-and-claim modeling guide, the operator configuration reference, and the two-way credential-eligibility and matching-policy validations the configuration loader enforces. | | 0.2.0 | 2026-07-07 | draft | Reworded REQ-DM-CLAIM-005 so the controlled-environment restriction on disabling matching-failure collapse reads as operational guidance rather than a runtime-enforced constraint, and documented the extract/exists value-type-checking gap and the empty-`formats`-list evaluation gap in Limitations. | +| 0.2.1 | 2026-07-11 | draft | Aligned the configuration-load contract with shipped validation for identifiers, disclosure defaults, rule source references, and response formats; removed the fixed non-CEL value-type limitation. | ## 1. Scope and references @@ -115,9 +116,9 @@ REQ-DM-CLAIM-008: Registry Notary MUST apply a claim's `default` disclosure mode ## 7. Response formats and value typing -A claim's `formats` list declares the response formats it can render, and its value descriptor declares the type the evaluated value takes. +A claim's `formats` list declares the output formats associated with the claim, and its value descriptor declares the type the evaluated value takes. Claim-result JSON and CCCEV JSON-LD are evaluation render formats. SD-JWT VC can appear as a credential capability, but a credential profile controls whether Registry Notary can issue it. -REQ-DM-CLAIM-009: A claim MUST be renderable as the claim-result JSON media type, and MAY additionally declare CCCEV-shaped JSON-LD in its `formats` (the data-model form of REQ-PR-NOTARY-011). SD-JWT VC issuance is governed by credential profiles, not by the evaluation render `formats` list. A claim declares the type its evaluated value takes; where that type is one Registry Notary recognizes, the evaluated value MUST conform to it or the evaluation is refused rather than returned. +REQ-DM-CLAIM-009: A claim MUST be renderable as the claim-result JSON media type, and MAY additionally declare CCCEV-shaped JSON-LD in its `formats` (the data-model form of REQ-PR-NOTARY-011). An omitted `formats` list defaults to the claim-result JSON media type. An explicit list MUST include that media type and MUST NOT name an unimplemented output format; Registry Notary rejects an empty, incomplete, or unsupported list at configuration load. Listing SD-JWT VC records a credential capability but does not authorize issuance; credential profiles govern issuance. A claim declares the type its evaluated value takes; where that type is one Registry Notary recognizes, the evaluated value MUST conform to it or the evaluation is refused rather than returned. ## 8. Credential eligibility @@ -133,19 +134,14 @@ REQ-DM-CLAIM-011: Batch evaluation MUST be semantically equivalent to evaluating ## 10. Limitations -These constraints are stated so a reader does not infer an invariant the reviewed implementation does not enforce at configuration load. +These constraints bound the reviewed implementation where it does not enforce the complete target model. - Plugin rule: Declared in configuration, unimplemented; an evaluation that reaches a `plugin` rule is refused at evaluation time, not at configuration load (REQ-DM-CLAIM-006). -- Identifier uniqueness: The configuration loader does not reject two claims that share an `id`; uniqueness is an operator responsibility (REQ-DM-CLAIM-001). -- Disclosure default: The loader does not verify that `default` is a member of `allowed`; a `default` outside `allowed` surfaces as a request error when the result is rendered, not as a startup failure (REQ-DM-CLAIM-008). -- Rule source reference: The loader does not verify that a rule's `source` names a declared source binding; a dangling reference surfaces when the source is read at evaluation, not at configuration load (REQ-DM-CLAIM-006). - Matching fallback: A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating (Section 4). -- Value-type conformance: Registry Notary checks that an evaluated value conforms to its declared `value.type` only for a `cel` rule; the `extract` and `exists` rule paths return their result without checking it against `value.type` (REQ-DM-CLAIM-009). Tracked in [GH#232](https://github.com/registrystack/registry-stack/issues/232). -- Empty formats list: A claim definition whose `formats` list is empty (the default) can never be evaluated, because format checking rejects any format not listed, including the default-requested claim-result JSON format (REQ-DM-CLAIM-009). This is not rejected at configuration load. Tracked in [GH#170](https://github.com/registrystack/registry-stack/issues/170). ## Conformance -A claim definition, and the Registry Notary deployment that serves it, conforms to the target model in this specification when it: +A claim definition, and the Registry Notary deployment that serves it, conforms to this specification when it: - carries a stable identifier by which it is evaluated and named (REQ-DM-CLAIM-001); - binds each source read to the caller scope it requires, enforced before the read, and rejects inputs outside a declared allow-list (REQ-DM-CLAIM-002, REQ-DM-CLAIM-003); @@ -153,7 +149,7 @@ A claim definition, and the Registry Notary deployment that serves it, conforms - declares exactly one rule of an implemented kind and never depends on the unimplemented plugin kind (REQ-DM-CLAIM-006); - keeps `depends_on` references resolvable and acyclic (REQ-DM-CLAIM-007); - applies the default disclosure mode and refuses modes outside the allowed set (REQ-DM-CLAIM-008); -- renders the claim-result format and produces a value matching its declared type after the gaps in Section 10 are closed (REQ-DM-CLAIM-009); +- renders the claim-result format and produces a value matching its declared type (REQ-DM-CLAIM-009); - issues a credential only under the two-way claim-and-profile allow-list (REQ-DM-CLAIM-010); - treats batch evaluation as equivalent to repeated single evaluation and never as a way around per-evaluation rules (REQ-DM-CLAIM-011). @@ -161,7 +157,7 @@ Conformance to this specification does not imply conformance to any external sta ## Evidence -This specification is `partial`: most requirements describe shipped behavior a reader can inspect, per RS-DOC REQ-DOC-014, but Section 10 records two target-model gaps that keep REQ-DM-CLAIM-009 from being fully verified today. +This specification is `verified`: every requirement describes shipped behavior a reader can inspect, per RS-DOC REQ-DOC-014. - The [source and claim modeling guide](../../products/registry-notary/source-claim-modeling-guide/) walks the claim structure, the three rule kinds, source bindings, the disclosure policy, the two-way credential eligibility, and batch behavior that Sections 2 through 9 make precise. - The [Registry Notary operator configuration reference](../../products/registry-notary/operator-config-reference/) lists the claim, matching-policy, and credential-profile fields and the loader validations they carry (non-empty matching labels and lists, the rejected empty `allowed_claims`, the input allow-list minimization) that Sections 3, 4, and 8 state normatively. @@ -169,7 +165,6 @@ This specification is `partial`: most requirements describe shipped behavior a r - [RS-PR-NOTARY](../rs-pr-notary/) carries the protocol-level form of the scope, rule, disclosure, format, and credential requirements this document gives a data-model form (REQ-PR-NOTARY-004/006/007/009/011/013/015). - [RS-ARC-G](../rs-arc-g/) Section 3 holds the Registry Notary component invariant (REQ-ARC-G-007) that both this document and RS-PR-NOTARY refine. - The [standards register](../../reference/standards/) records the adoption mode for SD-JWT VC, the W3C DID method, and SP DCI named in `standards_referenced`. -- [GH#170](https://github.com/registrystack/registry-stack/issues/170) and [GH#232](https://github.com/registrystack/registry-stack/issues/232) track the empty-`formats` evaluation gap and the non-CEL value-type conformance gap called out in Section 10. ## Next diff --git a/products/notary/CHANGELOG.md b/products/notary/CHANGELOG.md index f81385bb8..4926c567a 100644 --- a/products/notary/CHANGELOG.md +++ b/products/notary/CHANGELOG.md @@ -138,9 +138,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `disclosure.default` outside the claim's allowed disclosure set (REQ-DM-CLAIM-008), and an `extract`/`exists` rule whose `source` does not name a declared source binding (REQ-DM-CLAIM-006), with an error naming the - offending claim and field. Configurations that previously loaded with one - of these inconsistencies fail to load until corrected; behavior for - consistent configurations is unchanged. + offending claim and field. An omitted claim `formats` list now defaults to + claim-result JSON. An explicitly configured list must include claim-result + JSON and may contain only implemented formats (REQ-DM-CLAIM-009). + Configurations that previously loaded with one of these inconsistencies fail + to load until corrected. - `extract` and `exists` rule results are now checked against the claim's declared `value.type` at evaluation time (REQ-DM-CLAIM-009), the same enforcement CEL rules already had. A source value that does not conform diff --git a/products/notary/docs/operator-config-reference.md b/products/notary/docs/operator-config-reference.md index 28b69f6a8..2532ca80e 100644 --- a/products/notary/docs/operator-config-reference.md +++ b/products/notary/docs/operator-config-reference.md @@ -736,7 +736,12 @@ Important fields: - `depends_on`: prerequisite claims for CEL rules that reuse earlier results. - `operations`: enable or cap `evaluate` and `batch_evaluate`. - `disclosure`: default and allowed response disclosure modes. -- `formats`: response formats the claim can render. +- `formats`: evaluation response formats and credential capabilities associated + with the claim. When omitted, the list + defaults to `application/vnd.registry-notary.claim-result+json`. An explicit + list must include that media type and may also contain CCCEV JSON-LD and + SD-JWT VC. Listing SD-JWT VC does not authorize issuance; `credential_profiles` + controls that separately. Empty or unknown lists fail configuration load. - `credential_profiles`: profiles allowed to issue from this claim. `semantics` is metadata, not a new credential shape. It helps clients understand