Skip to main content

tf_core_no_std/
relay.rs

1//! Relay-authority verification (TF-0011 §5), no_std edition.
2//!
3//! A `RelayAuthority` is a signed grant that lets a relay carry frames
4//! for a particular trust domain and packet kinds. Forwarding authority
5//! is strictly separate from action authority — even an authorised
6//! relay sees only ciphertext.
7//!
8//! The constrained variant carries the same field set as the std
9//! `tf-types::relay::RelayAuthority` but uses fixed-cap heapless
10//! containers. The signing-bytes derivation uses the same SSZ-style
11//! length-prefixed concatenation as `packet::packet_signing_bytes`.
12
13use core::convert::TryInto;
14
15use ed25519_compact::{PublicKey, Signature};
16use heapless::String as HString;
17use heapless::Vec as HVec;
18use sha2::{Digest, Sha256};
19
20/// Capacity for ID and timestamp string fields.
21pub const STRING_CAP: usize = 256;
22/// Maximum number of `kinds` strings we can hold. TF-0011 lists ~6 today.
23pub const KINDS_CAP: usize = 16;
24
25/// Compact ed25519 signature envelope.
26#[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/// A relay authority grant. Mirrors `tf-types::relay::RelayAuthority`.
34#[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
48/// Compute the 32-byte digest used to bind the relay-authority
49/// signature. The signature envelope is cleared before hashing.
50pub 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
71/// Verify a relay authority against the issuer's known public key.
72/// Returns `true` only on a clean, well-formed verify; any structural
73/// or cryptographic problem yields `false`. Use the std side if you
74/// need the textual reason.
75pub 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}