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