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/// **Deprecated** — the legacy affinidi-internal `bbs-2023` encoding (not
92/// interoperable with other vc-di-bbs implementations). Use
93/// [`bbs_2023_transform`] instead. Enabled via the `bbs-2023` feature flag.
94#[cfg(feature = "bbs-2023")]
95pub mod bbs_2023;
96
97/// W3C vc-di-bbs `bbs-2023` cryptosuite (RDF-canonical, standards-interoperable)
98/// — **the** `bbs-2023` implementation. Pinned byte-for-byte to the official
99/// `w3c/vc-di-bbs` vectors: issuer ([`bbs_2023_transform::sign_base_document`]),
100/// holder ([`bbs_2023_transform::create_derived_proof`]), verifier
101/// ([`bbs_2023_transform::verify_derived_proof`]), plus per-verifier pseudonym /
102/// holder binding. Supersedes the legacy [`bbs_2023`] module.
103#[cfg(feature = "bbs-2023")]
104pub mod bbs_2023_transform;
105
106pub use error::{DataIntegrityError, SignatureFailure};
107pub use options::{SignOptions, VerifyOptions};
108
109/// Serialized Data Integrity proof.
110#[derive(Clone, Debug, Deserialize, Serialize)]
111#[serde(rename_all = "camelCase")]
112pub struct DataIntegrityProof {
113    /// Must be 'DataIntegrityProof'
114    #[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    /// Produces a Data Integrity proof over `data_doc`.
135    ///
136    /// The cryptosuite is picked from [`SignOptions::cryptosuite`] if
137    /// set, otherwise from [`Signer::cryptosuite`]. Canonicalization
138    /// (JCS or RDFC) is derived from the suite.
139    ///
140    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    /// Verifies a proof against `data_doc` using caller-provided public
194    /// key bytes.
195    ///
196    /// Sync because this is pure CPU — callers who already have the key
197    /// should not be forced into an async runtime. See [`verify`] for
198    /// the resolver-based async variant.
199    ///
200    /// [`verify`]: Self::verify
201    #[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    /// Verifies a proof by resolving the public key from its
215    /// `verificationMethod` via a [`VerificationMethodResolver`].
216    ///
217    /// Use [`did_vm::DidKeyResolver`] for `did:key:` URIs (no I/O); plug
218    /// in a custom resolver for `did:web`, `did:webvh`, or any other
219    /// method. The library also checks that the resolved key's
220    /// [`KeyType`] matches the proof's cryptosuite — a cheap guard
221    /// against "right proof, wrong key" class bugs.
222    ///
223    /// Async because typical resolvers perform I/O (HTTP, cache lookups,
224    /// HSM introspection).
225    ///
226    /// [`KeyType`]: affinidi_secrets_resolver::secrets::KeyType
227    #[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        // Belt-and-braces key-type check. The CryptoSuiteOps verify will
241        // fail on a mismatched key anyway, but a typed error here is
242        // clearer to callers and saves the canonicalization work.
243        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
259// -----------------------------------------------------------------------
260// Internal signing helpers
261// -----------------------------------------------------------------------
262
263async 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    // Proof context: caller override, else pulled from document @context.
314    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    // Cryptosuite allowlist.
368    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    // Context match (only when caller explicitly supplied one).
376    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    // Decode proofValue.
385    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    // Strip the proof_value from the proof config for re-hashing.
395    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    // Canonicalize & hash (JCS or RDFC depending on suite).
419    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
449// -----------------------------------------------------------------------
450// Hashing pipelines (shared by all cryptosuites in this family)
451// -----------------------------------------------------------------------
452
453/// Hashing Algorithm for EDDSA JCS
454fn 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
462/// Hashing Algorithm for EDDSA RDFC.
463/// Runs both document and proof config through the RDFC pipeline
464/// (JSON-LD expansion → RDF Dataset → RDFC-1.0 canonicalization → SHA-256)
465/// and concatenates the two 32-byte hashes.
466fn 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
481// -----------------------------------------------------------------------
482// Remote-signer helper: returns the exact bytes a signer is expected to
483// sign over, so remote-signing protocols can compute the input ahead of
484// time without recomputing the canonicalization/hash pipeline.
485// -----------------------------------------------------------------------
486
487/// Returns the byte string a [`Signer`] is expected to sign over, given
488/// a document, a partial proof config, and the target cryptosuite.
489///
490/// Remote signers (KMS, HSM) typically want this so they can submit a
491/// well-formed "sign these bytes" request to their backend without
492/// re-implementing canonicalization. The returned bytes are exactly
493/// what [`DataIntegrityProof::sign`] passes to `signer.sign(data)`.
494///
495/// `proof_config` should be the proof JSON value with `proofValue`
496/// absent but all other fields set (cryptosuite, verificationMethod,
497/// proofPurpose, created, optional @context).
498pub 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
523// -----------------------------------------------------------------------
524// Internal date helpers
525// -----------------------------------------------------------------------
526
527fn 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        // Use the library-built VM URI so the resolver can find the key.
559        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        // Signer defaulted to mldsa44-jcs-2024 via Signer::cryptosuite().
615        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        // An Ed25519 signer asked to produce mldsa44 must fail with
628        // KeyTypeMismatch — the caller overrode the default.
629        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    /// An attacker rewrites the `cryptosuite` field on a proof they
645    /// otherwise can't forge — e.g. swaps `eddsa-jcs-2022` to
646    /// `eddsa-rdfc-2022`. Verification must fail because the
647    /// canonicalization axis changes the hashed bytes.
648    #[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        // Attacker flips the suite.
660        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}