Commit 6fb1f2e7 authored by arnarb's avatar arnarb Committed by Commit bot

Update cryptotoken to 0.8.63

 - Request batch attestation instead of individual
 - Updates from U2F API revisions
 - Enumerate HID devieces based on usage page instead of vid/pid
 - Handle wrong keyHandle length and init messages
 - Smaller bug fixes and refactoring

BUG=416998
R=xiyuan,juanlang

Review URL: https://codereview.chromium.org/596083002

Cr-Commit-Position: refs/heads/master@{#296334}
parent ec689def
......@@ -199,6 +199,8 @@
<include name="IDR_CRYPTOTOKEN_DEVICEFACTORYREGISTRY_JS" file="cryptotoken/devicefactoryregistry.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_GSTATICORIGINCHECK_JS" file="cryptotoken/gstaticorigincheck.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_ORIGINCHECK_JS" file="cryptotoken/origincheck.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_INDIVIDUALATTEST_JS" file="cryptotoken/individualattest.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_GOOGLECORPINDIVIDUALATTESt_JS" file="cryptotoken/googlecorpindividualattest.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_CRYPTOTOKENBACKGROUND_JS" file="cryptotoken/cryptotokenbackground.js" type="BINDATA" />
<include name="IDR_WHISPERNET_PROXY_BACKGROUND_HTML" file="whispernet_proxy/background.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
<include name="IDR_WHISPERNET_PROXY_INIT_JS" file="whispernet_proxy/js/init.js" type="BINDATA" />
......
......@@ -23,43 +23,19 @@ var FACTORY_REGISTRY = new FactoryRegistry(
var DEVICE_FACTORY_REGISTRY = new DeviceFactoryRegistry(
new UsbGnubbyFactory(gnubbies),
TIMER_FACTORY);
/**
* @param {Object} request Request object
* @param {MessageSender} sender Sender frame
* @param {Function} sendResponse Response callback
* @return {?Closeable} Optional handler object that should be closed when port
* closes
*/
function handleWebPageRequest(request, sender, sendResponse) {
switch (request.type) {
case GnubbyMsgTypes.ENROLL_WEB_REQUEST:
return handleWebEnrollRequest(sender, request, sendResponse);
case GnubbyMsgTypes.SIGN_WEB_REQUEST:
return handleWebSignRequest(sender, request, sendResponse);
case MessageTypes.U2F_REGISTER_REQUEST:
return handleU2fEnrollRequest(sender, request, sendResponse);
case MessageTypes.U2F_SIGN_REQUEST:
return handleU2fSignRequest(sender, request, sendResponse);
default:
sendResponse(
makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
MessageTypes.U2F_REGISTER_RESPONSE));
return null;
}
}
TIMER_FACTORY,
new GoogleCorpIndividualAttestation());
// Listen to individual messages sent from (whitelisted) webpages via
// chrome.runtime.sendMessage
function messageHandlerExternal(request, sender, sendResponse) {
var closeable = handleWebPageRequest(request, sender, function(response) {
response['requestId'] = request['requestId'];
sendResponse(response);
try {
sendResponse(response);
} catch (e) {
console.warn(UTIL_fmt('caught: ' + e.message));
}
});
}
chrome.runtime.onMessageExternal.addListener(messageHandlerExternal);
......
......@@ -11,13 +11,18 @@
/**
* @param {!GnubbyFactory} gnubbyFactory A Gnubby factory.
* @param {!CountdownFactory} countdownFactory A countdown timer factory.
* @param {!IndividualAttestation} individualAttestation An individual
* attestation implementation.
* @constructor
*/
function DeviceFactoryRegistry(gnubbyFactory, countdownFactory) {
function DeviceFactoryRegistry(gnubbyFactory, countdownFactory,
individualAttestation) {
/** @private {!GnubbyFactory} */
this.gnubbyFactory_ = gnubbyFactory;
/** @private {!CountdownFactory} */
this.countdownFactory_ = countdownFactory;
/** @private {!IndividualAttestation} */
this.individualAttestation_ = individualAttestation;
}
/** @return {!GnubbyFactory} A Gnubby factory. */
......@@ -29,3 +34,9 @@ DeviceFactoryRegistry.prototype.getGnubbyFactory = function() {
DeviceFactoryRegistry.prototype.getCountdownFactory = function() {
return this.countdownFactory_;
};
/** @return {!IndividualAttestation} An individual attestation implementation.
*/
DeviceFactoryRegistry.prototype.getIndividualAttestation = function() {
return this.individualAttestation_;
};
......@@ -14,6 +14,12 @@ var DeviceStatusCodes = {};
*/
DeviceStatusCodes.OK_STATUS = 0;
/**
* Device operation wrong length status.
* @const
*/
DeviceStatusCodes.WRONG_LENGTH_STATUS = 0x6700;
/**
* Device operation wait touch status.
* @const
......
......@@ -83,13 +83,14 @@ function handleU2fEnrollRequest(sender, request, sendResponse) {
closeable =
validateAndBeginEnrollRequest(
sender, request, 'registerRequests', 'signRequests',
sendErrorResponse, sendSuccessResponse);
sendErrorResponse, sendSuccessResponse, 'registeredKeys');
return closeable;
}
/**
* Validates an enroll request using the given parameters, and, if valid, begins
* handling the enroll request.
* handling the enroll request. (The enroll request may be modified as a result
* of handling it.)
* @param {MessageSender} sender The sender of the message.
* @param {Object} request The web page's enroll request.
* @param {string} enrollChallengesName The name of the enroll challenges value
......@@ -99,11 +100,14 @@ function handleU2fEnrollRequest(sender, request, sendResponse) {
* @param {function(ErrorCodes)} errorCb Error callback.
* @param {function(string, string, (string|undefined))} successCb Success
* callback.
* @param {string=} opt_registeredKeysName The name of the registered keys
* value in the request.
* @return {Closeable} Request handler that should be closed when the browser
* message channel is closed.
*/
function validateAndBeginEnrollRequest(sender, request,
enrollChallengesName, signChallengesName, errorCb, successCb) {
enrollChallengesName, signChallengesName, errorCb, successCb,
opt_registeredKeysName) {
var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
if (!origin) {
errorCb(ErrorCodes.BAD_REQUEST);
......@@ -111,20 +115,31 @@ function validateAndBeginEnrollRequest(sender, request,
}
if (!isValidEnrollRequest(request, enrollChallengesName,
signChallengesName)) {
signChallengesName, opt_registeredKeysName)) {
errorCb(ErrorCodes.BAD_REQUEST);
return null;
}
var enrollChallenges = request[enrollChallengesName];
var signChallenges = request[signChallengesName];
var signChallenges;
if (opt_registeredKeysName &&
request.hasOwnProperty(opt_registeredKeysName)) {
// Convert registered keys to sign challenges by adding a challenge value.
signChallenges = request[opt_registeredKeysName];
for (var i = 0; i < signChallenges.length; i++) {
// The actual value doesn't matter, as long as it's a string.
signChallenges[i]['challenge'] = '';
}
} else {
signChallenges = request[signChallengesName];
}
var logMsgUrl = request['logMsgUrl'];
var timer = createTimerForRequest(
FACTORY_REGISTRY.getCountdownFactory(), request);
var enroller = new Enroller(timer, origin, errorCb, successCb,
sender.tlsChannelId, logMsgUrl);
enroller.doEnroll(enrollChallenges, signChallenges);
enroller.doEnroll(enrollChallenges, signChallenges, request['appId']);
return /** @type {Closeable} */ (enroller);
}
......@@ -135,22 +150,32 @@ function validateAndBeginEnrollRequest(sender, request,
* in the request.
* @param {string} signChallengesName The name of the sign challenges value in
* the request.
* @param {string=} opt_registeredKeysName The name of the registered keys
* value in the request.
* @return {boolean} Whether the request appears valid.
*/
function isValidEnrollRequest(request, enrollChallengesName,
signChallengesName) {
signChallengesName, opt_registeredKeysName) {
if (!request.hasOwnProperty(enrollChallengesName))
return false;
var enrollChallenges = request[enrollChallengesName];
if (!enrollChallenges.length)
return false;
if (!isValidEnrollChallengeArray(enrollChallenges))
var hasAppId = request.hasOwnProperty('appId');
if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId))
return false;
var signChallenges = request[signChallengesName];
// A missing sign challenge array is ok, in the case the user is not already
// enrolled.
if (signChallenges && !isValidSignChallengeArray(signChallenges))
if (signChallenges && !isValidSignChallengeArray(signChallenges, !hasAppId))
return false;
if (opt_registeredKeysName) {
var registeredKeys = request[opt_registeredKeysName];
if (registeredKeys &&
!isValidRegisteredKeyArray(registeredKeys, !hasAppId)) {
return false;
}
}
return true;
}
......@@ -166,10 +191,12 @@ var EnrollChallenge;
/**
* @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to
* validate.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the given array of challenges is a valid enroll
* challenges array.
*/
function isValidEnrollChallengeArray(enrollChallenges) {
function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) {
var seenVersions = {};
for (var i = 0; i < enrollChallenges.length; i++) {
var enrollChallenge = enrollChallenges[i];
......@@ -186,7 +213,7 @@ function isValidEnrollChallengeArray(enrollChallenges) {
return false;
}
seenVersions[version] = version;
if (!enrollChallenge['appId']) {
if (appIdRequired && !enrollChallenge['appId']) {
return false;
}
if (!enrollChallenge['challenge']) {
......@@ -300,10 +327,13 @@ Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
* @param {Array.<EnrollChallenge>} enrollChallenges A set of enroll challenges.
* @param {Array.<SignChallenge>} signChallenges A set of sign challenges for
* existing enrollments for this user and appId.
* @param {string=} opt_appId The app id for the entire request.
*/
Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges) {
var encodedEnrollChallenges = this.encodeEnrollChallenges_(enrollChallenges);
var encodedSignChallenges = encodeSignChallenges(signChallenges);
Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges,
opt_appId) {
var encodedEnrollChallenges =
this.encodeEnrollChallenges_(enrollChallenges, opt_appId);
var encodedSignChallenges = encodeSignChallenges(signChallenges, opt_appId);
var request = {
type: 'enroll_helper_request',
enrollChallenges: encodedEnrollChallenges,
......@@ -317,8 +347,19 @@ Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges) {
// Begin fetching/checking the app ids.
var enrollAppIds = [];
if (opt_appId) {
enrollAppIds.push(opt_appId);
}
for (var i = 0; i < enrollChallenges.length; i++) {
enrollAppIds.push(enrollChallenges[i]['appId']);
if (enrollChallenges[i].hasOwnProperty('appId')) {
enrollAppIds.push(enrollChallenges[i]['appId']);
}
}
// Sanity check
if (!enrollAppIds.length) {
console.warn(UTIL_fmt('empty enroll app ids?'));
this.notifyError_(ErrorCodes.BAD_REQUEST);
return;
}
var self = this;
this.checkAppIds_(enrollAppIds, signChallenges, function(result) {
......@@ -341,10 +382,11 @@ Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges) {
/**
* Encodes the enroll challenge as an enroll helper challenge.
* @param {EnrollChallenge} enrollChallenge The enroll challenge to encode.
* @param {string=} opt_appId The app id for the entire request.
* @return {EnrollHelperChallenge} The encoded challenge.
* @private
*/
Enroller.encodeEnrollChallenge_ = function(enrollChallenge) {
Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) {
var encodedChallenge = {};
var version;
if (enrollChallenge['version']) {
......@@ -354,19 +396,30 @@ Enroller.encodeEnrollChallenge_ = function(enrollChallenge) {
version = 'U2F_V1';
}
encodedChallenge['version'] = version;
encodedChallenge['challenge'] = enrollChallenge['challenge'];
encodedChallenge['appIdHash'] =
B64_encode(sha256HashOfString(enrollChallenge['appId']));
encodedChallenge['challengeHash'] = enrollChallenge['challenge'];
var appId;
if (enrollChallenge['appId']) {
appId = enrollChallenge['appId'];
} else {
appId = opt_appId;
}
if (!appId) {
// Sanity check. (Other code should fail if it's not set.)
console.warn(UTIL_fmt('No appId?'));
}
encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId));
return /** @type {EnrollHelperChallenge} */ (encodedChallenge);
};
/**
* Encodes the given enroll challenges using this enroller's state.
* @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges.
* @param {string=} opt_appId The app id for the entire request.
* @return {!Array.<EnrollHelperChallenge>} The encoded enroll challenges.
* @private
*/
Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges) {
Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges,
opt_appId) {
var challenges = [];
for (var i = 0; i < enrollChallenges.length; i++) {
var enrollChallenge = enrollChallenges[i];
......@@ -393,9 +446,10 @@ Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges) {
this.browserData_[version] =
B64_encode(UTIL_StringToBytes(browserData));
challenges.push(Enroller.encodeEnrollChallenge_(
/** @type {EnrollChallenge} */ (modifiedChallenge)));
/** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId));
} else {
challenges.push(Enroller.encodeEnrollChallenge_(enrollChallenge));
challenges.push(
Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId));
}
}
return challenges;
......
......@@ -34,26 +34,25 @@ Gnubby.U2F_V1 = 'U2F_V1';
/** V2 of the applet. */
Gnubby.U2F_V2 = 'U2F_V2';
/**
* Google corporate appId hash
* @private
*/
Gnubby.GOOGLE_CORP_APP_ID_HASH_ = 'ZEZHL99u7Xvzwzcg8jZnbDbhtF6-BIXbiaPN_dJL1p8';
/** Perform enrollment
* @param {ArrayBuffer|Uint8Array} challenge Enrollment challenge
* @param {ArrayBuffer|Uint8Array} appIdHash Hashed application id
* @param {function(...)} cb Result callback
* @param {boolean=} opt_individualAttestation Request the individual
* attestation cert rather than the batch one.
*/
Gnubby.prototype.enroll = function(challenge, appIdHash, cb) {
Gnubby.prototype.enroll = function(challenge, appIdHash, cb,
opt_individualAttestation) {
var p1 = Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME;
if (opt_individualAttestation) {
p1 |= Gnubby.P1_INDIVIDUAL_KEY;
}
var apdu = new Uint8Array(
[0x00,
Gnubby.U2F_ENROLL,
Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME,
p1,
0x00, 0x00, 0x00,
challenge.length + appIdHash.length]);
if (B64_encode(appIdHash) == Gnubby.GOOGLE_CORP_APP_ID_HASH_)
apdu[2] |= Gnubby.P1_INDIVIDUAL_KEY;
var u8 = new Uint8Array(apdu.length + challenge.length +
appIdHash.length + 2);
for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i];
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Provides a Google corp implementation of IndividualAttestation.
*/
'use strict';
/**
* Google corp implementation of IndividualAttestation that requests
* individual certificates for corp accounts.
* @constructor
* @implements IndividualAttestation
*/
function GoogleCorpIndividualAttestation() {}
/**
* @param {string} appIdHash The app id hash.
* @return {boolean} Whether to request the individual attestation certificate
* for this app id.
*/
GoogleCorpIndividualAttestation.prototype.requestIndividualAttestation =
function(appIdHash) {
return appIdHash == GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID_HASH;
};
/**
* App ID used by Google employee accounts.
* @const
*/
GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID =
'https://www.gstatic.com/securitykey/a/google.com/origins.json';
/**
* Hash of the app ID used by Google employee accounts.
* @const
*/
GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID_HASH =
B64_encode(sha256HashOfString(
GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID));
......@@ -71,6 +71,12 @@ HidGnubbyDevice.prototype.destroy = function() {
this.dev = null;
chrome.hid.disconnect(dev.connectionId, function() {
if (chrome.runtime.lastError) {
console.warn(UTIL_fmt('Device ' + dev.connectionId +
' couldn\'t be disconnected:'));
console.warn(chrome.runtime.lastError);
return;
}
console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed'));
});
};
......@@ -213,8 +219,9 @@ HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
if (this.lockCID != cid) {
// Some other channel has active lock.
if (cmd != GnubbyDevice.CMD_SYNC) {
// Anything but SYNC gets an immediate busy.
if (cmd != GnubbyDevice.CMD_SYNC &&
cmd != GnubbyDevice.CMD_INIT) {
// Anything but SYNC|INIT gets an immediate busy.
var busy = new Uint8Array(
[(cid >> 24) & 255,
(cid >> 16) & 255,
......@@ -229,8 +236,9 @@ HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
return false;
}
// SYNC gets to go to the device to flush OS tx/rx queues.
// The usb firmware always responds to SYNC, regardless of lock status.
// SYNC|INIT gets to go to the device to flush OS tx/rx queues.
// The usb firmware is to alway respond to SYNC/INIT,
// regardless of lock status.
}
}
return true;
......@@ -411,12 +419,18 @@ HidGnubbyDevice.enumerate = function(cb) {
}
}
GnubbyDevice.getPermittedUsbDevices(function(devs) {
permittedDevs = devs;
for (var i = 0; i < devs.length; i++) {
chrome.hid.getDevices(devs[i], enumerated);
}
});
try {
chrome.hid.getDevices({filters: [{usagePage: 0xf1d0}]}, cb);
} catch (e) {
console.log(e);
console.log(UTIL_fmt('falling back to vid/pid enumeration'));
GnubbyDevice.getPermittedUsbDevices(function(devs) {
permittedDevs = devs;
for (var i = 0; i < devs.length; i++) {
chrome.hid.getDevices(devs[i], enumerated);
}
});
}
};
/**
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Provides an interface to determine whether to request the
* individual attestation certificate during enrollment.
*/
'use strict';
/**
* Interface to determine whether to request the individual attestation
* certificate during enrollment.
* @interface
*/
function IndividualAttestation() {}
/**
* @param {string} appIdHash The app id hash.
* @return {boolean} Whether to request the individual attestation certificate
* for this app id.
*/
IndividualAttestation.prototype.requestIndividualAttestation =
function(appIdHash) {};
{
"name": "CryptoTokenExtension",
"description": "CryptoToken Component Extension",
"version": "0.8.59",
"version": "0.8.63",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7zRobvA+AVlvNqkHSSVhh1sEWsHSqz4oR/XptkDe/Cz3+gW9ZGumZ20NCHjaac8j1iiesdigp8B1LJsd/2WWv2Dbnto4f8GrQ5MVphKyQ9WJHwejEHN2K4vzrTcwaXqv5BSTXwxlxS/mXCmXskTfryKTLuYrcHEWK8fCHb+0gvr8b/kvsi75A1aMmb6nUnFJvETmCkOCPNX5CHTdy634Ts/x0fLhRuPlahk63rdf7agxQv5viVjQFk+tbgv6aa9kdSd11Js/RZ9yZjrFgHOBWgP4jTBqud4+HUglrzu8qynFipyNRLCZsaxhm+NItTyNgesxLdxZcwOz56KD1Q4IQIDAQAB",
"manifest_version": 2,
"permissions": [
"usb",
"hid",
"usb",
"u2fDevices",
"https://www.gstatic.com/",
{
"usbDevices": [
......@@ -22,6 +23,10 @@
"vendorId": 4176,
"productId": 275
},
{
"vendorId": 4176,
"productId": 276
},
{
"vendorId": 4176,
"productId": 277
......@@ -34,6 +39,10 @@
"vendorId": 4176,
"productId": 1025
},
{
"vendorId": 4176,
"productId": 1026
},
{
"vendorId": 9601,
"productId": 61904
......@@ -42,6 +51,10 @@
"vendorId": 7223,
"productId": 61904
},
{
"vendorId": 1155,
"productId": 61904
},
{
"vendorId": 1419,
"productId": 24579
......@@ -51,9 +64,9 @@
],
"externally_connectable": {
"matches": [
"https://login.corp.google.com/*",
"https://accounts.google.com/*",
"https://security.google.com/*",
"https://login.corp.google.com/*"
"https://security.google.com/*"
],
"accepts_tls_channel_id": true
},
......@@ -92,11 +105,13 @@
"multiplesigner.js",
"generichelper.js",
"inherits.js",
"individualattest.js",
"devicefactoryregistry.js",
"usbhelper.js",
"usbenrollhandler.js",
"usbsignhandler.js",
"usbgnubbyfactory.js",
"googlecorpindividualattest.js",
"cryptotokenbackground.js"
]
}
......
......@@ -262,7 +262,7 @@ MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) {
MultipleGnubbySigner.prototype.signCompletedCallback_ =
function(tracker, result) {
console.log(
UTIL_fmt(result.code ? 'failure.' : 'success!' +
UTIL_fmt((result.code ? 'failure.' : 'success!') +
' gnubby ' + tracker.index +
' got code ' + result.code.toString(16)));
if (!tracker.stillGoing) {
......
......@@ -144,9 +144,20 @@ function validateAndEnqueueSignRequest(sender, request,
}
var signChallenges = request[signChallengesName];
// A valid sign data has at least one challenge, so get the first appId from
// the first challenge.
var firstAppId = signChallenges[0]['appId'];
var appId;
if (request['appId']) {
appId = request['appId'];
} else {
// A valid sign data has at least one challenge, so get the appId from
// the first challenge.
appId = signChallenges[0]['appId'];
}
// Sanity check
if (!appId) {
console.warn(UTIL_fmt('empty sign appId?'));
errorCb(ErrorCodes.BAD_REQUEST);
return null;
}
var timer = createTimerForRequest(
FACTORY_REGISTRY.getCountdownFactory(), request);
var logMsgUrl = request['logMsgUrl'];
......@@ -154,9 +165,9 @@ function validateAndEnqueueSignRequest(sender, request,
// Queue sign requests from the same origin, to protect against simultaneous
// sign-out on many tabs resulting in repeated sign-in requests.
var queuedSignRequest = new QueuedSignRequest(signChallenges,
timer, nonNullOrigin, errorCb, successCb, sender.tlsChannelId,
timer, nonNullOrigin, errorCb, successCb, appId, sender.tlsChannelId,
logMsgUrl);
var requestToken = signRequestQueue.queueRequest(firstAppId, nonNullOrigin,
var requestToken = signRequestQueue.queueRequest(appId, nonNullOrigin,
queuedSignRequest.begin.bind(queuedSignRequest), timer);
queuedSignRequest.setToken(requestToken);
return queuedSignRequest;
......@@ -177,7 +188,8 @@ function isValidSignRequest(request, signChallengesName) {
// be fulfilled. Fail.
if (!signChallenges.length)
return false;
return isValidSignChallengeArray(signChallenges);
var hasAppId = request.hasOwnProperty('appId');
return isValidSignChallengeArray(signChallenges, !hasAppId);
}
/**
......@@ -187,13 +199,14 @@ function isValidSignRequest(request, signChallengesName) {
* @param {string} origin Signature origin
* @param {function(ErrorCodes)} errorCb Error callback
* @param {function(SignChallenge, string, string)} successCb Success callback
* @param {string|undefined} opt_appId The app id for the entire request.
* @param {string|undefined} opt_tlsChannelId TLS Channel Id
* @param {string|undefined} opt_logMsgUrl Url to post log messages to
* @constructor
* @implements {Closeable}
*/
function QueuedSignRequest(signChallenges, timer, origin, errorCb,
successCb, opt_tlsChannelId, opt_logMsgUrl) {
successCb, opt_appId, opt_tlsChannelId, opt_logMsgUrl) {
/** @private {!Array.<SignChallenge>} */
this.signChallenges_ = signChallenges;
/** @private {Countdown} */
......@@ -205,6 +218,8 @@ function QueuedSignRequest(signChallenges, timer, origin, errorCb,
/** @private {function(SignChallenge, string, string)} */
this.successCb_ = successCb;
/** @private {string|undefined} */
this.appId_ = opt_appId;
/** @private {string|undefined} */
this.tlsChannelId_ = opt_tlsChannelId;
/** @private {string|undefined} */
this.logMsgUrl_ = opt_logMsgUrl;
......@@ -244,7 +259,7 @@ QueuedSignRequest.prototype.begin = function(token) {
this.signer_ = new Signer(this.timer_, this.origin_,
this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
this.tlsChannelId_, this.logMsgUrl_);
if (!this.signer_.setChallenges(this.signChallenges_)) {
if (!this.signer_.setChallenges(this.signChallenges_, this.appId_)) {
token.complete();
this.errorCb_(ErrorCodes.BAD_REQUEST);
}
......@@ -320,13 +335,16 @@ function Signer(timer, origin, errorCb, successCb,
/**
* Sets the challenges to be signed.
* @param {Array.<SignChallenge>} signChallenges The challenges to set.
* @param {string=} opt_appId The app id for the entire request.
* @return {boolean} Whether the challenges could be set.
*/
Signer.prototype.setChallenges = function(signChallenges) {
Signer.prototype.setChallenges = function(signChallenges, opt_appId) {
if (this.challengesSet_ || this.done_)
return false;
/** @private {Array.<SignChallenge>} */
this.signChallenges_ = signChallenges;
/** @private {string|undefined} */
this.appId_ = opt_appId;
/** @private {boolean} */
this.challengesSet_ = true;
......@@ -340,6 +358,9 @@ Signer.prototype.setChallenges = function(signChallenges) {
*/
Signer.prototype.checkAppIds_ = function() {
var appIds = getDistinctAppIds(this.signChallenges_);
if (this.appId_) {
appIds = UTIL_unionArrays([this.appId_], appIds);
}
if (!appIds || !appIds.length) {
this.notifyError_(ErrorCodes.BAD_REQUEST);
return;
......@@ -405,7 +426,7 @@ Signer.prototype.doSign_ = function() {
}
var encodedChallenges = encodeSignChallenges(this.signChallenges_,
this.getChallengeHash_.bind(this));
this.appId_, this.getChallengeHash_.bind(this));
var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds,
......
......@@ -72,8 +72,8 @@ function SingleGnubbySigner(gnubbyId, forEnroll, completeCb, timer,
/** @private {boolean} */
this.challengesSet_ = false;
/** @private {!Array.<string>} */
this.notForMe_ = [];
/** @private {!Object.<string, number>} */
this.cachedError_ = [];
}
/** @enum {number} */
......@@ -242,7 +242,7 @@ SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
// TODO: This won't be confused with success, but should it be
// part of the same namespace as the other error codes, which are
// always in DeviceStatusCodes.*?
this.goToError_(rc);
this.goToError_(rc, true);
}
};
......@@ -254,7 +254,7 @@ SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
*/
SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
if (rc) {
this.goToError_(rc);
this.goToError_(rc, true);
return;
}
this.state_ = SingleGnubbySigner.State.IDLE;
......@@ -295,9 +295,9 @@ SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
var challengeHash = challenge.challengeHash;
var appIdHash = challenge.appIdHash;
var keyHandle = challenge.keyHandle;
if (this.notForMe_.indexOf(keyHandle) != -1) {
if (this.cachedError_.hasOwnProperty(keyHandle)) {
// Cache hit: return wrong data again.
this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
this.signCallback_(challengeIndex, this.cachedError_[keyHandle]);
} else if (challenge.version && challenge.version != this.version_) {
// Sign challenge for a different version of gnubby: return wrong data.
this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
......@@ -326,13 +326,14 @@ SingleGnubbySigner.prototype.signCallback_ =
return;
}
// Cache wrong data result, re-asking the gnubby to sign it won't produce
// different results.
if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
// Cache wrong data or wrong length results, re-asking the gnubby to sign it
// won't produce different results.
if (code == DeviceStatusCodes.WRONG_DATA_STATUS ||
code == DeviceStatusCodes.WRONG_LENGTH_STATUS) {
if (challengeIndex < this.challenges_.length) {
var challenge = this.challenges_[challengeIndex];
if (this.notForMe_.indexOf(challenge.keyHandle) == -1) {
this.notForMe_.push(challenge.keyHandle);
if (!this.cachedError_.hasOwnProperty(challenge.keyHandle)) {
this.cachedError_[challenge.keyHandle] = code;
}
}
}
......@@ -364,6 +365,7 @@ SingleGnubbySigner.prototype.signCallback_ =
break;
case DeviceStatusCodes.WRONG_DATA_STATUS:
case DeviceStatusCodes.WRONG_LENGTH_STATUS:
if (this.challengeIndex_ < this.challenges_.length - 1) {
this.doSign_(++this.challengeIndex_);
} else if (this.forEnroll_) {
......@@ -375,11 +377,11 @@ SingleGnubbySigner.prototype.signCallback_ =
default:
if (this.forEnroll_) {
this.goToError_(code);
this.goToError_(code, true);
} else if (this.challengeIndex_ < this.challenges_.length - 1) {
this.doSign_(++this.challengeIndex_);
} else {
this.goToError_(code);
this.goToError_(code, true);
}
}
};
......@@ -387,11 +389,13 @@ SingleGnubbySigner.prototype.signCallback_ =
/**
* Switches to the error state, and notifies caller.
* @param {number} code Error code
* @param {boolean=} opt_warn Whether to warn in the console about the error.
* @private
*/
SingleGnubbySigner.prototype.goToError_ = function(code) {
SingleGnubbySigner.prototype.goToError_ = function(code, opt_warn) {
this.state_ = SingleGnubbySigner.State.COMPLETE;
console.log(UTIL_fmt('failed (' + code.toString(16) + ')'));
var logFn = opt_warn ? console.warn.bind(console) : console.log.bind(console);
logFn(UTIL_fmt('failed (' + code.toString(16) + ')'));
// Since this gnubby can no longer produce a useful result, go ahead and
// close it.
this.close();
......
......@@ -17,10 +17,12 @@ function TextFetcher() {}
/**
* @param {string} url The URL to fetch.
* @param {string?} opt_method The HTTP method to use (default GET)
* @param {string?} opt_body The request body
* @return {!Promise.<string>} A promise for the fetched text. In case of an
* error, this promise is rejected with an HTTP status code.
*/
TextFetcher.prototype.fetch = function(url) {};
TextFetcher.prototype.fetch = function(url, opt_method, opt_body) {};
/**
* @constructor
......@@ -31,13 +33,16 @@ function XhrTextFetcher() {
/**
* @param {string} url The URL to fetch.
* @param {string?} opt_method The HTTP method to use (default GET)
* @param {string?} opt_body The request body
* @return {!Promise.<string>} A promise for the fetched text. In case of an
* error, this promise is rejected with an HTTP status code.
*/
XhrTextFetcher.prototype.fetch = function(url) {
XhrTextFetcher.prototype.fetch = function(url, opt_method, opt_body) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
var method = opt_method || 'GET';
xhr.open(method, url, true);
xhr.onloadend = function() {
if (xhr.status != 200) {
reject(xhr.status);
......@@ -45,6 +50,13 @@ XhrTextFetcher.prototype.fetch = function(url) {
}
resolve(xhr.responseText);
};
xhr.send();
xhr.onerror = function() {
// Treat any network-level errors as though the page didn't exist.
reject(404);
};
if (opt_body)
xhr.send(opt_body);
else
xhr.send();
});
};
......@@ -98,7 +98,8 @@ UsbEnrollHandler.prototype.signerFoundGnubby_ =
// caller, as the gnubby is already enrolled. Map ok to WRONG_DATA, so the
// caller knows what to do.
this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS);
} else if (signResult.code == DeviceStatusCodes.WRONG_DATA_STATUS) {
} else if (signResult.code == DeviceStatusCodes.WRONG_DATA_STATUS ||
signResult.code == DeviceStatusCodes.WRONG_LENGTH_STATUS) {
var gnubby = signResult['gnubby'];
// A valid helper request contains at least one enroll challenge, so use
// the app id hash from the first challenge.
......@@ -206,10 +207,13 @@ UsbEnrollHandler.prototype.tryEnroll_ = function(gnubby, version) {
this.removeWrongVersionGnubby_(gnubby);
return;
}
var challengeChallenge = B64_decode(challenge['challenge']);
var appIdHash = B64_decode(challenge['appIdHash']);
gnubby.enroll(challengeChallenge, appIdHash,
this.enrollCallback_.bind(this, gnubby, version));
var challengeValue = B64_decode(challenge['challengeHash']);
var appIdHash = challenge['appIdHash'];
var individualAttest =
DEVICE_FACTORY_REGISTRY.getIndividualAttestation().
requestIndividualAttestation(appIdHash);
gnubby.enroll(challengeValue, B64_decode(appIdHash),
this.enrollCallback_.bind(this, gnubby, version), individualAttest);
};
/**
......
......@@ -77,8 +77,20 @@ UsbGnubbyDevice.prototype.destroy = function() {
this.dev = null;
chrome.usb.releaseInterface(dev, 0, function() {
if (chrome.runtime.lastError) {
console.warn(UTIL_fmt('Device ' + dev.handle +
' couldn\'t be released:'));
console.warn(chrome.runtime.lastError);
return;
}
console.log(UTIL_fmt('Device ' + dev.handle + ' released'));
chrome.usb.closeDevice(dev, function() {
if (chrome.runtime.lastError) {
console.warn(UTIL_fmt('Device ' + dev.handle +
' couldn\'t be closed:'));
console.warn(chrome.runtime.lastError);
return;
}
console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
});
});
......@@ -276,8 +288,9 @@ UsbGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
if (this.lockCID != cid) {
// Some other channel has active lock.
if (cmd != GnubbyDevice.CMD_SYNC) {
// Anything but SYNC gets an immediate busy.
if (cmd != GnubbyDevice.CMD_SYNC &&
cmd != GnubbyDevice.CMD_INIT) {
// Anything but SYNC|INIT gets an immediate busy.
var busy = new Uint8Array(
[(cid >> 24) & 255,
(cid >> 16) & 255,
......@@ -292,8 +305,9 @@ UsbGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
return false;
}
// SYNC gets to go to the device to flush OS tx/rx queues.
// The usb firmware always responds to SYNC, regardless of lock status.
// SYNC|INIT get to go to the device to flush OS tx/rx queues.
// The usb firmware is to always respond to SYNC|INIT,
// regardless of lock status.
}
}
return true;
......@@ -439,7 +453,7 @@ var InterfaceDescriptor;
UsbGnubbyDevice.open = function(gnubbies, which, dev, cb) {
/** @param {chrome.usb.ConnectionHandle=} handle Connection handle */
function deviceOpened(handle) {
if (!handle) {
if (chrome.runtime.lastError) {
console.warn(UTIL_fmt('failed to open device. permissions issue?'));
cb(-GnubbyDevice.NODEVICE);
return;
......
......@@ -25,27 +25,56 @@ function getOriginFromUrl(url) {
return origin;
}
/**
* Returns whether the registered key appears to be valid.
* @param {Object} registeredKey The registered key object.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the object appears valid.
*/
function isValidRegisteredKey(registeredKey, appIdRequired) {
if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
return false;
}
if (!registeredKey.hasOwnProperty('keyHandle'))
return false;
if (registeredKey['version']) {
if (registeredKey['version'] != 'U2F_V1' &&
registeredKey['version'] != 'U2F_V2') {
return false;
}
}
return true;
}
/**
* Returns whether the array of registered keys appears to be valid.
* @param {Array.<Object>} registeredKeys The array of registered keys.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the array appears valid.
*/
function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
return registeredKeys.every(function(key) {
return isValidRegisteredKey(key, appIdRequired);
});
}
/**
* Returns whether the array of SignChallenges appears to be valid.
* @param {Array.<SignChallenge>} signChallenges The array of sign challenges.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the array appears valid.
*/
function isValidSignChallengeArray(signChallenges) {
function isValidSignChallengeArray(signChallenges, appIdRequired) {
for (var i = 0; i < signChallenges.length; i++) {
var incomingChallenge = signChallenges[i];
if (!incomingChallenge.hasOwnProperty('challenge'))
return false;
if (!incomingChallenge.hasOwnProperty('appId')) {
if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
return false;
}
if (!incomingChallenge.hasOwnProperty('keyHandle'))
return false;
if (incomingChallenge['version']) {
if (incomingChallenge['version'] != 'U2F_V1' &&
incomingChallenge['version'] != 'U2F_V2') {
return false;
}
}
}
return true;
}
......@@ -66,6 +95,35 @@ function logMessage(logMsg, opt_logMsgUrl) {
audio.src = opt_logMsgUrl + logMsg;
}
/**
* @param {Object} request Request object
* @param {MessageSender} sender Sender frame
* @param {Function} sendResponse Response callback
* @return {?Closeable} Optional handler object that should be closed when port
* closes
*/
function handleWebPageRequest(request, sender, sendResponse) {
switch (request.type) {
case GnubbyMsgTypes.ENROLL_WEB_REQUEST:
return handleWebEnrollRequest(sender, request, sendResponse);
case GnubbyMsgTypes.SIGN_WEB_REQUEST:
return handleWebSignRequest(sender, request, sendResponse);
case MessageTypes.U2F_REGISTER_REQUEST:
return handleU2fEnrollRequest(sender, request, sendResponse);
case MessageTypes.U2F_SIGN_REQUEST:
return handleU2fSignRequest(sender, request, sendResponse);
default:
sendResponse(
makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
MessageTypes.U2F_REGISTER_RESPONSE));
return null;
}
}
/**
* Makes a response to a request.
* @param {Object} request The request to make a response to.
......@@ -310,13 +368,16 @@ function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
/**
* Encodes the sign data as an array of sign helper challenges.
* @param {Array.<SignChallenge>} signChallenges The sign challenges to encode.
* @param {string=} opt_defaultAppId The app id to use for each challenge, if
* the challenge contains none.
* @param {function(string, string): string=} opt_challengeHashFunction
* A function that produces, from a key handle and a raw challenge, a hash
* of the raw challenge. If none is provided, a default hash function is
* used.
* @return {!Array.<SignHelperChallenge>} The sign challenges, encoded.
*/
function encodeSignChallenges(signChallenges, opt_challengeHashFunction) {
function encodeSignChallenges(signChallenges, opt_defaultAppId,
opt_challengeHashFunction) {
function encodedSha256(keyHandle, challenge) {
return B64_encode(sha256HashOfString(challenge));
}
......@@ -327,9 +388,15 @@ function encodeSignChallenges(signChallenges, opt_challengeHashFunction) {
var challenge = signChallenges[i];
var challengeHash =
challengeHashFn(challenge['keyHandle'], challenge['challenge']);
var appId;
if (challenge.hasOwnProperty('appId')) {
appId = challenge['appId'];
} else {
appId = opt_defaultAppId;
}
var encodedChallenge = {
'challengeHash': challengeHash,
'appIdHash': B64_encode(sha256HashOfString(challenge['appId'])),
'appIdHash': B64_encode(sha256HashOfString(appId)),
'keyHandle': challenge['keyHandle'],
'version': (challenge['version'] || 'U2F_V1')
};
......
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