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}