Skip to main content

agentmux_common/jekt_sign/
channel_v2.rs

1//! Cross-channel v2: bound to the sender's UID (identity M4d-6,
2//! `SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md` ยง6.5.10). v1 proves
3//! only that the sender holds *some* key published under the claimed name,
4//! and one name can belong to different agents in different channels. v2 is
5//! made with the sender's UID-keyed key and binds its UID, so a receiver that
6//! verifies it against the key published for that UID knows which agent sent
7//! it. Its own domain, so neither version's signature verifies as the other.
8
9use super::{BASE64, FIELD_SEP};
10use base64::Engine as _;
11use ed25519_dalek::{Signature, Verifier, VerifyingKey};
12
13const CHANNEL_V2_DOMAIN: &str = "amx-jekt-channel-v2";
14
15#[allow(clippy::too_many_arguments)]
16fn channel_v2_signed_material(
17    msgid: &str,
18    source_agent: &str,
19    source_uid: &str,
20    source_channel: &str,
21    target_agent: &str,
22    ts_secs: i64,
23    message: &str,
24) -> String {
25    format!(
26        "{CHANNEL_V2_DOMAIN}{FIELD_SEP}{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}{source_uid}{FIELD_SEP}\
27         {source_channel}{FIELD_SEP}{target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}"
28    )
29}
30
31/// Sign a cross-channel jekt as `source_uid`, with that UID's own LAN private
32/// key. `None` for a key that isn't 32 bytes, like the other signers.
33#[allow(clippy::too_many_arguments)]
34pub fn sign_channel_jekt_v2(
35    private_key: &[u8],
36    msgid: &str,
37    source_agent: &str,
38    source_uid: &str,
39    source_channel: &str,
40    target_agent: &str,
41    ts_secs: i64,
42    message: &str,
43) -> Option<String> {
44    let seed: [u8; 32] = private_key.try_into().ok()?;
45    let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
46    let material = channel_v2_signed_material(
47        msgid,
48        source_agent,
49        source_uid,
50        source_channel,
51        target_agent,
52        ts_secs,
53        message,
54    );
55    let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
56    Some(BASE64.encode(signature.to_bytes()))
57}
58
59/// Verify a v2 cross-channel signature against the public key published for
60/// `source_uid`. `false`, never a panic, for anything malformed.
61#[allow(clippy::too_many_arguments)]
62pub fn verify_channel_jekt_v2(
63    public_key: &[u8],
64    msgid: &str,
65    source_agent: &str,
66    source_uid: &str,
67    source_channel: &str,
68    target_agent: &str,
69    ts_secs: i64,
70    message: &str,
71    sig_b64: &str,
72) -> bool {
73    let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else {
74        return false;
75    };
76    let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else {
77        return false;
78    };
79    let Ok(sig_bytes) = BASE64.decode(sig_b64) else {
80        return false;
81    };
82    let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else {
83        return false;
84    };
85    let signature = Signature::from_bytes(&sig_arr);
86    let material = channel_v2_signed_material(
87        msgid,
88        source_agent,
89        source_uid,
90        source_channel,
91        target_agent,
92        ts_secs,
93        message,
94    );
95    verifying_key
96        .verify(material.as_bytes(), &signature)
97        .is_ok()
98}
99
100#[cfg(test)]
101mod channel_v2_tests {
102    use super::*;
103    use crate::jekt_sign::{generate_lan_keypair, sign_channel_jekt, verify_channel_jekt};
104
105    const TS: i64 = 1_790_000_000;
106
107    fn sign(private: &[u8], uid: &str) -> String {
108        sign_channel_jekt_v2(private, "m1", "aria", uid, "chan-a", "lark", TS, "hello").unwrap()
109    }
110
111    #[test]
112    fn a_v2_signature_verifies_only_for_the_uid_it_binds() {
113        let (public, private) = generate_lan_keypair([9u8; 32]);
114        let sig = sign(&private, "uid-aria");
115        assert!(verify_channel_jekt_v2(
116            &public, "m1", "aria", "uid-aria", "chan-a", "lark", TS, "hello", &sig
117        ));
118        assert!(
119            !verify_channel_jekt_v2(
120                &public,
121                "m1",
122                "aria",
123                "uid-other",
124                "chan-a",
125                "lark",
126                TS,
127                "hello",
128                &sig
129            ),
130            "another UID"
131        );
132        assert!(
133            !verify_channel_jekt_v2(
134                &public, "m1", "aria", "uid-aria", "chan-b", "lark", TS, "hello", &sig
135            ),
136            "another channel"
137        );
138        assert!(
139            !verify_channel_jekt_v2(
140                &public, "m1", "aria", "uid-aria", "chan-a", "lark", TS, "edited", &sig
141            ),
142            "another message"
143        );
144    }
145
146    #[test]
147    fn neither_version_verifies_as_the_other() {
148        let (public, private) = generate_lan_keypair([9u8; 32]);
149        let v1 = sign_channel_jekt(&private, "m1", "aria", "chan-a", "lark", TS, "hello").unwrap();
150        assert!(!verify_channel_jekt_v2(
151            &public, "m1", "aria", "", "chan-a", "lark", TS, "hello", &v1
152        ));
153        let v2 = sign(&private, "");
154        assert!(!verify_channel_jekt(
155            &public, "m1", "aria", "chan-a", "lark", TS, "hello", &v2
156        ));
157    }
158}