Skip to main content

attested_request/
reject.rs

1//! Why a canonical request was refused.
2
3use std::{error::Error, fmt};
4
5use http::StatusCode;
6
7use crate::profile::Platform;
8
9/// The reason a request was refused.
10///
11/// The value is API: it reaches the client in the error body and tags the rejection metric, and
12/// clients decide whether to retry on it. The names and statuses match `go-sonic`'s canonical
13/// request middleware so that clients see one taxonomy across services. Adding a reason is
14/// additive; renaming one breaks clients that already shipped.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum RejectReason {
17    /// `Integrity-Token`, `Signature-Input` or `Signature` is absent.
18    HeadersMissing,
19    /// The integrity token did not verify. A refreshed token may succeed.
20    IntegrityTokenInvalid,
21    /// `created` is older than the maximum age. Signing again may succeed.
22    CreatedTooOld,
23    /// `created` is too far in the future. The client clock needs fixing before a retry helps.
24    CreatedTooFarInFuture,
25    /// This exact request was already accepted.
26    Replayed,
27    /// The Attestation Gateway keys could not be obtained. Ours, not the client's.
28    JwksUnavailable,
29    /// The replay store could not be reached. Ours, not the client's.
30    NonceStoreUnavailable,
31    /// The body did not arrive in time.
32    BodyReadTimeout,
33    /// The client went away while sending the body.
34    ClientDisconnected,
35    /// The body could not be read for another reason.
36    BodyReadFailed,
37    /// The Attestation Gateway judged the device untrustworthy. Retrying cannot help.
38    DeviceIntegrityFailed,
39    /// The request signature does not verify under the attested key.
40    SignatureInvalid,
41    /// `Signature-Input` violates Structured Fields or the profile. A client bug.
42    SignatureInputMalformed,
43    /// `Signature` violates Structured Fields or the profile. A client bug.
44    SignatureMalformed,
45    /// A required component is not covered. A client bug.
46    ComponentMissing,
47    /// `alg` does not match the attested platform.
48    AlgMismatch,
49    /// The nonce is not canonical base64 of at least 16 bytes. A client bug.
50    NonceInvalid,
51    /// A covered component cannot be resolved. A client bug.
52    SignatureBaseIncomplete,
53    /// The body exceeds the size limit.
54    BodyTooLarge,
55}
56
57impl RejectReason {
58    /// The wire name of the reason.
59    #[must_use]
60    pub const fn as_str(self) -> &'static str {
61        match self {
62            Self::HeadersMissing => "headers_missing",
63            Self::IntegrityTokenInvalid => "integrity_token_invalid",
64            Self::CreatedTooOld => "created_too_old",
65            Self::CreatedTooFarInFuture => "created_too_far_in_future",
66            Self::Replayed => "replayed",
67            Self::JwksUnavailable => "jwks_unavailable",
68            Self::NonceStoreUnavailable => "nonce_store_unavailable",
69            Self::BodyReadTimeout => "body_read_timeout",
70            Self::ClientDisconnected => "client_disconnected",
71            Self::BodyReadFailed => "body_read_failed",
72            Self::DeviceIntegrityFailed => "device_integrity_failed",
73            Self::SignatureInvalid => "signature_invalid",
74            Self::SignatureInputMalformed => "signature_input_malformed",
75            Self::SignatureMalformed => "signature_malformed",
76            Self::ComponentMissing => "component_missing",
77            Self::AlgMismatch => "alg_mismatch",
78            Self::NonceInvalid => "nonce_invalid",
79            Self::SignatureBaseIncomplete => "signature_base_incomplete",
80            Self::BodyTooLarge => "body_too_large",
81        }
82    }
83
84    /// The HTTP status to answer with.
85    #[must_use]
86    pub fn status(self) -> StatusCode {
87        match self {
88            Self::HeadersMissing
89            | Self::IntegrityTokenInvalid
90            | Self::CreatedTooOld
91            | Self::CreatedTooFarInFuture
92            | Self::Replayed
93            | Self::AlgMismatch => StatusCode::UNAUTHORIZED,
94            Self::DeviceIntegrityFailed | Self::SignatureInvalid => StatusCode::FORBIDDEN,
95            Self::SignatureInputMalformed
96            | Self::SignatureMalformed
97            | Self::ComponentMissing
98            | Self::NonceInvalid
99            | Self::SignatureBaseIncomplete => StatusCode::BAD_REQUEST,
100            Self::JwksUnavailable | Self::NonceStoreUnavailable => StatusCode::SERVICE_UNAVAILABLE,
101            Self::BodyReadTimeout => StatusCode::REQUEST_TIMEOUT,
102            Self::BodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
103            Self::BodyReadFailed => StatusCode::INTERNAL_SERVER_ERROR,
104            // nginx's "client closed request"; the client will never see it.
105            Self::ClientDisconnected => {
106                StatusCode::from_u16(499).expect("499 is a valid status code")
107            }
108        }
109    }
110}
111
112impl fmt::Display for RejectReason {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(self.as_str())
115    }
116}
117
118/// A refused request: the reason, plus context for server-side telemetry.
119///
120/// Only the reason may reach the client. The source can name internal endpoints or token
121/// internals, and never includes the token, signature or body.
122#[derive(Debug)]
123pub struct Rejection {
124    /// Why the request was refused.
125    pub reason: RejectReason,
126    /// The attested platform, once the integrity token has been verified.
127    pub platform: Option<Platform>,
128    /// The underlying failure, when there is one.
129    pub source: Option<Box<dyn Error + Send + Sync>>,
130}
131
132impl Rejection {
133    /// A rejection for `reason` with no further context.
134    #[must_use]
135    pub const fn new(reason: RejectReason) -> Self {
136        Self {
137            reason,
138            platform: None,
139            source: None,
140        }
141    }
142
143    /// Attaches the underlying failure.
144    #[must_use]
145    pub fn with_source(mut self, source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
146        self.source = Some(source.into());
147        self
148    }
149
150    /// Attaches the attested platform.
151    #[must_use]
152    pub const fn with_platform(mut self, platform: Platform) -> Self {
153        self.platform = Some(platform);
154        self
155    }
156}
157
158impl From<RejectReason> for Rejection {
159    fn from(reason: RejectReason) -> Self {
160        Self::new(reason)
161    }
162}
163
164impl fmt::Display for Rejection {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(f, "request rejected: {}", self.reason)?;
167        if let Some(source) = &self.source {
168            write!(f, ": {source}")?;
169        }
170        Ok(())
171    }
172}
173
174impl Error for Rejection {
175    fn source(&self) -> Option<&(dyn Error + 'static)> {
176        self.source.as_deref().map(|source| source as _)
177    }
178}