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, verify_conformance_with_skew};
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::{DEFAULT_CLOCK_SKEW, SignOptions, VerifyOptions};
108
109/// Serialized Data Integrity proof.
110///
111/// `#[non_exhaustive]`: produce via [`DataIntegrityProof::sign`] or
112/// [`DataIntegrityProof::new`] rather than a struct literal. Fields stay public
113/// for reads.
114#[derive(Clone, Debug, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116#[non_exhaustive]
117pub struct DataIntegrityProof {
118    /// Must be 'DataIntegrityProof'
119    #[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    /// The proof's `nonce` (VC Data Integrity §2.1), an optional value the
135    /// proof creator supplies to reduce linkability between signatures.
136    ///
137    /// It carries no verification semantics of its own, but it is part of the
138    /// proof configuration, and every cryptosuite hashes the proof
139    /// configuration. Without this field serde dropped a producer's `nonce` on
140    /// deserialize, so [`verify`](Self::verify) re-hashed a configuration the
141    /// producer never signed and rejected a valid proof as
142    /// `signature invalid`. Producers that set one — `affinidi-ssi-dart` does on
143    /// every proof — could not be verified here at all.
144    ///
145    /// This crate's own signer does not set it.
146    #[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    /// Assemble a Data Integrity proof from its parts. `type_` is fixed to
155    /// `"DataIntegrityProof"`. Most callers should use [`DataIntegrityProof::sign`];
156    /// this is for reconstructing a proof from known components (e.g. tests or
157    /// custom flows).
158    #[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    /// Produces a Data Integrity proof over `data_doc`.
180    ///
181    /// The cryptosuite is picked from [`SignOptions::cryptosuite`] if
182    /// set, otherwise from [`Signer::cryptosuite`]. Canonicalization
183    /// (JCS or RDFC) is derived from the suite.
184    ///
185    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                // Two different failures reach here, and reporting them the
199                // same way sends the caller after the wrong thing.
200                //
201                // `Signer::cryptosuite()`'s default is
202                // `default_for_key_type(..).unwrap_or(EddsaJcs2022)`, so a key
203                // type with no suite compiled in arrives carrying an Ed25519
204                // suite the caller never asked for. Reporting that as
205                // `KeyTypeMismatch { expected: Ed25519, .. }` blames them for
206                // the library's own invention, and names an algorithm that
207                // appears nowhere in their configuration.
208                //
209                // The honest message for that case is that no cryptosuite
210                // exists for the key — which for ML-DSA-65/87 is a fact about
211                // W3C Quantum-Resistant Cryptosuites v1.0, which defines only
212                // the ML-DSA-44 variants, not a defect to be worked around. It
213                // is reachable: a VTA can mint an ML-DSA-65 key.
214                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                // A genuine mismatch: the caller named a suite and handed it a
228                // key it cannot use.
229                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    /// Verifies a proof against `data_doc` using caller-provided public
273    /// key bytes.
274    ///
275    /// Sync because this is pure CPU — callers who already have the key
276    /// should not be forced into an async runtime. See [`verify`] for
277    /// the resolver-based async variant.
278    ///
279    /// [`verify`]: Self::verify
280    #[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    /// Verifies a proof by resolving the public key from its
294    /// `verificationMethod` via a [`VerificationMethodResolver`].
295    ///
296    /// Use [`did_vm::DidKeyResolver`] for `did:key:` URIs (no I/O); plug
297    /// in a custom resolver for `did:web`, `did:webvh`, or any other
298    /// method. The library also checks that the resolved key's
299    /// [`KeyType`] matches the proof's cryptosuite — a cheap guard
300    /// against "right proof, wrong key" class bugs.
301    ///
302    /// Async because typical resolvers perform I/O (HTTP, cache lookups,
303    /// HSM introspection).
304    ///
305    /// [`KeyType`]: affinidi_secrets_resolver::secrets::KeyType
306    #[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        // Belt-and-braces key-type check. The CryptoSuiteOps verify will
320        // fail on a mismatched key anyway, but a typed error here is
321        // clearer to callers and saves the canonicalization work.
322        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
338// -----------------------------------------------------------------------
339// Internal signing helpers
340// -----------------------------------------------------------------------
341
342async 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    // Proof context: caller override, else pulled from document @context.
394    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    // Cryptosuite allowlist.
449    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    // Context match (only when caller explicitly supplied one).
457    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    // Decode proofValue.
466    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    // Strip the proof_value from the proof config for re-hashing.
476    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    // `created` is stamped by the signer's clock and checked against
488    // ours, so a strict comparison makes acceptance a race between clock
489    // skew and delivery latency. Allow `options.clock_skew` (default 60s)
490    // of drift; a negative setting is clamped to zero rather than making
491    // the check *stricter* than exact.
492    if let Some(created) = &proof_config.created {
493        let skew = options.clock_skew.max(chrono::TimeDelta::zero());
494        // `checked_add_signed` so an absurd caller-supplied skew saturates
495        // instead of panicking on overflow, as `+` would.
496        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    // Canonicalize & hash (JCS or RDFC depending on suite).
511    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
541// -----------------------------------------------------------------------
542// Hashing pipelines (shared by all cryptosuites in this family)
543// -----------------------------------------------------------------------
544
545/// Hashing Algorithm for EDDSA JCS
546fn 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
554/// Hashing Algorithm for EDDSA RDFC.
555/// Runs both document and proof config through the RDFC pipeline
556/// (JSON-LD expansion → RDF Dataset → RDFC-1.0 canonicalization → SHA-256)
557/// and concatenates the two 32-byte hashes.
558fn 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
573// -----------------------------------------------------------------------
574// Remote-signer helper: returns the exact bytes a signer is expected to
575// sign over, so remote-signing protocols can compute the input ahead of
576// time without recomputing the canonicalization/hash pipeline.
577// -----------------------------------------------------------------------
578
579/// Returns the byte string a [`Signer`] is expected to sign over, given
580/// a document, a partial proof config, and the target cryptosuite.
581///
582/// Remote signers (KMS, HSM) typically want this so they can submit a
583/// well-formed "sign these bytes" request to their backend without
584/// re-implementing canonicalization. The returned bytes are exactly
585/// what [`DataIntegrityProof::sign`] passes to `signer.sign(data)`.
586///
587/// `proof_config` should be the proof JSON value with `proofValue`
588/// absent but all other fields set (cryptosuite, verificationMethod,
589/// proofPurpose, created, optional @context).
590pub 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
615// -----------------------------------------------------------------------
616// Internal date helpers
617// -----------------------------------------------------------------------
618
619fn 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        // Use the library-built VM URI so the resolver can find the key.
652        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    /// The end-to-end property that matters: a P-256 key signs a document and
667    /// the proof verifies through the ordinary `did:key` resolver path, with no
668    /// suite named by hand at either end.
669    #[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    /// A proof must not survive its document changing — ECDSA is randomised, so
695    /// a round-trip test alone would still pass against a verify that ignored
696    /// the payload.
697    #[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        // Signer defaulted to mldsa44-jcs-2024 via Signer::cryptosuite().
763        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        // An Ed25519 signer asked to produce mldsa44 must fail with
776        // KeyTypeMismatch — the caller overrode the default.
777        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    /// A signer whose clock runs slightly ahead of the verifier's stamps
793    /// `created` in the verifier's future. With zero tolerance this made
794    /// acceptance a race between clock skew and delivery latency — the
795    /// same proof verified or not depending on how fast it arrived. The
796    /// default allowance must absorb it.
797    #[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    /// The allowance is bounded: a `created` beyond it is still rejected,
815    /// and the error names the allowance so the cause is diagnosable from
816    /// the wire message alone.
817    #[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    /// Zero skew restores the strict pre-allowance behaviour for callers
841    /// that want it.
842    #[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    /// An attacker rewrites the `cryptosuite` field on a proof they
865    /// otherwise can't forge — e.g. swaps `eddsa-jcs-2022` to
866    /// `eddsa-rdfc-2022`. Verification must fail because the
867    /// canonicalization axis changes the hashed bytes.
868    #[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        // Attacker flips the suite.
880        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    /// This key used to be the crate's "bad key" fixture, on the strength of
926    /// signing failing for it. It is a **P-256** key, and the failure was only
927    /// ever "no suite compiled in for this key type" — so `ecdsa-jcs-2019`
928    /// turns it into a supported key, and the old assertion asserted the
929    /// absence of a feature rather than any property of the key.
930    #[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    /// The genuine "unsupported key type" case the previous test was standing
952    /// in for. secp256k1 has no Data Integrity suite here, so it must still
953    /// fail — otherwise a key type nothing can verify would sign silently.
954    #[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}