1use base64::Engine;
13use base64::engine::general_purpose::STANDARD;
14use clatter::HybridHandshake;
15use clatter::HybridHandshakeParams;
16use clatter::KeyPair;
17use clatter::bytearray::ByteArray;
18use clatter::constants::MAX_MESSAGE_LEN;
19use clatter::crypto::cipher::AesGcm;
20use clatter::crypto::dh::X25519;
21use clatter::crypto::hash::Sha256;
22use clatter::crypto::kem::rust_crypto_ml_kem::MlKem768;
23use clatter::handshakepattern::noise_hybrid_ik;
24use clatter::traits::Cipher;
25use clatter::traits::Dh;
26use clatter::traits::Handshaker;
27use clatter::traits::Kem;
28use clatter::transportstate::TransportState;
29use serde::Deserialize;
30use serde::Serialize;
31
32pub(crate) const NOISE_CHANNEL_SUITE: &str = "Noise_hybridIK_X25519+MLKEM768_AESGCM_SHA256";
34
35const PROLOGUE_DOMAIN: &[u8] = b"codex-exec-server-relay-noise/v1";
36
37type Handshake = HybridHandshake<X25519, MlKem768, MlKem768, AesGcm, Sha256>;
38type Transport = TransportState<AesGcm, Sha256>;
39type DhKeyPair = KeyPair<<X25519 as Dh>::PubKey, <X25519 as Dh>::PrivateKey>;
40type MlKem768PublicKey = <MlKem768 as Kem>::PubKey;
41type KemKeyPair = KeyPair<<MlKem768 as Kem>::PubKey, <MlKem768 as Kem>::SecretKey>;
42
43#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct NoiseChannelPublicKey {
49 suite: String,
50 x25519_public_key: String,
51 mlkem768_public_key: String,
52}
53
54impl std::fmt::Debug for NoiseChannelPublicKey {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("NoiseChannelPublicKey")
57 .field("suite", &self.suite)
58 .field("x25519_public_key", &"<redacted>")
59 .field("mlkem768_public_key", &"<redacted>")
60 .finish()
61 }
62}
63
64impl NoiseChannelPublicKey {
65 fn decode(&self) -> Result<(<X25519 as Dh>::PubKey, MlKem768PublicKey), NoiseChannelError> {
67 if self.suite != NOISE_CHANNEL_SUITE {
68 return Err(NoiseChannelError::InvalidPublicKey(
69 "unsupported Noise channel suite",
70 ));
71 }
72 let dh = STANDARD
73 .decode(&self.x25519_public_key)
74 .map_err(|_| NoiseChannelError::InvalidPublicKey("invalid X25519 public key"))?;
75 let dh: <X25519 as Dh>::PubKey = dh
76 .try_into()
77 .map_err(|_| NoiseChannelError::InvalidPublicKey("invalid X25519 public key length"))?;
78 let kem = STANDARD
79 .decode(&self.mlkem768_public_key)
80 .map_err(|_| NoiseChannelError::InvalidPublicKey("invalid ML-KEM-768 public key"))?;
81 if kem.len() != MlKem768PublicKey::LENGTH {
82 return Err(NoiseChannelError::InvalidPublicKey(
83 "invalid ML-KEM-768 public key length",
84 ));
85 }
86
87 Ok((dh, MlKem768PublicKey::from_slice(kem.as_slice())))
88 }
89}
90
91#[derive(Clone)]
93pub struct NoiseChannelIdentity {
94 dh: DhKeyPair,
95 kem: KemKeyPair,
96}
97
98impl NoiseChannelIdentity {
99 pub fn generate() -> Result<Self, NoiseChannelError> {
100 let dh = X25519::genkey()
101 .map_err(|error| NoiseChannelError::KeyGeneration(error.to_string()))?;
102 let kem = MlKem768::genkey()
103 .map_err(|error| NoiseChannelError::KeyGeneration(error.to_string()))?;
104 Ok(Self { dh, kem })
105 }
106
107 pub fn public_key(&self) -> NoiseChannelPublicKey {
108 NoiseChannelPublicKey {
109 suite: NOISE_CHANNEL_SUITE.to_string(),
110 x25519_public_key: STANDARD.encode(self.dh.public),
111 mlkem768_public_key: STANDARD.encode(self.kem.public.as_slice()),
112 }
113 }
114}
115
116pub(crate) struct InitiatorHandshake {
119 handshake: Handshake,
120}
121
122impl InitiatorHandshake {
123 pub(crate) fn start(
127 identity: &NoiseChannelIdentity,
128 responder_public_key: &NoiseChannelPublicKey,
129 prologue: &[u8],
130 payload: &[u8],
131 ) -> Result<(Self, Vec<u8>), NoiseChannelError> {
132 let (responder_dh, responder_kem) = responder_public_key.decode()?;
133
134 let params = HybridHandshakeParams::new(noise_hybrid_ik(), true)
136 .with_prologue(prologue)
137 .with_s(identity.dh.clone())
138 .with_s_kem(identity.kem.clone())
139 .with_rs(responder_dh)
140 .with_rs_kem(responder_kem);
141 let mut handshake = Handshake::new(params)?;
142 let overhead = handshake.get_next_message_overhead()?;
143 if payload.len() > MAX_MESSAGE_LEN - overhead {
144 return Err(NoiseChannelError::InvalidMessage(
145 "handshake payload is too large",
146 ));
147 }
148 let mut output = vec![0u8; payload.len() + overhead];
149 let output_len = handshake.write_message(payload, &mut output)?;
150 output.truncate(output_len);
151 Ok((Self { handshake }, output))
152 }
153
154 pub(crate) fn finish(mut self, response: &[u8]) -> Result<NoiseTransport, NoiseChannelError> {
157 ensure_noise_frame_len(response.len(), "handshake response is too large")?;
158 let overhead = self.handshake.get_next_message_overhead()?;
159 let mut payload = vec![0u8; response.len().saturating_sub(overhead)];
160 let payload_len = self.handshake.read_message(response, &mut payload)?;
161 if payload_len != 0 {
162 return Err(NoiseChannelError::InvalidMessage(
163 "handshake response payload must be empty",
164 ));
165 }
166 Ok(NoiseTransport {
167 transport: self.handshake.finalize()?,
168 })
169 }
170}
171
172pub(crate) struct PendingResponderHandshake {
176 handshake: Handshake,
177 pub(crate) initiator_public_key: NoiseChannelPublicKey,
178 pub(crate) payload: Vec<u8>,
179}
180
181impl PendingResponderHandshake {
182 pub(crate) fn read_request(
185 identity: &NoiseChannelIdentity,
186 prologue: &[u8],
187 request: &[u8],
188 ) -> Result<Self, NoiseChannelError> {
189 ensure_noise_frame_len(request.len(), "handshake request is too large")?;
190 let params = HybridHandshakeParams::new(noise_hybrid_ik(), false)
191 .with_prologue(prologue)
192 .with_s(identity.dh.clone())
193 .with_s_kem(identity.kem.clone());
194 let mut handshake = Handshake::new(params)?;
195 let overhead = handshake.get_next_message_overhead()?;
196 let mut payload = vec![0u8; request.len().saturating_sub(overhead)];
197 let payload_len = handshake.read_message(request, &mut payload)?;
198 let remote = handshake
200 .get_remote_static()
201 .ok_or(NoiseChannelError::InvalidMessage(
202 "handshake request is missing initiator static key",
203 ))?;
204 let initiator_public_key = NoiseChannelPublicKey {
205 suite: NOISE_CHANNEL_SUITE.to_string(),
206 x25519_public_key: STANDARD.encode(remote.dh()),
207 mlkem768_public_key: STANDARD.encode(remote.kem().as_slice()),
208 };
209 payload.truncate(payload_len);
210 Ok(Self {
211 handshake,
212 initiator_public_key,
213 payload,
214 })
215 }
216
217 pub(crate) fn complete(mut self) -> Result<(NoiseTransport, Vec<u8>), NoiseChannelError> {
219 let overhead = self.handshake.get_next_message_overhead()?;
220 let mut response = vec![0u8; overhead];
221 let response_len = self.handshake.write_message(&[], &mut response)?;
222 response.truncate(response_len);
223 Ok((
224 NoiseTransport {
225 transport: self.handshake.finalize()?,
226 },
227 response,
228 ))
229 }
230}
231
232pub(crate) struct NoiseTransport {
236 transport: Transport,
237}
238
239impl NoiseTransport {
240 pub(crate) fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, NoiseChannelError> {
242 let frame_len = plaintext.len().checked_add(AesGcm::tag_len()).ok_or(
243 NoiseChannelError::InvalidMessage("transport plaintext is too large"),
244 )?;
245 ensure_noise_frame_len(frame_len, "transport plaintext is too large")?;
246 Ok(self.transport.send_vec(plaintext)?)
247 }
248
249 pub(crate) fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, NoiseChannelError> {
251 if ciphertext.len() < AesGcm::tag_len() {
252 return Err(NoiseChannelError::InvalidMessage(
253 "transport ciphertext is too short",
254 ));
255 }
256 ensure_noise_frame_len(ciphertext.len(), "transport ciphertext is too large")?;
257 Ok(self.transport.receive_vec(ciphertext)?)
258 }
259}
260
261pub(crate) fn noise_channel_prologue(
265 environment_id: &str,
266 executor_registration_id: &str,
267 stream_id: &str,
268) -> Vec<u8> {
269 let mut prologue = Vec::new();
270 append_prologue_part(&mut prologue, PROLOGUE_DOMAIN);
271 append_prologue_part(&mut prologue, environment_id.as_bytes());
272 append_prologue_part(&mut prologue, executor_registration_id.as_bytes());
273 append_prologue_part(&mut prologue, stream_id.as_bytes());
274 prologue
275}
276
277fn append_prologue_part(prologue: &mut Vec<u8>, part: &[u8]) {
278 let len = part.len() as u64;
281 prologue.extend_from_slice(&len.to_be_bytes());
282 prologue.extend_from_slice(part);
283}
284
285fn ensure_noise_frame_len(
286 frame_len: usize,
287 message: &'static str,
288) -> Result<(), NoiseChannelError> {
289 if frame_len > MAX_MESSAGE_LEN {
290 return Err(NoiseChannelError::InvalidMessage(message));
291 }
292 Ok(())
293}
294
295#[derive(Debug, thiserror::Error)]
296pub enum NoiseChannelError {
297 #[error("Noise channel key generation failed: {0}")]
298 KeyGeneration(String),
299 #[error("invalid Noise channel public key: {0}")]
300 InvalidPublicKey(&'static str),
301 #[error("invalid Noise channel message: {0}")]
302 InvalidMessage(&'static str),
303 #[error("Noise channel handshake failed: {0}")]
304 Handshake(String),
305 #[error("Noise channel transport failed: {0}")]
306 Transport(String),
307}
308
309impl From<clatter::error::HandshakeError> for NoiseChannelError {
310 fn from(error: clatter::error::HandshakeError) -> Self {
311 Self::Handshake(error.to_string())
312 }
313}
314
315impl From<clatter::error::TransportError> for NoiseChannelError {
316 fn from(error: clatter::error::TransportError) -> Self {
317 Self::Transport(error.to_string())
318 }
319}
320
321#[cfg(test)]
322#[path = "noise_channel_tests.rs"]
323mod tests;