1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[PM-4371] Implement PRF key rotation (#4157)

* Send rotateable keyset on list webauthn keys

* Implement basic prf key rotation

* Add validator for webauthn rotation

* Fix accounts controller tests

* Add webauthn rotation validator tests

* Introduce separate request model

* Fix tests

* Remove extra empty line

* Remove filtering in validator

* Don't send encrypted private key

* Fix tests

* Implement delegated webauthn db transactions

* Add backward compatibility

* Fix query not working

* Update migration sql

* Update dapper query

* Remove unused helper

* Rename webauthn to WebAuthnLogin

* Fix linter errors

* Fix tests

* Fix tests
This commit is contained in:
Bernd Schoolmann
2024-06-17 20:46:57 +02:00
committed by GitHub
parent a556462685
commit 3ad4bc1cab
19 changed files with 347 additions and 11 deletions

View File

@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Auth.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
@ -11,6 +12,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.UserKey;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
@ -67,6 +69,8 @@ public class AccountsControllerTests : IDisposable
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
_resetPasswordValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyRotationValidator;
public AccountsControllerTests()
@ -97,6 +101,7 @@ public class AccountsControllerTests : IDisposable
_sendValidator = Substitute.For<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>();
_emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
IEnumerable<EmergencyAccess>>>();
_webauthnKeyRotationValidator = Substitute.For<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>();
_resetPasswordValidator = Substitute
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>>();
@ -125,7 +130,8 @@ public class AccountsControllerTests : IDisposable
_folderValidator,
_sendValidator,
_emergencyAccessValidator,
_resetPasswordValidator
_resetPasswordValidator,
_webauthnKeyRotationValidator
);
}

View File

@ -1,6 +1,6 @@
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.Webauthn;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;

View File

@ -0,0 +1,93 @@
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Auth.Validators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Auth.Validators;
[SutProviderCustomize]
public class WebAuthnLoginKeyRotationValidatorTests
{
[Theory]
[BitAutoData]
public async Task ValidateAsync_WrongWebAuthnKeys_Throws(
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
{
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
EncryptedPublicKey = e.EncryptedPublicKey,
EncryptedUserKey = e.EncryptedUserKey
}).ToList();
var data = new WebAuthnCredential
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000002"),
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NullUserKey_Throws(
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
{
var guid = Guid.NewGuid();
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel
{
Id = guid,
EncryptedPublicKey = e.EncryptedPublicKey,
}).ToList();
var data = new WebAuthnCredential
{
Id = guid,
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_NullPublicKey_Throws(
SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,
IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)
{
var guid = Guid.NewGuid();
var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel
{
Id = guid,
EncryptedUserKey = e.EncryptedUserKey,
}).ToList();
var data = new WebAuthnCredential
{
Id = guid,
EncryptedPublicKey = "TestKey",
EncryptedUserKey = "Test"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new List<WebAuthnCredential> { data });
await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
using Bit.Core.Entities;
using Bit.Core.Services;
@ -19,6 +21,16 @@ public class RotateUserKeyCommandTests
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash)
.Returns(true);
foreach (var webauthnCred in model.WebAuthnKeys)
{
var dbWebauthnCred = new WebAuthnCredential
{
EncryptedPublicKey = "encryptedPublicKey",
EncryptedUserKey = "encryptedUserKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id)
.Returns(dbWebauthnCred);
}
var result = await sutProvider.Sut.RotateUserKeyAsync(user, model);
@ -31,6 +43,16 @@ public class RotateUserKeyCommandTests
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash)
.Returns(false);
foreach (var webauthnCred in model.WebAuthnKeys)
{
var dbWebauthnCred = new WebAuthnCredential
{
EncryptedPublicKey = "encryptedPublicKey",
EncryptedUserKey = "encryptedUserKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id)
.Returns(dbWebauthnCred);
}
var result = await sutProvider.Sut.RotateUserKeyAsync(user, model);
@ -43,6 +65,16 @@ public class RotateUserKeyCommandTests
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash)
.Returns(true);
foreach (var webauthnCred in model.WebAuthnKeys)
{
var dbWebauthnCred = new WebAuthnCredential
{
EncryptedPublicKey = "encryptedPublicKey",
EncryptedUserKey = "encryptedUserKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id)
.Returns(dbWebauthnCred);
}
await sutProvider.Sut.RotateUserKeyAsync(user, model);