1use 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
21const DOMAIN: &[u8] = b"interlink-v1\0";
24
25#[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#[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 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
75pub struct AgentKey(SigningKey);
77
78impl AgentKey {
79 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 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 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 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#[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 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
187fn 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
213const ANNOUNCE_DOMAIN: &[u8] = b"interlink-announce-v1\0";
217
218#[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 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
257pub 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 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 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 m.kind = MessageKind::PairRequest;
385 assert!(m.verify().is_err(), "kind must be signed");
386 }
387}