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 END. In finite mode, call `seal_finite_body` once first, even
306    /// for an empty body. The caller then ends the outer HTTP body.
307    ///
308    /// # Errors
309    /// Returns an order or cryptographic error and closes the writer.
310    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    /// Close this writer without a terminal record.
325    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
335/// A checked record reader that accepts any byte cuts.
336pub 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    /// Read at most one checked record and return how many input bytes were used.
370    /// Pass the unread suffix to the next call. Finite DATA stays inside the
371    /// reader and returns no public record.
372    ///
373    /// # Errors
374    /// Any parse, limit, order, or tag error closes this reader.
375    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    /// Call after the host observes real outer body EOF. Check END and release
552    /// a finite reply, if any. This method cannot observe transport EOF itself.
553    ///
554    /// # Errors
555    /// Returns an error if END is absent, the reader failed, or EOF was checked twice.
556    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    /// Close this reader without claiming a complete response.
567    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        // Use the record crypto directly so invalid clear bytes still have valid tags.
611        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}