Skip to main content

cratestack_axum/envelope_layer/
server_envelope.rs

1//! [`ServerEnvelope`]: the seam between the layer and whatever verifies and
2//! signs (the COSE envelope by default).
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use cratestack_core::{Binding, CratestackError};
7
8use super::opened::{OpenedRequest, SealContext, Sealed};
9
10/// What the layer needs from an envelope on the server side: open a request
11/// against its binding, seal a response against its binding.
12///
13/// `cratestack_cose::CoseEnvelope` implements it (feature `cose`).
14/// Implement it yourself to put a different verifier behind the same layer:
15/// an envelope whose keys live in an HSM or a KMS (for COSE, a custom
16/// `CoseSigner` / `CoseVerifierResolver` on a `CoseEnvelope` is usually
17/// less work), or the Sign1 + Mac0 composite of cratestack#1078. With the
18/// `envelope` feature alone, no COSE crate is compiled.
19///
20/// It is not `cratestack_core::CratestackEnvelope`, which is `Clone` (so not
21/// object-safe) and records its signer on a context; the layer runs before
22/// any context exists and holds the envelope as `dyn`.
23///
24/// # What the layer guarantees whatever the implementation does
25///
26/// - `open_request` is the only way a request reaches the router as
27///   "signed", and it is called for every request whose `Content-Type` is
28///   `application/cose`, whether or not
29///   [`is_envelope_content_type`](Self::is_envelope_content_type) says so.
30/// - An `Err(CratestackError::Unauthorized(_))` becomes the layer's own
31///   coarse, unsigned `401`; the message is dropped. Any other `Err` is a
32///   `500` whose detail is logged, never sent.
33/// - The binding passed in is built by the layer. The response binding for
34///   [`seal_response`](Self::seal_response) reuses the request's values and
35///   adds the request digest the layer computed (SHA-256 of the exact bytes
36///   handed to `open_request`) and the status.
37/// - The [`SealContext`] `open_request` returned comes back, untouched, to
38///   the `seal_response` of the same request, and to no other; a response
39///   to an unsigned request gets [`SealContext::empty`].
40/// - A [`Sealed`] whose media type is not `application/cose` (any
41///   parameters) or one this envelope claims through
42///   `is_envelope_content_type` is never sent: the layer answers with an
43///   unsigned `500` instead, so a response is never labelled as plain.
44///
45/// # What the implementation must do
46///
47/// - Verify **everything** the binding names (audience, method, route, path
48///   parameters, query, schema digest, payload media type, bound headers)
49///   and enforce replay protection before returning `Ok`. The layer cannot
50///   check that a verifier verified.
51/// - Return `Unauthorized` for every verification failure, and something
52///   else (`Internal`, `Unavailable`) only for a local or backend failure.
53/// - Return the payload exactly as signed, and the signer that actually
54///   verified it (for COSE: the key's full thumbprint, never the claimed
55///   `kid` alone).
56///
57/// # Wrapping another envelope
58///
59/// Delegate through the trait's fully qualified path,
60/// `ServerEnvelope::open_request(&self.inner, ..)`. `CoseEnvelope` also has
61/// inherent `open_request` / `seal_response` methods, its typed API (they
62/// return `Opened` and `Bytes`), and method-call syntax picks those over
63/// the trait's, so `self.inner.open_request(body, bind)` does not type-check
64/// here (second-review item S-3).
65#[cfg_attr(
66    feature = "cose",
67    doc = r#"
68```
69use std::sync::atomic::{AtomicUsize, Ordering};
70
71use bytes::Bytes;
72use cratestack_axum::envelope_layer::{
73    OpenedRequest, SealContext, Sealed, ServerEnvelope, async_trait,
74};
75use cratestack_core::{Binding, CratestackError};
76use cratestack_cose::CoseEnvelope;
77
78/// The COSE envelope, counting the requests it opens.
79struct Counted {
80    inner: CoseEnvelope,
81    opened: AtomicUsize,
82}
83
84#[async_trait]
85impl ServerEnvelope for Counted {
86    fn media_type(&self) -> &'static str {
87        ServerEnvelope::media_type(&self.inner)
88    }
89
90    fn is_envelope_content_type(&self, content_type: &str) -> bool {
91        ServerEnvelope::is_envelope_content_type(&self.inner, content_type)
92    }
93
94    async fn open_request(
95        &self,
96        body: Bytes,
97        bind: &Binding<'_>,
98    ) -> Result<OpenedRequest, CratestackError> {
99        self.opened.fetch_add(1, Ordering::Relaxed);
100        ServerEnvelope::open_request(&self.inner, body, bind).await
101    }
102
103    async fn seal_response(
104        &self,
105        payload: &[u8],
106        bind: &Binding<'_>,
107        context: &SealContext,
108    ) -> Result<Sealed, CratestackError> {
109        ServerEnvelope::seal_response(&self.inner, payload, bind, context).await
110    }
111}
112```
113
114The method-call form reaches the inherent method instead, and fails:
115
116```compile_fail,E0308
117# use bytes::Bytes;
118# use cratestack_axum::envelope_layer::OpenedRequest;
119# use cratestack_core::{Binding, CratestackError};
120# use cratestack_cose::CoseEnvelope;
121async fn open(
122    inner: &CoseEnvelope,
123    body: Bytes,
124    bind: &Binding<'_>,
125) -> Result<OpenedRequest, CratestackError> {
126    inner.open_request(body, bind).await
127}
128```
129"#
130)]
131#[async_trait]
132pub trait ServerEnvelope: Send + Sync + 'static {
133    /// The envelope type this implementation claims, e.g.
134    /// `application/cose; cose-type="cose-sign1"`, used for detection and
135    /// build validation: `build()` refuses a value that is not a valid
136    /// header value, or that the layer's own detection would not recognise
137    /// on a request (`application/cose`, or a type
138    /// [`is_envelope_content_type`](Self::is_envelope_content_type)
139    /// claims), so a client sending it is always opened, never forwarded.
140    /// The layer detects requests with those two checks, not with this
141    /// value, and does not send it: each response goes out as the
142    /// [`Sealed`]'s own media type, which lets a composite answer Mac0
143    /// with Mac0 (cratestack#1078).
144    fn media_type(&self) -> &'static str;
145
146    /// Whether `content_type` (a request's `Content-Type`, or one `Accept`
147    /// entry, parameters included) names this envelope's framing. Only
148    /// needed for a framing other than `application/cose`, which the layer
149    /// always recognises itself; the default recognises nothing more.
150    fn is_envelope_content_type(&self, content_type: &str) -> bool {
151        let _ = content_type;
152        false
153    }
154
155    /// Verify `body` (the request body exactly as received) against `bind`,
156    /// run the replay checks, and return the payload, the signer, and
157    /// anything the response to this request must know (the
158    /// [`SealContext`], e.g. that a composite answered Mac0 with Mac0).
159    async fn open_request(
160        &self,
161        body: Bytes,
162        bind: &Binding<'_>,
163    ) -> Result<OpenedRequest, CratestackError>;
164
165    /// [`open_request`](Self::open_request) against several candidate
166    /// bindings that **must** differ only in `contract_sha`, the op-contract
167    /// digest; any other difference (audience, route, method, path params,
168    /// query, payload type, bound headers) would widen what a signature
169    /// proves (the COSE envelope refuses it as misuse). Returns the opened
170    /// request and the index that verified. The default tries each in turn,
171    /// moving on only after the coarse `401`, so it repeats parse and key
172    /// resolution per candidate; the COSE envelope overrides it to do both
173    /// once and repeat only the verification. Empty `binds` is `Unauthorized`.
174    async fn open_request_any(
175        &self,
176        body: Bytes,
177        binds: &[Binding<'_>],
178    ) -> Result<(OpenedRequest, usize), CratestackError> {
179        let mut refused = CratestackError::Unauthorized(String::new());
180        for (index, bind) in binds.iter().enumerate() {
181            // A refcount bump: the next candidate needs the body again.
182            match self.open_request(body.clone(), bind).await {
183                Ok(opened) => return Ok((opened, index)),
184                Err(error @ CratestackError::Unauthorized(_)) => refused = error,
185                Err(error) => return Err(error),
186            }
187        }
188        Err(refused)
189    }
190
191    /// Seal `payload` (the response body the router produced, CBOR) for
192    /// `bind`, a response binding, with the `context` its request's
193    /// `open_request` returned.
194    async fn seal_response(
195        &self,
196        payload: &[u8],
197        bind: &Binding<'_>,
198        context: &SealContext,
199    ) -> Result<Sealed, CratestackError>;
200}