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, verify_conformance_with_skew};
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::{DEFAULT_CLOCK_SKEW, SignOptions, VerifyOptions};
108
109#[derive(Clone, Debug, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116#[non_exhaustive]
117pub struct DataIntegrityProof {
118 #[serde(rename = "type")]
120 pub type_: String,
121
122 pub cryptosuite: CryptoSuite,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub created: Option<String>,
126
127 pub verification_method: String,
128
129 pub proof_purpose: String,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub proof_value: Option<String>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
147 pub nonce: Option<String>,
148
149 #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
150 pub context: Option<Vec<String>>,
151}
152
153impl DataIntegrityProof {
154 #[allow(clippy::too_many_arguments)]
159 pub fn new(
160 cryptosuite: CryptoSuite,
161 verification_method: String,
162 proof_purpose: String,
163 proof_value: Option<String>,
164 created: Option<String>,
165 context: Option<Vec<String>>,
166 ) -> Self {
167 Self {
168 type_: "DataIntegrityProof".to_string(),
169 cryptosuite,
170 created,
171 verification_method,
172 proof_purpose,
173 proof_value,
174 context,
175 nonce: None,
176 }
177 }
178
179 pub async fn sign<S>(
186 data_doc: &S,
187 signer: &dyn Signer,
188 options: SignOptions,
189 ) -> Result<DataIntegrityProof, DataIntegrityError>
190 where
191 S: Serialize,
192 {
193 let caller_chose_the_suite = options.cryptosuite.is_some();
194 let crypto_suite = options.cryptosuite.unwrap_or_else(|| signer.cryptosuite());
195 crypto_suite
196 .validate_key_type(signer.key_type())
197 .map_err(|_| {
198 if !caller_chose_the_suite
215 && CryptoSuite::default_for_key_type(signer.key_type()).is_none()
216 {
217 return DataIntegrityError::UnsupportedCryptoSuite {
218 name: format!(
219 "no Data Integrity cryptosuite is available for {:?}; W3C \
220 Quantum-Resistant Cryptosuites v1.0 defines suites for ML-DSA-44 \
221 only, so a credential proof needs an ML-DSA-44 key (the key is \
222 usable for other signing, just not for a Data Integrity proof)",
223 signer.key_type(),
224 ),
225 };
226 }
227 DataIntegrityError::KeyTypeMismatch {
230 expected: crypto_suite
231 .compatible_key_types()
232 .first()
233 .copied()
234 .unwrap_or(affinidi_secrets_resolver::secrets::KeyType::Unknown),
235 actual: signer.key_type(),
236 suite: crypto_suite,
237 }
238 })?;
239
240 let created_str = options
241 .created
242 .map(format_created)
243 .unwrap_or_else(|| format_created(Utc::now()));
244
245 let proof_purpose = options
246 .proof_purpose
247 .unwrap_or_else(|| "assertionMethod".to_string());
248
249 if crypto_suite.is_rdfc() {
250 sign_rdfc(
251 data_doc,
252 crypto_suite,
253 options.context,
254 signer,
255 created_str,
256 proof_purpose,
257 )
258 .await
259 } else {
260 sign_jcs(
261 data_doc,
262 crypto_suite,
263 options.context,
264 signer,
265 created_str,
266 proof_purpose,
267 )
268 .await
269 }
270 }
271
272 #[must_use = "ignoring a verification result is a security bug"]
281 pub fn verify_with_public_key<S>(
282 &self,
283 data_doc: &S,
284 public_key_bytes: &[u8],
285 options: VerifyOptions,
286 ) -> Result<(), DataIntegrityError>
287 where
288 S: Serialize,
289 {
290 verify_proof_internal(self, data_doc, public_key_bytes, &options)
291 }
292
293 #[must_use = "ignoring a verification result is a security bug"]
307 pub async fn verify<S, R>(
308 &self,
309 data_doc: &S,
310 resolver: &R,
311 options: VerifyOptions,
312 ) -> Result<(), DataIntegrityError>
313 where
314 S: Serialize + Sync,
315 R: VerificationMethodResolver + ?Sized,
316 {
317 let resolved = resolver.resolve_vm(&self.verification_method).await?;
318
319 let compatible = self.cryptosuite.compatible_key_types();
323 if !compatible.is_empty() && !compatible.contains(&resolved.key_type) {
324 return Err(DataIntegrityError::KeyTypeMismatch {
325 expected: compatible
326 .first()
327 .copied()
328 .unwrap_or(affinidi_secrets_resolver::secrets::KeyType::Unknown),
329 actual: resolved.key_type,
330 suite: self.cryptosuite,
331 });
332 }
333
334 verify_proof_internal(self, data_doc, &resolved.public_key_bytes, &options)
335 }
336}
337
338async fn sign_jcs<S>(
343 data_doc: &S,
344 crypto_suite: CryptoSuite,
345 context: Option<Vec<String>>,
346 signer: &dyn Signer,
347 created: String,
348 proof_purpose: String,
349) -> Result<DataIntegrityProof, DataIntegrityError>
350where
351 S: Serialize,
352{
353 let jcs = to_string(data_doc)
354 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
355 debug!("Document (JCS): {}", jcs);
356
357 let mut proof_options = DataIntegrityProof {
358 nonce: None,
359 type_: "DataIntegrityProof".to_string(),
360 cryptosuite: crypto_suite,
361 created: Some(created),
362 verification_method: signer.verification_method().to_string(),
363 proof_purpose,
364 proof_value: None,
365 context,
366 };
367
368 let proof_jcs = to_string(&proof_options)
369 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
370 debug!("Proof options (JCS): {}", proof_jcs);
371
372 let hash_data = hashing_jcs(&jcs, &proof_jcs);
373 let signed = signer.sign(&hash_data).await?;
374 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed));
375
376 Ok(proof_options)
377}
378
379async fn sign_rdfc<S>(
380 data_doc: &S,
381 crypto_suite: CryptoSuite,
382 context: Option<Vec<String>>,
383 signer: &dyn Signer,
384 created: String,
385 proof_purpose: String,
386) -> Result<DataIntegrityProof, DataIntegrityError>
387where
388 S: Serialize,
389{
390 let doc_value = serde_json::to_value(data_doc)
391 .map_err(|e| DataIntegrityError::Canonicalization(format!("document serialize: {e}")))?;
392
393 let proof_context = if let Some(ctx) = context {
395 Some(ctx)
396 } else {
397 match doc_value.get("@context") {
398 Some(serde_json::Value::Array(arr)) => Some(
399 arr.iter()
400 .filter_map(|v| v.as_str().map(str::to_string))
401 .collect(),
402 ),
403 Some(serde_json::Value::String(s)) => Some(vec![s.clone()]),
404 Some(_) => {
405 return Err(DataIntegrityError::MalformedProof(
406 "Invalid @context format in document".to_string(),
407 ));
408 }
409 None => {
410 return Err(DataIntegrityError::MalformedProof(
411 "Document must contain @context for RDFC signing".to_string(),
412 ));
413 }
414 }
415 };
416
417 let mut proof_options = DataIntegrityProof {
418 nonce: None,
419 type_: "DataIntegrityProof".to_string(),
420 cryptosuite: crypto_suite,
421 created: Some(created),
422 verification_method: signer.verification_method().to_string(),
423 proof_purpose,
424 proof_value: None,
425 context: proof_context,
426 };
427
428 let proof_value = serde_json::to_value(&proof_options).map_err(|e| {
429 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
430 })?;
431
432 let hash_data = hashing_rdfc(&doc_value, &proof_value)?;
433 let signed = signer.sign(&hash_data).await?;
434 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed));
435
436 Ok(proof_options)
437}
438
439fn verify_proof_internal<S>(
440 proof: &DataIntegrityProof,
441 signed_doc: &S,
442 public_key_bytes: &[u8],
443 options: &VerifyOptions,
444) -> Result<(), DataIntegrityError>
445where
446 S: Serialize,
447{
448 if !options.allowed_suites.is_empty() && !options.allowed_suites.contains(&proof.cryptosuite) {
450 return Err(DataIntegrityError::Conformance(format!(
451 "cryptosuite {} is not in the caller's allowed suites",
452 String::try_from(proof.cryptosuite).unwrap_or_default()
453 )));
454 }
455
456 if let Some(expected) = &options.expected_context
458 && proof.context.as_ref() != Some(expected)
459 {
460 return Err(DataIntegrityError::Conformance(
461 "Document context does not match proof context".to_string(),
462 ));
463 }
464
465 let Some(proof_value) = &proof.proof_value else {
467 return Err(DataIntegrityError::MalformedProof(
468 "proofValue is missing in the proof".to_string(),
469 ));
470 };
471 let proof_value = multibase::decode(proof_value)
472 .map_err(|e| DataIntegrityError::MalformedProof(format!("Invalid proof value: {e}")))?
473 .1;
474
475 let proof_config = DataIntegrityProof {
477 proof_value: None,
478 ..proof.clone()
479 };
480
481 if proof_config.type_ != "DataIntegrityProof" {
482 return Err(DataIntegrityError::Conformance(
483 "Invalid proof type, expected 'DataIntegrityProof'".to_string(),
484 ));
485 }
486
487 if let Some(created) = &proof_config.created {
493 let skew = options.clock_skew.max(chrono::TimeDelta::zero());
494 let horizon = Utc::now()
497 .checked_add_signed(skew)
498 .unwrap_or(DateTime::<Utc>::MAX_UTC);
499 let created = created
500 .parse::<DateTime<Utc>>()
501 .map_err(|e| DataIntegrityError::Conformance(format!("Invalid created date: {e}")))?;
502 if created > horizon {
503 return Err(DataIntegrityError::Conformance(format!(
504 "Created date is in the future (beyond the {}s clock-skew allowance)",
505 skew.num_seconds()
506 )));
507 }
508 }
509
510 let hash_data = if proof_config.cryptosuite.is_rdfc() {
512 let doc_value = serde_json::to_value(signed_doc).map_err(|e| {
513 DataIntegrityError::Canonicalization(format!("document serialize: {e}"))
514 })?;
515 let proof_value_json = serde_json::to_value(&proof_config).map_err(|e| {
516 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
517 })?;
518 hashing_rdfc(&doc_value, &proof_value_json)?
519 } else {
520 #[cfg(feature = "bbs-2023")]
521 if matches!(proof_config.cryptosuite, CryptoSuite::Bbs2023) {
522 return Err(DataIntegrityError::UnsupportedCryptoSuite {
523 name: "bbs-2023 derived proofs are verified via \
524 bbs_2023_transform::verify_derived_proof (or \
525 verify_pseudonym_derived_proof), not the generic verify path"
526 .to_string(),
527 });
528 }
529 let jcs_doc = to_string(&signed_doc)
530 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
531 let jcs_proof_config = to_string(&proof_config)
532 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
533 hashing_jcs(&jcs_doc, &jcs_proof_config)
534 };
535
536 proof_config
537 .cryptosuite
538 .verify(public_key_bytes, &hash_data, &proof_value)
539}
540
541fn hashing_jcs(transformed_document: &str, canonical_proof_config: &str) -> Vec<u8> {
547 [
548 Sha256::digest(canonical_proof_config),
549 Sha256::digest(transformed_document),
550 ]
551 .concat()
552}
553
554fn hashing_rdfc(
559 document: &serde_json::Value,
560 proof_config: &serde_json::Value,
561) -> Result<Vec<u8>, DataIntegrityError> {
562 let doc_hash = affinidi_rdf_encoding::expand_canonicalize_and_hash(document)
563 .map_err(|e| DataIntegrityError::Canonicalization(format!("RDFC document hash: {e}")))?;
564
565 let proof_hash =
566 affinidi_rdf_encoding::expand_canonicalize_and_hash(proof_config).map_err(|e| {
567 DataIntegrityError::Canonicalization(format!("RDFC proof config hash: {e}"))
568 })?;
569
570 Ok([proof_hash.as_slice(), doc_hash.as_slice()].concat())
571}
572
573pub fn prepare_sign_input<S>(
591 data_doc: &S,
592 proof_config: &DataIntegrityProof,
593 cryptosuite: CryptoSuite,
594) -> Result<Vec<u8>, DataIntegrityError>
595where
596 S: Serialize,
597{
598 if cryptosuite.is_rdfc() {
599 let doc_value = serde_json::to_value(data_doc).map_err(|e| {
600 DataIntegrityError::Canonicalization(format!("document serialize: {e}"))
601 })?;
602 let proof_value = serde_json::to_value(proof_config).map_err(|e| {
603 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
604 })?;
605 hashing_rdfc(&doc_value, &proof_value)
606 } else {
607 let jcs_doc = to_string(data_doc)
608 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
609 let jcs_proof = to_string(proof_config)
610 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
611 Ok(hashing_jcs(&jcs_doc, &jcs_proof))
612 }
613}
614
615fn format_created(dt: DateTime<Utc>) -> String {
620 dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
621}
622
623#[cfg(test)]
624mod tests {
625 use affinidi_secrets_resolver::secrets::Secret;
626 use chrono::Utc;
627 use serde_json::json;
628
629 use crate::{DataIntegrityError, DataIntegrityProof, SignOptions, VerifyOptions, hashing_jcs};
630
631 #[test]
632 fn hashing_working() {
633 let hash = hashing_jcs("test1", "test2");
634 let mut output = String::new();
635 for x in hash {
636 output.push_str(&format!("{x:02x}"));
637 }
638
639 assert_eq!(
640 output.as_str(),
641 "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c7521b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014",
642 );
643 }
644
645 #[tokio::test]
646 async fn sign_and_verify_via_did_key_resolver_ed25519() {
647 use crate::{DidKeyResolver, VerifyOptions};
648
649 let secret = Secret::generate_ed25519(None, Some(&[11u8; 32]));
650 let pk_mb = secret.get_public_keymultibase().unwrap();
651 let mut signer_secret = secret.clone();
653 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
654
655 let doc = json!({ "hello": "did:key" });
656 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
657 .await
658 .expect("sign");
659
660 proof
661 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
662 .await
663 .expect("verify via resolver");
664 }
665
666 #[tokio::test]
670 async fn sign_and_verify_via_did_key_resolver_p256() {
671 use crate::{DidKeyResolver, VerifyOptions};
672
673 let secret = Secret::generate_p256(None, None).expect("generate P-256");
674 let pk_mb = secret.get_public_keymultibase().unwrap();
675 let mut signer_secret = secret.clone();
676 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
677
678 let doc = json!({ "ecdsa": "did:key" });
679 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
680 .await
681 .expect("sign");
682 assert_eq!(
683 proof.cryptosuite,
684 crate::crypto_suites::CryptoSuite::EcdsaJcs2019,
685 "a P-256 signer must default to ecdsa-jcs-2019 without being told"
686 );
687
688 proof
689 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
690 .await
691 .expect("verify via resolver");
692 }
693
694 #[tokio::test]
698 async fn a_p256_proof_does_not_verify_against_a_tampered_document() {
699 use crate::{DidKeyResolver, VerifyOptions};
700
701 let secret = Secret::generate_p256(None, None).expect("generate P-256");
702 let pk_mb = secret.get_public_keymultibase().unwrap();
703 let mut signer_secret = secret.clone();
704 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
705
706 let doc = json!({ "amount": 10 });
707 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
708 .await
709 .expect("sign");
710
711 let tampered = json!({ "amount": 1000 });
712 assert!(
713 proof
714 .verify(&tampered, &DidKeyResolver, VerifyOptions::new())
715 .await
716 .is_err(),
717 "a P-256 proof must not verify against a document it did not sign"
718 );
719 }
720
721 #[cfg(feature = "ml-dsa")]
722 #[tokio::test]
723 async fn sign_and_verify_via_did_key_resolver_ml_dsa() {
724 use crate::{DidKeyResolver, VerifyOptions};
725
726 let secret = Secret::generate_ml_dsa_44(None, Some(&[21u8; 32]));
727 let pk_mb = secret.get_public_keymultibase().unwrap();
728 let mut signer_secret = secret.clone();
729 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
730
731 let doc = json!({ "pqc": "did:key" });
732 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
733 .await
734 .expect("sign");
735
736 proof
737 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
738 .await
739 .expect("verify via resolver");
740 }
741
742 #[tokio::test]
743 async fn unified_sign_verify_ed25519_jcs() {
744 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[4u8; 32]));
745 let doc = json!({"hello": "world"});
746 let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
747 .await
748 .expect("sign");
749 proof
750 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
751 .expect("verify");
752 }
753
754 #[cfg(feature = "ml-dsa")]
755 #[tokio::test]
756 async fn unified_sign_verify_ml_dsa_44_jcs() {
757 let secret = Secret::generate_ml_dsa_44(Some("did:key:k#k"), Some(&[8u8; 32]));
758 let doc = json!({"pqc": true});
759 let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
760 .await
761 .expect("sign");
762 assert_eq!(
764 proof.cryptosuite,
765 crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024
766 );
767 proof
768 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
769 .expect("verify");
770 }
771
772 #[cfg(feature = "ml-dsa")]
773 #[tokio::test]
774 async fn override_suite_via_sign_options() {
775 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[1u8; 32]));
778 let doc = json!({"x": 1});
779 let err = DataIntegrityProof::sign(
780 &doc,
781 &secret,
782 SignOptions::new().with_cryptosuite(crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024),
783 )
784 .await
785 .unwrap_err();
786 assert!(matches!(
787 err,
788 crate::DataIntegrityError::KeyTypeMismatch { .. }
789 ));
790 }
791
792 #[tokio::test]
798 async fn verify_tolerates_default_clock_skew_on_created() {
799 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[3u8; 32]));
800 let doc = json!({"skewed": true});
801 let proof = DataIntegrityProof::sign(
802 &doc,
803 &secret,
804 SignOptions::new().with_created(Utc::now() + chrono::TimeDelta::seconds(5)),
805 )
806 .await
807 .expect("sign");
808
809 proof
810 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
811 .expect("a 5s-ahead signer is inside the default 60s allowance");
812 }
813
814 #[tokio::test]
818 async fn verify_rejects_created_beyond_the_allowance() {
819 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[4u8; 32]));
820 let doc = json!({"skewed": "far"});
821 let proof = DataIntegrityProof::sign(
822 &doc,
823 &secret,
824 SignOptions::new().with_created(Utc::now() + chrono::TimeDelta::hours(1)),
825 )
826 .await
827 .expect("sign");
828
829 let err = proof
830 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
831 .expect_err("an hour ahead is well beyond 60s");
832 let msg = format!("{err}");
833 assert!(msg.contains("future"), "unexpected message: {msg}");
834 assert!(
835 msg.contains("60s"),
836 "message should name the allowance: {msg}"
837 );
838 }
839
840 #[tokio::test]
843 async fn verify_zero_skew_rejects_any_future_created() {
844 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[5u8; 32]));
845 let doc = json!({"strict": true});
846 let proof = DataIntegrityProof::sign(
847 &doc,
848 &secret,
849 SignOptions::new().with_created(Utc::now() + chrono::TimeDelta::seconds(5)),
850 )
851 .await
852 .expect("sign");
853
854 let err = proof
855 .verify_with_public_key(
856 &doc,
857 secret.get_public_bytes(),
858 VerifyOptions::new().with_clock_skew(chrono::TimeDelta::zero()),
859 )
860 .expect_err("zero tolerance must reject a future created");
861 assert!(matches!(err, DataIntegrityError::Conformance(_)));
862 }
863
864 #[tokio::test]
869 async fn verify_rejects_cryptosuite_tampering() {
870 use crate::crypto_suites::CryptoSuite;
871
872 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[77u8; 32]));
873 let doc = json!({"tamper": "target"});
874 let mut proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
875 .await
876 .expect("sign");
877 assert_eq!(proof.cryptosuite, CryptoSuite::EddsaJcs2022);
878
879 proof.cryptosuite = CryptoSuite::EddsaRdfc2022;
881
882 let err = proof
883 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
884 .unwrap_err();
885 assert!(
886 matches!(err, crate::DataIntegrityError::InvalidSignature { .. }),
887 "expected InvalidSignature after cryptosuite tampering, got: {err:?}"
888 );
889 }
890
891 #[tokio::test]
892 async fn deterministic_signing_same_input_same_output() {
893 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[2u8; 32]));
894 let doc = json!({"deterministic": "yes"});
895 let created = chrono::Utc::now();
896 let opts = || SignOptions::new().with_created(created);
897 let a = DataIntegrityProof::sign(&doc, &secret, opts())
898 .await
899 .unwrap();
900 let b = DataIntegrityProof::sign(&doc, &secret, opts())
901 .await
902 .unwrap();
903 assert_eq!(
904 a.proof_value, b.proof_value,
905 "Ed25519 must be deterministic"
906 );
907 }
908
909 #[cfg(feature = "ml-dsa")]
910 #[tokio::test]
911 async fn deterministic_signing_ml_dsa() {
912 let secret = Secret::generate_ml_dsa_44(Some("did:key:k#k"), Some(&[5u8; 32]));
913 let doc = json!({"deterministic": "pqc"});
914 let created = chrono::Utc::now();
915 let opts = || SignOptions::new().with_created(created);
916 let a = DataIntegrityProof::sign(&doc, &secret, opts())
917 .await
918 .unwrap();
919 let b = DataIntegrityProof::sign(&doc, &secret, opts())
920 .await
921 .unwrap();
922 assert_eq!(a.proof_value, b.proof_value, "ML-DSA must be deterministic");
923 }
924
925 #[tokio::test]
931 async fn test_sign_p256_key_now_produces_an_ecdsa_jcs_2019_proof() {
932 let generic_doc = json!({"test": "test_data"});
933 let pub_key = "zruqgFba156mDWfMUjJUSAKUvgCgF5NfgSYwSuEZuXpixts8tw3ot5BasjeyM65f8dzk5k6zgXf7pkbaaBnPrjCUmcJ";
934 let pri_key = "z42tmXtqqQBLmEEwn8tfi1bA2ghBx9cBo6wo8a44kVJEiqyA";
935 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
936 .expect("Couldn't create test key data");
937 assert_eq!(
938 secret.get_key_type(),
939 affinidi_secrets_resolver::secrets::KeyType::P256
940 );
941
942 let proof = DataIntegrityProof::sign(&generic_doc, &secret, SignOptions::new())
943 .await
944 .expect("a P-256 key signs under ecdsa-jcs-2019");
945 assert_eq!(
946 proof.cryptosuite,
947 crate::crypto_suites::CryptoSuite::EcdsaJcs2019
948 );
949 }
950
951 #[tokio::test]
955 async fn test_sign_rejects_a_key_type_with_no_suite() {
956 let generic_doc = json!({"test": "test_data"});
957 let secret =
958 Secret::generate_secp256k1(Some("did:key:k#k"), None).expect("generate secp256k1");
959 assert!(
960 DataIntegrityProof::sign(&generic_doc, &secret, SignOptions::new())
961 .await
962 .is_err(),
963 "a key type with no compiled-in suite must not sign"
964 );
965 }
966
967 #[tokio::test]
968 async fn test_sign_good() {
969 let generic_doc = json!({"test": "test_data"});
970 let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
971 let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
972 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
973 .expect("Couldn't create test key data");
974 let context = vec![
975 "context1".to_string(),
976 "context2".to_string(),
977 "context3".to_string(),
978 ];
979 assert!(
980 DataIntegrityProof::sign(
981 &generic_doc,
982 &secret,
983 SignOptions::new().with_context(context)
984 )
985 .await
986 .is_ok(),
987 "Signing failed"
988 );
989 }
990
991 #[cfg(feature = "ml-dsa")]
992 #[tokio::test]
993 async fn sign_verify_jcs_ml_dsa_44() {
994 use crate::crypto_suites::CryptoSuite;
995
996 let secret = Secret::generate_ml_dsa_44(Some("k-did#k-did"), Some(&[5u8; 32]));
997 let doc = json!({"hello": "pqc"});
998
999 let proof = DataIntegrityProof::sign(
1000 &doc,
1001 &secret,
1002 SignOptions::new().with_cryptosuite(CryptoSuite::MlDsa44Jcs2024),
1003 )
1004 .await
1005 .expect("sign ml-dsa");
1006
1007 assert_eq!(proof.cryptosuite, CryptoSuite::MlDsa44Jcs2024);
1008
1009 proof
1010 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
1011 .expect("verify ml-dsa");
1012 }
1013
1014 #[cfg(feature = "ml-dsa")]
1015 #[tokio::test]
1016 async fn sign_wrong_suite_for_key_fails() {
1017 use crate::crypto_suites::CryptoSuite;
1018
1019 let secret = Secret::generate_ml_dsa_44(Some("k"), Some(&[1u8; 32]));
1020 let doc = json!({"x": 1});
1021 let err = DataIntegrityProof::sign(
1022 &doc,
1023 &secret,
1024 SignOptions::new().with_cryptosuite(CryptoSuite::EddsaJcs2022),
1025 )
1026 .await;
1027 assert!(err.is_err());
1028 }
1029
1030 #[cfg(feature = "slh-dsa")]
1031 #[tokio::test]
1032 async fn sign_verify_jcs_slh_dsa_128s() {
1033 use crate::crypto_suites::CryptoSuite;
1034
1035 let secret = Secret::generate_slh_dsa_sha2_128s(Some("k#k"));
1036 let doc = json!({"hello": "slh"});
1037
1038 let proof = DataIntegrityProof::sign(
1039 &doc,
1040 &secret,
1041 SignOptions::new().with_cryptosuite(CryptoSuite::SlhDsa128Jcs2024),
1042 )
1043 .await
1044 .expect("sign slh-dsa");
1045
1046 proof
1047 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
1048 .expect("verify slh-dsa");
1049 }
1050}