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}