Skip to main content

trust_tasks_proof/affinidi/
mod.rs

1//! [`Verifier`] — a [`ProofVerifier`] implementation backed by
2//! `affinidi-data-integrity` — and [`sign_trust_task`], its sign-side
3//! counterpart for producers.
4//!
5//! Supports the W3C Data Integrity cryptosuites `eddsa-rdfc-2022` and
6//! `eddsa-jcs-2022` out of the box; `bbs-2023` and post-quantum variants
7//! follow whatever feature flags the upstream crate exposes (see its
8//! changelog).
9//!
10//! ```rust,ignore
11//! use trust_tasks_proof::affinidi::Verifier;
12//!
13//! // For did:key — purely local, no I/O.
14//! let verifier = Verifier::for_did_key();
15//! verifier.verify(&inbound_doc).await?;
16//! ```
17//!
18//! For `did:web` or other resolvers, supply a
19//! [`affinidi_data_integrity::VerificationMethodResolver`] via
20//! [`Verifier::with_resolver`].
21//!
22//! The implementation removes the `proof` member from the document before
23//! handing it to the Affinidi `verify` call, as required by the W3C
24//! Data Integrity canonicalisation rules — the proof is over the doc
25//! *plus* the proof's own configuration (everything except `proofValue`),
26//! not over the embedded proof object itself. [`sign_trust_task`] applies
27//! the identical document-minus-`proof` contract on the sign side, so
28//! what it emits is what [`Verifier`] verifies.
29
30mod resolver;
31mod sign;
32pub use resolver::CachedDidResolver;
33pub use sign::{sign_trust_task, SignError};
34
35use std::sync::Arc;
36
37use affinidi_data_integrity::{
38    DataIntegrityError, DataIntegrityProof, DidKeyResolver, SignatureFailure,
39    VerificationMethodResolver, VerifyOptions,
40};
41use async_trait::async_trait;
42use serde::Serialize;
43use serde_json::Value;
44use trust_tasks_rs::{ProofVerifier, TrustTask, VerificationError};
45
46/// Re-export the upstream resolver trait so callers can implement custom
47/// `did:web` / `did:webvh` resolvers without adding a direct dep on the
48/// upstream crate.
49pub use affinidi_data_integrity::DidKeyResolver as AffinidiDidKeyResolver;
50
51/// Re-export the upstream signer trait so callers can drive
52/// [`sign_trust_task`] from a KMS/HSM-backed signer without adding a
53/// direct dep on the upstream crate.
54pub use affinidi_data_integrity::signer::Signer as AffinidiSigner;
55
56/// Re-export the upstream sign options + cryptosuite enum so callers can
57/// build [`sign_trust_task`] options without a direct upstream dep.
58pub use affinidi_data_integrity::{crypto_suites::CryptoSuite, SignOptions};
59
60/// [`ProofVerifier`] implementation backed by the Affinidi Data Integrity
61/// crate.
62///
63/// Construct with [`Self::for_did_key`] for `did:key`-only verification
64/// (no I/O, suitable for tests and self-issued documents), or with
65/// [`Self::with_resolver`] when you need to resolve `did:web` /
66/// `did:webvh` / other DID methods.
67pub struct Verifier {
68    resolver: Arc<dyn VerificationMethodResolver>,
69    options: VerifyOptions,
70}
71
72impl Verifier {
73    /// Verifier that resolves `did:key:` URIs locally; rejects every other
74    /// DID method.
75    pub fn for_did_key() -> Self {
76        Self::with_resolver(Arc::new(DidKeyResolver))
77    }
78
79    /// Verifier with a caller-supplied resolver. Use this with the
80    /// Affinidi DID-resolver cache SDK, an in-process `did:web` lookup,
81    /// a custom HSM bridge, etc.
82    pub fn with_resolver(resolver: Arc<dyn VerificationMethodResolver>) -> Self {
83        Self {
84            resolver,
85            options: VerifyOptions::default(),
86        }
87    }
88
89    /// Override the [`VerifyOptions`] (expected proof purpose, expected
90    /// domain/challenge, etc.). Defaults are equivalent to
91    /// `VerifyOptions::default()`.
92    pub fn with_options(mut self, options: VerifyOptions) -> Self {
93        self.options = options;
94        self
95    }
96}
97
98#[async_trait]
99impl ProofVerifier for Verifier {
100    async fn verify<P>(&self, doc: &TrustTask<P>) -> Result<(), VerificationError>
101    where
102        P: Serialize + Send + Sync,
103    {
104        // ─── 1. Extract the proof.
105        let Some(proof) = &doc.proof else {
106            return Err(VerificationError::MalformedProof(
107                "document carries no proof member".to_string(),
108            ));
109        };
110
111        // ─── 2. Round-trip our typed Proof into the Affinidi
112        //        DataIntegrityProof. The two structs are members-equivalent
113        //        but use slightly different field names (proof_type vs type_,
114        //        camelCase vs snake_case via serde).
115        let proof_value = serde_json::to_value(proof)
116            .map_err(|e| VerificationError::MalformedProof(format!("serialise proof: {e}")))?;
117        let parsed_proof: DataIntegrityProof = serde_json::from_value(proof_value)
118            .map_err(|e| VerificationError::MalformedProof(format!("parse proof: {e}")))?;
119
120        // ─── 3. Serialise the document minus the proof member. We can't
121        //        avoid the JSON round-trip because TrustTask is generic
122        //        over P; a manual "skip this field" path would force
123        //        re-deriving the serializer.
124        let mut doc_value = serde_json::to_value(doc).map_err(|e| {
125            VerificationError::Other(format!("serialise TrustTask for verification: {e}"))
126        })?;
127        if let Some(obj) = doc_value.as_object_mut() {
128            obj.remove("proof");
129        }
130
131        // ─── 3b. Bind the proof to the in-band issuer (SPEC §4.7 / §4.8 /
132        //        §7.2 item 7). A valid signature proves only that *some* key
133        //        signed the document; authenticity additionally requires that
134        //        key to be controlled by the document's declared `issuer`.
135        //        Without this check an attacker signs with their own key under
136        //        their own DID while claiming any `issuer`, and every
137        //        downstream authorization keyed on the issuer runs for a
138        //        spoofed identity. Compare the verificationMethod's DID (the
139        //        portion before `#`) to `issuer` by exact string equality — no
140        //        normalization, per §4.8. For the rare DID method whose
141        //        controller differs from the VM's own DID this is conservative
142        //        (it rejects rather than trusting an unverified delegation).
143        match doc_value.get("issuer").and_then(|v| v.as_str()) {
144            None => {
145                return Err(VerificationError::IssuerMismatch(
146                    "document carries a proof but no in-band issuer to bind it to".to_string(),
147                ));
148            }
149            Some(issuer) => {
150                let vm_did = proof
151                    .verification_method
152                    .split('#')
153                    .next()
154                    .unwrap_or(&proof.verification_method);
155                if vm_did != issuer {
156                    return Err(VerificationError::IssuerMismatch(format!(
157                        "verificationMethod is controlled by {vm_did}, not the document issuer {issuer}"
158                    )));
159                }
160            }
161        }
162
163        // ─── 4. Hand to Affinidi.
164        parsed_proof
165            .verify(&doc_value, &*self.resolver, self.options.clone())
166            .await
167            .map_err(map_error)?;
168        Ok(())
169    }
170}
171
172/// Map [`DataIntegrityError`] variants into the framework's
173/// [`VerificationError`] taxonomy. The mapping aligns with SPEC.md §8.3:
174/// every failure surfaces as `proof_invalid` to the wire, distinguished
175/// from `proof_required` (which our caller raises) — `VerificationError`
176/// is what the framework returns when a proof IS present but fails to
177/// verify.
178fn map_error(err: DataIntegrityError) -> VerificationError {
179    match err {
180        DataIntegrityError::UnsupportedCryptoSuite { name } => {
181            VerificationError::UnsupportedCryptosuite(name)
182        }
183        DataIntegrityError::KeyTypeMismatch {
184            expected,
185            actual,
186            suite,
187        } => VerificationError::IssuerMismatch(format!(
188            "key type {actual:?} does not match cryptosuite {suite:?} (expected {expected:?})"
189        )),
190        DataIntegrityError::InvalidSignature { reason, .. } => match reason {
191            SignatureFailure::Malformed | SignatureFailure::Invalid => {
192                VerificationError::SignatureInvalid
193            }
194            _ => VerificationError::SignatureInvalid,
195        },
196        DataIntegrityError::InvalidPublicKey { reason, .. } => {
197            VerificationError::MalformedProof(format!("public key: {reason}"))
198        }
199        DataIntegrityError::Canonicalization(reason) => {
200            VerificationError::Other(format!("canonicalisation: {reason}"))
201        }
202        DataIntegrityError::MalformedProof(reason) => VerificationError::MalformedProof(reason),
203        other => VerificationError::Other(other.to_string()),
204    }
205}
206
207/// Convenience: parse the framework `Proof` JSON-equivalent into an
208/// [`affinidi_data_integrity::DataIntegrityProof`]. Exposed for callers
209/// who want to verify by passing the doc body and proof separately
210/// (e.g. when the proof was carried out-of-band).
211pub fn parse_data_integrity_proof(value: &Value) -> Result<DataIntegrityProof, VerificationError> {
212    serde_json::from_value(value.clone())
213        .map_err(|e| VerificationError::MalformedProof(format!("parse DataIntegrityProof: {e}")))
214}