Skip to main content

webauthn_kit/
policy.rs

1//! Caller-enforceable credential policies for the registration and
2//! authentication ceremonies.
3//!
4//! A [`CredentialPolicy`] bundles the per-ceremony decisions that turn
5//! reported authenticator state into *enforced* requirements:
6//!
7//! - **User verification** ([`UserVerificationPolicy`]): whether the UV flag
8//!   is required, preferred, or discouraged. `Required` makes the kit reject
9//!   assertions/registrations whose UV flag is clear — before, the flag was
10//!   only *reported* and every caller had to hand-roll this check.
11//! - **Backup policy** ([`BackupPolicy`]): the WebAuthn L3 multi-device
12//!   (syncable passkey) story. BE (backup eligibility) is set at creation and
13//!   immutable; BS (backup state) reflects whether the credential is
14//!   currently backed up. A [`BackupPolicy::RequireDeviceBound`] policy
15//!   rejects BE=1 credentials, i.e. credentials that may exist in a cloud
16//!   account rather than only inside one hardware authenticator.
17//! - **Algorithm allowlist** ([`CredentialPolicy::allowed_algorithms`]):
18//!   registration-time server-side enforcement — a credential whose COSE
19//!   algorithm is not on the list is rejected even if the kit can verify it.
20//!
21//! # Security notes
22//!
23//! - Defaults are deliberately permissive (`Preferred` / `Allow` / empty
24//!   allowlist) so 0.2.x behavior is preserved; permissive UV never accepts
25//!   *less* than before, it only declines to reject.
26//! - Enforcement is server-side and unconditional: a hostile authenticator
27//!   cannot satisfy `UserVerificationPolicy::Required` without the UV flag
28//!   being genuinely set in the signed authenticator data.
29//! - BE/BS come from the signed authenticator data flags, so policy decisions
30//!   on them are authenticated (they cannot be flipped without breaking the
31//!   assertion signature).
32//!
33//! # Requirements
34//! REQ-WA-142, REQ-WA-143, REQ-WA-144, REQ-WA-145
35
36/// User-verification requirement for a ceremony (WebAuthn L2/L3
37/// `userVerification`).
38///
39/// `Required` is *enforced*: the ceremony fails with
40/// [`crate::WebauthnError::UserVerificationRequired`] when the authenticator
41/// flags lack the UV bit. `Preferred`/`Discouraged` never reject; the flag is
42/// reported via the result's `user_verified` field.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum UserVerificationPolicy {
45    /// Reject the ceremony unless the UV flag is set.
46    Required,
47    /// Ask the authenticator for UV but accept either outcome (default).
48    #[default]
49    Preferred,
50    /// Do not ask for UV; accept either outcome.
51    Discouraged,
52}
53
54impl UserVerificationPolicy {
55    /// The `userVerification` wire string for ceremony options.
56    #[must_use]
57    pub fn as_str(self) -> &'static str {
58        match self {
59            UserVerificationPolicy::Required => "required",
60            UserVerificationPolicy::Preferred => "preferred",
61            UserVerificationPolicy::Discouraged => "discouraged",
62        }
63    }
64}
65
66/// Resident-key (discoverable credential) preference for registration
67/// (WebAuthn L2/L3 `residentKey`).
68///
69/// This is a *preference conveyed to the authenticator* via ceremony options;
70/// the server cannot cryptographically verify discoverability from the
71/// response alone (see THREAT-MODEL.md).
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum ResidentKeyPolicy {
74    /// The authenticator must create a discoverable credential.
75    Required,
76    /// Ask for a discoverable credential, fall back to server-side ones
77    /// (default).
78    #[default]
79    Preferred,
80    /// Prefer server-side credentials.
81    Discouraged,
82}
83
84impl ResidentKeyPolicy {
85    /// The `residentKey` wire string for registration options.
86    #[must_use]
87    pub fn as_str(self) -> &'static str {
88        match self {
89            ResidentKeyPolicy::Required => "required",
90            ResidentKeyPolicy::Preferred => "preferred",
91            ResidentKeyPolicy::Discouraged => "discouraged",
92        }
93    }
94}
95
96/// Attestation conveyance preference for registration (WebAuthn L2/L3
97/// `attestation`).
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99pub enum AttestationConveyance {
100    /// Do not request attestation (default; privacy-preserving).
101    #[default]
102    None,
103    /// Ask the client to indicate to attestation CA keys may be shared.
104    Indirect,
105    /// Obtain attestation statements directly from the authenticator.
106    Direct,
107    /// Convey platform-enterprise attestation (may identify the device).
108    Enterprise,
109}
110
111impl AttestationConveyance {
112    /// The `attestation` wire string for registration options.
113    #[must_use]
114    pub fn as_str(self) -> &'static str {
115        match self {
116            AttestationConveyance::None => "none",
117            AttestationConveyance::Indirect => "indirect",
118            AttestationConveyance::Direct => "direct",
119            AttestationConveyance::Enterprise => "enterprise",
120        }
121    }
122}
123
124/// Policy for multi-device (syncable) credentials — the WebAuthn L3 BE/BS
125/// story.
126///
127/// BE (backup eligibility) is an immutable creation-time property: "this
128/// credential *may* be backed up / synced". BS (backup state) says whether it
129/// *currently is*. Both arrive in the signed authenticator data flags and are
130/// reported on both ceremony results.
131///
132/// # Account-recovery implications
133///
134/// - A **synced** (BE=1) credential lives in a cloud account (iCloud
135///   Keychain, Google Password Manager, ...). Account recovery therefore
136///   follows the *platform's* rules — a phone number or recovery email can
137///   resurrect the credential. Treat these like a possession factor that
138///   inherits the recovery strength of the user's cloud account.
139/// - A **device-bound** (BE=0) credential is confined to one hardware
140///   authenticator; losing the device means losing the credential (stronger
141///   possession semantics, worse recovery story).
142/// - RPs that want to exclude synced credentials from sensitive flows use
143///   [`BackupPolicy::RequireDeviceBound`], which rejects BE=1 credentials.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
145pub enum BackupPolicy {
146    /// Accept device-bound and synced credentials; report the flags
147    /// (default).
148    #[default]
149    Allow,
150    /// Reject BE=1 (syncable) credentials at both ceremonies.
151    RequireDeviceBound,
152}
153
154/// Per-ceremony credential policy: user verification, multi-device (backup)
155/// handling, and the registration algorithm allowlist.
156///
157/// Attach to [`crate::WebauthnConfig`] (`credential_policy`) and/or pass
158/// directly to [`crate::verify_registration`] /
159/// [`crate::protocol::AuthenticationParams::policy`].
160///
161/// # Requirements
162/// REQ-WA-142, REQ-WA-143, REQ-WA-144
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct CredentialPolicy {
165    /// User-verification requirement, enforced server-side
166    /// (default: [`UserVerificationPolicy::Preferred`] — report only).
167    pub user_verification: UserVerificationPolicy,
168    /// Multi-device credential policy (default: [`BackupPolicy::Allow`]).
169    pub backup: BackupPolicy,
170    /// Registration-time COSE algorithm allowlist. An empty list accepts
171    /// every algorithm the kit can verify (default). A non-empty list
172    /// rejects registration of any credential whose algorithm is absent
173    /// with [`crate::WebauthnError::UnsupportedAlgorithm`].
174    pub allowed_algorithms: Vec<i32>,
175}
176
177impl Default for CredentialPolicy {
178    /// Permissive-by-default policy preserving 0.2.x behavior: UV reported
179    /// but never required, any backup eligibility accepted, no algorithm
180    /// restriction beyond what the crypto layer supports.
181    fn default() -> Self {
182        Self {
183            user_verification: UserVerificationPolicy::Preferred,
184            backup: BackupPolicy::Allow,
185            allowed_algorithms: Vec::new(),
186        }
187    }
188}
189
190impl CredentialPolicy {
191    /// Strict policy: UV required, device-bound credentials only, ES256
192    /// only.
193    ///
194    /// Suitable for high-assurance flows (step-up, admin actions). Note that
195    /// `RequireDeviceBound` rejects synced passkeys outright — combined with
196    /// per-user registration, this limits credentials to hardware keys.
197    #[must_use]
198    pub fn strict() -> Self {
199        Self {
200            user_verification: UserVerificationPolicy::Required,
201            backup: BackupPolicy::RequireDeviceBound,
202            allowed_algorithms: vec![crate::crypto::COSE_ALG_ES256],
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn wire_strings_roundtrip() {
213        assert_eq!(UserVerificationPolicy::Required.as_str(), "required");
214        assert_eq!(UserVerificationPolicy::Preferred.as_str(), "preferred");
215        assert_eq!(UserVerificationPolicy::Discouraged.as_str(), "discouraged");
216        assert_eq!(ResidentKeyPolicy::Required.as_str(), "required");
217        assert_eq!(ResidentKeyPolicy::Preferred.as_str(), "preferred");
218        assert_eq!(ResidentKeyPolicy::Discouraged.as_str(), "discouraged");
219        assert_eq!(AttestationConveyance::None.as_str(), "none");
220        assert_eq!(AttestationConveyance::Indirect.as_str(), "indirect");
221        assert_eq!(AttestationConveyance::Direct.as_str(), "direct");
222        assert_eq!(AttestationConveyance::Enterprise.as_str(), "enterprise");
223    }
224
225    /// Defaults must preserve 0.2.x ceremony behavior (permissive).
226    #[test]
227    fn default_policy_is_permissive() {
228        let policy = CredentialPolicy::default();
229        assert_eq!(policy.user_verification, UserVerificationPolicy::Preferred);
230        assert_eq!(policy.backup, BackupPolicy::Allow);
231        assert!(policy.allowed_algorithms.is_empty());
232    }
233
234    #[test]
235    fn strict_policy_tightens_everything() {
236        let policy = CredentialPolicy::strict();
237        assert_eq!(policy.user_verification, UserVerificationPolicy::Required);
238        assert_eq!(policy.backup, BackupPolicy::RequireDeviceBound);
239        assert_eq!(
240            policy.allowed_algorithms,
241            vec![crate::crypto::COSE_ALG_ES256]
242        );
243    }
244}