Skip to main content

acdp_primitives/
keyless.rs

1//! Keyless-signing reason tokens (RFC-ACDP-0021 §13.3, 0.5.0 Draft).
2//!
3//! RFC-ACDP-0021 adds no wire error code and this library adds no
4//! [`AcdpError`] variant for it. A refusal reuses the variant of an
5//! existing code, and the reason travels in the message as a stable
6//! `keyless:<token>: ` prefix that [`KeylessReason::of`] reads back:
7//!
8//! - registry refusals: [`AcdpError::from_wire_error`] adds the prefix
9//!   when `details.reason` is one of the registry tokens of
10//!   `registries/error-codes.md` ("Keyless-signing reason tokens");
11//! - consumer refusals (§8.1): the keyless verifier builds its errors
12//!   with [`KeylessReason::error`].
13
14use crate::error::AcdpError;
15use serde::{Deserialize, Serialize};
16
17/// The prefix that marks a keyless reason in an [`AcdpError`] message.
18const PREFIX: &str = "keyless:";
19
20/// Why a keyless step refused: the registry `details.reason` tokens
21/// (§5.2, §6.3, §7) and the consumer verification results (§8.1).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24#[non_exhaustive]
25pub enum KeylessReason {
26    /// §5.2 step 2: the token's issuer (or SPIFFE trust domain) is not
27    /// accepted by the registry.
28    IssuerNotAccepted,
29    /// §5.2 step 3: the token fails verification.
30    TokenInvalid,
31    /// §5.2 step 3: the token's `aud` lacks the registry's DID.
32    AudienceMismatch,
33    /// §5.2 step 3: `cnf.jkt` is not the presented key's thumbprint.
34    ConfirmationMismatch,
35    /// §5.2 step 3: the token was already used.
36    TokenReplayed,
37    /// §5.2 step 6, §6.3 step 7: the controller does not list the
38    /// identity.
39    IdentityNotAuthorized,
40    /// §5.2 step 4: the proof of possession does not verify.
41    PopInvalid,
42    /// §6.3 step 1: a keyless key id with no `key_attestation`.
43    AttestationMissing,
44    /// §6.3 step 2: the attestation is on another registry.
45    AttestationForeign,
46    /// §6.3 step 3: no attestation at `key_attestation.ctx_id`.
47    AttestationNotFound,
48    /// §6.3 steps 3–4, §8.1 step 3: the attestation's bytes or bindings
49    /// differ.
50    AttestationMismatch,
51    /// §6.3 step 5, §8.1 step 7: the attestation was retracted.
52    AttestationWithdrawn,
53    /// §6.3 step 6, §8.1 step 5: the placing time is outside the window.
54    OutsideAttestationWindow,
55    /// §7: the successor's identity may not supersede the predecessor.
56    KeylessIdentityMismatch,
57    /// §8.1 step 1: the attestation does not verify or breaks §5.3.
58    AttestationInvalid,
59    /// §8.1 step 2: the consumer does not accept the attesting registry.
60    UntrustedAttester,
61    /// §8.1 step 4: no receipt by the attester places the signature.
62    Unplaceable,
63    /// §8.1 step 6: the signature does not verify under the attested key.
64    InvalidSignature,
65    /// §8.1 step 8: a revocation of the attested key applies.
66    Revoked,
67    /// §8.1 step 9: the controller's DID document could not be fetched.
68    ControllerUnreachable,
69    // New tokens go last, so existing discriminants never move.
70    /// §5.2 step 3: the issuer's published keys (OIDC JWKS or SPIFFE
71    /// bundle) could not be fetched. Transient: the wire code is
72    /// `key_resolution_unreachable`, so the client may retry.
73    IssuerUnreachable,
74    /// §6.3: a lifecycle event signed with a keyless key id; keyless
75    /// keys do not sign events in this version.
76    KeylessEventUnsupported,
77}
78
79impl KeylessReason {
80    /// Every reason, in declaration order. A slice, so a new spec token
81    /// is an additive change.
82    pub const ALL: &'static [KeylessReason] = &[
83        Self::IssuerNotAccepted,
84        Self::TokenInvalid,
85        Self::AudienceMismatch,
86        Self::ConfirmationMismatch,
87        Self::TokenReplayed,
88        Self::IdentityNotAuthorized,
89        Self::PopInvalid,
90        Self::AttestationMissing,
91        Self::AttestationForeign,
92        Self::AttestationNotFound,
93        Self::AttestationMismatch,
94        Self::AttestationWithdrawn,
95        Self::OutsideAttestationWindow,
96        Self::KeylessIdentityMismatch,
97        Self::AttestationInvalid,
98        Self::UntrustedAttester,
99        Self::Unplaceable,
100        Self::InvalidSignature,
101        Self::Revoked,
102        Self::ControllerUnreachable,
103        Self::IssuerUnreachable,
104        Self::KeylessEventUnsupported,
105    ];
106
107    /// The token as written on the wire and in reports.
108    pub fn as_str(self) -> &'static str {
109        match self {
110            Self::IssuerNotAccepted => "issuer_not_accepted",
111            Self::TokenInvalid => "token_invalid",
112            Self::IssuerUnreachable => "issuer_unreachable",
113            Self::AudienceMismatch => "audience_mismatch",
114            Self::ConfirmationMismatch => "confirmation_mismatch",
115            Self::TokenReplayed => "token_replayed",
116            Self::IdentityNotAuthorized => "identity_not_authorized",
117            Self::PopInvalid => "pop_invalid",
118            Self::AttestationMissing => "attestation_missing",
119            Self::KeylessEventUnsupported => "keyless_event_unsupported",
120            Self::AttestationForeign => "attestation_foreign",
121            Self::AttestationNotFound => "attestation_not_found",
122            Self::AttestationMismatch => "attestation_mismatch",
123            Self::AttestationWithdrawn => "attestation_withdrawn",
124            Self::OutsideAttestationWindow => "outside_attestation_window",
125            Self::KeylessIdentityMismatch => "keyless_identity_mismatch",
126            Self::AttestationInvalid => "attestation_invalid",
127            Self::UntrustedAttester => "untrusted_attester",
128            Self::Unplaceable => "unplaceable",
129            Self::InvalidSignature => "invalid_signature",
130            Self::Revoked => "revoked",
131            Self::ControllerUnreachable => "controller_unreachable",
132        }
133    }
134
135    /// Parse a token.
136    pub fn parse(token: &str) -> Option<Self> {
137        Self::ALL.iter().copied().find(|r| r.as_str() == token)
138    }
139
140    /// True for the tokens `registries/error-codes.md` lists as registry
141    /// `details.reason` values (the rest are consumer results, §13.3).
142    pub fn is_registry_token(self) -> bool {
143        !matches!(
144            self,
145            Self::AttestationInvalid
146                | Self::UntrustedAttester
147                | Self::Unplaceable
148                | Self::InvalidSignature
149                | Self::Revoked
150                | Self::ControllerUnreachable
151        )
152    }
153
154    /// `message` with this reason's prefix.
155    pub fn tag(self, message: impl std::fmt::Display) -> String {
156        format!("{PREFIX}{}: {message}", self.as_str())
157    }
158
159    /// The consumer-side error for this reason (§8.1, §8.2): the
160    /// existing variant whose meaning fits, with the reason prefix.
161    /// `controller_unreachable` and `issuer_unreachable` are the
162    /// transient refusals.
163    pub fn error(self, message: impl std::fmt::Display) -> AcdpError {
164        let msg = self.tag(message);
165        match self {
166            Self::InvalidSignature | Self::PopInvalid => AcdpError::InvalidSignature(msg),
167            Self::ControllerUnreachable | Self::IssuerUnreachable => {
168                AcdpError::KeyResolutionUnreachable(msg)
169            }
170            Self::AttestationInvalid
171            | Self::AttestationMissing
172            | Self::KeylessEventUnsupported
173            | Self::AttestationForeign
174            | Self::AttestationNotFound => AcdpError::KeyResolution(msg),
175            Self::KeylessIdentityMismatch => AcdpError::NotAuthorized(msg),
176            _ => AcdpError::KeyNotAuthorized(msg),
177        }
178    }
179
180    /// The keyless reason an error carries, if any. A
181    /// [`AcdpError::KeyRevoked`] reads as [`Self::Revoked`]: keyless
182    /// verification refuses a revoked key with that typed error.
183    pub fn of(error: &AcdpError) -> Option<Self> {
184        let msg = match error {
185            // §8.1 step 8 keeps the typed revocation refusal.
186            AcdpError::KeyRevoked(_) => return Some(Self::Revoked),
187            AcdpError::InvalidSignature(m)
188            | AcdpError::KeyResolution(m)
189            | AcdpError::KeyResolutionUnreachable(m)
190            | AcdpError::KeyNotAuthorized(m)
191            | AcdpError::NotAuthorized(m)
192            | AcdpError::SchemaViolation(m) => m,
193            _ => return None,
194        };
195        let (token, _) = msg.strip_prefix(PREFIX)?.split_once(": ")?;
196        Self::parse(token)
197    }
198}
199
200impl std::fmt::Display for KeylessReason {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.write_str(self.as_str())
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn tokens_round_trip_and_are_unique() {
212        let mut seen = std::collections::BTreeSet::new();
213        for &r in KeylessReason::ALL {
214            assert!(seen.insert(r.as_str()), "duplicate token {r}");
215            assert_eq!(KeylessReason::parse(r.as_str()), Some(r));
216            let json = serde_json::to_value(r).unwrap();
217            assert_eq!(json, r.as_str());
218            assert_eq!(KeylessReason::of(&r.error("x")), Some(r));
219        }
220    }
221
222    #[test]
223    fn untagged_errors_carry_no_reason() {
224        assert_eq!(
225            KeylessReason::of(&AcdpError::KeyNotAuthorized("plain".into())),
226            None
227        );
228        assert_eq!(
229            KeylessReason::of(&AcdpError::KeyNotAuthorized("keyless:nope: x".into())),
230            None
231        );
232    }
233}