1#![allow(clippy::items_after_test_module)]
2use core::convert::TryInto;
17
18use ed25519_compact::{KeyPair, PublicKey, Seed, Signature};
19use heapless::String as HString;
20use heapless::Vec as HVec;
21use sha2::{Digest, Sha256};
22
23pub const STRING_CAP: usize = 256;
27
28pub const PAYLOAD_CAP: usize = 1024;
32
33pub const SIGNATURE_CAP: usize = 64;
35
36#[derive(Clone, Debug)]
41pub struct Packet {
42 pub packet_version: HString<8>,
43 pub packet_id: HString<STRING_CAP>,
44 pub source: HString<STRING_CAP>,
45 pub destination: HString<STRING_CAP>,
46 pub priority: HString<8>,
47 pub emergency: bool,
48 pub created_at: HString<STRING_CAP>,
49 pub expires_at: Option<HString<STRING_CAP>>,
50 pub signer: HString<STRING_CAP>,
51 pub algorithm: HString<16>,
52 pub payload: HVec<u8, PAYLOAD_CAP>,
53 pub signature: HVec<u8, SIGNATURE_CAP>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum VerifyError {
59 UnsupportedVersion,
61 SignerMismatch,
63 P0NotEmergency,
65 Expired,
67 SignatureMalformed,
69 PublicKeyMalformed,
71 SignatureInvalid,
73 FieldTooLarge,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum SignError {
80 FieldTooLarge,
82 PayloadTooLarge,
84 P0NotEmergency,
86}
87
88#[allow(clippy::too_many_arguments)]
93pub fn sign_packet(
94 payload: &[u8],
95 private_key: &Seed,
96 signer: &str,
97 packet_id: &str,
98 source: &str,
99 destination: &str,
100 priority: &str,
101 expires_at: Option<&str>,
102) -> Result<Packet, SignError> {
103 if priority == "P0" {
104 return Err(SignError::P0NotEmergency);
105 }
106 let mut payload_vec: HVec<u8, PAYLOAD_CAP> = HVec::new();
107 payload_vec
108 .extend_from_slice(payload)
109 .map_err(|_| SignError::PayloadTooLarge)?;
110
111 let mut packet = Packet {
112 packet_version: hstring("1").map_err(|_| SignError::FieldTooLarge)?,
113 packet_id: hstring(packet_id).map_err(|_| SignError::FieldTooLarge)?,
114 source: hstring(source).map_err(|_| SignError::FieldTooLarge)?,
115 destination: hstring(destination).map_err(|_| SignError::FieldTooLarge)?,
116 priority: hstring(priority).map_err(|_| SignError::FieldTooLarge)?,
117 emergency: false,
118 created_at: HString::new(),
125 expires_at: match expires_at {
126 Some(e) => Some(hstring(e).map_err(|_| SignError::FieldTooLarge)?),
127 None => None,
128 },
129 signer: hstring(signer).map_err(|_| SignError::FieldTooLarge)?,
130 algorithm: hstring("ed25519").map_err(|_| SignError::FieldTooLarge)?,
131 payload: payload_vec,
132 signature: HVec::new(),
133 };
134
135 let digest = packet_signing_bytes(&packet);
136 let kp = KeyPair::from_seed(*private_key);
138 let sig: Signature = kp.sk.sign(digest, None);
139 let sig_bytes = sig.as_ref();
140 packet
141 .signature
142 .extend_from_slice(sig_bytes)
143 .expect("signature fits in 64-byte buffer");
144 Ok(packet)
145}
146
147pub fn verify_packet(packet: &Packet, public_key: &[u8; 32], now: &str) -> Result<(), VerifyError> {
150 if packet.packet_version.as_str() != "1" {
151 return Err(VerifyError::UnsupportedVersion);
152 }
153 if packet.signer.as_str() != packet.source.as_str() {
154 return Err(VerifyError::SignerMismatch);
155 }
156 if packet.priority.as_str() == "P0" && !packet.emergency {
157 return Err(VerifyError::P0NotEmergency);
158 }
159 if let Some(exp) = packet.expires_at.as_ref() {
160 if exp.as_str() < now {
161 return Err(VerifyError::Expired);
162 }
163 }
164 let digest = packet_signing_bytes(packet);
165 let sig_bytes: &[u8; 64] = packet
166 .signature
167 .as_slice()
168 .try_into()
169 .map_err(|_| VerifyError::SignatureMalformed)?;
170 let sig = Signature::from_slice(sig_bytes).map_err(|_| VerifyError::SignatureMalformed)?;
171 let pk = PublicKey::from_slice(public_key).map_err(|_| VerifyError::PublicKeyMalformed)?;
172 pk.verify(digest, &sig)
173 .map_err(|_| VerifyError::SignatureInvalid)?;
174 Ok(())
175}
176
177pub fn packet_signing_bytes(p: &Packet) -> [u8; 32] {
184 let mut h = Sha256::new();
185 write_field(&mut h, p.packet_version.as_bytes());
186 write_field(&mut h, p.packet_id.as_bytes());
187 write_field(&mut h, p.source.as_bytes());
188 write_field(&mut h, p.destination.as_bytes());
189 write_field(&mut h, p.priority.as_bytes());
190 write_field(&mut h, &[p.emergency as u8]);
191 write_field(&mut h, p.created_at.as_bytes());
192 match &p.expires_at {
193 Some(e) => {
194 h.update([1u8]);
195 write_field(&mut h, e.as_bytes());
196 }
197 None => {
198 h.update([0u8]);
199 }
200 }
201 write_field(&mut h, p.signer.as_bytes());
202 write_field(&mut h, p.algorithm.as_bytes());
203 write_field(&mut h, p.payload.as_slice());
204 let out = h.finalize();
205 let mut bytes = [0u8; 32];
206 bytes.copy_from_slice(&out);
207 bytes
208}
209
210fn write_field(h: &mut Sha256, bytes: &[u8]) {
211 let len = bytes.len() as u32;
212 h.update(len.to_be_bytes());
213 h.update(bytes);
214}
215
216fn hstring<const N: usize>(s: &str) -> Result<HString<N>, ()> {
217 let mut hs: HString<N> = HString::new();
218 hs.push_str(s).map_err(|_| ())?;
219 Ok(hs)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn fixed_seed() -> Seed {
227 Seed::from_slice(&[7u8; 32]).expect("seed")
228 }
229
230 #[test]
231 fn sign_and_verify_round_trip() {
232 let seed = fixed_seed();
233 let kp = KeyPair::from_seed(seed);
234 let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
235
236 let signer = "tf:actor:agent:example.com/sensor-1";
237 let packet = sign_packet(
238 b"hello",
239 &seed,
240 signer,
241 "pkt-001",
242 signer,
243 "tf:actor:service:example.com/ingest",
244 "P3",
245 Some("2099-01-01T00:00:00Z"),
246 )
247 .expect("sign");
248
249 verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z").expect("verify ok");
250 }
251
252 #[test]
253 fn verify_rejects_tampered_payload() {
254 let seed = fixed_seed();
255 let kp = KeyPair::from_seed(seed);
256 let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
257 let signer = "tf:actor:agent:example.com/sensor-1";
258 let mut packet = sign_packet(
259 b"original",
260 &seed,
261 signer,
262 "pkt-002",
263 signer,
264 "tf:actor:service:example.com/ingest",
265 "P3",
266 None,
267 )
268 .expect("sign");
269 packet.payload[0] ^= 0x01;
271 let r = verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z");
272 assert_eq!(r, Err(VerifyError::SignatureInvalid));
273 }
274
275 #[test]
276 fn verify_rejects_expired() {
277 let seed = fixed_seed();
278 let kp = KeyPair::from_seed(seed);
279 let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
280 let signer = "tf:actor:agent:example.com/x";
281 let packet = sign_packet(
282 b"x",
283 &seed,
284 signer,
285 "pkt-003",
286 signer,
287 "tf:actor:service:example.com/y",
288 "P3",
289 Some("2026-04-24T00:00:00Z"),
290 )
291 .expect("sign");
292 let r = verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z");
293 assert_eq!(r, Err(VerifyError::Expired));
294 }
295
296 #[test]
297 fn verify_rejects_signer_mismatch() {
298 let seed = fixed_seed();
299 let kp = KeyPair::from_seed(seed);
300 let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
301 let signer = "tf:actor:agent:example.com/a";
302 let mut packet = sign_packet(
303 b"x",
304 &seed,
305 signer,
306 "pkt-004",
307 signer,
308 "tf:actor:service:example.com/b",
309 "P3",
310 None,
311 )
312 .expect("sign");
313 packet.source = hstring("tf:actor:agent:example.com/other").unwrap();
316 let r = verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z");
317 assert_eq!(r, Err(VerifyError::SignerMismatch));
318 }
319}
320
321#[doc(hidden)]
324pub use ed25519_compact::SecretKey as Ed25519SecretKey;