Skip to main content

attested_request/
verify.rs

1//! Verifying a canonical request end to end.
2
3use std::{
4    sync::Arc,
5    time::{Duration, SystemTime, UNIX_EPOCH},
6};
7
8use http::{HeaderMap, request::Parts, uri::Authority};
9
10use crate::{
11    base::CanonicalRequest,
12    profile::{Component, INTEGRITY_TOKEN_HEADER, SIGNATURE_HEADER, SIGNATURE_INPUT_HEADER},
13    reject::{RejectReason, Rejection},
14    replay::ReplayGuard,
15    signature::{SignatureInputError, SignatureParams, parse_signature, validate_nonce},
16    token::{AttestedDevice, TokenError, TokenVerifier},
17};
18
19/// Default maximum age of `created`.
20pub const DEFAULT_MAX_AGE: Duration = Duration::from_mins(5);
21/// Default tolerance for a `created` ahead of the verifier's clock.
22pub const DEFAULT_MAX_FUTURE_SKEW: Duration = Duration::from_secs(60);
23
24/// A source of the current time.
25pub trait Clock: Send + Sync {
26    /// The current time.
27    fn now(&self) -> SystemTime;
28}
29
30/// The system clock.
31#[derive(Debug, Clone, Copy, Default)]
32pub struct SystemClock;
33
34impl Clock for SystemClock {
35    fn now(&self) -> SystemTime {
36        SystemTime::now()
37    }
38}
39
40/// A clock stopped at one instant.
41#[derive(Debug, Clone, Copy)]
42pub struct FixedClock(pub SystemTime);
43
44impl Clock for FixedClock {
45    fn now(&self) -> SystemTime {
46        self.0
47    }
48}
49
50/// What a verified request proves: this exact request was signed by the attested device key.
51///
52/// It says nothing about who the user is or what they may access. Authorization is separate.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct VerifiedAttestedKeyContext {
55    /// The attested device, from the integrity token.
56    pub device: AttestedDevice,
57    /// `base64(SHA-256(signature base))`, identifying this exact request.
58    pub request_binding: String,
59}
60
61/// Verifies canonical requests for one service.
62#[derive(Clone)]
63pub struct Verifier {
64    tokens: TokenVerifier,
65    scheme: String,
66    authority: String,
67    max_age: Duration,
68    max_future_skew: Duration,
69    replay_guard: Option<Arc<dyn ReplayGuard>>,
70    clock: Arc<dyn Clock>,
71}
72
73/// Builds a [`Verifier`].
74pub struct VerifierBuilder {
75    verifier: Verifier,
76}
77
78/// A [`Verifier`] could not be built from its configuration.
79#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
80pub enum VerifierConfigError {
81    /// The authority is empty.
82    #[error("authority must be set")]
83    EmptyAuthority,
84    /// The authority is not a valid URI authority.
85    #[error("authority must be a valid URI authority")]
86    InvalidAuthority,
87    /// The scheme is empty.
88    #[error("scheme must be set")]
89    EmptyScheme,
90    /// The maximum age or future skew is zero.
91    #[error("max age and max future skew must be positive")]
92    EmptyWindow,
93}
94
95impl VerifierBuilder {
96    /// The scheme clients dial. Defaults to `https`.
97    #[must_use]
98    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
99        self.verifier.scheme = scheme.into();
100        self
101    }
102
103    /// The maximum age of `created`. Defaults to [`DEFAULT_MAX_AGE`].
104    #[must_use]
105    pub const fn max_age(mut self, max_age: Duration) -> Self {
106        self.verifier.max_age = max_age;
107        self
108    }
109
110    /// How far ahead of this clock `created` may be. Defaults to [`DEFAULT_MAX_FUTURE_SKEW`].
111    #[must_use]
112    pub const fn max_future_skew(mut self, max_future_skew: Duration) -> Self {
113        self.verifier.max_future_skew = max_future_skew;
114        self
115    }
116
117    /// Refuses a second use of the same request. Off by default; see [`crate::replay`].
118    #[must_use]
119    pub fn replay_guard(mut self, replay_guard: Arc<dyn ReplayGuard>) -> Self {
120        self.verifier.replay_guard = Some(replay_guard);
121        self
122    }
123
124    /// The clock to check freshness against. Defaults to [`SystemClock`].
125    #[must_use]
126    pub fn clock(mut self, clock: Arc<dyn Clock>) -> Self {
127        self.verifier.clock = clock;
128        self
129    }
130
131    /// Builds the verifier.
132    ///
133    /// # Errors
134    ///
135    /// Returns a [`VerifierConfigError`] when the configuration cannot verify anything.
136    pub fn build(self) -> Result<Verifier, VerifierConfigError> {
137        let verifier = self.verifier;
138        if verifier.authority.trim().is_empty() {
139            return Err(VerifierConfigError::EmptyAuthority);
140        }
141        verifier
142            .authority
143            .parse::<Authority>()
144            .map_err(|_| VerifierConfigError::InvalidAuthority)?;
145        if verifier.scheme.trim().is_empty() {
146            return Err(VerifierConfigError::EmptyScheme);
147        }
148        if verifier.max_age.is_zero() || verifier.max_future_skew.is_zero() {
149            return Err(VerifierConfigError::EmptyWindow);
150        }
151        Ok(verifier)
152    }
153}
154
155impl Verifier {
156    /// Starts a verifier for requests addressed to `authority`, the host clients dial.
157    #[must_use]
158    pub fn builder(tokens: TokenVerifier, authority: impl Into<String>) -> VerifierBuilder {
159        VerifierBuilder {
160            verifier: Self {
161                tokens,
162                scheme: "https".to_owned(),
163                authority: authority.into(),
164                max_age: DEFAULT_MAX_AGE,
165                max_future_skew: DEFAULT_MAX_FUTURE_SKEW,
166                replay_guard: None,
167                clock: Arc::new(SystemClock),
168            },
169        }
170    }
171
172    /// The configured authority.
173    #[must_use]
174    pub fn authority(&self) -> &str {
175        &self.authority
176    }
177
178    /// Verifies a request from its head and its complete raw body.
179    ///
180    /// Local checks run first, so that malformed or stale requests are refused before anything
181    /// that can reach the issuer's keys or the replay store.
182    ///
183    /// # Errors
184    ///
185    /// Returns a [`Rejection`] whose reason says why the request was refused.
186    pub async fn verify(
187        &self,
188        head: &Parts,
189        body: &[u8],
190    ) -> Result<VerifiedAttestedKeyContext, Rejection> {
191        let headers = SignedHeaders::extract(&head.headers)?;
192
193        let params = SignatureParams::parse(headers.signature_input).map_err(|error| {
194            let reason = match error {
195                SignatureInputError::UnsupportedComponent => RejectReason::SignatureBaseIncomplete,
196                _ => RejectReason::SignatureInputMalformed,
197            };
198            Rejection::new(reason).with_source(error)
199        })?;
200        let signature = parse_signature(headers.signature)
201            .map_err(|error| Rejection::new(RejectReason::SignatureMalformed).with_source(error))?;
202        if let Some(missing) = Component::ALL
203            .into_iter()
204            .find(|component| !params.components().contains(component))
205        {
206            return Err(Rejection::new(RejectReason::ComponentMissing)
207                .with_source(format!("{missing} is not covered")));
208        }
209        let now = self.clock.now();
210        self.check_created(params.created(), now)?;
211        validate_nonce(params.nonce())
212            .map_err(|error| Rejection::new(RejectReason::NonceInvalid).with_source(error))?;
213
214        let device = self
215            .tokens
216            .verify(headers.integrity_token, now)
217            .await
218            .map_err(token_rejection)?;
219        let platform = device.platform;
220        if params.alg() != platform.alg() {
221            return Err(Rejection::new(RejectReason::AlgMismatch).with_platform(platform));
222        }
223
224        let base = CanonicalRequest::from_http(
225            &head.method,
226            &head.uri,
227            &self.scheme,
228            &self.authority,
229            body,
230        )
231        .map_err(|error| {
232            Rejection::new(RejectReason::SignatureBaseIncomplete)
233                .with_platform(platform)
234                .with_source(error)
235        })?
236        .signature_base(&params, headers.integrity_token);
237        device
238            .key
239            .verify(platform, &base.client_data_hash(), &signature)
240            .map_err(|error| {
241                Rejection::new(RejectReason::SignatureInvalid)
242                    .with_platform(platform)
243                    .with_source(error)
244            })?;
245
246        let request_binding = base.request_binding();
247        if let Some(replay_guard) = &self.replay_guard {
248            let ttl = self.max_age + self.max_future_skew;
249            let claimed = replay_guard
250                .claim(&request_binding, ttl)
251                .await
252                .map_err(|error| {
253                    Rejection::new(RejectReason::NonceStoreUnavailable)
254                        .with_platform(platform)
255                        .with_source(error)
256                })?;
257            if !claimed {
258                return Err(Rejection::new(RejectReason::Replayed).with_platform(platform));
259            }
260        }
261
262        Ok(VerifiedAttestedKeyContext {
263            device,
264            request_binding,
265        })
266    }
267
268    fn check_created(&self, created: i64, now: SystemTime) -> Result<(), Rejection> {
269        let created = UNIX_EPOCH + Duration::from_secs(created.unsigned_abs());
270        if let Ok(ahead) = created.duration_since(now) {
271            if ahead > self.max_future_skew {
272                return Err(Rejection::new(RejectReason::CreatedTooFarInFuture)
273                    .with_source(format!("created is {}s ahead", ahead.as_secs())));
274            }
275        } else if let Ok(age) = now.duration_since(created)
276            && age > self.max_age
277        {
278            return Err(Rejection::new(RejectReason::CreatedTooOld)
279                .with_source(format!("created is {}s old", age.as_secs())));
280        }
281        Ok(())
282    }
283}
284
285fn token_rejection(error: TokenError) -> Rejection {
286    let reason = match error {
287        TokenError::KeysUnavailable(_) => RejectReason::JwksUnavailable,
288        TokenError::IntegrityFailed => RejectReason::DeviceIntegrityFailed,
289        _ => RejectReason::IntegrityTokenInvalid,
290    };
291    Rejection::new(reason).with_source(error)
292}
293
294/// The three profile headers, each present exactly once.
295struct SignedHeaders<'a> {
296    integrity_token: &'a str,
297    signature_input: &'a str,
298    signature: &'a str,
299}
300
301impl<'a> SignedHeaders<'a> {
302    fn extract(headers: &'a HeaderMap) -> Result<Self, Rejection> {
303        let names = [
304            INTEGRITY_TOKEN_HEADER,
305            SIGNATURE_INPUT_HEADER,
306            SIGNATURE_HEADER,
307        ];
308        if names.iter().any(|name| !headers.contains_key(*name)) {
309            return Err(RejectReason::HeadersMissing.into());
310        }
311        Ok(Self {
312            integrity_token: single(headers, INTEGRITY_TOKEN_HEADER)
313                .ok_or(RejectReason::IntegrityTokenInvalid)?,
314            signature_input: single(headers, SIGNATURE_INPUT_HEADER)
315                .ok_or(RejectReason::SignatureInputMalformed)?,
316            signature: single(headers, SIGNATURE_HEADER).ok_or(RejectReason::SignatureMalformed)?,
317        })
318    }
319}
320
321// A repeated header could be read differently by us and by whatever sits behind us, so only a
322// single, visible-ASCII line is accepted.
323fn single<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
324    let mut values = headers.get_all(name).iter();
325    match (values.next(), values.next()) {
326        (Some(value), None) => value.to_str().ok(),
327        _ => None,
328    }
329}