1use chacha20poly1305::{
7 XChaCha20Poly1305, XNonce,
8 aead::{Aead, KeyInit, Payload},
9};
10use std::fmt;
11
12use crate::runtime_exchange::RuntimeExchangeProfile;
13
14const MAGIC: &[u8; 8] = b"LCTXIR02";
15pub const RUNTIME_FRAME_HEADER_BYTES: usize = 12;
17pub const RUNTIME_FRAME_NONCE_BYTES: usize = 24;
19pub const RUNTIME_FRAME_TAG_BYTES: usize = 16;
21pub const MAX_RUNTIME_FRAME_PAYLOAD_BYTES: usize =
23 crate::runtime_exchange::MAX_RUNTIME_EXCHANGE_BYTES;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RuntimeFrameDirection {
28 Request,
29 Response,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum RuntimeFrameError {
34 InvalidHeader,
35 InvalidLength,
36 EncryptionFailed,
37 AuthenticationFailed,
38}
39
40impl fmt::Display for RuntimeFrameError {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(match self {
43 Self::InvalidHeader => "invalid runtime frame header or version",
44 Self::InvalidLength => "invalid runtime frame length",
45 Self::EncryptionFailed => "runtime frame encryption failed",
46 Self::AuthenticationFailed => "runtime frame authentication failed",
47 })
48 }
49}
50
51impl std::error::Error for RuntimeFrameError {}
52
53const fn payload_limit(profile: RuntimeExchangeProfile, direction: RuntimeFrameDirection) -> usize {
55 match direction {
56 RuntimeFrameDirection::Request => profile.max_request_bytes(),
57 RuntimeFrameDirection::Response => profile.max_response_bytes(),
58 }
59}
60
61pub fn runtime_payload_length(header: &[u8]) -> Result<usize, RuntimeFrameError> {
63 payload_length(header, MAX_RUNTIME_FRAME_PAYLOAD_BYTES)
64}
65
66pub fn runtime_payload_length_in(
70 header: &[u8],
71 direction: RuntimeFrameDirection,
72 profile: RuntimeExchangeProfile,
73) -> Result<usize, RuntimeFrameError> {
74 payload_length(header, payload_limit(profile, direction))
75}
76
77fn payload_length(header: &[u8], limit: usize) -> Result<usize, RuntimeFrameError> {
78 let length_bytes: [u8; 4] = header
79 .strip_prefix(MAGIC)
80 .and_then(|bytes| bytes.try_into().ok())
81 .ok_or(RuntimeFrameError::InvalidHeader)?;
82 let length = usize::try_from(u32::from_be_bytes(length_bytes))
83 .map_err(|_| RuntimeFrameError::InvalidLength)?;
84 validate_length(length, limit)?;
85 Ok(length)
86}
87
88pub fn encode_runtime_frame(
91 key: &[u8; 32],
92 direction: RuntimeFrameDirection,
93 nonce: &[u8; RUNTIME_FRAME_NONCE_BYTES],
94 payload: &[u8],
95) -> Result<Vec<u8>, RuntimeFrameError> {
96 encode(
97 key,
98 direction,
99 nonce,
100 payload,
101 MAX_RUNTIME_FRAME_PAYLOAD_BYTES,
102 )
103}
104
105pub fn encode_runtime_frame_in(
108 key: &[u8; 32],
109 direction: RuntimeFrameDirection,
110 nonce: &[u8; RUNTIME_FRAME_NONCE_BYTES],
111 payload: &[u8],
112 profile: RuntimeExchangeProfile,
113) -> Result<Vec<u8>, RuntimeFrameError> {
114 encode(
115 key,
116 direction,
117 nonce,
118 payload,
119 payload_limit(profile, direction),
120 )
121}
122
123fn encode(
124 key: &[u8; 32],
125 direction: RuntimeFrameDirection,
126 nonce: &[u8; RUNTIME_FRAME_NONCE_BYTES],
127 payload: &[u8],
128 limit: usize,
129) -> Result<Vec<u8>, RuntimeFrameError> {
130 let aad = associated_data(direction, payload.len(), limit)?;
131 let ciphertext = XChaCha20Poly1305::new(key.into())
132 .encrypt(
133 XNonce::from_slice(nonce),
134 Payload {
135 msg: payload,
136 aad: &aad,
137 },
138 )
139 .map_err(|_| RuntimeFrameError::EncryptionFailed)?;
140 let mut frame = Vec::with_capacity(
141 RUNTIME_FRAME_HEADER_BYTES + RUNTIME_FRAME_NONCE_BYTES + ciphertext.len(),
142 );
143 frame.extend_from_slice(&aad[..RUNTIME_FRAME_HEADER_BYTES]);
144 frame.extend_from_slice(nonce);
145 frame.extend_from_slice(&ciphertext);
146 Ok(frame)
147}
148
149pub fn decode_runtime_frame(
153 key: &[u8; 32],
154 direction: RuntimeFrameDirection,
155 frame: &[u8],
156) -> Result<Vec<u8>, RuntimeFrameError> {
157 decode(key, direction, frame, MAX_RUNTIME_FRAME_PAYLOAD_BYTES)
158}
159
160pub fn decode_runtime_frame_in(
162 key: &[u8; 32],
163 direction: RuntimeFrameDirection,
164 frame: &[u8],
165 profile: RuntimeExchangeProfile,
166) -> Result<Vec<u8>, RuntimeFrameError> {
167 decode(key, direction, frame, payload_limit(profile, direction))
168}
169
170fn decode(
171 key: &[u8; 32],
172 direction: RuntimeFrameDirection,
173 frame: &[u8],
174 limit: usize,
175) -> Result<Vec<u8>, RuntimeFrameError> {
176 let overhead = RUNTIME_FRAME_HEADER_BYTES + RUNTIME_FRAME_NONCE_BYTES + RUNTIME_FRAME_TAG_BYTES;
177 if frame.len() < overhead {
178 return Err(RuntimeFrameError::InvalidLength);
179 }
180 let (header, body) = frame.split_at(RUNTIME_FRAME_HEADER_BYTES);
181 let length = payload_length(header, limit)?;
182 if frame.len() != overhead + length {
183 return Err(RuntimeFrameError::InvalidLength);
184 }
185 let (nonce, ciphertext) = body.split_at(RUNTIME_FRAME_NONCE_BYTES);
186 let aad = associated_data(direction, length, limit)?;
187 XChaCha20Poly1305::new(key.into())
188 .decrypt(
189 XNonce::from_slice(nonce),
190 Payload {
191 msg: ciphertext,
192 aad: &aad,
193 },
194 )
195 .map_err(|_| RuntimeFrameError::AuthenticationFailed)
196}
197
198fn validate_length(length: usize, limit: usize) -> Result<(), RuntimeFrameError> {
199 if length == 0 || length > limit {
200 return Err(RuntimeFrameError::InvalidLength);
201 }
202 Ok(())
203}
204
205fn associated_data(
206 direction: RuntimeFrameDirection,
207 length: usize,
208 limit: usize,
209) -> Result<Vec<u8>, RuntimeFrameError> {
210 validate_length(length, limit)?;
211 let length = u32::try_from(length).map_err(|_| RuntimeFrameError::InvalidLength)?;
212 let mut aad = MAGIC.to_vec();
213 aad.extend_from_slice(&length.to_be_bytes());
214 aad.extend_from_slice(match direction {
215 RuntimeFrameDirection::Request => b"request\0",
216 RuntimeFrameDirection::Response => b"response\0",
217 });
218 Ok(aad)
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 const KEY: [u8; 32] = [0x0b; 32];
227 const NONCE: [u8; 24] = [0x0d; 24];
228
229 #[test]
230 fn round_trip_authenticates_every_byte_and_direction() {
231 let request = RuntimeFrameDirection::Request;
232 let payload = b"bounded projection";
233 let frame = encode_runtime_frame(&KEY, request, &NONCE, payload).unwrap();
234 assert_eq!(runtime_payload_length(&frame[..12]).unwrap(), payload.len());
235 assert_eq!(
236 decode_runtime_frame(&KEY, request, &frame).unwrap(),
237 payload
238 );
239 assert!(!frame.windows(payload.len()).any(|window| window == payload));
240 assert_ne!(
241 frame,
242 encode_runtime_frame(&KEY, request, &[0x0e; 24], payload).unwrap()
243 );
244 assert!(decode_runtime_frame(&KEY, RuntimeFrameDirection::Response, &frame).is_err());
245 assert!(decode_runtime_frame(&[0x0c; 32], request, &frame).is_err());
246 for index in 0..frame.len() {
247 let mut tampered = frame.clone();
248 tampered[index] ^= 1;
249 assert!(
250 decode_runtime_frame(&KEY, request, &tampered).is_err(),
251 "byte {index}"
252 );
253 }
254 for length in 0..frame.len() {
255 assert!(decode_runtime_frame(&KEY, request, &frame[..length]).is_err());
256 }
257 let mut trailing = frame;
258 trailing.push(0);
259 assert!(decode_runtime_frame(&KEY, request, &trailing).is_err());
260 }
261
262 #[test]
263 fn unpublished_plaintext_hmac_predecessor_is_not_a_downgrade_path() {
264 let mut old = b"LCTXIR01".to_vec();
266 old.extend_from_slice(&18_u32.to_be_bytes());
267 old.extend_from_slice(&[
268 0x22, 0x7a, 0xca, 0x2b, 0x41, 0x04, 0xc8, 0x81, 0xa1, 0xd8, 0x67, 0xfa, 0x3d, 0x9b,
269 0x8d, 0xce, 0xdf, 0xa4, 0x8d, 0x1b, 0x61, 0x28, 0x6f, 0xd2, 0xe8, 0x1c, 0xc9, 0x2b,
270 0x3a, 0x92, 0x18, 0x2c,
271 ]);
272 old.extend_from_slice(b"bounded projection");
273 assert!(decode_runtime_frame(&KEY, RuntimeFrameDirection::Request, &old).is_err());
274 }
275
276 #[test]
277 fn validates_header_before_allocation_and_rejects_oversize() {
278 for length in [0_u32, 1_048_577, u32::MAX] {
279 let mut header = MAGIC.to_vec();
280 header.extend_from_slice(&length.to_be_bytes());
281 assert!(runtime_payload_length(&header).is_err());
282 }
283 assert!(runtime_payload_length(b"LCTXIR02").is_err());
284 let direction = RuntimeFrameDirection::Response;
285 assert!(encode_runtime_frame(&KEY, direction, &NONCE, b"").is_err());
286 assert!(encode_runtime_frame(&KEY, direction, &NONCE, &vec![0; 1_048_577]).is_err());
287 let payload = vec![0; MAX_RUNTIME_FRAME_PAYLOAD_BYTES];
288 let frame = encode_runtime_frame(&KEY, direction, &NONCE, &payload).unwrap();
289 assert_eq!(
290 decode_runtime_frame(&KEY, direction, &frame).unwrap(),
291 payload
292 );
293 }
294
295 #[test]
296 fn a_profile_bounds_each_direction_at_the_header_and_ordinary_frames_are_unchanged() {
297 use RuntimeFrameDirection::{Request, Response};
298 let ordinary = RuntimeExchangeProfile::ORDINARY;
299 let verifier = RuntimeExchangeProfile::ORG_POLICY_VERIFIER;
300 let header = |length: usize| {
301 let mut header = MAGIC.to_vec();
302 header.extend_from_slice(&u32::try_from(length).unwrap().to_be_bytes());
303 header
304 };
305 for (direction, limit) in [
307 (Request, verifier.max_request_bytes()),
308 (Response, verifier.max_response_bytes()),
309 ] {
310 assert_eq!(
311 runtime_payload_length_in(&header(limit), direction, verifier),
312 Ok(limit)
313 );
314 assert_eq!(
315 runtime_payload_length_in(&header(limit + 1), direction, verifier),
316 Err(RuntimeFrameError::InvalidLength)
317 );
318 assert_eq!(
319 runtime_payload_length_in(
320 &header(MAX_RUNTIME_FRAME_PAYLOAD_BYTES + 1),
321 direction,
322 ordinary
323 ),
324 Err(RuntimeFrameError::InvalidLength)
325 );
326 }
327 assert!(verifier.max_request_bytes() > verifier.max_response_bytes());
329 assert!(
330 runtime_payload_length_in(&header(verifier.max_request_bytes()), Response, verifier)
331 .is_err()
332 );
333 assert!(runtime_payload_length(&header(MAX_RUNTIME_FRAME_PAYLOAD_BYTES + 1)).is_err());
334
335 let payload = vec![7; MAX_RUNTIME_FRAME_PAYLOAD_BYTES + 1];
337 let frame = encode_runtime_frame_in(&KEY, Request, &NONCE, &payload, verifier).unwrap();
338 assert_eq!(
339 frame.len(),
340 payload.len()
341 + RUNTIME_FRAME_HEADER_BYTES
342 + RUNTIME_FRAME_NONCE_BYTES
343 + RUNTIME_FRAME_TAG_BYTES
344 );
345 assert_eq!(
346 decode_runtime_frame_in(&KEY, Request, &frame, verifier).unwrap(),
347 payload
348 );
349 assert!(decode_runtime_frame_in(&KEY, Response, &frame, verifier).is_err());
350 assert_eq!(
351 decode_runtime_frame(&KEY, Request, &frame),
352 Err(RuntimeFrameError::InvalidLength)
353 );
354 assert!(encode_runtime_frame(&KEY, Request, &NONCE, &payload).is_err());
355 }
356}