Skip to main content

asimov_http/
body.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Frame-preserving HTTP/1.1 and HTTP/2 body adapters.
4//!
5//! `SigningBody` hashes DATA frames as they are polled and emits a final trailer
6//! frame containing the digest and signature. It never buffers the content.
7//! `VerifyingBody` preserves data frame boundaries and withholds trailers until
8//! EOF verification succeeds. Data delivered before then is provisional.
9//!
10//! HTTP/1.1 senders must use chunked framing and receivers must advertise
11//! `TE: trailers`; HTTP/2 carries trailing HEADERS directly. `SigningBody`
12//! removes Content-Length and declares its trailers before transmission.
13//! Content coding must happen before signing and decoding after verification.
14
15use crate::{
16    ContentDigest, Error, FieldSection, Message, Signer, VerifiedSignature, Verifier, fields,
17};
18use alloc::{boxed::Box, string::String};
19use bytes::Bytes;
20use core::{
21    pin::Pin,
22    task::{Context, Poll},
23};
24use http::{
25    HeaderMap, HeaderValue, Request, Response, Version,
26    header::{CONTENT_LENGTH, TRAILER, TRANSFER_ENCODING},
27};
28use http_body::{Body, Frame, SizeHint};
29use sha2::{Digest as _, Sha256};
30
31/// A source-body failure or terminal integrity failure.
32#[derive(Debug, thiserror::Error)]
33pub enum BodyError<E> {
34    /// The underlying transport/body failed; no completion fields are emitted.
35    #[error("HTTP body failed: {0}")]
36    Inner(E),
37    /// Digest/signature construction or verification failed.
38    #[error(transparent)]
39    Integrity(#[from] Error),
40}
41
42/// Signs an already buffered request in its headers, without reserializing it.
43/// The signer must cover the SHA-256 content digest in the header section.
44pub fn sign_request<B: AsRef<[u8]>>(
45    request: &mut Request<B>,
46    effective_uri: &str,
47    signer: &Signer,
48) -> Result<(), Error> {
49    if !signer.input().covers_digest(FieldSection::Headers) {
50        return Err(Error::Policy("unsigned content digest"));
51    }
52    let mut message = Message::from_request(request, effective_uri)?;
53    message.headers.insert(
54        "content-digest",
55        fields::header(ContentDigest::new(request.body().as_ref()).to_field_value())?,
56    );
57    signer.sign(&message)?.append_to(&mut message.headers)?;
58    *request.headers_mut() = message.headers;
59    Ok(())
60}
61
62/// Verifies a buffered request's signature AND body. The caller may then
63/// authorize the key and atomically consume the authenticated nonce.
64pub fn verify_request<B: AsRef<[u8]>>(
65    request: &Request<B>,
66    effective_uri: &str,
67    verifier: &Verifier,
68    now: i64,
69) -> Result<VerifiedSignature, Error> {
70    let message = Message::from_request(request, effective_uri)?;
71    let verified = verifier.verify(&message, now)?;
72    require_digest_coverage(&verified, &message)?;
73    ContentDigest::new(request.body().as_ref()).verify(&message.headers, &message.trailers)?;
74    Ok(verified)
75}
76
77/// Signs a buffered response in its headers. Representation data and framing
78/// remain unchanged; only a content digest and signature fields are added.
79pub fn sign_buffered_response<B: AsRef<[u8]>>(
80    response: &mut Response<B>,
81    request: Option<Message>,
82    signer: &Signer,
83) -> Result<(), Error> {
84    if !signer.input().covers_digest(FieldSection::Headers) {
85        return Err(Error::Policy("unsigned content digest"));
86    }
87    let mut message = Message::response(response.status(), response.headers().clone(), request)?;
88    let content = response.body().as_ref();
89    if !message.permits_response_trailers() && !content.is_empty() {
90        return Err(Error::InvalidField(
91            "content is forbidden for this response",
92        ));
93    }
94    message.headers.insert(
95        "content-digest",
96        fields::header(ContentDigest::new(content).to_field_value())?,
97    );
98    signer.sign(&message)?.append_to(&mut message.headers)?;
99    *response.headers_mut() = message.headers;
100    Ok(())
101}
102
103/// A streaming response body that emits SHA-256 and optional Ed25519 trailers.
104/// No `Sync` or `Unpin` bound is imposed on the wrapped body.
105pub struct SigningBody<B> {
106    inner: Pin<Box<B>>,
107    hash: Sha256,
108    message: Message,
109    signer: Option<Signer>,
110    input_section: FieldSection,
111    trailers: Option<HeaderMap>,
112    done: bool,
113}
114
115impl<B> SigningBody<B> {
116    /// Wraps a response, declaring completion trailers before any data is sent.
117    ///
118    /// `None` emits only the digest. With a signer, `Signature` is a trailer;
119    /// `Signature-Input` can be in either section. The input must cover
120    /// `"content-digest";tr` (optionally with `sf` or `key="sha-256"`).
121    /// HEAD, bodyless status codes, successful CONNECT, and preexisting content
122    /// digests are rejected. For known digests/signatures in headers, use
123    /// `sign_buffered_response` or the lower-level `Signer` API instead.
124    pub fn response(
125        mut response: Response<B>,
126        request: Option<Message>,
127        signer: Option<Signer>,
128        input_section: FieldSection,
129    ) -> Result<Response<Self>, Error> {
130        if !matches!(response.version(), Version::HTTP_11 | Version::HTTP_2) {
131            return Err(Error::TrailersUnavailable);
132        }
133        let mut message =
134            Message::response(response.status(), response.headers().clone(), request)?;
135        if !message.permits_response_trailers() {
136            return Err(Error::TrailersUnavailable);
137        }
138        if message.headers.contains_key("content-digest") {
139            return Err(Error::InvalidField("content digest already supplied"));
140        }
141        // An unknown size is essential for HTTP/1.1 to keep polling through the
142        // final trailer frame, including for empty content.
143        message.headers.remove(CONTENT_LENGTH);
144        message.headers.remove(TRANSFER_ENCODING);
145        let mut names = alloc::vec!["content-digest"];
146        if let Some(signer) = &signer {
147            if !signer.input().covers_digest(FieldSection::Trailers) {
148                return Err(Error::Policy("unsigned trailer digest"));
149            }
150            check_label(&message.headers, signer.label())?;
151            signer.input().preflight(&message)?;
152            names.push("signature");
153            match input_section {
154                FieldSection::Headers => {
155                    message
156                        .headers
157                        .append("signature-input", signer.input_header()?);
158                },
159                FieldSection::Trailers => names.push("signature-input"),
160            }
161        }
162        declare_trailers(&mut message.headers, &names)?;
163        *response.headers_mut() = message.headers.clone();
164        Ok(response.map(|inner| Self {
165            inner: Box::pin(inner),
166            hash: Sha256::new(),
167            message,
168            signer,
169            input_section,
170            trailers: None,
171            done: false,
172        }))
173    }
174
175    fn finish(&mut self) -> Result<HeaderMap, Error> {
176        let mut trailers = self.trailers.take().unwrap_or_default();
177        if trailers.contains_key("content-digest") {
178            return Err(Error::InvalidField("body supplied its own digest"));
179        }
180        let digest = ContentDigest::from_bytes(self.hash.clone().finalize().into());
181        trailers.insert("content-digest", fields::header(digest.to_field_value())?);
182        if let Some(signer) = &self.signer {
183            check_label(&trailers, signer.label())?;
184            self.message.trailers = trailers.clone();
185            let signature = signer.sign(&self.message)?;
186            if self.input_section == FieldSection::Trailers {
187                signature.append_to(&mut trailers)?;
188            } else {
189                trailers.append("signature", signature.signature);
190            }
191        }
192        Ok(trailers)
193    }
194}
195
196impl<B: Body<Data = Bytes>> Body for SigningBody<B> {
197    type Data = Bytes;
198    type Error = BodyError<B::Error>;
199
200    fn poll_frame(
201        self: Pin<&mut Self>,
202        cx: &mut Context<'_>,
203    ) -> Poll<Option<Result<Frame<Bytes>, Self::Error>>> {
204        let this = self.get_mut();
205        if this.done {
206            return Poll::Ready(None);
207        }
208        loop {
209            match this.inner.as_mut().poll_frame(cx) {
210                Poll::Pending => return Poll::Pending,
211                Poll::Ready(Some(Err(error))) => {
212                    this.done = true;
213                    return Poll::Ready(Some(Err(BodyError::Inner(error))));
214                },
215                Poll::Ready(Some(Ok(frame))) => {
216                    if this.trailers.is_some() {
217                        this.done = true;
218                        return Poll::Ready(Some(Err(Error::FrameOrder.into())));
219                    }
220                    match frame.into_data() {
221                        Ok(data) => {
222                            this.hash.update(&data);
223                            return Poll::Ready(Some(Ok(Frame::data(data))));
224                        },
225                        Err(frame) => match frame.into_trailers() {
226                            Ok(trailers) => this.trailers = Some(trailers),
227                            Err(_) => {
228                                this.done = true;
229                                return Poll::Ready(Some(Err(Error::FrameOrder.into())));
230                            },
231                        },
232                    }
233                },
234                Poll::Ready(None) => {
235                    this.done = true;
236                    return Poll::Ready(Some(
237                        this.finish()
238                            .map(Frame::trailers)
239                            .map_err(BodyError::Integrity),
240                    ));
241                },
242            }
243        }
244    }
245
246    fn is_end_stream(&self) -> bool {
247        self.done
248    }
249    fn size_hint(&self) -> SizeHint {
250        SizeHint::default()
251    }
252}
253
254/// A frame-preserving body that verifies headers/trailers and content at EOF.
255///
256/// Data frames are provisional until `is_verified()` becomes true. Dropping the
257/// body, a source error, missing trailers, or an integrity failure never yields
258/// verified completion. A failure is emitted once and the body then terminates.
259///
260/// A policy entry for `"content-digest"` or `"content-digest";tr` restricts
261/// placement to that section. To let the server choose, omit the response's
262/// digest from [`crate::VerificationPolicy::required_components`]. With a
263/// verifier, this adapter still requires a matching SHA-256 digest and a valid
264/// signature covering it in its actual section, checked at EOF.
265///
266/// # Example
267///
268/// Verify a Hyper response against a trusted server key, allowing either digest
269/// location. The sender uses the same key ID and the `asimov` signature label.
270///
271/// ```no_run
272/// # extern crate alloc;
273/// use alloc::{boxed::Box, vec};
274/// use asimov_http::{
275///     Component, SignatureParams, VerificationPolicy, Verifier, VerifyingKey,
276///     body::VerifyingBody, http::Response, key_id,
277/// };
278/// use http_body_util::BodyExt as _;
279///
280/// async fn verify_response(
281///     response: Response<hyper::body::Incoming>,
282///     server_key: VerifyingKey,
283/// ) -> Result<(), Box<dyn core::error::Error>> {
284///     let policy = VerificationPolicy::new(vec![
285///         Component::new("@status")?,
286///         Component::new("content-type")?,
287///     ]);
288///     let verifier = Verifier::new(
289///         server_key, &key_id(&server_key), "asimov", policy,
290///     )?;
291///     let mut body = VerifyingBody::response(
292///         response, None, Some(verifier), SignatureParams::now()?,
293///     )?.into_body();
294///
295///     while let Some(frame) = body.frame().await {
296///         if let Ok(data) = frame?.into_data() {
297///             println!("Received {} provisional bytes", data.len());
298///         }
299///     }
300///     // EOF was reached without a transport, digest, or signature error.
301///     assert!(body.is_verified());
302///     Ok(())
303/// }
304/// ```
305pub struct VerifyingBody<B> {
306    inner: Pin<Box<B>>,
307    hash: Sha256,
308    message: Message,
309    verifier: Option<Verifier>,
310    now: i64,
311    trailers: Option<HeaderMap>,
312    done: bool,
313    verified: bool,
314    signature: Option<VerifiedSignature>,
315}
316
317impl<B> VerifyingBody<B> {
318    /// Wraps a body and its immutable message context. `None` verifies only the
319    /// SHA-256 digest; a verifier additionally requires authenticated coverage
320    /// of that digest. `now` is the caller's chosen validation instant.
321    pub fn new(inner: B, mut message: Message, verifier: Option<Verifier>, now: i64) -> Self {
322        // Only trailers observed from this body can establish its completion.
323        message.trailers.clear();
324        Self {
325            inner: Box::pin(inner),
326            hash: Sha256::new(),
327            message,
328            verifier,
329            now,
330            trailers: None,
331            done: false,
332            verified: false,
333            signature: None,
334        }
335    }
336
337    /// Wraps an HTTP response of any body type without consuming its data.
338    pub fn response(
339        response: Response<B>,
340        request: Option<Message>,
341        verifier: Option<Verifier>,
342        now: i64,
343    ) -> Result<Response<Self>, Error> {
344        let message = Message::response(response.status(), response.headers().clone(), request)?;
345        Ok(response.map(|inner| Self::new(inner, message, verifier, now)))
346    }
347
348    /// True only after the source reached EOF and all required checks succeeded.
349    pub const fn is_verified(&self) -> bool {
350        self.verified
351    }
352
353    /// Authenticated signature metadata, available only after full verification.
354    pub fn verified_signature(&self) -> Option<&VerifiedSignature> {
355        self.signature.as_ref()
356    }
357
358    fn finish(&mut self) -> Result<Option<HeaderMap>, Error> {
359        let trailers = self.trailers.take();
360        if let Some(trailers) = &trailers {
361            self.message.trailers = trailers.clone();
362        }
363        ContentDigest::from_bytes(self.hash.clone().finalize().into())
364            .verify(&self.message.headers, &self.message.trailers)?;
365        if let Some(verifier) = &self.verifier {
366            let signature = verifier.verify(&self.message, self.now)?;
367            require_digest_coverage(&signature, &self.message)?;
368            self.signature = Some(signature);
369        }
370        self.verified = true;
371        Ok(trailers)
372    }
373}
374
375impl<B: Body<Data = Bytes>> Body for VerifyingBody<B> {
376    type Data = Bytes;
377    type Error = BodyError<B::Error>;
378
379    fn poll_frame(
380        self: Pin<&mut Self>,
381        cx: &mut Context<'_>,
382    ) -> Poll<Option<Result<Frame<Bytes>, Self::Error>>> {
383        let this = self.get_mut();
384        if this.done {
385            return Poll::Ready(None);
386        }
387        loop {
388            match this.inner.as_mut().poll_frame(cx) {
389                Poll::Pending => return Poll::Pending,
390                Poll::Ready(Some(Err(error))) => {
391                    this.done = true;
392                    return Poll::Ready(Some(Err(BodyError::Inner(error))));
393                },
394                Poll::Ready(Some(Ok(frame))) => {
395                    if this.trailers.is_some() {
396                        this.done = true;
397                        return Poll::Ready(Some(Err(Error::FrameOrder.into())));
398                    }
399                    match frame.into_data() {
400                        Ok(data) => {
401                            this.hash.update(&data);
402                            return Poll::Ready(Some(Ok(Frame::data(data))));
403                        },
404                        Err(frame) => match frame.into_trailers() {
405                            Ok(trailers) => this.trailers = Some(trailers),
406                            Err(_) => {
407                                this.done = true;
408                                return Poll::Ready(Some(Err(Error::FrameOrder.into())));
409                            },
410                        },
411                    }
412                },
413                Poll::Ready(None) => {
414                    this.done = true;
415                    return Poll::Ready(match this.finish() {
416                        Ok(Some(trailers)) => Some(Ok(Frame::trailers(trailers))),
417                        Ok(None) => None,
418                        Err(error) => Some(Err(error.into())),
419                    });
420                },
421            }
422        }
423    }
424
425    fn is_end_stream(&self) -> bool {
426        self.done
427    }
428    fn size_hint(&self) -> SizeHint {
429        SizeHint::default()
430    }
431}
432
433fn require_digest_coverage(signature: &VerifiedSignature, message: &Message) -> Result<(), Error> {
434    for section in [FieldSection::Headers, FieldSection::Trailers] {
435        if signature.input.covers_digest(section)
436            && let Some(value) = fields::combined(&[message.fields(section)], "content-digest")?
437            && ContentDigest::parse(&value).is_ok()
438        {
439            return Ok(());
440        }
441    }
442    Err(Error::Policy(
443        "signature does not cover the verified SHA-256 digest",
444    ))
445}
446
447fn check_label(headers: &HeaderMap, label: &str) -> Result<(), Error> {
448    for name in ["signature-input", "signature"] {
449        if let Some(value) = fields::combined(&[headers], name)?
450            && fields::dictionary(&value)?.contains_key(label)
451        {
452            return Err(Error::DuplicateLabel);
453        }
454    }
455    Ok(())
456}
457
458fn declare_trailers(headers: &mut HeaderMap, names: &[&str]) -> Result<(), Error> {
459    let previous = fields::combined(&[headers], "trailer")?.unwrap_or_default();
460    let mut declared: alloc::vec::Vec<String> = previous
461        .split(',')
462        .map(str::trim)
463        .filter(|name| !name.is_empty())
464        .map(str::to_ascii_lowercase)
465        .collect();
466    for name in names {
467        if !declared.iter().any(|entry| entry == name) {
468            declared.push((*name).into());
469        }
470    }
471    headers.insert(
472        TRAILER,
473        HeaderValue::try_from(declared.join(", ")).map_err(|_| Error::InvalidField("Trailer"))?,
474    );
475    Ok(())
476}