Skip to main content

attested_request/
sign.rs

1//! Signing canonical requests on the client.
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use crate::{
6    base::CanonicalRequest,
7    profile::Platform,
8    signature::{
9        NonceError, SignatureInputError, SignatureParams, generate_nonce, signature_field,
10        validate_nonce,
11    },
12};
13
14/// Signs with an attested device key.
15///
16/// Hardware signers block, so call [`sign_request`] off any async executor.
17pub trait Signer {
18    /// The signer's failure.
19    type Error;
20
21    /// The platform whose signature encoding [`Signer::sign`] produces.
22    fn platform(&self) -> Platform;
23
24    /// Signs `client_data_hash`, the SHA-256 of the signature base, and returns the platform
25    /// encoding: DER-encoded ECDSA on Android, a CBOR App Attest assertion on iOS.
26    ///
27    /// # Errors
28    ///
29    /// Returns the signer's error when the key cannot sign.
30    fn sign(&self, client_data_hash: &[u8; 32]) -> Result<Vec<u8>, Self::Error>;
31}
32
33/// The headers of a signed request, plus the binding a response for it may echo.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct SignedHeaders {
36    /// The `Integrity-Token` value.
37    pub integrity_token: String,
38    /// The `Signature-Input` value.
39    pub signature_input: String,
40    /// The `Signature` value.
41    pub signature: String,
42    /// `base64(SHA-256(signature base))`, identifying this exact request.
43    pub request_binding: String,
44}
45
46impl SignedHeaders {
47    /// The headers as `(name, value)` pairs.
48    #[must_use]
49    pub fn headers(&self) -> [(&'static str, &str); 3] {
50        [
51            (
52                crate::profile::INTEGRITY_TOKEN_HEADER,
53                &self.integrity_token,
54            ),
55            (
56                crate::profile::SIGNATURE_INPUT_HEADER,
57                &self.signature_input,
58            ),
59            (crate::profile::SIGNATURE_HEADER, &self.signature),
60        ]
61    }
62}
63
64/// Why a request could not be signed.
65#[derive(Debug, thiserror::Error)]
66pub enum SignError<E> {
67    /// The operating system could not supply a nonce.
68    #[error("failed to generate a nonce")]
69    Randomness(#[source] getrandom::Error),
70    /// The nonce is not canonical base64 of at least 16 bytes.
71    #[error(transparent)]
72    Nonce(#[from] NonceError),
73    /// A signature parameter cannot be serialized.
74    #[error(transparent)]
75    Params(#[from] SignatureInputError),
76    /// The signer failed.
77    #[error("the signer failed")]
78    Signer(#[source] E),
79}
80
81/// Signs `request` now, with a fresh nonce.
82///
83/// Every attempt, including a retry or a reconnect, must be signed again: the nonce and `created`
84/// are what distinguish it.
85///
86/// # Errors
87///
88/// Returns a [`SignError`] when no nonce can be generated or the signer fails.
89pub fn sign_request<S: Signer>(
90    request: &CanonicalRequest<'_>,
91    integrity_token: &str,
92    signer: &S,
93) -> Result<SignedHeaders, SignError<S::Error>> {
94    let created = SystemTime::now()
95        .duration_since(UNIX_EPOCH)
96        .map_or(0, |elapsed| elapsed.as_secs());
97    let nonce = generate_nonce().map_err(SignError::Randomness)?;
98    sign_request_at(
99        request,
100        integrity_token,
101        i64::try_from(created).unwrap_or(i64::MAX),
102        &nonce,
103        signer,
104    )
105}
106
107/// Signs `request` with an explicit `created` and `nonce`, for deterministic output in tests and
108/// test vectors. Production callers should use [`sign_request`].
109///
110/// # Errors
111///
112/// Returns a [`SignError`] when the parameters are invalid or the signer fails.
113pub fn sign_request_at<S: Signer>(
114    request: &CanonicalRequest<'_>,
115    integrity_token: &str,
116    created: i64,
117    nonce: &str,
118    signer: &S,
119) -> Result<SignedHeaders, SignError<S::Error>> {
120    validate_nonce(nonce)?;
121    let params = SignatureParams::new(created, nonce, signer.platform().alg())?;
122    let base = request.signature_base(&params, integrity_token);
123    let signature = signer
124        .sign(&base.client_data_hash())
125        .map_err(SignError::Signer)?;
126    Ok(SignedHeaders {
127        integrity_token: integrity_token.to_owned(),
128        signature_input: params.to_field(),
129        signature: signature_field(&signature),
130        request_binding: base.request_binding(),
131    })
132}