Commit 1cb52119 authored by Nina Satragno's avatar Nina Satragno Committed by Commit Bot

[chromedriver] Add AddCredential command

Add the `AddCredential` command to ChromeDriver.

A draft of the proposed addition to the specification is available at
https://github.com/w3c/webauthn/pull/1256

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: Id9b9fb75ab6682126d5d1275b12785640b7f0053
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1729963
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Auto-Submit: Nina Satragno <nsatragno@chromium.org>
Reviewed-by: default avatarJohn Chen <johnchen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#683364}
parent def00bbb
......@@ -633,3 +633,23 @@ class ChromeDriver(object):
def RemoveVirtualAuthenticator(self, authenticatorId):
params = {'authenticatorId': authenticatorId}
return self.ExecuteCommand(Command.REMOVE_VIRTUAL_AUTHENTICATOR, params)
def AddCredential(self, authenticatorId=None, credentialId=None,
isResidentCredential=None, rpId=None, privateKey=None,
userHandle=None, signCount=None):
options = {}
if authenticatorId is not None:
options['authenticatorId'] = authenticatorId
if credentialId is not None:
options['credentialId'] = credentialId
if isResidentCredential is not None:
options['isResidentCredential'] = isResidentCredential
if rpId is not None:
options['rpId'] = rpId
if privateKey is not None:
options['privateKey'] = privateKey
if userHandle is not None:
options['userHandle'] = userHandle
if signCount is not None:
options['signCount'] = signCount
return self.ExecuteCommand(Command.ADD_CREDENTIAL, options)
......@@ -173,6 +173,9 @@ class Command(object):
REMOVE_VIRTUAL_AUTHENTICATOR = (
_Method.DELETE,
'/session/:sessionId/webauthn/authenticator/:authenticatorId')
ADD_CREDENTIAL = (
_Method.POST,
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential')
# Custom Chrome commands.
IS_LOADING = (_Method.GET, '/session/:sessionId/is_loading')
......
......@@ -756,6 +756,15 @@ HttpHandler::HttpHandler(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteRemoveVirtualAuthenticator)))),
CommandMapping(
kPost,
"session/:sessionId/webauthn/authenticator/:authenticatorId/"
"credential",
WrapToCommand(
"AddCredential",
base::BindRepeating(&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteAddCredential)))),
//
// Non-standard extension commands
//
......
......@@ -225,6 +225,7 @@ _ANDROID_NEGATIVE_FILTER['chrome'] = (
# Android does not support the virtual authenticator environment.
'ChromeDriverSecureContextTest.testAddVirtualAuthenticator',
'ChromeDriverSecureContextTest.testRemoveVirtualAuthenticator',
'ChromeDriverSecureContextTest.testAddCredential',
]
)
_ANDROID_NEGATIVE_FILTER['chrome_stable'] = (
......@@ -2078,6 +2079,42 @@ class ChromeDriverSecureContextTest(ChromeDriverBaseTest):
'Could not find a Virtual Authenticator matching the ID',
self._driver.RemoveVirtualAuthenticator, response['authenticatorId'])
def testAddCredential(self):
# The example attestation private key from the U2F spec at
# https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-example
# PKCS.8 encoded without encryption.
privateKey = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8/zMDQDYAxlU+Qhk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwURmgsJYxGP//fWN/S+j5sN4tT15XEpN/7QZnt14YvI6uvAgO0uJEboFaZlOEB"
script = """
let done = arguments[0];
getCredential({
type: "public-key",
id: new TextEncoder().encode("cred-1"),
transports: ["usb"],
}).then(done);
"""
self._driver.Load(self.GetHttpsUrlForFile(
'/chromedriver/webauthn_test.html', 'chromedriver.test'))
authenticatorId = self._driver.AddVirtualAuthenticator(
protocol = 'ctap2',
transport = 'usb',
hasResidentKey = False,
hasUserVerification = False,
)['authenticatorId']
# Register a credential and try authenticating with it.
self._driver.AddCredential(
authenticatorId = authenticatorId,
credentialId = base64.b64encode("cred-1"),
isResidentCredential=False,
rpId="chromedriver.test",
privateKey=privateKey,
signCount=1,
)
result = self._driver.ExecuteAsyncScript(script)
self.assertEquals('OK', result['status'])
# Tests in the following class are expected to be moved to ChromeDriverTest
# class when we no longer support the legacy mode.
......
......@@ -13,6 +13,24 @@
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/session.h"
namespace {
// Creates a base::DictionaryValue by cloning the parameters specified by
// |mapping| from |params|.
base::DictionaryValue MapParams(
const base::flat_map<const char*, const char*>& mapping,
const base::Value& params) {
base::DictionaryValue options;
for (const std::pair<const char*, const char*>& pair : mapping) {
const base::Value* value = params.FindKey(pair.second);
if (value)
options.SetPath(pair.first, value->Clone());
}
return options;
}
} // namespace
Status ExecuteWebAuthnCommand(const WebAuthnCommand& command,
Session* session,
const base::DictionaryValue& params,
......@@ -36,32 +54,43 @@ Status ExecuteWebAuthnCommand(const WebAuthnCommand& command,
Status ExecuteAddVirtualAuthenticator(WebView* web_view,
const base::Value& params,
std::unique_ptr<base::Value>* value) {
base::DictionaryValue options;
const base::flat_map<const char*, const char*> mapping = {
{"options.protocol", "protocol"},
{"options.transport", "transport"},
{"options.hasResidentKey", "hasResidentKey"},
{"options.hasUserVerification", "hasUserVerification"},
{"options.automaticPresenceSimulation", "isUserVerified"},
};
for (const std::pair<const char*, const char*>& pair : mapping) {
const base::Value* value = params.FindKey(pair.second);
if (value)
options.SetPath(pair.first, value->Clone());
}
return web_view->SendCommandAndGetResult("WebAuthn.addVirtualAuthenticator",
options, value);
return web_view->SendCommandAndGetResult(
"WebAuthn.addVirtualAuthenticator",
MapParams(
{
{"options.protocol", "protocol"},
{"options.transport", "transport"},
{"options.hasResidentKey", "hasResidentKey"},
{"options.hasUserVerification", "hasUserVerification"},
{"options.automaticPresenceSimulation", "isUserVerified"},
},
params),
value);
}
Status ExecuteRemoveVirtualAuthenticator(WebView* web_view,
const base::Value& params,
std::unique_ptr<base::Value>* value) {
base::DictionaryValue options;
const base::Value* authenticatorId = params.FindKey("authenticatorId");
if (authenticatorId)
options.SetKey("authenticatorId", authenticatorId->Clone());
return web_view->SendCommandAndGetResult(
"WebAuthn.removeVirtualAuthenticator",
MapParams({{"authenticatorId", "authenticatorId"}}, params), value);
}
Status ExecuteAddCredential(WebView* web_view,
const base::Value& params,
std::unique_ptr<base::Value>* value) {
return web_view->SendCommandAndGetResult(
"WebAuthn.removeVirtualAuthenticator", options, value);
"WebAuthn.addCredential",
MapParams(
{
{"authenticatorId", "authenticatorId"},
{"credential.credentialId", "credentialId"},
{"credential.isResidentCredential", "isResidentCredential"},
{"credential.rpId", "rpId"},
{"credential.privateKey", "privateKey"},
{"credential.userHandle", "userHandle"},
{"credential.signCount", "signCount"},
},
params),
value);
}
......@@ -39,4 +39,9 @@ Status ExecuteRemoveVirtualAuthenticator(WebView* web_view,
const base::Value& params,
std::unique_ptr<base::Value>* value);
// Inject a credential into an authenticator.
Status ExecuteAddCredential(WebView* web_view,
const base::Value& params,
std::unique_ptr<base::Value>* value);
#endif // CHROME_TEST_CHROMEDRIVER_WEBAUTHN_COMMANDS_H_
......@@ -34,5 +34,24 @@ async function registerCredential(options = {}) {
return {status: error.toString()};
}
}
async function getCredential(credential, options = {}) {
options = Object.assign({
challenge: Uint8Array.from("Winter is Coming"),
rpId: "chromedriver.test",
allowCredentials: [credential],
userVerification: "preferred",
}, options);
try {
const attestation = await navigator.credentials.get({publicKey: options});
return {
status: "OK",
attestation,
};
} catch (error) {
return {status: error.toString()};
}
}
</script>
</html>
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