Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/registry-notary-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions crates/registry-notary-core/src/config/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion crates/registry-notary-core/src/config/evidence/claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

use super::*;

fn default_claim_formats() -> Vec<String> {
vec![FORMAT_CLAIM_RESULT_JSON.to_string()]
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClaimDefinition {
Expand All @@ -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<String>,
#[serde(default)]
pub credential_profiles: Vec<String>,
Expand Down
25 changes: 25 additions & 0 deletions crates/registry-notary-core/src/config/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions crates/registry-notary-core/src/config/tests/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/registry-notary/tests/doctor_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 10 additions & 15 deletions docs/site/src/content/docs/spec/rs-dm-claim.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -133,43 +134,37 @@ 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);
- enforces a declared matching policy before reading, and collapses matching failures to a single public reason by default (REQ-DM-CLAIM-004, REQ-DM-CLAIM-005);
- 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).

Conformance to this specification does not imply conformance to any external standard cited in the `standards_referenced` frontmatter field. Each standard's adoption mode and scope are documented in the [standards register](../../reference/standards/).

## 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.
- [Identity and record matching](../../products/registry-notary/identity-and-record-matching/) is the outcome model behind the matching policy in Section 4.
- [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

Expand Down
8 changes: 5 additions & 3 deletions products/notary/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading