Skip to main content

agentmux_common/jekt_sign/
lan_v2.rs

1//! LAN v2: bound to the sender's UID (identity M4d-6,
2//! `SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md` ยง6.5.10). Made with
3//! the sender's UID-keyed key over the LAN material plus its UID. LAN peers are
4//! unauthenticated, so a receiver records the UID only as *claimed* by that
5//! LAN path, never as verified attribution. Its own domain: v1 LAN signs the
6//! bare material, and neither version's signature verifies as the other.
7
8use super::{BASE64, FIELD_SEP};
9use base64::Engine as _;
10use ed25519_dalek::{Signature, Verifier, VerifyingKey};
11
12const LAN_V2_DOMAIN: &str = "amx-jekt-lan-v2";
13
14fn lan_v2_signed_material(
15    msgid: &str,
16    source_agent: &str,
17    source_uid: &str,
18    target_agent: &str,
19    ts_secs: i64,
20    message: &str,
21) -> String {
22    format!(
23        "{LAN_V2_DOMAIN}{FIELD_SEP}{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}{source_uid}{FIELD_SEP}\
24         {target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}"
25    )
26}
27
28/// Sign a LAN jekt as `source_uid`, with that UID's own LAN private key.
29/// `None` for a key that isn't 32 bytes, like the other signers.
30pub fn sign_lan_jekt_v2(
31    private_key: &[u8],
32    msgid: &str,
33    source_agent: &str,
34    source_uid: &str,
35    target_agent: &str,
36    ts_secs: i64,
37    message: &str,
38) -> Option<String> {
39    let seed: [u8; 32] = private_key.try_into().ok()?;
40    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
41    let material = lan_v2_signed_material(msgid, source_agent, source_uid, target_agent, ts_secs, message);
42    let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
43    Some(BASE64.encode(signature.to_bytes()))
44}
45
46/// Verify a LAN v2 signature against the UID key the answering peer gave.
47/// `false`, never a panic, for anything malformed.
48#[allow(clippy::too_many_arguments)]
49pub fn verify_lan_jekt_v2(
50    public_key: &[u8],
51    msgid: &str,
52    source_agent: &str,
53    source_uid: &str,
54    target_agent: &str,
55    ts_secs: i64,
56    message: &str,
57    sig_b64: &str,
58) -> bool {
59    let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
60    let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
61    let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
62    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
63    let signature = Signature::from_bytes(&sig_arr);
64    let material = lan_v2_signed_material(msgid, source_agent, source_uid, target_agent, ts_secs, message);
65    verifying_key.verify(material.as_bytes(), &signature).is_ok()
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::jekt_sign::{generate_lan_keypair, sign_lan_jekt, verify_lan_jekt};
72
73    const TS: i64 = 1_790_000_000;
74
75    #[test]
76    fn a_lan_v2_signature_verifies_only_for_the_uid_it_binds() {
77        let (public, private) = generate_lan_keypair([3u8; 32]);
78        let sig = sign_lan_jekt_v2(&private, "m1", "aria", "uid-aria", "lark", TS, "hello").unwrap();
79        assert!(verify_lan_jekt_v2(&public, "m1", "aria", "uid-aria", "lark", TS, "hello", &sig));
80        assert!(!verify_lan_jekt_v2(&public, "m1", "aria", "uid-other", "lark", TS, "hello", &sig), "another UID");
81        assert!(!verify_lan_jekt_v2(&public, "m1", "bob", "uid-aria", "lark", TS, "hello", &sig), "another name");
82        assert!(!verify_lan_jekt_v2(&public, "m1", "aria", "uid-aria", "lark", TS, "edited", &sig), "another message");
83    }
84
85    #[test]
86    fn neither_lan_version_verifies_as_the_other() {
87        let (public, private) = generate_lan_keypair([3u8; 32]);
88        let v1 = sign_lan_jekt(&private, "m1", "aria", "lark", TS, "hello").unwrap();
89        assert!(!verify_lan_jekt_v2(&public, "m1", "aria", "", "lark", TS, "hello", &v1));
90        let v2 = sign_lan_jekt_v2(&private, "m1", "aria", "", "lark", TS, "hello").unwrap();
91        assert!(!verify_lan_jekt(&public, "m1", "aria", "lark", TS, "hello", &v2));
92    }
93
94    #[test]
95    fn lan_and_channel_v2_never_verify_as_each_other() {
96        let (public, private) = generate_lan_keypair([3u8; 32]);
97        let lan = sign_lan_jekt_v2(&private, "m1", "aria", "uid-aria", "lark", TS, "hello").unwrap();
98        assert!(!crate::jekt_sign::verify_channel_jekt_v2(
99            &public, "m1", "aria", "uid-aria", "", "lark", TS, "hello", &lan
100        ));
101    }
102}