1use chacha20poly1305::{
4 ChaCha20Poly1305,
5 aead::{AeadInOut, KeyInit, array::Array},
6};
7
8use crate::{
9 EntropySource, Error, HeaderField, Method, Response,
10 codec::{RESPONSE_NONCE_LEN, TAG_LEN, VERSION},
11 compression,
12 engine::{ResponseMaterial, derive_response_key_nonce},
13 message,
14 sse::validate_block,
15};
16
17const MAGIC: &[u8; 4] = b"HHRP";
18const PREFIX_LEN: usize = 4 + 1 + RESPONSE_NONCE_LEN;
19const AAD_DOMAIN: &[u8] = b"hpke-http/3 response record\0";
20const AAD_LEN: usize = AAD_DOMAIN.len() + PREFIX_LEN + 8 + 4;
21const START: u8 = 1;
22const DATA: u8 = 2;
23const END: u8 = 3;
24const MIN_CIPHERTEXT_LEN: usize = 1 + TAG_LEN;
25const RETAIN_FRAME_CAPACITY: usize = 64 * 1024;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum ResponseMode {
30 Finite,
32 Sse,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ResponseHead {
39 pub status: u16,
41 pub headers: Vec<HeaderField>,
43 pub mode: ResponseMode,
45}
46
47#[derive(Debug, Eq, PartialEq)]
49pub enum ResponseRecord {
50 Start(ResponseHead),
52 SseData(Vec<u8>),
54 End,
56}
57
58struct Crypto {
59 key: zeroize::Zeroizing<[u8; 32]>,
60 base_nonce: zeroize::Zeroizing<[u8; 12]>,
61 prefix: [u8; PREFIX_LEN],
62 sequence: u64,
63}
64
65impl Crypto {
66 fn new(material: &ResponseMaterial, prefix: [u8; PREFIX_LEN]) -> Result<Self, Error> {
67 let (key, base_nonce) = derive_response_key_nonce(material, &prefix[5..])?;
68 Ok(Self {
69 key,
70 base_nonce,
71 prefix,
72 sequence: 0,
73 })
74 }
75
76 fn nonce(&self) -> [u8; 12] {
77 let mut nonce = *self.base_nonce;
78 for (target, byte) in nonce[4..].iter_mut().zip(self.sequence.to_be_bytes()) {
79 *target ^= byte;
80 }
81 nonce
82 }
83
84 fn aad(&self, ciphertext_len: u32) -> [u8; AAD_LEN] {
85 let mut aad = [0; AAD_LEN];
86 let prefix_end = AAD_DOMAIN.len() + PREFIX_LEN;
87 aad[..AAD_DOMAIN.len()].copy_from_slice(AAD_DOMAIN);
88 aad[AAD_DOMAIN.len()..prefix_end].copy_from_slice(&self.prefix);
89 aad[prefix_end..prefix_end + 8].copy_from_slice(&self.sequence.to_be_bytes());
90 aad[prefix_end + 8..].copy_from_slice(&ciphertext_len.to_be_bytes());
91 aad
92 }
93
94 fn seal_record(
95 &mut self,
96 kind: u8,
97 coding: Option<u8>,
98 body: &[u8],
99 terminal: bool,
100 ) -> Result<Vec<u8>, Error> {
101 if self.sequence == u64::MAX && !terminal {
102 return Err(Error::LimitExceeded);
103 }
104 let length = body
105 .len()
106 .checked_add(1 + usize::from(coding.is_some()))
107 .and_then(|size| size.checked_add(TAG_LEN))
108 .ok_or(Error::LimitExceeded)?;
109 let length = u32::try_from(length).map_err(|_| Error::LimitExceeded)?;
110 let frame_len = 4_usize
111 .checked_add(length as usize)
112 .ok_or(Error::LimitExceeded)?;
113 let mut framed = Vec::with_capacity(frame_len);
114 framed.extend_from_slice(&length.to_be_bytes());
115 framed.push(kind);
116 if let Some(coding) = coding {
117 framed.push(coding);
118 }
119 framed.extend_from_slice(body);
120 let cipher = ChaCha20Poly1305::new(&Array(*self.key));
121 let tag = cipher
122 .encrypt_inout_detached(
123 &Array(self.nonce()),
124 &self.aad(length),
125 (&mut framed[4..]).into(),
126 )
127 .map_err(|_| Error::CryptoFailure)?;
128 framed.extend_from_slice(&tag);
129 if !terminal {
130 self.sequence += 1;
131 }
132 Ok(framed)
133 }
134
135 fn open(&mut self, ciphertext: &mut Vec<u8>, length: u32) -> Result<(), Error> {
136 let cipher = ChaCha20Poly1305::new(&Array(*self.key));
137 cipher
138 .decrypt_in_place(&Array(self.nonce()), &self.aad(length), ciphertext)
139 .map_err(|_| Error::AuthenticationFailed)
140 }
141
142 fn advance(&mut self) -> Result<(), Error> {
143 self.sequence = self.sequence.checked_add(1).ok_or(Error::LimitExceeded)?;
144 Ok(())
145 }
146}
147
148fn classify_head(
149 status: u16,
150 headers: &[HeaderField],
151 material: &ResponseMaterial,
152) -> Result<ResponseMode, Error> {
153 message::validate_response_head(status, headers, material.limits)?;
154 let mut content_type = None;
155 for field in headers {
156 if field.name == b"content-type" && content_type.replace(field.value.as_slice()).is_some() {
157 return Err(Error::InvalidConfiguration);
158 }
159 }
160 let is_sse = content_type.is_some_and(|value| {
161 value
162 .split(|byte| *byte == b';')
163 .next()
164 .unwrap_or_default()
165 .trim_ascii()
166 .eq_ignore_ascii_case(b"text/event-stream")
167 });
168 if is_sse {
169 if status != 200
170 || material.method == Method::Head
171 || headers
172 .iter()
173 .any(|field| field.name == b"content-length" || field.name == b"content-encoding")
174 {
175 return Err(Error::InvalidConfiguration);
176 }
177 Ok(ResponseMode::Sse)
178 } else {
179 Ok(ResponseMode::Finite)
180 }
181}
182
183pub struct ResponseSealer {
185 crypto: Option<Crypto>,
186 head: ResponseHead,
187 method: Method,
188 limits: crate::Limits,
189 finite_data_sent: bool,
190 done: bool,
191}
192
193impl ResponseSealer {
194 #[must_use]
196 pub const fn head_mode(&self) -> ResponseMode {
197 self.head.mode
198 }
199
200 pub(crate) fn new(
201 material: &ResponseMaterial,
202 status: u16,
203 headers: Vec<HeaderField>,
204 entropy: &mut impl EntropySource,
205 ) -> Result<(Self, Vec<u8>), Error> {
206 let mode = classify_head(status, &headers, material)?;
207 let mut prefix = [0_u8; PREFIX_LEN];
208 prefix[..4].copy_from_slice(MAGIC);
209 prefix[4] = VERSION;
210 entropy.fill(&mut prefix[5..])?;
211 let mut crypto = Crypto::new(material, prefix)?;
212 let fields = message::encode_fields(&headers)?;
213 let mut start = Vec::with_capacity(2 + fields.len());
214 start.extend_from_slice(&status.to_be_bytes());
215 start.extend_from_slice(&fields);
216 let frame = crypto.seal_record(START, None, &start, false)?;
217 let mut output = Vec::with_capacity(PREFIX_LEN + frame.len());
218 output.extend_from_slice(&prefix);
219 output.extend_from_slice(&frame);
220 Ok((
221 Self {
222 crypto: Some(crypto),
223 head: ResponseHead {
224 status,
225 headers,
226 mode,
227 },
228 method: material.method,
229 limits: material.limits,
230 finite_data_sent: false,
231 done: false,
232 },
233 output,
234 ))
235 }
236
237 pub fn seal_sse_block(&mut self, block: &[u8]) -> Result<Vec<u8>, Error> {
242 let result = self.seal_sse_block_inner(block);
243 if result.is_err() {
244 self.poison();
245 }
246 result
247 }
248
249 fn seal_sse_block_inner(&mut self, block: &[u8]) -> Result<Vec<u8>, Error> {
250 if self.done || self.head.mode != ResponseMode::Sse {
251 return Err(Error::InvalidConfiguration);
252 }
253 validate_block(block, self.limits.max_body_len)?;
254 let (coding, coded) = compression::encode(block);
255 self.crypto
256 .as_mut()
257 .ok_or(Error::InvalidConfiguration)?
258 .seal_record(DATA, Some(coding), &coded, false)
259 }
260
261 pub fn seal_finite_body(&mut self, body: &[u8]) -> Result<Option<Vec<u8>>, Error> {
266 let result = self.seal_finite_body_inner(body);
267 if result.is_err() {
268 self.poison();
269 }
270 result
271 }
272
273 fn seal_finite_body_inner(&mut self, body: &[u8]) -> Result<Option<Vec<u8>>, Error> {
274 if self.done || self.head.mode != ResponseMode::Finite || self.finite_data_sent {
275 return Err(Error::InvalidConfiguration);
276 }
277 if body.len() > self.limits.max_body_len {
278 return Err(Error::LimitExceeded);
279 }
280 message::validate_response_content_length(
281 self.head.status,
282 &self.head.headers,
283 body.len(),
284 self.method,
285 )?;
286 if !body.is_empty()
287 && (self.method == Method::Head || matches!(self.head.status, 204 | 205 | 304))
288 {
289 return Err(Error::InvalidConfiguration);
290 }
291 if body.is_empty() {
292 self.finite_data_sent = true;
293 return Ok(None);
294 }
295 let (coding, encoded) = compression::encode(body);
296 let frame = self
297 .crypto
298 .as_mut()
299 .ok_or(Error::InvalidConfiguration)?
300 .seal_record(DATA, Some(coding), &encoded, false)?;
301 self.finite_data_sent = true;
302 Ok(Some(frame))
303 }
304
305 pub fn finish(&mut self) -> Result<Vec<u8>, Error> {
311 if self.done || (self.head.mode == ResponseMode::Finite && !self.finite_data_sent) {
312 self.poison();
313 return Err(Error::InvalidConfiguration);
314 }
315 let result = self
316 .crypto
317 .as_mut()
318 .ok_or(Error::InvalidConfiguration)?
319 .seal_record(END, None, &[], true);
320 self.poison();
321 result
322 }
323
324 pub fn close(&mut self) {
326 self.poison();
327 }
328
329 fn poison(&mut self) {
330 self.crypto = None;
331 self.done = true;
332 }
333}
334
335pub struct ResponseOpener {
337 material: Option<ResponseMaterial>,
338 crypto: Option<Crypto>,
339 prefix: Vec<u8>,
340 length: Vec<u8>,
341 frame: Vec<u8>,
342 expected: Option<usize>,
343 head: Option<ResponseHead>,
344 finite_body: Option<Vec<u8>>,
345 finite_response: Option<Response>,
346 done: bool,
347 failed: bool,
348 eof_checked: bool,
349}
350
351impl ResponseOpener {
352 pub(crate) fn new(material: ResponseMaterial) -> Self {
353 Self {
354 material: Some(material),
355 crypto: None,
356 prefix: Vec::with_capacity(PREFIX_LEN),
357 length: Vec::with_capacity(4),
358 frame: Vec::new(),
359 expected: None,
360 head: None,
361 finite_body: None,
362 finite_response: None,
363 done: false,
364 failed: false,
365 eof_checked: false,
366 }
367 }
368
369 pub fn feed(&mut self, input: &[u8]) -> Result<(usize, Option<ResponseRecord>), Error> {
376 let result = self.feed_inner(input);
377 if result.is_err() {
378 self.poison();
379 }
380 result
381 }
382
383 fn feed_inner(&mut self, input: &[u8]) -> Result<(usize, Option<ResponseRecord>), Error> {
384 if self.failed || self.eof_checked || (self.done && !input.is_empty()) {
385 return Err(Error::MalformedEnvelope);
386 }
387 let mut used = 0;
388 if used < input.len() {
389 if self.crypto.is_none() {
390 let take = (PREFIX_LEN - self.prefix.len()).min(input.len() - used);
391 self.prefix.extend_from_slice(&input[used..used + take]);
392 used += take;
393 if self.prefix.len() < PREFIX_LEN {
394 return Ok((used, None));
395 }
396 if self.prefix[..4] != *MAGIC || self.prefix[4] != VERSION {
397 return Err(Error::MalformedEnvelope);
398 }
399 let prefix: [u8; PREFIX_LEN] = self
400 .prefix
401 .as_slice()
402 .try_into()
403 .map_err(|_| Error::MalformedEnvelope)?;
404 let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
405 self.crypto = Some(Crypto::new(material, prefix)?);
406 }
407 if self.expected.is_none() {
408 let take = (4 - self.length.len()).min(input.len() - used);
409 self.length.extend_from_slice(&input[used..used + take]);
410 used += take;
411 if self.length.len() < 4 {
412 return Ok((used, None));
413 }
414 let length = u32::from_be_bytes(
415 self.length
416 .as_slice()
417 .try_into()
418 .map_err(|_| Error::MalformedEnvelope)?,
419 ) as usize;
420 let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
421 let maximum = if self.head.is_none() {
422 4 + material.limits.max_header_bytes
423 + material.limits.max_header_count * 16
424 + TAG_LEN
425 } else {
426 2 + material.limits.max_body_len + TAG_LEN
427 };
428 if length < MIN_CIPHERTEXT_LEN {
429 return Err(Error::MalformedEnvelope);
430 }
431 if length > maximum {
432 return Err(Error::LimitExceeded);
433 }
434 self.expected = Some(length);
435 }
436 let expected = self.expected.ok_or(Error::MalformedEnvelope)?;
437 let take = (expected - self.frame.len()).min(input.len() - used);
438 self.frame.extend_from_slice(&input[used..used + take]);
439 used += take;
440 if self.frame.len() < expected {
441 return Ok((used, None));
442 }
443 let record = self.open_frame()?;
444 if self.frame.capacity() > RETAIN_FRAME_CAPACITY {
445 self.frame = Vec::new();
446 } else {
447 self.frame.clear();
448 }
449 self.length.clear();
450 self.expected = None;
451 if self.done && used < input.len() {
452 return Err(Error::MalformedEnvelope);
453 }
454 return Ok((used, record));
455 }
456 Ok((used, None))
457 }
458
459 fn open_frame(&mut self) -> Result<Option<ResponseRecord>, Error> {
460 let length = u32::try_from(self.frame.len()).map_err(|_| Error::LimitExceeded)?;
461 self.crypto
462 .as_mut()
463 .ok_or(Error::MalformedEnvelope)?
464 .open(&mut self.frame, length)?;
465 let (&kind, body) = self.frame.split_first().ok_or(Error::MalformedEnvelope)?;
466 if self.head.is_none() {
467 if kind != START || body.len() < 2 {
468 return Err(Error::MalformedEnvelope);
469 }
470 let status = u16::from_be_bytes([body[0], body[1]]);
471 let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
472 let headers =
473 message::decode_fields(&body[2..], material.limits).map_err(decode_error)?;
474 let mode = classify_head(status, &headers, material).map_err(decode_error)?;
475 let head = ResponseHead {
476 status,
477 headers,
478 mode,
479 };
480 self.head = Some(head.clone());
481 self.crypto
482 .as_mut()
483 .ok_or(Error::MalformedEnvelope)?
484 .advance()?;
485 return Ok(Some(ResponseRecord::Start(head)));
486 }
487 let head = self.head.as_ref().ok_or(Error::MalformedEnvelope)?;
488 match kind {
489 DATA => {
490 if body.len() < 2 {
491 return Err(Error::MalformedEnvelope);
492 }
493 if head.mode == ResponseMode::Sse {
494 let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
495 let block =
496 compression::decode(body[0], &body[1..], material.limits.max_body_len)?;
497 validate_block(&block, material.limits.max_body_len)?;
498 self.crypto
499 .as_mut()
500 .ok_or(Error::MalformedEnvelope)?
501 .advance()?;
502 Ok(Some(ResponseRecord::SseData(block)))
503 } else {
504 if self.finite_body.is_some() {
505 return Err(Error::MalformedEnvelope);
506 }
507 self.crypto
508 .as_mut()
509 .ok_or(Error::MalformedEnvelope)?
510 .advance()?;
511 let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
512 self.finite_body = Some(compression::decode(
513 body[0],
514 &body[1..],
515 material.limits.max_body_len,
516 )?);
517 Ok(None)
518 }
519 }
520 END if body.is_empty() && length as usize == MIN_CIPHERTEXT_LEN => {
521 if head.mode == ResponseMode::Finite {
522 self.finish_finite()?;
523 }
524 self.done = true;
525 self.crypto = None;
526 Ok(Some(ResponseRecord::End))
527 }
528 _ => Err(Error::MalformedEnvelope),
529 }
530 }
531
532 fn finish_finite(&mut self) -> Result<(), Error> {
533 let head = self.head.as_ref().ok_or(Error::MalformedEnvelope)?;
534 let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
535 let body = self.finite_body.take().unwrap_or_default();
536 let response = Response {
537 status: head.status,
538 headers: head.headers.clone(),
539 body,
540 };
541 message::validate_decoded_response_body(&response, material.method)?;
542 if !response.body.is_empty()
543 && (material.method == Method::Head || matches!(response.status, 204 | 205 | 304))
544 {
545 return Err(Error::MalformedEnvelope);
546 }
547 self.finite_response = Some(response);
548 Ok(())
549 }
550
551 pub fn finish_eof(&mut self) -> Result<Option<Response>, Error> {
557 if self.failed || !self.done || self.eof_checked {
558 self.poison();
559 return Err(Error::MalformedEnvelope);
560 }
561 self.eof_checked = true;
562 self.material = None;
563 Ok(self.finite_response.take())
564 }
565
566 pub fn close(&mut self) {
568 self.poison();
569 }
570
571 fn poison(&mut self) {
572 self.crypto = None;
573 self.material = None;
574 self.frame = Vec::new();
575 self.finite_body = None;
576 self.finite_response = None;
577 self.failed = true;
578 }
579}
580
581const fn decode_error(error: Error) -> Error {
582 match error {
583 Error::InvalidConfiguration => Error::MalformedEnvelope,
584 other => other,
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn field(name: &[u8], value: &[u8]) -> HeaderField {
593 HeaderField {
594 name: name.to_vec(),
595 value: value.to_vec(),
596 }
597 }
598
599 fn clear_start(status: u16, headers: &[HeaderField]) -> Result<Vec<u8>, Error> {
600 let mut clear = status.to_be_bytes().to_vec();
601 clear.extend(message::encode_fields(headers)?);
602 Ok(clear)
603 }
604
605 fn assert_valid_tag_response_rejected(
606 name: &str,
607 method: Method,
608 records: Vec<(u8, Option<u8>, Vec<u8>)>,
609 ) -> Result<(), Error> {
610 let material = ResponseMaterial {
612 secret: zeroize::Zeroizing::new([7; 32]),
613 enc: [8; 32],
614 method,
615 limits: crate::Limits::default(),
616 };
617 let mut prefix = [0; PREFIX_LEN];
618 prefix[..4].copy_from_slice(MAGIC);
619 prefix[4] = VERSION;
620 let mut writer = Crypto::new(&material, prefix)?;
621 let mut wire = prefix.to_vec();
622 for (kind, coding, body) in records {
623 wire.extend(writer.seal_record(kind, coding, &body, kind == END)?);
624 }
625 let mut reader = ResponseOpener::new(material);
626 let mut offset = 0;
627 let mut error = None;
628 while offset < wire.len() {
629 match reader.feed(&wire[offset..]) {
630 Ok((used, _)) if used > 0 => offset += used,
631 Ok(_) => return Err(Error::MalformedEnvelope),
632 Err(failure) => {
633 error = Some(failure);
634 break;
635 }
636 }
637 }
638 assert_eq!(error, Some(Error::MalformedEnvelope), "{name}");
639 Ok(())
640 }
641
642 #[test]
643 fn opener_rejects_authenticated_invalid_clear_records() -> Result<(), Error> {
644 let duplicate_type = clear_start(
645 200,
646 &[
647 field(b"content-type", b"text/plain"),
648 field(b"content-type", b"text/event-stream"),
649 ],
650 )?;
651 let sse_type = field(b"content-type", b"text/event-stream");
652 let sse_start = clear_start(200, std::slice::from_ref(&sse_type))?;
653 let mismatched_length = clear_start(200, &[field(b"content-length", b"4")])?;
654 let nonzero_205_length = clear_start(205, &[field(b"content-length", b"1")])?;
655 let sse_bad_status = clear_start(201, std::slice::from_ref(&sse_type))?;
656 let sse_bad_coding =
657 clear_start(200, &[sse_type, field(b"content-encoding", b"identity")])?;
658 let cases = [
659 (
660 "duplicate Content-Type",
661 Method::Get,
662 vec![(START, None, duplicate_type), (END, None, vec![])],
663 ),
664 (
665 "second finite DATA",
666 Method::Get,
667 vec![
668 (START, None, vec![0, 200]),
669 (DATA, Some(compression::RAW), b"first".to_vec()),
670 (DATA, Some(compression::RAW), b"second".to_vec()),
671 (END, None, vec![]),
672 ],
673 ),
674 (
675 "incomplete SSE block",
676 Method::Get,
677 vec![
678 (START, None, sse_start.clone()),
679 (DATA, Some(compression::RAW), b"data: incomplete\n".to_vec()),
680 (END, None, vec![]),
681 ],
682 ),
683 (
684 "finite Content-Length mismatch",
685 Method::Get,
686 vec![
687 (START, None, mismatched_length),
688 (DATA, Some(compression::RAW), b"abc".to_vec()),
689 (END, None, vec![]),
690 ],
691 ),
692 (
693 "nonzero 205 Content-Length",
694 Method::Get,
695 vec![(START, None, nonzero_205_length), (END, None, vec![])],
696 ),
697 (
698 "SSE with wrong status",
699 Method::Get,
700 vec![(START, None, sse_bad_status), (END, None, vec![])],
701 ),
702 (
703 "SSE with Content-Encoding",
704 Method::Get,
705 vec![(START, None, sse_bad_coding), (END, None, vec![])],
706 ),
707 (
708 "SSE on HEAD",
709 Method::Head,
710 vec![(START, None, sse_start), (END, None, vec![])],
711 ),
712 ];
713
714 for (name, method, records) in cases {
715 assert_valid_tag_response_rejected(name, method, records)?;
716 }
717 Ok(())
718 }
719
720 #[test]
721 fn final_counter_value_is_reserved_for_end() -> Result<(), Error> {
722 let material = ResponseMaterial {
723 secret: zeroize::Zeroizing::new([7; 32]),
724 enc: [8; 32],
725 method: Method::Get,
726 limits: crate::Limits::default(),
727 };
728 let mut prefix = [0; PREFIX_LEN];
729 prefix[..4].copy_from_slice(MAGIC);
730 prefix[4] = VERSION;
731 let mut writer = Crypto::new(&material, prefix)?;
732 writer.sequence = u64::MAX - 1;
733 let data = writer.seal_record(DATA, None, b"x", false)?;
734 assert_eq!(writer.sequence, u64::MAX);
735 assert_eq!(
736 writer.seal_record(DATA, None, b"y", false),
737 Err(Error::LimitExceeded)
738 );
739 let end = writer.seal_record(END, None, &[], true)?;
740 let mut reader = Crypto::new(&material, prefix)?;
741 reader.sequence = u64::MAX - 1;
742 let mut data_body = data[4..].to_vec();
743 reader.open(
744 &mut data_body,
745 u32::from_be_bytes(data[..4].try_into().map_err(|_| Error::MalformedEnvelope)?),
746 )?;
747 assert_eq!(data_body, [DATA, b'x']);
748 reader.advance()?;
749 let mut end_body = end[4..].to_vec();
750 reader.open(
751 &mut end_body,
752 u32::from_be_bytes(end[..4].try_into().map_err(|_| Error::MalformedEnvelope)?),
753 )?;
754 assert_eq!(end_body, [END]);
755 assert_eq!(reader.advance(), Err(Error::LimitExceeded));
756 Ok(())
757 }
758}