Skip to main content

hpke_http/
engine.rs

1//! Typed one-shot client and staged server transactions.
2
3use std::{
4    fmt,
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use hkdf::Hkdf;
9use hpke::{
10    Deserializable, Kem as KemTrait, OpModeR, OpModeS, PskBundle, Serializable,
11    aead::{AeadCtxR, AeadCtxS, AeadTag, ChaCha20Poly1305 as HpkeChaCha20Poly1305},
12    inout::InOutBuf,
13    kdf::HkdfSha256,
14    kem::X25519HkdfSha256,
15    setup_receiver, setup_sender_with_rng,
16};
17use rand_chacha::ChaCha20Rng;
18use rand_core::SeedableRng;
19use sha2::{Digest, Sha256};
20use zeroize::Zeroizing;
21
22use crate::{
23    CLOCK_SKEW_SECS, EntropySource, Error, HARD_MAX_ID_LEN, Limits, Method, REQUEST_LIFETIME_SECS,
24    SystemEntropy,
25    codec::{
26        ENC_LEN, REQUEST_FIXED_LEN, RESPONSE_NONCE_LEN, TAG_LEN, encode_request_header,
27        parse_stream_start, stream_start_length,
28    },
29    compression,
30    message::{self, HeaderField, Request, RequestHead, Response},
31    response::{ResponseMode, ResponseOpener, ResponseRecord, ResponseSealer},
32};
33
34type Kem = X25519HkdfSha256;
35type Kdf = HkdfSha256;
36type HpkeAead = HpkeChaCha20Poly1305;
37type RecipientContext = AeadCtxR<HpkeAead, Kdf, Kem>;
38type SenderContext = AeadCtxS<HpkeAead, Kdf, Kem>;
39
40const STREAM_REQUEST_INFO_LABEL: &[u8] = b"message/hpke-http request\0v3\0";
41const STREAM_REQUEST_AAD_LABEL: &[u8] = b"hpke-http/3 request record\0";
42const STREAM_START: u8 = 1;
43const STREAM_DATA: u8 = 2;
44const STREAM_END: u8 = 3;
45/// Largest clear DATA part in a protected upload.
46pub const STREAM_DATA_LEN: usize = 64 * 1024;
47/// Maximum DATA records, including small writes.
48const STREAM_MAX_DATA_RECORDS: u64 = 1_048_576;
49const RESPONSE_EXPORT_LABEL: &[u8] = b"message/hpke-http response\0v3";
50const REPLAY_LABEL: &[u8] = b"hpke-http/replay\0v3\0";
51const RESPONSE_KEY_INFO: &[u8] = b"hpke-http/3 response key";
52const RESPONSE_NONCE_INFO: &[u8] = b"hpke-http/3 response nonce";
53const RESPONSE_SECRET_LEN: usize = 32;
54const RESPONSE_KEY_LEN: usize = 32;
55const AEAD_NONCE_LEN: usize = 12;
56const MIN_PSK_LEN: usize = 32;
57pub(crate) type ResponseKey = Zeroizing<[u8; RESPONSE_KEY_LEN]>;
58pub(crate) type ResponseNonce = Zeroizing<[u8; AEAD_NONCE_LEN]>;
59
60/// A generated X25519 recipient key pair.
61pub struct KeyPair {
62    private_key: Zeroizing<Vec<u8>>,
63    public_key: Vec<u8>,
64}
65
66impl fmt::Debug for KeyPair {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        formatter
69            .debug_struct("KeyPair")
70            .field("private_key", &"[REDACTED]")
71            .field("public_key_len", &self.public_key.len())
72            .finish()
73    }
74}
75
76impl KeyPair {
77    /// Return the encoded private and public keys.
78    #[must_use]
79    pub fn into_parts(self) -> (Zeroizing<Vec<u8>>, Vec<u8>) {
80        (self.private_key, self.public_key)
81    }
82
83    /// Borrow the encoded public key.
84    #[must_use]
85    pub fn public_key(&self) -> &[u8] {
86        &self.public_key
87    }
88}
89
90/// Generate a recipient key pair with the platform entropy source.
91///
92/// # Errors
93///
94/// Returns [`Error::EntropyUnavailable`] if the platform source fails.
95pub fn generate_key_pair() -> Result<KeyPair, Error> {
96    generate_key_pair_with_entropy(&mut SystemEntropy)
97}
98
99/// Generate a recipient key pair with an injected fallible entropy source.
100///
101/// # Errors
102///
103/// Returns the error reported by `entropy` before producing a key pair.
104pub fn generate_key_pair_with_entropy(entropy: &mut impl EntropySource) -> Result<KeyPair, Error> {
105    let mut input_key_material = Zeroizing::new([0_u8; 32]);
106    entropy.fill(input_key_material.as_mut())?;
107    let (private_key, public_key) = Kem::derive_keypair(input_key_material.as_ref());
108    Ok(KeyPair {
109        private_key: Zeroizing::new(private_key.to_bytes().to_vec()),
110        public_key: public_key.to_bytes().to_vec(),
111    })
112}
113
114/// A request envelope and the one-use capability needed to open its response.
115#[must_use]
116pub struct ProtectedRequest {
117    envelope: Vec<u8>,
118    response_token: ResponseToken,
119}
120
121impl fmt::Debug for ProtectedRequest {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        formatter
124            .debug_struct("ProtectedRequest")
125            .field("envelope_len", &self.envelope.len())
126            .field("response_token", &self.response_token)
127            .finish()
128    }
129}
130
131impl ProtectedRequest {
132    /// Borrow the complete protected request envelope.
133    #[must_use]
134    pub fn envelope(&self) -> &[u8] {
135        &self.envelope
136    }
137
138    /// Split the envelope from its one-use response token.
139    pub fn into_parts(self) -> (Vec<u8>, ResponseToken) {
140        (self.envelope, self.response_token)
141    }
142}
143
144/// A client configured for one recipient key and one explicit PSK identity.
145pub struct Client {
146    recipient_public_key: <Kem as KemTrait>::PublicKey,
147    recipient_key_id: Vec<u8>,
148    psk: Zeroizing<Vec<u8>>,
149    psk_id: Vec<u8>,
150    limits: Limits,
151}
152
153impl fmt::Debug for Client {
154    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
155        formatter
156            .debug_struct("Client")
157            .field("recipient_key_id_len", &self.recipient_key_id.len())
158            .field("psk", &"[REDACTED]")
159            .field("psk_id_len", &self.psk_id.len())
160            .field("limits", &self.limits)
161            .finish_non_exhaustive()
162    }
163}
164
165impl Client {
166    /// Validate and create a client. The public PSK ID cannot equal the PSK.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`Error::InvalidConfiguration`] for invalid keys, identifiers,
171    /// credentials, or limits.
172    pub fn new(
173        recipient_public_key: &[u8],
174        recipient_key_id: Vec<u8>,
175        psk: Vec<u8>,
176        psk_id: Vec<u8>,
177        limits: Limits,
178    ) -> Result<Self, Error> {
179        let psk = Zeroizing::new(psk);
180        validate_id(&recipient_key_id)?;
181        validate_credential(psk.as_slice(), &psk_id)?;
182        let limits = limits.validate()?;
183        let recipient_public_key = <Kem as KemTrait>::PublicKey::from_bytes(recipient_public_key)
184            .map_err(|_| Error::InvalidConfiguration)?;
185        Ok(Self {
186            recipient_public_key,
187            recipient_key_id,
188            psk,
189            psk_id,
190            limits,
191        })
192    }
193
194    /// Protect one complete request with the platform entropy source.
195    ///
196    /// # Errors
197    ///
198    /// Returns a validation, clock, entropy, or cryptographic
199    /// error before exposing a partially protected envelope.
200    pub fn protect(&self, request: &Request) -> Result<ProtectedRequest, Error> {
201        self.protect_at_with_entropy(request, unix_time_seconds()?, &mut SystemEntropy)
202    }
203
204    /// Protect one complete request at an injected Unix time.
205    ///
206    /// This entry point exists for hosts with their own trusted clock and for
207    /// deterministic conformance tests. Normal native callers use [`Self::protect`].
208    ///
209    /// # Errors
210    ///
211    /// Returns a validation, entropy, or cryptographic error.
212    pub fn protect_at(
213        &self,
214        request: &Request,
215        now_unix_s: u64,
216    ) -> Result<ProtectedRequest, Error> {
217        self.protect_at_with_entropy(request, now_unix_s, &mut SystemEntropy)
218    }
219
220    /// Protect one complete request with an injected fallible entropy source.
221    ///
222    /// # Errors
223    ///
224    /// Returns a validation, clock, entropy, or cryptographic
225    /// error before exposing a partially protected envelope.
226    pub fn protect_with_entropy(
227        &self,
228        request: &Request,
229        entropy: &mut impl EntropySource,
230    ) -> Result<ProtectedRequest, Error> {
231        self.protect_at_with_entropy(request, unix_time_seconds()?, entropy)
232    }
233
234    /// Protect a request with injected time and entropy.
235    ///
236    /// # Errors
237    ///
238    /// Returns a validation, entropy, or cryptographic error
239    /// before exposing a partial envelope.
240    pub fn protect_at_with_entropy(
241        &self,
242        request: &Request,
243        now_unix_s: u64,
244        entropy: &mut impl EntropySource,
245    ) -> Result<ProtectedRequest, Error> {
246        message::validate_request(request, self.limits)?;
247        let head = RequestHead {
248            method: request.method,
249            authority: request.authority.clone(),
250            path: request.path.clone(),
251            headers: request.headers.clone(),
252        };
253        let (mut writer, mut envelope) =
254            self.begin_stream_at_with_entropy(&head, now_unix_s, entropy)?;
255        let mut remaining = request.body.as_slice();
256        while !remaining.is_empty() {
257            let (used, record) = writer.push(remaining)?;
258            remaining = &remaining[used..];
259            if let Some(record) = record {
260                envelope.extend_from_slice(&record);
261            }
262        }
263        let (end, response_token) = writer.finish()?;
264        envelope.extend_from_slice(&end);
265        Ok(ProtectedRequest {
266            envelope,
267            response_token,
268        })
269    }
270}
271
272/// Public information needed by a host credential resolver.
273#[derive(Clone, Debug, Eq, PartialEq)]
274pub struct CredentialRequest {
275    /// Opaque public PSK identifier authenticated with the request.
276    pub psk_id: Vec<u8>,
277}
278
279/// Result of the bounded public preparse stage.
280#[must_use]
281pub struct PreparsedRequest {
282    /// Input for the host credential resolver.
283    pub credential: CredentialRequest,
284    /// One-use input to [`Server::authenticate`].
285    pub token: StartToken,
286}
287
288/// Opaque one-use continuation after public preparse.
289#[must_use]
290pub struct StartToken {
291    envelope: Vec<u8>,
292}
293
294impl fmt::Debug for StartToken {
295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296        formatter
297            .debug_struct("StartToken")
298            .field("envelope_len", &self.envelope.len())
299            .finish()
300    }
301}
302
303/// Stable request identifier passed to an atomic replay-admission provider.
304#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
305pub struct ReplayRequest {
306    /// SHA-256 replay key for this authenticated attempt.
307    pub id: [u8; 32],
308    /// Unix second at which this reservation can be evicted.
309    pub retain_until_exclusive: u64,
310}
311
312impl ReplayRequest {
313    /// Bind one provider result to this exact replay request.
314    #[must_use]
315    pub const fn decision(self, admitted: bool) -> ReplayDecision {
316        ReplayDecision {
317            id: self.id,
318            retain_until_exclusive: self.retain_until_exclusive,
319            admitted,
320        }
321    }
322}
323
324/// One atomic replay-provider result bound to its request ID.
325#[derive(Clone, Copy, Debug, Eq, PartialEq)]
326pub struct ReplayDecision {
327    id: [u8; 32],
328    retain_until_exclusive: u64,
329    admitted: bool,
330}
331
332fn check_replay_decision(
333    replay_id: [u8; 32],
334    retain_until_exclusive: u64,
335    decision: ReplayDecision,
336    now_unix_s: u64,
337) -> Result<(), Error> {
338    if replay_id != decision.id || retain_until_exclusive != decision.retain_until_exclusive {
339        return Err(Error::ReplayDecisionMismatch);
340    }
341    if !decision.admitted {
342        return Err(Error::ReplayRejected);
343    }
344    if now_unix_s >= retain_until_exclusive {
345        return Err(Error::InvalidRequestTime);
346    }
347    Ok(())
348}
349
350/// Result of authentication, paused before replay admission and plaintext release.
351#[must_use]
352pub struct AuthenticatedRequest {
353    /// Input for an atomic replay-admission provider.
354    pub replay: ReplayRequest,
355    /// One-use input to [`ReplayToken::admit`].
356    pub token: ReplayToken,
357}
358
359/// Opaque one-use continuation after request authentication.
360#[must_use]
361pub struct ReplayToken {
362    replay_id: [u8; 32],
363    retain_until_exclusive: u64,
364    request: Request,
365    response: ResponseCapability,
366}
367
368impl fmt::Debug for ReplayToken {
369    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
370        formatter
371            .debug_struct("ReplayToken")
372            .field("replay_id", &self.replay_id)
373            .field("retain_until_exclusive", &self.retain_until_exclusive)
374            .field("request", &"[REDACTED]")
375            .finish_non_exhaustive()
376    }
377}
378
379impl ReplayToken {
380    /// Apply one atomic replay decision before its deadline, then release the
381    /// authenticated request.
382    ///
383    /// # Errors
384    ///
385    /// Returns [`Error::ReplayDecisionMismatch`] when the decision belongs to a
386    /// different request, [`Error::ReplayRejected`] when the provider denies
387    /// admission, [`Error::InvalidRequestTime`] when admission completes after
388    /// the authenticated deadline, or [`Error::ClockUnavailable`] when the
389    /// platform clock cannot be read.
390    pub fn admit(self, decision: ReplayDecision) -> Result<OpenedRequest, Error> {
391        self.admit_at(decision, unix_time_seconds()?)
392    }
393
394    /// Apply one replay decision at a caller-supplied trusted Unix time.
395    ///
396    /// This entry point exists for hosts with their own trusted clock and for
397    /// deterministic conformance tests. Normal native callers use [`Self::admit`].
398    ///
399    /// # Errors
400    ///
401    /// Returns the same decision, freshness, and clock-independent errors as
402    /// [`Self::admit`].
403    pub fn admit_at(
404        self,
405        decision: ReplayDecision,
406        now_unix_s: u64,
407    ) -> Result<OpenedRequest, Error> {
408        check_replay_decision(
409            self.replay_id,
410            self.retain_until_exclusive,
411            decision,
412            now_unix_s,
413        )?;
414        Ok(OpenedRequest {
415            request: self.request,
416            response: self.response,
417        })
418    }
419}
420
421/// A fully authenticated request and its one-use response capability.
422#[must_use]
423pub struct OpenedRequest {
424    /// Verified request data safe for application dispatch.
425    pub request: Request,
426    /// One-use capability bound to this request.
427    pub response: ResponseCapability,
428}
429
430/// A server with one advertised recipient key and other accepted keys.
431pub struct Server {
432    recipient_private_key: <Kem as KemTrait>::PrivateKey,
433    recipient_key_id: Vec<u8>,
434    accepted_keys: Vec<(Vec<u8>, <Kem as KemTrait>::PrivateKey)>,
435    limits: Limits,
436}
437
438impl fmt::Debug for Server {
439    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
440        formatter
441            .debug_struct("Server")
442            .field("recipient_private_key", &"[REDACTED]")
443            .field("recipient_key_id_len", &self.recipient_key_id.len())
444            .field("accepted_key_count", &self.accepted_keys.len())
445            .field("limits", &self.limits)
446            .finish()
447    }
448}
449
450impl Server {
451    /// Validate and create a server for one static recipient key.
452    ///
453    /// # Errors
454    ///
455    /// Returns [`Error::InvalidConfiguration`] for invalid key material,
456    /// identifiers, or limits.
457    pub fn new(
458        recipient_private_key: &[u8],
459        recipient_key_id: Vec<u8>,
460        limits: Limits,
461    ) -> Result<Self, Error> {
462        Self::with_accepted_keys(recipient_private_key, recipient_key_id, Vec::new(), limits)
463    }
464
465    /// Create a server that advertises one key and also accepts other keys.
466    ///
467    /// Each accepted pair holds a private key followed by its public key ID.
468    ///
469    /// # Errors
470    ///
471    /// Returns [`Error::InvalidConfiguration`] for invalid or duplicate IDs,
472    /// invalid private keys, or invalid limits.
473    pub fn with_accepted_keys(
474        recipient_private_key: &[u8],
475        recipient_key_id: Vec<u8>,
476        accepted: Vec<(Vec<u8>, Vec<u8>)>,
477        limits: Limits,
478    ) -> Result<Self, Error> {
479        validate_id(&recipient_key_id)?;
480        let limits = limits.validate()?;
481        let recipient_private_key =
482            <Kem as KemTrait>::PrivateKey::from_bytes(recipient_private_key)
483                .map_err(|_| Error::InvalidConfiguration)?;
484        let accepted = accepted
485            .into_iter()
486            .map(|(private_key, key_id)| (Zeroizing::new(private_key), key_id))
487            .collect::<Vec<_>>();
488        let mut accepted_keys = Vec::with_capacity(accepted.len());
489        for (private_key, key_id) in accepted {
490            validate_id(&key_id)?;
491            if key_id == recipient_key_id || accepted_keys.iter().any(|(id, _)| *id == key_id) {
492                return Err(Error::InvalidConfiguration);
493            }
494            let private_key = <Kem as KemTrait>::PrivateKey::from_bytes(&private_key)
495                .map_err(|_| Error::InvalidConfiguration)?;
496            accepted_keys.push((key_id, private_key));
497        }
498        Ok(Self {
499            recipient_private_key,
500            recipient_key_id,
501            accepted_keys,
502            limits,
503        })
504    }
505
506    fn private_key_for(&self, key_id: &[u8]) -> Result<&<Kem as KemTrait>::PrivateKey, Error> {
507        if key_id == self.recipient_key_id {
508            return Ok(&self.recipient_private_key);
509        }
510        self.accepted_keys
511            .iter()
512            .find(|(id, _)| id == key_id)
513            .map(|(_, private_key)| private_key)
514            .ok_or(Error::UnknownRecipientKey)
515    }
516
517    /// Return the encoded public key for this server's private key.
518    #[must_use]
519    pub fn public_key(&self) -> Vec<u8> {
520        Kem::sk_to_pk(&self.recipient_private_key)
521            .to_bytes()
522            .to_vec()
523    }
524
525    /// Return the largest complete request envelope accepted by `preparse`.
526    ///
527    /// Bindings use this limit before they copy a complete envelope.
528    ///
529    /// # Errors
530    ///
531    /// Returns [`Error::LimitExceeded`] if the size cannot fit in `usize`.
532    pub fn max_complete_envelope_len(&self) -> Result<usize, Error> {
533        max_complete_envelope_len(self.limits)
534    }
535
536    /// Parse bounded public fields without accepting credentials or plaintext.
537    ///
538    /// # Errors
539    ///
540    /// Returns a stable parse, limit, version, suite, or recipient-key error.
541    pub fn preparse(&self, envelope: &[u8]) -> Result<PreparsedRequest, Error> {
542        let credential = self.preparse_credential(envelope)?;
543        Ok(PreparsedRequest {
544            credential,
545            token: StartToken {
546                envelope: envelope.to_vec(),
547            },
548        })
549    }
550
551    /// Parse an owned envelope without cloning its complete ciphertext.
552    ///
553    /// Bindings which already own their input can move it into the one-shot
554    /// credential-resolution token. The same bounded public checks run before
555    /// the token is created.
556    ///
557    /// # Errors
558    ///
559    /// Returns a stable parse, limit, version, suite, or recipient-key error.
560    pub fn preparse_owned(&self, envelope: Vec<u8>) -> Result<PreparsedRequest, Error> {
561        let credential = self.preparse_credential(&envelope)?;
562        Ok(PreparsedRequest {
563            credential,
564            token: StartToken { envelope },
565        })
566    }
567
568    fn preparse_credential(&self, envelope: &[u8]) -> Result<CredentialRequest, Error> {
569        if envelope.len() > self.max_complete_envelope_len()? {
570            return Err(Error::LimitExceeded);
571        }
572        let first_len = self
573            .stream_start_length(envelope)?
574            .ok_or(Error::MalformedEnvelope)?;
575        if first_len > envelope.len() {
576            return Err(Error::MalformedEnvelope);
577        }
578        let parsed = parse_stream_start(
579            &envelope[..first_len],
580            max_stream_start_ciphertext(self.limits),
581        )?;
582        self.private_key_for(parsed.key_id)?;
583        Ok(CredentialRequest {
584            psk_id: parsed.psk_id.to_vec(),
585        })
586    }
587
588    /// Authenticate and parse the full request, then pause before replay admission.
589    ///
590    /// Plaintext stays inside the returned opaque token until admission.
591    ///
592    /// # Errors
593    ///
594    /// Returns a credential, recipient-key, parse, authentication,
595    /// request-time, or clock error.
596    pub fn authenticate(
597        &self,
598        token: StartToken,
599        psk: &[u8],
600    ) -> Result<AuthenticatedRequest, Error> {
601        self.authenticate_at(token, psk, unix_time_seconds()?)
602    }
603
604    /// Authenticate at an injected Unix time, then pause before replay admission.
605    ///
606    /// # Errors
607    ///
608    /// Returns a credential, recipient-key, parse, authentication, or request-time error.
609    pub fn authenticate_at(
610        &self,
611        token: StartToken,
612        psk: &[u8],
613        now_unix_s: u64,
614    ) -> Result<AuthenticatedRequest, Error> {
615        let StartToken { envelope } = token;
616        let first_len = self
617            .stream_start_length(&envelope)?
618            .ok_or(Error::MalformedEnvelope)?;
619        if first_len > envelope.len() {
620            return Err(Error::MalformedEnvelope);
621        }
622        let authenticated = self.authenticate_stream_at(
623            StreamStartToken {
624                first: envelope[..first_len].to_vec(),
625            },
626            psk,
627            now_unix_s,
628        )?;
629        let replay = authenticated.replay;
630        let StreamReplayToken {
631            head, mut reader, ..
632        } = authenticated.token;
633        let mut body = Vec::new();
634        let mut remaining = &envelope[first_len..];
635        while !remaining.is_empty() {
636            let (used, record) = reader.feed(remaining)?;
637            remaining = &remaining[used..];
638            if let Some(StreamRequestRecord::Data(part)) = record {
639                if part.len() > self.limits.max_body_len.saturating_sub(body.len()) {
640                    return Err(Error::LimitExceeded);
641                }
642                body.extend_from_slice(&part);
643            }
644        }
645        let response = reader.finish_eof()?;
646        let request = Request {
647            method: head.method,
648            authority: head.authority,
649            path: head.path,
650            headers: head.headers,
651            body,
652        };
653        Ok(AuthenticatedRequest {
654            replay,
655            token: ReplayToken {
656                replay_id: replay.id,
657                retain_until_exclusive: replay.retain_until_exclusive,
658                request,
659                response,
660            },
661        })
662    }
663}
664
665#[cfg(test)]
666mod stream_tests {
667    use super::STREAM_MAX_DATA_RECORDS;
668    use crate::{Client, Error, Limits, Method, Request, RequestHead, Server, generate_key_pair};
669
670    #[test]
671    fn server_accepts_old_key_during_rotation() -> Result<(), Error> {
672        let old = generate_key_pair()?;
673        let new = generate_key_pair()?;
674        let old_public = old.public_key().to_vec();
675        let new_public = new.public_key().to_vec();
676        let (old_private, _) = old.into_parts();
677        let (new_private, _) = new.into_parts();
678        let psk = b"a 32-byte minimum rotation credential";
679        let server = Server::with_accepted_keys(
680            &new_private,
681            b"new".to_vec(),
682            vec![(old_private.to_vec(), b"old".to_vec())],
683            Limits::default(),
684        )?;
685        assert_eq!(server.public_key(), new_public);
686        let request = Request {
687            method: Method::Get,
688            authority: b"api.example.test".to_vec(),
689            path: b"/status".to_vec(),
690            headers: Vec::new(),
691            body: Vec::new(),
692        };
693        for (public_key, id) in [
694            (&old_public, b"old".as_slice()),
695            (&new_public, b"new".as_slice()),
696        ] {
697            let client = Client::new(
698                public_key,
699                id.to_vec(),
700                psk.to_vec(),
701                b"token-id".to_vec(),
702                Limits::default(),
703            )?;
704            let protected = client.protect(&request)?;
705            let preparsed = server.preparse(protected.envelope())?;
706            let _authenticated = server.authenticate(preparsed.token, psk)?;
707        }
708        let unknown = Client::new(
709            &old_public,
710            b"unknown".to_vec(),
711            psk.to_vec(),
712            b"token-id".to_vec(),
713            Limits::default(),
714        )?;
715        let protected = unknown.protect(&request)?;
716        assert_eq!(
717            server.preparse(protected.envelope()).err(),
718            Some(Error::UnknownRecipientKey)
719        );
720        assert_eq!(
721            Server::with_accepted_keys(
722                &new_private,
723                b"new".to_vec(),
724                vec![(new_private.to_vec(), b"new".to_vec())],
725                Limits::default(),
726            )
727            .err(),
728            Some(Error::InvalidConfiguration),
729        );
730        Ok(())
731    }
732
733    #[test]
734    fn data_record_count_guard_closes_both_sides() -> Result<(), Error> {
735        let keys = generate_key_pair()?;
736        let psk = b"a 32-byte minimum upload credential";
737        let client = Client::new(
738            keys.public_key(),
739            b"key".to_vec(),
740            psk.to_vec(),
741            b"psk".to_vec(),
742            Limits::default(),
743        )?;
744        let server = Server::new(&keys.into_parts().0, b"key".to_vec(), Limits::default())?;
745        let head = RequestHead {
746            method: Method::Post,
747            authority: b"api.example.test".to_vec(),
748            path: b"/upload".to_vec(),
749            headers: Vec::new(),
750        };
751        let (mut writer, _) = client.begin_stream(&head)?;
752        writer.data_records = STREAM_MAX_DATA_RECORDS;
753        assert_eq!(writer.push(b"x").err(), Some(Error::LimitExceeded));
754        assert!(writer.context.is_none());
755
756        let (mut writer, first) = client.begin_stream(&head)?;
757        let (used, record) = writer.push(b"x")?;
758        assert_eq!(used, 1);
759        assert!(record.is_none());
760        let (data, _) = writer.finish()?;
761        let preparsed = server.preparse_stream(&first)?;
762        let authenticated = server.authenticate_stream(preparsed.token, psk)?;
763        let mut reader = authenticated
764            .token
765            .admit(authenticated.replay.decision(true))?
766            .reader;
767        reader.data_records = STREAM_MAX_DATA_RECORDS;
768        assert_eq!(reader.feed(&data).err(), Some(Error::LimitExceeded));
769        assert!(reader.context.is_none());
770        assert!(reader.finish_eof().is_err());
771        Ok(())
772    }
773}
774
775pub(crate) struct ResponseMaterial {
776    pub(crate) secret: Zeroizing<[u8; RESPONSE_SECRET_LEN]>,
777    pub(crate) enc: [u8; ENC_LEN],
778    pub(crate) method: Method,
779    pub(crate) limits: Limits,
780}
781
782impl fmt::Debug for ResponseMaterial {
783    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
784        formatter
785            .debug_struct("ResponseMaterial")
786            .field("secret", &"[REDACTED]")
787            .field("enc_len", &self.enc.len())
788            .field("method", &self.method)
789            .field("limits", &self.limits)
790            .finish()
791    }
792}
793
794/// One-use client capability for opening the response to one request.
795#[must_use]
796pub struct ResponseToken(ResponseMaterial);
797
798impl fmt::Debug for ResponseToken {
799    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
800        formatter
801            .debug_tuple("ResponseToken")
802            .field(&self.0)
803            .finish()
804    }
805}
806
807impl ResponseToken {
808    /// Consume this token to check response records as they arrive.
809    #[must_use]
810    pub fn into_opener(self) -> ResponseOpener {
811        ResponseOpener::new(self.0)
812    }
813
814    /// Open one complete finite response with the v3 record reader.
815    ///
816    /// # Errors
817    /// Returns a parse, limit, authentication, or response-mode error.
818    pub fn open_finite(self, envelope: &[u8]) -> Result<Response, Error> {
819        let mut opener = self.into_opener();
820        let mut offset = 0;
821        while offset < envelope.len() {
822            let (used, record) = opener.feed(&envelope[offset..])?;
823            offset += used;
824            if matches!(record, Some(ResponseRecord::Start(ref head)) if head.mode == ResponseMode::Sse)
825            {
826                return Err(Error::InvalidConfiguration);
827            }
828            if used == 0 {
829                return Err(Error::MalformedEnvelope);
830            }
831        }
832        opener.finish_eof()?.ok_or(Error::MalformedEnvelope)
833    }
834}
835
836/// One-use server capability for protecting the response to one opened request.
837#[must_use]
838pub struct ResponseCapability(ResponseMaterial);
839
840impl fmt::Debug for ResponseCapability {
841    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
842        formatter
843            .debug_tuple("ResponseCapability")
844            .field(&self.0)
845            .finish()
846    }
847}
848
849impl ResponseCapability {
850    /// Start one checked response. The returned bytes hold the prefix and START.
851    ///
852    /// # Errors
853    /// Returns a validation, entropy, or cryptographic error.
854    pub fn into_sealer(
855        self,
856        status: u16,
857        headers: Vec<HeaderField>,
858    ) -> Result<(ResponseSealer, Vec<u8>), Error> {
859        self.into_sealer_with_entropy(status, headers, &mut SystemEntropy)
860    }
861
862    /// Start one checked response with an injected entropy source.
863    ///
864    /// # Errors
865    /// Returns a validation, entropy, or cryptographic error.
866    pub fn into_sealer_with_entropy(
867        self,
868        status: u16,
869        headers: Vec<HeaderField>,
870        entropy: &mut impl EntropySource,
871    ) -> Result<(ResponseSealer, Vec<u8>), Error> {
872        ResponseSealer::new(&self.0, status, headers, entropy)
873    }
874
875    /// Protect one complete finite response with the v3 record writer.
876    ///
877    /// # Errors
878    /// Returns a validation, limit, entropy, or cryptographic error.
879    pub fn protect_finite(self, response: &Response) -> Result<Vec<u8>, Error> {
880        self.protect_finite_with_entropy(response, &mut SystemEntropy)
881    }
882
883    /// Protect one complete finite response with injected entropy.
884    ///
885    /// # Errors
886    /// Returns a validation, limit, entropy, or cryptographic error.
887    pub fn protect_finite_with_entropy(
888        self,
889        response: &Response,
890        entropy: &mut impl EntropySource,
891    ) -> Result<Vec<u8>, Error> {
892        self.protect_finite_parts_with_entropy(
893            response.status,
894            response.headers.clone(),
895            &response.body,
896            entropy,
897        )
898    }
899
900    /// Protect a finite response from a borrowed body without copying it first.
901    ///
902    /// # Errors
903    /// Returns a validation, limit, entropy, or cryptographic error.
904    pub fn protect_finite_parts(
905        self,
906        status: u16,
907        headers: Vec<HeaderField>,
908        body: &[u8],
909    ) -> Result<Vec<u8>, Error> {
910        self.protect_finite_parts_with_entropy(status, headers, body, &mut SystemEntropy)
911    }
912
913    fn protect_finite_parts_with_entropy(
914        self,
915        status: u16,
916        headers: Vec<HeaderField>,
917        body: &[u8],
918        entropy: &mut impl EntropySource,
919    ) -> Result<Vec<u8>, Error> {
920        let (mut sealer, mut output) = self.into_sealer_with_entropy(status, headers, entropy)?;
921        if sealer.head_mode() != ResponseMode::Finite {
922            return Err(Error::InvalidConfiguration);
923        }
924        let data = sealer.seal_finite_body(body)?;
925        let end = sealer.finish()?;
926        output.reserve(data.as_ref().map_or(0, Vec::len) + end.len());
927        if let Some(data) = data {
928            output.extend_from_slice(&data);
929        }
930        output.extend_from_slice(&end);
931        Ok(output)
932    }
933}
934
935fn validate_id(identifier: &[u8]) -> Result<(), Error> {
936    if identifier.is_empty() || identifier.len() > HARD_MAX_ID_LEN {
937        return Err(Error::InvalidConfiguration);
938    }
939    Ok(())
940}
941
942fn validate_credential(psk: &[u8], psk_id: &[u8]) -> Result<(), Error> {
943    validate_id(psk_id)?;
944    if psk.len() < MIN_PSK_LEN || psk == psk_id {
945        return Err(Error::InvalidConfiguration);
946    }
947    Ok(())
948}
949
950fn unix_time_seconds() -> Result<u64, Error> {
951    SystemTime::now()
952        .duration_since(UNIX_EPOCH)
953        .map_err(|_| Error::ClockUnavailable)
954        .map(|duration| duration.as_secs())
955}
956
957fn replay_retention_deadline(issued_at_unix_s: u64, now_unix_s: u64) -> Result<u64, Error> {
958    let latest_issued_at = now_unix_s
959        .checked_add(CLOCK_SKEW_SECS)
960        .ok_or(Error::InvalidRequestTime)?;
961    let retain_until_exclusive = issued_at_unix_s
962        .checked_add(REQUEST_LIFETIME_SECS)
963        .and_then(|value| value.checked_add(CLOCK_SKEW_SECS))
964        .ok_or(Error::InvalidRequestTime)?;
965    if issued_at_unix_s > latest_issued_at || now_unix_s >= retain_until_exclusive {
966        return Err(Error::InvalidRequestTime);
967    }
968    Ok(retain_until_exclusive)
969}
970
971fn replay_id(header: &[u8], enc: &[u8]) -> [u8; 32] {
972    let mut digest = Sha256::new();
973    digest.update(REPLAY_LABEL);
974    digest.update(header);
975    digest.update(enc);
976    digest.finalize().into()
977}
978
979fn export_response_material<C>(
980    context: &C,
981    enc: &[u8],
982    method: Method,
983    limits: Limits,
984) -> Result<ResponseMaterial, Error>
985where
986    C: Exporter,
987{
988    let enc: [u8; ENC_LEN] = enc.try_into().map_err(|_| Error::CryptoFailure)?;
989    let mut secret = Zeroizing::new([0_u8; RESPONSE_SECRET_LEN]);
990    context.export_secret(RESPONSE_EXPORT_LABEL, secret.as_mut())?;
991    Ok(ResponseMaterial {
992        secret,
993        enc,
994        method,
995        limits,
996    })
997}
998
999pub(crate) fn derive_response_key_nonce(
1000    material: &ResponseMaterial,
1001    response_nonce: &[u8],
1002) -> Result<(ResponseKey, ResponseNonce), Error> {
1003    if response_nonce.len() != RESPONSE_NONCE_LEN {
1004        return Err(Error::MalformedEnvelope);
1005    }
1006    let mut salt = Zeroizing::new(Vec::with_capacity(ENC_LEN + RESPONSE_NONCE_LEN));
1007    salt.extend_from_slice(&material.enc);
1008    salt.extend_from_slice(response_nonce);
1009    let hkdf = Hkdf::<Sha256>::new(Some(&salt), material.secret.as_ref());
1010    let mut key = Zeroizing::new([0_u8; RESPONSE_KEY_LEN]);
1011    hkdf.expand(RESPONSE_KEY_INFO, key.as_mut())
1012        .map_err(|_| Error::CryptoFailure)?;
1013    let mut nonce = Zeroizing::new([0_u8; AEAD_NONCE_LEN]);
1014    hkdf.expand(RESPONSE_NONCE_INFO, nonce.as_mut())
1015        .map_err(|_| Error::CryptoFailure)?;
1016    Ok((key, nonce))
1017}
1018
1019trait Exporter {
1020    fn export_secret(&self, context: &[u8], output: &mut [u8]) -> Result<(), Error>;
1021}
1022
1023impl Exporter for hpke::aead::AeadCtxS<HpkeAead, Kdf, Kem> {
1024    fn export_secret(&self, context: &[u8], output: &mut [u8]) -> Result<(), Error> {
1025        self.export(context, output)
1026            .map_err(|_| Error::CryptoFailure)
1027    }
1028}
1029
1030impl Exporter for RecipientContext {
1031    fn export_secret(&self, context: &[u8], output: &mut [u8]) -> Result<(), Error> {
1032        self.export(context, output)
1033            .map_err(|_| Error::CryptoFailure)
1034    }
1035}
1036
1037fn max_stream_start_ciphertext(limits: Limits) -> usize {
1038    limits.max_header_bytes + limits.max_target_len + limits.max_header_count * 16 + 256 + TAG_LEN
1039}
1040
1041fn max_complete_envelope_len(limits: Limits) -> Result<usize, Error> {
1042    let records = limits
1043        .max_body_len
1044        .min(usize::try_from(STREAM_MAX_DATA_RECORDS).map_err(|_| Error::LimitExceeded)?);
1045    let data_overhead = records
1046        .checked_mul(4 + 2 + TAG_LEN)
1047        .ok_or(Error::LimitExceeded)?;
1048    REQUEST_FIXED_LEN
1049        .checked_add(2 * HARD_MAX_ID_LEN)
1050        .and_then(|length| length.checked_add(ENC_LEN + 4))
1051        .and_then(|length| length.checked_add(max_stream_start_ciphertext(limits)))
1052        .and_then(|length| length.checked_add(limits.max_body_len))
1053        .and_then(|length| length.checked_add(data_overhead))
1054        .and_then(|length| length.checked_add(4 + 1 + TAG_LEN))
1055        .ok_or(Error::LimitExceeded)
1056}
1057
1058fn stream_request_info(header: &[u8]) -> Vec<u8> {
1059    let mut info = Vec::with_capacity(STREAM_REQUEST_INFO_LABEL.len() + header.len());
1060    info.extend_from_slice(STREAM_REQUEST_INFO_LABEL);
1061    info.extend_from_slice(header);
1062    info
1063}
1064
1065fn stream_record_aad(header: &[u8], sequence: u64, ciphertext_len: u32) -> Vec<u8> {
1066    let mut aad = Vec::with_capacity(STREAM_REQUEST_AAD_LABEL.len() + header.len() + 12);
1067    aad.extend_from_slice(STREAM_REQUEST_AAD_LABEL);
1068    aad.extend_from_slice(header);
1069    aad.extend_from_slice(&sequence.to_be_bytes());
1070    aad.extend_from_slice(&ciphertext_len.to_be_bytes());
1071    aad
1072}
1073
1074fn seal_stream_record(
1075    context: &mut SenderContext,
1076    header: &[u8],
1077    sequence: u64,
1078    kind: u8,
1079    coding: Option<u8>,
1080    body: &[u8],
1081) -> Result<Vec<u8>, Error> {
1082    let ciphertext_len = body
1083        .len()
1084        .checked_add(1 + usize::from(coding.is_some()) + TAG_LEN)
1085        .ok_or(Error::LimitExceeded)?;
1086    let ciphertext_len = u32::try_from(ciphertext_len).map_err(|_| Error::LimitExceeded)?;
1087    let mut frame = Vec::with_capacity(4 + ciphertext_len as usize);
1088    frame.extend_from_slice(&ciphertext_len.to_be_bytes());
1089    frame.push(kind);
1090    if let Some(coding) = coding {
1091        frame.push(coding);
1092    }
1093    frame.extend_from_slice(body);
1094    let aad = stream_record_aad(header, sequence, ciphertext_len);
1095    let tag = context
1096        .seal_inout_detached(InOutBuf::from(&mut frame[4..]), &aad)
1097        .map_err(|_| Error::CryptoFailure)?;
1098    frame.extend_from_slice(tag.to_bytes().as_ref());
1099    Ok(frame)
1100}
1101
1102/// One request writer. A failed call closes it and prevents a valid END.
1103pub struct StreamRequestSealer {
1104    context: Option<SenderContext>,
1105    header: Vec<u8>,
1106    sequence: u64,
1107    data_records: u64,
1108    total_bytes: u64,
1109    content_length: Option<u64>,
1110    max_upload_bytes: u64,
1111    pending: Vec<u8>,
1112    response_token: Option<ResponseToken>,
1113}
1114
1115impl fmt::Debug for StreamRequestSealer {
1116    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1117        formatter
1118            .debug_struct("StreamRequestSealer")
1119            .field("sequence", &self.sequence)
1120            .field("total_bytes", &self.total_bytes)
1121            .finish_non_exhaustive()
1122    }
1123}
1124
1125impl StreamRequestSealer {
1126    /// Add clear bytes. A full 64 KiB DATA record is returned when ready.
1127    /// The returned count can be less than `part.len()`. Pass `&part[used..]`
1128    /// to the next call until all input bytes are used.
1129    ///
1130    /// # Errors
1131    /// Returns a state, length, count, or cryptographic error and closes the writer.
1132    pub fn push(&mut self, part: &[u8]) -> Result<(usize, Option<Vec<u8>>), Error> {
1133        let result = self.push_inner(part);
1134        if result.is_err() {
1135            self.context = None;
1136            self.response_token = None;
1137        }
1138        result
1139    }
1140
1141    fn push_inner(&mut self, part: &[u8]) -> Result<(usize, Option<Vec<u8>>), Error> {
1142        if self.context.is_none() {
1143            return Err(Error::InvalidConfiguration);
1144        }
1145        if part.is_empty() {
1146            return Ok((0, None));
1147        }
1148        if self.data_records >= STREAM_MAX_DATA_RECORDS {
1149            return Err(Error::LimitExceeded);
1150        }
1151        let consumed = part.len().min(STREAM_DATA_LEN - self.pending.len());
1152        let next_total = self
1153            .total_bytes
1154            .checked_add(consumed as u64)
1155            .ok_or(Error::LimitExceeded)?;
1156        if next_total > self.max_upload_bytes
1157            || self
1158                .content_length
1159                .is_some_and(|length| next_total > length)
1160        {
1161            return Err(Error::LimitExceeded);
1162        }
1163        self.pending.extend_from_slice(&part[..consumed]);
1164        self.total_bytes = next_total;
1165        if self.pending.len() == STREAM_DATA_LEN {
1166            let frame = self.seal_pending()?;
1167            Ok((consumed, Some(frame)))
1168        } else {
1169            Ok((consumed, None))
1170        }
1171    }
1172
1173    fn seal_pending(&mut self) -> Result<Vec<u8>, Error> {
1174        if self.pending.is_empty() || self.data_records >= STREAM_MAX_DATA_RECORDS {
1175            return Err(Error::LimitExceeded);
1176        }
1177        let (coding, coded) = compression::encode(&self.pending);
1178        let context = self.context.as_mut().ok_or(Error::InvalidConfiguration)?;
1179        let frame = seal_stream_record(
1180            context,
1181            &self.header,
1182            self.sequence,
1183            STREAM_DATA,
1184            Some(coding),
1185            &coded,
1186        )?;
1187        self.pending.clear();
1188        self.sequence += 1;
1189        self.data_records += 1;
1190        Ok(frame)
1191    }
1192
1193    /// Protect END and release the one-use response right.
1194    ///
1195    /// # Errors
1196    /// Returns a state, logical length, or cryptographic error.
1197    pub fn finish(mut self) -> Result<(Vec<u8>, ResponseToken), Error> {
1198        if self
1199            .content_length
1200            .is_some_and(|length| self.total_bytes != length)
1201        {
1202            return Err(Error::InvalidConfiguration);
1203        }
1204        let mut frame = if self.pending.is_empty() {
1205            Vec::new()
1206        } else {
1207            self.seal_pending()?
1208        };
1209        let context = self.context.as_mut().ok_or(Error::InvalidConfiguration)?;
1210        frame.extend_from_slice(&seal_stream_record(
1211            context,
1212            &self.header,
1213            self.sequence,
1214            STREAM_END,
1215            None,
1216            &[],
1217        )?);
1218        let token = self
1219            .response_token
1220            .take()
1221            .ok_or(Error::InvalidConfiguration)?;
1222        Ok((frame, token))
1223    }
1224}
1225
1226impl Client {
1227    /// Start a protected request and return its bounded prefix and checked START.
1228    ///
1229    /// # Errors
1230    /// Returns a validation, entropy, or cryptographic error before source bytes are read.
1231    pub fn begin_stream(
1232        &self,
1233        head: &RequestHead,
1234    ) -> Result<(StreamRequestSealer, Vec<u8>), Error> {
1235        self.begin_stream_at_with_entropy(head, unix_time_seconds()?, &mut SystemEntropy)
1236    }
1237
1238    /// Start a stream with caller-supplied time and entropy for wire tests.
1239    ///
1240    /// # Errors
1241    /// Returns a validation, entropy, or cryptographic error.
1242    pub fn begin_stream_at_with_entropy(
1243        &self,
1244        head: &RequestHead,
1245        now_unix_s: u64,
1246        entropy: &mut impl EntropySource,
1247    ) -> Result<(StreamRequestSealer, Vec<u8>), Error> {
1248        let max_upload_bytes = self.limits.max_request_bytes;
1249        let content_length = message::stream_content_length(&head.headers)?;
1250        if content_length.is_some_and(|length| length > max_upload_bytes) {
1251            return Err(Error::LimitExceeded);
1252        }
1253        let clear_head = message::encode_stream_head(head, self.limits)?;
1254        let header = encode_request_header(&self.recipient_key_id, &self.psk_id, now_unix_s)?;
1255        let psk_bundle = PskBundle::new(self.psk.as_slice(), &self.psk_id)
1256            .map_err(|_| Error::InvalidConfiguration)?;
1257        let mut random_seed = Zeroizing::new([0_u8; 32]);
1258        entropy.fill(random_seed.as_mut())?;
1259        let mut random = ChaCha20Rng::from_seed(*random_seed);
1260        let (encapped_key, mut context) = setup_sender_with_rng::<HpkeAead, Kdf, Kem>(
1261            &OpModeS::Psk(psk_bundle),
1262            &self.recipient_public_key,
1263            &stream_request_info(&header),
1264            &mut random,
1265        )
1266        .map_err(|_| Error::CryptoFailure)?;
1267        let enc = encapped_key.to_bytes();
1268        let response_material =
1269            export_response_material(&context, enc.as_ref(), head.method, self.limits)?;
1270        let start = seal_stream_record(&mut context, &header, 0, STREAM_START, None, &clear_head)?;
1271        let mut first = Vec::with_capacity(header.len() + ENC_LEN + start.len());
1272        first.extend_from_slice(&header);
1273        first.extend_from_slice(enc.as_ref());
1274        first.extend_from_slice(&start);
1275        Ok((
1276            StreamRequestSealer {
1277                context: Some(context),
1278                header,
1279                sequence: 1,
1280                data_records: 0,
1281                total_bytes: 0,
1282                content_length,
1283                max_upload_bytes,
1284                pending: Vec::with_capacity(STREAM_DATA_LEN),
1285                response_token: Some(ResponseToken(response_material)),
1286            },
1287            first,
1288        ))
1289    }
1290}
1291
1292/// Public credential ID and a one-use bounded START token.
1293#[must_use]
1294pub struct PreparsedStreamRequest {
1295    /// Input for the host credential resolver.
1296    pub credential: CredentialRequest,
1297    /// One-use input for START authentication.
1298    pub token: StreamStartToken,
1299}
1300
1301/// Opaque START bytes before credential resolution.
1302#[must_use]
1303pub struct StreamStartToken {
1304    first: Vec<u8>,
1305}
1306
1307/// Authenticated START, with plaintext withheld until replay admission.
1308#[must_use]
1309pub struct AuthenticatedStreamRequest {
1310    /// Input for an atomic replay-admission provider.
1311    pub replay: ReplayRequest,
1312    /// One-use input for replay admission.
1313    pub token: StreamReplayToken,
1314}
1315
1316/// One-use continuation after START authentication.
1317#[must_use]
1318pub struct StreamReplayToken {
1319    replay_id: [u8; 32],
1320    retain_until_exclusive: u64,
1321    head: RequestHead,
1322    reader: StreamRequestOpener,
1323}
1324
1325impl StreamReplayToken {
1326    /// Apply the replay result before plaintext or DATA is released.
1327    ///
1328    /// # Errors
1329    /// Returns a replay or clock error.
1330    pub fn admit(self, decision: ReplayDecision) -> Result<OpenedStreamRequest, Error> {
1331        self.admit_at(decision, unix_time_seconds()?)
1332    }
1333
1334    /// Apply the replay result at a caller-supplied trusted Unix time.
1335    ///
1336    /// # Errors
1337    /// Returns a replay or freshness error.
1338    pub fn admit_at(
1339        self,
1340        decision: ReplayDecision,
1341        now_unix_s: u64,
1342    ) -> Result<OpenedStreamRequest, Error> {
1343        check_replay_decision(
1344            self.replay_id,
1345            self.retain_until_exclusive,
1346            decision,
1347            now_unix_s,
1348        )?;
1349        Ok(OpenedStreamRequest {
1350            head: self.head,
1351            reader: self.reader,
1352        })
1353    }
1354}
1355
1356/// Checked request head and a live body reader. The response right waits for END and EOF.
1357#[must_use]
1358pub struct OpenedStreamRequest {
1359    /// Authenticated request fields.
1360    pub head: RequestHead,
1361    /// Checks each DATA part before it is released.
1362    pub reader: StreamRequestOpener,
1363}
1364
1365/// One checked upload record.
1366#[derive(Debug, Eq, PartialEq)]
1367pub enum StreamRequestRecord {
1368    /// One checked nonempty clear DATA part.
1369    Data(Vec<u8>),
1370    /// Checked END. True outer EOF must still follow.
1371    End,
1372}
1373
1374/// Incremental request reader with bounded frame storage.
1375pub struct StreamRequestOpener {
1376    context: Option<RecipientContext>,
1377    header: Vec<u8>,
1378    sequence: u64,
1379    data_records: u64,
1380    total_bytes: u64,
1381    content_length: Option<u64>,
1382    max_upload_bytes: u64,
1383    frame: Vec<u8>,
1384    ended: bool,
1385    response_material: Option<ResponseMaterial>,
1386}
1387
1388impl fmt::Debug for StreamRequestOpener {
1389    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1390        formatter
1391            .debug_struct("StreamRequestOpener")
1392            .field("sequence", &self.sequence)
1393            .field("total_bytes", &self.total_bytes)
1394            .field("ended", &self.ended)
1395            .finish_non_exhaustive()
1396    }
1397}
1398
1399impl StreamRequestOpener {
1400    /// Read at most one record from any byte cut.
1401    /// The returned count is the number of input bytes used. Pass the unread
1402    /// suffix to the next call.
1403    ///
1404    /// # Errors
1405    /// Returns a parse, order, limit, or authentication error and closes the reader.
1406    pub fn feed(&mut self, input: &[u8]) -> Result<(usize, Option<StreamRequestRecord>), Error> {
1407        let result = self.feed_inner(input);
1408        if result.is_err() {
1409            self.context = None;
1410            self.response_material = None;
1411            self.frame.clear();
1412        }
1413        result
1414    }
1415
1416    fn feed_inner(&mut self, input: &[u8]) -> Result<(usize, Option<StreamRequestRecord>), Error> {
1417        if self.context.is_none() || (self.ended && !input.is_empty()) {
1418            return Err(Error::MalformedEnvelope);
1419        }
1420        let mut used = 0;
1421        if self.frame.len() < 4 {
1422            let take = (4 - self.frame.len()).min(input.len());
1423            self.frame.extend_from_slice(&input[..take]);
1424            used += take;
1425            if self.frame.len() < 4 {
1426                return Ok((used, None));
1427            }
1428        }
1429        let ciphertext_len = u32::from_be_bytes(
1430            self.frame[..4]
1431                .try_into()
1432                .map_err(|_| Error::MalformedEnvelope)?,
1433        ) as usize;
1434        if ciphertext_len < 1 + TAG_LEN {
1435            return Err(Error::MalformedEnvelope);
1436        }
1437        if ciphertext_len > STREAM_DATA_LEN + 2 + TAG_LEN {
1438            return Err(Error::LimitExceeded);
1439        }
1440        let frame_len = 4 + ciphertext_len;
1441        let take = (frame_len - self.frame.len()).min(input.len() - used);
1442        self.frame.extend_from_slice(&input[used..used + take]);
1443        used += take;
1444        if self.frame.len() < frame_len {
1445            return Ok((used, None));
1446        }
1447        let plain_end = frame_len - TAG_LEN;
1448        let tag = AeadTag::<HpkeAead>::from_bytes(&self.frame[plain_end..])
1449            .map_err(|_| Error::AuthenticationFailed)?;
1450        let aad = stream_record_aad(
1451            &self.header,
1452            self.sequence,
1453            u32::try_from(ciphertext_len).map_err(|_| Error::LimitExceeded)?,
1454        );
1455        self.context
1456            .as_mut()
1457            .ok_or(Error::MalformedEnvelope)?
1458            .open_inout_detached(InOutBuf::from(&mut self.frame[4..plain_end]), &aad, &tag)
1459            .map_err(|_| Error::AuthenticationFailed)?;
1460        let kind = self.frame[4];
1461        let body = &self.frame[5..plain_end];
1462        let record = match kind {
1463            STREAM_DATA => {
1464                if body.len() < 2 || self.data_records >= STREAM_MAX_DATA_RECORDS {
1465                    return Err(if body.len() < 2 {
1466                        Error::MalformedEnvelope
1467                    } else {
1468                        Error::LimitExceeded
1469                    });
1470                }
1471                let clear = compression::decode(body[0], &body[1..], STREAM_DATA_LEN)?;
1472                let next_total = self
1473                    .total_bytes
1474                    .checked_add(clear.len() as u64)
1475                    .ok_or(Error::LimitExceeded)?;
1476                if next_total > self.max_upload_bytes
1477                    || self
1478                        .content_length
1479                        .is_some_and(|length| next_total > length)
1480                {
1481                    return Err(Error::LimitExceeded);
1482                }
1483                self.total_bytes = next_total;
1484                self.data_records += 1;
1485                self.sequence += 1;
1486                StreamRequestRecord::Data(clear)
1487            }
1488            STREAM_END => {
1489                if !body.is_empty()
1490                    || self
1491                        .content_length
1492                        .is_some_and(|length| self.total_bytes != length)
1493                {
1494                    return Err(Error::MalformedEnvelope);
1495                }
1496                self.ended = true;
1497                StreamRequestRecord::End
1498            }
1499            _ => return Err(Error::MalformedEnvelope),
1500        };
1501        self.frame.clear();
1502        Ok((used, Some(record)))
1503    }
1504
1505    /// Check true outer EOF after END and release the response right.
1506    ///
1507    /// # Errors
1508    /// Returns an error for a missing END or partial frame.
1509    pub fn finish_eof(mut self) -> Result<ResponseCapability, Error> {
1510        if !self.ended || !self.frame.is_empty() || self.context.is_none() {
1511            return Err(Error::MalformedEnvelope);
1512        }
1513        Ok(ResponseCapability(
1514            self.response_material
1515                .take()
1516                .ok_or(Error::MalformedEnvelope)?,
1517        ))
1518    }
1519}
1520
1521impl Server {
1522    /// Return the START boundary once its bounded frame length is known.
1523    ///
1524    /// # Errors
1525    /// Returns a parse, version, suite, or size error.
1526    pub fn stream_start_length(&self, input: &[u8]) -> Result<Option<usize>, Error> {
1527        stream_start_length(input, max_stream_start_ciphertext(self.limits))
1528    }
1529
1530    /// Read the exact public prefix and START frame before credential lookup.
1531    ///
1532    /// # Errors
1533    /// Returns a parse, limit, or recipient-key error.
1534    pub fn preparse_stream(&self, first: &[u8]) -> Result<PreparsedStreamRequest, Error> {
1535        let parsed = parse_stream_start(first, max_stream_start_ciphertext(self.limits))?;
1536        self.private_key_for(parsed.key_id)?;
1537        Ok(PreparsedStreamRequest {
1538            credential: CredentialRequest {
1539                psk_id: parsed.psk_id.to_vec(),
1540            },
1541            token: StreamStartToken {
1542                first: first.to_vec(),
1543            },
1544        })
1545    }
1546
1547    /// Authenticate START, then pause before replay admission.
1548    ///
1549    /// # Errors
1550    /// Returns a credential, time, authentication, or parse error.
1551    pub fn authenticate_stream(
1552        &self,
1553        token: StreamStartToken,
1554        psk: &[u8],
1555    ) -> Result<AuthenticatedStreamRequest, Error> {
1556        self.authenticate_stream_at(token, psk, unix_time_seconds()?)
1557    }
1558
1559    /// Authenticate START at a caller-supplied trusted Unix time.
1560    ///
1561    /// # Errors
1562    /// Returns a credential, time, authentication, or parse error.
1563    pub fn authenticate_stream_at(
1564        &self,
1565        token: StreamStartToken,
1566        psk: &[u8],
1567        now_unix_s: u64,
1568    ) -> Result<AuthenticatedStreamRequest, Error> {
1569        let StreamStartToken { first } = token;
1570        let max_upload_bytes = self.limits.max_request_bytes;
1571        let parsed = parse_stream_start(&first, max_stream_start_ciphertext(self.limits))?;
1572        let private_key = self.private_key_for(parsed.key_id)?;
1573        validate_credential(psk, parsed.psk_id).map_err(|_| Error::InvalidCredential)?;
1574        let encapped_key = <Kem as KemTrait>::EncappedKey::from_bytes(parsed.enc)
1575            .map_err(|_| Error::AuthenticationFailed)?;
1576        let psk_bundle =
1577            PskBundle::new(psk, parsed.psk_id).map_err(|_| Error::InvalidCredential)?;
1578        let mut context = setup_receiver::<HpkeAead, Kdf, Kem>(
1579            &OpModeR::Psk(psk_bundle),
1580            private_key,
1581            &encapped_key,
1582            &stream_request_info(parsed.header),
1583        )
1584        .map_err(|_| Error::AuthenticationFailed)?;
1585        let mut clear = parsed.ciphertext.to_vec();
1586        let plain_end = clear.len() - TAG_LEN;
1587        let tag = AeadTag::<HpkeAead>::from_bytes(&clear[plain_end..])
1588            .map_err(|_| Error::AuthenticationFailed)?;
1589        let aad = stream_record_aad(
1590            parsed.header,
1591            0,
1592            u32::try_from(clear.len()).map_err(|_| Error::LimitExceeded)?,
1593        );
1594        context
1595            .open_inout_detached(InOutBuf::from(&mut clear[..plain_end]), &aad, &tag)
1596            .map_err(|_| Error::AuthenticationFailed)?;
1597        if clear.first() != Some(&STREAM_START) {
1598            return Err(Error::MalformedEnvelope);
1599        }
1600        let head = message::decode_stream_head(&clear[1..plain_end], self.limits)?;
1601        let content_length = message::stream_content_length(&head.headers)?;
1602        if content_length.is_some_and(|length| length > max_upload_bytes) {
1603            return Err(Error::LimitExceeded);
1604        }
1605        let retain_until_exclusive =
1606            replay_retention_deadline(parsed.issued_at_unix_s, now_unix_s)?;
1607        let replay_id = replay_id(parsed.header, parsed.enc);
1608        let response_material =
1609            export_response_material(&context, parsed.enc, head.method, self.limits)?;
1610        Ok(AuthenticatedStreamRequest {
1611            replay: ReplayRequest {
1612                id: replay_id,
1613                retain_until_exclusive,
1614            },
1615            token: StreamReplayToken {
1616                replay_id,
1617                retain_until_exclusive,
1618                head,
1619                reader: StreamRequestOpener {
1620                    context: Some(context),
1621                    header: parsed.header.to_vec(),
1622                    sequence: 1,
1623                    data_records: 0,
1624                    total_bytes: 0,
1625                    content_length,
1626                    max_upload_bytes,
1627                    frame: Vec::new(),
1628                    ended: false,
1629                    response_material: Some(response_material),
1630                },
1631            },
1632        })
1633    }
1634}