Skip to main content

hpke_http/
response.rs

1//! Checked v3 response records. A caller supplies true transport EOF separately.
2
3use 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/// The checked response kind, based on its single Content-Type field.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum ResponseMode {
30    /// A bounded complete body follows, in zero or one DATA record.
31    Finite,
32    /// Each DATA record contains one complete SSE block.
33    Sse,
34}
35
36/// Fields released after a valid START tag.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ResponseHead {
39    /// Inner HTTP status.
40    pub status: u16,
41    /// Ordered checked fields.
42    pub headers: Vec<HeaderField>,
43    /// Response body kind.
44    pub mode: ResponseMode,
45}
46
47/// One checked record. Finite body bytes remain in the opener until END.
48#[derive(Debug, Eq, PartialEq)]
49pub enum ResponseRecord {
50    /// The first checked record.
51    Start(ResponseHead),
52    /// One checked complete SSE block.
53    SseData(Vec<u8>),
54    /// The checked terminal record. Transport EOF must still follow.
55    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
183/// One response writer. Its key and sequence cannot be reused after failure.
184pub 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    /// Return the checked response mode.
195    #[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    /// Protect one complete SSE block, including its final blank line.
238    ///
239    /// # Errors
240    /// Returns a shape, limit, or cryptographic error and closes the writer.
241    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    /// Protect the complete logical finite body. An empty body emits no DATA.
262    ///
263    /// # Errors
264    /// Returns a validation, limit, or cryptographic error and closes the writer.
265    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    /// Protect the terminal record. The caller then ends the outer HTTP body.
306    ///
307    /// # Errors
308    /// Returns an order or cryptographic error and closes the writer.
309    pub fn finish(&mut self) -> Result<Vec<u8>, Error> {
310        if self.done || (self.head.mode == ResponseMode::Finite && !self.finite_data_sent) {
311            self.poison();
312            return Err(Error::InvalidConfiguration);
313        }
314        let result = self
315            .crypto
316            .as_mut()
317            .ok_or(Error::InvalidConfiguration)?
318            .seal_record(END, None, &[], true);
319        self.poison();
320        result
321    }
322
323    /// Close this writer without a terminal record.
324    pub fn close(&mut self) {
325        self.poison();
326    }
327
328    fn poison(&mut self) {
329        self.crypto = None;
330        self.done = true;
331    }
332}
333
334/// A checked record reader that accepts any byte cuts.
335pub struct ResponseOpener {
336    material: Option<ResponseMaterial>,
337    crypto: Option<Crypto>,
338    prefix: Vec<u8>,
339    length: Vec<u8>,
340    frame: Vec<u8>,
341    expected: Option<usize>,
342    head: Option<ResponseHead>,
343    finite_body: Option<Vec<u8>>,
344    finite_response: Option<Response>,
345    done: bool,
346    failed: bool,
347    eof_checked: bool,
348}
349
350impl ResponseOpener {
351    pub(crate) fn new(material: ResponseMaterial) -> Self {
352        Self {
353            material: Some(material),
354            crypto: None,
355            prefix: Vec::with_capacity(PREFIX_LEN),
356            length: Vec::with_capacity(4),
357            frame: Vec::new(),
358            expected: None,
359            head: None,
360            finite_body: None,
361            finite_response: None,
362            done: false,
363            failed: false,
364            eof_checked: false,
365        }
366    }
367
368    /// Read at most one checked record and return how many input bytes were used.
369    /// Pass the unread suffix to the next call. Finite DATA stays inside the
370    /// reader and returns no public record.
371    ///
372    /// # Errors
373    /// Any parse, limit, order, or tag error closes this reader.
374    pub fn feed(&mut self, input: &[u8]) -> Result<(usize, Option<ResponseRecord>), Error> {
375        let result = self.feed_inner(input);
376        if result.is_err() {
377            self.poison();
378        }
379        result
380    }
381
382    fn feed_inner(&mut self, input: &[u8]) -> Result<(usize, Option<ResponseRecord>), Error> {
383        if self.failed || self.eof_checked || (self.done && !input.is_empty()) {
384            return Err(Error::MalformedEnvelope);
385        }
386        let mut used = 0;
387        if used < input.len() {
388            if self.crypto.is_none() {
389                let take = (PREFIX_LEN - self.prefix.len()).min(input.len() - used);
390                self.prefix.extend_from_slice(&input[used..used + take]);
391                used += take;
392                if self.prefix.len() < PREFIX_LEN {
393                    return Ok((used, None));
394                }
395                if self.prefix[..4] != *MAGIC || self.prefix[4] != VERSION {
396                    return Err(Error::MalformedEnvelope);
397                }
398                let prefix: [u8; PREFIX_LEN] = self
399                    .prefix
400                    .as_slice()
401                    .try_into()
402                    .map_err(|_| Error::MalformedEnvelope)?;
403                let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
404                self.crypto = Some(Crypto::new(material, prefix)?);
405            }
406            if self.expected.is_none() {
407                let take = (4 - self.length.len()).min(input.len() - used);
408                self.length.extend_from_slice(&input[used..used + take]);
409                used += take;
410                if self.length.len() < 4 {
411                    return Ok((used, None));
412                }
413                let length = u32::from_be_bytes(
414                    self.length
415                        .as_slice()
416                        .try_into()
417                        .map_err(|_| Error::MalformedEnvelope)?,
418                ) as usize;
419                let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
420                let maximum = if self.head.is_none() {
421                    4 + material.limits.max_header_bytes
422                        + material.limits.max_header_count * 16
423                        + TAG_LEN
424                } else {
425                    2 + material.limits.max_body_len + TAG_LEN
426                };
427                if length < MIN_CIPHERTEXT_LEN {
428                    return Err(Error::MalformedEnvelope);
429                }
430                if length > maximum {
431                    return Err(Error::LimitExceeded);
432                }
433                self.expected = Some(length);
434            }
435            let expected = self.expected.ok_or(Error::MalformedEnvelope)?;
436            let take = (expected - self.frame.len()).min(input.len() - used);
437            self.frame.extend_from_slice(&input[used..used + take]);
438            used += take;
439            if self.frame.len() < expected {
440                return Ok((used, None));
441            }
442            let record = self.open_frame()?;
443            if self.frame.capacity() > RETAIN_FRAME_CAPACITY {
444                self.frame = Vec::new();
445            } else {
446                self.frame.clear();
447            }
448            self.length.clear();
449            self.expected = None;
450            if self.done && used < input.len() {
451                return Err(Error::MalformedEnvelope);
452            }
453            return Ok((used, record));
454        }
455        Ok((used, None))
456    }
457
458    fn open_frame(&mut self) -> Result<Option<ResponseRecord>, Error> {
459        let length = u32::try_from(self.frame.len()).map_err(|_| Error::LimitExceeded)?;
460        self.crypto
461            .as_mut()
462            .ok_or(Error::MalformedEnvelope)?
463            .open(&mut self.frame, length)?;
464        let (&kind, body) = self.frame.split_first().ok_or(Error::MalformedEnvelope)?;
465        if self.head.is_none() {
466            if kind != START || body.len() < 2 {
467                return Err(Error::MalformedEnvelope);
468            }
469            let status = u16::from_be_bytes([body[0], body[1]]);
470            let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
471            let headers =
472                message::decode_fields(&body[2..], material.limits).map_err(decode_error)?;
473            let mode = classify_head(status, &headers, material).map_err(decode_error)?;
474            let head = ResponseHead {
475                status,
476                headers,
477                mode,
478            };
479            self.head = Some(head.clone());
480            self.crypto
481                .as_mut()
482                .ok_or(Error::MalformedEnvelope)?
483                .advance()?;
484            return Ok(Some(ResponseRecord::Start(head)));
485        }
486        let head = self.head.as_ref().ok_or(Error::MalformedEnvelope)?;
487        match kind {
488            DATA => {
489                if body.len() < 2 {
490                    return Err(Error::MalformedEnvelope);
491                }
492                if head.mode == ResponseMode::Sse {
493                    let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
494                    let block =
495                        compression::decode(body[0], &body[1..], material.limits.max_body_len)?;
496                    validate_block(&block, material.limits.max_body_len)?;
497                    self.crypto
498                        .as_mut()
499                        .ok_or(Error::MalformedEnvelope)?
500                        .advance()?;
501                    Ok(Some(ResponseRecord::SseData(block)))
502                } else {
503                    if self.finite_body.is_some() {
504                        return Err(Error::MalformedEnvelope);
505                    }
506                    self.crypto
507                        .as_mut()
508                        .ok_or(Error::MalformedEnvelope)?
509                        .advance()?;
510                    let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
511                    self.finite_body = Some(compression::decode(
512                        body[0],
513                        &body[1..],
514                        material.limits.max_body_len,
515                    )?);
516                    Ok(None)
517                }
518            }
519            END if body.is_empty() && length as usize == MIN_CIPHERTEXT_LEN => {
520                if head.mode == ResponseMode::Finite {
521                    self.finish_finite()?;
522                }
523                self.done = true;
524                self.crypto = None;
525                Ok(Some(ResponseRecord::End))
526            }
527            _ => Err(Error::MalformedEnvelope),
528        }
529    }
530
531    fn finish_finite(&mut self) -> Result<(), Error> {
532        let head = self.head.as_ref().ok_or(Error::MalformedEnvelope)?;
533        let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
534        let body = self.finite_body.take().unwrap_or_default();
535        let response = Response {
536            status: head.status,
537            headers: head.headers.clone(),
538            body,
539        };
540        message::validate_decoded_response_body(&response, material.method)?;
541        if !response.body.is_empty()
542            && (material.method == Method::Head || matches!(response.status, 204 | 205 | 304))
543        {
544            return Err(Error::MalformedEnvelope);
545        }
546        self.finite_response = Some(response);
547        Ok(())
548    }
549
550    /// Confirm true outer body EOF after END and release a complete finite reply.
551    ///
552    /// # Errors
553    /// Returns an error if END is absent, the reader failed, or EOF was checked twice.
554    pub fn finish_eof(&mut self) -> Result<Option<Response>, Error> {
555        if self.failed || !self.done || self.eof_checked {
556            self.poison();
557            return Err(Error::MalformedEnvelope);
558        }
559        self.eof_checked = true;
560        self.material = None;
561        Ok(self.finite_response.take())
562    }
563
564    /// Close this reader without claiming a complete response.
565    pub fn close(&mut self) {
566        self.poison();
567    }
568
569    fn poison(&mut self) {
570        self.crypto = None;
571        self.material = None;
572        self.frame = Vec::new();
573        self.finite_body = None;
574        self.finite_response = None;
575        self.failed = true;
576    }
577}
578
579const fn decode_error(error: Error) -> Error {
580    match error {
581        Error::InvalidConfiguration => Error::MalformedEnvelope,
582        other => other,
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    #[test]
591    fn final_counter_value_is_reserved_for_end() -> Result<(), Error> {
592        let material = ResponseMaterial {
593            secret: zeroize::Zeroizing::new([7; 32]),
594            enc: [8; 32],
595            method: Method::Get,
596            limits: crate::Limits::default(),
597        };
598        let mut prefix = [0; PREFIX_LEN];
599        prefix[..4].copy_from_slice(MAGIC);
600        prefix[4] = VERSION;
601        let mut writer = Crypto::new(&material, prefix)?;
602        writer.sequence = u64::MAX - 1;
603        let data = writer.seal_record(DATA, None, b"x", false)?;
604        assert_eq!(writer.sequence, u64::MAX);
605        assert_eq!(
606            writer.seal_record(DATA, None, b"y", false),
607            Err(Error::LimitExceeded)
608        );
609        let end = writer.seal_record(END, None, &[], true)?;
610        let mut reader = Crypto::new(&material, prefix)?;
611        reader.sequence = u64::MAX - 1;
612        let mut data_body = data[4..].to_vec();
613        reader.open(
614            &mut data_body,
615            u32::from_be_bytes(data[..4].try_into().map_err(|_| Error::MalformedEnvelope)?),
616        )?;
617        assert_eq!(data_body, [DATA, b'x']);
618        reader.advance()?;
619        let mut end_body = end[4..].to_vec();
620        reader.open(
621            &mut end_body,
622            u32::from_be_bytes(end[..4].try_into().map_err(|_| Error::MalformedEnvelope)?),
623        )?;
624        assert_eq!(end_body, [END]);
625        assert_eq!(reader.advance(), Err(Error::LimitExceeded));
626        Ok(())
627    }
628}