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}