Skip to main content

attested_request/
base.rs

1//! The RFC 9421 signature base both sides sign and verify.
2
3use std::fmt;
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use http::uri::{Authority, InvalidUri};
7use sha2::{Digest, Sha256};
8
9use crate::{profile::Component, signature::SignatureParams};
10
11/// The request as the client addressed it: the values the covered components resolve to.
12///
13/// `scheme` and `authority` are what the client dialled. A verifier takes them from its own
14/// configuration, never from the request's `Host` header, which infrastructure may rewrite.
15#[derive(Debug, Clone)]
16pub struct CanonicalRequest<'a> {
17    method: &'a str,
18    scheme: &'a str,
19    authority: Authority,
20    path: &'a str,
21    query: Option<&'a str>,
22    body: &'a [u8],
23}
24
25impl<'a> CanonicalRequest<'a> {
26    /// A request from its parts. `path` is percent-encoded and `query` excludes the leading `?`.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if `authority` cannot be parsed as a URI authority.
31    pub fn new(
32        method: &'a str,
33        scheme: &'a str,
34        authority: &'a str,
35        path: &'a str,
36        query: Option<&'a str>,
37        body: &'a [u8],
38    ) -> Result<Self, InvalidUri> {
39        Ok(Self {
40            method,
41            scheme,
42            authority: authority.parse()?,
43            path,
44            query,
45            body,
46        })
47    }
48
49    /// A received request, with `scheme` and `authority` from verifier configuration.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if `authority` cannot be parsed as a URI authority.
54    pub fn from_http(
55        method: &'a http::Method,
56        uri: &'a http::Uri,
57        scheme: &'a str,
58        authority: &'a str,
59        body: &'a [u8],
60    ) -> Result<Self, InvalidUri> {
61        Self::new(
62            method.as_str(),
63            scheme,
64            authority,
65            uri.path(),
66            uri.query(),
67            body,
68        )
69    }
70
71    /// The values the covered components resolve to, with `integrity_token` as the token.
72    /// Scheme and host are lowercased, and the HTTP(S) default port is omitted.
73    #[must_use]
74    pub fn component_values(&self, integrity_token: &str) -> ComponentValues {
75        let scheme = self.scheme.to_ascii_lowercase();
76        let authority = match (scheme.as_str(), self.authority.port_u16()) {
77            ("http", Some(80)) | ("https", Some(443)) => self.authority.host(),
78            _ => self.authority.as_str(),
79        }
80        .to_ascii_lowercase();
81
82        ComponentValues {
83            method: self.method.to_owned(),
84            scheme,
85            authority,
86            path: if self.path.is_empty() { "/" } else { self.path }.to_owned(),
87            query: format!("?{}", self.query.unwrap_or_default()),
88            content_digest: content_digest(self.body),
89            integrity_token: integrity_token.to_owned(),
90        }
91    }
92
93    /// Builds the signature base for `params`, with `integrity_token` as the token component.
94    #[must_use]
95    pub fn signature_base(&self, params: &SignatureParams, integrity_token: &str) -> SignatureBase {
96        self.component_values(integrity_token)
97            .signature_base(params)
98    }
99}
100
101/// The resolved value of every covered component.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct ComponentValues {
104    /// `@method`.
105    pub method: String,
106    /// `@scheme`.
107    pub scheme: String,
108    /// `@authority`.
109    pub authority: String,
110    /// `@path`, `/` when the request path is empty.
111    pub path: String,
112    /// `@query`, including the leading `?`.
113    pub query: String,
114    /// `content-digest`.
115    pub content_digest: String,
116    /// `integrity-token`.
117    pub integrity_token: String,
118}
119
120impl ComponentValues {
121    /// The value `component` resolves to.
122    #[must_use]
123    pub fn get(&self, component: Component) -> &str {
124        match component {
125            Component::Method => &self.method,
126            Component::Scheme => &self.scheme,
127            Component::Authority => &self.authority,
128            Component::Path => &self.path,
129            Component::Query => &self.query,
130            Component::ContentDigest => &self.content_digest,
131            Component::IntegrityToken => &self.integrity_token,
132        }
133    }
134
135    /// Builds the signature base (RFC 9421 ยง2.5): one line per covered component in signed
136    /// order, then `@signature-params`.
137    ///
138    /// Every value comes from a parsed URI, a method token or a header value, none of which can
139    /// hold a line break, so no value can add a line to the base.
140    #[must_use]
141    pub fn signature_base(&self, params: &SignatureParams) -> SignatureBase {
142        let mut lines: Vec<String> = params
143            .components()
144            .iter()
145            .map(|component| format!("\"{component}\": {}", self.get(*component)))
146            .collect();
147        lines.push(format!("\"@signature-params\": {}", params.serialize()));
148        SignatureBase(lines.join("\n"))
149    }
150}
151
152/// A serialized signature base.
153#[derive(Clone, PartialEq, Eq)]
154pub struct SignatureBase(String);
155
156impl SignatureBase {
157    /// The base as text.
158    #[must_use]
159    pub fn as_str(&self) -> &str {
160        &self.0
161    }
162
163    /// `SHA-256(base)`: the digest the device key signs, which App Attest calls `clientDataHash`.
164    #[must_use]
165    pub fn client_data_hash(&self) -> [u8; 32] {
166        Sha256::digest(self.0.as_bytes()).into()
167    }
168
169    /// `base64(SHA-256(base))`: identifies this exact request, for replay tracking and for
170    /// binding a response to it.
171    #[must_use]
172    pub fn request_binding(&self) -> String {
173        STANDARD.encode(self.client_data_hash())
174    }
175}
176
177// The base embeds the integrity token, so keep it out of debug output.
178impl fmt::Debug for SignatureBase {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.write_str("SignatureBase(..)")
181    }
182}
183
184/// The RFC 9530 `content-digest` value of `body`: `sha-256=:<base64(SHA-256(body))>:`.
185#[must_use]
186pub fn content_digest(body: &[u8]) -> String {
187    format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
188}