1use chrono::{DateTime, Utc};
67use crypto_suites::CryptoSuite;
68use multibase::Base;
69use serde::{Deserialize, Serialize};
70use serde_json_canonicalizer::to_string;
71use sha2::{Digest, Sha256};
72use signer::Signer;
73use tracing::debug;
74
75pub mod caching_signer;
76pub mod conformance;
77pub mod crypto_suites;
78pub mod did_vm;
79pub mod error;
80pub mod multi;
81pub mod options;
82pub mod signer;
83pub mod suite_ops;
84pub mod verification_proof;
85
86pub use caching_signer::{CachingSigner, GetPrivateBytes};
87pub use conformance::verify_conformance;
88pub use did_vm::{DidKeyResolver, ResolvedKey, VerificationMethodResolver};
89pub use multi::{MultiVerifyResult, VerifyPolicy, verify_multi};
90
91#[cfg(feature = "bbs-2023")]
95pub mod bbs_2023;
96
97pub use error::{DataIntegrityError, SignatureFailure};
98pub use options::{SignOptions, VerifyOptions};
99
100#[derive(Clone, Debug, Deserialize, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct DataIntegrityProof {
104 #[serde(rename = "type")]
106 pub type_: String,
107
108 pub cryptosuite: CryptoSuite,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub created: Option<String>,
112
113 pub verification_method: String,
114
115 pub proof_purpose: String,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub proof_value: Option<String>,
119
120 #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
121 pub context: Option<Vec<String>>,
122}
123
124impl DataIntegrityProof {
125 pub async fn sign<S>(
132 data_doc: &S,
133 signer: &dyn Signer,
134 options: SignOptions,
135 ) -> Result<DataIntegrityProof, DataIntegrityError>
136 where
137 S: Serialize,
138 {
139 let crypto_suite = options.cryptosuite.unwrap_or_else(|| signer.cryptosuite());
140 crypto_suite
141 .validate_key_type(signer.key_type())
142 .map_err(|_| DataIntegrityError::KeyTypeMismatch {
143 expected: crypto_suite
144 .compatible_key_types()
145 .first()
146 .copied()
147 .unwrap_or(affinidi_secrets_resolver::secrets::KeyType::Unknown),
148 actual: signer.key_type(),
149 suite: crypto_suite,
150 })?;
151
152 let created_str = options
153 .created
154 .map(format_created)
155 .unwrap_or_else(|| format_created(Utc::now()));
156
157 let proof_purpose = options
158 .proof_purpose
159 .unwrap_or_else(|| "assertionMethod".to_string());
160
161 if crypto_suite.is_rdfc() {
162 sign_rdfc(
163 data_doc,
164 crypto_suite,
165 options.context,
166 signer,
167 created_str,
168 proof_purpose,
169 )
170 .await
171 } else {
172 sign_jcs(
173 data_doc,
174 crypto_suite,
175 options.context,
176 signer,
177 created_str,
178 proof_purpose,
179 )
180 .await
181 }
182 }
183
184 #[must_use = "ignoring a verification result is a security bug"]
193 pub fn verify_with_public_key<S>(
194 &self,
195 data_doc: &S,
196 public_key_bytes: &[u8],
197 options: VerifyOptions,
198 ) -> Result<(), DataIntegrityError>
199 where
200 S: Serialize,
201 {
202 verify_proof_internal(self, data_doc, public_key_bytes, &options)
203 }
204
205 #[must_use = "ignoring a verification result is a security bug"]
219 pub async fn verify<S, R>(
220 &self,
221 data_doc: &S,
222 resolver: &R,
223 options: VerifyOptions,
224 ) -> Result<(), DataIntegrityError>
225 where
226 S: Serialize + Sync,
227 R: VerificationMethodResolver + ?Sized,
228 {
229 let resolved = resolver.resolve_vm(&self.verification_method).await?;
230
231 let compatible = self.cryptosuite.compatible_key_types();
235 if !compatible.is_empty() && !compatible.contains(&resolved.key_type) {
236 return Err(DataIntegrityError::KeyTypeMismatch {
237 expected: compatible
238 .first()
239 .copied()
240 .unwrap_or(affinidi_secrets_resolver::secrets::KeyType::Unknown),
241 actual: resolved.key_type,
242 suite: self.cryptosuite,
243 });
244 }
245
246 verify_proof_internal(self, data_doc, &resolved.public_key_bytes, &options)
247 }
248}
249
250async fn sign_jcs<S>(
255 data_doc: &S,
256 crypto_suite: CryptoSuite,
257 context: Option<Vec<String>>,
258 signer: &dyn Signer,
259 created: String,
260 proof_purpose: String,
261) -> Result<DataIntegrityProof, DataIntegrityError>
262where
263 S: Serialize,
264{
265 let jcs = to_string(data_doc)
266 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
267 debug!("Document (JCS): {}", jcs);
268
269 let mut proof_options = DataIntegrityProof {
270 type_: "DataIntegrityProof".to_string(),
271 cryptosuite: crypto_suite,
272 created: Some(created),
273 verification_method: signer.verification_method().to_string(),
274 proof_purpose,
275 proof_value: None,
276 context,
277 };
278
279 let proof_jcs = to_string(&proof_options)
280 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
281 debug!("Proof options (JCS): {}", proof_jcs);
282
283 let hash_data = hashing_jcs(&jcs, &proof_jcs);
284 let signed = signer.sign(&hash_data).await?;
285 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed));
286
287 Ok(proof_options)
288}
289
290async fn sign_rdfc<S>(
291 data_doc: &S,
292 crypto_suite: CryptoSuite,
293 context: Option<Vec<String>>,
294 signer: &dyn Signer,
295 created: String,
296 proof_purpose: String,
297) -> Result<DataIntegrityProof, DataIntegrityError>
298where
299 S: Serialize,
300{
301 let doc_value = serde_json::to_value(data_doc)
302 .map_err(|e| DataIntegrityError::Canonicalization(format!("document serialize: {e}")))?;
303
304 let proof_context = if let Some(ctx) = context {
306 Some(ctx)
307 } else {
308 match doc_value.get("@context") {
309 Some(serde_json::Value::Array(arr)) => Some(
310 arr.iter()
311 .filter_map(|v| v.as_str().map(str::to_string))
312 .collect(),
313 ),
314 Some(serde_json::Value::String(s)) => Some(vec![s.clone()]),
315 Some(_) => {
316 return Err(DataIntegrityError::MalformedProof(
317 "Invalid @context format in document".to_string(),
318 ));
319 }
320 None => {
321 return Err(DataIntegrityError::MalformedProof(
322 "Document must contain @context for RDFC signing".to_string(),
323 ));
324 }
325 }
326 };
327
328 let mut proof_options = DataIntegrityProof {
329 type_: "DataIntegrityProof".to_string(),
330 cryptosuite: crypto_suite,
331 created: Some(created),
332 verification_method: signer.verification_method().to_string(),
333 proof_purpose,
334 proof_value: None,
335 context: proof_context,
336 };
337
338 let proof_value = serde_json::to_value(&proof_options).map_err(|e| {
339 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
340 })?;
341
342 let hash_data = hashing_rdfc(&doc_value, &proof_value)?;
343 let signed = signer.sign(&hash_data).await?;
344 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed));
345
346 Ok(proof_options)
347}
348
349fn verify_proof_internal<S>(
350 proof: &DataIntegrityProof,
351 signed_doc: &S,
352 public_key_bytes: &[u8],
353 options: &VerifyOptions,
354) -> Result<(), DataIntegrityError>
355where
356 S: Serialize,
357{
358 if !options.allowed_suites.is_empty() && !options.allowed_suites.contains(&proof.cryptosuite) {
360 return Err(DataIntegrityError::Conformance(format!(
361 "cryptosuite {} is not in the caller's allowed suites",
362 String::try_from(proof.cryptosuite).unwrap_or_default()
363 )));
364 }
365
366 if let Some(expected) = &options.expected_context
368 && proof.context.as_ref() != Some(expected)
369 {
370 return Err(DataIntegrityError::Conformance(
371 "Document context does not match proof context".to_string(),
372 ));
373 }
374
375 let Some(proof_value) = &proof.proof_value else {
377 return Err(DataIntegrityError::MalformedProof(
378 "proofValue is missing in the proof".to_string(),
379 ));
380 };
381 let proof_value = multibase::decode(proof_value)
382 .map_err(|e| DataIntegrityError::MalformedProof(format!("Invalid proof value: {e}")))?
383 .1;
384
385 let proof_config = DataIntegrityProof {
387 proof_value: None,
388 ..proof.clone()
389 };
390
391 if proof_config.type_ != "DataIntegrityProof" {
392 return Err(DataIntegrityError::Conformance(
393 "Invalid proof type, expected 'DataIntegrityProof'".to_string(),
394 ));
395 }
396
397 if let Some(created) = &proof_config.created {
398 let now = Utc::now();
399 let created = created
400 .parse::<DateTime<Utc>>()
401 .map_err(|e| DataIntegrityError::Conformance(format!("Invalid created date: {e}")))?;
402 if created > now {
403 return Err(DataIntegrityError::Conformance(
404 "Created date is in the future".to_string(),
405 ));
406 }
407 }
408
409 let hash_data = if proof_config.cryptosuite.is_rdfc() {
411 let doc_value = serde_json::to_value(signed_doc).map_err(|e| {
412 DataIntegrityError::Canonicalization(format!("document serialize: {e}"))
413 })?;
414 let proof_value_json = serde_json::to_value(&proof_config).map_err(|e| {
415 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
416 })?;
417 hashing_rdfc(&doc_value, &proof_value_json)?
418 } else {
419 #[cfg(feature = "bbs-2023")]
420 if matches!(proof_config.cryptosuite, CryptoSuite::Bbs2023) {
421 return Err(DataIntegrityError::UnsupportedCryptoSuite {
422 name: "bbs-2023 proofs must be verified via bbs_2023::verify_proof".to_string(),
423 });
424 }
425 let jcs_doc = to_string(&signed_doc)
426 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
427 let jcs_proof_config = to_string(&proof_config)
428 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
429 hashing_jcs(&jcs_doc, &jcs_proof_config)
430 };
431
432 proof_config
433 .cryptosuite
434 .verify(public_key_bytes, &hash_data, &proof_value)
435}
436
437fn hashing_jcs(transformed_document: &str, canonical_proof_config: &str) -> Vec<u8> {
443 [
444 Sha256::digest(canonical_proof_config),
445 Sha256::digest(transformed_document),
446 ]
447 .concat()
448}
449
450fn hashing_rdfc(
455 document: &serde_json::Value,
456 proof_config: &serde_json::Value,
457) -> Result<Vec<u8>, DataIntegrityError> {
458 let doc_hash = affinidi_rdf_encoding::expand_canonicalize_and_hash(document)
459 .map_err(|e| DataIntegrityError::Canonicalization(format!("RDFC document hash: {e}")))?;
460
461 let proof_hash =
462 affinidi_rdf_encoding::expand_canonicalize_and_hash(proof_config).map_err(|e| {
463 DataIntegrityError::Canonicalization(format!("RDFC proof config hash: {e}"))
464 })?;
465
466 Ok([proof_hash.as_slice(), doc_hash.as_slice()].concat())
467}
468
469pub fn prepare_sign_input<S>(
487 data_doc: &S,
488 proof_config: &DataIntegrityProof,
489 cryptosuite: CryptoSuite,
490) -> Result<Vec<u8>, DataIntegrityError>
491where
492 S: Serialize,
493{
494 if cryptosuite.is_rdfc() {
495 let doc_value = serde_json::to_value(data_doc).map_err(|e| {
496 DataIntegrityError::Canonicalization(format!("document serialize: {e}"))
497 })?;
498 let proof_value = serde_json::to_value(proof_config).map_err(|e| {
499 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
500 })?;
501 hashing_rdfc(&doc_value, &proof_value)
502 } else {
503 let jcs_doc = to_string(data_doc)
504 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
505 let jcs_proof = to_string(proof_config)
506 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
507 Ok(hashing_jcs(&jcs_doc, &jcs_proof))
508 }
509}
510
511fn format_created(dt: DateTime<Utc>) -> String {
516 dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
517}
518
519#[cfg(test)]
520mod tests {
521 use affinidi_secrets_resolver::secrets::Secret;
522 use serde_json::json;
523
524 use crate::{DataIntegrityProof, SignOptions, VerifyOptions, hashing_jcs};
525
526 #[test]
527 fn hashing_working() {
528 let hash = hashing_jcs("test1", "test2");
529 let mut output = String::new();
530 for x in hash {
531 output.push_str(&format!("{x:02x}"));
532 }
533
534 assert_eq!(
535 output.as_str(),
536 "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c7521b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014",
537 );
538 }
539
540 #[tokio::test]
541 async fn sign_and_verify_via_did_key_resolver_ed25519() {
542 use crate::{DidKeyResolver, VerifyOptions};
543
544 let secret = Secret::generate_ed25519(None, Some(&[11u8; 32]));
545 let pk_mb = secret.get_public_keymultibase().unwrap();
546 let mut signer_secret = secret.clone();
548 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
549
550 let doc = json!({ "hello": "did:key" });
551 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
552 .await
553 .expect("sign");
554
555 proof
556 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
557 .await
558 .expect("verify via resolver");
559 }
560
561 #[cfg(feature = "ml-dsa")]
562 #[tokio::test]
563 async fn sign_and_verify_via_did_key_resolver_ml_dsa() {
564 use crate::{DidKeyResolver, VerifyOptions};
565
566 let secret = Secret::generate_ml_dsa_44(None, Some(&[21u8; 32]));
567 let pk_mb = secret.get_public_keymultibase().unwrap();
568 let mut signer_secret = secret.clone();
569 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
570
571 let doc = json!({ "pqc": "did:key" });
572 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
573 .await
574 .expect("sign");
575
576 proof
577 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
578 .await
579 .expect("verify via resolver");
580 }
581
582 #[tokio::test]
583 async fn unified_sign_verify_ed25519_jcs() {
584 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[4u8; 32]));
585 let doc = json!({"hello": "world"});
586 let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
587 .await
588 .expect("sign");
589 proof
590 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
591 .expect("verify");
592 }
593
594 #[cfg(feature = "ml-dsa")]
595 #[tokio::test]
596 async fn unified_sign_verify_ml_dsa_44_jcs() {
597 let secret = Secret::generate_ml_dsa_44(Some("did:key:k#k"), Some(&[8u8; 32]));
598 let doc = json!({"pqc": true});
599 let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
600 .await
601 .expect("sign");
602 assert_eq!(
604 proof.cryptosuite,
605 crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024
606 );
607 proof
608 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
609 .expect("verify");
610 }
611
612 #[cfg(feature = "ml-dsa")]
613 #[tokio::test]
614 async fn override_suite_via_sign_options() {
615 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[1u8; 32]));
618 let doc = json!({"x": 1});
619 let err = DataIntegrityProof::sign(
620 &doc,
621 &secret,
622 SignOptions::new().with_cryptosuite(crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024),
623 )
624 .await
625 .unwrap_err();
626 assert!(matches!(
627 err,
628 crate::DataIntegrityError::KeyTypeMismatch { .. }
629 ));
630 }
631
632 #[tokio::test]
637 async fn verify_rejects_cryptosuite_tampering() {
638 use crate::crypto_suites::CryptoSuite;
639
640 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[77u8; 32]));
641 let doc = json!({"tamper": "target"});
642 let mut proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
643 .await
644 .expect("sign");
645 assert_eq!(proof.cryptosuite, CryptoSuite::EddsaJcs2022);
646
647 proof.cryptosuite = CryptoSuite::EddsaRdfc2022;
649
650 let err = proof
651 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
652 .unwrap_err();
653 assert!(
654 matches!(err, crate::DataIntegrityError::InvalidSignature { .. }),
655 "expected InvalidSignature after cryptosuite tampering, got: {err:?}"
656 );
657 }
658
659 #[tokio::test]
660 async fn deterministic_signing_same_input_same_output() {
661 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[2u8; 32]));
662 let doc = json!({"deterministic": "yes"});
663 let created = chrono::Utc::now();
664 let opts = || SignOptions::new().with_created(created);
665 let a = DataIntegrityProof::sign(&doc, &secret, opts())
666 .await
667 .unwrap();
668 let b = DataIntegrityProof::sign(&doc, &secret, opts())
669 .await
670 .unwrap();
671 assert_eq!(
672 a.proof_value, b.proof_value,
673 "Ed25519 must be deterministic"
674 );
675 }
676
677 #[cfg(feature = "ml-dsa")]
678 #[tokio::test]
679 async fn deterministic_signing_ml_dsa() {
680 let secret = Secret::generate_ml_dsa_44(Some("did:key:k#k"), Some(&[5u8; 32]));
681 let doc = json!({"deterministic": "pqc"});
682 let created = chrono::Utc::now();
683 let opts = || SignOptions::new().with_created(created);
684 let a = DataIntegrityProof::sign(&doc, &secret, opts())
685 .await
686 .unwrap();
687 let b = DataIntegrityProof::sign(&doc, &secret, opts())
688 .await
689 .unwrap();
690 assert_eq!(a.proof_value, b.proof_value, "ML-DSA must be deterministic");
691 }
692
693 #[tokio::test]
694 async fn test_sign_bad_key() {
695 let generic_doc = json!({"test": "test_data"});
696 let pub_key = "zruqgFba156mDWfMUjJUSAKUvgCgF5NfgSYwSuEZuXpixts8tw3ot5BasjeyM65f8dzk5k6zgXf7pkbaaBnPrjCUmcJ";
697 let pri_key = "z42tmXtqqQBLmEEwn8tfi1bA2ghBx9cBo6wo8a44kVJEiqyA";
698 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
699 .expect("Couldn't create test key data");
700 assert!(
701 DataIntegrityProof::sign(&generic_doc, &secret, SignOptions::new())
702 .await
703 .is_err()
704 );
705 }
706
707 #[tokio::test]
708 async fn test_sign_good() {
709 let generic_doc = json!({"test": "test_data"});
710 let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
711 let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
712 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
713 .expect("Couldn't create test key data");
714 let context = vec![
715 "context1".to_string(),
716 "context2".to_string(),
717 "context3".to_string(),
718 ];
719 assert!(
720 DataIntegrityProof::sign(
721 &generic_doc,
722 &secret,
723 SignOptions::new().with_context(context)
724 )
725 .await
726 .is_ok(),
727 "Signing failed"
728 );
729 }
730
731 #[cfg(feature = "ml-dsa")]
732 #[tokio::test]
733 async fn sign_verify_jcs_ml_dsa_44() {
734 use crate::crypto_suites::CryptoSuite;
735
736 let secret = Secret::generate_ml_dsa_44(Some("k-did#k-did"), Some(&[5u8; 32]));
737 let doc = json!({"hello": "pqc"});
738
739 let proof = DataIntegrityProof::sign(
740 &doc,
741 &secret,
742 SignOptions::new().with_cryptosuite(CryptoSuite::MlDsa44Jcs2024),
743 )
744 .await
745 .expect("sign ml-dsa");
746
747 assert_eq!(proof.cryptosuite, CryptoSuite::MlDsa44Jcs2024);
748
749 proof
750 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
751 .expect("verify ml-dsa");
752 }
753
754 #[cfg(feature = "ml-dsa")]
755 #[tokio::test]
756 async fn sign_wrong_suite_for_key_fails() {
757 use crate::crypto_suites::CryptoSuite;
758
759 let secret = Secret::generate_ml_dsa_44(Some("k"), Some(&[1u8; 32]));
760 let doc = json!({"x": 1});
761 let err = DataIntegrityProof::sign(
762 &doc,
763 &secret,
764 SignOptions::new().with_cryptosuite(CryptoSuite::EddsaJcs2022),
765 )
766 .await;
767 assert!(err.is_err());
768 }
769
770 #[cfg(feature = "slh-dsa")]
771 #[tokio::test]
772 async fn sign_verify_jcs_slh_dsa_128s() {
773 use crate::crypto_suites::CryptoSuite;
774
775 let secret = Secret::generate_slh_dsa_sha2_128s(Some("k#k"));
776 let doc = json!({"hello": "slh"});
777
778 let proof = DataIntegrityProof::sign(
779 &doc,
780 &secret,
781 SignOptions::new().with_cryptosuite(CryptoSuite::SlhDsa128Jcs2024),
782 )
783 .await
784 .expect("sign slh-dsa");
785
786 proof
787 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
788 .expect("verify slh-dsa");
789 }
790}