Skip to main content

acdp_verify/
lib.rs

1//! High-level body / publish-request verification — RFC-ACDP-0001 §5.11
2//! (7-step algorithm).
3//!
4//! This layer sits above `validation`, `types`, `crypto`, and `did`: it
5//! recomputes the `content_hash`, runs structural validation, resolves
6//! the producer DID, and verifies the signature envelope. The byte-level
7//! primitives ([`acdp_crypto::verify_ed25519`] /
8//! [`acdp_crypto::verify_ecdsa_p256`]) live in `crypto`.
9
10use acdp_crypto::{verify_content_hash, verify_ecdsa_p256, verify_ed25519};
11use acdp_primitives::error::AcdpError;
12use acdp_types::body::{Body, Signature};
13use acdp_types::primitives::ContentHash;
14use acdp_types::publish::PublishRequest;
15
16#[cfg(feature = "client")]
17use {acdp_did::web::WebResolver, acdp_types::primitives::AgentDid};
18
19/// Stateless verifier.  Requires a DID resolver to fetch producer keys.
20#[cfg(feature = "client")]
21pub struct Verifier<'a> {
22    resolver: &'a WebResolver,
23}
24
25#[cfg(feature = "client")]
26impl<'a> Verifier<'a> {
27    pub fn new(resolver: &'a WebResolver) -> Self {
28        Self { resolver }
29    }
30
31    /// Full end-to-end verification per RFC-ACDP-0001 §5.11.
32    ///
33    /// Steps:
34    ///  1. (Implicit) Check `key_id` has a `#fragment`.
35    ///  2. Verify `key_id` DID portion equals `body.agent_id`.
36    ///  3. Resolve the DID document.
37    ///  4. Find the verification method by fragment.
38    ///  5. Check `assertionMethod` authorization.
39    ///  6. Extract the Ed25519 public key.
40    ///  7. Verify the signature over the content_hash ASCII bytes.
41    ///
42    ///  (Hash recomputation is step 0, performed first.)
43    pub async fn verify_body(&self, body: &Body) -> Result<(), AcdpError> {
44        // Step -1 (BUG-04): structural / runtime validation. A body may be
45        // cryptographically correct but protocol-invalid (non-did:web
46        // producer, inverted data_period, oversize metadata). Catch those
47        // before paying the SHA-256 + DID resolution cost.
48        acdp_validation::validate_body(body)?;
49
50        self.verify_body_signed(body).await
51    }
52
53    /// Verify only the hash recomputation + DID resolution + signature
54    /// envelope, assuming structural validation has already been done by
55    /// the caller. Use when you want to separate structural failures
56    /// from cryptographic ones — e.g.
57    /// `acdp::client::VerifiedContext::fetch_report` runs the
58    /// structural part itself and records per-`DataRef` outcomes
59    /// individually.
60    pub async fn verify_body_signed(&self, body: &Body) -> Result<(), AcdpError> {
61        self.verify_body_hash(body)?;
62        self.verify_body_signature(body).await
63    }
64
65    /// Step 0 only — recompute the `content_hash` over ProducerContent
66    /// and compare against `body.content_hash`. Lets diagnostic
67    /// callers record hash-pass/fail independently of the signature
68    /// stage (FEAT-05).
69    pub fn verify_body_hash(&self, body: &Body) -> Result<(), AcdpError> {
70        let body_val = serde_json::to_value(body)?;
71        verify_content_hash(&body_val, &body.content_hash)
72    }
73
74    /// Steps 1–7 only — resolve the producer's DID, find the signing
75    /// key, verify the signature over the (already-stored)
76    /// `body.content_hash`. Assumes [`Self::verify_body_hash`] (or an
77    /// equivalent check) has already run.
78    pub async fn verify_body_signature(&self, body: &Body) -> Result<(), AcdpError> {
79        verify_signature_envelope(
80            &body.agent_id,
81            &body.signature,
82            &body.content_hash,
83            self.resolver,
84        )
85        .await
86    }
87}
88
89/// Verify the producer signature on a [`PublishRequest`] per RFC-ACDP-0003
90/// §2.1 steps 7–8.
91///
92/// Assumes structural validation and `content_hash` recomputation have
93/// already been performed (e.g. by `acdp::registry::PublishValidator::validate_post_schema`).
94/// Executes only the DID resolution + signature verification steps shared
95/// with [`Verifier::verify_body`].
96///
97/// Used by `acdp::registry::RegistryServer::publish_verified` to fulfill
98/// the §2.1 publish algorithm before persistence; consumers wanting end-to-end
99/// verification on retrieval should prefer
100/// `acdp::client::VerifiedContext::fetch` which calls [`Verifier::verify_body`].
101#[cfg(feature = "client")]
102pub async fn verify_publish_request_signature(
103    req: &PublishRequest,
104    resolver: &WebResolver,
105) -> Result<(), AcdpError> {
106    verify_signature_envelope(&req.agent_id, &req.signature, &req.content_hash, resolver).await
107}
108
109/// Steps 1–7 of RFC-ACDP-0001 §5.11 — the part of body verification that
110/// operates only on the signature envelope and is identical for stored
111/// `Body` values and incoming `PublishRequest` values. Caller is responsible
112/// for hash recomputation (step 0).
113#[cfg(feature = "client")]
114async fn verify_signature_envelope(
115    agent_id: &AgentDid,
116    signature: &Signature,
117    content_hash: &ContentHash,
118    resolver: &WebResolver,
119) -> Result<(), AcdpError> {
120    // Step 1: parse key_id — must contain a non-empty '#' fragment
121    // (RFC-ACDP-0001 §5.11 step 1). An empty fragment (`did:web:x#`) is
122    // rejected rather than used as a lookup key (#22).
123    let key_id = &signature.key_id;
124    let (did_part, fragment) = key_id.split_once('#').ok_or_else(|| {
125        AcdpError::KeyResolution(format!("signature.key_id '{key_id}' has no '#fragment'"))
126    })?;
127    if fragment.is_empty() {
128        return Err(AcdpError::KeyResolution(format!(
129            "signature.key_id '{key_id}' has an empty '#fragment'"
130        )));
131    }
132
133    // Step 2: DID portion MUST equal agent_id
134    if did_part != agent_id.as_str() {
135        return Err(AcdpError::KeyNotAuthorized(format!(
136            "key_id DID '{did_part}' ≠ agent_id '{agent_id}'"
137        )));
138    }
139
140    // Step 1.5: method dispatch. `did:key` resolves purely (the DID is
141    // the key — no document fetch, no assertionMethod check); `did:web`
142    // takes the HTTPS resolver path below. Any other method has no
143    // resolver in this version.
144    if did_part.starts_with("did:key:") {
145        return verify_did_key_envelope(signature, content_hash);
146    }
147    if !did_part.starts_with("did:web:") {
148        return Err(AcdpError::KeyNotAuthorized(format!(
149            "signatures require a did:web or did:key key_id; got '{did_part}'"
150        )));
151    }
152
153    // Step 3: resolve DID document
154    let doc = resolver.resolve(did_part).await?;
155
156    // Step 4: find verification method by fragment
157    let method = doc.find_by_fragment(fragment).ok_or_else(|| {
158        AcdpError::KeyResolution(format!(
159            "no verification method with fragment '#{fragment}'"
160        ))
161    })?;
162
163    // Step 5: assertionMethod authorization
164    if !doc.is_assertion_method(&method.id) {
165        return Err(AcdpError::KeyNotAuthorized(format!(
166            "'{}' is not in assertionMethod",
167            method.id
168        )));
169    }
170
171    // Step 5.5: algorithm-downgrade rejection (RFC-ACDP-0008 §3.9 +
172    // RFC-ACDP-0001 §5.11 step 6). When the verification method declares
173    // an algorithm via its `type` (or `publicKeyJwk` params), it MUST equal
174    // `signature.algorithm`. Otherwise an attacker could route an Ed25519
175    // key through a verifier that thinks it's checking some other algorithm.
176    if let Some(declared) = method.declared_algorithm() {
177        if declared != signature.algorithm {
178            return Err(AcdpError::InvalidSignature(format!(
179                "signature.algorithm '{}' does not match verification method type \
180                 (resolved key declares '{declared}')",
181                signature.algorithm
182            )));
183        }
184    }
185
186    // Steps 6 + 7: dispatch by algorithm.
187    match signature.algorithm.as_str() {
188        "ed25519" => {
189            let pub_bytes = method.ed25519_public_key_bytes()?;
190            verify_ed25519(&pub_bytes, &signature.value, content_hash.as_str())
191        }
192        "ecdsa-p256" => {
193            let pub_sec1 = method.ecdsa_p256_public_key_sec1()?;
194            verify_ecdsa_p256(&pub_sec1, &signature.value, content_hash.as_str())
195        }
196        other => Err(AcdpError::UnsupportedAlgorithm(format!(
197            "verifier does not support signature algorithm '{other}'"
198        ))),
199    }
200}
201
202/// Verify a signature envelope whose key is a `did:key` — a pure
203/// function available without the `client` feature (no resolver, no
204/// network, no async).
205///
206/// Performs:
207/// 1. `key_id` form check (`did:key:z<mb>#z<mb>`, fragment = key).
208/// 2. Pure key resolution from the DID itself.
209/// 3. Algorithm-downgrade rejection: `signature.algorithm` MUST equal
210///    the algorithm implied by the key's multicodec prefix
211///    (RFC-ACDP-0008 §3.9).
212/// 4. Signature verification over the ASCII bytes of `content_hash`.
213///
214/// The caller is responsible for the `key_id`-DID-equals-`agent_id`
215/// binding check and for `content_hash` recomputation (use
216/// [`verify_body_offline`] for the full pipeline).
217pub fn verify_did_key_envelope(
218    signature: &Signature,
219    content_hash: &ContentHash,
220) -> Result<(), AcdpError> {
221    let material = acdp_did::key::resolve_did_key_url(&signature.key_id)?;
222
223    if material.algorithm() != signature.algorithm {
224        return Err(AcdpError::InvalidSignature(format!(
225            "signature.algorithm '{}' does not match the did:key multicodec \
226             (key implies '{}')",
227            signature.algorithm,
228            material.algorithm()
229        )));
230    }
231
232    match material {
233        acdp_did::key::DidKeyMaterial::Ed25519(pub_bytes) => {
234            verify_ed25519(&pub_bytes, &signature.value, content_hash.as_str())
235        }
236        acdp_did::key::DidKeyMaterial::EcdsaP256(sec1_compressed) => {
237            verify_ecdsa_p256(&sec1_compressed, &signature.value, content_hash.as_str())
238        }
239    }
240}
241
242/// Full offline body verification for `did:key` producers — works with
243/// `--no-default-features` (no HTTP stack, no resolver, no async).
244///
245/// Pipeline (mirrors [`Verifier::verify_body`] minus DID-document
246/// resolution, which did:key does not have):
247/// 1. Structural validation ([`acdp_validation::validate_body`]).
248/// 2. `content_hash` recomputation over ProducerContent (§5.7).
249/// 3. `key_id` DID portion equals `agent_id`.
250/// 4. Pure did:key envelope verification (algorithm + signature).
251///
252/// Returns [`AcdpError::KeyResolution`] for a `did:web` (or other
253/// method) body — those require the resolver-backed
254/// [`Verifier::verify_body`] under the `client` feature.
255pub fn verify_body_offline(body: &Body) -> Result<(), AcdpError> {
256    acdp_validation::validate_body(body)?;
257
258    if !body.agent_id.as_str().starts_with("did:key:") {
259        return Err(AcdpError::KeyResolution(format!(
260            "verify_body_offline supports did:key producers only; '{}' requires \
261             the resolver-backed Verifier (client feature)",
262            body.agent_id
263        )));
264    }
265
266    let body_val = serde_json::to_value(body)?;
267    verify_content_hash(&body_val, &body.content_hash)?;
268
269    let did_part = body
270        .signature
271        .key_id
272        .split_once('#')
273        .map(|(d, _)| d)
274        .unwrap_or(body.signature.key_id.as_str());
275    if did_part != body.agent_id.as_str() {
276        return Err(AcdpError::KeyNotAuthorized(format!(
277            "key_id DID '{did_part}' ≠ agent_id '{}'",
278            body.agent_id
279        )));
280    }
281
282    verify_did_key_envelope(&body.signature, &body.content_hash)
283}
284
285/// Offline counterpart of [`verify_publish_request_signature`] for
286/// `did:key` producers — used by registries (and the bindings) to verify
287/// a publish request without the `client` feature. Assumes structural
288/// validation and `content_hash` recomputation have already run
289/// (e.g. via `PublishValidator::validate_post_schema`).
290pub fn verify_publish_request_signature_offline(req: &PublishRequest) -> Result<(), AcdpError> {
291    let key_id = req.signature.key_id.as_str();
292    let did_part = key_id.split_once('#').map(|(d, _)| d).unwrap_or(key_id);
293    if did_part != req.agent_id.as_str() {
294        return Err(AcdpError::KeyNotAuthorized(format!(
295            "key_id DID '{did_part}' ≠ agent_id '{}'",
296            req.agent_id
297        )));
298    }
299    if !did_part.starts_with("did:key:") {
300        return Err(AcdpError::KeyResolution(format!(
301            "offline verification supports did:key only; got '{did_part}'"
302        )));
303    }
304    verify_did_key_envelope(&req.signature, &req.content_hash)
305}
306
307/// Historical-key body-signature verification (ACDP 0.2, WS-B).
308///
309/// Identical to the standard envelope verification EXCEPT that the
310/// `assertionMethod` membership check is skipped: a rotated-out key
311/// that the producer retained in `verificationMethod` (per the
312/// RFC-ACDP-0010 key-retention rule) still verifies. Callers MUST
313/// gate this on a **verified registry receipt** whose
314/// `key_fingerprint` matches this key — without that attestation,
315/// accepting a non-assertion key is exactly the bypass the
316/// `assertionMethod` check exists to prevent. did:key bodies never
317/// take this path (the key cannot rotate).
318#[cfg(feature = "client")]
319pub async fn verify_body_signature_historical(
320    body: &Body,
321    resolver: &WebResolver,
322) -> Result<(), AcdpError> {
323    let key_id = &body.signature.key_id;
324    let (did_part, fragment) = key_id.split_once('#').ok_or_else(|| {
325        AcdpError::KeyResolution(format!("signature.key_id '{key_id}' has no '#fragment'"))
326    })?;
327    if did_part != body.agent_id.as_str() {
328        return Err(AcdpError::KeyNotAuthorized(format!(
329            "key_id DID '{did_part}' ≠ agent_id '{}'",
330            body.agent_id
331        )));
332    }
333    if !did_part.starts_with("did:web:") {
334        return Err(AcdpError::KeyResolution(format!(
335            "historical-key verification applies to did:web only; got '{did_part}'"
336        )));
337    }
338    let doc = resolver.resolve(did_part).await?;
339    // Key fully removed from the DID document → fail closed. The
340    // producer's obligation is to RETAIN rotated keys in
341    // verificationMethod (RFC-ACDP-0010); a deleted key is
342    // unverifiable by design.
343    let method = doc.find_by_fragment(fragment).ok_or_else(|| {
344        AcdpError::KeyResolution(format!(
345            "no verification method with fragment '#{fragment}' — the key was \
346             removed from the DID document, not just rotated out of assertionMethod"
347        ))
348    })?;
349    if let Some(declared) = method.declared_algorithm() {
350        if declared != body.signature.algorithm {
351            return Err(AcdpError::InvalidSignature(format!(
352                "signature.algorithm '{}' does not match verification method type \
353                 (resolved key declares '{declared}')",
354                body.signature.algorithm
355            )));
356        }
357    }
358    match body.signature.algorithm.as_str() {
359        "ed25519" => verify_ed25519(
360            &method.ed25519_public_key_bytes()?,
361            &body.signature.value,
362            body.content_hash.as_str(),
363        ),
364        "ecdsa-p256" => verify_ecdsa_p256(
365            &method.ecdsa_p256_public_key_sec1()?,
366            &body.signature.value,
367            body.content_hash.as_str(),
368        ),
369        other => Err(AcdpError::UnsupportedAlgorithm(format!(
370            "verifier does not support signature algorithm '{other}'"
371        ))),
372    }
373}