Skip to main content

cratestack_axum/envelope_layer/
seal_policy.rs

1//! [`ResponseSealPolicy`]: under `Optional`, whether an unsigned request's
2//! response is sealed (maintainer decision D10).
3
4use http::{HeaderMap, Method};
5
6use super::resolver::ResolvedRoute;
7
8/// An unsigned request under `Optional` that carries a valid
9/// `Cratestack-Nonce`, as a [`ResponseSealPolicy`] sees it.
10#[derive(Debug)]
11pub struct UnsignedRequest<'a> {
12    method: &'a Method,
13    route: &'a ResolvedRoute,
14    headers: &'a HeaderMap,
15    accept_names_envelope: bool,
16}
17
18impl<'a> UnsignedRequest<'a> {
19    /// A request as the layer describes it; `accept_names_envelope` is the
20    /// layer's own reading of `headers`. Public so a policy can be
21    /// unit-tested (unlike [`super::VerifiedRequest`], nothing here claims a
22    /// verification happened).
23    pub fn new(
24        method: &'a Method,
25        route: &'a ResolvedRoute,
26        headers: &'a HeaderMap,
27        accept_names_envelope: bool,
28    ) -> Self {
29        Self {
30            method,
31            route,
32            headers,
33            accept_names_envelope,
34        }
35    }
36
37    /// The request method.
38    pub fn method(&self) -> &'a Method {
39        self.method
40    }
41
42    /// The op it addresses.
43    pub fn route(&self) -> &'a ResolvedRoute {
44        self.route
45    }
46
47    /// Its headers.
48    pub fn headers(&self) -> &'a HeaderMap {
49        self.headers
50    }
51
52    /// Whether an `Accept` entry with a non-zero weight names
53    /// `application/cose` or the envelope's own media type.
54    pub fn accept_names_envelope(&self) -> bool {
55        self.accept_names_envelope
56    }
57}
58
59/// Decides whether the response to an unsigned request is sealed, under
60/// `Optional`. The default is [`AcceptNamesEnvelope`] (D10). A closure is a
61/// policy (second-review nit):
62///
63/// ```
64/// use cratestack_axum::envelope_layer::{EnvelopeLayerBuilder, UnsignedRequest};
65///
66/// fn reads_only(builder: EnvelopeLayerBuilder) -> EnvelopeLayerBuilder {
67///     builder.response_seal_policy(|r: &UnsignedRequest<'_>| {
68///         r.accept_names_envelope() && r.method().as_str() == "GET"
69///     })
70/// }
71/// # let _ = reads_only;
72/// ```
73///
74/// Such a seal binds the client's `Cratestack-Nonce` and the request
75/// payload, and nothing about the caller: the request was not signed, so
76/// the response proves only that this server answered this nonce and body,
77/// not who asked. A client must not read it as an authenticated exchange.
78///
79/// **What the layer enforces whatever this returns:** it is asked only
80/// under `Optional`, only about an unsigned request, and only when that
81/// request carries a well-formed `Cratestack-Nonce` (without one the
82/// response could not be bound to this request, and a cached one could be
83/// replayed for the next). A signed request's response is always sealed,
84/// and under `Required` every response is: neither is this policy's call.
85pub trait ResponseSealPolicy: Send + Sync + 'static {
86    /// `true` to seal this request's response.
87    fn seal_unsigned(&self, request: &UnsignedRequest<'_>) -> bool;
88}
89
90/// Seal when the request's `Accept` names `application/cose` (D10).
91#[derive(Debug, Clone, Copy, Default)]
92pub struct AcceptNamesEnvelope;
93
94impl ResponseSealPolicy for AcceptNamesEnvelope {
95    fn seal_unsigned(&self, request: &UnsignedRequest<'_>) -> bool {
96        request.accept_names_envelope
97    }
98}
99
100impl<F> ResponseSealPolicy for F
101where
102    F: Fn(&UnsignedRequest<'_>) -> bool + Send + Sync + 'static,
103{
104    fn seal_unsigned(&self, request: &UnsignedRequest<'_>) -> bool {
105        self(request)
106    }
107}