Commit 31caeef4 authored by Adam Langley's avatar Adam Langley Committed by Commit Bot

VirtualU2fDevice: add ability to create attestation certificates.

This change allows VirtualU2fDevice to create custom attestation
certificates at run-time. It also adds tests for attestation behaviour
that depend on that ability.

Change-Id: I88323de6baaba7883ab0db676c26ccdb4b0dafbb
Reviewed-on: https://chromium-review.googlesource.com/967066Reviewed-by: default avatarBalazs Engedy <engedy@chromium.org>
Commit-Queue: Adam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/master@{#544970}
parent 2e9117b5
......@@ -16,6 +16,8 @@
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "components/cbor/cbor_reader.h"
#include "components/cbor/cbor_values.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/test_service_manager_context.h"
#include "content/test/test_render_frame_host.h"
......@@ -30,6 +32,9 @@ namespace content {
using ::testing::_;
using cbor::CBORValue;
using cbor::CBORReader;
using webauth::mojom::AttestationConveyancePreference;
using webauth::mojom::AuthenticatorPtr;
using webauth::mojom::AuthenticatorSelectionCriteria;
using webauth::mojom::AuthenticatorSelectionCriteriaPtr;
......@@ -253,6 +258,11 @@ class TestMakeCredentialCallback {
AuthenticatorStatus GetResponseStatus() { return response_.first; }
const MakeCredentialAuthenticatorResponsePtr& response() const {
CHECK_EQ(AuthenticatorStatus::SUCCESS, response_.first);
return response_.second;
}
private:
std::pair<AuthenticatorStatus, MakeCredentialAuthenticatorResponsePtr>
response_;
......@@ -665,4 +675,233 @@ TEST_F(AuthenticatorImplTest, TestGetAssertionTimeout) {
cb.WaitForCallback();
EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, cb.GetResponseStatus());
}
namespace {
enum class IndividualAttestation {
REQUESTED,
NOT_REQUESTED,
};
enum class AttestationConsent {
GRANTED,
DENIED,
};
// Implements ContentBrowserClient and allows webauthn-related calls to be
// mocked.
class AuthenticatorTestContentBrowserClient : public ContentBrowserClient {
public:
bool ShouldPermitIndividualAttestationForWebauthnRPID(
content::BrowserContext* browser_context,
const std::string& rp_id) override {
return individual_attestation == IndividualAttestation::REQUESTED;
}
void ShouldReturnAttestationForWebauthnRPID(
content::RenderFrameHost* rfh,
const std::string& rp_id,
const url::Origin& origin,
base::OnceCallback<void(bool)> callback) override {
std::move(callback).Run(attestation_consent == AttestationConsent::GRANTED);
}
IndividualAttestation individual_attestation =
IndividualAttestation::NOT_REQUESTED;
AttestationConsent attestation_consent = AttestationConsent::DENIED;
};
// A test class that installs and removes an
// |AuthenticatorTestContentBrowserClient| automatically.
class AuthenticatorContentBrowserClientTest : public AuthenticatorImplTest {
public:
AuthenticatorContentBrowserClientTest() = default;
void SetUp() override {
AuthenticatorImplTest::SetUp();
old_client_ = SetBrowserClientForTesting(&test_client_);
}
void TearDown() override {
SetBrowserClientForTesting(old_client_);
AuthenticatorImplTest::TearDown();
}
protected:
AuthenticatorTestContentBrowserClient test_client_;
private:
ContentBrowserClient* old_client_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(AuthenticatorContentBrowserClientTest);
};
const char* AttestationConveyancePreferenceToString(
AttestationConveyancePreference v) {
switch (v) {
case AttestationConveyancePreference::NONE:
return "none";
case AttestationConveyancePreference::INDIRECT:
return "indirect";
case AttestationConveyancePreference::DIRECT:
return "direct";
default:
NOTREACHED();
return "";
}
}
// Expects that |map| contains the given key with a string-value equal to
// |expected|.
void ExpectMapHasKeyWithStringValue(const CBORValue::MapValue& map,
const char* key,
const char* expected) {
const auto it = map.find(CBORValue(key));
ASSERT_TRUE(it != map.end()) << "No such key '" << key << "'";
const auto& value = it->second;
EXPECT_TRUE(value.is_string())
<< "Value of '" << key << "' has type " << static_cast<int>(value.type())
<< ", but expected to find a string";
EXPECT_EQ(std::string(expected), value.GetString())
<< "Value of '" << key << "' is '" << value.GetString()
<< "', but expected to find '" << expected << "'";
}
// Asserts that the webauthn attestation CBOR map in |attestation| contains a
// single X.509 certificate containing |substring|.
void ExpectCertificateContainingSubstring(
const CBORValue::MapValue& attestation,
const std::string& substring) {
const auto& attestation_statement_it = attestation.find(CBORValue("attStmt"));
ASSERT_TRUE(attestation_statement_it != attestation.end());
ASSERT_TRUE(attestation_statement_it->second.is_map());
const auto& attestation_statement = attestation_statement_it->second.GetMap();
const auto& x5c_it = attestation_statement.find(CBORValue("x5c"));
ASSERT_TRUE(x5c_it != attestation_statement.end());
ASSERT_TRUE(x5c_it->second.is_array());
const auto& x5c = x5c_it->second.GetArray();
ASSERT_EQ(1u, x5c.size());
ASSERT_TRUE(x5c[0].is_bytestring());
base::StringPiece cert = x5c[0].GetBytestringAsString();
EXPECT_TRUE(cert.find(substring) != cert.npos);
}
} // anonymous namespace
TEST_F(AuthenticatorContentBrowserClientTest, AttestationBehaviour) {
const char kStandardCommonName[] = "U2F Attestation";
const char kIndividualCommonName[] = "Individual Cert";
const struct {
AttestationConveyancePreference attestation_requested;
IndividualAttestation individual_attestation;
AttestationConsent attestation_consent;
AuthenticatorStatus expected_status;
const char* expected_attestation_format;
const char* expected_certificate_substring;
} kTests[] = {
{
AttestationConveyancePreference::NONE,
IndividualAttestation::NOT_REQUESTED, AttestationConsent::DENIED,
AuthenticatorStatus::SUCCESS, "none", "",
},
{
AttestationConveyancePreference::NONE,
IndividualAttestation::REQUESTED, AttestationConsent::DENIED,
AuthenticatorStatus::SUCCESS, "none", "",
},
{
AttestationConveyancePreference::INDIRECT,
IndividualAttestation::NOT_REQUESTED, AttestationConsent::DENIED,
AuthenticatorStatus::NOT_ALLOWED_ERROR, "", "",
},
{
AttestationConveyancePreference::INDIRECT,
IndividualAttestation::REQUESTED, AttestationConsent::DENIED,
AuthenticatorStatus::NOT_ALLOWED_ERROR, "", "",
},
{
AttestationConveyancePreference::INDIRECT,
IndividualAttestation::NOT_REQUESTED, AttestationConsent::GRANTED,
AuthenticatorStatus::SUCCESS, "fido-u2f", kStandardCommonName,
},
{
AttestationConveyancePreference::INDIRECT,
IndividualAttestation::REQUESTED, AttestationConsent::GRANTED,
AuthenticatorStatus::SUCCESS, "fido-u2f", kIndividualCommonName,
},
{
AttestationConveyancePreference::DIRECT,
IndividualAttestation::NOT_REQUESTED, AttestationConsent::DENIED,
AuthenticatorStatus::NOT_ALLOWED_ERROR, "", "",
},
{
AttestationConveyancePreference::DIRECT,
IndividualAttestation::REQUESTED, AttestationConsent::DENIED,
AuthenticatorStatus::NOT_ALLOWED_ERROR, "", "",
},
{
AttestationConveyancePreference::DIRECT,
IndividualAttestation::NOT_REQUESTED, AttestationConsent::GRANTED,
AuthenticatorStatus::SUCCESS, "fido-u2f", kStandardCommonName,
},
{
AttestationConveyancePreference::DIRECT,
IndividualAttestation::REQUESTED, AttestationConsent::GRANTED,
AuthenticatorStatus::SUCCESS, "fido-u2f", kIndividualCommonName,
},
};
TestServiceManagerContext smc;
device::test::ScopedVirtualU2fDevice virtual_device;
virtual_device.mutable_state()->attestation_cert_common_name =
kStandardCommonName;
virtual_device.mutable_state()->individual_attestation_cert_common_name =
kIndividualCommonName;
NavigateAndCommit(GURL("https://example.com"));
AuthenticatorPtr authenticator = ConnectToAuthenticator();
for (size_t i = 0; i < arraysize(kTests); i++) {
const auto& test = kTests[i];
SCOPED_TRACE(test.attestation_consent == AttestationConsent::GRANTED
? "consent granted"
: "consent denied");
SCOPED_TRACE(test.individual_attestation == IndividualAttestation::REQUESTED
? "individual attestation"
: "no individual attestation");
SCOPED_TRACE(
AttestationConveyancePreferenceToString(test.attestation_requested));
test_client_.individual_attestation = test.individual_attestation;
test_client_.attestation_consent = test.attestation_consent;
PublicKeyCredentialCreationOptionsPtr options =
GetTestPublicKeyCredentialCreationOptions();
options->relying_party->id = "example.com";
options->adjusted_timeout = base::TimeDelta::FromSeconds(1);
options->attestation = test.attestation_requested;
TestMakeCredentialCallback cb;
authenticator->MakeCredential(std::move(options), cb.callback());
cb.WaitForCallback();
ASSERT_EQ(test.expected_status, cb.GetResponseStatus());
if (test.expected_status != AuthenticatorStatus::SUCCESS) {
ASSERT_STREQ("", test.expected_attestation_format);
continue;
}
base::Optional<CBORValue> attestation_value =
CBORReader::Read(cb.response()->attestation_object);
ASSERT_TRUE(attestation_value);
ASSERT_TRUE(attestation_value->is_map());
const auto& attestation = attestation_value->GetMap();
ExpectMapHasKeyWithStringValue(attestation, "fmt",
test.expected_attestation_format);
if (strlen(test.expected_certificate_substring) > 0) {
ExpectCertificateContainingSubstring(attestation,
test.expected_certificate_substring);
}
}
}
} // namespace content
......@@ -3,5 +3,6 @@ include_rules = [
"+components/cbor",
"+crypto",
"+net/base",
"+net/cert",
"+third_party/boringssl/src/include",
]
......@@ -43,6 +43,10 @@ ScopedVirtualU2fDevice::ScopedVirtualU2fDevice()
: state_(new VirtualU2fDevice::State) {}
ScopedVirtualU2fDevice::~ScopedVirtualU2fDevice() = default;
VirtualU2fDevice::State* ScopedVirtualU2fDevice::mutable_state() {
return state_.get();
}
std::unique_ptr<FidoDiscovery> ScopedVirtualU2fDevice::CreateFidoDiscovery(
U2fTransportProtocol transport,
::service_manager::Connector* connector) {
......
......@@ -23,6 +23,8 @@ class ScopedVirtualU2fDevice
ScopedVirtualU2fDevice();
~ScopedVirtualU2fDevice() override;
VirtualU2fDevice::State* mutable_state();
protected:
std::unique_ptr<FidoDiscovery> CreateFidoDiscovery(
U2fTransportProtocol transport,
......
......@@ -8,14 +8,17 @@
#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/apdu/apdu_command.h"
#include "components/apdu/apdu_response.h"
#include "crypto/ec_private_key.h"
#include "crypto/ec_signature_creator.h"
#include "crypto/sha2.h"
#include "device/fido/fido_constants.h"
#include "net/cert/x509_util.h"
namespace device {
......@@ -44,7 +47,11 @@ struct VirtualU2fDevice::State::Internal {
std::map<std::vector<uint8_t>, RegistrationData> registrations;
};
VirtualU2fDevice::State::State() : internal_(new Internal) {}
VirtualU2fDevice::State::State()
: attestation_cert_common_name("Batch Certificate"),
individual_attestation_cert_common_name("Individual Certificate"),
internal_(new Internal) {}
VirtualU2fDevice::State::~State() = default;
namespace {
......@@ -71,36 +78,6 @@ constexpr uint8_t kAttestationKey[]{
0xd7, 0x86, 0x2f, 0x23, 0xab, 0xaf, 0x02, 0x03, 0xb4, 0xb8, 0x91, 0x1b,
0xa0, 0x56, 0x99, 0x94, 0xe1, 0x01};
// The corresponding example attestation certificate from the spec.
constexpr uint8_t kAttestationCert[]{
0x30, 0x82, 0x01, 0x3c, 0x30, 0x81, 0xe4, 0xa0, 0x03, 0x02, 0x01, 0x02,
0x02, 0x0a, 0x47, 0x90, 0x12, 0x80, 0x00, 0x11, 0x55, 0x95, 0x73, 0x52,
0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02,
0x30, 0x17, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13,
0x0c, 0x47, 0x6e, 0x75, 0x62, 0x62, 0x79, 0x20, 0x50, 0x69, 0x6c, 0x6f,
0x74, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x32, 0x30, 0x38, 0x31, 0x34, 0x31,
0x38, 0x32, 0x39, 0x33, 0x32, 0x5a, 0x17, 0x0d, 0x31, 0x33, 0x30, 0x38,
0x31, 0x34, 0x31, 0x38, 0x32, 0x39, 0x33, 0x32, 0x5a, 0x30, 0x31, 0x31,
0x2f, 0x30, 0x2d, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x26, 0x50, 0x69,
0x6c, 0x6f, 0x74, 0x47, 0x6e, 0x75, 0x62, 0x62, 0x79, 0x2d, 0x30, 0x2e,
0x34, 0x2e, 0x31, 0x2d, 0x34, 0x37, 0x39, 0x30, 0x31, 0x32, 0x38, 0x30,
0x30, 0x30, 0x31, 0x31, 0x35, 0x35, 0x39, 0x35, 0x37, 0x33, 0x35, 0x32,
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
0x42, 0x00, 0x04, 0x8d, 0x61, 0x7e, 0x65, 0xc9, 0x50, 0x8e, 0x64, 0xbc,
0xc5, 0x67, 0x3a, 0xc8, 0x2a, 0x67, 0x99, 0xda, 0x3c, 0x14, 0x46, 0x68,
0x2c, 0x25, 0x8c, 0x46, 0x3f, 0xff, 0xdf, 0x58, 0xdf, 0xd2, 0xfa, 0x3e,
0x6c, 0x37, 0x8b, 0x53, 0xd7, 0x95, 0xc4, 0xa4, 0xdf, 0xfb, 0x41, 0x99,
0xed, 0xd7, 0x86, 0x2f, 0x23, 0xab, 0xaf, 0x02, 0x03, 0xb4, 0xb8, 0x91,
0x1b, 0xa0, 0x56, 0x99, 0x94, 0xe1, 0x01, 0x30, 0x0a, 0x06, 0x08, 0x2a,
0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x47, 0x00, 0x30, 0x44,
0x02, 0x20, 0x60, 0xcd, 0xb6, 0x06, 0x1e, 0x9c, 0x22, 0x26, 0x2d, 0x1a,
0xac, 0x1d, 0x96, 0xd8, 0xc7, 0x08, 0x29, 0xb2, 0x36, 0x65, 0x31, 0xdd,
0xa2, 0x68, 0x83, 0x2c, 0xb8, 0x36, 0xbc, 0xd3, 0x0d, 0xfa, 0x02, 0x20,
0x63, 0x1b, 0x14, 0x59, 0xf0, 0x9e, 0x63, 0x30, 0x05, 0x57, 0x22, 0xc8,
0xd8, 0x9b, 0x7f, 0x48, 0x88, 0x3b, 0x90, 0x89, 0xb8, 0x8d, 0x60, 0xd1,
0xd9, 0x79, 0x59, 0x02, 0xb3, 0x04, 0x10, 0xdf};
std::vector<uint8_t> GetAttestationKey() {
return std::vector<uint8_t>(std::begin(kAttestationKey),
std::end(kAttestationKey));
......@@ -187,9 +164,9 @@ base::Optional<std::vector<uint8_t>> VirtualU2fDevice::DoRegister(
return ErrorStatus(apdu::ApduResponse::Status::SW_WRONG_LENGTH);
}
// For now we ignore P1 here. Spec says it should always be 0, and TUP is
// implied for registration. However Chrome does send TUP (0x03) and sometimes
// adds in the propietary request for an individual attestation cert.
const bool individual_attestation_requested = p1 & kP1IndividualAttestation;
// The spec says that the other bits of P1 should be zero. However, Chrome
// sends Test User Presence (0x03) so we ignore those bits.
auto challenge_param = data.first(32);
auto application_parameter = data.last(32);
......@@ -230,15 +207,28 @@ base::Optional<std::vector<uint8_t>> VirtualU2fDevice::DoRegister(
status = signer->Sign(sign_buffer.data(), sign_buffer.size(), &sig);
DCHECK(status);
constexpr uint32_t kAttestationCertSerialNumber = 1;
std::string attestation_cert;
if (!net::x509_util::CreateSelfSignedCert(
attestation_private_key->key(), net::x509_util::DIGEST_SHA256,
"CN=" + (individual_attestation_requested
? state_->individual_attestation_cert_common_name
: state_->attestation_cert_common_name),
kAttestationCertSerialNumber, base::Time::FromTimeT(1500000000),
base::Time::FromTimeT(1500000000), &attestation_cert)) {
DVLOG(2) << "Failed to create attestation certificate";
return ErrorStatus(apdu::ApduResponse::Status::SW_INS_NOT_SUPPORTED);
}
// U2F response data.
std::vector<uint8_t> response;
response.reserve(1 + public_key.size() + 1 + key_handle.size() +
sizeof(kAttestationCert) + sig.size());
attestation_cert.size() + sig.size());
response.push_back(kU2fRegistrationResponseHeader);
AppendTo(&response, public_key);
response.push_back(key_handle.size());
AppendTo(&response, key_handle);
AppendTo(&response, kAttestationCert);
AppendTo(&response, attestation_cert);
AppendTo(&response, sig);
// Store the registration.
......
......@@ -34,6 +34,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualU2fDevice : public FidoDevice {
public:
State();
// The common name in the attestation certificate.
std::string attestation_cert_common_name;
// The common name in the attestation certificate if individual attestation
// is requested.
std::string individual_attestation_cert_common_name;
private:
friend class ::device::VirtualU2fDevice;
friend class base::RefCounted<State>;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment