From 2c8ea4601692c04bd0e34c8b55e50365f47dddec Mon Sep 17 00:00:00 2001 From: ManuA Date: Sat, 25 Jul 2026 16:11:30 +0200 Subject: [PATCH 1/4] add "prefered" to WebAuth --- .env.template | 5 + ISHIELD_WEBAUTHN_NOTES.md | 333 ++++++++++++++++++++++++++++ src/api/core/two_factor/webauthn.rs | 40 +++- src/config.rs | 10 + 4 files changed, 383 insertions(+), 5 deletions(-) create mode 100644 ISHIELD_WEBAUTHN_NOTES.md diff --git a/.env.template b/.env.template index fd7c2fd2..3ce84263 100644 --- a/.env.template +++ b/.env.template @@ -601,6 +601,11 @@ ## Enabling this would force the users to use a second factor to login every time. ## Note that the checkbox would still be present, but ignored. # DISABLE_2FA_REMEMBER=false + +## WebAuthn 2FA user verification policy +## Valid values are "discouraged" and "preferred". +## Setting this to "preferred" allows clients to request PIN or biometric verification when supported by the authenticator. +# WEBAUTHN_2FA_USER_VERIFICATION=discouraged ## ## Authenticator Settings ## Disable authenticator time drifted codes to be valid. diff --git a/ISHIELD_WEBAUTHN_NOTES.md b/ISHIELD_WEBAUTHN_NOTES.md new file mode 100644 index 00000000..694f6c0a --- /dev/null +++ b/ISHIELD_WEBAUTHN_NOTES.md @@ -0,0 +1,333 @@ +# Swissbit iShield Key 2 Pro / WebAuthn notes + +Status as of 2026-07-19. + +## Current status + +- Affected authenticator: Swissbit iShield Key 2 Pro MIFARE +- Device version: `1.1.0` +- FIDO applet: `v1.4.0-0-gd69b47b` +- AAGUID: `7787a482-13e8-4784-8a06-c7ed49a7aaf4` +- Supported protocols: U2F, CTAP 2.0, CTAP 2.1 +- Relevant reported options: + - `clientPin: yes` + - `alwaysUv: no` + - `makeCredUvNotRqd: yes` +- Tested Vaultwarden release: `1.36.0` +- Tested Web Vault release: `2026.4.1` +- Tested browser: Google Chrome `150.0.7871.124` +- Server: Debian 13, Linux x86_64, SQLite, native installation created by the Proxmox VE Community Script +- Vaultwarden serves HTTPS directly; no reverse proxy is used. + +With an FIDO2 PIN already configured on the iShield, unmodified Vaultwarden does +not show a PIN prompt during registration. The WebAuthn ceremony eventually +times out. Registering the key without a configured PIN works. YubiKeys with an +existing PIN work correctly in the same environment, and the iShield works on +webauthn.io. + +Vaultwarden deliberately changes the registration and authentication policy to +`userVerification: discouraged`, since WebAuthn is being used as a second factor. +The iShield advertises `makeCredUvNotRqd: yes`, so the client is allowed to try +credential creation without PIN-based user verification. That path does not +complete in the tested combination. + +Changing both the client challenge and server-side ceremony state from +`discouraged` to `preferred` fixes the problem. Registration then requests the +PIN, completes after touching the key, and authentication also succeeds in a new +Incognito session. + +Upstream issue: + +- + +Swissbit support has also been informed. Following maintainer feedback, an +upstream implementation is in progress on the local branch +`webauthn-2fa-user-verification-config`. It adds the editable server option +`WEBAUTHN_2FA_USER_VERIFICATION`, with `discouraged` remaining the default and +`preferred` enabling the working PIN/UV flow. The option is also exposed by the +existing Vaultwarden admin configuration UI. + +## Diagnostic source patch + +For Vaultwarden 1.36.0, four values in +`src/api/core/two_factor/webauthn.rs` were changed: + +```diff +- state["rs"]["policy"] = Value::String("discouraged".to_string()); ++ state["rs"]["policy"] = Value::String("preferred".to_string()); + +- asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; ++ asc.user_verification = UserVerificationPolicy::Preferred; + +- state["ast"]["policy"] = Value::String("discouraged".to_string()); ++ state["ast"]["policy"] = Value::String("preferred".to_string()); + +- response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; ++ response.public_key.user_verification = UserVerificationPolicy::Preferred; +``` + +Both the response sent to the browser and the server-side state must remain +consistent. Changing only the response is not the recommended test. + +## Current LXC binary and rollback + +The original 1.36.0 binary was saved as: + +```text +/opt/vaultwarden/bin/vaultwarden.original +``` + +The original SHA-256 recorded during the test was: + +```text +c7d507bb05a30af1ea3974fac53713be747730ce653e9ee26a1b3cd0a65b2c7e +``` + +Rollback to that binary: + +```bash +systemctl stop vaultwarden + +install -o root -g root -m 0755 \ + /opt/vaultwarden/bin/vaultwarden.original \ + /opt/vaultwarden/bin/vaultwarden + +rm -f /opt/vaultwarden/bin/.uv-preferred-version + +systemctl start vaultwarden +systemctl status vaultwarden --no-pager +``` + +An official PVE Community Script update will overwrite the active custom binary. +The iShield is expected to fail again after such an update unless upstream has +changed the policy or the custom build is reapplied. + +## Post-update rebuild script + +Install the following as `/usr/local/sbin/vaultwarden-uv-patch` inside the LXC. +It intentionally aborts if the upstream source no longer contains exactly the +four expected policy expressions. + +```bash +#!/usr/bin/env bash +set -Eeuo pipefail + +VW_ROOT="/opt/vaultwarden" +VW_BIN="${VW_ROOT}/bin/vaultwarden" +BUILD_DIR="/tmp/vaultwarden-uv-build" +SOURCE_FILE="src/api/core/two_factor/webauthn.rs" + +if [[ $EUID -ne 0 ]]; then + echo "ERROR: This script must be run as root." >&2 + exit 1 +fi + +if [[ ! -x "$VW_BIN" ]]; then + echo "ERROR: Vaultwarden binary not found: $VW_BIN" >&2 + exit 1 +fi + +VERSION="$("$VW_BIN" --version 2>/dev/null | + grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | + head -n1)" + +if [[ -z "$VERSION" ]]; then + echo "ERROR: Could not determine the installed Vaultwarden version." >&2 + exit 1 +fi + +PATCHED_MARKER="${VW_ROOT}/bin/.uv-preferred-version" + +if [[ -f "$PATCHED_MARKER" ]] && + [[ "$(<"$PATCHED_MARKER")" == "$VERSION" ]] && + "$VW_BIN" --version 2>/dev/null | grep -q 'uv-preferred'; then + echo "Vaultwarden $VERSION is already patched." + exit 0 +fi + +echo "Preparing UV-preferred build for Vaultwarden $VERSION" + +command -v git >/dev/null || + { echo "ERROR: git is not installed." >&2; exit 1; } + +command -v cargo >/dev/null || + { echo "ERROR: cargo is not available." >&2; exit 1; } + +if [[ "$BUILD_DIR" != "/tmp/vaultwarden-uv-build" ]]; then + echo "ERROR: Unexpected build directory: $BUILD_DIR" >&2 + exit 1 +fi + +rm -rf -- "$BUILD_DIR" + +git clone \ + --branch "$VERSION" \ + --depth 1 \ + https://github.com/dani-garcia/vaultwarden.git \ + "$BUILD_DIR" + +cd "$BUILD_DIR" + +if [[ ! -f "$SOURCE_FILE" ]]; then + echo "ERROR: Expected source file is missing: $SOURCE_FILE" >&2 + exit 1 +fi + +STATE_PATTERN='Value::String("discouraged".to_string())' +POLICY_PATTERN='UserVerificationPolicy::Discouraged_DO_NOT_USE' + +STATE_COUNT="$(grep -Fc "$STATE_PATTERN" "$SOURCE_FILE" || true)" +POLICY_COUNT="$(grep -Fc "$POLICY_PATTERN" "$SOURCE_FILE" || true)" + +if [[ "$STATE_COUNT" -ne 2 || "$POLICY_COUNT" -ne 2 ]]; then + echo "ERROR: Vaultwarden's WebAuthn implementation has changed." >&2 + echo "Expected two state policies and two challenge policies." >&2 + echo "Found state=$STATE_COUNT, challenge=$POLICY_COUNT." >&2 + echo "No binary was changed." >&2 + exit 1 +fi + +sed -i \ + 's/Value::String("discouraged".to_string())/Value::String("preferred".to_string())/g' \ + "$SOURCE_FILE" + +sed -i \ + 's/UserVerificationPolicy::Discouraged_DO_NOT_USE/UserVerificationPolicy::Preferred/g' \ + "$SOURCE_FILE" + +REMAINING_STATE="$(grep -Fc "$STATE_PATTERN" "$SOURCE_FILE" || true)" +REMAINING_POLICY="$(grep -Fc "$POLICY_PATTERN" "$SOURCE_FILE" || true)" +PREFERRED_STATE="$(grep -Fc 'Value::String("preferred".to_string())' "$SOURCE_FILE" || true)" +PREFERRED_POLICY="$(grep -Fc 'UserVerificationPolicy::Preferred' "$SOURCE_FILE" || true)" + +if [[ "$REMAINING_STATE" -ne 0 || + "$REMAINING_POLICY" -ne 0 || + "$PREFERRED_STATE" -lt 2 || + "$PREFERRED_POLICY" -lt 2 ]]; then + echo "ERROR: Patch verification failed. No binary was changed." >&2 + exit 1 +fi + +git diff --check + +echo "Applied source patch:" +git diff -- "$SOURCE_FILE" + +export VW_VERSION="${VERSION}-uv-preferred" + +cargo build \ + --locked \ + --features "sqlite,mysql,postgresql" \ + --release + +TEST_BIN="${VW_ROOT}/bin/vaultwarden.uv-preferred-${VERSION}" +OFFICIAL_BACKUP="${VW_ROOT}/bin/vaultwarden.official-${VERSION}" + +install -o root -g root -m 0755 \ + target/release/vaultwarden \ + "$TEST_BIN" + +if ldd "$TEST_BIN" | grep -q 'not found'; then + echo "ERROR: The new binary has unresolved libraries." >&2 + rm -f -- "$TEST_BIN" + exit 1 +fi + +"$TEST_BIN" --version + +if [[ ! -e "$OFFICIAL_BACKUP" ]]; then + install -o root -g root -m 0755 \ + "$VW_BIN" \ + "$OFFICIAL_BACKUP" +fi + +echo "Installing patched binary..." + +systemctl stop vaultwarden + +install -o root -g root -m 0755 \ + "$TEST_BIN" \ + "$VW_BIN" + +printf '%s\n' "$VERSION" >"$PATCHED_MARKER" + +if ! systemctl start vaultwarden; then + echo "ERROR: Patched Vaultwarden failed to start; restoring official binary." >&2 + + install -o root -g root -m 0755 \ + "$OFFICIAL_BACKUP" \ + "$VW_BIN" + + rm -f -- "$PATCHED_MARKER" + systemctl start vaultwarden + exit 1 +fi + +if ! systemctl is-active --quiet vaultwarden; then + echo "ERROR: Service is not active; restoring official binary." >&2 + + systemctl stop vaultwarden || true + install -o root -g root -m 0755 \ + "$OFFICIAL_BACKUP" \ + "$VW_BIN" + + rm -f -- "$PATCHED_MARKER" + systemctl start vaultwarden + exit 1 +fi + +echo +echo "Successfully installed:" +"$VW_BIN" --version + +systemctl status vaultwarden --no-pager +echo +echo "Official binary backup: $OFFICIAL_BACKUP" +``` + +Make it executable: + +```bash +chmod 0755 /usr/local/sbin/vaultwarden-uv-patch +``` + +Run it after the normal PVE Community Script update: + +```bash +vaultwarden-uv-patch +``` + +Then verify: + +```bash +/opt/vaultwarden/bin/vaultwarden --version +systemctl status vaultwarden --no-pager +journalctl -u vaultwarden -n 50 --no-pager +``` + +The version should contain a suffix similar to: + +```text +Vaultwarden 1.37.0-uv-preferred +``` + +## Version-specific rollback after using the script + +For example, to restore the official 1.36.0 binary saved by the script: + +```bash +systemctl stop vaultwarden + +install -o root -g root -m 0755 \ + /opt/vaultwarden/bin/vaultwarden.official-1.36.0 \ + /opt/vaultwarden/bin/vaultwarden + +rm -f /opt/vaultwarden/bin/.uv-preferred-version + +systemctl start vaultwarden +systemctl status vaultwarden --no-pager +``` + +Before running the script against a future release, first check issue #7437 and +the release notes. Do not apply the custom build if Vaultwarden has introduced an +official setting or otherwise changed the relevant WebAuthn behavior. diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 07b964e5..d0dd2b6d 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -44,6 +44,17 @@ static WEBAUTHN: LazyLock = LazyLock::new(|| { webauthn.build().expect("Building Webauthn failed") }); +fn user_verification_policy() -> UserVerificationPolicy { + user_verification_policy_from_config(&CONFIG.webauthn_2fa_user_verification()) +} + +fn user_verification_policy_from_config(value: &str) -> UserVerificationPolicy { + match value { + "preferred" => UserVerificationPolicy::Preferred, + _ => UserVerificationPolicy::Discouraged_DO_NOT_USE, + } +} + pub fn routes() -> Vec { routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] } @@ -150,7 +161,8 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea )?; let mut state = serde_json::to_value(&state)?; - state["rs"]["policy"] = Value::String("discouraged".to_owned()); + let user_verification_policy = user_verification_policy(); + state["rs"]["policy"] = serde_json::to_value(&user_verification_policy)?; state["rs"]["extensions"].as_object_mut().unwrap().clear(); let type_ = TwoFactorType::WebauthnRegisterChallenge; @@ -160,7 +172,7 @@ async fn generate_webauthn_challenge(data: Json, headers: Hea // we need to modify some of the default settings defined by `start_passkey_registration()`. challenge.public_key.extensions = None; if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() { - asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; + asc.user_verification = user_verification_policy; } let mut challenge_value = serde_json::to_value(challenge.public_key)?; @@ -387,15 +399,16 @@ pub async fn generate_webauthn_login(user_id: &UserId, conn: &DbConn) -> JsonRes // Generate a challenge based on the credentials let (mut response, state) = WEBAUTHN.start_passkey_authentication(&creds)?; - // Modify to discourage user verification + // Apply the configured user verification policy let mut state = serde_json::to_value(&state)?; - state["ast"]["policy"] = Value::String("discouraged".to_owned()); + let user_verification_policy = user_verification_policy(); + state["ast"]["policy"] = serde_json::to_value(&user_verification_policy)?; // Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well let app_id = format!("{}/app-id.json", CONFIG.domain()); state["ast"]["appid"] = Value::String(app_id.clone()); - response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; + response.public_key.user_verification = user_verification_policy; response .public_key .extensions @@ -516,3 +529,20 @@ fn check_and_update_backup_eligible( } Ok(false) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn configured_user_verification_policy() { + assert_eq!( + serde_json::to_value(user_verification_policy_from_config("discouraged")).unwrap(), + "discouraged" + ); + assert_eq!( + serde_json::to_value(user_verification_policy_from_config("preferred")).unwrap(), + "preferred" + ); + } +} diff --git a/src/config.rs b/src/config.rs index c4457478..29f717a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -712,6 +712,9 @@ make_config! { /// Note that the checkbox would still be present, but ignored. disable_2fa_remember: bool, true, def, false; + /// WebAuthn 2FA user verification |> Controls whether PIN or biometric user verification is discouraged or preferred for WebAuthn 2FA. + webauthn_2fa_user_verification: String, true, def, "discouraged".to_owned(); + /// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid /// TOTP codes of the previous and next 30 seconds will be invalid. authenticator_disable_time_drift: bool, true, def, false; @@ -982,6 +985,13 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`."); } + match cfg.webauthn_2fa_user_verification.as_str() { + "discouraged" | "preferred" => (), + _ => err!( + "`WEBAUTHN_2FA_USER_VERIFICATION` is invalid. It needs to be one of the following options: discouraged or preferred" + ), + } + if let Some(log_file) = &cfg.log_file && std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() { From 416fe4aa843f5aa9b5fb8b527e048268aaf2f0f0 Mon Sep 17 00:00:00 2001 From: ManuA Date: Tue, 28 Jul 2026 16:48:20 +0200 Subject: [PATCH 2/4] changed admin look for webauth options --- ISHIELD_WEBAUTHN_NOTES.md | 14 ++++++++++++++ src/static/templates/admin/settings.hbs | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/ISHIELD_WEBAUTHN_NOTES.md b/ISHIELD_WEBAUTHN_NOTES.md index 694f6c0a..4b699533 100644 --- a/ISHIELD_WEBAUTHN_NOTES.md +++ b/ISHIELD_WEBAUTHN_NOTES.md @@ -25,6 +25,20 @@ times out. Registering the key without a configured PIN works. YubiKeys with an existing PIN work correctly in the same environment, and the iShield works on webauthn.io. +The timeout was subsequently reproduced on a clean Debian 13 test instance and +narrowed down further: + +| Existing WebAuthn credentials | Policy | iShield registration | +| --- | --- | --- | +| none | `discouraged` | succeeds without PIN | +| at least one YubiKey | `discouraged` | times out | +| at least one YubiKey | `preferred` | succeeds with PIN | + +The registered YubiKey was physically disconnected during the failing iShield +attempt. Removing only the YubiKey registration makes the iShield work with +`discouraged` again. This isolates the additional trigger to the non-empty +`excludeCredentials` list sent by Vaultwarden, rather than USB device contention. + Vaultwarden deliberately changes the registration and authentication policy to `userVerification: discouraged`, since WebAuthn is being used as a second factor. The iShield advertises `makeCredUvNotRqd: yes`, so the client is allowed to try diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs index cd5e3ca2..365b5a57 100644 --- a/src/static/templates/admin/settings.hbs +++ b/src/static/templates/admin/settings.hbs @@ -26,6 +26,16 @@ {{#case type "text" "number" "password"}}
+ {{#if (eq name "webauthn_2fa_user_verification")}} + +
+ Discouraged avoids requesting a PIN or biometric check for standard WebAuthn 2FA. + Preferred asks compatible authenticators for user verification and can improve compatibility with some security keys. +
+ {{else}}
@@ -33,6 +43,7 @@ {{/case}}
+ {{/if}}
{{/case}} {{#case type "checkbox"}} From c1325efe89eade45a71761ffaaec2503850dfa66 Mon Sep 17 00:00:00 2001 From: ManuA Date: Tue, 28 Jul 2026 17:18:07 +0200 Subject: [PATCH 3/4] changed admin-panel appearence to comply default admin-design --- .env.template | 7 +++---- src/api/core/two_factor/webauthn.rs | 15 ++++++++------- src/config.rs | 11 ++--------- src/static/templates/admin/settings.hbs | 11 ----------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/.env.template b/.env.template index 3ce84263..de6f11f9 100644 --- a/.env.template +++ b/.env.template @@ -602,10 +602,9 @@ ## Note that the checkbox would still be present, but ignored. # DISABLE_2FA_REMEMBER=false -## WebAuthn 2FA user verification policy -## Valid values are "discouraged" and "preferred". -## Setting this to "preferred" allows clients to request PIN or biometric verification when supported by the authenticator. -# WEBAUTHN_2FA_USER_VERIFICATION=discouraged +## Set WebAuthn 2FA user verification to preferred +## When enabled, compatible authenticators can request PIN or biometric verification. +# WEBAUTHN_2FA_USER_VERIFICATION=false ## ## Authenticator Settings ## Disable authenticator time drifted codes to be valid. diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index d0dd2b6d..98542311 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -45,13 +45,14 @@ static WEBAUTHN: LazyLock = LazyLock::new(|| { }); fn user_verification_policy() -> UserVerificationPolicy { - user_verification_policy_from_config(&CONFIG.webauthn_2fa_user_verification()) + user_verification_policy_from_config(CONFIG.webauthn_2fa_user_verification()) } -fn user_verification_policy_from_config(value: &str) -> UserVerificationPolicy { - match value { - "preferred" => UserVerificationPolicy::Preferred, - _ => UserVerificationPolicy::Discouraged_DO_NOT_USE, +fn user_verification_policy_from_config(preferred: bool) -> UserVerificationPolicy { + if preferred { + UserVerificationPolicy::Preferred + } else { + UserVerificationPolicy::Discouraged_DO_NOT_USE } } @@ -537,11 +538,11 @@ mod tests { #[test] fn configured_user_verification_policy() { assert_eq!( - serde_json::to_value(user_verification_policy_from_config("discouraged")).unwrap(), + serde_json::to_value(user_verification_policy_from_config(false)).unwrap(), "discouraged" ); assert_eq!( - serde_json::to_value(user_verification_policy_from_config("preferred")).unwrap(), + serde_json::to_value(user_verification_policy_from_config(true)).unwrap(), "preferred" ); } diff --git a/src/config.rs b/src/config.rs index 29f717a0..f49fbb98 100644 --- a/src/config.rs +++ b/src/config.rs @@ -712,8 +712,8 @@ make_config! { /// Note that the checkbox would still be present, but ignored. disable_2fa_remember: bool, true, def, false; - /// WebAuthn 2FA user verification |> Controls whether PIN or biometric user verification is discouraged or preferred for WebAuthn 2FA. - webauthn_2fa_user_verification: String, true, def, "discouraged".to_owned(); + /// Set WebAuthn 2FA user verification to preferred |> Discouraged avoids requesting a PIN or biometric check for standard WebAuthn 2FA. Preferred asks compatible authenticators for user verification and can improve compatibility with some security keys. + webauthn_2fa_user_verification: bool, true, def, false; /// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid /// TOTP codes of the previous and next 30 seconds will be invalid. @@ -985,13 +985,6 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`."); } - match cfg.webauthn_2fa_user_verification.as_str() { - "discouraged" | "preferred" => (), - _ => err!( - "`WEBAUTHN_2FA_USER_VERIFICATION` is invalid. It needs to be one of the following options: discouraged or preferred" - ), - } - if let Some(log_file) = &cfg.log_file && std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() { diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs index 365b5a57..cd5e3ca2 100644 --- a/src/static/templates/admin/settings.hbs +++ b/src/static/templates/admin/settings.hbs @@ -26,16 +26,6 @@ {{#case type "text" "number" "password"}}
- {{#if (eq name "webauthn_2fa_user_verification")}} - -
- Discouraged avoids requesting a PIN or biometric check for standard WebAuthn 2FA. - Preferred asks compatible authenticators for user verification and can improve compatibility with some security keys. -
- {{else}}
@@ -43,7 +33,6 @@ {{/case}}
- {{/if}}
{{/case}} {{#case type "checkbox"}} From 7823ac7e0f8b84d547d5dae24bcdbb2eb610b976 Mon Sep 17 00:00:00 2001 From: ManuA Date: Tue, 28 Jul 2026 18:11:07 +0200 Subject: [PATCH 4/4] Remove investigation notes from change --- ISHIELD_WEBAUTHN_NOTES.md | 347 -------------------------------------- 1 file changed, 347 deletions(-) delete mode 100644 ISHIELD_WEBAUTHN_NOTES.md diff --git a/ISHIELD_WEBAUTHN_NOTES.md b/ISHIELD_WEBAUTHN_NOTES.md deleted file mode 100644 index 4b699533..00000000 --- a/ISHIELD_WEBAUTHN_NOTES.md +++ /dev/null @@ -1,347 +0,0 @@ -# Swissbit iShield Key 2 Pro / WebAuthn notes - -Status as of 2026-07-19. - -## Current status - -- Affected authenticator: Swissbit iShield Key 2 Pro MIFARE -- Device version: `1.1.0` -- FIDO applet: `v1.4.0-0-gd69b47b` -- AAGUID: `7787a482-13e8-4784-8a06-c7ed49a7aaf4` -- Supported protocols: U2F, CTAP 2.0, CTAP 2.1 -- Relevant reported options: - - `clientPin: yes` - - `alwaysUv: no` - - `makeCredUvNotRqd: yes` -- Tested Vaultwarden release: `1.36.0` -- Tested Web Vault release: `2026.4.1` -- Tested browser: Google Chrome `150.0.7871.124` -- Server: Debian 13, Linux x86_64, SQLite, native installation created by the Proxmox VE Community Script -- Vaultwarden serves HTTPS directly; no reverse proxy is used. - -With an FIDO2 PIN already configured on the iShield, unmodified Vaultwarden does -not show a PIN prompt during registration. The WebAuthn ceremony eventually -times out. Registering the key without a configured PIN works. YubiKeys with an -existing PIN work correctly in the same environment, and the iShield works on -webauthn.io. - -The timeout was subsequently reproduced on a clean Debian 13 test instance and -narrowed down further: - -| Existing WebAuthn credentials | Policy | iShield registration | -| --- | --- | --- | -| none | `discouraged` | succeeds without PIN | -| at least one YubiKey | `discouraged` | times out | -| at least one YubiKey | `preferred` | succeeds with PIN | - -The registered YubiKey was physically disconnected during the failing iShield -attempt. Removing only the YubiKey registration makes the iShield work with -`discouraged` again. This isolates the additional trigger to the non-empty -`excludeCredentials` list sent by Vaultwarden, rather than USB device contention. - -Vaultwarden deliberately changes the registration and authentication policy to -`userVerification: discouraged`, since WebAuthn is being used as a second factor. -The iShield advertises `makeCredUvNotRqd: yes`, so the client is allowed to try -credential creation without PIN-based user verification. That path does not -complete in the tested combination. - -Changing both the client challenge and server-side ceremony state from -`discouraged` to `preferred` fixes the problem. Registration then requests the -PIN, completes after touching the key, and authentication also succeeds in a new -Incognito session. - -Upstream issue: - -- - -Swissbit support has also been informed. Following maintainer feedback, an -upstream implementation is in progress on the local branch -`webauthn-2fa-user-verification-config`. It adds the editable server option -`WEBAUTHN_2FA_USER_VERIFICATION`, with `discouraged` remaining the default and -`preferred` enabling the working PIN/UV flow. The option is also exposed by the -existing Vaultwarden admin configuration UI. - -## Diagnostic source patch - -For Vaultwarden 1.36.0, four values in -`src/api/core/two_factor/webauthn.rs` were changed: - -```diff -- state["rs"]["policy"] = Value::String("discouraged".to_string()); -+ state["rs"]["policy"] = Value::String("preferred".to_string()); - -- asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; -+ asc.user_verification = UserVerificationPolicy::Preferred; - -- state["ast"]["policy"] = Value::String("discouraged".to_string()); -+ state["ast"]["policy"] = Value::String("preferred".to_string()); - -- response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE; -+ response.public_key.user_verification = UserVerificationPolicy::Preferred; -``` - -Both the response sent to the browser and the server-side state must remain -consistent. Changing only the response is not the recommended test. - -## Current LXC binary and rollback - -The original 1.36.0 binary was saved as: - -```text -/opt/vaultwarden/bin/vaultwarden.original -``` - -The original SHA-256 recorded during the test was: - -```text -c7d507bb05a30af1ea3974fac53713be747730ce653e9ee26a1b3cd0a65b2c7e -``` - -Rollback to that binary: - -```bash -systemctl stop vaultwarden - -install -o root -g root -m 0755 \ - /opt/vaultwarden/bin/vaultwarden.original \ - /opt/vaultwarden/bin/vaultwarden - -rm -f /opt/vaultwarden/bin/.uv-preferred-version - -systemctl start vaultwarden -systemctl status vaultwarden --no-pager -``` - -An official PVE Community Script update will overwrite the active custom binary. -The iShield is expected to fail again after such an update unless upstream has -changed the policy or the custom build is reapplied. - -## Post-update rebuild script - -Install the following as `/usr/local/sbin/vaultwarden-uv-patch` inside the LXC. -It intentionally aborts if the upstream source no longer contains exactly the -four expected policy expressions. - -```bash -#!/usr/bin/env bash -set -Eeuo pipefail - -VW_ROOT="/opt/vaultwarden" -VW_BIN="${VW_ROOT}/bin/vaultwarden" -BUILD_DIR="/tmp/vaultwarden-uv-build" -SOURCE_FILE="src/api/core/two_factor/webauthn.rs" - -if [[ $EUID -ne 0 ]]; then - echo "ERROR: This script must be run as root." >&2 - exit 1 -fi - -if [[ ! -x "$VW_BIN" ]]; then - echo "ERROR: Vaultwarden binary not found: $VW_BIN" >&2 - exit 1 -fi - -VERSION="$("$VW_BIN" --version 2>/dev/null | - grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | - head -n1)" - -if [[ -z "$VERSION" ]]; then - echo "ERROR: Could not determine the installed Vaultwarden version." >&2 - exit 1 -fi - -PATCHED_MARKER="${VW_ROOT}/bin/.uv-preferred-version" - -if [[ -f "$PATCHED_MARKER" ]] && - [[ "$(<"$PATCHED_MARKER")" == "$VERSION" ]] && - "$VW_BIN" --version 2>/dev/null | grep -q 'uv-preferred'; then - echo "Vaultwarden $VERSION is already patched." - exit 0 -fi - -echo "Preparing UV-preferred build for Vaultwarden $VERSION" - -command -v git >/dev/null || - { echo "ERROR: git is not installed." >&2; exit 1; } - -command -v cargo >/dev/null || - { echo "ERROR: cargo is not available." >&2; exit 1; } - -if [[ "$BUILD_DIR" != "/tmp/vaultwarden-uv-build" ]]; then - echo "ERROR: Unexpected build directory: $BUILD_DIR" >&2 - exit 1 -fi - -rm -rf -- "$BUILD_DIR" - -git clone \ - --branch "$VERSION" \ - --depth 1 \ - https://github.com/dani-garcia/vaultwarden.git \ - "$BUILD_DIR" - -cd "$BUILD_DIR" - -if [[ ! -f "$SOURCE_FILE" ]]; then - echo "ERROR: Expected source file is missing: $SOURCE_FILE" >&2 - exit 1 -fi - -STATE_PATTERN='Value::String("discouraged".to_string())' -POLICY_PATTERN='UserVerificationPolicy::Discouraged_DO_NOT_USE' - -STATE_COUNT="$(grep -Fc "$STATE_PATTERN" "$SOURCE_FILE" || true)" -POLICY_COUNT="$(grep -Fc "$POLICY_PATTERN" "$SOURCE_FILE" || true)" - -if [[ "$STATE_COUNT" -ne 2 || "$POLICY_COUNT" -ne 2 ]]; then - echo "ERROR: Vaultwarden's WebAuthn implementation has changed." >&2 - echo "Expected two state policies and two challenge policies." >&2 - echo "Found state=$STATE_COUNT, challenge=$POLICY_COUNT." >&2 - echo "No binary was changed." >&2 - exit 1 -fi - -sed -i \ - 's/Value::String("discouraged".to_string())/Value::String("preferred".to_string())/g' \ - "$SOURCE_FILE" - -sed -i \ - 's/UserVerificationPolicy::Discouraged_DO_NOT_USE/UserVerificationPolicy::Preferred/g' \ - "$SOURCE_FILE" - -REMAINING_STATE="$(grep -Fc "$STATE_PATTERN" "$SOURCE_FILE" || true)" -REMAINING_POLICY="$(grep -Fc "$POLICY_PATTERN" "$SOURCE_FILE" || true)" -PREFERRED_STATE="$(grep -Fc 'Value::String("preferred".to_string())' "$SOURCE_FILE" || true)" -PREFERRED_POLICY="$(grep -Fc 'UserVerificationPolicy::Preferred' "$SOURCE_FILE" || true)" - -if [[ "$REMAINING_STATE" -ne 0 || - "$REMAINING_POLICY" -ne 0 || - "$PREFERRED_STATE" -lt 2 || - "$PREFERRED_POLICY" -lt 2 ]]; then - echo "ERROR: Patch verification failed. No binary was changed." >&2 - exit 1 -fi - -git diff --check - -echo "Applied source patch:" -git diff -- "$SOURCE_FILE" - -export VW_VERSION="${VERSION}-uv-preferred" - -cargo build \ - --locked \ - --features "sqlite,mysql,postgresql" \ - --release - -TEST_BIN="${VW_ROOT}/bin/vaultwarden.uv-preferred-${VERSION}" -OFFICIAL_BACKUP="${VW_ROOT}/bin/vaultwarden.official-${VERSION}" - -install -o root -g root -m 0755 \ - target/release/vaultwarden \ - "$TEST_BIN" - -if ldd "$TEST_BIN" | grep -q 'not found'; then - echo "ERROR: The new binary has unresolved libraries." >&2 - rm -f -- "$TEST_BIN" - exit 1 -fi - -"$TEST_BIN" --version - -if [[ ! -e "$OFFICIAL_BACKUP" ]]; then - install -o root -g root -m 0755 \ - "$VW_BIN" \ - "$OFFICIAL_BACKUP" -fi - -echo "Installing patched binary..." - -systemctl stop vaultwarden - -install -o root -g root -m 0755 \ - "$TEST_BIN" \ - "$VW_BIN" - -printf '%s\n' "$VERSION" >"$PATCHED_MARKER" - -if ! systemctl start vaultwarden; then - echo "ERROR: Patched Vaultwarden failed to start; restoring official binary." >&2 - - install -o root -g root -m 0755 \ - "$OFFICIAL_BACKUP" \ - "$VW_BIN" - - rm -f -- "$PATCHED_MARKER" - systemctl start vaultwarden - exit 1 -fi - -if ! systemctl is-active --quiet vaultwarden; then - echo "ERROR: Service is not active; restoring official binary." >&2 - - systemctl stop vaultwarden || true - install -o root -g root -m 0755 \ - "$OFFICIAL_BACKUP" \ - "$VW_BIN" - - rm -f -- "$PATCHED_MARKER" - systemctl start vaultwarden - exit 1 -fi - -echo -echo "Successfully installed:" -"$VW_BIN" --version - -systemctl status vaultwarden --no-pager -echo -echo "Official binary backup: $OFFICIAL_BACKUP" -``` - -Make it executable: - -```bash -chmod 0755 /usr/local/sbin/vaultwarden-uv-patch -``` - -Run it after the normal PVE Community Script update: - -```bash -vaultwarden-uv-patch -``` - -Then verify: - -```bash -/opt/vaultwarden/bin/vaultwarden --version -systemctl status vaultwarden --no-pager -journalctl -u vaultwarden -n 50 --no-pager -``` - -The version should contain a suffix similar to: - -```text -Vaultwarden 1.37.0-uv-preferred -``` - -## Version-specific rollback after using the script - -For example, to restore the official 1.36.0 binary saved by the script: - -```bash -systemctl stop vaultwarden - -install -o root -g root -m 0755 \ - /opt/vaultwarden/bin/vaultwarden.official-1.36.0 \ - /opt/vaultwarden/bin/vaultwarden - -rm -f /opt/vaultwarden/bin/.uv-preferred-version - -systemctl start vaultwarden -systemctl status vaultwarden --no-pager -``` - -Before running the script against a future release, first check issue #7437 and -the release notes. Do not apply the custom build if Vaultwarden has introduced an -official setting or otherwise changed the relevant WebAuthn behavior.