Commit a91eca7e authored by Nina Satragno's avatar Nina Satragno Committed by Commit Bot

[devtools] Add support for WebAuthn large blobs

Add support for storing and retrieving large blobs through the WebAuthn
DevTools Domain. This will in turn be used by WebDriver to implement the
webauthn:extension:largeBlob capability [1].

This patch also wires up the CTAP version through devtools. This is a
soft requirement for large blobs, and will also be used to move
credProps tests to WPTs [2].

[1] https://w3c.github.io/webauthn/#sctn-authenticator-extension-capabilities
[2] https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/web_tests/http/tests/credentialmanager/credentialscontainer-create-with-resident-keys.html

Bug: 1114875
Change-Id: Ia9a5353285bd9aec540d5e92ec737919c7790b62
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2486254
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Reviewed-by: default avatarAndrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#819488}
parent d1ab3b63
......@@ -20,6 +20,8 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend {
public:
CONTENT_EXPORT WebAuthnHandler();
CONTENT_EXPORT ~WebAuthnHandler() override;
WebAuthnHandler(const WebAuthnHandler&) = delete;
WebAuthnHandler operator=(const WebAuthnHandler&) = delete;
// DevToolsDomainHandler:
void SetRenderer(int process_host_id,
......@@ -33,17 +35,15 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend {
std::unique_ptr<WebAuthn::VirtualAuthenticatorOptions> options,
String* out_authenticator_id) override;
Response RemoveVirtualAuthenticator(const String& authenticator_id) override;
Response AddCredential(
void AddCredential(const String& authenticator_id,
std::unique_ptr<protocol::WebAuthn::Credential> credential,
std::unique_ptr<AddCredentialCallback> callback) override;
void GetCredential(const String& authenticator_id,
const Binary& credential_id,
std::unique_ptr<GetCredentialCallback> callback) override;
void GetCredentials(
const String& authenticator_id,
std::unique_ptr<protocol::WebAuthn::Credential> credential) override;
Response GetCredential(
const String& authenticator_id,
const Binary& credential_id,
std::unique_ptr<WebAuthn::Credential>* out_credential) override;
Response GetCredentials(
const String& authenticator_id,
std::unique_ptr<protocol::Array<protocol::WebAuthn::Credential>>*
out_credentials) override;
std::unique_ptr<GetCredentialsCallback> callback) override;
Response RemoveCredential(const String& in_authenticator_id,
const Binary& credential_id) override;
Response ClearCredentials(const String& in_authenticator_id) override;
......@@ -58,7 +58,6 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend {
Response FindAuthenticator(const String& id,
VirtualAuthenticator** out_authenticator);
RenderFrameHostImpl* frame_host_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(WebAuthnHandler);
};
} // namespace protocol
......
......@@ -110,7 +110,8 @@
},
{
"domain": "WebAuthn",
"include": ["enable", "disable", "addVirtualAuthenticator", "removeVirtualAuthenticator", "addCredential", "removeCredential", "clearCredentials", "getCredential", "getCredentials", "setUserVerified", "setAutomaticPresenceSimulation"]
"include": ["enable", "disable", "addVirtualAuthenticator", "removeVirtualAuthenticator", "addCredential", "removeCredential", "clearCredentials", "getCredential", "getCredentials", "setUserVerified", "setAutomaticPresenceSimulation"],
"async": ["addCredential", "getCredential", "getCredentials"]
}
]
},
......
......@@ -51,7 +51,7 @@ void VirtualAuthenticator::AddReceiver(
bool VirtualAuthenticator::AddRegistration(
std::vector<uint8_t> key_handle,
const std::string& rp_id,
const std::vector<uint8_t>& private_key,
base::span<const uint8_t> private_key,
int32_t counter) {
base::Optional<std::unique_ptr<device::VirtualFidoDevice::PrivateKey>>
fido_private_key =
......@@ -71,7 +71,7 @@ bool VirtualAuthenticator::AddRegistration(
bool VirtualAuthenticator::AddResidentRegistration(
std::vector<uint8_t> key_handle,
std::string rp_id,
const std::vector<uint8_t>& private_key,
base::span<const uint8_t> private_key,
int32_t counter,
std::vector<uint8_t> user_handle) {
base::Optional<std::unique_ptr<device::VirtualFidoDevice::PrivateKey>>
......@@ -221,7 +221,7 @@ void VirtualAuthenticator::OnLargeBlobUncompressed(
}
void VirtualAuthenticator::OnLargeBlobCompressed(
const std::vector<uint8_t>& key_handle,
base::span<const uint8_t> key_handle,
SetLargeBlobCallback callback,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result) {
auto registration = state_->registrations.find(key_handle);
......
......@@ -9,6 +9,7 @@
#include <string>
#include <vector>
#include "base/containers/span.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
......@@ -52,14 +53,14 @@ class CONTENT_EXPORT VirtualAuthenticator
// false otherwise.
bool AddRegistration(std::vector<uint8_t> key_handle,
const std::string& rp_id,
const std::vector<uint8_t>& private_key,
base::span<const 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,
base::span<const uint8_t> private_key,
int32_t counter,
std::vector<uint8_t> user_handle);
......@@ -70,11 +71,6 @@ class CONTENT_EXPORT VirtualAuthenticator
// credential was found and removed, false otherwise.
bool RemoveRegistration(const std::vector<uint8_t>& key_handle);
// Returns the large blob associated with the credential identified by
// |key_handle|, if any.
base::Optional<std::vector<uint8_t>> GetLargeBlob(
base::span<const uint8_t> key_handle);
// Sets whether tests of user presence succeed or not for new requests sent to
// this authenticator. The default is true.
void SetUserPresence(bool is_user_present);
......@@ -129,7 +125,7 @@ class CONTENT_EXPORT VirtualAuthenticator
GetLargeBlobCallback callback,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result);
void OnLargeBlobCompressed(
const std::vector<uint8_t>& key_handle,
base::span<const uint8_t> key_handle,
SetLargeBlobCallback callback,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result);
......
......@@ -564,8 +564,10 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state,
if (config.large_blob_support) {
DCHECK(config.resident_key_support);
DCHECK(base::Contains(config.ctap2_versions, Ctap2Version::kCtap2_1));
DCHECK(config.pin_uv_auth_token_support)
<< "PinUvAuthToken support is required to write large blobs";
DCHECK((!config.pin_support && !config.internal_uv_support) ||
config.pin_uv_auth_token_support)
<< "PinUvAuthToken support is required to write large blobs for "
"uv-enabled authenticators";
options_updated = true;
options.supports_large_blobs = true;
}
......
......@@ -8255,6 +8255,11 @@ experimental domain WebAuthn
# Client To Authenticator Protocol 2.
ctap2
type Ctap2Version extends string
enum
ctap2_0
ctap2_1
type AuthenticatorTransport extends string
enum
# Cross-Platform authenticator attachments:
......@@ -8268,6 +8273,8 @@ experimental domain WebAuthn
type VirtualAuthenticatorOptions extends object
properties
AuthenticatorProtocol protocol
# Defaults to ctap2_0. Ignored if |protocol| == u2f.
optional Ctap2Version ctap2Version
AuthenticatorTransport transport
# Defaults to false.
optional boolean hasResidentKey
......@@ -8300,6 +8307,9 @@ experimental domain WebAuthn
# assertion.
# See https://w3c.github.io/webauthn/#signature-counter
integer signCount
# The large blob associated with the credential.
# See https://w3c.github.io/webauthn/#sctn-large-blob-extension
optional binary largeBlob
# Enable the WebAuthn domain and start intercepting credential storage and
# retrieval with a virtual authenticator.
......
......@@ -58,14 +58,19 @@ async function registerCredential(options = {}) {
try {
const credential = await navigator.credentials.create({publicKey: options});
return {
let result = {
status: "OK",
credential: {
id: credential.id,
rawId: Array.from(new Uint8Array(credential.rawId)),
transports: credential.response.getTransports(),
}
},
};
if (credential.getClientExtensionResults().largeBlob) {
result.largeBlobSupported =
credential.getClientExtensionResults().largeBlob.supported;
}
return result;
} catch (error) {
return {status: error.toString()};
}
......@@ -81,10 +86,15 @@ async function getCredential(credential, options = {}) {
try {
const attestation = await navigator.credentials.get({publicKey: options});
return {
let result = {
status: "OK",
attestation,
};
if (attestation.getClientExtensionResults().largeBlob) {
result.blob = new TextDecoder().decode(
attestation.getClientExtensionResults().largeBlob.blob);
}
return result;
} catch (error) {
return {status: error.toString()};
}
......
......@@ -55,4 +55,12 @@ Check that the WebAuthn command addCredential validates parameters
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : Large blob requires resident key support
}
id : <number>
sessionId : <string>
}
......@@ -59,5 +59,12 @@
credentialOptions.credential.userHandle = btoa("nina");
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
// Try with a large blob on a non resident credential.
credentialOptions.credential.privateKey =
await session.evaluateAsync("generateBase64Key()");
credentialOptions.credential.largeBlob = btoa("large blob");
credentialOptions.credential.isResidentCredential = false;
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions));
testRunner.completeTest();
})
......@@ -15,6 +15,14 @@ Check that the WebAuthn addVirtualAuthenticator command validates parameters
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : Invalid CTAP version. Valid values are "ctap2_0" and "ctap2_1"
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
......@@ -31,4 +39,28 @@ Check that the WebAuthn addVirtualAuthenticator command validates parameters
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : Large blob requires resident key support
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : Large blob requires a CTAP 2.1 authenticator
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32602
message : Large blob requires a CTAP 2.1 authenticator
}
id : <number>
sessionId : <string>
}
......@@ -25,6 +25,17 @@
});
testRunner.log(protocolError);
const ctapVersionError = await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
ctap2Version: "nonsense",
transport: "usb",
hasResidentKey: false,
hasUserVerification: false,
},
});
testRunner.log(ctapVersionError);
const transportError = await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
......@@ -45,5 +56,41 @@
});
testRunner.log(u2fCableError);
const largeBlobRequiresRKError = await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
ctap2Version: "ctap2_0",
transport: "usb",
hasResidentKey: false,
hasUserVerification: false,
hasLargeBlob: true
},
});
testRunner.log(largeBlobRequiresRKError);
const largeBlobRequiresCtapError = await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "u2f",
ctap2Version: "ctap2_1",
transport: "usb",
hasResidentKey: true,
hasUserVerification: false,
hasLargeBlob: true
},
});
testRunner.log(largeBlobRequiresCtapError);
const largeBlobRequiresCtap2_1Error = await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
ctap2Version: "ctap2_0",
transport: "usb",
hasResidentKey: true,
hasUserVerification: false,
hasLargeBlob: true
},
});
testRunner.log(largeBlobRequiresCtap2_1Error);
testRunner.completeTest();
})
Check that WebAuthn large blob operations work
Create credential result: OK
Large blob support: true
{
id : <number>
result : {
}
sessionId : <string>
}
Assertion result: OK
Got I'm Commander Shepard, and this is my favorite blob on the Citadel!
Got I'm Commander Shepard, and this is my favorite blob on the Citadel!
(async function(testRunner) {
const {page, session, dp} =
await testRunner.startURL(
"https://devtools.test:8443/inspector-protocol/webauthn/resources/webauthn-test.https.html",
"Check that WebAuthn large blob operations work");
// Create an authenticator.
await dp.WebAuthn.enable();
const authenticatorId = (await dp.WebAuthn.addVirtualAuthenticator({
options: {
protocol: "ctap2",
ctap2Version: "ctap2_1",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
hasLargeBlob: true,
isUserVerified: true,
},
})).result.authenticatorId;
// Register a credential with a large blob through webauthn.
let result = await session.evaluateAsync(`registerCredential({
extensions: {
largeBlob: {
support: "preferred",
},
},
authenticatorSelection: {
requireResidentKey: true,
},
})`);
testRunner.log(`Create credential result: ${result.status}`);
testRunner.log(`Large blob support: ${result.largeBlobSupported}`);
// Register a credential with a large blob through devtools.
const credentialId = btoa("cred-1");
const largeBlob =
"I'm Commander Shepard, and this is my favorite blob on the Citadel!";
testRunner.log(await dp.WebAuthn.addCredential({
authenticatorId,
credential: {
credentialId,
userHandle: btoa("isabelle"),
rpId: "devtools.test",
privateKey: await session.evaluateAsync("generateBase64Key()"),
signCount: 0,
isResidentCredential: true,
largeBlob: btoa(largeBlob),
}
}));
// Read the large blob through the WebAuthn API.
result = await session.evaluateAsync(`getCredential({
type: "public-key",
id: new TextEncoder().encode("cred-1"),
transports: ["usb", "ble", "nfc"],
}, {
extensions: {
largeBlob: {
read: true,
},
},
})`);
testRunner.log(`Assertion result: ${result.status}`);
testRunner.log(`Got ${result.blob}`);
// Read the large blob through Devtools.
let credential =
(await dp.WebAuthn.getCredential({authenticatorId, credentialId})).result.credential;
testRunner.log(`Got ${atob(credential.largeBlob)}`);
testRunner.completeTest();
})
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