1use blake3::Hasher;
2use ed25519_dalek::VerifyingKey;
3use serde::{Deserialize, Serialize};
4
5use crate::cert::DelegationCert;
6use crate::error::A1Error;
7use crate::identity::Signer;
8use crate::registry::fresh_nonce;
9
10const DOMAIN_NEG_REQUEST: &str = "a1::dyolo::negotiate::request::v2.8.0";
11const DOMAIN_NEG_OFFER: &str = "a1::dyolo::negotiate::offer::v2.8.0";
12const DOMAIN_NEG_ACCEPT: &str = "a1::dyolo::negotiate::accept::v2.8.0";
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CapabilityRequest {
28 pub requester_did: String,
30 pub requester_pk_hex: String,
32 pub requested_capabilities: Vec<String>,
34 pub intent_name: String,
36 pub ttl_secs: u64,
38 pub nonce: String,
40 pub timestamp_unix: u64,
42 pub signature: String,
44}
45
46impl CapabilityRequest {
47 pub fn build(
49 requester: &dyn Signer,
50 requested_capabilities: Vec<String>,
51 intent_name: impl Into<String>,
52 ttl_secs: u64,
53 timestamp_unix: u64,
54 ) -> Self {
55 let vk = requester.verifying_key();
56 let requester_did = format!("did:a1:{}", hex::encode(vk.as_bytes()));
57 let nonce = fresh_nonce();
58 let intent_str: String = intent_name.into();
59
60 let msg = request_signable_bytes(
61 &requester_did,
62 &nonce,
63 timestamp_unix,
64 ttl_secs,
65 &intent_str,
66 &requested_capabilities,
67 );
68 let sig = requester.sign_message(&msg);
69
70 Self {
71 requester_did,
72 requester_pk_hex: hex::encode(vk.as_bytes()),
73 requested_capabilities,
74 intent_name: intent_str,
75 ttl_secs,
76 nonce: hex::encode(nonce),
77 timestamp_unix,
78 signature: hex::encode(sig.to_bytes()),
79 }
80 }
81
82 pub fn verify_signature(&self) -> Result<VerifyingKey, A1Error> {
84 let vk = parse_pk_hex(&self.requester_pk_hex)?;
85
86 let nonce = parse_nonce_hex(&self.nonce)?;
87 let msg = request_signable_bytes(
88 &self.requester_did,
89 &nonce,
90 self.timestamp_unix,
91 self.ttl_secs,
92 &self.intent_name,
93 &self.requested_capabilities,
94 );
95 let sig = parse_sig_hex(&self.signature)?;
96
97 use ed25519_dalek::Verifier;
98 vk.verify(&msg, &sig)
99 .map_err(|_| A1Error::InvalidSignature(0))?;
100
101 Ok(vk)
102 }
103
104 pub fn verify_freshness(&self, now_unix: u64, max_age_secs: u64) -> Result<(), A1Error> {
108 let age = now_unix.saturating_sub(self.timestamp_unix);
109 if age > max_age_secs {
110 return Err(A1Error::Expired(
111 0,
112 self.timestamp_unix + max_age_secs,
113 now_unix,
114 ));
115 }
116 Ok(())
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct DelegationOffer {
131 pub offerer_did: String,
133 pub request_nonce: String,
135 pub cert: DelegationCert,
137 pub offer_nonce: String,
139 pub timestamp_unix: u64,
141 pub offer_ttl_secs: u64,
143 pub signature: String,
145}
146
147impl DelegationOffer {
148 pub fn build(
153 offerer: &dyn Signer,
154 request: &CapabilityRequest,
155 cert: DelegationCert,
156 timestamp_unix: u64,
157 offer_ttl_secs: u64,
158 ) -> Result<Self, A1Error> {
159 let vk = offerer.verifying_key();
160 let offerer_did = format!("did:a1:{}", hex::encode(vk.as_bytes()));
161 let offer_nonce = fresh_nonce();
162 let cert_fp = cert.fingerprint();
163
164 let request_nonce = parse_nonce_hex(&request.nonce)?;
165 let msg = offer_signable_bytes(
166 &offerer_did,
167 &request_nonce,
168 &offer_nonce,
169 timestamp_unix,
170 &cert_fp,
171 );
172 let sig = offerer.sign_message(&msg);
173
174 Ok(Self {
175 offerer_did,
176 request_nonce: request.nonce.clone(),
177 cert,
178 offer_nonce: hex::encode(offer_nonce),
179 timestamp_unix,
180 offer_ttl_secs,
181 signature: hex::encode(sig.to_bytes()),
182 })
183 }
184
185 pub fn verify_signature(&self) -> Result<VerifyingKey, A1Error> {
187 let pk_hex = self
188 .offerer_did
189 .strip_prefix("did:a1:")
190 .ok_or_else(|| A1Error::WireFormatError("invalid offerer DID".into()))?;
191 let vk = parse_pk_hex(pk_hex)?;
192
193 let request_nonce = parse_nonce_hex(&self.request_nonce)?;
194 let offer_nonce = parse_nonce_hex(&self.offer_nonce)?;
195 let cert_fp = self.cert.fingerprint();
196
197 let msg = offer_signable_bytes(
198 &self.offerer_did,
199 &request_nonce,
200 &offer_nonce,
201 self.timestamp_unix,
202 &cert_fp,
203 );
204 let sig = parse_sig_hex(&self.signature)?;
205
206 use ed25519_dalek::Verifier;
207 vk.verify(&msg, &sig)
208 .map_err(|_| A1Error::InvalidSignature(0))?;
209
210 Ok(vk)
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct DelegationAcceptance {
221 pub acceptor_did: String,
223 pub offer_nonce: String,
225 pub timestamp_unix: u64,
227 pub signature: String,
229}
230
231impl DelegationAcceptance {
232 pub fn build(
234 acceptor: &dyn Signer,
235 offer: &DelegationOffer,
236 timestamp_unix: u64,
237 ) -> Result<Self, A1Error> {
238 let vk = acceptor.verifying_key();
239 let acceptor_did = format!("did:a1:{}", hex::encode(vk.as_bytes()));
240
241 let offer_nonce = parse_nonce_hex(&offer.offer_nonce)?;
242 let msg = accept_signable_bytes(&acceptor_did, &offer_nonce, timestamp_unix);
243 let sig = acceptor.sign_message(&msg);
244
245 Ok(Self {
246 acceptor_did,
247 offer_nonce: offer.offer_nonce.clone(),
248 timestamp_unix,
249 signature: hex::encode(sig.to_bytes()),
250 })
251 }
252
253 pub fn verify_signature(&self) -> Result<VerifyingKey, A1Error> {
255 let pk_hex = self
256 .acceptor_did
257 .strip_prefix("did:a1:")
258 .ok_or_else(|| A1Error::WireFormatError("invalid acceptor DID".into()))?;
259 let vk = parse_pk_hex(pk_hex)?;
260
261 let offer_nonce = parse_nonce_hex(&self.offer_nonce)?;
262 let msg = accept_signable_bytes(&self.acceptor_did, &offer_nonce, self.timestamp_unix);
263 let sig = parse_sig_hex(&self.signature)?;
264
265 use ed25519_dalek::Verifier;
266 vk.verify(&msg, &sig)
267 .map_err(|_| A1Error::InvalidSignature(0))?;
268
269 Ok(vk)
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct NegotiationResult {
280 pub cert: DelegationCert,
282 pub offer: DelegationOffer,
284 pub fingerprint_hex: String,
286 pub offerer_did: String,
288 pub requester_did: String,
290}
291
292fn request_signable_bytes(
295 requester_did: &str,
296 nonce: &[u8; 16],
297 timestamp: u64,
298 ttl: u64,
299 intent_name: &str,
300 caps: &[String],
301) -> Vec<u8> {
302 let mut h = Hasher::new_derive_key(DOMAIN_NEG_REQUEST);
303 h.update(&(requester_did.len() as u64).to_le_bytes());
304 h.update(requester_did.as_bytes());
305 h.update(nonce);
306 h.update(×tamp.to_le_bytes());
307 h.update(&ttl.to_le_bytes());
308 h.update(&(intent_name.len() as u64).to_le_bytes());
309 h.update(intent_name.as_bytes());
310 h.update(&(caps.len() as u64).to_le_bytes());
311 for cap in caps {
312 h.update(&(cap.len() as u64).to_le_bytes());
313 h.update(cap.as_bytes());
314 }
315 h.finalize().as_bytes().to_vec()
316}
317
318fn offer_signable_bytes(
319 offerer_did: &str,
320 request_nonce: &[u8; 16],
321 offer_nonce: &[u8; 16],
322 timestamp: u64,
323 cert_fp: &[u8; 32],
324) -> Vec<u8> {
325 let mut h = Hasher::new_derive_key(DOMAIN_NEG_OFFER);
326 h.update(&(offerer_did.len() as u64).to_le_bytes());
327 h.update(offerer_did.as_bytes());
328 h.update(request_nonce);
329 h.update(offer_nonce);
330 h.update(×tamp.to_le_bytes());
331 h.update(cert_fp);
332 h.finalize().as_bytes().to_vec()
333}
334
335fn accept_signable_bytes(acceptor_did: &str, offer_nonce: &[u8; 16], timestamp: u64) -> Vec<u8> {
336 let mut h = Hasher::new_derive_key(DOMAIN_NEG_ACCEPT);
337 h.update(&(acceptor_did.len() as u64).to_le_bytes());
338 h.update(acceptor_did.as_bytes());
339 h.update(offer_nonce);
340 h.update(×tamp.to_le_bytes());
341 h.finalize().as_bytes().to_vec()
342}
343
344fn parse_pk_hex(hex_str: &str) -> Result<VerifyingKey, A1Error> {
347 let bytes = hex::decode(hex_str)
348 .map_err(|_| A1Error::WireFormatError("invalid public key hex".into()))?;
349 let arr: [u8; 32] = bytes
350 .try_into()
351 .map_err(|_| A1Error::WireFormatError("public key must be 32 bytes".into()))?;
352 VerifyingKey::from_bytes(&arr)
353 .map_err(|e| A1Error::WireFormatError(format!("invalid Ed25519 key: {e}")))
354}
355
356fn parse_nonce_hex(hex_str: &str) -> Result<[u8; 16], A1Error> {
357 let bytes =
358 hex::decode(hex_str).map_err(|_| A1Error::WireFormatError("invalid nonce hex".into()))?;
359 bytes
360 .try_into()
361 .map_err(|_| A1Error::WireFormatError("nonce must be 16 bytes".into()))
362}
363
364fn parse_sig_hex(hex_str: &str) -> Result<ed25519_dalek::Signature, A1Error> {
365 let bytes = hex::decode(hex_str)
366 .map_err(|_| A1Error::WireFormatError("invalid signature hex".into()))?;
367 let arr: [u8; 64] = bytes
368 .try_into()
369 .map_err(|_| A1Error::WireFormatError("signature must be 64 bytes".into()))?;
370 Ok(ed25519_dalek::Signature::from_bytes(&arr))
371}
372
373#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::cert::CertBuilder;
379 use crate::identity::DyoloIdentity;
380 use crate::intent::Intent;
381
382 #[test]
383 fn capability_request_sign_verify() {
384 let requester = DyoloIdentity::generate();
385 let now = 1_700_000_000u64;
386 let req = CapabilityRequest::build(
387 &requester,
388 vec!["trade.equity".into(), "portfolio.read".into()],
389 "trade.equity",
390 3600,
391 now,
392 );
393 let vk = req.verify_signature().unwrap();
394 assert_eq!(vk.as_bytes(), requester.verifying_key().as_bytes());
395 }
396
397 #[test]
398 fn capability_request_tampered_fails() {
399 let requester = DyoloIdentity::generate();
400 let now = 1_700_000_000u64;
401 let mut req = CapabilityRequest::build(
402 &requester,
403 vec!["trade.equity".into()],
404 "trade.equity",
405 3600,
406 now,
407 );
408 req.requested_capabilities.push("admin.everything".into());
409 assert!(req.verify_signature().is_err());
410 }
411
412 #[test]
413 fn capability_request_freshness() {
414 let requester = DyoloIdentity::generate();
415 let now = 1_700_000_000u64;
416 let req = CapabilityRequest::build(&requester, vec!["read".into()], "read", 3600, now);
417 assert!(req.verify_freshness(now + 60, 300).is_ok());
418 assert!(req.verify_freshness(now + 400, 300).is_err());
419 }
420
421 #[test]
422 fn full_negotiation_handshake() {
423 let requester = DyoloIdentity::generate();
424 let offerer = DyoloIdentity::generate();
425 let now = 1_700_000_000u64;
426
427 let intent = Intent::new("trade.equity").unwrap().hash();
428 let cert =
429 CertBuilder::new(requester.verifying_key(), intent, now, now + 3600).sign(&offerer);
430
431 let req = CapabilityRequest::build(
432 &requester,
433 vec!["trade.equity".into()],
434 "trade.equity",
435 3600,
436 now,
437 );
438 req.verify_signature().unwrap();
439
440 let offer = DelegationOffer::build(&offerer, &req, cert, now, 120).unwrap();
441 offer.verify_signature().unwrap();
442
443 let acceptance = DelegationAcceptance::build(&requester, &offer, now + 1).unwrap();
444 acceptance.verify_signature().unwrap();
445 }
446
447 #[test]
448 fn offer_tampered_cert_fails_signature() {
449 let requester = DyoloIdentity::generate();
450 let offerer = DyoloIdentity::generate();
451 let now = 1_700_000_000u64;
452 let intent = Intent::new("read").unwrap().hash();
453
454 let cert =
455 CertBuilder::new(requester.verifying_key(), intent, now, now + 3600).sign(&offerer);
456 let req = CapabilityRequest::build(&requester, vec!["read".into()], "read", 3600, now);
457 let mut offer = DelegationOffer::build(&offerer, &req, cert, now, 120).unwrap();
458 offer.offer_nonce = hex::encode([0u8; 16]);
459 assert!(offer.verify_signature().is_err());
460 }
461}