#![allow(clippy::items_after_test_module)]
use core::convert::TryInto;
use ed25519_compact::{KeyPair, PublicKey, Seed, Signature};
use heapless::String as HString;
use heapless::Vec as HVec;
use sha2::{Digest, Sha256};
pub const STRING_CAP: usize = 256;
pub const PAYLOAD_CAP: usize = 1024;
pub const SIGNATURE_CAP: usize = 64;
#[derive(Clone, Debug)]
pub struct Packet {
pub packet_version: HString<8>,
pub packet_id: HString<STRING_CAP>,
pub source: HString<STRING_CAP>,
pub destination: HString<STRING_CAP>,
pub priority: HString<8>,
pub emergency: bool,
pub created_at: HString<STRING_CAP>,
pub expires_at: Option<HString<STRING_CAP>>,
pub signer: HString<STRING_CAP>,
pub algorithm: HString<16>,
pub payload: HVec<u8, PAYLOAD_CAP>,
pub signature: HVec<u8, SIGNATURE_CAP>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifyError {
UnsupportedVersion,
SignerMismatch,
P0NotEmergency,
Expired,
SignatureMalformed,
PublicKeyMalformed,
SignatureInvalid,
FieldTooLarge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignError {
FieldTooLarge,
PayloadTooLarge,
P0NotEmergency,
}
#[allow(clippy::too_many_arguments)]
pub fn sign_packet(
payload: &[u8],
private_key: &Seed,
signer: &str,
packet_id: &str,
source: &str,
destination: &str,
priority: &str,
expires_at: Option<&str>,
) -> Result<Packet, SignError> {
if priority == "P0" {
return Err(SignError::P0NotEmergency);
}
let mut payload_vec: HVec<u8, PAYLOAD_CAP> = HVec::new();
payload_vec
.extend_from_slice(payload)
.map_err(|_| SignError::PayloadTooLarge)?;
let mut packet = Packet {
packet_version: hstring("1").map_err(|_| SignError::FieldTooLarge)?,
packet_id: hstring(packet_id).map_err(|_| SignError::FieldTooLarge)?,
source: hstring(source).map_err(|_| SignError::FieldTooLarge)?,
destination: hstring(destination).map_err(|_| SignError::FieldTooLarge)?,
priority: hstring(priority).map_err(|_| SignError::FieldTooLarge)?,
emergency: false,
created_at: HString::new(),
expires_at: match expires_at {
Some(e) => Some(hstring(e).map_err(|_| SignError::FieldTooLarge)?),
None => None,
},
signer: hstring(signer).map_err(|_| SignError::FieldTooLarge)?,
algorithm: hstring("ed25519").map_err(|_| SignError::FieldTooLarge)?,
payload: payload_vec,
signature: HVec::new(),
};
let digest = packet_signing_bytes(&packet);
let kp = KeyPair::from_seed(*private_key);
let sig: Signature = kp.sk.sign(digest, None);
let sig_bytes = sig.as_ref();
packet
.signature
.extend_from_slice(sig_bytes)
.expect("signature fits in 64-byte buffer");
Ok(packet)
}
pub fn verify_packet(packet: &Packet, public_key: &[u8; 32], now: &str) -> Result<(), VerifyError> {
if packet.packet_version.as_str() != "1" {
return Err(VerifyError::UnsupportedVersion);
}
if packet.signer.as_str() != packet.source.as_str() {
return Err(VerifyError::SignerMismatch);
}
if packet.priority.as_str() == "P0" && !packet.emergency {
return Err(VerifyError::P0NotEmergency);
}
if let Some(exp) = packet.expires_at.as_ref() {
if exp.as_str() < now {
return Err(VerifyError::Expired);
}
}
let digest = packet_signing_bytes(packet);
let sig_bytes: &[u8; 64] = packet
.signature
.as_slice()
.try_into()
.map_err(|_| VerifyError::SignatureMalformed)?;
let sig = Signature::from_slice(sig_bytes).map_err(|_| VerifyError::SignatureMalformed)?;
let pk = PublicKey::from_slice(public_key).map_err(|_| VerifyError::PublicKeyMalformed)?;
pk.verify(digest, &sig)
.map_err(|_| VerifyError::SignatureInvalid)?;
Ok(())
}
pub fn packet_signing_bytes(p: &Packet) -> [u8; 32] {
let mut h = Sha256::new();
write_field(&mut h, p.packet_version.as_bytes());
write_field(&mut h, p.packet_id.as_bytes());
write_field(&mut h, p.source.as_bytes());
write_field(&mut h, p.destination.as_bytes());
write_field(&mut h, p.priority.as_bytes());
write_field(&mut h, &[p.emergency as u8]);
write_field(&mut h, p.created_at.as_bytes());
match &p.expires_at {
Some(e) => {
h.update([1u8]);
write_field(&mut h, e.as_bytes());
}
None => {
h.update([0u8]);
}
}
write_field(&mut h, p.signer.as_bytes());
write_field(&mut h, p.algorithm.as_bytes());
write_field(&mut h, p.payload.as_slice());
let out = h.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&out);
bytes
}
fn write_field(h: &mut Sha256, bytes: &[u8]) {
let len = bytes.len() as u32;
h.update(len.to_be_bytes());
h.update(bytes);
}
fn hstring<const N: usize>(s: &str) -> Result<HString<N>, ()> {
let mut hs: HString<N> = HString::new();
hs.push_str(s).map_err(|_| ())?;
Ok(hs)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed_seed() -> Seed {
Seed::from_slice(&[7u8; 32]).expect("seed")
}
#[test]
fn sign_and_verify_round_trip() {
let seed = fixed_seed();
let kp = KeyPair::from_seed(seed);
let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
let signer = "tf:actor:agent:example.com/sensor-1";
let packet = sign_packet(
b"hello",
&seed,
signer,
"pkt-001",
signer,
"tf:actor:service:example.com/ingest",
"P3",
Some("2099-01-01T00:00:00Z"),
)
.expect("sign");
verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z").expect("verify ok");
}
#[test]
fn verify_rejects_tampered_payload() {
let seed = fixed_seed();
let kp = KeyPair::from_seed(seed);
let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
let signer = "tf:actor:agent:example.com/sensor-1";
let mut packet = sign_packet(
b"original",
&seed,
signer,
"pkt-002",
signer,
"tf:actor:service:example.com/ingest",
"P3",
None,
)
.expect("sign");
packet.payload[0] ^= 0x01;
let r = verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z");
assert_eq!(r, Err(VerifyError::SignatureInvalid));
}
#[test]
fn verify_rejects_expired() {
let seed = fixed_seed();
let kp = KeyPair::from_seed(seed);
let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
let signer = "tf:actor:agent:example.com/x";
let packet = sign_packet(
b"x",
&seed,
signer,
"pkt-003",
signer,
"tf:actor:service:example.com/y",
"P3",
Some("2026-04-24T00:00:00Z"),
)
.expect("sign");
let r = verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z");
assert_eq!(r, Err(VerifyError::Expired));
}
#[test]
fn verify_rejects_signer_mismatch() {
let seed = fixed_seed();
let kp = KeyPair::from_seed(seed);
let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
let signer = "tf:actor:agent:example.com/a";
let mut packet = sign_packet(
b"x",
&seed,
signer,
"pkt-004",
signer,
"tf:actor:service:example.com/b",
"P3",
None,
)
.expect("sign");
packet.source = hstring("tf:actor:agent:example.com/other").unwrap();
let r = verify_packet(&packet, &pk_bytes, "2026-04-25T00:00:00Z");
assert_eq!(r, Err(VerifyError::SignerMismatch));
}
}
#[doc(hidden)]
pub use ed25519_compact::SecretKey as Ed25519SecretKey;