Commit 7aeebf41 authored by Nina Satragno's avatar Nina Satragno Committed by Commit Bot

[webauthn] Resident support for AddCredential command

Add support for injecting resident credentials via the DevTools API. This was
missed in the original implementation. Resident Credentials need to store the
Relying Party ID and User Handle, so these two parameters are added. The
Relying Party ID hash is made redundant and therefore removed.

This is one in a series of patches intended to create a Testing API for
WebAuthn, for use in Web Platform Tests and by external webauthn tests.

For an overview of overall design, please see
https://docs.google.com/document/d/1bp2cMgjm2HSpvL9-WsJoIQMsBi1oKGQY6CvWD-9WmIQ/edit?usp=sharing

Bug: 922572
Change-Id: Ifee5edb0150593238e425fc3b2963ed04dda1609
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1728423
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Auto-Submit: Nina Satragno <nsatragno@chromium.org>
Reviewed-by: default avatarKen Buchanan <kenrb@chromium.org>
Reviewed-by: default avatarAndrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#683281}
parent 9740f4ef
include_rules = [
# For parsing and validating fido enums in protocol/webauthn_handler.cc.
"+device/fido/fido_constants.h",
"+device/fido/fido_parsing_utils.h",
"+device/fido/fido_transport_protocol.h",
# For VirtualFidoDevice::RegistrationData used in protocol/webauthn_handler.cc.
"+device/fido/virtual_fido_device.h",
......
......@@ -5,6 +5,8 @@
#include "content/browser/devtools/protocol/webauthn_handler.h"
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "base/strings/string_number_conversions.h"
......@@ -14,6 +16,7 @@
#include "content/browser/webauth/virtual_authenticator.h"
#include "content/browser/webauth/virtual_fido_discovery_factory.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/virtual_fido_device.h"
#include "device/fido/virtual_u2f_device.h"
......@@ -32,10 +35,16 @@ static constexpr char kDevToolsNotAttached[] =
"The DevTools session is not attached to a frame";
static constexpr char kErrorCreatingAuthenticator[] =
"An error occurred when trying to create the authenticator";
static constexpr char kHandleRequiredForResidentCredential[] =
"The User Handle is required for Resident Credentials";
static constexpr char kInvalidProtocol[] = "The protocol is not valid";
static constexpr char kInvalidRpIdHash[] =
"The Relying Party ID hash must have a size of ";
static constexpr char kInvalidTransport[] = "The transport is not valid";
static constexpr char kInvalidUserHandle[] =
"The User Handle must have a maximum size of ";
static constexpr char kResidentCredentialNotSupported[] =
"The Authenticator does not support Resident Credentials.";
static constexpr char kRpIdRequired[] =
"The Relying Party ID is a required parameter";
static constexpr char kVirtualEnvironmentNotEnabled[] =
"The Virtual Authenticator Environment has not been enabled for this "
"session";
......@@ -147,19 +156,42 @@ Response WebAuthnHandler::AddCredential(
if (!response.isSuccess())
return response;
if (credential->GetRpIdHash().size() != device::kRpIdHashLength) {
Binary user_handle = credential->GetUserHandle(Binary());
if (credential->HasUserHandle() &&
user_handle.size() > device::kUserHandleMaxLength) {
return Response::InvalidParams(
kInvalidRpIdHash + base::NumberToString(device::kRpIdHashLength));
kInvalidUserHandle +
base::NumberToString(device::kUserHandleMaxLength));
}
if (!authenticator->AddRegistration(
CopyBinaryToVector(credential->GetCredentialId()),
CopyBinaryToVector(credential->GetRpIdHash()),
CopyBinaryToVector(credential->GetPrivateKey()),
credential->GetSignCount())) {
return Response::Error(kCouldNotCreateCredential);
if (!credential->HasRpId())
return Response::InvalidParams(kRpIdRequired);
std::string rp_id = credential->GetRpId("");
bool credential_created;
if (credential->GetIsResidentCredential()) {
if (!authenticator->has_resident_key())
return Response::InvalidParams(kResidentCredentialNotSupported);
if (!credential->HasUserHandle())
return Response::InvalidParams(kHandleRequiredForResidentCredential);
credential_created = authenticator->AddResidentRegistration(
CopyBinaryToVector(credential->GetCredentialId()), rp_id,
CopyBinaryToVector(credential->GetPrivateKey()),
credential->GetSignCount(), CopyBinaryToVector(user_handle));
} else {
credential_created = authenticator->AddRegistration(
CopyBinaryToVector(credential->GetCredentialId()),
base::make_span<uint8_t, device::kRpIdHashLength>(
device::fido_parsing_utils::CreateSHA256Hash(rp_id)),
CopyBinaryToVector(credential->GetPrivateKey()),
credential->GetSignCount());
}
if (!credential_created)
return Response::Error(kCouldNotCreateCredential);
return Response::OK();
}
......@@ -172,19 +204,25 @@ Response WebAuthnHandler::GetCredentials(
return response;
*out_credentials = std::make_unique<Array<WebAuthn::Credential>>();
for (const auto& credential : authenticator->registrations()) {
const auto& rp_id_hash = credential.second.application_parameter;
for (const auto& registration : authenticator->registrations()) {
std::vector<uint8_t> private_key;
credential.second.private_key->ExportPrivateKey(&private_key);
(*out_credentials)
->emplace_back(
WebAuthn::Credential::Create()
.SetCredentialId(Binary::fromVector(credential.first))
.SetRpIdHash(
Binary::fromSpan(rp_id_hash.data(), rp_id_hash.size()))
.SetPrivateKey(Binary::fromVector(std::move(private_key)))
.SetSignCount(credential.second.counter)
.Build());
registration.second.private_key->ExportPrivateKey(&private_key);
auto credential =
WebAuthn::Credential::Create()
.SetCredentialId(Binary::fromVector(registration.first))
.SetPrivateKey(Binary::fromVector(std::move(private_key)))
.SetSignCount(registration.second.counter)
.SetIsResidentCredential(registration.second.is_resident)
.Build();
if (registration.second.rp)
credential->SetRpId(registration.second.rp->id);
if (registration.second.user) {
credential->SetUserHandle(
Binary::fromVector(registration.second.user->id));
}
(*out_credentials)->emplace_back(std::move(credential));
}
return Response::OK();
}
......
......@@ -10,6 +10,8 @@
#include "base/containers/span.h"
#include "base/guid.h"
#include "crypto/ec_private_key.h"
#include "device/fido/public_key_credential_rp_entity.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_u2f_device.h"
......@@ -43,26 +45,39 @@ void VirtualAuthenticator::AddBinding(
bool VirtualAuthenticator::AddRegistration(
std::vector<uint8_t> key_handle,
const std::vector<uint8_t>& rp_id_hash,
base::span<const uint8_t, device::kRpIdHashLength> rp_id_hash,
const std::vector<uint8_t>& private_key,
int32_t counter) {
if (rp_id_hash.size() != device::kRpIdHashLength)
return false;
auto ec_private_key =
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(private_key);
if (!ec_private_key)
return false;
return state_->registrations
.emplace(
std::move(key_handle),
::device::VirtualFidoDevice::RegistrationData(
std::move(ec_private_key),
base::make_span<device::kRpIdHashLength>(rp_id_hash), counter))
.emplace(std::move(key_handle),
::device::VirtualFidoDevice::RegistrationData(
std::move(ec_private_key), std::move(rp_id_hash), counter))
.second;
}
bool VirtualAuthenticator::AddResidentRegistration(
std::vector<uint8_t> key_handle,
std::string rp_id,
const std::vector<uint8_t>& private_key,
int32_t counter,
std::vector<uint8_t> user_handle) {
auto ec_private_key =
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(private_key);
if (!ec_private_key)
return false;
return state_->InjectResidentKey(
std::move(key_handle),
device::PublicKeyCredentialRpEntity(std::move(rp_id)),
device::PublicKeyCredentialUserEntity(std::move(user_handle)), counter,
std::move(ec_private_key));
}
void VirtualAuthenticator::ClearRegistrations() {
state_->registrations.clear();
}
......@@ -118,9 +133,16 @@ void VirtualAuthenticator::GetRegistrations(GetRegistrationsCallback callback) {
void VirtualAuthenticator::AddRegistration(
blink::test::mojom::RegisteredKeyPtr registration,
AddRegistrationCallback callback) {
std::move(callback).Run(AddRegistration(
std::move(registration->key_handle), registration->application_parameter,
registration->private_key, registration->counter));
if (registration->application_parameter.size() != device::kRpIdHashLength) {
std::move(callback).Run(false);
return;
}
std::move(callback).Run(
AddRegistration(std::move(registration->key_handle),
base::make_span<device::kRpIdHashLength>(
registration->application_parameter),
registration->private_key, registration->counter));
}
void VirtualAuthenticator::ClearRegistrations(
......
......@@ -43,10 +43,19 @@ class CONTENT_EXPORT VirtualAuthenticator
// Register a new credential. Returns true if the registration was successful,
// false otherwise.
bool AddRegistration(std::vector<uint8_t> key_handle,
const std::vector<uint8_t>& rp_id_hash,
const std::vector<uint8_t>& private_key,
int32_t counter);
bool AddRegistration(
std::vector<uint8_t> key_handle,
base::span<const uint8_t, device::kRpIdHashLength> rp_id_hash,
const std::vector<uint8_t>& private_key,
int32_t counter);
// Register a new resident credential. Returns true if the registration was
// successful, false otherwise.
bool AddResidentRegistration(std::vector<uint8_t> key_handle,
std::string rp_id,
const std::vector<uint8_t>& private_key,
int32_t counter,
std::vector<uint8_t> user_handle);
// Removes all the credentials.
void ClearRegistrations();
......@@ -61,6 +70,8 @@ class CONTENT_EXPORT VirtualAuthenticator
is_user_verified_ = is_user_verified;
}
bool has_resident_key() const { return has_resident_key_; }
::device::FidoTransportProtocol transport() const {
return state_->transport;
}
......
......@@ -69,6 +69,10 @@ constexpr size_t kClientDataHashLength = 32;
// https://www.w3.org/TR/webauthn/#sec-authenticator-data
constexpr size_t kRpIdHashLength = 32;
// Max length for the user handle:
// https://www.w3.org/TR/webauthn/#user-handle
constexpr size_t kUserHandleMaxLength = 64;
static_assert(kU2fApplicationParamLength == kRpIdHashLength,
"kU2fApplicationParamLength must be equal to kRpIdHashLength.");
......
......@@ -83,7 +83,9 @@ bool VirtualFidoDevice::State::InjectRegistration(
bool VirtualFidoDevice::State::InjectResidentKey(
base::span<const uint8_t> credential_id,
device::PublicKeyCredentialRpEntity rp,
device::PublicKeyCredentialUserEntity user) {
device::PublicKeyCredentialUserEntity user,
int32_t signature_counter,
std::unique_ptr<crypto::ECPrivateKey> private_key) {
auto application_parameter = fido_parsing_utils::CreateSHA256Hash(rp.id);
// Cannot create a duplicate credential for the same (RP ID, user ID) pair.
......@@ -95,12 +97,9 @@ bool VirtualFidoDevice::State::InjectResidentKey(
}
}
auto private_key = crypto::ECPrivateKey::Create();
DCHECK(private_key);
RegistrationData registration(std::move(private_key),
std::move(application_parameter),
0 /* signature counter */);
signature_counter);
registration.is_resident = true;
registration.rp = std::move(rp);
registration.user = std::move(user);
......@@ -111,6 +110,17 @@ bool VirtualFidoDevice::State::InjectResidentKey(
return was_inserted;
}
bool VirtualFidoDevice::State::InjectResidentKey(
base::span<const uint8_t> credential_id,
device::PublicKeyCredentialRpEntity rp,
device::PublicKeyCredentialUserEntity user) {
auto private_key = crypto::ECPrivateKey::Create();
DCHECK(private_key);
return InjectResidentKey(std::move(credential_id), std::move(rp),
std::move(user), /*signature_counter=*/0,
std::move(private_key));
}
bool VirtualFidoDevice::State::InjectResidentKey(
base::span<const uint8_t> credential_id,
const std::string& relying_party_id,
......
......@@ -157,7 +157,18 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualFidoDevice : public FidoDevice {
bool InjectRegistration(base::span<const uint8_t> credential_id,
const std::string& relying_party_id);
// InjectResidentKey adds a resident credential with the specified values.
// Adds a resident credential with the specified values.
// Returns false if there already exists a resident credential for the same
// (RP ID, user ID) pair, or for the same credential ID. Otherwise returns
// true.
bool InjectResidentKey(base::span<const uint8_t> credential_id,
device::PublicKeyCredentialRpEntity rp,
device::PublicKeyCredentialUserEntity user,
int32_t signature_counter,
std::unique_ptr<crypto::ECPrivateKey> private_key);
// Adds a resident credential with the specified values, creating a new
// private key.
// Returns false if there already exists a resident credential for the same
// (RP ID, user ID) pair, or for the same credential ID. Otherwise returns
// true.
......
......@@ -7109,12 +7109,15 @@ experimental domain WebAuthn
type Credential extends object
properties
binary credentialId
# SHA-256 hash of the Relying Party ID the credential is scoped to. Must
# be 32 bytes long.
# See https://w3c.github.io/webauthn/#rpidhash
binary rpIdHash
# The private key in PKCS#8 format.
boolean isResidentCredential
# Relying Party ID the credential is scoped to. Must be set when adding a
# credential.
optional string rpId
# The ECDSA P-256 private key in PKCS#8 format.
binary privateKey
# An opaque byte sequence with a maximum size of 64 bytes mapping the
# credential to a specific user.
optional binary userHandle
# Signature counter. This is incremented by one for each successful
# assertion.
# See https://w3c.github.io/webauthn/#signature-counter
......
......@@ -18,7 +18,31 @@ Check that the WebAuthn command addCredential validates parameters
{
error : {
code : -32602
message : The Relying Party ID hash must have a size of 32
message : The Relying Party ID is a required parameter
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : The Authenticator does not support Resident Credentials.
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : The User Handle is required for Resident Credentials
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : The User Handle must have a maximum size of 64
}
id : <number>
sessionId : <string>
......
......@@ -10,8 +10,8 @@
credential: {
credentialId: btoa(credentialId),
privateKey: btoa("invalid private key"),
rpIdHash: btoa("invalid hash"),
signCount: 0,
isResidentCredential: true,
}
};
......@@ -22,7 +22,7 @@
await dp.WebAuthn.enable();
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
// Try with an invalid RP ID hash.
// Try without an RP ID.
credentialOptions.authenticatorId = (await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
......@@ -33,9 +33,31 @@
})).result.authenticatorId;
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
// Try registering a resident credential on an authenticator not capable of
// resident credentials.
credentialOptions.credential.rpId = "devtools.test";
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
// Try registering a resident credential without a user handle.
credentialOptions.authenticatorId = (await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: false,
},
})).result.authenticatorId;
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
// Try a user handle that exceeds the max size.
const MAX_USER_HANDLE_SIZE = 64;
const longHandle = "a".repeat(MAX_USER_HANDLE_SIZE + 1);
credentialOptions.credential.userHandle = btoa(longHandle);
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
// Try with a private key that is not valid.
credentialOptions.credential.rpIdHash =
await session.evaluateAsync("generateRpIdHash()");
credentialOptions.credential.userHandle = btoa("nina");
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
testRunner.completeTest();
})
......@@ -10,4 +10,15 @@ Check that the WebAuthn command addCredential works
}
status : OK
}
{
id : <number>
result : {
}
sessionId : <string>
}
{
attestation : {
}
status : OK
}
......@@ -10,28 +10,50 @@
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: false,
hasResidentKey: true,
hasUserVerification: false,
},
})).result.authenticatorId;
// Register a credential.
const credentialId = "cred-1";
const addCredentialResult = (await dp.WebAuthn.addCredential({
// Register a non-resident credential.
const nonResidentCredentialId = "cred-1";
testRunner.log(await dp.WebAuthn.addCredential({
authenticatorId,
credential: {
credentialId: btoa(credentialId),
rpIdHash: await session.evaluateAsync("generateRpIdHash()"),
credentialId: btoa(nonResidentCredentialId),
rpId: "devtools.test",
privateKey: await session.evaluateAsync("generateBase64Key()"),
signCount: 0,
isResidentCredential: false,
}
}));
testRunner.log(addCredentialResult);
// Authenticate with the registered credential.
// Authenticate with the non-resident credential.
testRunner.log(await session.evaluateAsync(`getCredential({
type: "public-key",
id: new TextEncoder().encode("${credentialId}"),
id: new TextEncoder().encode("${nonResidentCredentialId}"),
transports: ["usb", "ble", "nfc"],
})`));
// Register a resident credential.
const userHandle = "nina";
const residentCredentialId = "cred-2";
testRunner.log(await dp.WebAuthn.addCredential({
authenticatorId,
credential: {
credentialId: btoa(residentCredentialId),
rpId: "devtools.test",
privateKey: await session.evaluateAsync("generateBase64Key()"),
signCount: 0,
isResidentCredential: true,
userHandle: btoa(userHandle),
}
}));
// Authenticate with the resident credential.
testRunner.log(await session.evaluateAsync(`getCredential({
type: "public-key",
id: new TextEncoder().encode("${residentCredentialId}"),
transports: ["usb", "ble", "nfc"],
})`));
......
......@@ -8,11 +8,22 @@ Check that the WebAuthn command getCredentials works
sessionId : <string>
}
OK
OK
RP ID hash matches expected value
1
RP ID hash matches expected value
1
{
id : <number>
result : {
}
sessionId : <string>
}
Resident Credential:
isResidentCredential: true
signCount: 1
rpId: devtools.test
userHandle: nina
Non-Resident Credential:
isResidentCredential: false
signCount: 1
rpId: undefined
userHandle:
{
attestation : {
}
......
......@@ -9,7 +9,7 @@
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: false,
hasResidentKey: true,
hasUserVerification: false,
},
})).result.authenticatorId;
......@@ -17,37 +17,56 @@
// No credentials registered yet.
testRunner.log(await dp.WebAuthn.getCredentials({authenticatorId}));
// Register two credentials.
testRunner.log((await session.evaluateAsync("registerCredential()")).status);
// Register a non-resident credential.
testRunner.log((await session.evaluateAsync("registerCredential()")).status);
// TODO(nsatragno): content_shell does not support registering resident
// credentials through navigator.credentials.create(). Update this test to use
// registerCredential() once that feature is supported.
const userHandle = "nina";
const credentialId = "cred-2";
testRunner.log(await dp.WebAuthn.addCredential({
authenticatorId,
credential: {
credentialId: btoa(credentialId),
rpId: "devtools.test",
privateKey: await session.evaluateAsync("generateBase64Key()"),
signCount: 1,
isResidentCredential: true,
userHandle: btoa(userHandle),
}
}));
let logCredential = credential => {
testRunner.log("isResidentCredential: " + credential.isResidentCredential);
testRunner.log("signCount: " + credential.signCount);
testRunner.log("rpId: " + credential.rpId);
testRunner.log("userHandle: " + atob(credential.userHandle || ""));
};
// Get the registered credentials.
let credentials = (await dp.WebAuthn.getCredentials({authenticatorId})).result.credentials;
let expectedRpIdHash = await session.evaluateAsync("generateRpIdHash()");
for (let credential of credentials) {
if (credential.rpIdHash === expectedRpIdHash)
testRunner.log("RP ID hash matches expected value");
else
testRunner.log(`RP ID hash does not match. Actual: ${credential.rpIdHash}, expected: ${expectedRpIdHash}`);
testRunner.log(credential.signCount);
}
// Authenticating with the first credential should succeed.
let credential = credentials[0];
let residentCredential = credentials.find(cred => cred.isResidentCredential);
let nonResidentCredential = credentials.find(cred => !cred.isResidentCredential);
testRunner.log("Resident Credential:");
logCredential(residentCredential);
testRunner.log("Non-Resident Credential:");
logCredential(nonResidentCredential);
// Authenticating with the non resident credential should succeed.
testRunner.log(await session.evaluateAsync(`getCredential({
type: "public-key",
id: base64ToArrayBuffer("${credential.credentialId}"),
id: base64ToArrayBuffer("${nonResidentCredential.credentialId}"),
transports: ["usb", "ble", "nfc"],
})`));
// Sign count should be increased by one for |credential|.
// Sign count should be increased by one for |nonResidentCredential|.
credentials = (await dp.WebAuthn.getCredentials({authenticatorId})).result.credentials;
testRunner.log(credentials.find(
cred => cred.id === credential.id).signCount);
cred => cred.credentialId === nonResidentCredential.credentialId).signCount);
// We should be able to parse the private key.
let keyData =
Uint8Array.from(atob(credential.privateKey), c => c.charCodeAt(0)).buffer;
Uint8Array.from(atob(nonResidentCredential.privateKey), c => c.charCodeAt(0)).buffer;
let key = await window.crypto.subtle.importKey(
"pkcs8", keyData, { name: "ECDSA", namedCurve: "P-256" },
true /* extractable */, ["sign"]);
......
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