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}
70
71impl KeylessReason {
72    /// Every reason, in declaration order.
73    pub const ALL: [KeylessReason; 20] = [
74        Self::IssuerNotAccepted,
75        Self::TokenInvalid,
76        Self::AudienceMismatch,
77        Self::ConfirmationMismatch,
78        Self::TokenReplayed,
79        Self::IdentityNotAuthorized,
80        Self::PopInvalid,
81        Self::AttestationMissing,
82        Self::AttestationForeign,
83        Self::AttestationNotFound,
84        Self::AttestationMismatch,
85        Self::AttestationWithdrawn,
86        Self::OutsideAttestationWindow,
87        Self::KeylessIdentityMismatch,
88        Self::AttestationInvalid,
89        Self::UntrustedAttester,
90        Self::Unplaceable,
91        Self::InvalidSignature,
92        Self::Revoked,
93        Self::ControllerUnreachable,
94    ];
95
96    /// The token as written on the wire and in reports.
97    pub fn as_str(self) -> &'static str {
98        match self {
99            Self::IssuerNotAccepted => "issuer_not_accepted",
100            Self::TokenInvalid => "token_invalid",
101            Self::AudienceMismatch => "audience_mismatch",
102            Self::ConfirmationMismatch => "confirmation_mismatch",
103            Self::TokenReplayed => "token_replayed",
104            Self::IdentityNotAuthorized => "identity_not_authorized",
105            Self::PopInvalid => "pop_invalid",
106            Self::AttestationMissing => "attestation_missing",
107            Self::AttestationForeign => "attestation_foreign",
108            Self::AttestationNotFound => "attestation_not_found",
109            Self::AttestationMismatch => "attestation_mismatch",
110            Self::AttestationWithdrawn => "attestation_withdrawn",
111            Self::OutsideAttestationWindow => "outside_attestation_window",
112            Self::KeylessIdentityMismatch => "keyless_identity_mismatch",
113            Self::AttestationInvalid => "attestation_invalid",
114            Self::UntrustedAttester => "untrusted_attester",
115            Self::Unplaceable => "unplaceable",
116            Self::InvalidSignature => "invalid_signature",
117            Self::Revoked => "revoked",
118            Self::ControllerUnreachable => "controller_unreachable",
119        }
120    }
121
122    /// Parse a token.
123    pub fn parse(token: &str) -> Option<Self> {
124        Self::ALL.into_iter().find(|r| r.as_str() == token)
125    }
126
127    /// True for the tokens `registries/error-codes.md` lists as registry
128    /// `details.reason` values (the rest are consumer results, §13.3).
129    pub fn is_registry_token(self) -> bool {
130        !matches!(
131            self,
132            Self::AttestationInvalid
133                | Self::UntrustedAttester
134                | Self::Unplaceable
135                | Self::InvalidSignature
136                | Self::Revoked
137                | Self::ControllerUnreachable
138        )
139    }
140
141    /// `message` with this reason's prefix.
142    pub fn tag(self, message: impl std::fmt::Display) -> String {
143        format!("{PREFIX}{}: {message}", self.as_str())
144    }
145
146    /// The consumer-side error for this reason (§8.1, §8.2): the
147    /// existing variant whose meaning fits, with the reason prefix.
148    /// `controller_unreachable` is the one transient refusal.
149    pub fn error(self, message: impl std::fmt::Display) -> AcdpError {
150        let msg = self.tag(message);
151        match self {
152            Self::InvalidSignature | Self::PopInvalid => AcdpError::InvalidSignature(msg),
153            Self::ControllerUnreachable => AcdpError::KeyResolutionUnreachable(msg),
154            Self::AttestationInvalid
155            | Self::AttestationMissing
156            | Self::AttestationForeign
157            | Self::AttestationNotFound => AcdpError::KeyResolution(msg),
158            Self::KeylessIdentityMismatch => AcdpError::NotAuthorized(msg),
159            _ => AcdpError::KeyNotAuthorized(msg),
160        }
161    }
162
163    /// The keyless reason an error carries, if any. A
164    /// [`AcdpError::KeyRevoked`] reads as [`Self::Revoked`]: keyless
165    /// verification refuses a revoked key with that typed error.
166    pub fn of(error: &AcdpError) -> Option<Self> {
167        let msg = match error {
168            // §8.1 step 8 keeps the typed revocation refusal.
169            AcdpError::KeyRevoked(_) => return Some(Self::Revoked),
170            AcdpError::InvalidSignature(m)
171            | AcdpError::KeyResolution(m)
172            | AcdpError::KeyResolutionUnreachable(m)
173            | AcdpError::KeyNotAuthorized(m)
174            | AcdpError::NotAuthorized(m)
175            | AcdpError::SchemaViolation(m) => m,
176            _ => return None,
177        };
178        let (token, _) = msg.strip_prefix(PREFIX)?.split_once(": ")?;
179        Self::parse(token)
180    }
181}
182
183impl std::fmt::Display for KeylessReason {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.write_str(self.as_str())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn tokens_round_trip_and_are_unique() {
195        let mut seen = std::collections::BTreeSet::new();
196        for r in KeylessReason::ALL {
197            assert!(seen.insert(r.as_str()), "duplicate token {r}");
198            assert_eq!(KeylessReason::parse(r.as_str()), Some(r));
199            let json = serde_json::to_value(r).unwrap();
200            assert_eq!(json, r.as_str());
201            assert_eq!(KeylessReason::of(&r.error("x")), Some(r));
202        }
203    }
204
205    #[test]
206    fn untagged_errors_carry_no_reason() {
207        assert_eq!(
208            KeylessReason::of(&AcdpError::KeyNotAuthorized("plain".into())),
209            None
210        );
211        assert_eq!(
212            KeylessReason::of(&AcdpError::KeyNotAuthorized("keyless:nope: x".into())),
213            None
214        );
215    }
216}