Commit 8b88cf61 authored by Matthew Braithwaite's avatar Matthew Braithwaite Committed by Commit Bot

cryptotoken: incorporate attestation-certificate scrubbing option.

This allows the Relying Party to request that a token's attestation
certificate be replaced with a randomly-generated certificate.

The default behavior is unchanged.

BUG=780299

Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: I98d6a94399ac1896b6893c8c080f874d66b818b3
Reviewed-on: https://chromium-review.googlesource.com/804978Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Reviewed-by: default avatarAdam Langley <agl@chromium.org>
Commit-Queue: Matt Braithwaite <mab@google.com>
Cr-Commit-Position: refs/heads/master@{#521934}
parent bbe89b73
...@@ -155,6 +155,7 @@ ...@@ -155,6 +155,7 @@
<include name="IDR_CRYPTOTOKEN_GNUBBYFACTORY_JS" file="cryptotoken/gnubbyfactory.js" type="BINDATA" /> <include name="IDR_CRYPTOTOKEN_GNUBBYFACTORY_JS" file="cryptotoken/gnubbyfactory.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_USBGNUBBYFACTORY_JS" file="cryptotoken/usbgnubbyfactory.js" type="BINDATA" /> <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_DEVICESTATUSCODES_JS" file="cryptotoken/devicestatuscodes.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_ASN1_JS" file="cryptotoken/asn1.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_ENROLLER_JS" file="cryptotoken/enroller.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_USBENROLLHANDLER_JS" file="cryptotoken/usbenrollhandler.js" type="BINDATA" />
<include name="IDR_CRYPTOTOKEN_REQUESTQUEUE_JS" file="cryptotoken/requestqueue.js" type="BINDATA" /> <include name="IDR_CRYPTOTOKEN_REQUESTQUEUE_JS" file="cryptotoken/requestqueue.js" type="BINDATA" />
......
// Copyright 2017 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.
// This makes "async function" not an error.
module.exports = {
parserOptions: {ecmaVersion: 2017},
};
// Copyright 2017 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.
/**
* ASN.1 parser, in the manner of BoringSSL's CBS (crypto byte string) lib.
*
* A |ByteString| is a buffer of DER-encoded bytes. To decode the buffer, you
* must know something about the expected sequence of tags, which allows you to
* call getASN1() and friends with the right arguments and in the right order.
*
* https://commondatastorage.googleapis.com/chromium-boringssl-docs/bytestring.h.html
* is the canonical API reference.
*/
const ByteString = class {
/**
* Creates a new ASN.1 parser.
* @param {!Uint8Array} buffer DER-encoded ASN.1 bytes.
*/
constructor(buffer) {
/** @private {!Uint8Array} */
this.slice_ = buffer;
}
/**
* @return {!Uint8Array} The DER-encoded bytes remaining in the buffer.
*/
get data() {
return this.slice_;
}
/**
* @return {number} The number of DER-encoded bytes remaining in the buffer.
*/
get length() {
return this.slice_.length;
}
/**
* @return {boolean} True if the buffer is empty.
*/
get empty() {
return this.slice_.length == 0;
}
/**
* Pops a byte from the start of the buffer.
* @return {number} A byte.
* @throws {Error} if the buffer is empty.
* @private
*/
getU8_() {
if (this.empty) {
throw Error('getU8_: slice empty');
}
const b = this.slice_[0];
this.slice_ = this.slice_.subarray(1);
return b;
}
/**
* Pops |n| bytes from the buffer.
* @param {number} n The number of bytes to pop.
* @throws {Error}
* @private
*/
skip_(n) {
if (this.slice_.length < n) {
throw Error('skip_: too few bytes in input');
}
this.slice_ = this.slice_.subarray(n);
}
/**
* @param {number} n The number of bytes to read from the buffer.
* @return {!Uint8Array} an array of |n| bytes.
* @throws {Error}
*/
getBytes(n) {
if (this.slice_.length < n) {
throw Error('getBytes: too few bytes in input');
}
const prefix = this.slice_.subarray(0, n);
this.slice_ = this.slice_.subarray(n);
return prefix;
}
/**
* Returns a value of the specified type.
* @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next
* value in the buffer.
* @param {boolean=} opt_includeHeader If true, include header bytes in the
* buffer.
* @return {!ByteString} The DER-encoded value bytes.
* @throws {Error}
* @private
*/
getASN1_(expectedTag, opt_includeHeader) {
if (this.empty) {
throw Error('getASN1: empty slice, expected tag ' + expectedTag);
}
const v = this.getAnyASN1();
if (v.tag != expectedTag) {
throw Error('getASN1: got tag ' + v.tag + ', want ' + expectedTag);
}
if (!opt_includeHeader) {
v.val.skip_(v.headerLen);
}
return v.val;
}
/**
* Returns a value of the specified type.
* @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next
* value in the buffer.
* @return {!ByteString} The DER-encoded value bytes.
* @throws {Error}
*/
getASN1(expectedTag) {
return this.getASN1_(expectedTag, false);
}
/**
* Returns a base128-encoded integer.
* @return {number} an int32.
* @private
*/
getBase128Int_() {
var lookahead = this.slice_.length;
if (lookahead > 4) {
lookahead = 4;
}
var len = 0;
for (var i = 0; i < lookahead; i++) {
if (!(this.data[i] & 0x80)) {
len = i + 1;
break;
}
}
if (len == 0) {
throw Error('terminating byte not found');
}
var n = 0;
var octets = this.getBytes(len);
for (var i = 0; i < len; i++) {
n |= (octets[i] & 0x7f) << 7 * (len - i - 1);
}
return n;
}
/**
* Returns an OBJECT IDENTIFIER.
* @return {Array<number>}
*/
getASN1ObjectIdentifier() {
var b = this.getASN1(Tag.OBJECT);
var result = [];
var first = b.getBase128Int_();
result[1] = first % 40;
result[0] = (first - result[1]) / 40;
var n = 2;
while (!b.empty) {
result[n++] = b.getBase128Int_();
}
return result;
}
/**
* Returns a value of the specified type, with its header.
* @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next
* value in the buffer.
* @return {!ByteString} The DER-encoded header and value bytes.
* @throws {Error}
*/
getASN1Element(expectedTag) {
return this.getASN1_(expectedTag, true);
}
/**
* Returns an optional value of the specified type.
* @param {number} expectedTag The expected tag, e.g. |SEQUENCE|, of the next
* value in the buffer.
* @return {ByteString}
* */
getOptionalASN1(expectedTag) {
if (this.slice_.length < 1 || this.slice_[0] != expectedTag) {
return null;
}
return this.getASN1(expectedTag);
}
/**
* Matches and returns any ASN.1 type.
* @return {{tag: number, headerLen: number, val: !ByteString}} An ASN.1
* value. The returned |ByteString| includes the DER header bytes.
* @throws {Error}
*/
getAnyASN1() {
const header = new ByteString(this.slice_);
const tag = header.getU8_();
const lengthByte = header.getU8_();
if ((tag & 0x1f) == 0x1f) {
throw Error('getAnyASN1: long-form tag found');
}
var len = 0;
var headerLen = 0;
if ((lengthByte & 0x80) == 0) {
// Short form length.
len = lengthByte + 2;
headerLen = 2;
} else {
// The high bit indicates that this is the long form, while the next 7
// bits encode the number of subsequent octets used to encode the length
// (ITU-T X.690 clause 8.1.3.5.b).
const numBytes = lengthByte & 0x7f;
// Bitwise operations are always on signed 32-bit two's complement
// numbers. This check ensures that we stay under this limit. We could
// do this in a better way, but there's no need to process very large
// objects.
if (numBytes == 0 || numBytes > 3) {
throw Error('getAnyASN1: bad ASN.1 long-form length');
}
const lengthBytes = header.getBytes(numBytes);
for (var i = 0; i < numBytes; i++) {
len <<= 8;
len |= lengthBytes[i];
}
if (len < 128 || (len >> ((numBytes - 1) * 8)) == 0) {
throw Error('getAnyASN1: incorrectly encoded ASN.1 length');
}
headerLen = 2 + numBytes;
len += headerLen;
}
if (this.slice_.length < len) {
throw Error('getAnyASN1: too few bytes in input');
}
const prefix = this.slice_.subarray(0, len);
this.slice_ = this.slice_.subarray(len);
return {tag: tag, headerLen: headerLen, val: new ByteString(prefix)};
}
};
/**
* Tag is a container for ASN.1 tag values, like |SEQUENCE|. These values
* are arguments to e.g. getASN1().
*/
const Tag = class {
/** @return {number} */
static get BOOLEAN() {
return 1;
}
/** @return {number} */
static get INTEGER() {
return 2;
}
/** @return {number} */
static get BITSTRING() {
return 3;
}
/** @return {number} */
static get OCTETSTRING() {
return 4;
}
/** @return {number} */
static get NULL() {
return 5;
}
/** @return {number} */
static get OBJECT() {
return 6;
}
/** @return {number} */
static get UTF8String() {
return 12;
}
/** @return {number} */
static get PrintableString() {
return 19;
}
/** @return {number} */
static get UTCTime() {
return 23;
}
/** @return {number} */
static get GeneralizedTime() {
return 24;
}
/** @return {number} */
static get CONSTRUCTED() {
return 0x20;
}
/** @return {number} */
static get SEQUENCE() {
return 0x30;
}
/** @return {number} */
static get SET() {
return 0x31;
}
/** @return {number} */
static get CONTEXT_SPECIFIC() {
return 0x80;
}
};
/**
* ASN.1 builder, in the manner of BoringSSL's CBB (crypto byte builder).
*
* A |ByteBuilder| maintains a |Uint8Array| slice and appends to it on demand.
* After appending all the necessary values, the |data| property returns a
* slice containing the result. Utility functions are provided for appending
* ASN.1 DER-formatted values.
*
* Several of the functions take a "continuation" parameter. This is a function
* that makes calls to its argument in order to lay down the contents of a
* value. Once the continuation returns, the length prefix will be serialised.
* It is illegal to call methods on a parent ByteBuilder while a continuation
* function is running.
*/
const ByteBuilder = class {
constructor() {
/** @private {?Uint8Array} */
this.slice_ = null;
/** @private {number} */
this.len_ = 0;
/** @private {?ByteBuilder} */
this.child_ = null;
}
/**
* @return {!Uint8Array} The constructed bytes
*/
get data() {
if (this.child_ != null) {
throw Error('data access while child is pending');
}
if (this.slice_ === null) {
return new Uint8Array(0);
}
return this.slice_.subarray(0, this.len_);
}
/**
* Reallocates the slice to at least a given size.
* @param {number} minNewSize The minimum resulting size of the slice.
* @private
*/
realloc_(minNewSize) {
var newSize = 0;
if (minNewSize > Number.MAX_SAFE_INTEGER - minNewSize) {
// Cannot grow exponentially without overflow.
newSize = minNewSize;
} else {
newSize = minNewSize * 2;
}
if (this.slice_ === null) {
if (newSize < 128) {
newSize = 128;
}
this.slice_ = new Uint8Array(newSize);
return;
}
const newSlice = new Uint8Array(newSize);
for (var i = 0; i < this.len_; i++) {
newSlice[i] = this.slice_[i];
}
this.slice_ = newSlice;
}
/**
* Extends the current slice by the given number of bytes.
* @param {number} n The number of extra bytes needed in the slice.
* @return {number} The offset of the new bytes.
* @throws {Error}
* @private
*/
extend_(n) {
if (this.child_ != null) {
throw Error('write while child pending');
}
if (this.len_ > Number.MAX_SAFE_INTEGER - n) {
throw Error('length overflow');
}
if (this.slice_ === null || this.len_ + n > this.slice_.length) {
this.realloc_(this.len_ + n);
}
const offset = this.len_;
this.len_ += n;
return offset;
}
/**
* Appends a uint8 to the slice.
* @param {number} b The byte to append.
* @throws {Error}
* @private
*/
addU8_(b) {
const offset = this.extend_(1);
this.slice_[offset] = b;
}
/**
* Appends a length prefixed value to the slice.
* @param {number} lenLen The number of length-prefix bytes.
* @param {boolean} isASN1 True iff an ASN.1 length should be prefixed.
* @param {function(ByteBuilder)} k A function to construct the contents.
* @throws {Error}
* @private
*/
addLengthPrefixed_(lenLen, isASN1, k) {
var offset = this.extend_(lenLen);
var child = new ByteBuilder();
child.slice_ = this.slice_;
child.len_ = this.len_;
this.child_ = child;
k(child);
var length = child.len_ - lenLen - offset;
if (length > 0x7fffffff) {
// If a number larger than this is used with a shift operation in
// Javascript, the result is incorrect.
throw Error('length too large');
}
if (isASN1) {
// In the case of ASN.1 a single byte was reserved for
// the length. The contents of the array may need to be
// shifted along if the length needs more than that.
if (lenLen != 1) {
throw Error('internal error');
}
var lenByte = 0;
if (length > 0xffffff) {
lenLen = 5;
lenByte = 0x80 | 4;
} else if (length > 0xffff) {
lenLen = 4;
lenByte = 0x80 | 3;
} else if (length > 0xff) {
lenLen = 3;
lenByte = 0x80 | 2;
} else if (length > 0x7f) {
lenLen = 2;
lenByte = 0x80 | 1;
} else {
lenLen = 1;
lenByte = length;
length = 0;
}
child.slice_[offset] = lenByte;
const extraBytesNeeded = lenLen - 1;
if (extraBytesNeeded > 0) {
child.extend_(extraBytesNeeded);
child.slice_.copyWithin(offset + lenLen, offset + 1, child.len_);
}
offset++;
lenLen = extraBytesNeeded;
}
var l = length;
for (var i = lenLen - 1; i >= 0; i--) {
child.slice_[offset + i] = l;
l >>= 8;
}
if (l != 0) {
throw Error('pending child length exceeds reserved space');
}
this.slice_ = child.slice_;
this.len_ = child.len_;
this.child_ = null;
}
/**
* Appends an ASN.1 element to the slice.
* @param {number} tag The ASN.1 tag value (must be < 31).
* @param {function(ByteBuilder)} k A function to construct the contents.
* @throws {Error}
*/
addASN1(tag, k) {
if (tag > 255) {
throw Error('high-tag values not supported');
}
this.addU8_(tag);
this.addLengthPrefixed_(1, true, k);
}
/**
* Appends an ASN.1 INTEGER to the slice.
* @param {number} n The value of the integer. Must be within the range of an
* int32.
* @throws {Error}
*/
addASN1Int(n) {
if (n < (0x80000000 << 0) || n > 0x7fffffff) {
// Numbers this large (or small) cannot be correctly shifted in
// Javascript.
throw Error('integer out of encodable range');
}
var length = 1;
for (var nn = n; nn >= 0x80 || nn <= -0x80; nn >>= 8) {
length++;
}
this.addASN1(Tag.INTEGER, (b) => {
for (var i = length - 1; i >= 0; i--) {
b.addU8_((n >> (8 * i)) & 0xff);
}
});
}
/**
* Appends a non-negative ASN.1 INTEGER to the slice given its big-endian
* encoding. This can be useful when interacting with the WebCrypto API.
* @param {!Uint8Array} bytes The big-endian encoding of the integer.
* @throws {Error}
*/
addASN1BigInt(bytes) {
// Zero is representated as a single zero byte, rather than no bytes.
if (bytes.length == 0) {
bytes = new Uint8Array(1);
}
// Leading zero bytes need to be removed, unless that would make the number
// negative.
while (bytes.length >= 2 && bytes[0] == 0 && (bytes[1] & 0x80) == 0) {
bytes = bytes.slice(1);
}
// If the MSB is set, the number will be considered to be negative. Thus
// a zero prefix is needed in that case.
if (bytes.length > 0 && (bytes[0] & 0x80) == 0x80) {
if (bytes.length > Number.MAX_SAFE_INTEGER - 1) {
throw Error('bigint array too long');
}
var newBytes = new Uint8Array(bytes.length + 1);
newBytes.set(bytes, 1);
bytes = newBytes;
}
this.addASN1(Tag.INTEGER, (b) => b.addBytes(bytes));
}
/**
* Appends a base128-encoded integer to the slice.
* @param {number} n The value of the integer. Must be non-negative and within
* the range of an int32.
* @throws {Error}
* @private
*/
addBase128Int_(n) {
if (n < 0 || n > 0x7fffffff) {
// Cannot encode negative numbers and large numbers cannot be shifted in
// Javascript.
throw Error('integer out of encodable range');
}
var length = 0;
if (n == 0) {
length = 1;
} else {
for (var i = n; i > 0; i >>= 7) {
length++;
}
}
for (var i = length - 1; i >= 0; i--) {
var octet = 0x7f & (n >> (7 * i));
if (i != 0) {
octet |= 0x80;
}
this.addU8_(octet);
}
}
/**
* Appends an OBJECT IDENTIFIER to the slice.
* @param {Array<number>} oid The OID as a list of integer elements.
* @throws {Error}
*/
addASN1ObjectIdentifier(oid) {
if (oid.length < 2 || oid[0] > 2 || (oid[0] <= 1 && oid[1] >= 40)) {
throw Error('invalid OID');
}
this.addASN1(Tag.OBJECT, (b) => {
b.addBase128Int_(oid[0] * 40 + oid[1]);
for (var i = 2; i < oid.length; i++) {
b.addBase128Int_(oid[i]);
}
});
}
/**
* Appends an ASN.1 NULL to the slice.
* @throws {Error}
*/
addASN1Null() {
const offset = this.extend_(2);
this.slice_[offset] = Tag.NULL;
this.slice_[offset + 1] = 0;
}
/**
* Appends an ASN.1 PrintableString to the slice.
* @param {string} s The contents of the string.
* @throws {Error}
*/
addASN1PrintableString(s) {
var buf = new Uint8Array(s.length);
for (var i = 0; i < s.length; i++) {
const code = s.charCodeAt(i);
if ((code < 97 && code > 122) && // a-z
(code < 65 && code > 90) && // A-Z
' \'()+,-/:=?'.indexOf(String.fromCharCode(code)) == -1) {
throw Error(
'cannot encode \'' + String.fromCharCode(code) + '\' in' +
' PrintableString');
}
buf[i] = code;
}
this.addASN1(Tag.PrintableString, (b) => {
b.addBytes(buf);
});
}
/**
* Appends an ASN.1 UTF8String to the slice.
* @param {string} s The contents of the string.
* @throws {Error}
*/
addASN1UTF8String(s) {
this.addASN1(Tag.UTF8String, (b) => {
b.addBytes((new TextEncoder()).encode(s));
});
}
/**
* Appends an ASN.1 BIT STRING to the slice.
* @param {!Uint8Array} bytes The contents, which must be a whole number of
* bytes.
* @throws {Error}
*/
addASN1BitString(bytes) {
this.addASN1(Tag.BITSTRING, (b) => {
b.addU8_(0); // no superfluous bits in encoding.
b.addBytes(bytes);
});
}
/**
* Appends raw data to the slice.
* @param {string} s The contents to append. All character values must
* be < 256.
* @throws {Error}
*/
addBytesFromString(s) {
const buf = new Uint8Array(s.length);
for (var i = 0; i < s.length; i++) {
const code = s.charCodeAt(i);
if (code > 255) {
throw Error('out-of-range character in string of bytes');
}
buf[i] = code;
}
this.addBytes(buf);
}
/**
* Appends raw bytes to the slice.
* @param {!Array<number>|!Uint8Array} bytes Data to append.
* @throws {Error}
*/
addBytes(bytes) {
const offset = this.extend_(bytes.length);
for (var i = 0; i < bytes.length; i++) {
this.slice_[offset + i] = bytes[i];
}
}
};
...@@ -8,6 +8,342 @@ ...@@ -8,6 +8,342 @@
'use strict'; 'use strict';
/**
* webSafeBase64ToNormal reencodes a base64-encoded string.
*
* @param {string} s A string encoded as web-safe base64.
* @return {string} A string encoded in normal base64.
*/
function webSafeBase64ToNormal(s) {
return s.replace(/-/g, '+').replace(/_/g, '/');
}
/**
* decodeWebSafeBase64ToArray decodes a base64-encoded string.
*
* @param {string} s A base64-encoded string.
* @return {!Uint8Array}
*/
function decodeWebSafeBase64ToArray(s) {
var bytes = atob(webSafeBase64ToNormal(s));
var buffer = new ArrayBuffer(bytes.length);
var ret = new Uint8Array(buffer);
for (var i = 0; i < bytes.length; i++) {
ret[i] = bytes.charCodeAt(i);
}
return ret;
}
// See "FIDO U2F Authenticator Transports Extension", §3.2.1.
const transportTypeOID = [1, 3, 6, 1, 4, 1, 45724, 2, 1, 1];
/**
* Returns the value of the transport-type X.509 extension from the supplied
* attestation certificate, or 0.
*
* @param {!Uint8Array} der The DER bytes of an attestation certificate.
* @returns {Uint8Array} the bytes of the transport-type extension, if present,
* or null.
* @throws {Error}
*/
function transportType(der) {
var topLevel = new ByteString(der);
const tbsCert = topLevel.getASN1(Tag.SEQUENCE).getASN1(Tag.SEQUENCE);
tbsCert.getOptionalASN1(
Tag.CONSTRUCTED | Tag.CONTEXT_SPECIFIC | 0); // version
tbsCert.getASN1(Tag.INTEGER); // serialNumber
tbsCert.getASN1(Tag.SEQUENCE); // signature algorithm
tbsCert.getASN1(Tag.SEQUENCE); // issuer
tbsCert.getASN1(Tag.SEQUENCE); // validity
tbsCert.getASN1(Tag.SEQUENCE); // subject
tbsCert.getASN1(Tag.SEQUENCE); // SPKI
tbsCert.getOptionalASN1( // issuerUniqueID
Tag.CONSTRUCTED | Tag.CONTEXT_SPECIFIC | 1);
tbsCert.getOptionalASN1( // subjectUniqueID
Tag.CONSTRUCTED | Tag.CONTEXT_SPECIFIC | 2);
const outerExtensions =
tbsCert.getOptionalASN1(Tag.CONSTRUCTED | Tag.CONTEXT_SPECIFIC | 3);
if (outerExtensions == null) {
return null;
}
const extensions = outerExtensions.getASN1(Tag.SEQUENCE);
if (extensions.empty) {
return null;
}
while (!extensions.empty) {
const extension = extensions.getASN1(Tag.SEQUENCE);
const oid = extension.getASN1ObjectIdentifier();
if (oid.length != transportTypeOID.length) {
continue;
}
var matches = true;
for (var i = 0; i < oid.length; i++) {
if (oid[i] != transportTypeOID[i]) {
matches = false;
break;
}
}
if (!matches) {
continue;
}
extension.getOptionalASN1(Tag.BOOLEAN); // 'critical' flag
const contents = extension.getASN1(Tag.OCTETSTRING);
if (!extension.empty) {
throw Error('trailing garbage after extension');
}
return contents.getASN1(Tag.BITSTRING).data;
}
return null;
}
/**
* 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.
* @return {Promise<{privateKey: !webCrypto.CryptoKey, certDER: !Uint8Array}>}
*/
async function makeCertAndKey(original) {
var transport = transportType(original);
if (transport !== null) {
if (transport.length != 2) {
throw Error('bad extension length');
}
if (transport[0] < 3) {
throw Error('too many bits set'); // Only 5 bits are defined.
}
}
const keyalg = {name: 'ECDSA', namedCurve: 'P-256'};
const keypair =
await crypto.subtle.generateKey(keyalg, true, ['sign', 'verify']);
const publicKey = await crypto.subtle.exportKey('raw', keypair.publicKey);
var serialBuffer = new ArrayBuffer(10);
var serial = new Uint8Array(serialBuffer);
crypto.getRandomValues(serial);
const ecdsaWithSHA256 = [1, 2, 840, 10045, 4, 3, 2];
const ansiX962 = [1, 2, 840, 10045, 2, 1];
const secp256R1 = [1, 2, 840, 10045, 3, 1, 7];
const commonName = [2, 5, 4, 3];
const x509V3 = 2;
const certBuilder = new ByteBuilder();
certBuilder.addASN1(Tag.SEQUENCE, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => { // TBSCertificate
b.addASN1(Tag.CONTEXT_SPECIFIC | Tag.CONSTRUCTED | 0, (b) => {
b.addASN1Int(x509V3); // Version
});
b.addASN1BigInt(serial); // Serial number
b.addASN1(Tag.SEQUENCE, (b) => { // Signature algorithm
b.addASN1ObjectIdentifier(ecdsaWithSHA256);
});
b.addASN1(Tag.SEQUENCE, (b) => { // Issuer
b.addASN1(Tag.SET, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => {
b.addASN1ObjectIdentifier(commonName);
b.addASN1PrintableString('U2F');
});
});
});
b.addASN1(Tag.SEQUENCE, (b) => { // Validity
b.addASN1(Tag.UTCTime, (b) => {
b.addBytesFromString('0001010000Z');
});
b.addASN1(Tag.UTCTime, (b) => {
b.addBytesFromString('0001010000Z');
});
});
b.addASN1(Tag.SEQUENCE, (b) => { // Subject
b.addASN1(Tag.SET, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => {
b.addASN1ObjectIdentifier(commonName);
b.addASN1PrintableString('U2F');
});
});
});
b.addASN1(Tag.SEQUENCE, (b) => { // Public key
b.addASN1(Tag.SEQUENCE, (b) => { // Algorithm identifier
b.addASN1ObjectIdentifier(ansiX962);
b.addASN1ObjectIdentifier(secp256R1);
});
b.addASN1BitString(new Uint8Array(publicKey));
});
if (transport !== null) {
var t = transport; // This causes the compiler to see t cannot be null.
// Extensions
b.addASN1(Tag.CONTEXT_SPECIFIC | Tag.CONSTRUCTED | 3, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => { // Transport-type extension.
b.addASN1ObjectIdentifier(transportTypeOID);
b.addASN1(Tag.OCTETSTRING, (b) => {
b.addASN1(Tag.BITSTRING, (b) => {
b.addBytes(t);
});
});
});
});
});
}
});
b.addASN1(Tag.SEQUENCE, (b) => { // Algorithm identifier
b.addASN1ObjectIdentifier(ecdsaWithSHA256);
});
b.addASN1(Tag.BITSTRING, (b) => { // Signature
b.addBytesFromString('\x00'); // (not valid, obviously.)
});
});
return {privateKey: keypair.privateKey, certDER: certBuilder.data};
}
/**
* Registration encodes a registration response success message. See "FIDO U2F
* Raw Message Formats" (§4.3).
*/
const Registration = class {
/**
* @param {string} registrationData the registration response message,
* base64-encoded.
* @param {string} appId the application identifier.
* @param {string=} opt_clientData the client data, base64-encoded. This
* field is not really optional; it is an error if it is empty or missing.
* @throws {Error}
*/
constructor(registrationData, appId, opt_clientData) {
var data = new ByteString(decodeWebSafeBase64ToArray(registrationData));
var magic = data.getBytes(1);
if (magic[0] != 5) {
throw Error('bad magic number');
}
/** @private {!Uint8Array} */
this.publicKey_ = data.getBytes(65);
/** @private {!Uint8Array} */
this.keyHandleLen_ = data.getBytes(1);
/** @private {!Uint8Array} */
this.keyHandle_ = data.getBytes(this.keyHandleLen_[0]);
/** @private {!Uint8Array} */
this.certificate_ = data.getASN1Element(Tag.SEQUENCE).data;
/** @private {!Uint8Array} */
this.signature_ = data.getASN1Element(Tag.SEQUENCE).data;
if (!data.empty) {
throw Error('extra trailing bytes');
}
if (!opt_clientData) {
throw Error('missing client data');
}
/** @private {string} */
this.clientData_ = atob(webSafeBase64ToNormal(opt_clientData));
JSON.parse(this.clientData_); // Just checking.
/** @private {string} */
this.appId_ = appId;
}
/** @return {!Uint8Array} the attestation certificate, DER-encoded. */
get certificate() {
return this.certificate_;
}
/** @return {!Uint8Array} the attestation signature, DER-encoded. */
get signature() {
return this.signature_;
}
/**
* toBeSigned marshals the parts of a registration that are signed by the
* attestation key, however obtained.
*
* @return {!Uint8Array} data to be signed.
*/
toBeSigned() {
var tbs = new ByteBuilder();
tbs.addBytesFromString('\0');
tbs.addBytes(sha256HashOfString(this.appId_));
tbs.addBytes(sha256HashOfString(this.clientData_));
tbs.addBytes(this.keyHandle_);
tbs.addBytes(this.publicKey_);
return tbs.data;
}
/**
* sign signs data from the registration (see toBeSigned()) using the supplied
* private key. This is used in |RANDOMIZE| mode.
*
* @param {!webCrypto.CryptoKey} key ECDSA P-256 signing key in WebCrypto
* format
* @return {Promise<!Uint8Array>} ASN.1 DER encoded ECDSA signature.
*/
async sign(key) {
const algo = {name: 'ECDSA', hash: {name: 'SHA-256'}};
var signatureBuf = await crypto.subtle.sign(algo, key, this.toBeSigned());
var signatureRaw = new ByteString(new Uint8Array(signatureBuf));
var signatureASN1 = new ByteBuilder();
signatureASN1.addASN1(Tag.SEQUENCE, (b) => {
// The P-256 signature from WebCrypto is a pair of 32-byte, big-endian
// values concatenated.
b.addASN1BigInt(signatureRaw.getBytes(32));
b.addASN1BigInt(signatureRaw.getBytes(32));
});
return signatureASN1.data;
}
/**
* withReplacement marshals the registration (to base64) with the certificate
* and signature replaced.
*
* @param {!Uint8Array} certificate new certificate, as DER.
* @param {!Uint8Array} signature new signature, as DER.
* @return {string} The supplied registration data with certificate and
* signature replaced, base64.
*/
withReplacement(certificate, signature) {
var result = new ByteBuilder();
result.addBytesFromString('\x05');
result.addBytes(this.publicKey_);
result.addBytes(this.keyHandleLen_);
result.addBytes(this.keyHandle_);
result.addBytes(certificate);
result.addBytes(signature);
return B64_encode(result.data);
}
};
/**
* ConveyancePreference describes how to alter (if at all) the attestation
* certificate in a registration response.
* @enum
*/
var ConveyancePreference = {
/**
* NONE means that the token's attestation certificate should be replaced with
* a randomly generated one, and that response should be re-signed using a
* corresponding key.
*/
NONE: 1,
/**
* DIRECT means that the token's attestation cert should be returned unchanged
* to the relying party.
*/
DIRECT: 0,
};
/**
* conveyancePreference returns the attestation certificate replacement mode.
*
* @param {EnrollChallenge} enrollChallenge
* @return {ConveyancePreference}
*/
function conveyancePreference(enrollChallenge) {
if (enrollChallenge.hasOwnProperty('attestation') &&
enrollChallenge['attestation'] == 'none') {
return ConveyancePreference.NONE;
}
return ConveyancePreference.DIRECT;
}
/** /**
* Handles a U2F enroll request. * Handles a U2F enroll request.
* @param {MessageSender} messageSender The message sender. * @param {MessageSender} messageSender The message sender.
...@@ -26,18 +362,50 @@ function handleU2fEnrollRequest(messageSender, request, sendResponse) { ...@@ -26,18 +362,50 @@ function handleU2fEnrollRequest(messageSender, request, sendResponse) {
sendResponseOnce(sentResponse, closeable, response, sendResponse); sendResponseOnce(sentResponse, closeable, response, sendResponse);
} }
function sendSuccessResponse(u2fVersion, info, clientData) { /**
* @param {string} u2fVersion
* @param {string} registrationData Registration data, base64
* @param {string=} opt_clientData Base64.
*/
function sendSuccessResponse(u2fVersion, registrationData, opt_clientData) {
var enrollChallenges = request['registerRequests']; var enrollChallenges = request['registerRequests'];
var enrollChallenge = var enrollChallengeOrNull =
findEnrollChallengeOfVersion(enrollChallenges, u2fVersion); findEnrollChallengeOfVersion(enrollChallenges, u2fVersion);
if (!enrollChallenge) { if (!enrollChallengeOrNull) {
sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR}); sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
return; return;
} }
var responseData = var enrollChallenge = enrollChallengeOrNull; // Avoids compiler warning.
makeEnrollResponseData(enrollChallenge, u2fVersion, info, clientData); var appId = request['appId'];
if (enrollChallenge.hasOwnProperty('appId')) {
appId = enrollChallenge['appId'];
}
var promise = Promise.resolve(registrationData);
switch (conveyancePreference(enrollChallenge)) {
case ConveyancePreference.NONE: {
console.log('randomizing attestation certificate');
promise = new Promise(async function(resolve, reject) {
const reg = new Registration(registrationData, appId, opt_clientData);
const keypair = await makeCertAndKey(reg.certificate);
const signature = await reg.sign(keypair.privateKey);
resolve(reg.withReplacement(keypair.certDER, signature));
});
break;
}
}
promise.then(
(registrationData) => {
var responseData = makeEnrollResponseData(
enrollChallenge, u2fVersion, registrationData, opt_clientData);
var response = makeU2fSuccessResponse(request, responseData); var response = makeU2fSuccessResponse(request, responseData);
sendResponseOnce(sentResponse, closeable, response, sendResponse); sendResponseOnce(sentResponse, closeable, response, sendResponse);
},
(err) => {
console.warn('attestation certificate replacement failed: ' + err);
sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR});
});
} }
function timeout() { function timeout() {
...@@ -69,6 +437,7 @@ function handleU2fEnrollRequest(messageSender, request, sendResponse) { ...@@ -69,6 +437,7 @@ function handleU2fEnrollRequest(messageSender, request, sendResponse) {
new WatchdogRequestHandler(watchdogTimeoutValueSeconds, timeout); new WatchdogRequestHandler(watchdogTimeoutValueSeconds, timeout);
var wrappedErrorCb = watchdog.wrapCallback(sendErrorResponse); var wrappedErrorCb = watchdog.wrapCallback(sendErrorResponse);
var wrappedSuccessCb = watchdog.wrapCallback(sendSuccessResponse); var wrappedSuccessCb = watchdog.wrapCallback(sendSuccessResponse);
// TODO: Fix unused; intended to pass wrapped callbacks to Enroller?
var timer = createAttenuatedTimer( var timer = createAttenuatedTimer(
FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds); FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
...@@ -262,7 +631,7 @@ function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) { ...@@ -262,7 +631,7 @@ function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) {
/** @private {boolean} */ /** @private {boolean} */
this.allowHttp_ = this.allowHttp_ =
this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false; this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false;
/** @private {Closeable} */ /** @private {RequestHandler} */
this.handler_ = null; this.handler_ = null;
} }
......
...@@ -237,7 +237,7 @@ Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS = 30000; ...@@ -237,7 +237,7 @@ Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS = 30000;
Gnubbies.SYS_TIMER_ = new WindowTimer(); Gnubbies.SYS_TIMER_ = new WindowTimer();
/** /**
* @param {number|undefined} opt_timeoutMillis Timeout in milliseconds * @param {number=} opt_timeoutMillis Timeout in milliseconds
*/ */
Gnubbies.prototype.resetInactivityTimer = function(opt_timeoutMillis) { Gnubbies.prototype.resetInactivityTimer = function(opt_timeoutMillis) {
var millis = opt_timeoutMillis ? var millis = opt_timeoutMillis ?
......
...@@ -399,7 +399,7 @@ HidGnubbyDevice.prototype.writePump_ = function() { ...@@ -399,7 +399,7 @@ HidGnubbyDevice.prototype.writePump_ = function() {
var frame = this.txqueue[0]; var frame = this.txqueue[0];
var self = this; var self = this;
function transferComplete() { var transferComplete = function() {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.log(UTIL_fmt('send got lastError:')); console.log(UTIL_fmt('send got lastError:'));
console.log(UTIL_fmt(chrome.runtime.lastError.message)); console.log(UTIL_fmt(chrome.runtime.lastError.message));
...@@ -414,7 +414,7 @@ HidGnubbyDevice.prototype.writePump_ = function() { ...@@ -414,7 +414,7 @@ HidGnubbyDevice.prototype.writePump_ = function() {
self.writePump_(); self.writePump_();
}, 0); }, 0);
} }
} };
var u8 = new Uint8Array(frame); var u8 = new Uint8Array(frame);
......
...@@ -48,6 +48,7 @@ ...@@ -48,6 +48,7 @@
"factoryregistry.js", "factoryregistry.js",
"closeable.js", "closeable.js",
"requesthelper.js", "requesthelper.js",
"asn1.js",
"enroller.js", "enroller.js",
"requestqueue.js", "requestqueue.js",
"signer.js", "signer.js",
......
...@@ -55,9 +55,9 @@ SHA256.prototype._compress = function(buf) { ...@@ -55,9 +55,9 @@ SHA256.prototype._compress = function(buf) {
var W = this._W; var W = this._W;
var k = this._k; var k = this._k;
function _rotr(w, r) { var _rotr = function(w, r) {
return ((w << (32 - r)) | (w >>> r)); return ((w << (32 - r)) | (w >>> r));
} };
// get 16 big endian words // get 16 big endian words
for (var i = 0; i < 64; i += 4) { for (var i = 0; i < 64; i += 4) {
...@@ -147,7 +147,7 @@ SHA256.prototype.updateRange = function(bytes, start, end) { ...@@ -147,7 +147,7 @@ SHA256.prototype.updateRange = function(bytes, start, end) {
* Optionally update the hash with additional arguments, and return the * Optionally update the hash with additional arguments, and return the
* resulting hash value. * resulting hash value.
* @param {...*} var_args Data buffers to hash * @param {...*} var_args Data buffers to hash
* @return {Array<number>} the SHA256 hash value. * @return {!Array<number>} the SHA256 hash value.
*/ */
SHA256.prototype.digest = function(var_args) { SHA256.prototype.digest = function(var_args) {
for (var i = 0; i < arguments.length; ++i) for (var i = 0; i < arguments.length; ++i)
......
...@@ -324,7 +324,7 @@ function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) { ...@@ -324,7 +324,7 @@ function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) {
/** @private {boolean} */ /** @private {boolean} */
this.allowHttp_ = this.allowHttp_ =
this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false; this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false;
/** @private {Closeable} */ /** @private {RequestHandler} */
this.handler_ = null; this.handler_ = null;
} }
...@@ -542,10 +542,9 @@ Signer.prototype.helperComplete_ = function(helperReply, opt_source) { ...@@ -542,10 +542,9 @@ Signer.prototype.helperComplete_ = function(helperReply, opt_source) {
'helper reported ' + reply.code.toString(16) + ', returning ' + 'helper reported ' + reply.code.toString(16) + ', returning ' +
reportedError.errorCode)); reportedError.errorCode));
// Log non-expected reply codes if we have an url to send them // Log non-expected reply codes if we have an url to send them
if (reportedError.errorCode == ErrorCodes.OTHER_ERROR) { if ((reportedError.errorCode == ErrorCodes.OTHER_ERROR) &&
var logMsg = 'log=u2fsign&rc=' + reply.code.toString(16); this.logMsgUrl_) {
if (this.logMsgUrl_) logMessage('log=u2fsign&rc=' + reply.code.toString(16), this.logMsgUrl_);
logMessage(logMsg, this.logMsgUrl_);
} }
this.notifyError_(reportedError); this.notifyError_(reportedError);
} else { } else {
......
...@@ -276,7 +276,7 @@ UsbGnubbyDevice.prototype.writeOneRequest_ = function() { ...@@ -276,7 +276,7 @@ UsbGnubbyDevice.prototype.writeOneRequest_ = function() {
var frame = this.txqueue[0]; var frame = this.txqueue[0];
var self = this; var self = this;
function OutTransferComplete(x) { var OutTransferComplete = function(x) {
self.outTransferPending = false; self.outTransferPending = false;
if (!self.readyToUse_()) if (!self.readyToUse_())
...@@ -294,7 +294,7 @@ UsbGnubbyDevice.prototype.writeOneRequest_ = function() { ...@@ -294,7 +294,7 @@ UsbGnubbyDevice.prototype.writeOneRequest_ = function() {
window.setTimeout(function() { window.setTimeout(function() {
self.readOneReply_(); self.readOneReply_();
}, 0); }, 0);
} };
var u8 = new Uint8Array(frame); var u8 = new Uint8Array(frame);
......
...@@ -243,7 +243,7 @@ function sendResponseOnce(sentResponse, closeable, response, sendResponse) { ...@@ -243,7 +243,7 @@ function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
/** /**
* @param {!string} string Input string * @param {!string} string Input string
* @return {Array<number>} SHA256 hash value of string. * @return {!Array<number>} SHA256 hash value of string.
*/ */
function sha256HashOfString(string) { function sha256HashOfString(string) {
var s = new SHA256(); var s = new SHA256();
......
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