diff --git a/crates/registry-notary-core/src/config.rs b/crates/registry-notary-core/src/config.rs index 274247171..a6f083e3a 100644 --- a/crates/registry-notary-core/src/config.rs +++ b/crates/registry-notary-core/src/config.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; use crate::deployment::DeploymentConfig; use crate::model::{ - is_request_variable_name, DisclosureProfile, EvidenceAuthorizationDetails, + is_request_variable_name, DisclosureProfile, EvidenceAuthorizationDetails, FORMAT_CCCEV_JSONLD, FORMAT_CLAIM_RESULT_JSON, FORMAT_SD_JWT_VC, MAX_REQUEST_VARIABLES_V1, SD_JWT_VC_HOLDER_BINDING_METHOD, SD_JWT_VC_SIGNING_ALG, }; diff --git a/crates/registry-notary-core/src/config/errors.rs b/crates/registry-notary-core/src/config/errors.rs index c6192c505..f9626359a 100644 --- a/crates/registry-notary-core/src/config/errors.rs +++ b/crates/registry-notary-core/src/config/errors.rs @@ -80,6 +80,17 @@ pub enum EvidenceConfigError { application/vnd.registry-notary.claim-result+json representation, or list one or more response formats" )] EmptyClaimFormats { claim: String }, + #[error( + "claim '{claim}' formats must include the canonical evaluation response format \ + application/vnd.registry-notary.claim-result+json; add it alongside any supported additional evaluation renderers" + )] + MissingCanonicalClaimFormat { claim: String }, + #[error( + "claim '{claim}' has unsupported evaluation response format '{format}' in formats; \ + supported formats are application/vnd.registry-notary.claim-result+json and \ + application/ld+json; profile=\"cccev\". SD-JWT VC belongs in credential_profiles, not claim formats" + )] + UnsupportedClaimFormat { claim: String, format: String }, #[error("allowed purpose must not be empty")] InvalidPurpose, #[error("concurrency.subjects must be >= 1")] diff --git a/crates/registry-notary-core/src/config/root.rs b/crates/registry-notary-core/src/config/root.rs index 16e28d66b..ba7edf592 100644 --- a/crates/registry-notary-core/src/config/root.rs +++ b/crates/registry-notary-core/src/config/root.rs @@ -243,12 +243,31 @@ impl StandaloneRegistryNotaryConfig { } // REQ-DM-CLAIM-009: omitted formats deserialize to the canonical // claim-result representation, but an authored empty list cannot - // render any response and must fail before startup. + // render any response and must fail before startup. The response + // format set is closed: credential issuance formats do not render + // evaluation responses. if claim.formats.is_empty() { return Err(EvidenceConfigError::EmptyClaimFormats { claim: claim.id.clone(), }); } + for format in &claim.formats { + if format != FORMAT_CLAIM_RESULT_JSON && format != FORMAT_CCCEV_JSONLD { + 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::MissingCanonicalClaimFormat { + claim: claim.id.clone(), + }); + } } // Registry Notary currently resolves holder material only from // did:jwk. Reject any other configured method so discovery metadata diff --git a/crates/registry-notary-core/src/config/subject_access.rs b/crates/registry-notary-core/src/config/subject_access.rs index 9cc5551d2..09c6446bd 100644 --- a/crates/registry-notary-core/src/config/subject_access.rs +++ b/crates/registry-notary-core/src/config/subject_access.rs @@ -155,7 +155,6 @@ impl SubjectAccessConfig { profile, &claim_ids, &allowed_claim_ids, - &allowed_formats, self.token_policy.max_credential_validity_seconds, )?; } @@ -771,7 +770,6 @@ pub(super) fn validate_subject_access_profile( profile: &CredentialProfileConfig, claim_ids: &HashSet<&str>, allowed_claim_ids: &HashSet<&str>, - allowed_formats: &HashSet<&str>, max_credential_validity_seconds: u64, ) -> Result<(), EvidenceConfigError> { if profile.validity_seconds <= 0 { @@ -791,12 +789,6 @@ pub(super) fn validate_subject_access_profile( "credential profile '{profile_id}' validity_seconds must not exceed the subject-access ceiling" )); } - if !allowed_formats.contains(profile.format.as_str()) { - return invalid_subject_access(format!( - "credential profile '{profile_id}' uses unallowed format '{}'", - profile.format - )); - } if profile.holder_binding.mode != "did" { return invalid_subject_access(format!( "credential profile '{profile_id}' holder_binding.mode must be did" @@ -867,12 +859,9 @@ pub(super) fn validate_subject_access_allow_lists_are_supported( let supported_by_claim = allowed_claims .iter() .any(|claim| claim.formats.iter().any(|candidate| candidate == format)); - let supported_by_profile = allowed_profiles - .iter() - .any(|profile| profile.format == *format); - if !supported_by_claim && !supported_by_profile { + if !supported_by_claim { return invalid_subject_access(format!( - "allowed_formats entry '{format}' is not supported by any allowed claim or profile" + "allowed_formats entry '{format}' is not supported by any allowed claim" )); } } @@ -1046,12 +1035,9 @@ pub(super) fn validate_delegated_attestation_allow_lists_are_supported( let supported_by_claim = allowed_claims .iter() .any(|claim| claim.formats.iter().any(|candidate| candidate == format)); - let supported_by_profile = allowed_profiles - .iter() - .any(|profile| profile.format == *format); - if !supported_by_claim && !supported_by_profile { + if !supported_by_claim { return invalid_subject_access(format!( - "subject_access.delegation allowed_formats entry '{format}' is not supported by any allowed claim or profile" + "subject_access.delegation allowed_formats entry '{format}' is not supported by any allowed claim" )); } } diff --git a/crates/registry-notary-core/src/config/tests/credentials.rs b/crates/registry-notary-core/src/config/tests/credentials.rs index cf34b156e..35fb7c2c2 100644 --- a/crates/registry-notary-core/src/config/tests/credentials.rs +++ b/crates/registry-notary-core/src/config/tests/credentials.rs @@ -885,6 +885,129 @@ formats: [] ); } +#[test] +pub(super) fn canonical_claim_format_is_valid() { + let mut config = minimal_config(); + let mut claim = minimal_claim("canonical-format"); + claim.formats = vec![FORMAT_CLAIM_RESULT_JSON.to_string()]; + config.evidence.claims = vec![claim]; + + config + .validate() + .expect("the canonical evaluation format must validate"); +} + +#[test] +pub(super) fn canonical_and_cccev_claim_formats_are_valid() { + let mut config = minimal_config(); + let mut claim = minimal_claim("canonical-and-cccev"); + claim.formats = vec![ + FORMAT_CLAIM_RESULT_JSON.to_string(), + FORMAT_CCCEV_JSONLD.to_string(), + ]; + config.evidence.claims = vec![claim]; + + config + .validate() + .expect("the canonical and CCCEV evaluation formats must validate"); +} + +#[test] +pub(super) fn cccev_without_canonical_claim_format_is_rejected() { + 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("CCCEV-only formats must fail validation"); + match &err { + EvidenceConfigError::MissingCanonicalClaimFormat { claim } => { + assert_eq!(claim, "cccev-only"); + } + other => panic!("unexpected error variant: {other}"), + } + let message = err.to_string(); + assert!( + message.contains("cccev-only") && message.contains(FORMAT_CLAIM_RESULT_JSON), + "error must name the claim and canonical format: {message}" + ); +} + +#[test] +pub(super) fn sd_jwt_vc_claim_format_is_rejected_before_canonical_omission() { + let mut config = minimal_config(); + let mut claim = minimal_claim("sd-jwt-only"); + claim.formats = vec![FORMAT_SD_JWT_VC.to_string()]; + config.evidence.claims = vec![claim]; + + let err = config + .validate() + .expect_err("SD-JWT VC is not an evaluation response format"); + match &err { + EvidenceConfigError::UnsupportedClaimFormat { claim, format } => { + assert_eq!(claim, "sd-jwt-only"); + assert_eq!(format, FORMAT_SD_JWT_VC); + } + other => panic!("unexpected error variant: {other}"), + } + let message = err.to_string(); + assert!( + message.contains("credential_profiles") && message.contains(FORMAT_SD_JWT_VC), + "error must identify SD-JWT VC and its configuration home: {message}" + ); +} + +#[test] +pub(super) fn mixed_claim_formats_reject_the_first_unsupported_format() { + let mut config = minimal_config(); + let mut claim = minimal_claim("mixed-formats"); + claim.formats = vec![ + FORMAT_CLAIM_RESULT_JSON.to_string(), + FORMAT_SD_JWT_VC.to_string(), + "application/example+json".to_string(), + ]; + config.evidence.claims = vec![claim]; + + let err = config + .validate() + .expect_err("mixed formats must reject the unsupported SD-JWT VC entry"); + match &err { + EvidenceConfigError::UnsupportedClaimFormat { claim, format } => { + assert_eq!(claim, "mixed-formats"); + assert_eq!(format, FORMAT_SD_JWT_VC); + } + other => panic!("unexpected error variant: {other}"), + } +} + +#[test] +pub(super) fn unknown_claim_format_is_rejected() { + let mut config = minimal_config(); + let mut claim = minimal_claim("unknown-format"); + claim.formats = vec![ + FORMAT_CLAIM_RESULT_JSON.to_string(), + "application/example+json".to_string(), + ]; + config.evidence.claims = vec![claim]; + + let err = config + .validate() + .expect_err("unknown evaluation formats must fail validation"); + match &err { + EvidenceConfigError::UnsupportedClaimFormat { claim, format } => { + assert_eq!(claim, "unknown-format"); + assert_eq!(format, "application/example+json"); + } + other => panic!("unexpected error variant: {other}"), + } + assert!( + err.to_string().contains("application/example+json"), + "error must name the offending format: {err}" + ); +} + #[test] pub(super) fn empty_allowed_claims_is_rejected() { // A credential profile with an empty allowed_claims would silently diff --git a/crates/registry-notary-core/src/config/tests/root.rs b/crates/registry-notary-core/src/config/tests/root.rs index e1f1f52e5..6b4e27499 100644 --- a/crates/registry-notary-core/src/config/tests/root.rs +++ b/crates/registry-notary-core/src/config/tests/root.rs @@ -596,7 +596,6 @@ evidence: - value formats: - application/vnd.registry-notary.claim-result+json - - application/dc+sd-jwt credential_profiles: - civil_status_sd_jwt auth: @@ -644,7 +643,6 @@ subject_access: - date-of-birth allowed_formats: - application/vnd.registry-notary.claim-result+json - - application/dc+sd-jwt allowed_disclosures: - value required_scopes: @@ -743,10 +741,7 @@ token_file: /run/secrets/registry-notary-relay.jwt target_id_type: Some("civil_registration_id".to_string()), allowed_claims: vec!["dependent-date-of-birth".to_string()], allowed_purposes: vec!["dependent_attestation".to_string()], - allowed_formats: vec![ - "application/vnd.registry-notary.claim-result+json".to_string(), - "application/dc+sd-jwt".to_string(), - ], + allowed_formats: vec!["application/vnd.registry-notary.claim-result+json".to_string()], allowed_disclosures: vec!["value".to_string()], credential_profiles: vec!["civil_status_sd_jwt".to_string()], }], diff --git a/crates/registry-notary-server/src/api/credentials.rs b/crates/registry-notary-server/src/api/credentials.rs index 0c42ff667..21dcd4f3d 100644 --- a/crates/registry-notary-server/src/api/credentials.rs +++ b/crates/registry-notary-server/src/api/credentials.rs @@ -156,7 +156,7 @@ pub(super) async fn issue_credential( Ok(profile) => profile, Err(error) => return evidence_error_response(error), }; - if evaluation.format != FORMAT_SD_JWT_VC { + if evaluation.format != FORMAT_CLAIM_RESULT_JSON { return credential_denial_response_for_evaluation( &state, EvidenceError::EvaluationBindingMismatch, @@ -176,7 +176,7 @@ pub(super) async fn issue_credential( .disclosure .as_deref() .unwrap_or(&evaluation.disclosure), - request.format.as_deref().unwrap_or(&evaluation.format), + &evaluation.format, Some(profile_id), ) { return credential_denial_response_for_evaluation( diff --git a/crates/registry-notary-server/src/api/oid4vci/credential.rs b/crates/registry-notary-server/src/api/oid4vci/credential.rs index 8e6aa9e4e..fb84a9eac 100644 --- a/crates/registry-notary-server/src/api/oid4vci/credential.rs +++ b/crates/registry-notary-server/src/api/oid4vci/credential.rs @@ -198,7 +198,7 @@ pub(in crate::api) async fn oid4vci_credential( .map(|claim_id| ClaimRef::from(claim_id.as_str())) .collect(), disclosure: None, - format: Some(FORMAT_SD_JWT_VC.to_string()), + format: Some(FORMAT_CLAIM_RESULT_JSON.to_string()), purpose: None, }; let mut request = request; @@ -545,7 +545,7 @@ pub(in crate::api) fn oid4vci_issuance_authorization_details( locations: vec![evidence.service_id.clone()], claims, disclosure: Some(disclosure), - format: Some(FORMAT_SD_JWT_VC.to_string()), + format: Some(FORMAT_CLAIM_RESULT_JSON.to_string()), purpose: Some(purpose), subject: Some(registry_notary_core::EvidenceAuthorizationSubject { binding_claim: config.subject_binding.token_claim.clone(), diff --git a/crates/registry-notary-server/src/api/tests/credentials.rs b/crates/registry-notary-server/src/api/tests/credentials.rs index 7aa3fd72b..9fd29fccc 100644 --- a/crates/registry-notary-server/src/api/tests/credentials.rs +++ b/crates/registry-notary-server/src/api/tests/credentials.rs @@ -69,7 +69,7 @@ async fn issue_credential_fails_closed_when_status_record_write_fails() { claim_ids: vec!["person-is-alive".to_string()], claim_refs: Vec::new(), disclosure: "predicate".to_string(), - format: FORMAT_SD_JWT_VC.to_string(), + format: FORMAT_CLAIM_RESULT_JSON.to_string(), results: vec![claim_result_view( "eval-status-write-fails", "person-is-alive", @@ -155,7 +155,7 @@ async fn issue_credential_rejects_purpose_mismatch() { claim_ids: vec!["person-is-alive".to_string()], claim_refs: Vec::new(), disclosure: "predicate".to_string(), - format: FORMAT_SD_JWT_VC.to_string(), + format: FORMAT_CLAIM_RESULT_JSON.to_string(), results: vec![claim_result_view( "eval-purpose-mismatch", "person-is-alive", diff --git a/crates/registry-notary-server/src/api/tests/evaluations.rs b/crates/registry-notary-server/src/api/tests/evaluations.rs index b34102437..4a3397f2b 100644 --- a/crates/registry-notary-server/src/api/tests/evaluations.rs +++ b/crates/registry-notary-server/src/api/tests/evaluations.rs @@ -310,7 +310,7 @@ fn evaluation_access_uses_stored_claim_version_scope() { claim_ids: vec!["person-is-alive".to_string()], claim_refs: vec![ClaimRef::with_version("person-is-alive", "2.0")], disclosure: "predicate".to_string(), - format: FORMAT_SD_JWT_VC.to_string(), + format: FORMAT_CLAIM_RESULT_JSON.to_string(), results: Vec::new(), created_at: "2026-05-23T00:00:00Z".to_string(), expires_at: "2999-01-01T00:00:00Z".to_string(), diff --git a/crates/registry-notary-server/src/api/tests/oid4vci.rs b/crates/registry-notary-server/src/api/tests/oid4vci.rs index ac3d9e976..b73858ba2 100644 --- a/crates/registry-notary-server/src/api/tests/oid4vci.rs +++ b/crates/registry-notary-server/src/api/tests/oid4vci.rs @@ -629,17 +629,8 @@ async fn oid4vci_token_error_fails_closed_when_denial_audit_fails() { #[tokio::test] async fn oid4vci_credential_issues_sd_jwt_and_rejects_nonce_replay() { let store = Arc::new(EvidenceStore::default()); - let mut subject_access = subject_access_config(); - subject_access - .allowed_formats - .push(FORMAT_SD_JWT_VC.to_string()); + let subject_access = subject_access_config(); let mut evidence = evidence_config(); - evidence - .claims - .first_mut() - .expect("claim exists") - .formats - .push(FORMAT_SD_JWT_VC.to_string()); evidence .claims .first_mut() @@ -829,17 +820,8 @@ async fn oid4vci_credential_issues_sd_jwt_and_rejects_nonce_replay() { #[tokio::test] async fn oid4vci_rejects_holder_key_equal_to_issuer_key_before_side_effects() { let store = Arc::new(EvidenceStore::default()); - let mut subject_access = subject_access_config(); - subject_access - .allowed_formats - .push(FORMAT_SD_JWT_VC.to_string()); + let subject_access = subject_access_config(); let mut evidence = evidence_config(); - evidence - .claims - .first_mut() - .expect("claim exists") - .formats - .push(FORMAT_SD_JWT_VC.to_string()); evidence .claims .first_mut() @@ -1013,7 +995,7 @@ fn oid4vci_issuance_authorization_details_bind_selected_configuration() { assert_eq!(details.locations, vec![evidence.service_id.clone()]); assert_eq!(details.claims, vec![ClaimRef::from("person-is-alive")]); assert_eq!(details.disclosure.as_deref(), Some("predicate")); - assert_eq!(details.format.as_deref(), Some(FORMAT_SD_JWT_VC)); + assert_eq!(details.format.as_deref(), Some(FORMAT_CLAIM_RESULT_JSON)); assert_eq!(details.purpose.as_deref(), Some("citizen_subject_access")); assert_eq!(details.access_mode, Some(AccessMode::SubjectBound)); let subject = details.subject.as_ref().expect("subject binding is set"); diff --git a/crates/registry-notary-server/src/api/tests/support.rs b/crates/registry-notary-server/src/api/tests/support.rs index babc65b2c..9f5cc37df 100644 --- a/crates/registry-notary-server/src/api/tests/support.rs +++ b/crates/registry-notary-server/src/api/tests/support.rs @@ -364,7 +364,6 @@ fn oid4vci_evidence_config() -> EvidenceConfig { let mut evidence = evidence_config(); let claim = evidence.claims.first_mut().expect("claim exists"); - claim.formats.push(FORMAT_SD_JWT_VC.to_string()); claim .credential_profiles .push("civil_status_sd_jwt".to_string()); @@ -819,7 +818,7 @@ evaluation_profiles: claim_ids: vec!["claim-a".to_string()], claim_refs: Vec::new(), disclosure: "redacted".to_string(), - format: FORMAT_SD_JWT_VC.to_string(), + format: FORMAT_CLAIM_RESULT_JSON.to_string(), results: Vec::new(), created_at: "1970-01-01T00:00:00Z".to_string(), expires_at: "1970-01-01T00:00:00Z".to_string(), @@ -845,7 +844,7 @@ evaluation_profiles: satisfied: Some(true), disclosure: "predicate".to_string(), redacted_fields: Vec::new(), - format: FORMAT_SD_JWT_VC.to_string(), + format: FORMAT_CLAIM_RESULT_JSON.to_string(), issued_at: "2026-05-23T00:00:00Z".to_string(), expires_at: None, provenance: registry_notary_core::ClaimProvenance::new( diff --git a/crates/registry-notary-server/src/openapi.rs b/crates/registry-notary-server/src/openapi.rs index 0056f92dc..1376ca731 100644 --- a/crates/registry-notary-server/src/openapi.rs +++ b/crates/registry-notary-server/src/openapi.rs @@ -3580,8 +3580,7 @@ fn farmer_under_4ha_claim_example() -> Value { }, "formats": [ "application/vnd.registry-notary.claim-result+json", - "application/ld+json; profile=\"cccev\"", - "application/dc+sd-jwt" + "application/ld+json; profile=\"cccev\"" ], "disclosure": { "default": "predicate", diff --git a/crates/registry-notary-server/tests/standalone_http/credentials.rs b/crates/registry-notary-server/tests/standalone_http/credentials.rs index 4b14c9065..6eab011ad 100644 --- a/crates/registry-notary-server/tests/standalone_http/credentials.rs +++ b/crates/registry-notary-server/tests/standalone_http/credentials.rs @@ -265,13 +265,6 @@ pub(super) async fn direct_credentials_issue_creates_retrievable_status_record() ); enable_credential_status(&mut config); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -297,11 +290,15 @@ pub(super) async fn direct_credentials_issue_creates_retrievable_status_record() "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); let evaluate_body: Value = evaluate.json(); + assert_eq!( + evaluate_body["results"][0]["format"], + json!("application/vnd.registry-notary.claim-result+json") + ); let evaluation_id = evaluate_body["results"][0]["evaluation_id"] .as_str() .expect("evaluation id returned") @@ -332,6 +329,7 @@ pub(super) async fn direct_credentials_issue_creates_retrievable_status_record() issue_body["credential_profile"], json!("civil_status_sd_jwt") ); + assert_eq!(issue_body["format"], json!("application/dc+sd-jwt")); let issuer_signed_jwt = issue_body["issuer_signed_jwt"] .as_str() .expect("issuer signed JWT returned"); @@ -376,19 +374,12 @@ pub(super) async fn direct_credential_operation_denial_is_audited_and_preserves_ let idp = MockIdp::start().await; let tmp = TempDir::new().expect("tempdir"); let audit_path = tmp.path().join("audit.jsonl"); - let mut config = subject_access_oidc_config( + let config = subject_access_oidc_config( "http://127.0.0.1:1", audit_path.to_str().expect("audit path is UTF-8"), &idp.issuer(), &idp.jwks_uri(), ); - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); assert!(config.subject_access.allowed_operations.evaluate); assert!(!config.subject_access.allowed_operations.issue_credential); let app = standalone_router(config) @@ -416,7 +407,7 @@ pub(super) async fn direct_credential_operation_denial_is_audited_and_preserves_ "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); @@ -531,13 +522,6 @@ pub(super) async fn direct_credential_rate_limit_is_audited_with_stored_context( .rate_limits .credential_issuance_per_principal_per_hour = 1; config.subject_access.token_policy.max_auth_age_seconds = 60; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -575,7 +559,7 @@ pub(super) async fn direct_credential_rate_limit_is_audited_with_stored_context( "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); @@ -783,13 +767,6 @@ pub(super) async fn direct_credential_holder_proof_replay_is_audited_and_redacte &idp.jwks_uri(), ); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -815,7 +792,7 @@ pub(super) async fn direct_credential_holder_proof_replay_is_audited_and_redacte "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); @@ -942,13 +919,6 @@ pub(super) async fn strict_credentials_issue_rejects_oid4vci_proof_at_http_bound &idp.jwks_uri(), ); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -974,7 +944,7 @@ pub(super) async fn strict_credentials_issue_rejects_oid4vci_proof_at_http_bound "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); @@ -1082,13 +1052,6 @@ pub(super) async fn direct_credential_purpose_mismatch_denial_is_audited_and_red &idp.jwks_uri(), ); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -1114,7 +1077,7 @@ pub(super) async fn direct_credential_purpose_mismatch_denial_is_audited_and_red "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); @@ -1210,13 +1173,6 @@ pub(super) async fn direct_credential_binding_denials_are_audited_and_redacted() &idp.jwks_uri(), ); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -1242,7 +1198,7 @@ pub(super) async fn direct_credential_binding_denials_are_audited_and_redacted() "target": person_identifier_target("national_id", "person-1"), "claims": ["person-is-alive"], "disclosure": "value", - "format": "application/dc+sd-jwt" + "format": "application/vnd.registry-notary.claim-result+json" })) .await; evaluate.assert_status_ok(); diff --git a/crates/registry-notary-server/tests/standalone_http/federation.rs b/crates/registry-notary-server/tests/standalone_http/federation.rs index ec180d3bd..5705d8758 100644 --- a/crates/registry-notary-server/tests/standalone_http/federation.rs +++ b/crates/registry-notary-server/tests/standalone_http/federation.rs @@ -357,7 +357,6 @@ subject_access: - person-is-alive allowed_formats: - application/vnd.registry-notary.claim-result+json - - application/dc+sd-jwt allowed_disclosures: - value - redacted @@ -451,10 +450,7 @@ pub(super) fn add_subject_access_projection_claim( .to_string(), bindings: Default::default(), }; - claim.formats = vec![ - "application/vnd.registry-notary.claim-result+json".to_string(), - "application/dc+sd-jwt".to_string(), - ]; + claim.formats = vec!["application/vnd.registry-notary.claim-result+json".to_string()]; claim.credential_profiles = vec!["civil_status_sd_jwt".to_string()]; config.evidence.claims.push(claim); config diff --git a/crates/registry-notary-server/tests/standalone_http/oid4vci.rs b/crates/registry-notary-server/tests/standalone_http/oid4vci.rs index 8a85d5e22..74cc0c5cb 100644 --- a/crates/registry-notary-server/tests/standalone_http/oid4vci.rs +++ b/crates/registry-notary-server/tests/standalone_http/oid4vci.rs @@ -1104,13 +1104,6 @@ pub(super) async fn oid4vci_credential_route_issues_holder_bound_sd_jwt() { vec!["registry_notary:admin".to_string()], ); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); @@ -1468,13 +1461,6 @@ pub(super) async fn oid4vci_credential_route_rejects_replayed_nonce() { &idp.jwks_uri(), ); config.subject_access.allowed_operations.issue_credential = true; - config - .evidence - .claims - .first_mut() - .expect("person-is-alive claim exists") - .formats - .push("application/dc+sd-jwt".to_string()); let app = standalone_router(config) .await .expect("standalone router builds"); diff --git a/crates/registry-notary-server/tests/standalone_http/preauth.rs b/crates/registry-notary-server/tests/standalone_http/preauth.rs index fae50adb9..ea558254a 100644 --- a/crates/registry-notary-server/tests/standalone_http/preauth.rs +++ b/crates/registry-notary-server/tests/standalone_http/preauth.rs @@ -522,7 +522,7 @@ pub(super) async fn preauth_token_endpoint_issues_access_token_and_c_nonce() { ); assert_eq!( claims["authorization_details"][0]["format"], - json!("application/dc+sd-jwt") + json!("application/vnd.registry-notary.claim-result+json") ); assert_eq!( claims["authorization_details"][0]["purpose"], @@ -1619,6 +1619,7 @@ pub(super) async fn preauth_end_to_end_issues_sd_jwt_vc_bound_to_holder() { .await; credential.assert_status_ok(); let credential_body: Value = credential.json(); + assert_eq!(credential_body["format"], json!("dc+sd-jwt")); let sd_jwt = credential_body["credential"] .as_str() .expect("credential issued"); diff --git a/crates/registry-notary-server/tests/standalone_http/preauth_support.rs b/crates/registry-notary-server/tests/standalone_http/preauth_support.rs index b605b3431..5c89a0d94 100644 --- a/crates/registry-notary-server/tests/standalone_http/preauth_support.rs +++ b/crates/registry-notary-server/tests/standalone_http/preauth_support.rs @@ -63,15 +63,6 @@ pub(super) fn subject_access_preauth_config( // The credential endpoint must be allowed to issue credentials for the // pre-auth happy path. config.subject_access.allowed_operations.issue_credential = true; - // The person-is-alive claim must support the SD-JWT VC format for OID4VCI - // issuance (the base config only lists the claim-result format). - for claim in config.evidence.claims.iter_mut() { - if claim.id == "person-is-alive" { - claim - .formats - .push(registry_notary_core::FORMAT_SD_JWT_VC.to_string()); - } - } config .subject_access .rate_limits diff --git a/crates/registry-notary-server/tests/subject_access_guard_test.rs b/crates/registry-notary-server/tests/subject_access_guard_test.rs index d7dd24fc6..6e162c46c 100644 --- a/crates/registry-notary-server/tests/subject_access_guard_test.rs +++ b/crates/registry-notary-server/tests/subject_access_guard_test.rs @@ -312,7 +312,6 @@ evidence: allowed: [redacted] formats: - {FORMAT_CLAIM_RESULT_JSON} - - {FORMAT_SD_JWT_VC} credential_profiles: - civil_status_sd_jwt subject_access: @@ -342,7 +341,6 @@ subject_access: - person-is-alive allowed_formats: - {FORMAT_CLAIM_RESULT_JSON} - - {FORMAT_SD_JWT_VC} allowed_disclosures: - redacted required_scopes: @@ -636,7 +634,7 @@ async fn subject_access_credential_issuance_hides_other_principal_evaluation_ids "target": subject_access_target(), "claims": ["person-is-alive"], "disclosure": "redacted", - "format": FORMAT_SD_JWT_VC + "format": FORMAT_CLAIM_RESULT_JSON })) .await; evaluate.assert_status_ok(); @@ -740,7 +738,7 @@ async fn subject_access_credential_issuance_requires_holder_proof_and_hides_civi "target": subject_access_target(), "claims": ["person-is-alive"], "disclosure": "redacted", - "format": FORMAT_SD_JWT_VC + "format": FORMAT_CLAIM_RESULT_JSON })) .await; evaluate.assert_status_ok(); @@ -851,7 +849,7 @@ async fn subject_access_credential_issuance_rejects_disallowed_profile() { "target": subject_access_target(), "claims": ["person-is-alive"], "disclosure": "redacted", - "format": FORMAT_SD_JWT_VC + "format": FORMAT_CLAIM_RESULT_JSON })) .await; evaluate.assert_status_ok(); diff --git a/crates/registry-notary/tests/doctor_cli.rs b/crates/registry-notary/tests/doctor_cli.rs index f6c3059a9..a82b78920 100644 --- a/crates/registry-notary/tests/doctor_cli.rs +++ b/crates/registry-notary/tests/doctor_cli.rs @@ -132,7 +132,7 @@ fn write_config_with_options(tmp: &TempDir, options: TestConfigOptions<'_>) -> P type: cel expression: "true" formats: - - application/dc+sd-jwt + - application/vnd.registry-notary.claim-result+json credential_profiles: - unbound_sd_jwt "#, @@ -197,15 +197,15 @@ fn write_invalid_config(tmp: &TempDir) -> PathBuf { path } -fn write_config_with_empty_claim_formats(tmp: &TempDir) -> PathBuf { +fn write_config_with_claim_formats(tmp: &TempDir, claim_id: &str, formats: &[&str]) -> PathBuf { let path = write_config(tmp); let mut config: registry_notary_core::StandaloneRegistryNotaryConfig = serde_norway::from_str(&std::fs::read_to_string(&path).expect("config reads")) .expect("config parses"); - config.evidence.claims = vec![serde_norway::from_str( + let mut claim: registry_notary_core::ClaimDefinition = serde_norway::from_str(&format!( r#" -id: empty-format -title: Empty format claim +id: {claim_id} +title: Claim format validation version: "1.0" subject_type: person evidence_mode: @@ -213,10 +213,11 @@ evidence_mode: rule: type: cel expression: "true" -formats: [] "#, - ) - .expect("claim YAML parses")]; + )) + .expect("claim YAML parses"); + claim.formats = formats.iter().map(ToString::to_string).collect(); + config.evidence.claims = vec![claim]; std::fs::write( &path, serde_norway::to_string(&config).expect("config serializes"), @@ -225,6 +226,10 @@ formats: [] path } +fn write_config_with_empty_claim_formats(tmp: &TempDir) -> PathBuf { + write_config_with_claim_formats(tmp, "empty-format", &[]) +} + fn doctor_command(config_path: &Path, env_file: Option<&Path>) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_registry-notary")); command.arg("--config").arg(config_path); @@ -1073,3 +1078,99 @@ fn doctor_json_reports_empty_claim_formats_with_remediation() { "remediation is reported: {message}" ); } + +#[test] +fn doctor_json_reports_sd_jwt_claim_format_with_remediation() { + let tmp = TempDir::new().expect("tempdir"); + let config = write_config_with_claim_formats(&tmp, "sd-jwt-format", &["application/dc+sd-jwt"]); + + let output = doctor_command(&config, None) + .args(["--format", "json"]) + .output() + .expect("doctor runs"); + + assert!( + !output.status.success(), + "doctor must reject SD-JWT formats" + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("doctor emits JSON"); + assert_product_diagnostic_report(&report); + let diagnostic = diagnostic_with_code(&report, "failed").expect("failure diagnostic"); + let message = diagnostic["message"].as_str().expect("message string"); + assert!( + message.contains("sd-jwt-format"), + "claim id is reported: {message}" + ); + assert!( + message.contains("application/dc+sd-jwt") && message.contains("credential_profiles"), + "offending format and remediation are reported: {message}" + ); +} + +#[test] +fn doctor_json_reports_unknown_claim_format_with_remediation() { + let tmp = TempDir::new().expect("tempdir"); + let config = write_config_with_claim_formats( + &tmp, + "unknown-format", + &[ + "application/vnd.registry-notary.claim-result+json", + "application/example+json", + ], + ); + + let output = doctor_command(&config, None) + .args(["--format", "json"]) + .output() + .expect("doctor runs"); + + assert!( + !output.status.success(), + "doctor must reject unknown formats" + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("doctor emits JSON"); + assert_product_diagnostic_report(&report); + let diagnostic = diagnostic_with_code(&report, "failed").expect("failure diagnostic"); + let message = diagnostic["message"].as_str().expect("message string"); + assert!( + message.contains("unknown-format"), + "claim id is reported: {message}" + ); + assert!( + message.contains("application/example+json") && message.contains("supported formats"), + "offending format and remediation are reported: {message}" + ); +} + +#[test] +fn doctor_json_reports_missing_canonical_claim_format_with_remediation() { + let tmp = TempDir::new().expect("tempdir"); + let config = write_config_with_claim_formats( + &tmp, + "missing-canonical", + &["application/ld+json; profile=\"cccev\""], + ); + + let output = doctor_command(&config, None) + .args(["--format", "json"]) + .output() + .expect("doctor runs"); + + assert!( + !output.status.success(), + "doctor must reject formats without the canonical renderer" + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("doctor emits JSON"); + assert_product_diagnostic_report(&report); + let diagnostic = diagnostic_with_code(&report, "failed").expect("failure diagnostic"); + let message = diagnostic["message"].as_str().expect("message string"); + assert!( + message.contains("missing-canonical"), + "claim id is reported: {message}" + ); + assert!( + message.contains("application/vnd.registry-notary.claim-result+json") + && message.contains("add it"), + "canonical format and remediation are reported: {message}" + ); +} diff --git a/crates/registryctl/src/project_authoring/compiler/notary.rs b/crates/registryctl/src/project_authoring/compiler/notary.rs index 6662d4fce..9b46f0e1b 100644 --- a/crates/registryctl/src/project_authoring/compiler/notary.rs +++ b/crates/registryctl/src/project_authoring/compiler/notary.rs @@ -78,16 +78,11 @@ fn generated_notary_config( .filter(|(_, credential)| credential.claims.iter().any(|id| id == claim_id)) .map(|(credential, _)| bounded_join_id(&[service_id, credential])) .collect::>>()?; - let mut formats = vec!["application/vnd.registry-notary.claim-result+json".to_string()]; - formats.extend( - service - .credential_profiles - .values() - .filter(|credential| credential.claims.iter().any(|id| id == claim_id)) - .map(|credential| normalize_credential_format(&credential.format)), - ); - formats.sort(); - formats.dedup(); + // Claim formats describe evaluation renderers, not credential + // issuance. SD-JWT VC stays on the credential profile it belongs + // to, so every generated claim retains the canonical evaluation + // response format. + let formats = vec!["application/vnd.registry-notary.claim-result+json".to_string()]; let (default_disclosure, allowed_disclosures) = expanded_disclosure(&claim.disclosure); let (evidence_mode, value_type, nullable, rule) = match inferred_claim_evidence(service, claim)? { @@ -436,7 +431,7 @@ fn add_oid4vci_config( }, "allowed_purposes": [service.purpose], "allowed_claims": [claim_id], - "allowed_formats": ["application/dc+sd-jwt"], + "allowed_formats": ["application/vnd.registry-notary.claim-result+json"], "allowed_disclosures": allowed_disclosures, "scope_policy": "disabled", "required_scopes": [], diff --git a/crates/registryctl/tests/project_authoring.rs b/crates/registryctl/tests/project_authoring.rs index 6d6bd1f4a..7c0feb722 100644 --- a/crates/registryctl/tests/project_authoring.rs +++ b/crates/registryctl/tests/project_authoring.rs @@ -3405,7 +3405,7 @@ fn check_and_build_produce_deterministic_product_inputs() { assert_eq!(first_closure, directory_closure(&output)); assert_eq!( closure_digest(&first_closure), - "3acc1511dd07eff495355319ce1ef4d82dead98bd9ea2109df43aff8e4d1e985", + "b2b6a70bc5e15f330f81d069af55024ea8473c637ed33d4a9d364a6e0f091fc6", "project inputs must match the cross-machine golden digest" ); } @@ -4257,6 +4257,13 @@ fn authored_oid4vci_binding_generates_the_complete_notary_owned_issuer() { notary["subject_access"]["allowed_claims"][0].as_str(), Some("household-record-exists") ); + assert_eq!( + notary["subject_access"]["allowed_formats"], + serde_yaml::from_str::( + "[application/vnd.registry-notary.claim-result+json]" + ) + .expect("canonical evaluation format parses") + ); assert_eq!( notary["subject_access"]["allowed_wallet_origins"][0].as_str(), Some("https://wallet.example.invalid") diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index db81127f8..40478292e 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -183,9 +183,10 @@ model](../../spec/rs-dm-manifest/). rejects it for both registry-backed and self-attested evidence modes. A conforming claim uses `consultation_output`, `consultation_matched`, or `cel` under the constraints of its evidence mode. -- An empty formats list cannot be evaluated: A claim whose `formats` list is empty does not admit - even the default claim-result JSON format. Project-generated claims declare their supported - formats explicitly. +- Claim evaluation formats are closed: A claim must include the canonical + `application/vnd.registry-notary.claim-result+json` response format and may additionally use + `application/ld+json; profile="cccev"`. An empty list, SD-JWT VC, and unknown formats are + rejected during configuration load. SD-JWT VC remains a credential-profile issuance format. - 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 cd15928ab..49f4ab514 100644 --- a/docs/site/src/content/docs/spec/rs-dm-claim.mdx +++ b/docs/site/src/content/docs/spec/rs-dm-claim.mdx @@ -40,6 +40,7 @@ The key words in this document are interpreted per [RS-DOC](../rs-doc/) Section | 0.3.1 | 2026-07-14 | draft | Updated load-time validation limits after duplicate ids, disclosure defaults, evidence-mode rules, and registry-backed output types became startup checks. | | 0.3.2 | 2026-07-14 | draft | Aligned credential eligibility with Registry Stack project authoring: profiles declare claims once, and the compiler derives the product-level reverse index. | | 0.3.3 | 2026-07-17 | draft | Made omitted claim `formats` default to the canonical claim-result JSON representation and made an explicitly empty list a configuration-load error. | +| 0.3.4 | 2026-07-17 | draft | Made the evaluation response-format set closed: the canonical claim-result JSON format is required, CCCEV-shaped JSON-LD is the only additional renderer, and credential issuance formats are rejected. | ## 1. Scope and references @@ -127,7 +128,7 @@ REQ-DM-CLAIM-008: Registry Notary MUST apply a claim's `default` disclosure mode A claim's `formats` list declares the response formats it can render, and its value descriptor declares the type the evaluated value takes. -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). When `formats` is omitted, Registry Notary uses the claim-result JSON media type. An explicitly empty `formats` list MUST be rejected at configuration load. 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's `formats` MUST include `application/vnd.registry-notary.claim-result+json`, and MAY additionally include only `application/ld+json; profile="cccev"` (the data-model form of REQ-PR-NOTARY-011). When `formats` is omitted, Registry Notary uses the claim-result JSON media type. An explicitly empty list, an unknown format, or a list that omits the canonical format MUST be rejected at configuration load. SD-JWT VC issuance uses `application/dc+sd-jwt` in credential profiles and MUST NOT appear in a claim 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. ## 8. Credential eligibility diff --git a/products/notary/CHANGELOG.md b/products/notary/CHANGELOG.md index fd86ab8eb..d12bb4124 100644 --- a/products/notary/CHANGELOG.md +++ b/products/notary/CHANGELOG.md @@ -37,7 +37,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Omitted claim `formats` now defaults to the canonical `application/vnd.registry-notary.claim-result+json` evaluation response format. An explicitly empty `formats: []` is rejected at configuration load - with the claim id and remediation. + with the claim id and remediation. Explicit lists must include the canonical + format and may otherwise contain only `application/ld+json; profile="cccev"`. + SD-JWT VC is a credential-profile issuance format, not a claim evaluation + renderer. Subject-access `allowed_formats` now follows the same evaluation + boundary and no longer needs to repeat the credential profile's output + format. ## [0.10.0] - 2026-07-17 diff --git a/products/notary/docs/operator-config-reference.md b/products/notary/docs/operator-config-reference.md index b63cc86e1..b6900aff0 100644 --- a/products/notary/docs/operator-config-reference.md +++ b/products/notary/docs/operator-config-reference.md @@ -80,8 +80,13 @@ Relay outputs are never credentials or public claims by themselves. `formats` is optional for each claim. When omitted, Notary uses `application/vnd.registry-notary.claim-result+json`, the canonical evaluation -response format. Do not set `formats: []`: it is invalid configuration. List -one or more formats only when the claim needs an explicit response-format set. +response format. An explicit list must include that format and may otherwise +contain only `application/ld+json; profile="cccev"`. Do not set `formats: []`, +an unknown format, or `application/dc+sd-jwt`: all are invalid configuration. +SD-JWT VC belongs in a credential profile's issuance `format`, not a claim's +evaluation response-format list. The same separation applies to +`subject_access.allowed_formats`: list evaluation renderers there and select +the credential output through `credential_profiles`. ## Authentication and delegation diff --git a/products/notary/docs/self-attestation-operator-guide.md b/products/notary/docs/self-attestation-operator-guide.md index d09b08120..06c74d736 100644 --- a/products/notary/docs/self-attestation-operator-guide.md +++ b/products/notary/docs/self-attestation-operator-guide.md @@ -210,7 +210,7 @@ subject_access: allowed_claims: - birth-record-exists allowed_formats: - - application/dc+sd-jwt + - application/vnd.registry-notary.claim-result+json allowed_disclosures: - value - redacted @@ -221,6 +221,9 @@ subject_access: Rules: - Enable only operations the citizen flow actually needs. +- Include the canonical claim-result JSON format for the internal evaluation + that backs issuance. The named credential profile separately selects + `application/dc+sd-jwt` as the credential output. - `batch_evaluate` must remain false; batch evaluation is not supported. - `allowed_claims` must reference existing claims. - `credential_profiles` must reference existing profiles. diff --git a/products/notary/openapi/registry-notary.openapi.json b/products/notary/openapi/registry-notary.openapi.json index 8f9f933e2..414195e47 100644 --- a/products/notary/openapi/registry-notary.openapi.json +++ b/products/notary/openapi/registry-notary.openapi.json @@ -3889,8 +3889,7 @@ "evidence_type_iri": "https://demo.example.gov/evidence-types/smallholder-farmer", "formats": [ "application/vnd.registry-notary.claim-result+json", - "application/ld+json; profile=\"cccev\"", - "application/dc+sd-jwt" + "application/ld+json; profile=\"cccev\"" ], "id": "farmer-under-4ha", "oots": null, @@ -4010,8 +4009,7 @@ "evidence_type_iri": "https://demo.example.gov/evidence-types/smallholder-farmer", "formats": [ "application/vnd.registry-notary.claim-result+json", - "application/ld+json; profile=\"cccev\"", - "application/dc+sd-jwt" + "application/ld+json; profile=\"cccev\"" ], "id": "farmer-under-4ha", "oots": null, diff --git a/products/notary/specs/citizen-self-attestation-spec.md b/products/notary/specs/citizen-self-attestation-spec.md index 318210e39..cd195ac7a 100644 --- a/products/notary/specs/citizen-self-attestation-spec.md +++ b/products/notary/specs/citizen-self-attestation-spec.md @@ -184,7 +184,6 @@ self_attestation: - person-is-alive allowed_formats: - application/vnd.registry-notary.claim-result+json - - application/dc+sd-jwt allowed_disclosures: - predicate - value @@ -523,7 +522,7 @@ bounded operational details: "self_attestation": { "enabled": true, "allowed_claim_ids": ["person-is-alive"], - "allowed_formats": ["application/vnd.registry-notary.claim-result+json", "application/dc+sd-jwt"], + "allowed_formats": ["application/vnd.registry-notary.claim-result+json"], "credential_profile_ids": ["civil_status_sd_jwt"] } }