Skip to main content

interlink/
identity.rs

1//! Ed25519 identity. **The public key is the identity**; names are local petnames.
2//!
3//! Claiming the name "alice" gets you nothing without her key, so the sender
4//! gate in the channel server checks a signature rather than a string an agent
5//! typed. This is the property the channel docs demand: *"gate on the sender's
6//! identity."*
7//!
8//! Signing covers a domain-separated, length-prefixed canonical encoding, so a
9//! signature can never be replayed into a different context and no pair of
10//! fields can be shifted across their boundary.
11
12use anyhow::{Context, Result, anyhow, bail};
13use base64::Engine;
14use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
15use ed25519_dalek::{
16    PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH, Signature, Signer, SigningKey,
17    VerifyingKey,
18};
19use serde::{Deserialize, Serialize};
20
21/// Bound into every message signature. Bumped v1→v2 when `kind` entered the
22/// canonical encoding; a bump makes old and new signatures mutually unverifiable.
23const DOMAIN: &[u8] = b"interlink-v1\0";
24
25/// What a signed message *is*. A plain `Message` is the everyday case; the pairing
26/// kinds are the only thing a non-peer may deliver (a knock), gated specially.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum MessageKind {
30    #[default]
31    Message,
32    PairRequest,
33    PairAccept,
34}
35
36impl MessageKind {
37    fn as_str(self) -> &'static str {
38        match self {
39            MessageKind::Message => "message",
40            MessageKind::PairRequest => "pair_request",
41            MessageKind::PairAccept => "pair_accept",
42        }
43    }
44}
45
46/// An agent's identity: its Ed25519 public key.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct AgentId(VerifyingKey);
49
50impl AgentId {
51    pub fn from_b64(s: &str) -> Result<Self> {
52        let raw = B64.decode(s).context("agent id is not valid base64")?;
53        let bytes: [u8; PUBLIC_KEY_LENGTH] = raw
54            .try_into()
55            .map_err(|_| anyhow!("agent id must be {PUBLIC_KEY_LENGTH} bytes"))?;
56        Ok(Self(
57            VerifyingKey::from_bytes(&bytes).context("not a valid ed25519 public key")?,
58        ))
59    }
60
61    pub fn to_b64(self) -> String {
62        B64.encode(self.0.as_bytes())
63    }
64
65    /// Short form for logs and `<channel>` tags. Not for authentication.
66    pub fn fingerprint(self) -> String {
67        self.to_b64().chars().take(8).collect()
68    }
69
70    pub fn as_verifying_key(&self) -> &VerifyingKey {
71        &self.0
72    }
73}
74
75/// An agent's secret key. Zeroized on drop by `ed25519-dalek`'s default features.
76pub struct AgentKey(SigningKey);
77
78impl AgentKey {
79    /// Seeded straight from the OS CSPRNG.
80    pub fn generate() -> Result<Self> {
81        let mut secret = [0u8; SECRET_KEY_LENGTH];
82        getrandom::fill(&mut secret).map_err(|e| anyhow!("OS entropy unavailable: {e}"))?;
83        Ok(Self(SigningKey::from_bytes(&secret)))
84    }
85
86    pub fn from_b64(s: &str) -> Result<Self> {
87        let raw = B64
88            .decode(s.trim())
89            .context("secret key is not valid base64")?;
90        let bytes: [u8; SECRET_KEY_LENGTH] = raw
91            .try_into()
92            .map_err(|_| anyhow!("secret key must be {SECRET_KEY_LENGTH} bytes"))?;
93        Ok(Self(SigningKey::from_bytes(&bytes)))
94    }
95
96    pub fn to_b64(&self) -> String {
97        B64.encode(self.0.to_bytes())
98    }
99
100    pub fn id(&self) -> AgentId {
101        AgentId(self.0.verifying_key())
102    }
103
104    /// Sign a plain message to `to`. `ts` and `msg_id` are covered, giving replay
105    /// protection when paired with the receiver's dedupe set.
106    pub fn sign(&self, to: AgentId, text: &str, ts: u64, msg_id: &str) -> SignedMessage {
107        self.sign_as(to, text, ts, msg_id, MessageKind::Message)
108    }
109
110    /// Sign a message of a specific `kind` (plain, or a pairing knock/accept).
111    pub fn sign_as(
112        &self,
113        to: AgentId,
114        text: &str,
115        ts: u64,
116        msg_id: &str,
117        kind: MessageKind,
118    ) -> SignedMessage {
119        let bytes = canonical(self.id(), to, ts, msg_id, text, kind);
120        let sig: Signature = self.0.sign(&bytes);
121        SignedMessage {
122            from: self.id().to_b64(),
123            to: to.to_b64(),
124            text: text.to_string(),
125            ts,
126            msg_id: msg_id.to_string(),
127            kind,
128            sig: B64.encode(sig.to_bytes()),
129        }
130    }
131
132    /// Sign a presence announcement: "this key is online, calling itself `name`".
133    pub fn announce(&self, name: &str, ts: u64) -> Announcement {
134        let sig: Signature = self.0.sign(&announce_canonical(self.id(), name, ts));
135        Announcement {
136            pubkey: self.id().to_b64(),
137            name: name.to_string(),
138            ts,
139            sig: B64.encode(sig.to_bytes()),
140        }
141    }
142}
143
144/// What travels over the bus. The bus treats it as an opaque payload.
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146pub struct SignedMessage {
147    pub from: String,
148    pub to: String,
149    pub text: String,
150    pub ts: u64,
151    pub msg_id: String,
152    #[serde(default)]
153    pub kind: MessageKind,
154    pub sig: String,
155}
156
157impl SignedMessage {
158    /// Verify the signature and return the *authenticated* sender.
159    ///
160    /// Uses `verify_strict`, which rejects small-order public keys and
161    /// non-canonical signature encodings.
162    pub fn verify(&self) -> Result<AgentId> {
163        let from = AgentId::from_b64(&self.from)?;
164        let to = AgentId::from_b64(&self.to)?;
165
166        let raw = B64
167            .decode(&self.sig)
168            .context("signature is not valid base64")?;
169        let sig_bytes: [u8; SIGNATURE_LENGTH] = raw
170            .try_into()
171            .map_err(|_| anyhow!("signature must be {SIGNATURE_LENGTH} bytes"))?;
172        let sig = Signature::from_bytes(&sig_bytes);
173
174        let bytes = canonical(from, to, self.ts, &self.msg_id, &self.text, self.kind);
175        from.as_verifying_key()
176            .verify_strict(&bytes, &sig)
177            .map_err(|_| {
178                anyhow!(
179                    "signature does not verify for sender {}",
180                    from.fingerprint()
181                )
182            })?;
183        Ok(from)
184    }
185}
186
187/// Domain-separated, length-prefixed. Length prefixes stop an attacker shifting
188/// bytes across the `msg_id`/`text` boundary to forge a different message under
189/// the same signature.
190fn canonical(
191    from: AgentId,
192    to: AgentId,
193    ts: u64,
194    msg_id: &str,
195    text: &str,
196    kind: MessageKind,
197) -> Vec<u8> {
198    let k = kind.as_str();
199    let mut b = Vec::with_capacity(DOMAIN.len() + 88 + k.len() + msg_id.len() + text.len());
200    b.extend_from_slice(DOMAIN);
201    b.extend_from_slice(from.as_verifying_key().as_bytes());
202    b.extend_from_slice(to.as_verifying_key().as_bytes());
203    b.extend_from_slice(&ts.to_le_bytes());
204    b.extend_from_slice(&(k.len() as u32).to_le_bytes());
205    b.extend_from_slice(k.as_bytes());
206    b.extend_from_slice(&(msg_id.len() as u32).to_le_bytes());
207    b.extend_from_slice(msg_id.as_bytes());
208    b.extend_from_slice(&(text.len() as u32).to_le_bytes());
209    b.extend_from_slice(text.as_bytes());
210    b
211}
212
213/// Bound into presence announcements. Separate from the message `DOMAIN` so the
214/// two version independently — a message-format change need not reissue the
215/// announcement format, and vice versa.
216const ANNOUNCE_DOMAIN: &[u8] = b"interlink-announce-v1\0";
217
218/// A signed presence announcement, published to the bus roster. The `name` is a
219/// self-claim; identity is the key, so a peer [`verify`](Announcement::verify)s
220/// before ever trusting the name.
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
222pub struct Announcement {
223    pub pubkey: String,
224    pub name: String,
225    pub ts: u64,
226    pub sig: String,
227}
228
229impl Announcement {
230    /// Verify the self-signature; returns the authenticated key on success.
231    pub fn verify(&self) -> Result<AgentId> {
232        let id = AgentId::from_b64(&self.pubkey)?;
233        let raw = B64
234            .decode(&self.sig)
235            .context("announcement signature is not valid base64")?;
236        let sig_bytes: [u8; SIGNATURE_LENGTH] = raw
237            .try_into()
238            .map_err(|_| anyhow!("signature must be {SIGNATURE_LENGTH} bytes"))?;
239        let sig = Signature::from_bytes(&sig_bytes);
240        id.as_verifying_key()
241            .verify_strict(&announce_canonical(id, &self.name, self.ts), &sig)
242            .map_err(|_| anyhow!("announcement does not verify for {}", id.fingerprint()))?;
243        Ok(id)
244    }
245}
246
247fn announce_canonical(pubkey: AgentId, name: &str, ts: u64) -> Vec<u8> {
248    let mut b = Vec::with_capacity(ANNOUNCE_DOMAIN.len() + 40 + name.len());
249    b.extend_from_slice(ANNOUNCE_DOMAIN);
250    b.extend_from_slice(pubkey.as_verifying_key().as_bytes());
251    b.extend_from_slice(&(name.len() as u32).to_le_bytes());
252    b.extend_from_slice(name.as_bytes());
253    b.extend_from_slice(&ts.to_le_bytes());
254    b
255}
256
257/// Reject messages whose timestamp is too far from now, bounding replay windows.
258pub fn check_freshness(ts: u64, now: u64, max_skew_ms: u64) -> Result<()> {
259    let delta = now.abs_diff(ts);
260    if delta > max_skew_ms {
261        bail!("message timestamp is {delta}ms from now (max {max_skew_ms}ms)");
262    }
263    Ok(())
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    fn key() -> AgentKey {
271        AgentKey::generate().unwrap()
272    }
273
274    #[test]
275    fn sign_then_verify_returns_the_sender() {
276        let (alice, bob) = (key(), key());
277        let msg = alice.sign(bob.id(), "hello", 1234, "m1");
278        assert_eq!(msg.verify().unwrap(), alice.id());
279    }
280
281    #[test]
282    fn tampered_text_fails() {
283        let (alice, bob) = (key(), key());
284        let mut msg = alice.sign(bob.id(), "transfer 1", 1234, "m1");
285        msg.text = "transfer 1000".into();
286        assert!(msg.verify().is_err());
287    }
288
289    #[test]
290    fn tampered_recipient_fails() {
291        let (alice, bob, eve) = (key(), key(), key());
292        let mut msg = alice.sign(bob.id(), "hi", 1, "m1");
293        msg.to = eve.id().to_b64();
294        assert!(msg.verify().is_err());
295    }
296
297    #[test]
298    fn forged_sender_fails() {
299        // Eve signs, then claims to be Alice.
300        let (alice, bob, eve) = (key(), key(), key());
301        let mut msg = eve.sign(bob.id(), "hi", 1, "m1");
302        msg.from = alice.id().to_b64();
303        assert!(msg.verify().is_err());
304    }
305
306    #[test]
307    fn ts_and_msg_id_are_covered() {
308        let (alice, bob) = (key(), key());
309        let orig = alice.sign(bob.id(), "hi", 1, "m1");
310        for mut m in [orig.clone(), orig.clone()] {
311            m.ts = 2;
312            assert!(m.verify().is_err(), "ts must be signed");
313        }
314        let mut m = orig;
315        m.msg_id = "m2".into();
316        assert!(m.verify().is_err(), "msg_id must be signed");
317    }
318
319    #[test]
320    fn length_prefixes_stop_boundary_shifting() {
321        // ("ab", "c") and ("a", "bc") must not produce the same signed bytes.
322        let (alice, bob) = (key(), key());
323        let a = canonical(alice.id(), bob.id(), 1, "ab", "c", MessageKind::Message);
324        let b = canonical(alice.id(), bob.id(), 1, "a", "bc", MessageKind::Message);
325        assert_ne!(a, b);
326    }
327
328    #[test]
329    fn domain_separation_is_bound_in() {
330        let (alice, bob) = (key(), key());
331        let bytes = canonical(alice.id(), bob.id(), 1, "m1", "hi", MessageKind::Message);
332        assert!(bytes.starts_with(DOMAIN));
333    }
334
335    #[test]
336    fn id_b64_round_trips() {
337        let alice = key();
338        let id = alice.id();
339        assert_eq!(AgentId::from_b64(&id.to_b64()).unwrap(), id);
340        assert_eq!(id.fingerprint().len(), 8);
341    }
342
343    #[test]
344    fn secret_key_b64_round_trips() {
345        let alice = key();
346        let restored = AgentKey::from_b64(&alice.to_b64()).unwrap();
347        assert_eq!(restored.id(), alice.id());
348    }
349
350    #[test]
351    fn freshness_bounds_replay_window() {
352        assert!(check_freshness(1_000, 1_500, 1_000).is_ok());
353        assert!(check_freshness(1_000, 5_000, 1_000).is_err());
354        assert!(
355            check_freshness(5_000, 1_000, 1_000).is_err(),
356            "future ts too"
357        );
358    }
359
360    #[test]
361    fn announcement_round_trips_and_rejects_tampering() {
362        let alice = key();
363        let a = alice.announce("alice-laptop", 1234);
364        assert_eq!(a.verify().unwrap(), alice.id());
365
366        let mut tampered_name = a.clone();
367        tampered_name.name = "eve-laptop".into();
368        assert!(tampered_name.verify().is_err(), "name is signed");
369
370        let mut forged_key = a;
371        forged_key.pubkey = key().id().to_b64();
372        assert!(
373            forged_key.verify().is_err(),
374            "can't reattribute to another key"
375        );
376    }
377
378    #[test]
379    fn kind_is_covered_by_signature() {
380        let (alice, bob) = (key(), key());
381        let mut m = alice.sign_as(bob.id(), "hi", 1, "m1", MessageKind::Message);
382        assert!(m.verify().is_ok());
383        // A message can't be re-typed into a pairing knock under the same signature.
384        m.kind = MessageKind::PairRequest;
385        assert!(m.verify().is_err(), "kind must be signed");
386    }
387}