From 721fda0aaa5a68278ab6f5cc1b2ca80f87bd1a35 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:30:00 +0200 Subject: [PATCH] [PM-25473] Non-encryption passkeys prevent key rotation (#6359) * use webauthn credentials that have encrypted user key for user key rotation * where condition simplification --- .../WebAuthnLoginKeyRotationValidator.cs | 28 +++++---- src/Core/Auth/Entities/WebAuthnCredential.cs | 17 +++++ .../WebauthnLoginKeyRotationValidatorTests.cs | 62 ++++++++++++------- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs b/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs index 9c7efe0fbe..e92be11cd2 100644 --- a/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs @@ -1,4 +1,5 @@ using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; @@ -6,7 +7,13 @@ using Bit.Core.Exceptions; namespace Bit.Api.KeyManagement.Validators; -public class WebAuthnLoginKeyRotationValidator : IRotationValidator, IEnumerable> +/// +/// Validates WebAuthn credentials during key rotation. Only processes credentials that have PRF enabled +/// and have encrypted user, public, and private keys. Ensures all such credentials are included +/// in the rotation request with the required encrypted keys. +/// +public class WebAuthnLoginKeyRotationValidator : IRotationValidator, + IEnumerable> { private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; @@ -15,24 +22,20 @@ public class WebAuthnLoginKeyRotationValidator : IRotationValidator> ValidateAsync(User user, IEnumerable keysToRotate) + public async Task> ValidateAsync(User user, + IEnumerable keysToRotate) { var result = new List(); - var existing = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); - if (existing == null) + var validCredentials = (await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id)) + .Where(credential => credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled).ToList(); + if (validCredentials.Count == 0) { return result; } - var validCredentials = existing.Where(credential => credential.SupportsPrf); - if (!validCredentials.Any()) + foreach (var webAuthnCredential in validCredentials) { - return result; - } - - foreach (var ea in validCredentials) - { - var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == ea.Id); + var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == webAuthnCredential.Id); if (keyToRotate == null) { throw new BadRequestException("All existing webauthn prf keys must be included in the rotation."); @@ -42,6 +45,7 @@ public class WebAuthnLoginKeyRotationValidator : IRotationValidator [MaxLength(20)] public string Type { get; set; } public Guid AaGuid { get; set; } + + /// + /// User key encrypted with this WebAuthn credential's public key (EncryptedPublicKey field). + /// [MaxLength(2000)] public string EncryptedUserKey { get; set; } + + /// + /// Private key encrypted with an external key for secure storage. + /// [MaxLength(2000)] public string EncryptedPrivateKey { get; set; } + + /// + /// Public key encrypted with the user key for key rotation. + /// [MaxLength(2000)] public string EncryptedPublicKey { get; set; } + + /// + /// Indicates whether this credential supports PRF (Pseudo-Random Function) extension. + /// public bool SupportsPrf { get; set; } + public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs index 93652735ef..664a46bc9c 100644 --- a/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs @@ -33,8 +33,9 @@ public class WebAuthnLoginKeyRotationValidatorTests { Id = guid, SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) .Returns(new List { data }); @@ -45,8 +46,12 @@ public class WebAuthnLoginKeyRotationValidatorTests } [Theory] - [BitAutoData] - public async Task ValidateAsync_DoesNotSupportPRF_Ignores( + [BitAutoData(false, null, null, null)] + [BitAutoData(true, null, "TestPublicKey", "TestPrivateKey")] + [BitAutoData(true, "TestUserKey", null, "TestPrivateKey")] + [BitAutoData(true, "TestUserKey", "TestPublicKey", null)] + public async Task ValidateAsync_NotEncryptedOrPrfNotSupported_Ignores( + bool supportsPrf, string encryptedUserKey, string encryptedPublicKey, string encryptedPrivateKey, SutProvider sutProvider, User user, IEnumerable webauthnRotateCredentialData) { @@ -58,7 +63,14 @@ public class WebAuthnLoginKeyRotationValidatorTests EncryptedPublicKey = e.EncryptedPublicKey, }).ToList(); - var data = new WebAuthnCredential { Id = guid, EncryptedUserKey = "Test", EncryptedPublicKey = "TestKey" }; + var data = new WebAuthnCredential + { + Id = guid, + SupportsPrf = supportsPrf, + EncryptedUserKey = encryptedUserKey, + EncryptedPublicKey = encryptedPublicKey, + EncryptedPrivateKey = encryptedPrivateKey + }; sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) .Returns(new List { data }); @@ -69,7 +81,7 @@ public class WebAuthnLoginKeyRotationValidatorTests [Theory] [BitAutoData] - public async Task ValidateAsync_WrongWebAuthnKeys_Throws( + public async Task ValidateAsync_WebAuthnKeysNotMatchingExisting_Throws( SutProvider sutProvider, User user, IEnumerable webauthnRotateCredentialData) { @@ -84,10 +96,12 @@ public class WebAuthnLoginKeyRotationValidatorTests { Id = Guid.Parse("00000000-0000-0000-0000-000000000002"), SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) + .Returns(new List { data }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); @@ -100,20 +114,24 @@ public class WebAuthnLoginKeyRotationValidatorTests IEnumerable webauthnRotateCredentialData) { var guid = Guid.NewGuid(); - var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel - { - Id = guid, - EncryptedPublicKey = e.EncryptedPublicKey, - }).ToList(); + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => + new WebAuthnLoginRotateKeyRequestModel + { + Id = guid, + EncryptedPublicKey = e.EncryptedPublicKey, + EncryptedUserKey = null + }).ToList(); var data = new WebAuthnCredential { Id = guid, SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) + .Returns(new List { data }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); @@ -131,19 +149,21 @@ public class WebAuthnLoginKeyRotationValidatorTests { Id = guid, EncryptedUserKey = e.EncryptedUserKey, + EncryptedPublicKey = null, }).ToList(); var data = new WebAuthnCredential { Id = guid, SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) + .Returns(new List { data }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); } - }