1use std::time::SystemTime;
4
5use hmac::{Hmac, KeyInit, Mac};
6use serde::{Deserialize, Serialize};
7use sha2::Sha256;
8use thiserror::Error;
9use uuid::Uuid;
10
11type HmacSha256 = Hmac<Sha256>;
12
13pub type AgentId = String;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct DealEnvelope {
20 pub id: Uuid,
21 pub buyer: AgentId,
22 pub seller: AgentId,
23 pub terms: serde_json::Value,
24 pub signed_at: SystemTime,
25 pub prev_hash: [u8; 32],
26 pub hmac: [u8; 32],
27}
28
29#[derive(Debug, Error, PartialEq)]
30pub enum EnvelopeError {
31 #[error("HMAC key must be at least 32 bytes (got {0})")]
32 KeyTooShort(usize),
33 #[error("envelope HMAC verification failed at id {0}")]
34 HmacMismatch(Uuid),
35}
36
37impl DealEnvelope {
38 pub fn sign(
41 buyer: impl Into<String>,
42 seller: impl Into<String>,
43 terms: serde_json::Value,
44 prev_hash: [u8; 32],
45 signed_at: SystemTime,
46 key: &[u8],
47 ) -> Result<Self, EnvelopeError> {
48 if key.len() < 32 {
49 return Err(EnvelopeError::KeyTooShort(key.len()));
50 }
51 let id = Uuid::now_v7();
52 let buyer: String = buyer.into();
53 let seller: String = seller.into();
54 let canonical = canonical_bytes(&id, &buyer, &seller, &terms, signed_at, &prev_hash);
55 let mut mac =
56 <HmacSha256 as KeyInit>::new_from_slice(key).expect("KeyInit accepts >=32 bytes");
57 mac.update(&canonical);
58 let hmac: [u8; 32] = mac.finalize().into_bytes().into();
59 Ok(Self {
60 id,
61 buyer,
62 seller,
63 terms,
64 signed_at,
65 prev_hash,
66 hmac,
67 })
68 }
69
70 pub fn verify_hmac(&self, key: &[u8]) -> Result<(), EnvelopeError> {
72 if key.len() < 32 {
73 return Err(EnvelopeError::KeyTooShort(key.len()));
74 }
75 let canonical = canonical_bytes(
76 &self.id,
77 &self.buyer,
78 &self.seller,
79 &self.terms,
80 self.signed_at,
81 &self.prev_hash,
82 );
83 let mut mac =
84 <HmacSha256 as KeyInit>::new_from_slice(key).expect("KeyInit accepts >=32 bytes");
85 mac.update(&canonical);
86 mac.verify_slice(&self.hmac)
87 .map_err(|_| EnvelopeError::HmacMismatch(self.id))
88 }
89
90 pub fn next_prev_hash(&self) -> [u8; 32] {
94 let canonical = canonical_bytes(
95 &self.id,
96 &self.buyer,
97 &self.seller,
98 &self.terms,
99 self.signed_at,
100 &self.prev_hash,
101 );
102 use sha2::Digest;
103 let mut h = Sha256::new();
104 h.update(canonical);
105 h.update(self.hmac);
106 h.finalize().into()
107 }
108}
109
110fn canonical_bytes(
111 id: &Uuid,
112 buyer: &str,
113 seller: &str,
114 terms: &serde_json::Value,
115 signed_at: SystemTime,
116 prev_hash: &[u8; 32],
117) -> Vec<u8> {
118 let mut out = Vec::with_capacity(256);
119 out.extend_from_slice(id.as_bytes());
120 out.push(b'|');
121 out.extend_from_slice(buyer.as_bytes());
122 out.push(b'|');
123 out.extend_from_slice(seller.as_bytes());
124 out.push(b'|');
125 let canon = serde_json::to_string(terms).unwrap_or_default();
130 out.extend_from_slice(canon.as_bytes());
131 out.push(b'|');
132 let dt: chrono::DateTime<chrono::Utc> = signed_at.into();
133 out.extend_from_slice(dt.to_rfc3339().as_bytes());
134 out.push(b'|');
135 out.extend_from_slice(prev_hash);
136 out
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn key() -> [u8; 32] {
144 [42u8; 32]
145 }
146
147 #[test]
148 fn sign_verify_round_trip() {
149 let e = DealEnvelope::sign(
150 "buyer-a",
151 "seller-b",
152 serde_json::json!({"price": "10 USDC"}),
153 [0u8; 32],
154 SystemTime::UNIX_EPOCH,
155 &key(),
156 )
157 .unwrap();
158 assert!(e.verify_hmac(&key()).is_ok());
159 }
160
161 #[test]
162 fn flipped_byte_breaks_verify() {
163 let mut e = DealEnvelope::sign(
164 "buyer-a",
165 "seller-b",
166 serde_json::json!({"price": "10 USDC"}),
167 [0u8; 32],
168 SystemTime::UNIX_EPOCH,
169 &key(),
170 )
171 .unwrap();
172 e.terms = serde_json::json!({"price": "1000 USDC"});
173 let err = e.verify_hmac(&key()).unwrap_err();
174 assert!(matches!(err, EnvelopeError::HmacMismatch(_)));
175 }
176
177 #[test]
178 fn short_key_is_rejected() {
179 let err = DealEnvelope::sign(
180 "a",
181 "b",
182 serde_json::json!({}),
183 [0u8; 32],
184 SystemTime::UNIX_EPOCH,
185 &[1u8; 16],
186 )
187 .unwrap_err();
188 assert_eq!(err, EnvelopeError::KeyTooShort(16));
189 }
190
191 #[test]
192 fn next_prev_hash_is_deterministic() {
193 let e = DealEnvelope::sign(
194 "a",
195 "b",
196 serde_json::json!({}),
197 [0u8; 32],
198 SystemTime::UNIX_EPOCH,
199 &key(),
200 )
201 .unwrap();
202 assert_eq!(e.next_prev_hash(), e.next_prev_hash());
203 }
204}