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 { ...@@ -20,6 +20,8 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend {
public: public:
CONTENT_EXPORT WebAuthnHandler(); CONTENT_EXPORT WebAuthnHandler();
CONTENT_EXPORT ~WebAuthnHandler() override; CONTENT_EXPORT ~WebAuthnHandler() override;
WebAuthnHandler(const WebAuthnHandler&) = delete;
WebAuthnHandler operator=(const WebAuthnHandler&) = delete;
// DevToolsDomainHandler: // DevToolsDomainHandler:
void SetRenderer(int process_host_id, void SetRenderer(int process_host_id,
...@@ -33,17 +35,15 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend { ...@@ -33,17 +35,15 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend {
std::unique_ptr<WebAuthn::VirtualAuthenticatorOptions> options, std::unique_ptr<WebAuthn::VirtualAuthenticatorOptions> options,
String* out_authenticator_id) override; String* out_authenticator_id) override;
Response RemoveVirtualAuthenticator(const String& 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, const String& authenticator_id,
std::unique_ptr<protocol::WebAuthn::Credential> credential) override; std::unique_ptr<GetCredentialsCallback> callback) 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;
Response RemoveCredential(const String& in_authenticator_id, Response RemoveCredential(const String& in_authenticator_id,
const Binary& credential_id) override; const Binary& credential_id) override;
Response ClearCredentials(const String& in_authenticator_id) override; Response ClearCredentials(const String& in_authenticator_id) override;
...@@ -58,7 +58,6 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend { ...@@ -58,7 +58,6 @@ class WebAuthnHandler : public DevToolsDomainHandler, public WebAuthn::Backend {
Response FindAuthenticator(const String& id, Response FindAuthenticator(const String& id,
VirtualAuthenticator** out_authenticator); VirtualAuthenticator** out_authenticator);
RenderFrameHostImpl* frame_host_ = nullptr; RenderFrameHostImpl* frame_host_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(WebAuthnHandler);
}; };
} // namespace protocol } // namespace protocol
......
...@@ -110,7 +110,8 @@ ...@@ -110,7 +110,8 @@
}, },
{ {
"domain": "WebAuthn", "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( ...@@ -51,7 +51,7 @@ void VirtualAuthenticator::AddReceiver(
bool VirtualAuthenticator::AddRegistration( bool VirtualAuthenticator::AddRegistration(
std::vector<uint8_t> key_handle, std::vector<uint8_t> key_handle,
const std::string& rp_id, const std::string& rp_id,
const std::vector<uint8_t>& private_key, base::span<const uint8_t> private_key,
int32_t counter) { int32_t counter) {
base::Optional<std::unique_ptr<device::VirtualFidoDevice::PrivateKey>> base::Optional<std::unique_ptr<device::VirtualFidoDevice::PrivateKey>>
fido_private_key = fido_private_key =
...@@ -71,7 +71,7 @@ bool VirtualAuthenticator::AddRegistration( ...@@ -71,7 +71,7 @@ bool VirtualAuthenticator::AddRegistration(
bool VirtualAuthenticator::AddResidentRegistration( bool VirtualAuthenticator::AddResidentRegistration(
std::vector<uint8_t> key_handle, std::vector<uint8_t> key_handle,
std::string rp_id, std::string rp_id,
const std::vector<uint8_t>& private_key, base::span<const uint8_t> private_key,
int32_t counter, int32_t counter,
std::vector<uint8_t> user_handle) { std::vector<uint8_t> user_handle) {
base::Optional<std::unique_ptr<device::VirtualFidoDevice::PrivateKey>> base::Optional<std::unique_ptr<device::VirtualFidoDevice::PrivateKey>>
...@@ -221,7 +221,7 @@ void VirtualAuthenticator::OnLargeBlobUncompressed( ...@@ -221,7 +221,7 @@ void VirtualAuthenticator::OnLargeBlobUncompressed(
} }
void VirtualAuthenticator::OnLargeBlobCompressed( void VirtualAuthenticator::OnLargeBlobCompressed(
const std::vector<uint8_t>& key_handle, base::span<const uint8_t> key_handle,
SetLargeBlobCallback callback, SetLargeBlobCallback callback,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result) { data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result) {
auto registration = state_->registrations.find(key_handle); auto registration = state_->registrations.find(key_handle);
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "base/containers/span.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
...@@ -52,14 +53,14 @@ class CONTENT_EXPORT VirtualAuthenticator ...@@ -52,14 +53,14 @@ class CONTENT_EXPORT VirtualAuthenticator
// false otherwise. // false otherwise.
bool AddRegistration(std::vector<uint8_t> key_handle, bool AddRegistration(std::vector<uint8_t> key_handle,
const std::string& rp_id, const std::string& rp_id,
const std::vector<uint8_t>& private_key, base::span<const uint8_t> private_key,
int32_t counter); int32_t counter);
// Register a new resident credential. Returns true if the registration was // Register a new resident credential. Returns true if the registration was
// successful, false otherwise. // successful, false otherwise.
bool AddResidentRegistration(std::vector<uint8_t> key_handle, bool AddResidentRegistration(std::vector<uint8_t> key_handle,
std::string rp_id, std::string rp_id,
const std::vector<uint8_t>& private_key, base::span<const uint8_t> private_key,
int32_t counter, int32_t counter,
std::vector<uint8_t> user_handle); std::vector<uint8_t> user_handle);
...@@ -70,11 +71,6 @@ class CONTENT_EXPORT VirtualAuthenticator ...@@ -70,11 +71,6 @@ class CONTENT_EXPORT VirtualAuthenticator
// credential was found and removed, false otherwise. // credential was found and removed, false otherwise.
bool RemoveRegistration(const std::vector<uint8_t>& key_handle); 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 // Sets whether tests of user presence succeed or not for new requests sent to
// this authenticator. The default is true. // this authenticator. The default is true.
void SetUserPresence(bool is_user_present); void SetUserPresence(bool is_user_present);
...@@ -129,7 +125,7 @@ class CONTENT_EXPORT VirtualAuthenticator ...@@ -129,7 +125,7 @@ class CONTENT_EXPORT VirtualAuthenticator
GetLargeBlobCallback callback, GetLargeBlobCallback callback,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result); data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result);
void OnLargeBlobCompressed( void OnLargeBlobCompressed(
const std::vector<uint8_t>& key_handle, base::span<const uint8_t> key_handle,
SetLargeBlobCallback callback, SetLargeBlobCallback callback,
data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result); data_decoder::DataDecoder::ResultOrError<mojo_base::BigBuffer> result);
......
...@@ -564,8 +564,10 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, ...@@ -564,8 +564,10 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state,
if (config.large_blob_support) { if (config.large_blob_support) {
DCHECK(config.resident_key_support); DCHECK(config.resident_key_support);
DCHECK(base::Contains(config.ctap2_versions, Ctap2Version::kCtap2_1)); DCHECK(base::Contains(config.ctap2_versions, Ctap2Version::kCtap2_1));
DCHECK(config.pin_uv_auth_token_support) DCHECK((!config.pin_support && !config.internal_uv_support) ||
<< "PinUvAuthToken support is required to write large blobs"; config.pin_uv_auth_token_support)
<< "PinUvAuthToken support is required to write large blobs for "
"uv-enabled authenticators";
options_updated = true; options_updated = true;
options.supports_large_blobs = true; options.supports_large_blobs = true;
} }
......
...@@ -8255,6 +8255,11 @@ experimental domain WebAuthn ...@@ -8255,6 +8255,11 @@ experimental domain WebAuthn
# Client To Authenticator Protocol 2. # Client To Authenticator Protocol 2.
ctap2 ctap2
type Ctap2Version extends string
enum
ctap2_0
ctap2_1
type AuthenticatorTransport extends string type AuthenticatorTransport extends string
enum enum
# Cross-Platform authenticator attachments: # Cross-Platform authenticator attachments:
...@@ -8268,6 +8273,8 @@ experimental domain WebAuthn ...@@ -8268,6 +8273,8 @@ experimental domain WebAuthn
type VirtualAuthenticatorOptions extends object type VirtualAuthenticatorOptions extends object
properties properties
AuthenticatorProtocol protocol AuthenticatorProtocol protocol
# Defaults to ctap2_0. Ignored if |protocol| == u2f.
optional Ctap2Version ctap2Version
AuthenticatorTransport transport AuthenticatorTransport transport
# Defaults to false. # Defaults to false.
optional boolean hasResidentKey optional boolean hasResidentKey
...@@ -8300,6 +8307,9 @@ experimental domain WebAuthn ...@@ -8300,6 +8307,9 @@ experimental domain WebAuthn
# assertion. # assertion.
# See https://w3c.github.io/webauthn/#signature-counter # See https://w3c.github.io/webauthn/#signature-counter
integer signCount 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 # Enable the WebAuthn domain and start intercepting credential storage and
# retrieval with a virtual authenticator. # retrieval with a virtual authenticator.
......
...@@ -58,14 +58,19 @@ async function registerCredential(options = {}) { ...@@ -58,14 +58,19 @@ async function registerCredential(options = {}) {
try { try {
const credential = await navigator.credentials.create({publicKey: options}); const credential = await navigator.credentials.create({publicKey: options});
return { let result = {
status: "OK", status: "OK",
credential: { credential: {
id: credential.id, id: credential.id,
rawId: Array.from(new Uint8Array(credential.rawId)), rawId: Array.from(new Uint8Array(credential.rawId)),
transports: credential.response.getTransports(), transports: credential.response.getTransports(),
} },
}; };
if (credential.getClientExtensionResults().largeBlob) {
result.largeBlobSupported =
credential.getClientExtensionResults().largeBlob.supported;
}
return result;
} catch (error) { } catch (error) {
return {status: error.toString()}; return {status: error.toString()};
} }
...@@ -81,10 +86,15 @@ async function getCredential(credential, options = {}) { ...@@ -81,10 +86,15 @@ async function getCredential(credential, options = {}) {
try { try {
const attestation = await navigator.credentials.get({publicKey: options}); const attestation = await navigator.credentials.get({publicKey: options});
return { let result = {
status: "OK", status: "OK",
attestation, attestation,
}; };
if (attestation.getClientExtensionResults().largeBlob) {
result.blob = new TextDecoder().decode(
attestation.getClientExtensionResults().largeBlob.blob);
}
return result;
} catch (error) { } catch (error) {
return {status: error.toString()}; return {status: error.toString()};
} }
......
...@@ -55,4 +55,12 @@ Check that the WebAuthn command addCredential validates parameters ...@@ -55,4 +55,12 @@ Check that the WebAuthn command addCredential validates parameters
id : <number> id : <number>
sessionId : <string> sessionId : <string>
} }
{
error : {
code : -32602
message : Large blob requires resident key support
}
id : <number>
sessionId : <string>
}
...@@ -59,5 +59,12 @@ ...@@ -59,5 +59,12 @@
credentialOptions.credential.userHandle = btoa("nina"); credentialOptions.credential.userHandle = btoa("nina");
testRunner.log(await dp.WebAuthn.addCredential(credentialOptions)); 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(); testRunner.completeTest();
}) })
...@@ -15,6 +15,14 @@ Check that the WebAuthn addVirtualAuthenticator command validates parameters ...@@ -15,6 +15,14 @@ Check that the WebAuthn addVirtualAuthenticator command validates parameters
id : <number> id : <number>
sessionId : <string> sessionId : <string>
} }
{
error : {
code : -32602
message : Invalid CTAP version. Valid values are "ctap2_0" and "ctap2_1"
}
id : <number>
sessionId : <string>
}
{ {
error : { error : {
code : -32602 code : -32602
...@@ -31,4 +39,28 @@ Check that the WebAuthn addVirtualAuthenticator command validates parameters ...@@ -31,4 +39,28 @@ Check that the WebAuthn addVirtualAuthenticator command validates parameters
id : <number> id : <number>
sessionId : <string> 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 @@ ...@@ -25,6 +25,17 @@
}); });
testRunner.log(protocolError); 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({ const transportError = await dp.WebAuthn.addVirtualAuthenticator({
options: { options: {
protocol: "ctap2", protocol: "ctap2",
...@@ -45,5 +56,41 @@ ...@@ -45,5 +56,41 @@
}); });
testRunner.log(u2fCableError); 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(); 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