blob: 94a7829f76ffa1b6bdde02073721ba20c5233fdb [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/trusted_vault/download_keys_response_handler.h"
#include <vector>
#include "base/strings/string_number_conversions.h"
#include "components/trusted_vault/proto/vault.pb.h"
#include "components/trusted_vault/proto_string_bytes_conversion.h"
#include "components/trusted_vault/securebox.h"
#include "components/trusted_vault/standalone_trusted_vault_server_constants.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/trusted_vault/trusted_vault_crypto.h"
#include "components/trusted_vault/trusted_vault_server_constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace trusted_vault {
namespace {
using testing::ElementsAre;
using testing::Eq;
using testing::IsEmpty;
const char kEncodedPrivateKey[] =
"49e052293c29b5a50b0013eec9d030ac2ad70a42fe093be084264647cb04e16f";
std::unique_ptr<SecureBoxKeyPair> MakeTestKeyPair() {
std::vector<uint8_t> private_key_bytes;
bool success = base::HexStringToBytes(kEncodedPrivateKey, &private_key_bytes);
DCHECK(success);
return SecureBoxKeyPair::CreateByPrivateKeyImport(private_key_bytes);
}
void AddSecurityDomainMembership(
const std::string security_domain_path,
const SecureBoxPublicKey& member_public_key,
const std::vector<std::vector<uint8_t>>& trusted_vault_keys,
const std::vector<int>& trusted_vault_keys_versions,
const std::vector<std::vector<uint8_t>>& signing_keys,
trusted_vault_pb::SecurityDomainMember* member) {
DCHECK(member);
DCHECK_EQ(trusted_vault_keys.size(), trusted_vault_keys_versions.size());
DCHECK_EQ(trusted_vault_keys.size(), signing_keys.size());
trusted_vault_pb::SecurityDomainMember::SecurityDomainMembership* membership =
member->add_memberships();
membership->set_security_domain(std::move(security_domain_path));
for (size_t i = 0; i < trusted_vault_keys.size(); ++i) {
trusted_vault_pb::SharedMemberKey* shared_key = membership->add_keys();
shared_key->set_epoch(trusted_vault_keys_versions[i]);
AssignBytesToProtoString(
ComputeTrustedVaultWrappedKey(member_public_key, trusted_vault_keys[i]),
shared_key->mutable_wrapped_key());
if (!signing_keys[i].empty()) {
trusted_vault_pb::RotationProof* rotation_proof =
membership->add_rotation_proofs();
rotation_proof->set_new_epoch(trusted_vault_keys_versions[i]);
AssignBytesToProtoString(ComputeRotationProofForTesting(
/*trusted_vault_key=*/trusted_vault_keys[i],
/*prev_trusted_vault_key=*/signing_keys[i]),
rotation_proof->mutable_rotation_proof());
}
}
}
std::string CreateGetSecurityDomainMemberResponseWithSyncMembership(
const std::vector<std::vector<uint8_t>>& trusted_vault_keys,
const std::vector<int>& trusted_vault_keys_versions,
const std::vector<std::vector<uint8_t>>& signing_keys) {
trusted_vault_pb::SecurityDomainMember member;
AddSecurityDomainMembership(
GetSecurityDomainPath(SecurityDomainId::kChromeSync),
MakeTestKeyPair()->public_key(), trusted_vault_keys,
trusted_vault_keys_versions, signing_keys, &member);
return member.SerializeAsString();
}
class DownloadKeysResponseHandlerTest : public testing::Test {
public:
DownloadKeysResponseHandlerTest()
: handler_(SecurityDomainId::kChromeSync,
TrustedVaultKeyAndVersion(kKnownTrustedVaultKey,
kKnownTrustedVaultKeyVersion),
MakeTestKeyPair()) {}
~DownloadKeysResponseHandlerTest() override = default;
const DownloadKeysResponseHandler& handler() const { return handler_; }
const int kKnownTrustedVaultKeyVersion = 5;
const std::vector<uint8_t> kKnownTrustedVaultKey = {1, 2, 3, 4};
const std::vector<uint8_t> kTrustedVaultKey1 = {1, 2, 3, 5};
const std::vector<uint8_t> kTrustedVaultKey2 = {1, 2, 3, 6};
const std::vector<uint8_t> kTrustedVaultKey3 = {1, 2, 3, 7};
private:
const DownloadKeysResponseHandler handler_;
};
// All HttpStatuses except kSuccess should end up in kOtherError, kNetworkError
// or kMemberNotFound reporting.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleHttpErrors) {
EXPECT_THAT(
handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kNotFound,
/*response_body=*/std::string())
.status,
Eq(TrustedVaultDownloadKeysStatus::kMemberNotFound));
EXPECT_THAT(
handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kBadRequest,
/*response_body=*/std::string())
.status,
Eq(TrustedVaultDownloadKeysStatus::kOtherError));
EXPECT_THAT(
handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kConflict,
/*response_body=*/std::string())
.status,
Eq(TrustedVaultDownloadKeysStatus::kOtherError));
EXPECT_THAT(
handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kNetworkError,
/*response_body=*/std::string())
.status,
Eq(TrustedVaultDownloadKeysStatus::kNetworkError));
EXPECT_THAT(
handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kOtherError,
/*response_body=*/std::string())
.status,
Eq(TrustedVaultDownloadKeysStatus::kOtherError));
}
// Simplest legitimate case of key rotation, server side state corresponds to
// kKnownTrustedVaultKey -> kTrustedVaultKey1 key chain.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleSingleKeyRotation) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/{kKnownTrustedVaultKey, kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion, kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{{}, kKnownTrustedVaultKey}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kSuccess));
EXPECT_THAT(processed_response.downloaded_keys,
ElementsAre(kKnownTrustedVaultKey, kTrustedVaultKey1));
EXPECT_THAT(processed_response.last_key_version,
Eq(kKnownTrustedVaultKeyVersion + 1));
}
// Multiple key rotations may happen while client is offline, server-side key
// chain is kKnownTrustedVaultKey -> kTrustedVaultKey1 -> kTrustedVaultKey2.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleMultipleKeyRotations) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/
{kKnownTrustedVaultKey, kTrustedVaultKey1, kTrustedVaultKey2},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion, kKnownTrustedVaultKeyVersion + 1,
kKnownTrustedVaultKeyVersion + 2},
/*signing_keys=*/{{}, kKnownTrustedVaultKey, kTrustedVaultKey1}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kSuccess));
EXPECT_THAT(
processed_response.downloaded_keys,
ElementsAre(kKnownTrustedVaultKey, kTrustedVaultKey1, kTrustedVaultKey2));
EXPECT_THAT(processed_response.last_key_version,
Eq(kKnownTrustedVaultKeyVersion + 2));
}
// Server can already clean-up kKnownTrustedVaultKey, but it might still be
// possible to validate the key-chain.
// Full key chain is: kKnownTrustedVaultKey -> kTrustedVaultKey1 ->
// kTrustedVaultKey2.
// Server-side key chain is: kTrustedVaultKey1 -> kTrustedVaultKey2.
TEST_F(DownloadKeysResponseHandlerTest,
ShouldHandleAbsenseOfKnownKeyWhenKeyChainIsRecoverable) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/
{kTrustedVaultKey1, kTrustedVaultKey2},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion + 1,
kKnownTrustedVaultKeyVersion + 2},
/*signing_keys=*/
{kKnownTrustedVaultKey, kTrustedVaultKey1}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kSuccess));
EXPECT_THAT(processed_response.downloaded_keys,
ElementsAre(kTrustedVaultKey1, kTrustedVaultKey2));
EXPECT_THAT(processed_response.last_key_version,
Eq(kKnownTrustedVaultKeyVersion + 2));
}
// Server can already clean-up kKnownTrustedVaultKey and the following key. In
// this case client state is not sufficient to silently download keys and
// kKeyProofsVerificationFailed should be reported.
// Possible full key chain is: kKnownTrustedVaultKey -> kTrustedVaultKey1 ->
// kTrustedVaultKey2 -> kTrustedVaultKey3.
// Server side key chain is: kTrustedVaultKey2 -> kTrustedVaultKey3.
TEST_F(DownloadKeysResponseHandlerTest,
ShouldHandleAbsenseOfKnownKeyWhenKeyChainIsNotRecoverable) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/
{kTrustedVaultKey2, kTrustedVaultKey3},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion + 2,
kKnownTrustedVaultKeyVersion + 3},
/*signing_keys=*/
{kTrustedVaultKey1, kTrustedVaultKey2}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kKeyProofsVerificationFailed));
EXPECT_THAT(processed_response.downloaded_keys, IsEmpty());
}
// The test populates undecryptable/corrupted |wrapped_key| field, handler
// should return kMembershipCorrupted to allow client to restore the member by
// re-registration.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleUndecryptableKey) {
trusted_vault_pb::SecurityDomainMember member;
AddSecurityDomainMembership(
GetSecurityDomainPath(SecurityDomainId::kChromeSync),
MakeTestKeyPair()->public_key(),
/*trusted_vault_keys=*/{kKnownTrustedVaultKey, kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion, kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{{}, kKnownTrustedVaultKey}, &member);
// Corrupt wrapped key corresponding to kTrustedVaultKey1.
member.mutable_memberships(0)->mutable_keys(1)->set_wrapped_key(
"undecryptable_key");
EXPECT_THAT(handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/member.SerializeAsString())
.status,
Eq(TrustedVaultDownloadKeysStatus::kMembershipCorrupted));
}
// The test populates invalid |rotation_proof| field for the single key
// rotation. kTrustedVaultKey1 is expected to be signed with
// kKnownTrustedVaultKey, but instead it's signed with kTrustedVaultKey2.
TEST_F(DownloadKeysResponseHandlerTest,
ShouldHandleInvalidKeyProofOnSingleKeyRotation) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/{kKnownTrustedVaultKey, kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion, kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{{}, kTrustedVaultKey2}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kKeyProofsVerificationFailed));
EXPECT_THAT(processed_response.downloaded_keys, IsEmpty());
}
// The test populates invalid |rotation_proof| field for intermediate key when
// multiple key rotations have happened.
// kTrustedVaultKey1 is expected to be signed with kKnownTrustedVaultKey, but
// instead it's signed with kTrustedVaultKey2.
TEST_F(DownloadKeysResponseHandlerTest,
ShouldHandleInvalidKeyProofOnMultipleKeyRotations) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/{kKnownTrustedVaultKey, kTrustedVaultKey1,
kTrustedVaultKey2},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion, kKnownTrustedVaultKeyVersion + 1,
kKnownTrustedVaultKeyVersion + 2},
/*signing_keys=*/{{}, kTrustedVaultKey2, kTrustedVaultKey1}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kKeyProofsVerificationFailed));
EXPECT_THAT(processed_response.downloaded_keys, IsEmpty());
}
// In this scenario client already has most recent trusted vault key.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleAbsenseOfNewKeys) {
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/
CreateGetSecurityDomainMemberResponseWithSyncMembership(
/*trusted_vault_keys=*/{kKnownTrustedVaultKey},
/*trusted_vault_keys_versions=*/
{kKnownTrustedVaultKeyVersion},
/*signing_keys=*/{{}}));
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kNoNewKeys));
EXPECT_THAT(processed_response.downloaded_keys,
ElementsAre(kKnownTrustedVaultKey));
}
// Tests handling the situation, when response isn't a valid serialized
// SecurityDomainMemberProto proto.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleCorruptedResponseProto) {
EXPECT_THAT(handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/"corrupted_proto")
.status,
Eq(TrustedVaultDownloadKeysStatus::kOtherError));
}
// Client expects that the sync security domain membership exists, but the
// response indicates it doesn't by having no memberships.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleAbsenseOfMemberships) {
EXPECT_THAT(handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/trusted_vault_pb::SecurityDomainMember()
.SerializeAsString())
.status,
Eq(TrustedVaultDownloadKeysStatus::kMembershipNotFound));
}
// Same as above, but there is a different security domain membership.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleAbsenseOfSyncMembership) {
trusted_vault_pb::SecurityDomainMember member;
AddSecurityDomainMembership(
"other_domain", MakeTestKeyPair()->public_key(),
/*trusted_vault_keys=*/{kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/{kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{kKnownTrustedVaultKey}, &member);
EXPECT_THAT(handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/member.SerializeAsString())
.status,
Eq(TrustedVaultDownloadKeysStatus::kMembershipNotFound));
}
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleEmptyMembership) {
trusted_vault_pb::SecurityDomainMember member;
AddSecurityDomainMembership(
GetSecurityDomainPath(SecurityDomainId::kChromeSync),
MakeTestKeyPair()->public_key(),
/*trusted_vault_keys=*/{},
/*trusted_vault_keys_versions=*/{},
/*signing_keys=*/{}, &member);
EXPECT_THAT(handler()
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/member.SerializeAsString())
.status,
Eq(TrustedVaultDownloadKeysStatus::kMembershipEmpty));
}
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleAllSecurityDomains) {
for (const SecurityDomainId security_domain : kAllSecurityDomainIdValues) {
trusted_vault_pb::SecurityDomainMember member;
AddSecurityDomainMembership(
GetSecurityDomainPath(security_domain), MakeTestKeyPair()->public_key(),
/*trusted_vault_keys=*/{kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/{kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{kKnownTrustedVaultKey}, &member);
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
DownloadKeysResponseHandler(
security_domain,
TrustedVaultKeyAndVersion(kKnownTrustedVaultKey,
kKnownTrustedVaultKeyVersion),
MakeTestKeyPair())
.ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/member.SerializeAsString());
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kSuccess));
EXPECT_THAT(processed_response.downloaded_keys,
ElementsAre(kTrustedVaultKey1));
EXPECT_THAT(processed_response.last_key_version,
Eq(kKnownTrustedVaultKeyVersion + 1));
}
}
// Tests handling presence of other security domain memberships.
TEST_F(DownloadKeysResponseHandlerTest, ShouldHandleMultipleSecurityDomains) {
trusted_vault_pb::SecurityDomainMember member;
AddSecurityDomainMembership(
"other_domain", MakeTestKeyPair()->public_key(),
/*trusted_vault_keys=*/{kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/{kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{{}}, &member);
// Note: sync security domain membership is different by having correct
// rotation proof.
AddSecurityDomainMembership(
GetSecurityDomainPath(SecurityDomainId::kChromeSync),
MakeTestKeyPair()->public_key(),
/*trusted_vault_keys=*/{kTrustedVaultKey1},
/*trusted_vault_keys_versions=*/{kKnownTrustedVaultKeyVersion + 1},
/*signing_keys=*/{kKnownTrustedVaultKey}, &member);
const DownloadKeysResponseHandler::ProcessedResponse processed_response =
handler().ProcessResponse(
/*http_status=*/TrustedVaultRequest::HttpStatus::kSuccess,
/*response_body=*/member.SerializeAsString());
EXPECT_THAT(processed_response.status,
Eq(TrustedVaultDownloadKeysStatus::kSuccess));
EXPECT_THAT(processed_response.downloaded_keys,
ElementsAre(kTrustedVaultKey1));
EXPECT_THAT(processed_response.last_key_version,
Eq(kKnownTrustedVaultKeyVersion + 1));
}
} // namespace
} // namespace trusted_vault