Skip to main content

a1/
negotiate.rs

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// ── Message types ─────────────────────────────────────────────────────────────
15
16/// An agent's request for a delegated capability sub-cert.
17///
18/// Agent A sends a `CapabilityRequest` to Agent B (or B's gateway) when it
19/// needs authorization to perform a specific action that B can delegate.
20///
21/// # Signature
22///
23/// `signature` is an Ed25519 signature over
24/// `Blake3(DOMAIN || requester_did || nonce || timestamp || ttl || intent || caps...)`,
25/// proving that the requester controls the private key for `requester_pk_hex`.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CapabilityRequest {
28    /// `did:a1:` identifier of the requesting agent.
29    pub requester_did: String,
30    /// Hex-encoded Ed25519 public key of the requesting agent.
31    pub requester_pk_hex: String,
32    /// Capabilities the requesting agent needs.
33    pub requested_capabilities: Vec<String>,
34    /// Name of the specific intent to be authorized.
35    pub intent_name: String,
36    /// Requested delegation lifetime in seconds.
37    pub ttl_secs: u64,
38    /// 16-byte anti-replay nonce (hex).
39    pub nonce: String,
40    /// Unix timestamp when this request was created.
41    pub timestamp_unix: u64,
42    /// Ed25519 signature over the canonical request bytes (hex).
43    pub signature: String,
44}
45
46impl CapabilityRequest {
47    /// Build and sign a capability request.
48    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    /// Verify the requester's signature and return the verifying key.
83    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    /// Verify and enforce that the request is not stale.
105    ///
106    /// A request is stale if `|now - timestamp| > max_age_secs`.
107    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/// A delegation offer from the responding agent (or gateway).
121///
122/// Contains a signed `DelegationCert` scoped to the requested capabilities
123/// and a new nonce that the requester must echo in the `DelegationAcceptance`.
124///
125/// # Signature
126///
127/// `signature` is an Ed25519 signature over
128/// `Blake3(DOMAIN || offerer_did || request_nonce || offer_nonce || timestamp || cert_fingerprint)`.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct DelegationOffer {
131    /// `did:a1:` identifier of the offering party.
132    pub offerer_did: String,
133    /// Echoed nonce from the `CapabilityRequest` (prevents substitution).
134    pub request_nonce: String,
135    /// The issued delegation certificate.
136    pub cert: DelegationCert,
137    /// New nonce for the acceptance handshake.
138    pub offer_nonce: String,
139    /// Unix timestamp when this offer was created.
140    pub timestamp_unix: u64,
141    /// Offer expiry in seconds from `timestamp_unix`.
142    pub offer_ttl_secs: u64,
143    /// Ed25519 signature over the canonical offer bytes (hex).
144    pub signature: String,
145}
146
147impl DelegationOffer {
148    /// Build and sign a delegation offer in response to a verified request.
149    ///
150    /// `offerer` signs the cert and the offer metadata. The cert's
151    /// `delegator_pk` will be `offerer.verifying_key()`.
152    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    /// Verify the offerer's signature.
186    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/// The requester's final acceptance of a delegation offer.
215///
216/// Echoes the `offer_nonce` to confirm receipt and proves the requester
217/// controls their private key. After this message, the cert in the offer
218/// is live and usable.
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct DelegationAcceptance {
221    /// `did:a1:` identifier of the accepting agent.
222    pub acceptor_did: String,
223    /// Echoed nonce from the `DelegationOffer`.
224    pub offer_nonce: String,
225    /// Unix timestamp when this acceptance was created.
226    pub timestamp_unix: u64,
227    /// Ed25519 signature over the canonical acceptance bytes (hex).
228    pub signature: String,
229}
230
231impl DelegationAcceptance {
232    /// Build and sign an acceptance for a delegation offer.
233    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    /// Verify the acceptor's signature.
254    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/// The result of a complete three-way negotiation handshake.
274///
275/// Returned by the gateway's `/v1/negotiate` endpoint after the requester
276/// calls `A1Client.negotiateDelegation()`. The cert is ready to push onto
277/// a `DyoloChain`.
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct NegotiationResult {
280    /// The delegation certificate, ready to use.
281    pub cert: DelegationCert,
282    /// The offer that produced this cert.
283    pub offer: DelegationOffer,
284    /// Certificate fingerprint (hex) for logging.
285    pub fingerprint_hex: String,
286    /// `did:a1:` of the delegating party.
287    pub offerer_did: String,
288    /// `did:a1:` of the receiving agent.
289    pub requester_did: String,
290}
291
292// ── Canonical byte helpers ────────────────────────────────────────────────────
293
294fn 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(&timestamp.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(&timestamp.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(&timestamp.to_le_bytes());
341    h.finalize().as_bytes().to_vec()
342}
343
344// ── Parse helpers ─────────────────────────────────────────────────────────────
345
346fn 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// ── Tests ─────────────────────────────────────────────────────────────────────
374
375#[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}