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}