From 19b5cc3e388b03a64af1af6c8a93448d0c4a7b72 Mon Sep 17 00:00:00 2001 From: Santiago Rodriguez <46354312+santiagorodriguez96@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:18:07 -0300 Subject: [PATCH 1/3] fix: reject authenticator data with BS flag set but BE flag unset WebAuthn Level 3 requires the relying party to verify that the BS bit is not set when the BE bit is not set, in both the registration and authentication ceremonies. A credential that is not backup eligible cannot be backed up, so this combination is invalid. Verification now raises `WebAuthn::BackupFlagsVerificationError` for it. `FakeClient#get` defaulted to BE=0/BS=1, which this check rejects. Its `backup_state` default is now `false`. https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion --- lib/webauthn/authenticator_response.rb | 7 ++++ lib/webauthn/fake_client.rb | 2 +- .../authenticator_assertion_response_spec.rb | 34 +++++++++++++++++ ...authenticator_attestation_response_spec.rb | 38 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/webauthn/authenticator_response.rb b/lib/webauthn/authenticator_response.rb index c31f45d4..3c16c648 100644 --- a/lib/webauthn/authenticator_response.rb +++ b/lib/webauthn/authenticator_response.rb @@ -10,6 +10,7 @@ module WebAuthn class VerificationError < Error; end class AuthenticatorDataVerificationError < VerificationError; end + class BackupFlagsVerificationError < VerificationError; end class ChallengeVerificationError < VerificationError; end class OriginVerificationError < VerificationError; end class RpIdVerificationError < VerificationError; end @@ -51,6 +52,8 @@ def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_v verify_item(:user_verified) end + verify_item(:backup_flags) + true end @@ -122,6 +125,10 @@ def valid_user_verified? authenticator_data.user_verified? end + def valid_backup_flags? + authenticator_data.credential_backup_eligible? || !authenticator_data.credential_backed_up? + end + def rp_id_from_origin(expected_origin) URI.parse(expected_origin.first).host if expected_origin.size == 1 end diff --git a/lib/webauthn/fake_client.rb b/lib/webauthn/fake_client.rb index 027ef704..0fe0ffc1 100644 --- a/lib/webauthn/fake_client.rb +++ b/lib/webauthn/fake_client.rb @@ -85,7 +85,7 @@ def get(challenge: fake_challenge, user_present: true, user_verified: false, backup_eligibility: false, - backup_state: true, + backup_state: false, sign_count: nil, extensions: nil, user_handle: nil, diff --git a/spec/webauthn/authenticator_assertion_response_spec.rb b/spec/webauthn/authenticator_assertion_response_spec.rb index ce828fe6..14e59b65 100644 --- a/spec/webauthn/authenticator_assertion_response_spec.rb +++ b/spec/webauthn/authenticator_assertion_response_spec.rb @@ -223,6 +223,40 @@ end end + describe "backup flags validation" do + let(:assertion) do + client.get(challenge: original_challenge, backup_eligibility: backup_eligibility, backup_state: backup_state) + end + + context "when the credential is backup eligible and backed up" do + let(:backup_eligibility) { true } + let(:backup_state) { true } + + it_behaves_like "a valid assertion response" + end + + context "when the credential is backup eligible and not backed up" do + let(:backup_eligibility) { true } + let(:backup_state) { false } + + it_behaves_like "a valid assertion response" + end + + context "when the credential is not backup eligible and not backed up" do + let(:backup_eligibility) { false } + let(:backup_state) { false } + + it_behaves_like "a valid assertion response" + end + + context "when the credential is not backup eligible but backed up" do + let(:backup_eligibility) { false } + let(:backup_state) { true } + + it_behaves_like "an invalid assertion response that raises", WebAuthn::BackupFlagsVerificationError + end + end + describe "challenge validation" do context "if challenge doesn't match" do let(:challenge) { fake_challenge } diff --git a/spec/webauthn/authenticator_attestation_response_spec.rb b/spec/webauthn/authenticator_attestation_response_spec.rb index 91ce28a3..25b2e76b 100644 --- a/spec/webauthn/authenticator_attestation_response_spec.rb +++ b/spec/webauthn/authenticator_attestation_response_spec.rb @@ -577,6 +577,44 @@ end end + describe "backup flags validation" do + let(:public_key_credential) do + client.create(challenge: original_challenge, backup_eligibility: backup_eligibility, backup_state: backup_state) + end + + before do + WebAuthn.configuration.allowed_origins = [origin] + end + + context "when the credential is backup eligible and backed up" do + let(:backup_eligibility) { true } + let(:backup_state) { true } + + it_behaves_like "a valid attestation response" + end + + context "when the credential is backup eligible and not backed up" do + let(:backup_eligibility) { true } + let(:backup_state) { false } + + it_behaves_like "a valid attestation response" + end + + context "when the credential is not backup eligible and not backed up" do + let(:backup_eligibility) { false } + let(:backup_state) { false } + + it_behaves_like "a valid attestation response" + end + + context "when the credential is not backup eligible but backed up" do + let(:backup_eligibility) { false } + let(:backup_state) { true } + + it_behaves_like "an invalid attestation response that raises", WebAuthn::BackupFlagsVerificationError + end + end + describe "rp_id validation" do let(:original_challenge) { fake_challenge } From ab3c419cec1531a99c67a256d4642604eb456e41 Mon Sep 17 00:00:00 2001 From: Santiago Rodriguez <46354312+santiagorodriguez96@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:18:07 -0300 Subject: [PATCH 2/3] docs: add CHANGELOG entry for backup flags verification --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 127d5532..5808e26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Added - Add support for crossOrigin/topOrigin verification during credential registration and authentication. [#486](https://github.com/cedarcode/webauthn-ruby/pull/486) [@nicolastemciuc] +- Reject authenticator data where the BS (backup state) flag is set but the BE (backup eligibility) flag is not, during credential registration and authentication. Raises `WebAuthn::BackupFlagsVerificationError`. [#516](https://github.com/cedarcode/webauthn-ruby/pull/516) [@santiagorodriguez96] + +### Changed + +- `WebAuthn::FakeClient#get` now defaults `backup_state` to `false`. The previous default (`backup_eligibility: false`, `backup_state: true`) produced authenticator data that now fails verification. [@santiagorodriguez96] ## [v3.4.3](https://github.com/cedarcode/webauthn-ruby/compare/v3.4.2...v3.4.3/) - 2025-10-23 From 9ed0074aa05e5f9920370dafd9b3ee9d6bebbb16 Mon Sep 17 00:00:00 2001 From: Santiago Rodriguez <46354312+santiagorodriguez96@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:08:29 -0300 Subject: [PATCH 3/3] docs: add missing PR link to CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8535cafb..452b660f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ ### Changed - Bump the default `credential_options_timeout` from `120_000` to `300_000` milliseconds (5 minutes). [#515](https://github.com/cedarcode/webauthn-ruby/pull/515) [@santiagorodriguez96] -- `WebAuthn::FakeClient#get` now defaults `backup_state` to `false`. The previous default (`backup_eligibility: false`, `backup_state: true`) produced authenticator data that now fails verification. [@santiagorodriguez96] +- `WebAuthn::FakeClient#get` now defaults `backup_state` to `false`. The previous default (`backup_eligibility: false`, `backup_state: true`) produced authenticator data that now fails verification. [#516](https://github.com/cedarcode/webauthn-ruby/pull/516) [@santiagorodriguez96] ### Removed