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