Skip to main content

affinidi_data_integrity/
lib.rs

1/*!
2W3C Data Integrity — sign and verify [Data Integrity Proofs] for
3Verifiable Credentials, DID documents, and arbitrary JSON documents.
4
5# Quickstart — sign and verify
6
7```no_run
8use affinidi_data_integrity::{DataIntegrityProof, SignOptions, VerifyOptions};
9use affinidi_secrets_resolver::secrets::Secret;
10use serde_json::json;
11
12# async fn demo() -> Result<(), affinidi_data_integrity::DataIntegrityError> {
13let secret = Secret::generate_ed25519(Some("did:key:z6Mk...#key-0"), None);
14let doc = json!({ "name": "Alice" });
15
16// Sign — the library picks `eddsa-jcs-2022` automatically via
17// Signer::cryptosuite() because `secret` is an Ed25519 key.
18let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new()).await?;
19
20// Verify — pass the raw public-key bytes.
21proof.verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())?;
22# Ok(()) }
23```
24
25# Post-quantum cryptography
26
27Enable the `post-quantum` feature (off by default) to sign with
28ML-DSA-44 or SLH-DSA-SHA2-128s:
29
30```ignore
31[dependencies]
32affinidi-data-integrity = { version = "0.5", features = ["post-quantum"] }
33```
34
35Then generate a PQC key — the library selects `mldsa44-jcs-2024` or
36`slhdsa128-jcs-2024` automatically from the key type.
37
38# Cryptosuites
39
40See [`crypto_suites::CryptoSuite`] for the full list. Each suite has a
41canonicalization (JCS or RDFC), a signing algorithm, and a
42[`compatible_key_types`] list. Callers rarely need to pick a suite
43directly — [`Signer::cryptosuite`] provides a sensible default per key
44type, and `SignOptions::with_cryptosuite` is the escape hatch for
45explicit selection (e.g. forcing RDFC).
46
47# Forward compatibility
48
49All public enums (`KeyType`, [`CryptoSuite`], [`DataIntegrityError`])
50are `#[non_exhaustive]`. Future algorithms and error variants arrive in
51minor releases without breaking callers that include a `_ =>` arm.
52
53# Out of scope
54
55This crate implements W3C Data Integrity only. JOSE / JWS / COSE
56post-quantum profiles are being standardised separately by IETF and
57will live in sibling crates (`affinidi-data-integrity-jose`,
58`-cose`) when those drafts stabilise.
59
60[Data Integrity Proofs]: https://www.w3.org/TR/vc-data-integrity/
61[`compatible_key_types`]: crate::crypto_suites::CryptoSuite::compatible_key_types
62[`CryptoSuite`]: crate::crypto_suites::CryptoSuite
63[`Signer::cryptosuite`]: crate::signer::Signer::cryptosuite
64*/
65
66use 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/// BBS-2023 Data Integrity Cryptosuite for zero-knowledge selective disclosure.
92///
93/// Enabled via the `bbs-2023` feature flag.
94#[cfg(feature = "bbs-2023")]
95pub mod bbs_2023;
96
97pub use error::{DataIntegrityError, SignatureFailure};
98pub use options::{SignOptions, VerifyOptions};
99
100/// Serialized Data Integrity proof.
101#[derive(Clone, Debug, Deserialize, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct DataIntegrityProof {
104    /// Must be 'DataIntegrityProof'
105    #[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    /// Produces a Data Integrity proof over `data_doc`.
126    ///
127    /// The cryptosuite is picked from [`SignOptions::cryptosuite`] if
128    /// set, otherwise from [`Signer::cryptosuite`]. Canonicalization
129    /// (JCS or RDFC) is derived from the suite.
130    ///
131    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    /// Verifies a proof against `data_doc` using caller-provided public
185    /// key bytes.
186    ///
187    /// Sync because this is pure CPU — callers who already have the key
188    /// should not be forced into an async runtime. See [`verify`] for
189    /// the resolver-based async variant.
190    ///
191    /// [`verify`]: Self::verify
192    #[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    /// Verifies a proof by resolving the public key from its
206    /// `verificationMethod` via a [`VerificationMethodResolver`].
207    ///
208    /// Use [`did_vm::DidKeyResolver`] for `did:key:` URIs (no I/O); plug
209    /// in a custom resolver for `did:web`, `did:webvh`, or any other
210    /// method. The library also checks that the resolved key's
211    /// [`KeyType`] matches the proof's cryptosuite — a cheap guard
212    /// against "right proof, wrong key" class bugs.
213    ///
214    /// Async because typical resolvers perform I/O (HTTP, cache lookups,
215    /// HSM introspection).
216    ///
217    /// [`KeyType`]: affinidi_secrets_resolver::secrets::KeyType
218    #[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        // Belt-and-braces key-type check. The CryptoSuiteOps verify will
232        // fail on a mismatched key anyway, but a typed error here is
233        // clearer to callers and saves the canonicalization work.
234        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
250// -----------------------------------------------------------------------
251// Internal signing helpers
252// -----------------------------------------------------------------------
253
254async 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    // Proof context: caller override, else pulled from document @context.
305    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    // Cryptosuite allowlist.
359    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    // Context match (only when caller explicitly supplied one).
367    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    // Decode proofValue.
376    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    // Strip the proof_value from the proof config for re-hashing.
386    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    // Canonicalize & hash (JCS or RDFC depending on suite).
410    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
437// -----------------------------------------------------------------------
438// Hashing pipelines (shared by all cryptosuites in this family)
439// -----------------------------------------------------------------------
440
441/// Hashing Algorithm for EDDSA JCS
442fn 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
450/// Hashing Algorithm for EDDSA RDFC.
451/// Runs both document and proof config through the RDFC pipeline
452/// (JSON-LD expansion → RDF Dataset → RDFC-1.0 canonicalization → SHA-256)
453/// and concatenates the two 32-byte hashes.
454fn 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
469// -----------------------------------------------------------------------
470// Remote-signer helper: returns the exact bytes a signer is expected to
471// sign over, so remote-signing protocols can compute the input ahead of
472// time without recomputing the canonicalization/hash pipeline.
473// -----------------------------------------------------------------------
474
475/// Returns the byte string a [`Signer`] is expected to sign over, given
476/// a document, a partial proof config, and the target cryptosuite.
477///
478/// Remote signers (KMS, HSM) typically want this so they can submit a
479/// well-formed "sign these bytes" request to their backend without
480/// re-implementing canonicalization. The returned bytes are exactly
481/// what [`DataIntegrityProof::sign`] passes to `signer.sign(data)`.
482///
483/// `proof_config` should be the proof JSON value with `proofValue`
484/// absent but all other fields set (cryptosuite, verificationMethod,
485/// proofPurpose, created, optional @context).
486pub 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
511// -----------------------------------------------------------------------
512// Internal date helpers
513// -----------------------------------------------------------------------
514
515fn 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        // Use the library-built VM URI so the resolver can find the key.
547        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        // Signer defaulted to mldsa44-jcs-2024 via Signer::cryptosuite().
603        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        // An Ed25519 signer asked to produce mldsa44 must fail with
616        // KeyTypeMismatch — the caller overrode the default.
617        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    /// An attacker rewrites the `cryptosuite` field on a proof they
633    /// otherwise can't forge — e.g. swaps `eddsa-jcs-2022` to
634    /// `eddsa-rdfc-2022`. Verification must fail because the
635    /// canonicalization axis changes the hashed bytes.
636    #[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        // Attacker flips the suite.
648        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}