anvilsign in

collin/anvil · c4cfe9ad

Stop enforcing credProtect, which broke registration on real authenticators

Collin Richards · 2026-08-18 10:21 UTC · c4cfe9ad070a818f501f58672c7df51a575af6d1 · parent 8887ef61 · browse files

modifiedcrates/anvil-web/src/passkeys.rs+17 −5
⋯ 56 unchanged lines
5757 Encode,
5858 },
5959 request::{
60+ ExtensionInfo,
6061 PublicKeyCredentialDescriptor,
6162 auth::AuthenticationVerificationOptions,
6263 register::{
64+ CredProtect,
6365 Nickname,
6466 PublicKeyCredentialUserEntity,
6567 RegistrationVerificationOptions,
⋯ 100 unchanged lines
166168 display_name,
167169 };
168170
169- let (server_state, client_state) =
170- match PublicKeyCredentialCreationOptions::passkey(&rp, entity, exclude).start_ceremony() {
171- Ok(pair) => pair,
172- Err(e) => return server_error(format!("building registration options: {e}")),
173- };
171+ let mut options = PublicKeyCredentialCreationOptions::passkey(&rp, entity, exclude);
172+ // Ask for credProtect, but never *enforce* it. The crate's passkey preset
173+ // enforces the policy, and an authenticator that does not implement the
174+ // extension — a phone over hybrid, many security keys — then fails the whole
175+ // ceremony rather than ignoring it ("Something went wrong" in Chrome's
176+ // dialog, with nothing reaching the server). Enforcement buys nothing here:
177+ // both ceremonies already require user verification, and the UV flag is
178+ // checked on every assertion, so a UV-less credential could not sign in
179+ // anyway.
180+ options.extensions.cred_protect =
181+ CredProtect::UserVerificationRequired(ExtensionInfo::AllowDontEnforceValue);
182+ let (server_state, client_state) = match options.start_ceremony() {
183+ Ok(pair) => pair,
184+ Err(e) => return server_error(format!("building registration options: {e}")),
185+ };
174186 let options = match serde_json::to_value(&client_state) {
175187 Ok(value) => value,
176188 Err(e) => return server_error(e),
⋯ 556 unchanged lines
modifiedcrates/anvil-web/tests/passkey_flow.rs+29 −0
⋯ 417 unchanged lines
418418 );
419419 }
420420
421+/// Registration options must stay compatible with authenticators that do not
422+/// implement credProtect — phones over hybrid, plenty of security keys. With
423+/// enforcement on, those fail the whole ceremony inside the browser, before
424+/// anything reaches the server. User verification is still required, which is
425+/// what actually gates a sign-in.
426+#[tokio::test]
427+async fn registration_asks_for_credprotect_without_enforcing_it() {
428+ let harness = harness().await;
429+ let (status, body) = harness
430+ .post_json("/-/settings/passkeys/begin", serde_json::json!({}))
431+ .await;
432+ assert_eq!(status, StatusCode::OK);
433+ let options: serde_json::Value = serde_json::from_str(&body).unwrap();
434+ let options = &options["options"];
435+
436+ assert_eq!(
437+ options["extensions"]["enforceCredentialProtectionPolicy"],
438+ serde_json::json!(false),
439+ );
440+ assert_eq!(
441+ options["authenticatorSelection"]["userVerification"],
442+ serde_json::json!("required"),
443+ );
444+ assert_eq!(
445+ options["authenticatorSelection"]["residentKey"],
446+ serde_json::json!("required"),
447+ );
448+}
449+
421450 #[tokio::test]
422451 async fn a_forged_signature_is_refused() {
423452 let harness = harness().await;
⋯ 94 unchanged lines