Skip to main content

hpke_http/
response.rs

1//! Checked v2 response records. A caller supplies true transport EOF separately.
2
3use std::borrow::Cow;
4
5use chacha20poly1305::{
6    ChaCha20Poly1305,
7    aead::{AeadInOut, KeyInit, array::Array},
8};
9
10use crate::{
11    CompressionCoding, EntropySource, Error, HeaderField, Method, Response,
12    codec::{RESPONSE_NONCE_LEN, TAG_LEN, VERSION},
13    compression,
14    engine::{ResponseMaterial, derive_response_key_nonce},
15    message,
16    sse::validate_block,
17};
18
19const MAGIC: &[u8; 4] = b"HHRP";
20const PREFIX_LEN: usize = 4 + 1 + RESPONSE_NONCE_LEN;
21const AAD_DOMAIN: &[u8] = b"hpke-http/2 response record\0";
22const AAD_LEN: usize = AAD_DOMAIN.len() + PREFIX_LEN + 8 + 4;
23const START: u8 = 1;
24const DATA: u8 = 2;
25const END: u8 = 3;
26const MIN_CIPHERTEXT_LEN: usize = 1 + TAG_LEN;
27
28/// The checked response kind, based on its single Content-Type field.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum ResponseMode {
31    /// A bounded complete body follows, in zero or one DATA record.
32    Finite,
33    /// Each DATA record contains one complete SSE block.
34    Sse,
35}
36
37/// Fields released after a valid START tag.
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct ResponseHead {
40    /// Inner HTTP status.
41    pub status: u16,
42    /// Ordered checked fields.
43    pub headers: Vec<HeaderField>,
44    /// Response body kind.
45    pub mode: ResponseMode,
46    /// Private body coding. SSE always uses identity.
47    pub coding: Option<CompressionCoding>,
48}
49
50/// One checked record. Finite body bytes remain in the opener until END.
51#[derive(Debug, Eq, PartialEq)]
52pub enum ResponseRecord {
53    /// The first checked record.
54    Start(ResponseHead),
55    /// One checked complete SSE block.
56    SseData(Vec<u8>),
57    /// The checked terminal record. Transport EOF must still follow.
58    End,
59}
60
61struct Crypto {
62    key: zeroize::Zeroizing<[u8; 32]>,
63    base_nonce: zeroize::Zeroizing<[u8; 12]>,
64    prefix: [u8; PREFIX_LEN],
65    sequence: u64,
66}
67
68impl Crypto {
69    fn new(material: &ResponseMaterial, prefix: [u8; PREFIX_LEN]) -> Result<Self, Error> {
70        let (key, base_nonce) = derive_response_key_nonce(material, &prefix[5..])?;
71        Ok(Self {
72            key,
73            base_nonce,
74            prefix,
75            sequence: 0,
76        })
77    }
78
79    fn nonce(&self) -> [u8; 12] {
80        let mut nonce = *self.base_nonce;
81        for (target, byte) in nonce[4..].iter_mut().zip(self.sequence.to_be_bytes()) {
82            *target ^= byte;
83        }
84        nonce
85    }
86
87    fn aad(&self, ciphertext_len: u32) -> [u8; AAD_LEN] {
88        let mut aad = [0; AAD_LEN];
89        let prefix_end = AAD_DOMAIN.len() + PREFIX_LEN;
90        aad[..AAD_DOMAIN.len()].copy_from_slice(AAD_DOMAIN);
91        aad[AAD_DOMAIN.len()..prefix_end].copy_from_slice(&self.prefix);
92        aad[prefix_end..prefix_end + 8].copy_from_slice(&self.sequence.to_be_bytes());
93        aad[prefix_end + 8..].copy_from_slice(&ciphertext_len.to_be_bytes());
94        aad
95    }
96
97    fn seal_record(&mut self, kind: u8, body: &[u8], terminal: bool) -> Result<Vec<u8>, Error> {
98        if self.sequence == u64::MAX && !terminal {
99            return Err(Error::LimitExceeded);
100        }
101        let length = body
102            .len()
103            .checked_add(1)
104            .and_then(|size| size.checked_add(TAG_LEN))
105            .ok_or(Error::LimitExceeded)?;
106        let length = u32::try_from(length).map_err(|_| Error::LimitExceeded)?;
107        let frame_len = 4_usize
108            .checked_add(length as usize)
109            .ok_or(Error::LimitExceeded)?;
110        let mut framed = Vec::with_capacity(frame_len);
111        framed.extend_from_slice(&length.to_be_bytes());
112        framed.push(kind);
113        framed.extend_from_slice(body);
114        let cipher = ChaCha20Poly1305::new(&Array(*self.key));
115        let tag = cipher
116            .encrypt_inout_detached(
117                &Array(self.nonce()),
118                &self.aad(length),
119                (&mut framed[4..]).into(),
120            )
121            .map_err(|_| Error::CryptoFailure)?;
122        framed.extend_from_slice(&tag);
123        if !terminal {
124            self.sequence += 1;
125        }
126        Ok(framed)
127    }
128
129    fn open(&mut self, ciphertext: &mut Vec<u8>, length: u32) -> Result<(), Error> {
130        let cipher = ChaCha20Poly1305::new(&Array(*self.key));
131        cipher
132            .decrypt_in_place(&Array(self.nonce()), &self.aad(length), ciphertext)
133            .map_err(|_| Error::AuthenticationFailed)
134    }
135
136    fn advance(&mut self) -> Result<(), Error> {
137        self.sequence = self.sequence.checked_add(1).ok_or(Error::LimitExceeded)?;
138        Ok(())
139    }
140}
141
142fn classify_head(
143    status: u16,
144    headers: &[HeaderField],
145    coding: Option<CompressionCoding>,
146    material: &ResponseMaterial,
147) -> Result<ResponseMode, Error> {
148    message::validate_response_head(status, headers, material.limits)?;
149    let mut content_type = None;
150    for field in headers {
151        if field.name == b"content-type" && content_type.replace(field.value.as_slice()).is_some() {
152            return Err(Error::InvalidConfiguration);
153        }
154    }
155    let is_sse = content_type.is_some_and(|value| {
156        value
157            .split(|byte| *byte == b';')
158            .next()
159            .unwrap_or_default()
160            .trim_ascii()
161            .eq_ignore_ascii_case(b"text/event-stream")
162    });
163    if is_sse {
164        if status != 200
165            || material.method == Method::Head
166            || coding.is_some()
167            || headers
168                .iter()
169                .any(|field| field.name == b"content-length" || field.name == b"content-encoding")
170        {
171            return Err(Error::InvalidConfiguration);
172        }
173        Ok(ResponseMode::Sse)
174    } else if coding.is_some() && coding != material.compression {
175        Err(Error::InvalidConfiguration)
176    } else {
177        Ok(ResponseMode::Finite)
178    }
179}
180
181/// One response writer. Its key and sequence cannot be reused after failure.
182pub struct ResponseSealer {
183    crypto: Option<Crypto>,
184    head: ResponseHead,
185    method: Method,
186    limits: crate::Limits,
187    finite_data_sent: bool,
188    done: bool,
189}
190
191impl ResponseSealer {
192    /// Return the checked response mode.
193    #[must_use]
194    pub const fn head_mode(&self) -> ResponseMode {
195        self.head.mode
196    }
197
198    pub(crate) fn new(
199        material: &ResponseMaterial,
200        status: u16,
201        headers: Vec<HeaderField>,
202        coding: Option<CompressionCoding>,
203        entropy: &mut impl EntropySource,
204    ) -> Result<(Self, Vec<u8>), Error> {
205        let mode = classify_head(status, &headers, coding, material)?;
206        let mut prefix = [0_u8; PREFIX_LEN];
207        prefix[..4].copy_from_slice(MAGIC);
208        prefix[4] = VERSION;
209        entropy.fill(&mut prefix[5..])?;
210        let mut crypto = Crypto::new(material, prefix)?;
211        let fields = message::encode_fields(&headers)?;
212        let mut start = Vec::with_capacity(3 + fields.len());
213        start.extend_from_slice(&status.to_be_bytes());
214        start.push(coding.map_or(0, CompressionCoding::wire_id));
215        start.extend_from_slice(&fields);
216        let frame = crypto.seal_record(START, &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                    coding,
228                },
229                method: material.method,
230                limits: material.limits,
231                finite_data_sent: false,
232                done: false,
233            },
234            output,
235        ))
236    }
237
238    /// Protect one complete SSE block, including its final blank line.
239    ///
240    /// # Errors
241    /// Returns a shape, limit, or cryptographic error and closes the writer.
242    pub fn seal_sse_block(&mut self, block: &[u8]) -> Result<Vec<u8>, Error> {
243        let result = self.seal_sse_block_inner(block);
244        if result.is_err() {
245            self.poison();
246        }
247        result
248    }
249
250    fn seal_sse_block_inner(&mut self, block: &[u8]) -> Result<Vec<u8>, Error> {
251        if self.done || self.head.mode != ResponseMode::Sse {
252            return Err(Error::InvalidConfiguration);
253        }
254        validate_block(block, self.limits.max_body_len)?;
255        self.crypto
256            .as_mut()
257            .ok_or(Error::InvalidConfiguration)?
258            .seal_record(DATA, block, false)
259    }
260
261    /// Protect the complete logical finite body. An empty body emits no DATA.
262    ///
263    /// # Errors
264    /// Returns a validation, limit, compression, or cryptographic error and closes the writer.
265    pub fn seal_finite_body(&mut self, body: &[u8]) -> Result<Option<Vec<u8>>, Error> {
266        self.seal_finite_body_with_precompressed(body, None)
267    }
268
269    pub(crate) fn seal_finite_body_with_precompressed(
270        &mut self,
271        body: &[u8],
272        precompressed: Option<&[u8]>,
273    ) -> Result<Option<Vec<u8>>, Error> {
274        let result = self.seal_finite_body_inner(body, precompressed);
275        if result.is_err() {
276            self.poison();
277        }
278        result
279    }
280
281    fn seal_finite_body_inner(
282        &mut self,
283        body: &[u8],
284        precompressed: Option<&[u8]>,
285    ) -> Result<Option<Vec<u8>>, Error> {
286        if self.done || self.head.mode != ResponseMode::Finite || self.finite_data_sent {
287            return Err(Error::InvalidConfiguration);
288        }
289        if body.len() > self.limits.max_body_len {
290            return Err(Error::LimitExceeded);
291        }
292        message::validate_response_content_length(
293            self.head.status,
294            &self.head.headers,
295            body.len(),
296            self.method,
297        )?;
298        if !body.is_empty()
299            && (self.method == Method::Head || matches!(self.head.status, 204 | 205 | 304))
300        {
301            return Err(Error::InvalidConfiguration);
302        }
303        if body.is_empty() {
304            if self.head.coding.is_some() || precompressed.is_some() {
305                return Err(Error::InvalidConfiguration);
306            }
307            self.finite_data_sent = true;
308            return Ok(None);
309        }
310        let encoded = match (self.head.coding, precompressed) {
311            (Some(_), Some(encoded)) => Cow::Borrowed(encoded),
312            (Some(coding), None) => Cow::Owned(compression::compress(body, coding)?),
313            (None, None) => Cow::Borrowed(body),
314            (None, Some(_)) => return Err(Error::InvalidConfiguration),
315        };
316        if encoded.len() > self.limits.max_body_len {
317            return Err(Error::LimitExceeded);
318        }
319        let frame = self
320            .crypto
321            .as_mut()
322            .ok_or(Error::InvalidConfiguration)?
323            .seal_record(DATA, &encoded, false)?;
324        self.finite_data_sent = true;
325        Ok(Some(frame))
326    }
327
328    /// Protect the terminal record. The caller then ends the outer HTTP body.
329    ///
330    /// # Errors
331    /// Returns an order or cryptographic error and closes the writer.
332    pub fn finish(&mut self) -> Result<Vec<u8>, Error> {
333        if self.done || (self.head.mode == ResponseMode::Finite && !self.finite_data_sent) {
334            self.poison();
335            return Err(Error::InvalidConfiguration);
336        }
337        let result = self
338            .crypto
339            .as_mut()
340            .ok_or(Error::InvalidConfiguration)?
341            .seal_record(END, &[], true);
342        self.poison();
343        result
344    }
345
346    /// Close this writer without a terminal record.
347    pub fn close(&mut self) {
348        self.poison();
349    }
350
351    fn poison(&mut self) {
352        self.crypto = None;
353        self.done = true;
354    }
355}
356
357/// A checked record reader that accepts any byte cuts.
358pub struct ResponseOpener {
359    material: Option<ResponseMaterial>,
360    crypto: Option<Crypto>,
361    prefix: Vec<u8>,
362    length: Vec<u8>,
363    frame: Vec<u8>,
364    expected: Option<usize>,
365    head: Option<ResponseHead>,
366    finite_body: Option<Vec<u8>>,
367    finite_response: Option<Response>,
368    done: bool,
369    failed: bool,
370    eof_checked: bool,
371}
372
373impl ResponseOpener {
374    pub(crate) fn new(material: ResponseMaterial) -> Self {
375        Self {
376            material: Some(material),
377            crypto: None,
378            prefix: Vec::with_capacity(PREFIX_LEN),
379            length: Vec::with_capacity(4),
380            frame: Vec::new(),
381            expected: None,
382            head: None,
383            finite_body: None,
384            finite_response: None,
385            done: false,
386            failed: false,
387            eof_checked: false,
388        }
389    }
390
391    /// Read at most one checked record and return how many input bytes were used.
392    /// Finite DATA stays inside the reader and returns no public record.
393    ///
394    /// # Errors
395    /// Any parse, limit, order, or tag error closes this reader.
396    pub fn feed(&mut self, input: &[u8]) -> Result<(usize, Option<ResponseRecord>), Error> {
397        let result = self.feed_inner(input);
398        if result.is_err() {
399            self.poison();
400        }
401        result
402    }
403
404    fn feed_inner(&mut self, input: &[u8]) -> Result<(usize, Option<ResponseRecord>), Error> {
405        if self.failed || self.eof_checked || (self.done && !input.is_empty()) {
406            return Err(Error::MalformedEnvelope);
407        }
408        let mut used = 0;
409        if used < input.len() {
410            if self.crypto.is_none() {
411                let take = (PREFIX_LEN - self.prefix.len()).min(input.len() - used);
412                self.prefix.extend_from_slice(&input[used..used + take]);
413                used += take;
414                if self.prefix.len() < PREFIX_LEN {
415                    return Ok((used, None));
416                }
417                if self.prefix[..4] != *MAGIC || self.prefix[4] != VERSION {
418                    return Err(Error::MalformedEnvelope);
419                }
420                let prefix: [u8; PREFIX_LEN] = self
421                    .prefix
422                    .as_slice()
423                    .try_into()
424                    .map_err(|_| Error::MalformedEnvelope)?;
425                let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
426                self.crypto = Some(Crypto::new(material, prefix)?);
427            }
428            if self.expected.is_none() {
429                let take = (4 - self.length.len()).min(input.len() - used);
430                self.length.extend_from_slice(&input[used..used + take]);
431                used += take;
432                if self.length.len() < 4 {
433                    return Ok((used, None));
434                }
435                let length = u32::from_be_bytes(
436                    self.length
437                        .as_slice()
438                        .try_into()
439                        .map_err(|_| Error::MalformedEnvelope)?,
440                ) as usize;
441                let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
442                let maximum = if self.head.is_none() {
443                    4 + material.limits.max_header_bytes
444                        + material.limits.max_header_count * 16
445                        + TAG_LEN
446                } else {
447                    1 + material.limits.max_body_len + TAG_LEN
448                };
449                if length < MIN_CIPHERTEXT_LEN {
450                    return Err(Error::MalformedEnvelope);
451                }
452                if length > maximum {
453                    return Err(Error::LimitExceeded);
454                }
455                self.expected = Some(length);
456            }
457            let expected = self.expected.ok_or(Error::MalformedEnvelope)?;
458            let take = (expected - self.frame.len()).min(input.len() - used);
459            self.frame.extend_from_slice(&input[used..used + take]);
460            used += take;
461            if self.frame.len() < expected {
462                return Ok((used, None));
463            }
464            let record = self.open_frame()?;
465            self.frame = Vec::new();
466            self.length.clear();
467            self.expected = None;
468            if self.done && used < input.len() {
469                return Err(Error::MalformedEnvelope);
470            }
471            return Ok((used, record));
472        }
473        Ok((used, None))
474    }
475
476    fn open_frame(&mut self) -> Result<Option<ResponseRecord>, Error> {
477        let length = u32::try_from(self.frame.len()).map_err(|_| Error::LimitExceeded)?;
478        self.crypto
479            .as_mut()
480            .ok_or(Error::MalformedEnvelope)?
481            .open(&mut self.frame, length)?;
482        let (&kind, body) = self.frame.split_first().ok_or(Error::MalformedEnvelope)?;
483        if self.head.is_none() {
484            if kind != START || body.len() < 3 {
485                return Err(Error::MalformedEnvelope);
486            }
487            let status = u16::from_be_bytes([body[0], body[1]]);
488            let coding = CompressionCoding::from_wire_id(body[2])?;
489            let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
490            let headers =
491                message::decode_fields(&body[3..], material.limits).map_err(decode_error)?;
492            let mode = classify_head(status, &headers, coding, material).map_err(decode_error)?;
493            let head = ResponseHead {
494                status,
495                headers,
496                mode,
497                coding,
498            };
499            self.head = Some(head.clone());
500            self.crypto
501                .as_mut()
502                .ok_or(Error::MalformedEnvelope)?
503                .advance()?;
504            return Ok(Some(ResponseRecord::Start(head)));
505        }
506        let head = self.head.as_ref().ok_or(Error::MalformedEnvelope)?;
507        match kind {
508            DATA => {
509                if body.is_empty() {
510                    return Err(Error::MalformedEnvelope);
511                }
512                if head.mode == ResponseMode::Sse {
513                    let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
514                    validate_block(body, material.limits.max_body_len)?;
515                    self.crypto
516                        .as_mut()
517                        .ok_or(Error::MalformedEnvelope)?
518                        .advance()?;
519                    let body_len = body.len();
520                    self.frame.copy_within(1.., 0);
521                    self.frame.truncate(body_len);
522                    Ok(Some(ResponseRecord::SseData(std::mem::take(
523                        &mut self.frame,
524                    ))))
525                } else {
526                    if self.finite_body.is_some() {
527                        return Err(Error::MalformedEnvelope);
528                    }
529                    self.crypto
530                        .as_mut()
531                        .ok_or(Error::MalformedEnvelope)?
532                        .advance()?;
533                    let body_len = body.len();
534                    self.frame.copy_within(1.., 0);
535                    self.frame.truncate(body_len);
536                    self.finite_body = Some(std::mem::take(&mut self.frame));
537                    Ok(None)
538                }
539            }
540            END if body.is_empty() && length as usize == MIN_CIPHERTEXT_LEN => {
541                if head.mode == ResponseMode::Finite {
542                    self.finish_finite()?;
543                }
544                self.done = true;
545                self.crypto = None;
546                Ok(Some(ResponseRecord::End))
547            }
548            _ => Err(Error::MalformedEnvelope),
549        }
550    }
551
552    fn finish_finite(&mut self) -> Result<(), Error> {
553        let head = self.head.as_ref().ok_or(Error::MalformedEnvelope)?;
554        let material = self.material.as_ref().ok_or(Error::MalformedEnvelope)?;
555        let encoded = self.finite_body.take().unwrap_or_default();
556        let body = if let Some(coding) = head.coding {
557            compression::decompress(&encoded, coding, material.limits.max_body_len)?
558        } else {
559            encoded
560        };
561        let response = Response {
562            status: head.status,
563            headers: head.headers.clone(),
564            body,
565        };
566        message::validate_decoded_response_body(&response, material.method)?;
567        if !response.body.is_empty()
568            && (material.method == Method::Head || matches!(response.status, 204 | 205 | 304))
569        {
570            return Err(Error::MalformedEnvelope);
571        }
572        self.finite_response = Some(response);
573        Ok(())
574    }
575
576    /// Confirm true outer body EOF after END and release a complete finite reply.
577    ///
578    /// # Errors
579    /// Returns an error if END is absent, the reader failed, or EOF was checked twice.
580    pub fn finish_eof(&mut self) -> Result<Option<Response>, Error> {
581        if self.failed || !self.done || self.eof_checked {
582            self.poison();
583            return Err(Error::MalformedEnvelope);
584        }
585        self.eof_checked = true;
586        self.material = None;
587        Ok(self.finite_response.take())
588    }
589
590    /// Close this reader without claiming a complete response.
591    pub fn close(&mut self) {
592        self.poison();
593    }
594
595    fn poison(&mut self) {
596        self.crypto = None;
597        self.material = None;
598        self.frame = Vec::new();
599        self.finite_body = None;
600        self.finite_response = None;
601        self.failed = true;
602    }
603}
604
605const fn decode_error(error: Error) -> Error {
606    match error {
607        Error::InvalidConfiguration => Error::MalformedEnvelope,
608        other => other,
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    #[test]
617    fn final_counter_value_is_reserved_for_end() -> Result<(), Error> {
618        let material = ResponseMaterial {
619            secret: zeroize::Zeroizing::new([7; 32]),
620            enc: [8; 32],
621            method: Method::Get,
622            limits: crate::Limits::default(),
623            compression: None,
624        };
625        let mut prefix = [0; PREFIX_LEN];
626        prefix[..4].copy_from_slice(MAGIC);
627        prefix[4] = VERSION;
628        let mut writer = Crypto::new(&material, prefix)?;
629        writer.sequence = u64::MAX - 1;
630        let data = writer.seal_record(DATA, b"x", false)?;
631        assert_eq!(writer.sequence, u64::MAX);
632        assert_eq!(
633            writer.seal_record(DATA, b"y", false),
634            Err(Error::LimitExceeded)
635        );
636        let end = writer.seal_record(END, &[], true)?;
637        let mut reader = Crypto::new(&material, prefix)?;
638        reader.sequence = u64::MAX - 1;
639        let mut data_body = data[4..].to_vec();
640        reader.open(
641            &mut data_body,
642            u32::from_be_bytes(data[..4].try_into().map_err(|_| Error::MalformedEnvelope)?),
643        )?;
644        assert_eq!(data_body, [DATA, b'x']);
645        reader.advance()?;
646        let mut end_body = end[4..].to_vec();
647        reader.open(
648            &mut end_body,
649            u32::from_be_bytes(end[..4].try_into().map_err(|_| Error::MalformedEnvelope)?),
650        )?;
651        assert_eq!(end_body, [END]);
652        assert_eq!(reader.advance(), Err(Error::LimitExceeded));
653        Ok(())
654    }
655}