1use core::convert::TryInto;
14
15use ed25519_compact::{PublicKey, Signature};
16use heapless::String as HString;
17use heapless::Vec as HVec;
18use sha2::{Digest, Sha256};
19
20pub const STRING_CAP: usize = 256;
22pub const KINDS_CAP: usize = 16;
24
25#[derive(Clone, Debug)]
27pub struct SignatureEnvelope {
28 pub algorithm: HString<16>,
29 pub signer: HString<STRING_CAP>,
30 pub signature: HVec<u8, 64>,
31}
32
33#[derive(Clone, Debug)]
35pub struct RelayAuthority {
36 pub relay_authority_version: HString<8>,
37 pub relay: HString<STRING_CAP>,
38 pub trust_domain: HString<STRING_CAP>,
39 pub kinds: HVec<HString<32>, KINDS_CAP>,
40 pub max_hop_count: Option<u32>,
41 pub rate_limit_per_minute: Option<u32>,
42 pub valid_from: HString<STRING_CAP>,
43 pub valid_until: Option<HString<STRING_CAP>>,
44 pub issuer: HString<STRING_CAP>,
45 pub signature: SignatureEnvelope,
46}
47
48pub fn relay_authority_signing_bytes(a: &RelayAuthority) -> [u8; 32] {
51 let mut h = Sha256::new();
52 write_field(&mut h, a.relay_authority_version.as_bytes());
53 write_field(&mut h, a.relay.as_bytes());
54 write_field(&mut h, a.trust_domain.as_bytes());
55 let count = a.kinds.len() as u32;
56 h.update(count.to_be_bytes());
57 for k in a.kinds.iter() {
58 write_field(&mut h, k.as_bytes());
59 }
60 write_optional_u32(&mut h, a.max_hop_count);
61 write_optional_u32(&mut h, a.rate_limit_per_minute);
62 write_field(&mut h, a.valid_from.as_bytes());
63 write_optional_str(&mut h, a.valid_until.as_ref().map(|s| s.as_str()));
64 write_field(&mut h, a.issuer.as_bytes());
65 let out = h.finalize();
66 let mut bytes = [0u8; 32];
67 bytes.copy_from_slice(&out);
68 bytes
69}
70
71pub fn verify_relay_authority(authority: &RelayAuthority, issuer_pub: &[u8; 32]) -> bool {
76 if authority.relay_authority_version.as_str() != "1" {
77 return false;
78 }
79 if authority.signature.algorithm.as_str() != "ed25519" {
80 return false;
81 }
82 if authority.signature.signer.as_str() != authority.issuer.as_str() {
83 return false;
84 }
85 let digest = relay_authority_signing_bytes(authority);
86 let sig_bytes: &[u8; 64] = match authority.signature.signature.as_slice().try_into() {
87 Ok(s) => s,
88 Err(_) => return false,
89 };
90 let sig = match Signature::from_slice(sig_bytes) {
91 Ok(s) => s,
92 Err(_) => return false,
93 };
94 let pk = match PublicKey::from_slice(issuer_pub) {
95 Ok(p) => p,
96 Err(_) => return false,
97 };
98 pk.verify(digest, &sig).is_ok()
99}
100
101fn write_field(h: &mut Sha256, bytes: &[u8]) {
102 let len = bytes.len() as u32;
103 h.update(len.to_be_bytes());
104 h.update(bytes);
105}
106
107fn write_optional_u32(h: &mut Sha256, v: Option<u32>) {
108 match v {
109 Some(n) => {
110 h.update([1u8]);
111 h.update(n.to_be_bytes());
112 }
113 None => h.update([0u8]),
114 }
115}
116
117fn write_optional_str(h: &mut Sha256, v: Option<&str>) {
118 match v {
119 Some(s) => {
120 h.update([1u8]);
121 write_field(h, s.as_bytes());
122 }
123 None => {
124 h.update([0u8]);
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use ed25519_compact::{KeyPair, Seed};
133
134 fn hstring<const N: usize>(s: &str) -> HString<N> {
135 let mut hs = HString::new();
136 hs.push_str(s).unwrap();
137 hs
138 }
139
140 fn build_and_sign() -> (RelayAuthority, [u8; 32]) {
141 let seed = Seed::from_slice(&[3u8; 32]).unwrap();
142 let kp = KeyPair::from_seed(seed);
143 let pk_bytes: [u8; 32] = kp.pk.as_ref().try_into().unwrap();
144
145 let mut kinds: HVec<HString<32>, KINDS_CAP> = HVec::new();
146 kinds.push(hstring("packet")).unwrap();
147 kinds.push(hstring("relay-frame")).unwrap();
148
149 let mut auth = RelayAuthority {
150 relay_authority_version: hstring("1"),
151 relay: hstring("tf:actor:relay:example.com/edge-01"),
152 trust_domain: hstring("example.com"),
153 kinds,
154 max_hop_count: Some(4),
155 rate_limit_per_minute: Some(60),
156 valid_from: hstring("2026-01-01T00:00:00Z"),
157 valid_until: Some(hstring("2099-01-01T00:00:00Z")),
158 issuer: hstring("tf:actor:authority:example.com/root"),
159 signature: SignatureEnvelope {
160 algorithm: hstring("ed25519"),
161 signer: hstring("tf:actor:authority:example.com/root"),
162 signature: HVec::new(),
163 },
164 };
165 let digest = relay_authority_signing_bytes(&auth);
166 let sig = kp.sk.sign(digest, None);
167 auth.signature
168 .signature
169 .extend_from_slice(sig.as_ref())
170 .unwrap();
171 (auth, pk_bytes)
172 }
173
174 #[test]
175 fn verify_relay_authority_happy_path() {
176 let (auth, pk) = build_and_sign();
177 assert!(verify_relay_authority(&auth, &pk));
178 }
179
180 #[test]
181 fn verify_relay_authority_rejects_tamper() {
182 let (mut auth, pk) = build_and_sign();
183 auth.relay = hstring("tf:actor:relay:evil.example/imposter");
184 assert!(!verify_relay_authority(&auth, &pk));
185 }
186
187 #[test]
188 fn verify_relay_authority_rejects_signer_mismatch() {
189 let (mut auth, pk) = build_and_sign();
190 auth.signature.signer = hstring("tf:actor:other:example.com/a");
191 assert!(!verify_relay_authority(&auth, &pk));
192 }
193}