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(rename = "@context", skip_serializing_if = "Option::is_none")]
135 pub context: Option<Vec<String>>,
136}
137
138impl DataIntegrityProof {
139 #[allow(clippy::too_many_arguments)]
144 pub fn new(
145 cryptosuite: CryptoSuite,
146 verification_method: String,
147 proof_purpose: String,
148 proof_value: Option<String>,
149 created: Option<String>,
150 context: Option<Vec<String>>,
151 ) -> Self {
152 Self {
153 type_: "DataIntegrityProof".to_string(),
154 cryptosuite,
155 created,
156 verification_method,
157 proof_purpose,
158 proof_value,
159 context,
160 }
161 }
162
163 pub async fn sign<S>(
170 data_doc: &S,
171 signer: &dyn Signer,
172 options: SignOptions,
173 ) -> Result<DataIntegrityProof, DataIntegrityError>
174 where
175 S: Serialize,
176 {
177 let crypto_suite = options.cryptosuite.unwrap_or_else(|| signer.cryptosuite());
178 crypto_suite
179 .validate_key_type(signer.key_type())
180 .map_err(|_| DataIntegrityError::KeyTypeMismatch {
181 expected: crypto_suite
182 .compatible_key_types()
183 .first()
184 .copied()
185 .unwrap_or(affinidi_secrets_resolver::secrets::KeyType::Unknown),
186 actual: signer.key_type(),
187 suite: crypto_suite,
188 })?;
189
190 let created_str = options
191 .created
192 .map(format_created)
193 .unwrap_or_else(|| format_created(Utc::now()));
194
195 let proof_purpose = options
196 .proof_purpose
197 .unwrap_or_else(|| "assertionMethod".to_string());
198
199 if crypto_suite.is_rdfc() {
200 sign_rdfc(
201 data_doc,
202 crypto_suite,
203 options.context,
204 signer,
205 created_str,
206 proof_purpose,
207 )
208 .await
209 } else {
210 sign_jcs(
211 data_doc,
212 crypto_suite,
213 options.context,
214 signer,
215 created_str,
216 proof_purpose,
217 )
218 .await
219 }
220 }
221
222 #[must_use = "ignoring a verification result is a security bug"]
231 pub fn verify_with_public_key<S>(
232 &self,
233 data_doc: &S,
234 public_key_bytes: &[u8],
235 options: VerifyOptions,
236 ) -> Result<(), DataIntegrityError>
237 where
238 S: Serialize,
239 {
240 verify_proof_internal(self, data_doc, public_key_bytes, &options)
241 }
242
243 #[must_use = "ignoring a verification result is a security bug"]
257 pub async fn verify<S, R>(
258 &self,
259 data_doc: &S,
260 resolver: &R,
261 options: VerifyOptions,
262 ) -> Result<(), DataIntegrityError>
263 where
264 S: Serialize + Sync,
265 R: VerificationMethodResolver + ?Sized,
266 {
267 let resolved = resolver.resolve_vm(&self.verification_method).await?;
268
269 let compatible = self.cryptosuite.compatible_key_types();
273 if !compatible.is_empty() && !compatible.contains(&resolved.key_type) {
274 return Err(DataIntegrityError::KeyTypeMismatch {
275 expected: compatible
276 .first()
277 .copied()
278 .unwrap_or(affinidi_secrets_resolver::secrets::KeyType::Unknown),
279 actual: resolved.key_type,
280 suite: self.cryptosuite,
281 });
282 }
283
284 verify_proof_internal(self, data_doc, &resolved.public_key_bytes, &options)
285 }
286}
287
288async fn sign_jcs<S>(
293 data_doc: &S,
294 crypto_suite: CryptoSuite,
295 context: Option<Vec<String>>,
296 signer: &dyn Signer,
297 created: String,
298 proof_purpose: String,
299) -> Result<DataIntegrityProof, DataIntegrityError>
300where
301 S: Serialize,
302{
303 let jcs = to_string(data_doc)
304 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
305 debug!("Document (JCS): {}", jcs);
306
307 let mut proof_options = DataIntegrityProof {
308 type_: "DataIntegrityProof".to_string(),
309 cryptosuite: crypto_suite,
310 created: Some(created),
311 verification_method: signer.verification_method().to_string(),
312 proof_purpose,
313 proof_value: None,
314 context,
315 };
316
317 let proof_jcs = to_string(&proof_options)
318 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
319 debug!("Proof options (JCS): {}", proof_jcs);
320
321 let hash_data = hashing_jcs(&jcs, &proof_jcs);
322 let signed = signer.sign(&hash_data).await?;
323 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed));
324
325 Ok(proof_options)
326}
327
328async fn sign_rdfc<S>(
329 data_doc: &S,
330 crypto_suite: CryptoSuite,
331 context: Option<Vec<String>>,
332 signer: &dyn Signer,
333 created: String,
334 proof_purpose: String,
335) -> Result<DataIntegrityProof, DataIntegrityError>
336where
337 S: Serialize,
338{
339 let doc_value = serde_json::to_value(data_doc)
340 .map_err(|e| DataIntegrityError::Canonicalization(format!("document serialize: {e}")))?;
341
342 let proof_context = if let Some(ctx) = context {
344 Some(ctx)
345 } else {
346 match doc_value.get("@context") {
347 Some(serde_json::Value::Array(arr)) => Some(
348 arr.iter()
349 .filter_map(|v| v.as_str().map(str::to_string))
350 .collect(),
351 ),
352 Some(serde_json::Value::String(s)) => Some(vec![s.clone()]),
353 Some(_) => {
354 return Err(DataIntegrityError::MalformedProof(
355 "Invalid @context format in document".to_string(),
356 ));
357 }
358 None => {
359 return Err(DataIntegrityError::MalformedProof(
360 "Document must contain @context for RDFC signing".to_string(),
361 ));
362 }
363 }
364 };
365
366 let mut proof_options = DataIntegrityProof {
367 type_: "DataIntegrityProof".to_string(),
368 cryptosuite: crypto_suite,
369 created: Some(created),
370 verification_method: signer.verification_method().to_string(),
371 proof_purpose,
372 proof_value: None,
373 context: proof_context,
374 };
375
376 let proof_value = serde_json::to_value(&proof_options).map_err(|e| {
377 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
378 })?;
379
380 let hash_data = hashing_rdfc(&doc_value, &proof_value)?;
381 let signed = signer.sign(&hash_data).await?;
382 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed));
383
384 Ok(proof_options)
385}
386
387fn verify_proof_internal<S>(
388 proof: &DataIntegrityProof,
389 signed_doc: &S,
390 public_key_bytes: &[u8],
391 options: &VerifyOptions,
392) -> Result<(), DataIntegrityError>
393where
394 S: Serialize,
395{
396 if !options.allowed_suites.is_empty() && !options.allowed_suites.contains(&proof.cryptosuite) {
398 return Err(DataIntegrityError::Conformance(format!(
399 "cryptosuite {} is not in the caller's allowed suites",
400 String::try_from(proof.cryptosuite).unwrap_or_default()
401 )));
402 }
403
404 if let Some(expected) = &options.expected_context
406 && proof.context.as_ref() != Some(expected)
407 {
408 return Err(DataIntegrityError::Conformance(
409 "Document context does not match proof context".to_string(),
410 ));
411 }
412
413 let Some(proof_value) = &proof.proof_value else {
415 return Err(DataIntegrityError::MalformedProof(
416 "proofValue is missing in the proof".to_string(),
417 ));
418 };
419 let proof_value = multibase::decode(proof_value)
420 .map_err(|e| DataIntegrityError::MalformedProof(format!("Invalid proof value: {e}")))?
421 .1;
422
423 let proof_config = DataIntegrityProof {
425 proof_value: None,
426 ..proof.clone()
427 };
428
429 if proof_config.type_ != "DataIntegrityProof" {
430 return Err(DataIntegrityError::Conformance(
431 "Invalid proof type, expected 'DataIntegrityProof'".to_string(),
432 ));
433 }
434
435 if let Some(created) = &proof_config.created {
441 let skew = options.clock_skew.max(chrono::TimeDelta::zero());
442 let horizon = Utc::now()
445 .checked_add_signed(skew)
446 .unwrap_or(DateTime::<Utc>::MAX_UTC);
447 let created = created
448 .parse::<DateTime<Utc>>()
449 .map_err(|e| DataIntegrityError::Conformance(format!("Invalid created date: {e}")))?;
450 if created > horizon {
451 return Err(DataIntegrityError::Conformance(format!(
452 "Created date is in the future (beyond the {}s clock-skew allowance)",
453 skew.num_seconds()
454 )));
455 }
456 }
457
458 let hash_data = if proof_config.cryptosuite.is_rdfc() {
460 let doc_value = serde_json::to_value(signed_doc).map_err(|e| {
461 DataIntegrityError::Canonicalization(format!("document serialize: {e}"))
462 })?;
463 let proof_value_json = serde_json::to_value(&proof_config).map_err(|e| {
464 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
465 })?;
466 hashing_rdfc(&doc_value, &proof_value_json)?
467 } else {
468 #[cfg(feature = "bbs-2023")]
469 if matches!(proof_config.cryptosuite, CryptoSuite::Bbs2023) {
470 return Err(DataIntegrityError::UnsupportedCryptoSuite {
471 name: "bbs-2023 derived proofs are verified via \
472 bbs_2023_transform::verify_derived_proof (or \
473 verify_pseudonym_derived_proof), not the generic verify path"
474 .to_string(),
475 });
476 }
477 let jcs_doc = to_string(&signed_doc)
478 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
479 let jcs_proof_config = to_string(&proof_config)
480 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
481 hashing_jcs(&jcs_doc, &jcs_proof_config)
482 };
483
484 proof_config
485 .cryptosuite
486 .verify(public_key_bytes, &hash_data, &proof_value)
487}
488
489fn hashing_jcs(transformed_document: &str, canonical_proof_config: &str) -> Vec<u8> {
495 [
496 Sha256::digest(canonical_proof_config),
497 Sha256::digest(transformed_document),
498 ]
499 .concat()
500}
501
502fn hashing_rdfc(
507 document: &serde_json::Value,
508 proof_config: &serde_json::Value,
509) -> Result<Vec<u8>, DataIntegrityError> {
510 let doc_hash = affinidi_rdf_encoding::expand_canonicalize_and_hash(document)
511 .map_err(|e| DataIntegrityError::Canonicalization(format!("RDFC document hash: {e}")))?;
512
513 let proof_hash =
514 affinidi_rdf_encoding::expand_canonicalize_and_hash(proof_config).map_err(|e| {
515 DataIntegrityError::Canonicalization(format!("RDFC proof config hash: {e}"))
516 })?;
517
518 Ok([proof_hash.as_slice(), doc_hash.as_slice()].concat())
519}
520
521pub fn prepare_sign_input<S>(
539 data_doc: &S,
540 proof_config: &DataIntegrityProof,
541 cryptosuite: CryptoSuite,
542) -> Result<Vec<u8>, DataIntegrityError>
543where
544 S: Serialize,
545{
546 if cryptosuite.is_rdfc() {
547 let doc_value = serde_json::to_value(data_doc).map_err(|e| {
548 DataIntegrityError::Canonicalization(format!("document serialize: {e}"))
549 })?;
550 let proof_value = serde_json::to_value(proof_config).map_err(|e| {
551 DataIntegrityError::Canonicalization(format!("proof config serialize: {e}"))
552 })?;
553 hashing_rdfc(&doc_value, &proof_value)
554 } else {
555 let jcs_doc = to_string(data_doc)
556 .map_err(|e| DataIntegrityError::Canonicalization(format!("document: {e}")))?;
557 let jcs_proof = to_string(proof_config)
558 .map_err(|e| DataIntegrityError::Canonicalization(format!("proof config: {e}")))?;
559 Ok(hashing_jcs(&jcs_doc, &jcs_proof))
560 }
561}
562
563fn format_created(dt: DateTime<Utc>) -> String {
568 dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
569}
570
571#[cfg(test)]
572mod tests {
573 use affinidi_secrets_resolver::secrets::Secret;
574 use chrono::Utc;
575 use serde_json::json;
576
577 use crate::{DataIntegrityError, DataIntegrityProof, SignOptions, VerifyOptions, hashing_jcs};
578
579 #[test]
580 fn hashing_working() {
581 let hash = hashing_jcs("test1", "test2");
582 let mut output = String::new();
583 for x in hash {
584 output.push_str(&format!("{x:02x}"));
585 }
586
587 assert_eq!(
588 output.as_str(),
589 "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c7521b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014",
590 );
591 }
592
593 #[tokio::test]
594 async fn sign_and_verify_via_did_key_resolver_ed25519() {
595 use crate::{DidKeyResolver, VerifyOptions};
596
597 let secret = Secret::generate_ed25519(None, Some(&[11u8; 32]));
598 let pk_mb = secret.get_public_keymultibase().unwrap();
599 let mut signer_secret = secret.clone();
601 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
602
603 let doc = json!({ "hello": "did:key" });
604 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
605 .await
606 .expect("sign");
607
608 proof
609 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
610 .await
611 .expect("verify via resolver");
612 }
613
614 #[tokio::test]
618 async fn sign_and_verify_via_did_key_resolver_p256() {
619 use crate::{DidKeyResolver, VerifyOptions};
620
621 let secret = Secret::generate_p256(None, None).expect("generate P-256");
622 let pk_mb = secret.get_public_keymultibase().unwrap();
623 let mut signer_secret = secret.clone();
624 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
625
626 let doc = json!({ "ecdsa": "did:key" });
627 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
628 .await
629 .expect("sign");
630 assert_eq!(
631 proof.cryptosuite,
632 crate::crypto_suites::CryptoSuite::EcdsaJcs2019,
633 "a P-256 signer must default to ecdsa-jcs-2019 without being told"
634 );
635
636 proof
637 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
638 .await
639 .expect("verify via resolver");
640 }
641
642 #[tokio::test]
646 async fn a_p256_proof_does_not_verify_against_a_tampered_document() {
647 use crate::{DidKeyResolver, VerifyOptions};
648
649 let secret = Secret::generate_p256(None, None).expect("generate P-256");
650 let pk_mb = secret.get_public_keymultibase().unwrap();
651 let mut signer_secret = secret.clone();
652 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
653
654 let doc = json!({ "amount": 10 });
655 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
656 .await
657 .expect("sign");
658
659 let tampered = json!({ "amount": 1000 });
660 assert!(
661 proof
662 .verify(&tampered, &DidKeyResolver, VerifyOptions::new())
663 .await
664 .is_err(),
665 "a P-256 proof must not verify against a document it did not sign"
666 );
667 }
668
669 #[cfg(feature = "ml-dsa")]
670 #[tokio::test]
671 async fn sign_and_verify_via_did_key_resolver_ml_dsa() {
672 use crate::{DidKeyResolver, VerifyOptions};
673
674 let secret = Secret::generate_ml_dsa_44(None, Some(&[21u8; 32]));
675 let pk_mb = secret.get_public_keymultibase().unwrap();
676 let mut signer_secret = secret.clone();
677 signer_secret.id = format!("did:key:{pk_mb}#{pk_mb}");
678
679 let doc = json!({ "pqc": "did:key" });
680 let proof = DataIntegrityProof::sign(&doc, &signer_secret, SignOptions::new())
681 .await
682 .expect("sign");
683
684 proof
685 .verify(&doc, &DidKeyResolver, VerifyOptions::new())
686 .await
687 .expect("verify via resolver");
688 }
689
690 #[tokio::test]
691 async fn unified_sign_verify_ed25519_jcs() {
692 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[4u8; 32]));
693 let doc = json!({"hello": "world"});
694 let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
695 .await
696 .expect("sign");
697 proof
698 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
699 .expect("verify");
700 }
701
702 #[cfg(feature = "ml-dsa")]
703 #[tokio::test]
704 async fn unified_sign_verify_ml_dsa_44_jcs() {
705 let secret = Secret::generate_ml_dsa_44(Some("did:key:k#k"), Some(&[8u8; 32]));
706 let doc = json!({"pqc": true});
707 let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
708 .await
709 .expect("sign");
710 assert_eq!(
712 proof.cryptosuite,
713 crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024
714 );
715 proof
716 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
717 .expect("verify");
718 }
719
720 #[cfg(feature = "ml-dsa")]
721 #[tokio::test]
722 async fn override_suite_via_sign_options() {
723 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[1u8; 32]));
726 let doc = json!({"x": 1});
727 let err = DataIntegrityProof::sign(
728 &doc,
729 &secret,
730 SignOptions::new().with_cryptosuite(crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024),
731 )
732 .await
733 .unwrap_err();
734 assert!(matches!(
735 err,
736 crate::DataIntegrityError::KeyTypeMismatch { .. }
737 ));
738 }
739
740 #[tokio::test]
746 async fn verify_tolerates_default_clock_skew_on_created() {
747 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[3u8; 32]));
748 let doc = json!({"skewed": true});
749 let proof = DataIntegrityProof::sign(
750 &doc,
751 &secret,
752 SignOptions::new().with_created(Utc::now() + chrono::TimeDelta::seconds(5)),
753 )
754 .await
755 .expect("sign");
756
757 proof
758 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
759 .expect("a 5s-ahead signer is inside the default 60s allowance");
760 }
761
762 #[tokio::test]
766 async fn verify_rejects_created_beyond_the_allowance() {
767 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[4u8; 32]));
768 let doc = json!({"skewed": "far"});
769 let proof = DataIntegrityProof::sign(
770 &doc,
771 &secret,
772 SignOptions::new().with_created(Utc::now() + chrono::TimeDelta::hours(1)),
773 )
774 .await
775 .expect("sign");
776
777 let err = proof
778 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
779 .expect_err("an hour ahead is well beyond 60s");
780 let msg = format!("{err}");
781 assert!(msg.contains("future"), "unexpected message: {msg}");
782 assert!(
783 msg.contains("60s"),
784 "message should name the allowance: {msg}"
785 );
786 }
787
788 #[tokio::test]
791 async fn verify_zero_skew_rejects_any_future_created() {
792 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[5u8; 32]));
793 let doc = json!({"strict": true});
794 let proof = DataIntegrityProof::sign(
795 &doc,
796 &secret,
797 SignOptions::new().with_created(Utc::now() + chrono::TimeDelta::seconds(5)),
798 )
799 .await
800 .expect("sign");
801
802 let err = proof
803 .verify_with_public_key(
804 &doc,
805 secret.get_public_bytes(),
806 VerifyOptions::new().with_clock_skew(chrono::TimeDelta::zero()),
807 )
808 .expect_err("zero tolerance must reject a future created");
809 assert!(matches!(err, DataIntegrityError::Conformance(_)));
810 }
811
812 #[tokio::test]
817 async fn verify_rejects_cryptosuite_tampering() {
818 use crate::crypto_suites::CryptoSuite;
819
820 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[77u8; 32]));
821 let doc = json!({"tamper": "target"});
822 let mut proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new())
823 .await
824 .expect("sign");
825 assert_eq!(proof.cryptosuite, CryptoSuite::EddsaJcs2022);
826
827 proof.cryptosuite = CryptoSuite::EddsaRdfc2022;
829
830 let err = proof
831 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
832 .unwrap_err();
833 assert!(
834 matches!(err, crate::DataIntegrityError::InvalidSignature { .. }),
835 "expected InvalidSignature after cryptosuite tampering, got: {err:?}"
836 );
837 }
838
839 #[tokio::test]
840 async fn deterministic_signing_same_input_same_output() {
841 let secret = Secret::generate_ed25519(Some("did:key:k#k"), Some(&[2u8; 32]));
842 let doc = json!({"deterministic": "yes"});
843 let created = chrono::Utc::now();
844 let opts = || SignOptions::new().with_created(created);
845 let a = DataIntegrityProof::sign(&doc, &secret, opts())
846 .await
847 .unwrap();
848 let b = DataIntegrityProof::sign(&doc, &secret, opts())
849 .await
850 .unwrap();
851 assert_eq!(
852 a.proof_value, b.proof_value,
853 "Ed25519 must be deterministic"
854 );
855 }
856
857 #[cfg(feature = "ml-dsa")]
858 #[tokio::test]
859 async fn deterministic_signing_ml_dsa() {
860 let secret = Secret::generate_ml_dsa_44(Some("did:key:k#k"), Some(&[5u8; 32]));
861 let doc = json!({"deterministic": "pqc"});
862 let created = chrono::Utc::now();
863 let opts = || SignOptions::new().with_created(created);
864 let a = DataIntegrityProof::sign(&doc, &secret, opts())
865 .await
866 .unwrap();
867 let b = DataIntegrityProof::sign(&doc, &secret, opts())
868 .await
869 .unwrap();
870 assert_eq!(a.proof_value, b.proof_value, "ML-DSA must be deterministic");
871 }
872
873 #[tokio::test]
879 async fn test_sign_p256_key_now_produces_an_ecdsa_jcs_2019_proof() {
880 let generic_doc = json!({"test": "test_data"});
881 let pub_key = "zruqgFba156mDWfMUjJUSAKUvgCgF5NfgSYwSuEZuXpixts8tw3ot5BasjeyM65f8dzk5k6zgXf7pkbaaBnPrjCUmcJ";
882 let pri_key = "z42tmXtqqQBLmEEwn8tfi1bA2ghBx9cBo6wo8a44kVJEiqyA";
883 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
884 .expect("Couldn't create test key data");
885 assert_eq!(
886 secret.get_key_type(),
887 affinidi_secrets_resolver::secrets::KeyType::P256
888 );
889
890 let proof = DataIntegrityProof::sign(&generic_doc, &secret, SignOptions::new())
891 .await
892 .expect("a P-256 key signs under ecdsa-jcs-2019");
893 assert_eq!(
894 proof.cryptosuite,
895 crate::crypto_suites::CryptoSuite::EcdsaJcs2019
896 );
897 }
898
899 #[tokio::test]
903 async fn test_sign_rejects_a_key_type_with_no_suite() {
904 let generic_doc = json!({"test": "test_data"});
905 let secret =
906 Secret::generate_secp256k1(Some("did:key:k#k"), None).expect("generate secp256k1");
907 assert!(
908 DataIntegrityProof::sign(&generic_doc, &secret, SignOptions::new())
909 .await
910 .is_err(),
911 "a key type with no compiled-in suite must not sign"
912 );
913 }
914
915 #[tokio::test]
916 async fn test_sign_good() {
917 let generic_doc = json!({"test": "test_data"});
918 let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
919 let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
920 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
921 .expect("Couldn't create test key data");
922 let context = vec![
923 "context1".to_string(),
924 "context2".to_string(),
925 "context3".to_string(),
926 ];
927 assert!(
928 DataIntegrityProof::sign(
929 &generic_doc,
930 &secret,
931 SignOptions::new().with_context(context)
932 )
933 .await
934 .is_ok(),
935 "Signing failed"
936 );
937 }
938
939 #[cfg(feature = "ml-dsa")]
940 #[tokio::test]
941 async fn sign_verify_jcs_ml_dsa_44() {
942 use crate::crypto_suites::CryptoSuite;
943
944 let secret = Secret::generate_ml_dsa_44(Some("k-did#k-did"), Some(&[5u8; 32]));
945 let doc = json!({"hello": "pqc"});
946
947 let proof = DataIntegrityProof::sign(
948 &doc,
949 &secret,
950 SignOptions::new().with_cryptosuite(CryptoSuite::MlDsa44Jcs2024),
951 )
952 .await
953 .expect("sign ml-dsa");
954
955 assert_eq!(proof.cryptosuite, CryptoSuite::MlDsa44Jcs2024);
956
957 proof
958 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
959 .expect("verify ml-dsa");
960 }
961
962 #[cfg(feature = "ml-dsa")]
963 #[tokio::test]
964 async fn sign_wrong_suite_for_key_fails() {
965 use crate::crypto_suites::CryptoSuite;
966
967 let secret = Secret::generate_ml_dsa_44(Some("k"), Some(&[1u8; 32]));
968 let doc = json!({"x": 1});
969 let err = DataIntegrityProof::sign(
970 &doc,
971 &secret,
972 SignOptions::new().with_cryptosuite(CryptoSuite::EddsaJcs2022),
973 )
974 .await;
975 assert!(err.is_err());
976 }
977
978 #[cfg(feature = "slh-dsa")]
979 #[tokio::test]
980 async fn sign_verify_jcs_slh_dsa_128s() {
981 use crate::crypto_suites::CryptoSuite;
982
983 let secret = Secret::generate_slh_dsa_sha2_128s(Some("k#k"));
984 let doc = json!({"hello": "slh"});
985
986 let proof = DataIntegrityProof::sign(
987 &doc,
988 &secret,
989 SignOptions::new().with_cryptosuite(CryptoSuite::SlhDsa128Jcs2024),
990 )
991 .await
992 .expect("sign slh-dsa");
993
994 proof
995 .verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())
996 .expect("verify slh-dsa");
997 }
998}