Skip to main content

cratestack_cose/call/
payload.rs

1//! The payload-type half of a [`CallBinding`]: validation, the selector
2//! headers, and the response's type (cratestack#1168, compact wire keys).
3//!
4//! The rules are `cratestack-client-rust`'s `check_payload_types` and
5//! `PendingResponse::open`, so a Dart or wasm app seals exactly what the
6//! Rust client does.
7
8use std::borrow::Cow;
9
10use cratestack_core::{
11    Binding, CratestackError, DEFAULT_PAYLOAD_MEDIA_TYPE, PAYLOAD_ACCEPT_HEADER,
12    PAYLOAD_TYPE_HEADER, ResponseBinding, is_sealable_payload_type, parse_payload_accept,
13    parse_payload_type, request_digest,
14};
15
16use super::CallBinding;
17
18impl CallBinding {
19    /// The type the request payload is sealed under: the CBOR codec's
20    /// unless [`payload_type`](Self::payload_type) says otherwise.
21    pub fn request_payload_type(&self) -> &str {
22        self.payload_type
23            .as_deref()
24            .unwrap_or(DEFAULT_PAYLOAD_MEDIA_TYPE)
25    }
26
27    /// The types the call reads its answer in, in order of preference.
28    pub fn accepted_payload_types(&self) -> Vec<&str> {
29        match &self.payload_accept {
30            Some(types) => types.iter().map(String::as_str).collect(),
31            None => vec![DEFAULT_PAYLOAD_MEDIA_TYPE],
32        }
33    }
34
35    /// Check the payload type and the accept list: each must be sealable
36    /// (`cratestack_core::is_sealable_payload_type`, so never an envelope,
37    /// a `+cbor-seq` stream or a multipart body) and the list well formed.
38    /// Fails with `Validation`, before anything is sealed.
39    pub fn validate(&self) -> Result<(), CratestackError> {
40        let payload_type = self.request_payload_type();
41        if !is_sealable_payload_type(payload_type) {
42            return Err(CratestackError::Validation(format!(
43                "a COSE envelope cannot wrap a payload of type {payload_type:?}: a payload type \
44                 is a lowercase type/subtype without parameters, and never an envelope, a \
45                 stream or a multipart body"
46            )));
47        }
48        let joined = self.accepted_payload_types().join(", ");
49        let accept = parse_payload_accept(joined.as_bytes())
50            .map_err(|error| CratestackError::Validation(error.to_string()))?;
51        match accept.iter().find(|entry| !is_sealable_payload_type(entry)) {
52            Some(entry) => Err(CratestackError::Validation(format!(
53                "a COSE envelope cannot carry an answer of type {entry:?}"
54            ))),
55            None => Ok(()),
56        }
57    }
58
59    /// The payload-type selector headers this call must send, as
60    /// `(name, value)`: `Cratestack-Payload-Type` when the request type is
61    /// not CBOR, `Cratestack-Payload-Accept` when the accept list is not
62    /// just CBOR. Empty for a default call, which sends neither. They are
63    /// not themselves signed; the binding makes a wrong one fail
64    /// verification.
65    pub fn selector_headers(&self) -> Vec<(&'static str, String)> {
66        let mut headers = Vec::new();
67        if self.request_payload_type() != DEFAULT_PAYLOAD_MEDIA_TYPE {
68            headers.push((PAYLOAD_TYPE_HEADER, self.request_payload_type().to_owned()));
69        }
70        let accept = self.accepted_payload_types();
71        if accept != [DEFAULT_PAYLOAD_MEDIA_TYPE] {
72            headers.push((PAYLOAD_ACCEPT_HEADER, accept.join(", ")));
73        }
74        headers
75    }
76
77    /// The binding of the response to `sealed_request` with the HTTP
78    /// `status`, sealed under the type the response's
79    /// `Cratestack-Payload-Type` header named (`response_type`; `None`
80    /// means CBOR). The type is the binding's `payload_media_type`.
81    ///
82    /// A type the call did not ask for (not in the accept list) is
83    /// `Unauthorized`, the coarse refusal, before any body is opened. The
84    /// header is not authenticated by itself: it names the string the
85    /// binding is rebuilt with, and a wrong one fails verification.
86    pub fn response_for(
87        &self,
88        sealed_request: &[u8],
89        status: u16,
90        response_type: Option<&str>,
91    ) -> Result<Binding<'_>, CratestackError> {
92        let named = match response_type {
93            None => DEFAULT_PAYLOAD_MEDIA_TYPE,
94            Some(text) => parse_payload_type(text.as_bytes()).map_err(|_| unexpected())?,
95        };
96        let accepted = self.accepted_payload_types();
97        let Some(&payload) = accepted.iter().find(|entry| **entry == named) else {
98            return Err(unexpected());
99        };
100        let mut binding = self.request();
101        binding.payload_media_type = Cow::Owned(payload.to_owned());
102        binding.response = Some(ResponseBinding {
103            request: request_digest(sealed_request),
104            status,
105        });
106        Ok(binding)
107    }
108}
109
110/// A response type the call did not ask for: the coarse refusal, with no
111/// detail (the received header is untrusted).
112fn unexpected() -> CratestackError {
113    CratestackError::Unauthorized(crate::UNAUTHENTICATED.to_owned())
114}