1
0
mirror of https://github.com/bitwarden/server synced 2026-01-10 04:23:31 +00:00

[PM-27279] Implement TDE Registration with V2 Keys (#6671)

* Implement TDE v2 signup

* Clean up fallback logic for account keys

* Fix broken v2 logic

* Add comment

* Update comment
This commit is contained in:
Bernd Schoolmann
2025-12-15 17:48:37 +01:00
committed by GitHub
parent e9ba7ba315
commit 4f7e76dac7
7 changed files with 156 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Repositories;
@@ -38,6 +39,7 @@ public class AccountsControllerTests : IDisposable
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
private readonly IUserRepository _userRepository;
public AccountsControllerTests()
{
@@ -53,6 +55,7 @@ public class AccountsControllerTests : IDisposable
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
_userRepository = Substitute.For<IUserRepository>();
_sut = new AccountsController(
_organizationService,
@@ -66,7 +69,8 @@ public class AccountsControllerTests : IDisposable
_featureService,
_userAccountKeysQuery,
_twoFactorEmailService,
_changeKdfCommand
_changeKdfCommand,
_userRepository
);
}
@@ -738,5 +742,79 @@ public class AccountsControllerTests : IDisposable
_userService.GetUserByIdAsync(Arg.Any<Guid>())
.Returns(Task.FromResult((User)null));
}
[Theory, BitAutoData]
public async Task PostKeys_WithAccountKeys_CallsSetV2AccountCryptographicState(
User user,
KeysRequestModel model)
{
// Arrange
user.PublicKey = "public-key";
user.PrivateKey = "encrypted-private-key";
model.AccountKeys = new AccountKeysRequestModel
{
UserKeyEncryptedAccountPrivateKey = "wrapped-private-key",
AccountPublicKey = "public-key",
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "public-key",
WrappedPrivateKey = "wrapped-private-key",
SignedPublicKey = "signed-public-key"
},
SignatureKeyPair = new SignatureKeyPairRequestModel
{
VerifyingKey = "verifying-key",
SignatureAlgorithm = "ed25519",
WrappedSigningKey = "wrapped-signing-key"
},
SecurityState = new SecurityStateModel
{
SecurityState = "security-state",
SecurityVersion = 2
}
};
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.ReturnErrorOnExistingKeypair).Returns(false);
// Act
var result = await _sut.PostKeys(model);
// Assert
await _userRepository.Received(1).SetV2AccountCryptographicStateAsync(
user.Id,
Arg.Any<UserAccountKeysData>());
await _userService.DidNotReceiveWithAnyArgs().SaveUserAsync(Arg.Any<User>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
[Theory, BitAutoData]
public async Task PostKeys_WithoutAccountKeys_CallsSaveUser(
User user,
KeysRequestModel model)
{
// Arrange
user.PublicKey = null;
user.PrivateKey = null;
model.AccountKeys = null;
model.PublicKey = "public-key";
model.EncryptedPrivateKey = "encrypted-private-key";
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.ReturnErrorOnExistingKeypair).Returns(false);
// Act
var result = await _sut.PostKeys(model);
// Assert
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
u.PublicKey == model.PublicKey &&
u.PrivateKey == model.EncryptedPrivateKey));
await _userRepository.DidNotReceiveWithAnyArgs()
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
}

View File

@@ -139,6 +139,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
[StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey,
[Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
{
userAsymmetricKeys.AccountKeys = null;
// Localize substitutions to this test.
var localFactory = new IdentityApplicationFactory();
@@ -202,6 +203,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
[StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey,
[Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
{
userAsymmetricKeys.AccountKeys = null;
// Localize substitutions to this test.
var localFactory = new IdentityApplicationFactory();
localFactory.UpdateConfiguration("globalSettings:disableUserRegistration", "true");
@@ -233,6 +235,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
{
userAsymmetricKeys.AccountKeys = null;
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();
@@ -310,6 +313,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism, Guid orgSponsorshipId)
{
userAsymmetricKeys.AccountKeys = null;
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();
@@ -386,6 +390,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism, EmergencyAccess emergencyAccess)
{
userAsymmetricKeys.AccountKeys = null;
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();
@@ -455,6 +460,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
{
userAsymmetricKeys.AccountKeys = null;
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();

View File

@@ -21,6 +21,13 @@ namespace Bit.Identity.IntegrationTest.Endpoints;
[SutProviderCustomize]
public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
{
private static readonly KeysRequestModel TEST_ACCOUNT_KEYS = new KeysRequestModel
{
AccountKeys = null,
PublicKey = "public-key",
EncryptedPrivateKey = "encrypted-private-key",
};
private const int SecondsInMinute = 60;
private const int MinutesInHour = 60;
private const int SecondsInHour = SecondsInMinute * MinutesInHour;
@@ -53,6 +60,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
public async Task TokenEndpoint_GrantTypePassword_Success(RegisterFinishRequestModel requestModel)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
var localFactory = new IdentityApplicationFactory();
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
@@ -78,6 +86,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
requestModel.Email = $"{generatedUsername}@example.com";
var localFactory = new IdentityApplicationFactory();
@@ -103,6 +112,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
requestModel.Email = $"{generatedUsername}@example.com";
var localFactory = new IdentityApplicationFactory();
@@ -129,6 +139,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
requestModel.Email = $"{generatedUsername}@example.com";
var localFactory = new IdentityApplicationFactory();
@@ -152,6 +163,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
requestModel.Email = $"{generatedUsername}@example.com";
var localFactory = new IdentityApplicationFactory();
@@ -175,6 +187,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
requestModel.Email = $"{generatedUsername}@example.com";
var localFactory = new IdentityApplicationFactory();
@@ -196,6 +209,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
public async Task TokenEndpoint_GrantTypeRefreshToken_Success(RegisterFinishRequestModel requestModel)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
var localFactory = new IdentityApplicationFactory();
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
@@ -218,6 +232,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
public async Task TokenEndpoint_GrantTypeClientCredentials_Success(RegisterFinishRequestModel model)
{
model.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
var localFactory = new IdentityApplicationFactory();
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(model);
@@ -242,6 +257,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
RegisterFinishRequestModel model,
string deviceId)
{
model.UserAsymmetricKeys.AccountKeys = null;
var localFactory = new IdentityApplicationFactory();
var server = localFactory.WithWebHostBuilder(builder =>
{
@@ -445,6 +461,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(
RegisterFinishRequestModel requestModel)
{
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
const int AmountInOneSecondAllowed = 10;
// The rule we are testing is 10 requests in 1 second