1use 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#[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 #[cfg_attr(
44 feature = "tracing",
45 tracing::instrument(
46 name = "acdp.verify_body",
47 skip_all,
48 fields(ctx_id = %body.ctx_id.0, agent_id = body.agent_id.as_str()),
49 err(Display)
50 )
51 )]
52 pub async fn verify_body(&self, body: &Body) -> Result<(), AcdpError> {
53 acdp_validation::validate_body(body)?;
58
59 self.verify_body_signed(body).await
60 }
61
62 #[cfg_attr(
70 feature = "tracing",
71 tracing::instrument(
72 name = "acdp.verify_body_signed",
73 skip_all,
74 fields(ctx_id = %body.ctx_id.0),
75 err(Display)
76 )
77 )]
78 pub async fn verify_body_signed(&self, body: &Body) -> Result<(), AcdpError> {
79 self.verify_body_hash(body)?;
80 #[cfg(feature = "tracing")]
81 tracing::debug!(
82 stage = "content_hash",
83 "content hash recomputed and matched"
84 );
85 self.verify_body_signature(body).await?;
86 #[cfg(feature = "tracing")]
87 tracing::debug!(stage = "signature", "producer signature verified");
88 Ok(())
89 }
90
91 pub fn verify_body_hash(&self, body: &Body) -> Result<(), AcdpError> {
96 let body_val = serde_json::to_value(body)?;
97 verify_content_hash(&body_val, &body.content_hash)
98 }
99
100 pub async fn verify_body_signature(&self, body: &Body) -> Result<(), AcdpError> {
105 verify_signature_envelope(
106 &body.agent_id,
107 &body.signature,
108 &body.content_hash,
109 self.resolver,
110 )
111 .await
112 }
113}
114
115#[cfg(feature = "client")]
128#[cfg_attr(
129 feature = "tracing",
130 tracing::instrument(
131 name = "acdp.verify_publish_request_signature",
132 skip_all,
133 fields(agent_id = req.agent_id.as_str(), key_id = %req.signature.key_id),
134 err(Display)
135 )
136)]
137pub async fn verify_publish_request_signature(
138 req: &PublishRequest,
139 resolver: &WebResolver,
140) -> Result<(), AcdpError> {
141 verify_signature_envelope(&req.agent_id, &req.signature, &req.content_hash, resolver).await
142}
143
144#[cfg(feature = "client")]
149async fn verify_signature_envelope(
150 agent_id: &AgentDid,
151 signature: &Signature,
152 content_hash: &ContentHash,
153 resolver: &WebResolver,
154) -> Result<(), AcdpError> {
155 let key_id = &signature.key_id;
159 let (did_part, fragment) = key_id.split_once('#').ok_or_else(|| {
160 AcdpError::KeyResolution(format!("signature.key_id '{key_id}' has no '#fragment'"))
161 })?;
162 if fragment.is_empty() {
163 return Err(AcdpError::KeyResolution(format!(
164 "signature.key_id '{key_id}' has an empty '#fragment'"
165 )));
166 }
167
168 if did_part != agent_id.as_str() {
170 return Err(AcdpError::KeyNotAuthorized(format!(
171 "key_id DID '{did_part}' ≠ agent_id '{agent_id}'"
172 )));
173 }
174
175 if did_part.starts_with("did:key:") {
180 return verify_did_key_envelope(signature, content_hash);
181 }
182 if !did_part.starts_with("did:web:") {
183 return Err(AcdpError::KeyNotAuthorized(format!(
184 "signatures require a did:web or did:key key_id; got '{did_part}'"
185 )));
186 }
187
188 let doc = resolver.resolve(did_part).await?;
190
191 let method = doc.find_by_fragment(fragment).ok_or_else(|| {
193 AcdpError::KeyResolution(format!(
194 "no verification method with fragment '#{fragment}'"
195 ))
196 })?;
197
198 if !doc.is_assertion_method(&method.id) {
200 return Err(AcdpError::KeyNotAuthorized(format!(
201 "'{}' is not in assertionMethod",
202 method.id
203 )));
204 }
205
206 if let Some(declared) = method.declared_algorithm() {
212 if declared != signature.algorithm {
213 return Err(AcdpError::InvalidSignature(format!(
214 "signature.algorithm '{}' does not match verification method type \
215 (resolved key declares '{declared}')",
216 signature.algorithm
217 )));
218 }
219 }
220
221 match signature.algorithm.as_str() {
223 "ed25519" => {
224 let pub_bytes = method.ed25519_public_key_bytes()?;
225 verify_ed25519(&pub_bytes, &signature.value, content_hash.as_str())
226 }
227 "ecdsa-p256" => {
228 let pub_sec1 = method.ecdsa_p256_public_key_sec1()?;
229 verify_ecdsa_p256(&pub_sec1, &signature.value, content_hash.as_str())
230 }
231 other => Err(AcdpError::UnsupportedAlgorithm(format!(
232 "verifier does not support signature algorithm '{other}'"
233 ))),
234 }
235}
236
237pub fn verify_did_key_envelope(
253 signature: &Signature,
254 content_hash: &ContentHash,
255) -> Result<(), AcdpError> {
256 let material = acdp_did::key::resolve_did_key_url(&signature.key_id)?;
257
258 if material.algorithm() != signature.algorithm {
259 return Err(AcdpError::InvalidSignature(format!(
260 "signature.algorithm '{}' does not match the did:key multicodec \
261 (key implies '{}')",
262 signature.algorithm,
263 material.algorithm()
264 )));
265 }
266
267 match material {
268 acdp_did::key::DidKeyMaterial::Ed25519(pub_bytes) => {
269 verify_ed25519(&pub_bytes, &signature.value, content_hash.as_str())
270 }
271 acdp_did::key::DidKeyMaterial::EcdsaP256(sec1_compressed) => {
272 verify_ecdsa_p256(&sec1_compressed, &signature.value, content_hash.as_str())
273 }
274 }
275}
276
277pub fn verify_body_offline(body: &Body) -> Result<(), AcdpError> {
291 acdp_validation::validate_body(body)?;
292
293 if !body.agent_id.as_str().starts_with("did:key:") {
294 return Err(AcdpError::KeyResolution(format!(
295 "verify_body_offline supports did:key producers only; '{}' requires \
296 the resolver-backed Verifier (client feature)",
297 body.agent_id
298 )));
299 }
300
301 let body_val = serde_json::to_value(body)?;
302 verify_content_hash(&body_val, &body.content_hash)?;
303
304 let did_part = body
305 .signature
306 .key_id
307 .split_once('#')
308 .map(|(d, _)| d)
309 .unwrap_or(body.signature.key_id.as_str());
310 if did_part != body.agent_id.as_str() {
311 return Err(AcdpError::KeyNotAuthorized(format!(
312 "key_id DID '{did_part}' ≠ agent_id '{}'",
313 body.agent_id
314 )));
315 }
316
317 verify_did_key_envelope(&body.signature, &body.content_hash)
318}
319
320pub fn verify_publish_request_signature_offline(req: &PublishRequest) -> Result<(), AcdpError> {
326 let key_id = req.signature.key_id.as_str();
327 let did_part = key_id.split_once('#').map(|(d, _)| d).unwrap_or(key_id);
328 if did_part != req.agent_id.as_str() {
329 return Err(AcdpError::KeyNotAuthorized(format!(
330 "key_id DID '{did_part}' ≠ agent_id '{}'",
331 req.agent_id
332 )));
333 }
334 if !did_part.starts_with("did:key:") {
335 return Err(AcdpError::KeyResolution(format!(
336 "offline verification supports did:key only; got '{did_part}'"
337 )));
338 }
339 verify_did_key_envelope(&req.signature, &req.content_hash)
340}
341
342#[cfg(feature = "client")]
354pub async fn verify_body_signature_historical(
355 body: &Body,
356 resolver: &WebResolver,
357) -> Result<(), AcdpError> {
358 let key_id = &body.signature.key_id;
359 let (did_part, fragment) = key_id.split_once('#').ok_or_else(|| {
360 AcdpError::KeyResolution(format!("signature.key_id '{key_id}' has no '#fragment'"))
361 })?;
362 if did_part != body.agent_id.as_str() {
363 return Err(AcdpError::KeyNotAuthorized(format!(
364 "key_id DID '{did_part}' ≠ agent_id '{}'",
365 body.agent_id
366 )));
367 }
368 if !did_part.starts_with("did:web:") {
369 return Err(AcdpError::KeyResolution(format!(
370 "historical-key verification applies to did:web only; got '{did_part}'"
371 )));
372 }
373 let doc = resolver.resolve(did_part).await?;
374 let method = doc.find_by_fragment(fragment).ok_or_else(|| {
379 AcdpError::KeyResolution(format!(
380 "no verification method with fragment '#{fragment}' — the key was \
381 removed from the DID document, not just rotated out of assertionMethod"
382 ))
383 })?;
384 if let Some(declared) = method.declared_algorithm() {
385 if declared != body.signature.algorithm {
386 return Err(AcdpError::InvalidSignature(format!(
387 "signature.algorithm '{}' does not match verification method type \
388 (resolved key declares '{declared}')",
389 body.signature.algorithm
390 )));
391 }
392 }
393 match body.signature.algorithm.as_str() {
394 "ed25519" => verify_ed25519(
395 &method.ed25519_public_key_bytes()?,
396 &body.signature.value,
397 body.content_hash.as_str(),
398 ),
399 "ecdsa-p256" => verify_ecdsa_p256(
400 &method.ecdsa_p256_public_key_sec1()?,
401 &body.signature.value,
402 body.content_hash.as_str(),
403 ),
404 other => Err(AcdpError::UnsupportedAlgorithm(format!(
405 "verifier does not support signature algorithm '{other}'"
406 ))),
407 }
408}