agentmux_common/jekt_sign/
channel_v2.rs1use 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#[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#[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}