| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/enclave/enclave_protocol_utils.h" |
| |
| #include <optional> |
| #include <string> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/containers/span.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/values.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/values.h" |
| #include "components/cbor/writer.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "components/sync/protocol/webauthn_credential_specifics.pb.h" |
| #include "device/fido/ctap_make_credential_request.h" |
| #include "device/fido/enclave/constants.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/fido_transport_protocol.h" |
| #include "device/fido/json_request.h" |
| #include "device/fido/public_key_credential_params.h" |
| #include "device/fido/public_key_credential_rp_entity.h" |
| #include "device/fido/public_key_credential_user_entity.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace device { |
| namespace { |
| |
| constexpr uint8_t kHandshakeHash[32] = { |
| 0xac, 0xf0, 0xdf, 0xe4, 0x51, 0xe4, 0x6d, 0x77, 0xfc, 0x64, 0xc1, |
| 0x1f, 0xe9, 0x40, 0xc5, 0x00, 0x66, 0xd9, 0x0c, 0x54, 0xaf, 0x27, |
| 0xe9, 0xaa, 0x91, 0xe4, 0x4a, 0xd2, 0x72, 0x55, 0xbe, 0xd1}; |
| constexpr uint8_t kDeviceId[] = "device0"; |
| constexpr uint8_t kSignature[] = "signature"; |
| constexpr uint8_t kUserId[] = "ab"; |
| constexpr uint8_t kEncryptedPasskey[] = {1, 2, 3, 4}; |
| constexpr char kClientDataJson[] = "client_data_json"; |
| constexpr uint8_t kClientDataJsonHash[] = { |
| 0x13, 0xb2, 0x37, 0x27, 0x97, 0xd5, 0xca, 0x8a, 0xfa, 0x37, 0xb2, |
| 0x91, 0x46, 0x0a, 0x82, 0x83, 0x35, 0x07, 0x0f, 0xea, 0xc6, 0x0f, |
| 0xb5, 0x42, 0xbf, 0x2d, 0x7a, 0x03, 0xda, 0xd6, 0xca, 0xf2, |
| }; |
| constexpr char kRpId[] = "test.example"; |
| constexpr char kGetAssertionRequestJson[] = R"({ |
| "allowCredentials":[ |
| { |
| "id":"FBUW", |
| "transports":["usb"], |
| "type":"public-key" |
| } |
| ], |
| "challenge":"dGVzdCBjaGFsbGVuZ2U", |
| "rpId":"test.example", |
| "userVerification":"required"})"; |
| constexpr char kMakeCredentialRequestJson[] = R"({ |
| "attestation":"direct", |
| "authenticatorSelection":{ |
| "authenticatorAttachment":"platform", |
| "residentKey":"required", |
| "userVerification":"required" |
| }, |
| "challenge":"dGVzdCBjaGFsbGVuZ2U", |
| "excludeCredentials":[ |
| { |
| "id":"FBUW", |
| "transports":["usb"], |
| "type":"public-key" |
| }], |
| "pubKeyCredParams":[ |
| { |
| "alg":-7, |
| "type":"public-key" |
| }, |
| { |
| "alg":-257, |
| "type":"public-key" |
| }], |
| "rp":{ |
| "id":"test.example", |
| "name":"Example LLC"}, |
| "user":{ |
| "displayName":"Example User", |
| "id":"dGVzdCB1c2VyIGlk", |
| "name":"user@test.example"}})"; |
| |
| // Hex outputs are encoded CBOR serializations of test responses. |
| constexpr char kGetAssertionHexResponse[] = |
| "81A1626F6BA168726573706F6E7365A3697369676E6174757265445369676E6A7573657248" |
| "616E646C654261627161757468656E74696361746F724461746158251194228DA8FDBDEEFD" |
| "261BD7B6595CFD70A50D70C6407BCF013DE96D4EFB17DE010000003B"; |
| constexpr char kMakeCredentialHexResponse[] = |
| "81A1626F6BA3677075625F6B657944050607086776657273696F6E0169656E637279707465" |
| "644401020304"; |
| |
| // An example response with the top-level "ok" key, dummy large blob and PRF |
| // values. |
| constexpr char kCompleteGetAssertionHexResponse[] = |
| "A1626F6B81A1626F6BA3637072661904D268726573706F6E7365A3697369676E6174757265" |
| "A2646461746184185318691867186E6474797065664275666665726A7573657248616E646C" |
| "65A2646461746182186118626474797065664275666665727161757468656E74696361746F" |
| "7244617461A2646461746198251118941822188D18A818FD18BD18EE18FD1826181B18D718" |
| "B61859185C18FD187018A50D187018C61840187B18CF01183D18E9186D184E18FB1718DE01" |
| "000000183B647479706566427566666572696C61726765426C6F62A264726561641904D264" |
| "73697A6501"; |
| |
| constexpr int32_t kWrappedSecretVersion = 952; |
| |
| struct BadResponseTestCase { |
| std::string name; |
| std::string hex_cbor; |
| }; |
| |
| BadResponseTestCase kFailingGetAssertionResponses[] = { |
| {"Command response element is not a map", "81F5"}, |
| {"Error received from enclave", |
| "81A16365727270536572766572206572726F7220313031"}, |
| {"Command response did not contain a successful response or an error", |
| "81A167556E6B6E6F776E70536572766572206572726F7220313031"}, |
| {"Command response did not contain a response field", |
| "81A1626F6BA167556E6B6E6F776E60"}, |
| {"Invalid AuthenticatorGetAssertionResponse", |
| "81A1626F6BA168726573706F6E7365A2697369676E61747572656655326C6E62676A75736" |
| "57248616E646C6563595749"}}; |
| |
| BadResponseTestCase kFailingMakeCredentialResponses[] = { |
| {"Command response element is not a map", "81F5"}, |
| {"Error received from enclave", |
| "81A16365727270536572766572206572726F7220313031"}, |
| {"Command response did not contain a successful response or an error", |
| "81A167556E6B6E6F776E70536572766572206572726F7220313031"}, |
| {"MakeCredential response did not contain a version", |
| "81A1626F6BA2667075624B6579440506070869656E637279707465644401020304"}, |
| {"MakeCredential response did not contain a public key", |
| "81A1626F6BA26776657273696F6E0169656E637279707465644401020304"}, |
| {"MakeCredential response did not contain an encrypted passkey", |
| "81A1626F6BA2667075624B657944050607086776657273696F6E01"}, |
| }; |
| |
| // A single data-driven test that covers all malformed largeBlob cases. |
| using BlobBuilder = base::RepeatingCallback<cbor::Value::MapValue()>; |
| |
| struct LargeBlobFailureCase { |
| BlobBuilder build_blob; |
| const char* expected_error; |
| }; |
| |
| sync_pb::WebauthnCredentialSpecifics PasskeyEntity() { |
| sync_pb::WebauthnCredentialSpecifics entity = |
| sync_pb::WebauthnCredentialSpecifics::default_instance(); |
| return entity; |
| } |
| |
| void FakeSigningCallback( |
| enclave::SignedMessage to_be_signed, |
| base::OnceCallback<void(std::optional<enclave::ClientSignature>)> |
| callback) { |
| base::span<const uint8_t> message_span = to_be_signed; |
| EXPECT_EQ(fido_parsing_utils::Materialize(message_span.first(32u)), |
| fido_parsing_utils::Materialize(kHandshakeHash)); |
| |
| enclave::ClientSignature ret; |
| ret.device_id = fido_parsing_utils::Materialize(kDeviceId); |
| ret.signature = fido_parsing_utils::Materialize(kSignature); |
| ret.key_type = enclave::ClientKeyType::kHardware; |
| std::move(callback).Run(std::move(ret)); |
| } |
| |
| cbor::Value MakeGetAssertionResponseWithLargeBlob( |
| cbor::Value::MapValue large_blob_map) { |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes(kGetAssertionHexResponse, &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| const cbor::Value::MapValue& outer_map = response_cbor.GetArray()[0].GetMap(); |
| const cbor::Value::MapValue& success_map = |
| outer_map.find(cbor::Value("ok"))->second.GetMap(); |
| const_cast<cbor::Value::MapValue&>(success_map) |
| .insert_or_assign(cbor::Value("largeBlob"), |
| cbor::Value(std::move(large_blob_map))); |
| return response_cbor; |
| } |
| |
| // Class to receive the result of a BuildCommandRequestBody call. Only usable |
| // once per instance. |
| class BuildCommandCompletionWaiter { |
| public: |
| BuildCommandCompletionWaiter() = default; |
| |
| BuildCommandCompletionWaiter(const BuildCommandCompletionWaiter&) = delete; |
| BuildCommandCompletionWaiter& operator=(const BuildCommandCompletionWaiter&) = |
| delete; |
| |
| ~BuildCommandCompletionWaiter() { loop_.Quit(); } |
| |
| void CompletionCallback(std::optional<std::vector<uint8_t>> result) { |
| result_ = std::move(*result); |
| loop_.Quit(); |
| } |
| |
| const std::vector<uint8_t>& result() { return result_; } |
| |
| void Wait() { loop_.Run(); } |
| |
| private: |
| std::vector<uint8_t> result_; |
| base::RunLoop loop_; |
| }; |
| |
| class EnclaveProtocolUtilsTest : public testing::Test { |
| public: |
| EnclaveProtocolUtilsTest() = default; |
| |
| EnclaveProtocolUtilsTest(const EnclaveProtocolUtilsTest&) = delete; |
| EnclaveProtocolUtilsTest& operator=(const EnclaveProtocolUtilsTest&) = delete; |
| |
| ~EnclaveProtocolUtilsTest() override = default; |
| |
| void SetUp() override { |
| device_id_ = fido_parsing_utils::Materialize(kDeviceId); |
| user_id_ = fido_parsing_utils::Materialize(kUserId); |
| encrypted_passkey_ = fido_parsing_utils::Materialize(kEncryptedPasskey); |
| } |
| |
| // This checks the outer map values of a request, which are common to all |
| // request types. |
| std::optional<cbor::Value> ValidateRequestFormatAndReturnCommandList( |
| const cbor::Value& request_cbor) { |
| EXPECT_TRUE(request_cbor.is_map()); |
| EXPECT_NE(request_cbor.GetMap().find(cbor::Value("sig")), |
| request_cbor.GetMap().end()); |
| EXPECT_EQ(request_cbor.GetMap() |
| .find(cbor::Value("auth_level")) |
| ->second.GetString(), |
| "hw"); |
| EXPECT_EQ(request_cbor.GetMap() |
| .find(cbor::Value("device_id")) |
| ->second.GetBytestring(), |
| device_id()); |
| auto encoded_request = |
| request_cbor.GetMap().find(cbor::Value("encoded_requests")); |
| EXPECT_NE(encoded_request, request_cbor.GetMap().end()); |
| std::optional<cbor::Value> decoded_command = |
| cbor::Reader::Read(encoded_request->second.GetBytestring()); |
| EXPECT_TRUE(decoded_command); |
| EXPECT_TRUE(decoded_command->is_array()); |
| EXPECT_EQ(decoded_command->GetArray().size(), 1u); |
| return decoded_command; |
| } |
| |
| std::vector<uint8_t>& device_id() { return device_id_; } |
| |
| base::span<const uint8_t, 32> handshake_hash() { return kHandshakeHash; } |
| |
| std::vector<uint8_t>& user_id() { return user_id_; } |
| |
| std::vector<uint8_t>& encrypted_passkey() { return encrypted_passkey_; } |
| |
| std::vector<uint8_t> wrapped_secret() { return wrapped_secret_; } |
| |
| std::vector<uint8_t> secret() { return secret_; } |
| |
| private: |
| const std::vector<uint8_t> wrapped_secret_ = {1, 2, 3, 4, 5}; |
| const std::vector<uint8_t> secret_ = {6, 7, 8, 9, 0}; |
| std::vector<uint8_t> device_id_; |
| std::vector<uint8_t> user_id_; |
| std::vector<uint8_t> encrypted_passkey_; |
| base::test::TaskEnvironment task_environment_; |
| base::test::ScopedFeatureList scoped_feature_list_{ |
| kWebAuthenticationHashClientDataJsonForEnclave}; |
| }; |
| |
| } // namespace |
| |
| namespace enclave { |
| |
| TEST_F(EnclaveProtocolUtilsTest, BuildGetAssertionRequest_Success) { |
| BuildCommandCompletionWaiter waiter; |
| auto entity = PasskeyEntity(); |
| entity.set_rp_id(kRpId); |
| std::optional<base::Value> parsed_json = base::JSONReader::Read( |
| kGetAssertionRequestJson, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| EXPECT_TRUE(parsed_json); |
| auto json_request = |
| base::MakeRefCounted<JSONRequest>(std::move(*parsed_json)); |
| BuildCommandRequestBody( |
| BuildGetAssertionCommand(std::move(entity), json_request, kClientDataJson, |
| /*claimed_pin=*/nullptr, wrapped_secret(), |
| /*secret=*/std::nullopt), |
| base::BindOnce(&FakeSigningCallback), handshake_hash(), |
| base::BindOnce(&BuildCommandCompletionWaiter::CompletionCallback, |
| base::Unretained(&waiter))); |
| |
| waiter.Wait(); |
| |
| std::optional<cbor::Value> request_cbor = cbor::Reader::Read(waiter.result()); |
| auto decoded_command = |
| ValidateRequestFormatAndReturnCommandList(*request_cbor); |
| auto& command_element = decoded_command->GetArray()[0]; |
| auto& command_map = command_element.GetMap(); |
| EXPECT_EQ(command_map.find(cbor::Value("cmd"))->second.GetString(), |
| "passkeys/assert"); |
| EXPECT_TRUE(command_map.find(cbor::Value("claimed_pin")) == |
| command_map.end()); |
| EXPECT_TRUE(command_map.find(cbor::Value("wrapped_pin_data")) == |
| command_map.end()); |
| EXPECT_THAT(command_map.find(cbor::Value("client_data_json_hash")) |
| ->second.GetBytestring(), |
| testing::ElementsAreArray(kClientDataJsonHash)); |
| auto& request_value_map = |
| command_map.find(cbor::Value("request"))->second.GetMap(); |
| EXPECT_EQ(request_value_map.find(cbor::Value("rpId"))->second.GetString(), |
| "test.example"); |
| |
| auto& serialized_passkey_entity = |
| command_map.find(cbor::Value("protobuf"))->second.GetBytestring(); |
| sync_pb::WebauthnCredentialSpecifics out_entity; |
| EXPECT_TRUE(out_entity.ParseFromArray(serialized_passkey_entity.data(), |
| serialized_passkey_entity.size())); |
| EXPECT_EQ(out_entity.rp_id(), std::string(kRpId)); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, BuildGetAssertionRequest_WithPIN) { |
| BuildCommandCompletionWaiter waiter; |
| auto entity = PasskeyEntity(); |
| entity.set_rp_id(kRpId); |
| std::optional<base::Value> parsed_json = base::JSONReader::Read( |
| kGetAssertionRequestJson, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| EXPECT_TRUE(parsed_json); |
| auto json_request = |
| base::MakeRefCounted<JSONRequest>(std::move(*parsed_json)); |
| const std::vector<uint8_t> pin_claim = {1, 2, 3}; |
| const std::vector<uint8_t> wrapped_pin = {4, 5, 6, 7}; |
| auto claimed_pin = std::make_unique<ClaimedPIN>(pin_claim, wrapped_pin); |
| BuildCommandRequestBody( |
| BuildGetAssertionCommand(std::move(entity), json_request, kClientDataJson, |
| std::move(claimed_pin), wrapped_secret(), |
| /*secret=*/std::nullopt), |
| base::BindOnce(&FakeSigningCallback), handshake_hash(), |
| base::BindOnce(&BuildCommandCompletionWaiter::CompletionCallback, |
| base::Unretained(&waiter))); |
| |
| waiter.Wait(); |
| |
| std::optional<cbor::Value> request_cbor = cbor::Reader::Read(waiter.result()); |
| auto decoded_command = |
| ValidateRequestFormatAndReturnCommandList(*request_cbor); |
| auto& command_element = decoded_command->GetArray()[0]; |
| auto& command_map = command_element.GetMap(); |
| |
| EXPECT_EQ(command_map.find(cbor::Value("claimed_pin")) |
| ->second.GetBytestring() |
| .size(), |
| 3u); |
| EXPECT_EQ(command_map.find(cbor::Value("wrapped_pin_data")) |
| ->second.GetBytestring() |
| .size(), |
| 4u); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, BuildMakeCredentialRequest_Success) { |
| BuildCommandCompletionWaiter waiter; |
| std::optional<base::Value> parsed_json = base::JSONReader::Read( |
| kMakeCredentialRequestJson, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| EXPECT_TRUE(parsed_json); |
| auto json_request = |
| base::MakeRefCounted<JSONRequest>(std::move(*parsed_json)); |
| BuildCommandRequestBody( |
| BuildMakeCredentialCommand(json_request, /*claimed_pin=*/nullptr, |
| wrapped_secret(), /*secret=*/std::nullopt), |
| base::BindOnce(&FakeSigningCallback), handshake_hash(), |
| base::BindOnce(&BuildCommandCompletionWaiter::CompletionCallback, |
| base::Unretained(&waiter))); |
| |
| waiter.Wait(); |
| |
| std::optional<cbor::Value> request_cbor = cbor::Reader::Read(waiter.result()); |
| auto decoded_command = |
| ValidateRequestFormatAndReturnCommandList(*request_cbor); |
| auto& command_element = decoded_command->GetArray()[0]; |
| auto& command_map = command_element.GetMap(); |
| EXPECT_EQ(command_map.find(cbor::Value("cmd"))->second.GetString(), |
| "passkeys/create"); |
| EXPECT_TRUE(command_map.find(cbor::Value("claimed_pin")) == |
| command_map.end()); |
| EXPECT_TRUE(command_map.find(cbor::Value("wrapped_pin_data")) == |
| command_map.end()); |
| auto& request_value_map = |
| command_map.find(cbor::Value("request"))->second.GetMap(); |
| EXPECT_EQ(request_value_map.find(cbor::Value("rp")) |
| ->second.GetMap() |
| .find(cbor::Value("id")) |
| ->second.GetString(), |
| "test.example"); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, BuildMakeCredentialRequest_WithPIN) { |
| BuildCommandCompletionWaiter waiter; |
| std::optional<base::Value> parsed_json = base::JSONReader::Read( |
| kMakeCredentialRequestJson, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| EXPECT_TRUE(parsed_json); |
| auto json_request = |
| base::MakeRefCounted<JSONRequest>(std::move(*parsed_json)); |
| const std::vector<uint8_t> pin_claim = {1, 2, 3}; |
| const std::vector<uint8_t> wrapped_pin = {4, 5, 6, 7}; |
| auto claimed_pin = std::make_unique<ClaimedPIN>(pin_claim, wrapped_pin); |
| BuildCommandRequestBody( |
| BuildMakeCredentialCommand(json_request, std::move(claimed_pin), |
| wrapped_secret(), /*secret=*/std::nullopt), |
| base::BindOnce(&FakeSigningCallback), handshake_hash(), |
| base::BindOnce(&BuildCommandCompletionWaiter::CompletionCallback, |
| base::Unretained(&waiter))); |
| |
| waiter.Wait(); |
| |
| std::optional<cbor::Value> request_cbor = cbor::Reader::Read(waiter.result()); |
| auto decoded_command = |
| ValidateRequestFormatAndReturnCommandList(*request_cbor); |
| auto& command_element = decoded_command->GetArray()[0]; |
| auto& command_map = command_element.GetMap(); |
| |
| EXPECT_EQ(command_map.find(cbor::Value("claimed_pin")) |
| ->second.GetBytestring() |
| .size(), |
| 3u); |
| EXPECT_EQ(command_map.find(cbor::Value("wrapped_pin_data")) |
| ->second.GetBytestring() |
| .size(), |
| 4u); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseGetAssertionResponse_Success) { |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes(kGetAssertionHexResponse, &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| std::vector<uint8_t> cred_id = {0, 1, 2}; |
| |
| std::optional<AuthenticatorGetAssertionResponse> response; |
| auto parse_result = |
| ParseGetAssertionResponse(std::move(response_cbor), cred_id); |
| EXPECT_TRUE( |
| std::holds_alternative<AuthenticatorGetAssertionResponse>(parse_result)); |
| const auto& assertion_response = |
| std::get<AuthenticatorGetAssertionResponse>(parse_result); |
| EXPECT_EQ(assertion_response.user_entity->id, |
| std::vector<uint8_t>({'a', 'b'})); |
| EXPECT_EQ(assertion_response.credential->id, std::vector<uint8_t>({0, 1, 2})); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseGetAssertionResponse_Failures) { |
| for (auto& test_case : kFailingGetAssertionResponses) { |
| std::optional<AuthenticatorGetAssertionResponse> response; |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes(test_case.hex_cbor, &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| std::vector<uint8_t> cred_id = {0, 1, 2}; |
| auto parse_result = |
| ParseGetAssertionResponse(std::move(response_cbor), cred_id); |
| EXPECT_TRUE(std::holds_alternative<ErrorResponse>(parse_result) && |
| std::get<ErrorResponse>(parse_result).error_string.has_value()) |
| << "Failed GetAssertion response parsing for: " << test_case.name; |
| } |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseMakeCredentialResponse_Success) { |
| std::vector<uint8_t> response_serialized; |
| CHECK( |
| base::HexStringToBytes(kMakeCredentialHexResponse, &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| CtapMakeCredentialRequest ctap_request( |
| kClientDataJson, PublicKeyCredentialRpEntity(kRpId), |
| PublicKeyCredentialUserEntity(user_id()), |
| PublicKeyCredentialParams( |
| std::vector<PublicKeyCredentialParams::CredentialInfo>())); |
| |
| auto parse_result = ParseMakeCredentialResponse( |
| std::move(response_cbor), ctap_request, kWrappedSecretVersion, |
| UserPresentAndVerifiedBits::kPresentAndVerified); |
| EXPECT_TRUE( |
| (std::holds_alternative<std::pair<AuthenticatorMakeCredentialResponse, |
| sync_pb::WebauthnCredentialSpecifics>>( |
| parse_result))); |
| const auto& entity = |
| std::get<std::pair<AuthenticatorMakeCredentialResponse, |
| sync_pb::WebauthnCredentialSpecifics>>(parse_result) |
| .second; |
| EXPECT_EQ(entity.rp_id(), std::string(kRpId)); |
| EXPECT_EQ(entity.user_id(), std::string(user_id().begin(), user_id().end())); |
| EXPECT_EQ(entity.key_version(), kWrappedSecretVersion); |
| EXPECT_EQ(entity.encrypted(), std::string(encrypted_passkey().begin(), |
| encrypted_passkey().end())); |
| |
| const auto& register_response = |
| std::get<std::pair<AuthenticatorMakeCredentialResponse, |
| sync_pb::WebauthnCredentialSpecifics>>(parse_result) |
| .first; |
| auto response_cred_id = |
| register_response.attestation_object.authenticator_data() |
| .GetCredentialId(); |
| EXPECT_EQ(entity.credential_id(), |
| std::string(response_cred_id.begin(), response_cred_id.end())); |
| EXPECT_TRUE( |
| register_response.transports->contains(FidoTransportProtocol::kInternal)); |
| EXPECT_TRUE( |
| register_response.transports->contains(FidoTransportProtocol::kHybrid)); |
| EXPECT_TRUE(register_response.is_resident_key); |
| EXPECT_TRUE(register_response.attestation_object.authenticator_data() |
| .obtained_user_presence()); |
| EXPECT_TRUE(register_response.attestation_object.authenticator_data() |
| .obtained_user_verification()); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, |
| ParseMakeCredentialResponse_WithoutUserPresence) { |
| std::vector<uint8_t> response_serialized; |
| CHECK( |
| base::HexStringToBytes(kMakeCredentialHexResponse, &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| CtapMakeCredentialRequest ctap_request( |
| kClientDataJson, PublicKeyCredentialRpEntity(kRpId), |
| PublicKeyCredentialUserEntity(user_id()), |
| PublicKeyCredentialParams( |
| std::vector<PublicKeyCredentialParams::CredentialInfo>())); |
| |
| // Passkey upgrade requests yield up=0, uv=0 responses. |
| auto parse_result = ParseMakeCredentialResponse( |
| std::move(response_cbor), ctap_request, kWrappedSecretVersion, |
| UserPresentAndVerifiedBits::kNeither); |
| EXPECT_TRUE( |
| (std::holds_alternative<std::pair<AuthenticatorMakeCredentialResponse, |
| sync_pb::WebauthnCredentialSpecifics>>( |
| parse_result))); |
| const auto& register_response = |
| std::get<std::pair<AuthenticatorMakeCredentialResponse, |
| sync_pb::WebauthnCredentialSpecifics>>(parse_result) |
| .first; |
| EXPECT_FALSE(register_response.attestation_object.authenticator_data() |
| .obtained_user_presence()); |
| EXPECT_FALSE(register_response.attestation_object.authenticator_data() |
| .obtained_user_verification()); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseMakeCredentialResponse_StringFailures) { |
| CtapMakeCredentialRequest ctap_request( |
| kClientDataJson, PublicKeyCredentialRpEntity(kRpId), |
| PublicKeyCredentialUserEntity(user_id()), |
| PublicKeyCredentialParams( |
| std::vector<PublicKeyCredentialParams::CredentialInfo>())); |
| |
| for (auto& test_case : kFailingMakeCredentialResponses) { |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes(test_case.hex_cbor, &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| auto parse_result = ParseMakeCredentialResponse( |
| std::move(response_cbor), ctap_request, kWrappedSecretVersion, |
| UserPresentAndVerifiedBits::kPresentOnly); |
| EXPECT_TRUE(std::holds_alternative<ErrorResponse>(parse_result) && |
| std::get<ErrorResponse>(parse_result).error_string.has_value()) |
| << "Failed MakeCredential response parsing for: " << test_case.name; |
| } |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseGetAssertionResponse_IntegerFailure) { |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes("81A16365727202", &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| std::vector<uint8_t> cred_id = {0, 1, 2}; |
| auto parse_result = |
| ParseGetAssertionResponse(std::move(response_cbor), cred_id); |
| |
| EXPECT_TRUE(std::holds_alternative<ErrorResponse>(parse_result)); |
| EXPECT_TRUE(std::get<ErrorResponse>(parse_result).error_code.has_value()); |
| EXPECT_EQ(*std::get<ErrorResponse>(parse_result).error_code, 2); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseMakeCredentialResponse_IntegerFailure) { |
| CtapMakeCredentialRequest ctap_request( |
| kClientDataJson, PublicKeyCredentialRpEntity(kRpId), |
| PublicKeyCredentialUserEntity(user_id()), |
| PublicKeyCredentialParams( |
| std::vector<PublicKeyCredentialParams::CredentialInfo>())); |
| |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes("81A16365727202", &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| auto parse_result = ParseMakeCredentialResponse( |
| std::move(response_cbor), ctap_request, kWrappedSecretVersion, |
| UserPresentAndVerifiedBits::kPresentOnly); |
| |
| EXPECT_TRUE(std::holds_alternative<ErrorResponse>(parse_result)); |
| EXPECT_TRUE(std::get<ErrorResponse>(parse_result).error_code.has_value()); |
| EXPECT_EQ(*std::get<ErrorResponse>(parse_result).error_code, 2); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseGetAssertionResponse_LargeBlob_Failures) { |
| const LargeBlobFailureCase kCases[] = { |
| // Data present but size absent. |
| {base::BindRepeating([]() { |
| cbor::Value::MapValue map; |
| map.emplace(cbor::Value("largeBlobData"), |
| cbor::Value(std::vector<uint8_t>{1, 2, 3})); |
| return map; |
| }), |
| "Malformed largeBlob: data/size field mismatch in enclave response."}, |
| |
| // Negative size value. |
| {base::BindRepeating([]() { |
| cbor::Value::MapValue map; |
| map.emplace(cbor::Value("largeBlobData"), |
| cbor::Value(std::vector<uint8_t>{1, 2, 3})); |
| map.emplace(cbor::Value("largeBlobSize"), cbor::Value(-1)); |
| return map; |
| }), |
| "Malformed largeBlob: largeBlobSize must be a non-negative int."}, |
| |
| // largeBlobWritten is not a boolean. |
| {base::BindRepeating([]() { |
| cbor::Value::MapValue map; |
| map.emplace(cbor::Value("largeBlobWritten"), cbor::Value("not-bool")); |
| return map; |
| }), |
| "Malformed largeBlob: largeBlobWritten is not a boolean."}, |
| |
| // written == true but encrypted data absent. |
| {base::BindRepeating([]() { |
| cbor::Value::MapValue map; |
| map.emplace(cbor::Value("largeBlobWritten"), cbor::Value(true)); |
| return map; |
| }), |
| "Malformed largeBlob: encrypted blob data missing when " |
| "largeBlobWritten is true."}, |
| }; |
| |
| for (const auto& tc : kCases) { |
| auto result = ParseGetAssertionResponse( |
| MakeGetAssertionResponseWithLargeBlob(tc.build_blob.Run()), |
| std::vector<uint8_t>{0x00}); |
| |
| const auto& err = std::get<ErrorResponse>(result); |
| ASSERT_TRUE(err.error_string); |
| EXPECT_EQ(*err.error_string, tc.expected_error); |
| } |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, ParseGetAssertionResponse_LargeBlob_Success) { |
| cbor::Value::MapValue large_blob_map; |
| large_blob_map.emplace(cbor::Value("largeBlobData"), |
| cbor::Value(std::vector<uint8_t>{1, 2, 3})); |
| large_blob_map.emplace(cbor::Value("largeBlobSize"), cbor::Value(3)); |
| |
| auto result = ParseGetAssertionResponse( |
| MakeGetAssertionResponseWithLargeBlob(std::move(large_blob_map)), |
| std::vector<uint8_t>{0x00}); |
| |
| ASSERT_TRUE( |
| std::holds_alternative<AuthenticatorGetAssertionResponse>(result)); |
| const auto& response = std::get<AuthenticatorGetAssertionResponse>(result); |
| |
| ASSERT_TRUE(response.large_blob_extension); |
| EXPECT_EQ(response.large_blob_extension->original_size, 3u); |
| EXPECT_THAT(response.large_blob_extension->compressed_data, |
| testing::ElementsAre(1, 2, 3)); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, RedactEnclaveRequest) { |
| auto entity = PasskeyEntity(); |
| entity.set_rp_id(kRpId); |
| std::optional<base::Value> parsed_json = base::JSONReader::Read( |
| kGetAssertionRequestJson, base::JSON_PARSE_CHROMIUM_EXTENSIONS); |
| EXPECT_TRUE(parsed_json); |
| auto json_request = |
| base::MakeRefCounted<JSONRequest>(std::move(*parsed_json)); |
| cbor::Value request_cbor = BuildGetAssertionCommand( |
| std::move(entity), json_request, kClientDataJson, |
| /*claimed_pin=*/nullptr, /*wrapped_secret=*/std::nullopt, secret()); |
| cbor::Value redacted = RedactEnclaveRequest(request_cbor); |
| ASSERT_TRUE(redacted.is_map()); |
| const auto& redacted_map = redacted.GetMap(); |
| const auto& redacted_value = |
| redacted_map.find(cbor::Value(kRequestSecretKey))->second; |
| EXPECT_EQ(redacted_value.GetString(), "[redacted]"); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, RedactErroneousEnclaveRequest) { |
| cbor::Value request = cbor::Value("not a valid request"); |
| EXPECT_EQ(cbor::Writer::Write(RedactEnclaveRequest(request)), |
| cbor::Writer::Write(request)); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, RedactEnclaveResponse) { |
| std::vector<uint8_t> response_serialized; |
| CHECK(base::HexStringToBytes(kCompleteGetAssertionHexResponse, |
| &response_serialized)); |
| cbor::Value response_cbor = cbor::Reader::Read(response_serialized).value(); |
| |
| const cbor::Value redacted = RedactEnclaveResponse(response_cbor); |
| const cbor::Value::MapValue& redacted_outer_map = |
| redacted.GetMap().find(cbor::Value("ok"))->second.GetArray()[0].GetMap(); |
| const cbor::Value::MapValue& redacted_map = |
| redacted_outer_map.find(cbor::Value("ok"))->second.GetMap(); |
| const auto& large_blob_value = |
| redacted_map.find(cbor::Value("largeBlob"))->second; |
| EXPECT_EQ(large_blob_value.GetString(), "[redacted]"); |
| const auto& prf_value = redacted_map.find(cbor::Value("prf"))->second; |
| EXPECT_EQ(prf_value.GetString(), "[redacted]"); |
| } |
| |
| TEST_F(EnclaveProtocolUtilsTest, RedactErroneousEnclaveResponse) { |
| cbor::Value response = cbor::Value("not a valid response"); |
| EXPECT_EQ(cbor::Writer::Write(RedactEnclaveResponse(response)), |
| cbor::Writer::Write(response)); |
| } |
| |
| } // namespace enclave |
| } // namespace device |