2424//! would force a JSON-LD context processor onto the lean build. The proof
2525//! configuration is supplied as a typed [`ProofConfig`] (not extracted from a
2626//! JSON-LD `proof` node), and its canonical RDF form uses the standard
27- //! `https://w3id.org/security#` vocabulary.
27+ //! `https://w3id.org/security#` vocabulary plus Dublin Core `dcterms:created`
28+ //! (see the mapping below).
29+ //!
30+ //! # Proof-configuration RDF mapping
31+ //!
32+ //! [OPUS-5.5] zkp-14.2: the typed [`ProofConfig`] maps to the same RDF the W3C
33+ //! vc-di-eddsa test vectors canonicalize (<https://www.w3.org/TR/vc-di-eddsa/#test-vectors>,
34+ //! `eddsa-rdfc-2022` representation): `created` is
35+ //! `<http://purl.org/dc/terms/created>` typed `xsd:dateTime`, and `cryptosuite`
36+ //! is a literal typed `<https://w3id.org/security#cryptosuiteString>`. The
37+ //! published proof-configuration hash and `proofValue` are regression-tested.
38+ //!
39+ //! **Incompatibility:** earlier releases used `sec:created` and a plain
40+ //! `cryptosuite` literal. Proofs produced by that mapping do **not** verify
41+ //! under this one, and there is deliberately no legacy fallback — re-sign them.
42+ //!
43+ //! Only the typed subset `ProofConfig` carries is represented (type,
44+ //! cryptosuite, verificationMethod, proofPurpose, created, domain, challenge);
45+ //! any other proof option or `@context` a JSON-LD proof may carry is not
46+ //! preserved. Field values (the `created` lexical form, IRIs, purpose) are not
47+ //! fully validated here.
2848
2949use oxrdf:: { Literal , NamedNode , NamedOrBlankNode , Term , Triple } ;
3050use sha2:: { Digest as _, Sha256 } ;
@@ -44,6 +64,12 @@ pub const CRYPTOSUITE: &str = "eddsa-rdfc-2022";
4464const SEC : & str = "https://w3id.org/security#" ;
4565const XSD_DATETIME : & str = "http://www.w3.org/2001/XMLSchema#dateTime" ;
4666const RDF_TYPE : & str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" ;
67+ // [OPUS-5.5] The Data Integrity `@context` maps `created` to Dublin Core, not
68+ // `sec:created`. Changing this breaks every existing proof and the W3C vectors.
69+ const DCTERMS_CREATED : & str = "http://purl.org/dc/terms/created" ;
70+ // [OPUS-5.5] Datatype of the `cryptosuite` literal in the Data Integrity
71+ // `@context`. Changing it breaks every existing proof and the W3C vectors.
72+ const CRYPTOSUITE_STRING : & str = "https://w3id.org/security#cryptosuiteString" ;
4773
4874/// A signing key — a vetted Ed25519 (RFC 8032) keypair.
4975///
@@ -93,7 +119,7 @@ pub struct ProofConfig {
93119 pub verification_method : String ,
94120 /// The proof purpose (`sec:proofPurpose`). Defaults to `assertionMethod`.
95121 pub proof_purpose : String ,
96- /// The proof creation time, an `xsd:dateTime` string (`sec :created`). Optional.
122+ /// The proof creation time, an `xsd:dateTime` string (`dcterms :created`). Optional.
97123 pub created : Option < String > ,
98124 /// An optional domain the proof is bound to (`sec:domain`).
99125 pub domain : Option < String > ,
@@ -323,7 +349,9 @@ fn hash_data(triples: &[Triple], config: &ProofConfig) -> Result<Vec<u8>, VcErro
323349/// Build the RDF triples representing the proof configuration, over a single fixed
324350/// blank-node subject (`_:proof`). RDFC-1.0 relabels the blank node deterministically,
325351/// so the canonical form depends only on the field VALUES — exactly the binding we
326- /// want. Uses the standard `https://w3id.org/security#` vocabulary.
352+ /// want. Uses the standard `https://w3id.org/security#` vocabulary, with
353+ /// `dcterms:created` and a `sec:cryptosuiteString`-typed cryptosuite (see the
354+ /// module-level mapping notes).
327355fn proof_config_triples ( config : & ProofConfig ) -> Vec < Triple > {
328356 let subject = || NamedOrBlankNode :: BlankNode ( oxrdf:: BlankNode :: new_unchecked ( "proof" ) ) ;
329357 let pred = |local : & str | NamedNode :: new_unchecked ( format ! ( "{}{}" , SEC , local) ) ;
@@ -337,11 +365,14 @@ fn proof_config_triples(config: &ProofConfig) -> Vec<Triple> {
337365 NamedNode :: new_unchecked( RDF_TYPE ) ,
338366 Term :: NamedNode ( pred( PROOF_TYPE ) ) ,
339367 ) ,
340- // sec:cryptosuite "eddsa-rdfc-2022"
368+ // sec:cryptosuite "eddsa-rdfc-2022"^^sec:cryptosuiteString
341369 Triple :: new(
342370 subject( ) ,
343371 pred( "cryptosuite" ) ,
344- Term :: Literal ( Literal :: new_simple_literal( CRYPTOSUITE ) ) ,
372+ Term :: Literal ( Literal :: new_typed_literal(
373+ CRYPTOSUITE ,
374+ NamedNode :: new_unchecked( CRYPTOSUITE_STRING ) ,
375+ ) ) ,
345376 ) ,
346377 // sec:verificationMethod <iri>
347378 Triple :: new(
@@ -357,9 +388,10 @@ fn proof_config_triples(config: &ProofConfig) -> Vec<Triple> {
357388 ) ,
358389 ] ;
359390 if let Some ( created) = & config. created {
391+ // dcterms:created "…"^^xsd:dateTime
360392 t. push ( Triple :: new (
361393 subject ( ) ,
362- pred ( "created" ) ,
394+ NamedNode :: new_unchecked ( DCTERMS_CREATED ) ,
363395 Term :: Literal ( Literal :: new_typed_literal (
364396 created. clone ( ) ,
365397 NamedNode :: new_unchecked ( XSD_DATETIME ) ,
@@ -518,4 +550,34 @@ mod tests {
518550 Err ( VcError :: UnsupportedProof ( _) )
519551 ) ) ;
520552 }
553+
554+ /// [OPUS-5.5] zkp-14.2: the proof-config RDF mapping reproduces the W3C
555+ /// vc-di-eddsa `eddsa-rdfc-2022` test vector byte-for-byte — canonical
556+ /// proof-config N-Quads (Example 12) and their published SHA-256 (Example 13).
557+ /// Source: <https://www.w3.org/TR/vc-di-eddsa/#test-vectors> (Recommendation,
558+ /// 15 May 2025, Appendix B.1). Guards against reverting to `sec:created` or a
559+ /// plain `cryptosuite` literal.
560+ #[ test]
561+ fn proof_config_matches_w3c_eddsa_rdfc_2022_vector ( ) {
562+ const VM : & str = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2\
563+ #z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
564+ const EXPECTED_NQUADS : & str = concat ! (
565+ "_:c14n0 <http://purl.org/dc/terms/created> \" 2023-02-24T23:36:38Z\" ^^<http://www.w3.org/2001/XMLSchema#dateTime> .\n " ,
566+ "_:c14n0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/security#DataIntegrityProof> .\n " ,
567+ "_:c14n0 <https://w3id.org/security#cryptosuite> \" eddsa-rdfc-2022\" ^^<https://w3id.org/security#cryptosuiteString> .\n " ,
568+ "_:c14n0 <https://w3id.org/security#proofPurpose> <https://w3id.org/security#assertionMethod> .\n " ,
569+ "_:c14n0 <https://w3id.org/security#verificationMethod> <did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2#z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2> .\n " ,
570+ ) ;
571+ const EXPECTED_SHA256 : & str =
572+ "bea7b7acfbad0126b135104024a5f1733e705108f42d59668b05c0c50004c6b0" ;
573+
574+ let cfg = ProofConfig :: new ( VM ) . with_created ( "2023-02-24T23:36:38Z" ) ;
575+ let canon = sparq_canon:: canonicalize_triples ( & proof_config_triples ( & cfg) ) . unwrap ( ) ;
576+ let nquads = canon. to_nquads ( ) ;
577+ assert_eq ! ( nquads, EXPECTED_NQUADS ) ;
578+ assert_eq ! (
579+ hex:: encode( <Sha256 as sha2:: Digest >:: digest( nquads. as_bytes( ) ) ) ,
580+ EXPECTED_SHA256
581+ ) ;
582+ }
521583}
0 commit comments