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, 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, CompressionCoding, EntropySource, Error, HARD_MAX_ID_LEN, Limits, Method,
24    REQUEST_LIFETIME_SECS, SystemEntropy,
25    codec::{
26        ENC_LEN, RESPONSE_NONCE_LEN, TAG_LEN, encode_request, encode_request_header, parse_request,
27    },
28    compression,
29    message::{HeaderField, Request, Response},
30    response::{ResponseMode, ResponseOpener, ResponseRecord, ResponseSealer},
31};
32
33type Kem = X25519HkdfSha256;
34type Kdf = HkdfSha256;
35type HpkeAead = HpkeChaCha20Poly1305;
36type RecipientContext = AeadCtxR<HpkeAead, Kdf, Kem>;
37
38const REQUEST_INFO_LABEL: &[u8] = b"message/hpke-http request\0v2";
39const RESPONSE_EXPORT_LABEL: &[u8] = b"message/hpke-http response\0v2";
40const REPLAY_LABEL: &[u8] = b"hpke-http/replay\0v2\0";
41const RESPONSE_KEY_INFO: &[u8] = b"hpke-http/2 response key";
42const RESPONSE_NONCE_INFO: &[u8] = b"hpke-http/2 response nonce";
43const RESPONSE_SECRET_LEN: usize = 32;
44const RESPONSE_KEY_LEN: usize = 32;
45const AEAD_NONCE_LEN: usize = 12;
46const MIN_PSK_LEN: usize = 32;
47pub(crate) type ResponseKey = Zeroizing<[u8; RESPONSE_KEY_LEN]>;
48pub(crate) type ResponseNonce = Zeroizing<[u8; AEAD_NONCE_LEN]>;
49
50/// A generated X25519 recipient key pair.
51pub struct KeyPair {
52    private_key: Zeroizing<Vec<u8>>,
53    public_key: Vec<u8>,
54}
55
56impl fmt::Debug for KeyPair {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        formatter
59            .debug_struct("KeyPair")
60            .field("private_key", &"[REDACTED]")
61            .field("public_key_len", &self.public_key.len())
62            .finish()
63    }
64}
65
66impl KeyPair {
67    /// Return the encoded private and public keys.
68    #[must_use]
69    pub fn into_parts(self) -> (Zeroizing<Vec<u8>>, Vec<u8>) {
70        (self.private_key, self.public_key)
71    }
72
73    /// Borrow the encoded public key.
74    #[must_use]
75    pub fn public_key(&self) -> &[u8] {
76        &self.public_key
77    }
78}
79
80/// Generate a recipient key pair with the platform entropy source.
81///
82/// # Errors
83///
84/// Returns [`Error::EntropyUnavailable`] if the platform source fails.
85pub fn generate_key_pair() -> Result<KeyPair, Error> {
86    generate_key_pair_with_entropy(&mut SystemEntropy)
87}
88
89/// Generate a recipient key pair with an injected fallible entropy source.
90///
91/// # Errors
92///
93/// Returns the error reported by `entropy` before producing a key pair.
94pub fn generate_key_pair_with_entropy(entropy: &mut impl EntropySource) -> Result<KeyPair, Error> {
95    let mut input_key_material = Zeroizing::new([0_u8; 32]);
96    entropy.fill(input_key_material.as_mut())?;
97    let (private_key, public_key) = Kem::derive_keypair(input_key_material.as_ref());
98    Ok(KeyPair {
99        private_key: Zeroizing::new(private_key.to_bytes().to_vec()),
100        public_key: public_key.to_bytes().to_vec(),
101    })
102}
103
104/// A request envelope and the one-use capability needed to open its response.
105#[must_use]
106pub struct ProtectedRequest {
107    envelope: Vec<u8>,
108    response_token: ResponseToken,
109}
110
111impl fmt::Debug for ProtectedRequest {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        formatter
114            .debug_struct("ProtectedRequest")
115            .field("envelope_len", &self.envelope.len())
116            .field("response_token", &self.response_token)
117            .finish()
118    }
119}
120
121impl ProtectedRequest {
122    /// Borrow the complete protected request envelope.
123    #[must_use]
124    pub fn envelope(&self) -> &[u8] {
125        &self.envelope
126    }
127
128    /// Split the envelope from its one-use response token.
129    pub fn into_parts(self) -> (Vec<u8>, ResponseToken) {
130        (self.envelope, self.response_token)
131    }
132}
133
134/// A client configured for one recipient key and one explicit PSK identity.
135pub struct Client {
136    recipient_public_key: <Kem as KemTrait>::PublicKey,
137    recipient_key_id: Vec<u8>,
138    psk: Zeroizing<Vec<u8>>,
139    psk_id: Vec<u8>,
140    limits: Limits,
141    compression: Option<CompressionCoding>,
142}
143
144impl fmt::Debug for Client {
145    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146        formatter
147            .debug_struct("Client")
148            .field("recipient_key_id_len", &self.recipient_key_id.len())
149            .field("psk", &"[REDACTED]")
150            .field("psk_id_len", &self.psk_id.len())
151            .field("limits", &self.limits)
152            .field("compression", &self.compression)
153            .finish_non_exhaustive()
154    }
155}
156
157impl Client {
158    /// Validate and create a client. A secret-derived public ID is rejected.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`Error::InvalidConfiguration`] for invalid keys, identifiers,
163    /// credentials, or limits.
164    pub fn new(
165        recipient_public_key: &[u8],
166        recipient_key_id: Vec<u8>,
167        psk: Vec<u8>,
168        psk_id: Vec<u8>,
169        limits: Limits,
170    ) -> Result<Self, Error> {
171        let psk = Zeroizing::new(psk);
172        validate_id(&recipient_key_id)?;
173        validate_credential(psk.as_slice(), &psk_id)?;
174        let limits = limits.validate()?;
175        let recipient_public_key = <Kem as KemTrait>::PublicKey::from_bytes(recipient_public_key)
176            .map_err(|_| Error::InvalidConfiguration)?;
177        Ok(Self {
178            recipient_public_key,
179            recipient_key_id,
180            psk,
181            psk_id,
182            limits,
183            compression: None,
184        })
185    }
186
187    /// Opt in to authenticated request-body compression and advertise the
188    /// same coding for responses. Even an identity-sized request carries the
189    /// extension marker; servers without compression support reject it.
190    /// Do not mix attacker-controlled text with secrets in a body being coded:
191    /// ciphertext length can reveal information about compression ratio.
192    #[must_use]
193    pub fn with_compression(mut self, coding: CompressionCoding) -> Self {
194        self.compression = Some(coding);
195        self
196    }
197
198    /// Protect one complete request with the platform entropy source.
199    ///
200    /// # Errors
201    ///
202    /// Returns a validation, clock, entropy, compression, or cryptographic
203    /// error before exposing a partially protected envelope.
204    pub fn protect(&self, request: &Request) -> Result<ProtectedRequest, Error> {
205        self.protect_at_with_entropy(request, unix_time_seconds()?, &mut SystemEntropy)
206    }
207
208    /// Protect one complete request at an injected Unix time.
209    ///
210    /// This entry point exists for hosts with their own trusted clock and for
211    /// deterministic conformance tests. Normal native callers use [`Self::protect`].
212    ///
213    /// # Errors
214    ///
215    /// Returns a validation, entropy, compression, or cryptographic error.
216    pub fn protect_at(
217        &self,
218        request: &Request,
219        now_unix_s: u64,
220    ) -> Result<ProtectedRequest, Error> {
221        self.protect_at_with_entropy(request, now_unix_s, &mut SystemEntropy)
222    }
223
224    /// Protect one complete request with an injected fallible entropy source.
225    ///
226    /// # Errors
227    ///
228    /// Returns a validation, clock, entropy, compression, or cryptographic
229    /// error before exposing a partially protected envelope.
230    pub fn protect_with_entropy(
231        &self,
232        request: &Request,
233        entropy: &mut impl EntropySource,
234    ) -> Result<ProtectedRequest, Error> {
235        self.protect_at_with_entropy(request, unix_time_seconds()?, entropy)
236    }
237
238    /// Protect a request with injected time and entropy.
239    ///
240    /// # Errors
241    ///
242    /// Returns a validation, entropy, compression, or cryptographic error
243    /// before exposing a partial envelope.
244    pub fn protect_at_with_entropy(
245        &self,
246        request: &Request,
247        now_unix_s: u64,
248        entropy: &mut impl EntropySource,
249    ) -> Result<ProtectedRequest, Error> {
250        let mut ciphertext = compression::encode_request(request, self.limits, self.compression)?;
251        let header = encode_request_header(&self.recipient_key_id, &self.psk_id, now_unix_s)?;
252        let info = request_info(&header);
253        let psk_bundle = PskBundle::new(self.psk.as_slice(), &self.psk_id)
254            .map_err(|_| Error::InvalidConfiguration)?;
255        let mut random_seed = Zeroizing::new([0_u8; 32]);
256        entropy.fill(random_seed.as_mut())?;
257        let mut random = ChaCha20Rng::from_seed(*random_seed);
258        let (encapped_key, mut context) = setup_sender_with_rng::<HpkeAead, Kdf, Kem>(
259            &OpModeS::Psk(psk_bundle),
260            &self.recipient_public_key,
261            &info,
262            &mut random,
263        )
264        .map_err(|_| Error::CryptoFailure)?;
265        let enc = encapped_key.to_bytes();
266        let tag = context
267            .seal_inout_detached(InOutBuf::from(ciphertext.as_mut_slice()), &[])
268            .map_err(|_| Error::CryptoFailure)?;
269        ciphertext.extend_from_slice(tag.to_bytes().as_ref());
270        let mut response_material =
271            export_response_material(&context, enc.as_ref(), request.method, self.limits)?;
272        response_material.compression = self.compression;
273        let envelope = encode_request(&header, enc.as_ref(), &ciphertext)?;
274        Ok(ProtectedRequest {
275            envelope,
276            response_token: ResponseToken(response_material),
277        })
278    }
279}
280
281/// Public information needed by a host credential resolver.
282#[derive(Clone, Debug, Eq, PartialEq)]
283pub struct CredentialRequest {
284    /// Opaque public PSK identifier authenticated with the request.
285    pub psk_id: Vec<u8>,
286}
287
288/// Result of the bounded public preparse stage.
289#[must_use]
290pub struct PreparsedRequest {
291    /// Input for the host credential resolver.
292    pub credential: CredentialRequest,
293    /// One-use input to [`Server::authenticate`].
294    pub token: StartToken,
295}
296
297/// Opaque one-use continuation after public preparse.
298#[must_use]
299pub struct StartToken {
300    envelope: Vec<u8>,
301}
302
303impl fmt::Debug for StartToken {
304    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
305        formatter
306            .debug_struct("StartToken")
307            .field("envelope_len", &self.envelope.len())
308            .finish()
309    }
310}
311
312/// Stable request identifier passed to an atomic replay-admission provider.
313#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
314pub struct ReplayRequest {
315    /// SHA-256 replay key for this authenticated attempt.
316    pub id: [u8; 32],
317    /// Unix second at which this reservation can be evicted.
318    pub retain_until_exclusive: u64,
319}
320
321impl ReplayRequest {
322    /// Bind one provider result to this exact replay request.
323    #[must_use]
324    pub const fn decision(self, admitted: bool) -> ReplayDecision {
325        ReplayDecision {
326            id: self.id,
327            retain_until_exclusive: self.retain_until_exclusive,
328            admitted,
329        }
330    }
331}
332
333/// One atomic replay-provider result bound to its request ID.
334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335pub struct ReplayDecision {
336    id: [u8; 32],
337    retain_until_exclusive: u64,
338    admitted: bool,
339}
340
341/// Result of authentication, paused before replay admission and plaintext release.
342#[must_use]
343pub struct AuthenticatedRequest {
344    /// Input for an atomic replay-admission provider.
345    pub replay: ReplayRequest,
346    /// One-use input to [`ReplayToken::admit`].
347    pub token: ReplayToken,
348}
349
350/// Opaque one-use continuation after request authentication.
351#[must_use]
352pub struct ReplayToken {
353    replay_id: [u8; 32],
354    retain_until_exclusive: u64,
355    request: Request,
356    request_coding: Option<CompressionCoding>,
357    response_material: ResponseMaterial,
358}
359
360impl fmt::Debug for ReplayToken {
361    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
362        formatter
363            .debug_struct("ReplayToken")
364            .field("replay_id", &self.replay_id)
365            .field("retain_until_exclusive", &self.retain_until_exclusive)
366            .field("request", &"[REDACTED]")
367            .field("request_coding", &self.request_coding)
368            .field("response_material", &self.response_material)
369            .finish()
370    }
371}
372
373impl ReplayToken {
374    /// Apply one atomic replay decision before its deadline, then release the
375    /// authenticated request.
376    ///
377    /// # Errors
378    ///
379    /// Returns [`Error::ReplayDecisionMismatch`] when the decision belongs to a
380    /// different request, [`Error::ReplayRejected`] when the provider denies
381    /// admission, [`Error::InvalidRequestTime`] when admission completes after
382    /// the authenticated deadline, or [`Error::ClockUnavailable`] when the
383    /// platform clock cannot be read.
384    pub fn admit(self, decision: ReplayDecision) -> Result<OpenedRequest, Error> {
385        self.admit_at(decision, unix_time_seconds()?)
386    }
387
388    /// Apply one replay decision at a caller-supplied trusted Unix time.
389    ///
390    /// This entry point exists for hosts with their own trusted clock and for
391    /// deterministic conformance tests. Normal native callers use [`Self::admit`].
392    ///
393    /// # Errors
394    ///
395    /// Returns the same decision, freshness, and clock-independent errors as
396    /// [`Self::admit`].
397    pub fn admit_at(
398        self,
399        decision: ReplayDecision,
400        now_unix_s: u64,
401    ) -> Result<OpenedRequest, Error> {
402        if self.replay_id != decision.id
403            || self.retain_until_exclusive != decision.retain_until_exclusive
404        {
405            return Err(Error::ReplayDecisionMismatch);
406        }
407        if !decision.admitted {
408            return Err(Error::ReplayRejected);
409        }
410        if now_unix_s >= self.retain_until_exclusive {
411            return Err(Error::InvalidRequestTime);
412        }
413        let request = compression::finish_request(
414            self.request,
415            self.request_coding,
416            self.response_material.limits,
417        )?;
418        Ok(OpenedRequest {
419            request,
420            response: ResponseCapability(self.response_material),
421        })
422    }
423}
424
425/// A fully authenticated request and its one-use response capability.
426#[must_use]
427pub struct OpenedRequest {
428    /// Verified request data safe for application dispatch.
429    pub request: Request,
430    /// One-use capability bound to this request.
431    pub response: ResponseCapability,
432}
433
434/// A server configured for one static recipient key.
435pub struct Server {
436    recipient_private_key: <Kem as KemTrait>::PrivateKey,
437    recipient_key_id: Vec<u8>,
438    limits: Limits,
439    compression_enabled: bool,
440}
441
442impl fmt::Debug for Server {
443    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
444        formatter
445            .debug_struct("Server")
446            .field("recipient_private_key", &"[REDACTED]")
447            .field("recipient_key_id_len", &self.recipient_key_id.len())
448            .field("limits", &self.limits)
449            .field("compression_enabled", &self.compression_enabled)
450            .finish()
451    }
452}
453
454impl Server {
455    /// Validate and create a server for one static recipient key.
456    ///
457    /// # Errors
458    ///
459    /// Returns [`Error::InvalidConfiguration`] for invalid key material,
460    /// identifiers, or limits.
461    pub fn new(
462        recipient_private_key: &[u8],
463        recipient_key_id: Vec<u8>,
464        limits: Limits,
465    ) -> Result<Self, Error> {
466        validate_id(&recipient_key_id)?;
467        let limits = limits.validate()?;
468        let recipient_private_key =
469            <Kem as KemTrait>::PrivateKey::from_bytes(recipient_private_key)
470                .map_err(|_| Error::InvalidConfiguration)?;
471        Ok(Self {
472            recipient_private_key,
473            recipient_key_id,
474            limits,
475            compression_enabled: false,
476        })
477    }
478
479    /// Accept the authenticated gzip/zstd body-coding extension and compress
480    /// a response only when its client advertised a supported coding.
481    #[must_use]
482    pub fn with_compression(mut self) -> Self {
483        self.compression_enabled = true;
484        self
485    }
486
487    /// Return the encoded public key for this server's private key.
488    #[must_use]
489    pub fn public_key(&self) -> Vec<u8> {
490        Kem::sk_to_pk(&self.recipient_private_key)
491            .to_bytes()
492            .to_vec()
493    }
494
495    /// Parse bounded public fields without accepting credentials or plaintext.
496    ///
497    /// # Errors
498    ///
499    /// Returns a stable parse, limit, version, suite, or recipient-key error.
500    pub fn preparse(&self, envelope: &[u8]) -> Result<PreparsedRequest, Error> {
501        let credential = self.preparse_credential(envelope)?;
502        Ok(PreparsedRequest {
503            credential,
504            token: StartToken {
505                envelope: envelope.to_vec(),
506            },
507        })
508    }
509
510    /// Parse an owned envelope without cloning its complete ciphertext.
511    ///
512    /// Bindings which already own their input can move it into the one-shot
513    /// credential-resolution token. The same bounded public checks run before
514    /// the token is created.
515    ///
516    /// # Errors
517    ///
518    /// Returns a stable parse, limit, version, suite, or recipient-key error.
519    pub fn preparse_owned(&self, envelope: Vec<u8>) -> Result<PreparsedRequest, Error> {
520        let credential = self.preparse_credential(&envelope)?;
521        Ok(PreparsedRequest {
522            credential,
523            token: StartToken { envelope },
524        })
525    }
526
527    fn preparse_credential(&self, envelope: &[u8]) -> Result<CredentialRequest, Error> {
528        let parsed = parse_request(envelope, self.limits)?;
529        if parsed.key_id != self.recipient_key_id {
530            return Err(Error::UnknownRecipientKey);
531        }
532        Ok(CredentialRequest {
533            psk_id: parsed.psk_id.to_vec(),
534        })
535    }
536
537    /// Authenticate and parse the full request, then pause before replay admission.
538    ///
539    /// Plaintext stays inside the returned opaque token until admission.
540    ///
541    /// # Errors
542    ///
543    /// Returns a credential, recipient-key, parse, authentication,
544    /// request-time, or clock error.
545    pub fn authenticate(
546        &self,
547        token: StartToken,
548        psk: &[u8],
549    ) -> Result<AuthenticatedRequest, Error> {
550        self.authenticate_at(token, psk, unix_time_seconds()?)
551    }
552
553    /// Authenticate at an injected Unix time, then pause before replay admission.
554    ///
555    /// # Errors
556    ///
557    /// Returns a credential, recipient-key, parse, authentication, or request-time error.
558    pub fn authenticate_at(
559        &self,
560        token: StartToken,
561        psk: &[u8],
562        now_unix_s: u64,
563    ) -> Result<AuthenticatedRequest, Error> {
564        let StartToken { mut envelope } = token;
565        let parsed = parse_request(&envelope, self.limits)?;
566        if parsed.key_id != self.recipient_key_id {
567            return Err(Error::UnknownRecipientKey);
568        }
569        validate_credential(psk, parsed.psk_id).map_err(|_| Error::InvalidCredential)?;
570        let encapped_key = <Kem as KemTrait>::EncappedKey::from_bytes(parsed.enc)
571            .map_err(|_| Error::AuthenticationFailed)?;
572        let psk_bundle =
573            PskBundle::new(psk, parsed.psk_id).map_err(|_| Error::InvalidCredential)?;
574        let info = request_info(parsed.header);
575        let mut context = setup_receiver::<HpkeAead, Kdf, Kem>(
576            &OpModeR::Psk(psk_bundle),
577            &self.recipient_private_key,
578            &encapped_key,
579            &info,
580        )
581        .map_err(|_| Error::AuthenticationFailed)?;
582        let ciphertext_start = envelope.len() - parsed.ciphertext.len();
583        let plaintext_end = envelope.len() - TAG_LEN;
584        let tag = AeadTag::<HpkeAead>::from_bytes(&envelope[plaintext_end..])
585            .map_err(|_| Error::AuthenticationFailed)?;
586        let replay_id = replay_id(parsed.header, parsed.enc);
587        let issued_at_unix_s = parsed.issued_at_unix_s;
588        let enc = parsed.enc.to_vec();
589        context
590            .open_inout_detached(
591                InOutBuf::from(&mut envelope[ciphertext_start..plaintext_end]),
592                &[],
593                &tag,
594            )
595            .map_err(|_| Error::AuthenticationFailed)?;
596        let (request, request_coding, response_coding) = compression::decode_request(
597            &envelope[ciphertext_start..plaintext_end],
598            self.limits,
599            self.compression_enabled,
600        )?;
601        let retain_until_exclusive = replay_retention_deadline(issued_at_unix_s, now_unix_s)?;
602        let mut response_material =
603            export_response_material(&context, &enc, request.method, self.limits)?;
604        response_material.compression = response_coding;
605        Ok(AuthenticatedRequest {
606            replay: ReplayRequest {
607                id: replay_id,
608                retain_until_exclusive,
609            },
610            token: ReplayToken {
611                replay_id,
612                retain_until_exclusive,
613                request,
614                request_coding,
615                response_material,
616            },
617        })
618    }
619}
620
621pub(crate) struct ResponseMaterial {
622    pub(crate) secret: Zeroizing<[u8; RESPONSE_SECRET_LEN]>,
623    pub(crate) enc: [u8; ENC_LEN],
624    pub(crate) method: Method,
625    pub(crate) limits: Limits,
626    pub(crate) compression: Option<CompressionCoding>,
627}
628
629impl fmt::Debug for ResponseMaterial {
630    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
631        formatter
632            .debug_struct("ResponseMaterial")
633            .field("secret", &"[REDACTED]")
634            .field("enc_len", &self.enc.len())
635            .field("method", &self.method)
636            .field("limits", &self.limits)
637            .field("compression", &self.compression)
638            .finish()
639    }
640}
641
642/// One-use client capability for opening the response to one request.
643#[must_use]
644pub struct ResponseToken(ResponseMaterial);
645
646impl fmt::Debug for ResponseToken {
647    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
648        formatter
649            .debug_tuple("ResponseToken")
650            .field(&self.0)
651            .finish()
652    }
653}
654
655impl ResponseToken {
656    /// Consume this token to check response records as they arrive.
657    #[must_use]
658    pub fn into_opener(self) -> ResponseOpener {
659        ResponseOpener::new(self.0)
660    }
661
662    /// Open one complete finite response with the v2 record reader.
663    ///
664    /// # Errors
665    /// Returns a parse, limit, authentication, or response-mode error.
666    pub fn open_finite(self, envelope: &[u8]) -> Result<Response, Error> {
667        let mut opener = self.into_opener();
668        let mut offset = 0;
669        while offset < envelope.len() {
670            let (used, record) = opener.feed(&envelope[offset..])?;
671            offset += used;
672            if matches!(record, Some(ResponseRecord::Start(ref head)) if head.mode == ResponseMode::Sse)
673            {
674                return Err(Error::InvalidConfiguration);
675            }
676            if used == 0 {
677                return Err(Error::MalformedEnvelope);
678            }
679        }
680        opener.finish_eof()?.ok_or(Error::MalformedEnvelope)
681    }
682}
683
684/// One-use server capability for protecting the response to one opened request.
685#[must_use]
686pub struct ResponseCapability(ResponseMaterial);
687
688impl fmt::Debug for ResponseCapability {
689    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
690        formatter
691            .debug_tuple("ResponseCapability")
692            .field(&self.0)
693            .finish()
694    }
695}
696
697impl ResponseCapability {
698    /// Start one checked response. The returned bytes hold the prefix and START.
699    ///
700    /// # Errors
701    /// Returns a validation, entropy, or cryptographic error.
702    pub fn into_sealer(
703        self,
704        status: u16,
705        headers: Vec<HeaderField>,
706        coding: Option<CompressionCoding>,
707    ) -> Result<(ResponseSealer, Vec<u8>), Error> {
708        self.into_sealer_with_entropy(status, headers, coding, &mut SystemEntropy)
709    }
710
711    /// Start one checked response with an injected entropy source.
712    ///
713    /// # Errors
714    /// Returns a validation, entropy, or cryptographic error.
715    pub fn into_sealer_with_entropy(
716        self,
717        status: u16,
718        headers: Vec<HeaderField>,
719        coding: Option<CompressionCoding>,
720        entropy: &mut impl EntropySource,
721    ) -> Result<(ResponseSealer, Vec<u8>), Error> {
722        ResponseSealer::new(&self.0, status, headers, coding, entropy)
723    }
724
725    /// Protect one complete finite response with the v2 record writer.
726    ///
727    /// # Errors
728    /// Returns a validation, limit, entropy, compression, or cryptographic error.
729    pub fn protect_finite(self, response: &Response) -> Result<Vec<u8>, Error> {
730        self.protect_finite_with_entropy(response, &mut SystemEntropy)
731    }
732
733    /// Protect one complete finite response with injected entropy.
734    ///
735    /// # Errors
736    /// Returns a validation, limit, entropy, compression, or cryptographic error.
737    pub fn protect_finite_with_entropy(
738        self,
739        response: &Response,
740        entropy: &mut impl EntropySource,
741    ) -> Result<Vec<u8>, Error> {
742        let (coding, compressed) = match self.0.compression {
743            Some(coding) if !response.body.is_empty() => {
744                let compressed = compression::maybe_compress(&response.body, coding)?;
745                (compressed.as_ref().map(|_| coding), compressed)
746            }
747            _ => (None, None),
748        };
749        let (mut sealer, mut output) = self.into_sealer_with_entropy(
750            response.status,
751            response.headers.clone(),
752            coding,
753            entropy,
754        )?;
755        if sealer.head_mode() != ResponseMode::Finite {
756            return Err(Error::InvalidConfiguration);
757        }
758        let data =
759            sealer.seal_finite_body_with_precompressed(&response.body, compressed.as_deref())?;
760        let end = sealer.finish()?;
761        output.reserve(data.as_ref().map_or(0, Vec::len) + end.len());
762        if let Some(data) = data {
763            output.extend_from_slice(&data);
764        }
765        output.extend_from_slice(&end);
766        Ok(output)
767    }
768}
769
770fn validate_id(identifier: &[u8]) -> Result<(), Error> {
771    if identifier.is_empty() || identifier.len() > HARD_MAX_ID_LEN {
772        return Err(Error::InvalidConfiguration);
773    }
774    Ok(())
775}
776
777fn validate_credential(psk: &[u8], psk_id: &[u8]) -> Result<(), Error> {
778    validate_id(psk_id)?;
779    if psk.len() < MIN_PSK_LEN || psk == psk_id {
780        return Err(Error::InvalidConfiguration);
781    }
782    Ok(())
783}
784
785fn unix_time_seconds() -> Result<u64, Error> {
786    SystemTime::now()
787        .duration_since(UNIX_EPOCH)
788        .map_err(|_| Error::ClockUnavailable)
789        .map(|duration| duration.as_secs())
790}
791
792fn replay_retention_deadline(issued_at_unix_s: u64, now_unix_s: u64) -> Result<u64, Error> {
793    let latest_issued_at = now_unix_s
794        .checked_add(CLOCK_SKEW_SECS)
795        .ok_or(Error::InvalidRequestTime)?;
796    let retain_until_exclusive = issued_at_unix_s
797        .checked_add(REQUEST_LIFETIME_SECS)
798        .and_then(|value| value.checked_add(CLOCK_SKEW_SECS))
799        .ok_or(Error::InvalidRequestTime)?;
800    if issued_at_unix_s > latest_issued_at || now_unix_s >= retain_until_exclusive {
801        return Err(Error::InvalidRequestTime);
802    }
803    Ok(retain_until_exclusive)
804}
805
806fn request_info(header: &[u8]) -> Vec<u8> {
807    let mut info = Vec::with_capacity(REQUEST_INFO_LABEL.len() + 1 + header.len());
808    info.extend_from_slice(REQUEST_INFO_LABEL);
809    info.push(0);
810    info.extend_from_slice(header);
811    info
812}
813
814fn replay_id(header: &[u8], enc: &[u8]) -> [u8; 32] {
815    let mut digest = Sha256::new();
816    digest.update(REPLAY_LABEL);
817    digest.update(header);
818    digest.update(enc);
819    digest.finalize().into()
820}
821
822fn export_response_material<C>(
823    context: &C,
824    enc: &[u8],
825    method: Method,
826    limits: Limits,
827) -> Result<ResponseMaterial, Error>
828where
829    C: Exporter,
830{
831    let enc: [u8; ENC_LEN] = enc.try_into().map_err(|_| Error::CryptoFailure)?;
832    let mut secret = Zeroizing::new([0_u8; RESPONSE_SECRET_LEN]);
833    context.export_secret(RESPONSE_EXPORT_LABEL, secret.as_mut())?;
834    Ok(ResponseMaterial {
835        secret,
836        enc,
837        method,
838        limits,
839        compression: None,
840    })
841}
842
843pub(crate) fn derive_response_key_nonce(
844    material: &ResponseMaterial,
845    response_nonce: &[u8],
846) -> Result<(ResponseKey, ResponseNonce), Error> {
847    if response_nonce.len() != RESPONSE_NONCE_LEN {
848        return Err(Error::MalformedEnvelope);
849    }
850    let mut salt = Zeroizing::new(Vec::with_capacity(ENC_LEN + RESPONSE_NONCE_LEN));
851    salt.extend_from_slice(&material.enc);
852    salt.extend_from_slice(response_nonce);
853    let hkdf = Hkdf::<Sha256>::new(Some(&salt), material.secret.as_ref());
854    let mut key = Zeroizing::new([0_u8; RESPONSE_KEY_LEN]);
855    hkdf.expand(RESPONSE_KEY_INFO, key.as_mut())
856        .map_err(|_| Error::CryptoFailure)?;
857    let mut nonce = Zeroizing::new([0_u8; AEAD_NONCE_LEN]);
858    hkdf.expand(RESPONSE_NONCE_INFO, nonce.as_mut())
859        .map_err(|_| Error::CryptoFailure)?;
860    Ok((key, nonce))
861}
862
863trait Exporter {
864    fn export_secret(&self, context: &[u8], output: &mut [u8]) -> Result<(), Error>;
865}
866
867impl Exporter for hpke::aead::AeadCtxS<HpkeAead, Kdf, Kem> {
868    fn export_secret(&self, context: &[u8], output: &mut [u8]) -> Result<(), Error> {
869        self.export(context, output)
870            .map_err(|_| Error::CryptoFailure)
871    }
872}
873
874impl Exporter for RecipientContext {
875    fn export_secret(&self, context: &[u8], output: &mut [u8]) -> Result<(), Error> {
876        self.export(context, output)
877            .map_err(|_| Error::CryptoFailure)
878    }
879}