Commit e3c917a0 authored by Casey Piper's avatar Casey Piper Committed by Commit Bot

WebAuthn: Call WebAuthn register APIs from Cryptotoken

When a registration request is received by Cryptotoken,
proxy that request to WebAuthn.

Bug: 906881
Change-Id: Ie3aac8bfb8bd4349eb29e7741bd5242b70842236
Reviewed-on: https://chromium-review.googlesource.com/c/1342687
Commit-Queue: Casey Piper <piperc@chromium.org>
Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Reviewed-by: default avatarAdam Langley <agl@chromium.org>
Reviewed-by: default avatarKim Paulhamus <kpaulhamus@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611307}
parent 25617f72
......@@ -151,6 +151,7 @@
<include name="IDR_CRYPTOTOKEN_USBGNUBBYFACTORY_JS" file="cryptotoken/usbgnubbyfactory.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_DEVICESTATUSCODES_JS" file="cryptotoken/devicestatuscodes.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_ASN1_JS" file="cryptotoken/asn1.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_CBOR_JS" file="cryptotoken/cbor.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_ENROLLER_JS" file="cryptotoken/enroller.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_USBENROLLHANDLER_JS" file="cryptotoken/usbenrollhandler.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_REQUESTQUEUE_JS" file="cryptotoken/requestqueue.js" type="BINDATA" />
......
......@@ -8,6 +8,7 @@ js_type_check("closure_compile") {
deps = [
":approvedorigins",
":b64",
":cbor",
":closeable",
":countdown",
":errorcodes",
......@@ -28,6 +29,9 @@ js_library("b64") {
js_library("approvedorigins") {
}
js_library("cbor") {
}
js_library("closeable") {
}
......
This diff is collapsed.
......@@ -102,11 +102,15 @@ function transportType(der) {
* makeCertAndKey creates a new ECDSA keypair and returns the private key
* and a cert containing the public key.
*
* @param {!Uint8Array} original The certificate being replaced, as DER bytes.
* @param {!Uint8Array=} opt_original The certificate being replaced, as DER
* bytes.
* @return {Promise<{privateKey: !webCrypto.CryptoKey, certDER: !Uint8Array}>}
*/
async function makeCertAndKey(original) {
var transport = transportType(original);
async function makeCertAndKey(opt_original) {
var transport = null;
if (opt_original) {
transport = transportType(opt_original);
}
if (transport !== null) {
if (transport.length != 2) {
throw Error('bad extension length');
......@@ -805,25 +809,230 @@ Enroller.prototype.sendEnrollRequestToHelper_ = function() {
return;
}
var self = this;
this.checkAppIds_(enrollAppIds, function(result) {
this.checkAppIds_(enrollAppIds, async (result) => {
if (self.done_)
return;
if (result) {
self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request);
if (self.handler_) {
var helperComplete =
/** @type {function(HelperReply)} */
(self.helperComplete_.bind(self));
self.handler_.run(helperComplete);
} else {
self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
}
// AppID is valid, so the request should be sent.
await new Promise(resolve => {
if (!chrome.cryptotokenPrivate || !window.PublicKeyCredential) {
resolve(false);
} else {
chrome.cryptotokenPrivate.canProxyToWebAuthn(resolve);
}
}).then(shouldUseWebAuthn => {
let v2Challenge;
for (let index = 0; index < self.enrollChallenges_.length; index++) {
if (self.enrollChallenges_[index]['version'] === 'U2F_V2') {
v2Challenge = self.enrollChallenges_[index]['challenge'];
}
}
if (v2Challenge && shouldUseWebAuthn) {
// If we can proxy to WebAuthn, send the request via WebAuthn.
this.doRegisterWebAuthn_(enrollAppIds[0], v2Challenge, request);
} else {
self.handler_ =
FACTORY_REGISTRY.getRequestHelper().getHandler(request);
if (self.handler_) {
var helperComplete =
/** @type {function(HelperReply)} */
(self.helperComplete_.bind(self));
self.handler_.run(helperComplete);
} else {
self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
}
}
});
} else {
self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
}
});
};
/**
* Proxies the registration request over the WebAuthn API.
* @private
*/
Enroller.prototype.doRegisterWebAuthn_ = function(appId, challenge, request) {
// Set a random ID.
const randomId = new Uint8Array(new ArrayBuffer(16));
crypto.getRandomValues(randomId);
const excludeList = [];
for (let index = 0; index < request['signData'].length; index++) {
const element = request['signData'][index];
excludeList.push({
type: 'public-key',
id: new Uint8Array(B64_decode(element['keyHandle'])).buffer,
transports: ['usb'],
});
}
const options = {
publicKey: {
rp: {
id: appId,
name: this.sender_.origin,
},
user: {
id: randomId.buffer,
displayName: this.sender_.origin,
name: this.sender_.origin,
},
challenge: new Uint8Array(B64_decode(challenge)).buffer,
pubKeyCredParams: [{
type: 'public-key',
alg: -7, // ES-256
}],
timeout: this.timer_.millisecondsUntilExpired(),
excludeCredentials: excludeList,
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
requireResidentKey: false,
userVerification: 'discouraged',
},
attestation: 'direct',
},
};
navigator.credentials.create(options)
.then(response => {
this.onWebAuthnSuccess_(response, appId);
})
.catch(exception => {
this.onWebAuthnError_(exception);
});
};
/**
* Handles a successful credential response from WebAuthn's make credential
* request.
* @private
*/
Enroller.prototype.onWebAuthnSuccess_ =
async function(publicKeyCredential, appId) {
const clientData =
new Uint8Array(publicKeyCredential['response']['clientDataJSON']);
const browserData = B64_encode(Array.from(clientData));
const u2fResponseData = await this.parseU2fResponseFromAttestationObject_(
publicKeyCredential['response']['attestationObject'], appId, browserData);
this.notifySuccess_('U2F_V2', u2fResponseData, browserData);
};
/**
* Parses the attestation object received from a WebAuthn make credential call
* and converts it into a U2F response message formatted into Base64.
* @private
*/
Enroller.prototype.parseU2fResponseFromAttestationObject_ =
async function(attestationObject, appId, clientData) {
// The first byte of the registration response is always 0x5.
let u2fResponse = [0x5];
// Parse the attestation object from CBOR into a JavaScript object.
const attestationObjectCbor = new Cbor(attestationObject).getCBOR();
// Authenticator data must be at least 120 bytes in length.
// https://www.w3.org/TR/webauthn/#fig-attStructs
if (!attestationObjectCbor['authData'] ||
attestationObjectCbor['authData'].length < 120) {
console.warn('Received invalid authenticator response');
this.notifyError_({
errorCode: ErrorCodes.OTHER_ERROR,
errorMessage: 'Invalid response message',
});
return;
}
const authData = attestationObjectCbor['authData'];
// Attested credential data starts after a 32 byte RP ID hash, a 1 byte flag,
// and a 4 byte counter value.
// https://www.w3.org/TR/webauthn/#sctn-attestation
const attestedCredentialData = authData.slice(37, authData.length);
let index = 16;
let credentialIdLength = (attestedCredentialData[index++] & 0xFF) << 8;
credentialIdLength |= (attestedCredentialData[index++] & 0xFF);
const credentialId =
attestedCredentialData.slice(index, index + credentialIdLength);
index += credentialIdLength;
const encodedPublicKey =
attestedCredentialData.slice(index, attestedCredentialData.length);
// Parse public key and format it in X509 format [0x4, 32-byte X, 32-byte Y].
const coseKey = new Cbor(encodedPublicKey).getCBOR();
const publicKeyArray = ([0x4].concat(Array.from(coseKey['-2'])))
.concat(Array.from(coseKey['-3']));
// Concatenate U2F registration response from the public key, key handle
// length, key handle, attestatation certificate, and signature.
u2fResponse = u2fResponse.concat(publicKeyArray);
u2fResponse.push(credentialIdLength);
u2fResponse = u2fResponse.concat(Array.from(credentialId));
const fmt = attestationObjectCbor['fmt'];
const attStatement = attestationObjectCbor['attStmt'];
let x5c;
let signature;
switch (new TextDecoder('utf-8').decode(fmt)) {
case 'fido-u2f':
x5c = attStatement['x5c'][0];
signature = attStatement['sig'];
break;
case 'none':
// Append empty x509 cert and signature to the registration message.
const emptySequence = new Uint8Array([0x30, 0]); // empty ASN.1 SEQUENCE.
const registrationData =
B64_encode(u2fResponse.concat(Array.from(emptySequence))
.concat(Array.from(emptySequence)));
const reg = new Registration(registrationData, appId, null, clientData);
const keypair = await makeCertAndKey();
signature = await reg.sign(keypair.privateKey);
x5c = keypair.certDER;
break;
default:
console.warn('Received unsupported non-U2F attestation');
this.notifyError_({
errorCode: ErrorCodes.OTHER_ERROR,
errorMessage: 'Invalid response message',
});
return;
}
u2fResponse = u2fResponse.concat(Array.from(x5c));
u2fResponse = u2fResponse.concat(Array.from(signature));
return B64_encode(u2fResponse);
};
/**
* Handles DOMExceptions returned as errors from the WebAuthn make credential
* call. Converts exceptions into U2F compatible exceptions.
* @param {*} exception Exception returned from the WebAuthn request.
* @private
*/
Enroller.prototype.onWebAuthnError_ = function(exception) {
const domError = /** @type {!DOMException} */ (exception);
let errorCode = ErrorCodes.OTHER_ERROR;
let errorDetails;
if (domError && domError.name) {
switch (domError.name) {
case 'NotAllowedError':
errorCode = ErrorCodes.TIMEOUT;
break;
case 'InvalidStateError':
errorCode = ErrorCodes.DEVICE_INELIGIBLE;
break;
default:
// Fall through
break;
}
}
this.notifyError_({
errorCode: errorCode,
errorMessage: domError.toString(),
});
};
/**
* Encodes the enroll challenge as an enroll helper challenge.
* @param {EnrollChallenge} enrollChallenge The enroll challenge to encode.
......
{
"name": "CryptoTokenExtension",
"description": "CryptoToken Component Extension",
"version": "0.9.73",
"version": "0.9.74",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7zRobvA+AVlvNqkHSSVhh1sEWsHSqz4oR/XptkDe/Cz3+gW9ZGumZ20NCHjaac8j1iiesdigp8B1LJsd/2WWv2Dbnto4f8GrQ5MVphKyQ9WJHwejEHN2K4vzrTcwaXqv5BSTXwxlxS/mXCmXskTfryKTLuYrcHEWK8fCHb+0gvr8b/kvsi75A1aMmb6nUnFJvETmCkOCPNX5CHTdy634Ts/x0fLhRuPlahk63rdf7agxQv5viVjQFk+tbgv6aa9kdSd11Js/RZ9yZjrFgHOBWgP4jTBqud4+HUglrzu8qynFipyNRLCZsaxhm+NItTyNgesxLdxZcwOz56KD1Q4IQIDAQAB",
"manifest_version": 2,
"permissions": [
......@@ -36,6 +36,7 @@
"scripts": [
"util.js",
"b64.js",
"cbor.js",
"sha256.js",
"timer.js",
"countdown.js",
......@@ -48,7 +49,7 @@
"factoryregistry.js",
"closeable.js",
"requesthelper.js",
"asn1.js",
"asn1.js",
"enroller.js",
"requestqueue.js",
"signer.js",
......
......@@ -643,9 +643,11 @@ void AuthenticatorImpl::MakeCredential(
// used to communicate with the origin.
if (OriginIsCryptoTokenExtension(caller_origin_)) {
// As Cryptotoken validates the origin, accept the relying party id as the
// origin from requests originating from Cryptotoken.
// origin from requests originating from Cryptotoken. The origin is provided
// in Cryptotoken requests as the relying party name, which should be used
// as part of client data.
client_data_json_ = SerializeCollectedClientDataToJson(
client_data::kU2fRegisterType, relying_party_id_,
client_data::kU2fRegisterType, options->relying_party->name,
std::move(options->challenge), true /* use_legacy_u2f_type_key */);
} else {
client_data_json_ = SerializeCollectedClientDataToJson(
......
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