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