340 lines
10 KiB
JavaScript
340 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
// Encodes |data| into base64url string. There is no '=' padding, and the
|
|
// characters '-' and '_' must be used instead of '+' and '/', respectively.
|
|
function base64urlEncode(data) {
|
|
let result = btoa(data);
|
|
return result.replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_");
|
|
}
|
|
|
|
// Decode |encoded| using base64url decoding.
|
|
function base64urlDecode(encoded) {
|
|
return atob(encoded.replace(/\-/g, "+").replace(/\_/g, "/"));
|
|
}
|
|
|
|
// Encodes a Uint8Array as a base64url string.
|
|
function uint8ArrayToBase64url(array) {
|
|
return base64urlEncode(String.fromCharCode.apply(null, array));
|
|
}
|
|
|
|
// Encodes a Uint8Array to lowercase hex.
|
|
function uint8ArrayToHex(array) {
|
|
const hexTable = '0123456789abcdef';
|
|
let s = '';
|
|
for (let i = 0; i < array.length; i++) {
|
|
s += hexTable.charAt(array[i] >> 4);
|
|
s += hexTable.charAt(array[i] & 15);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// Convert a EC signature from DER to a concatenation of the r and s parameters,
|
|
// as expected by the subtle crypto API.
|
|
function convertDERSignatureToSubtle(der) {
|
|
let index = -1;
|
|
const SEQUENCE = 0x30;
|
|
const INTEGER = 0x02;
|
|
assert_equals(der[++index], SEQUENCE);
|
|
|
|
let size = der[++index];
|
|
assert_equals(size + 2, der.length);
|
|
|
|
assert_equals(der[++index], INTEGER);
|
|
let rSize = der[++index];
|
|
++index;
|
|
while (der[index] == 0) {
|
|
++index;
|
|
--rSize;
|
|
}
|
|
let r = der.slice(index, index + rSize);
|
|
index += rSize;
|
|
|
|
assert_equals(der[index], INTEGER);
|
|
let sSize = der[++index];
|
|
++index;
|
|
while (der[index] == 0) {
|
|
++index;
|
|
--sSize;
|
|
}
|
|
let s = der.slice(index, index + sSize);
|
|
assert_equals(index + sSize, der.length);
|
|
|
|
let result = new Uint8Array(64);
|
|
result.set(r, 32 - rSize);
|
|
result.set(s, 64 - sSize);
|
|
return result;
|
|
};
|
|
|
|
function coseObjectToJWK(cose) {
|
|
// Convert an object representing a COSE_Key encoded public key into a JSON
|
|
// Web Key object.
|
|
// https://tools.ietf.org/html/rfc7517
|
|
|
|
// The example used on the test is a ES256 key, so we only implement that.
|
|
let jwk = {};
|
|
if (cose.type != 2)
|
|
assert_unreached("Unknown type: " + cose.type);
|
|
|
|
jwk.kty = "EC";
|
|
if (cose.alg != ES256_ID)
|
|
assert_unreached("Unknown alg: " + cose.alg);
|
|
|
|
if (cose.crv != 1)
|
|
assert_unreached("Unknown curve: " + jwk.crv);
|
|
|
|
jwk.crv = "P-256";
|
|
jwk.x = uint8ArrayToBase64url(cose.x);
|
|
jwk.y = uint8ArrayToBase64url(cose.y);
|
|
return jwk;
|
|
}
|
|
|
|
function parseCosePublicKey(coseKey) {
|
|
// Parse a CTAP2 canonical CBOR encoding form key.
|
|
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#ctap2-canonical-cbor-encoding-form
|
|
let parsed = new Cbor(coseKey);
|
|
let cbor = parsed.getCBOR();
|
|
let key = {
|
|
type: cbor[1],
|
|
alg: cbor[3],
|
|
};
|
|
if (key.type != 2)
|
|
assert_unreached("Unknown key type: " + key.type);
|
|
|
|
key.crv = cbor[-1];
|
|
key.x = new Uint8Array(cbor[-2]);
|
|
key.y = new Uint8Array(cbor[-3]);
|
|
return key;
|
|
}
|
|
|
|
function parseAttestedCredentialData(attestedCredentialData) {
|
|
// Parse the attested credential data according to
|
|
// https://w3c.github.io/webauthn/#attested-credential-data
|
|
let aaguid = attestedCredentialData.slice(0, 16);
|
|
let credentialIdLength = (attestedCredentialData[16] << 8)
|
|
+ attestedCredentialData[17];
|
|
let credentialId =
|
|
attestedCredentialData.slice(18, 18 + credentialIdLength);
|
|
let credentialPublicKey = parseCosePublicKey(
|
|
attestedCredentialData.slice(18 + credentialIdLength,
|
|
attestedCredentialData.length));
|
|
|
|
return { aaguid, credentialIdLength, credentialId, credentialPublicKey };
|
|
}
|
|
|
|
function parseAuthenticatorData(authenticatorData) {
|
|
// Parse the authenticator data according to
|
|
// https://w3c.github.io/webauthn/#sctn-authenticator-data
|
|
assert_greater_than_equal(authenticatorData.length, 37);
|
|
let flags = authenticatorData[32];
|
|
let counter = authenticatorData.slice(33, 37);
|
|
|
|
let attestedCredentialData = authenticatorData.length > 37 ?
|
|
parseAttestedCredentialData(authenticatorData.slice(37)) : null;
|
|
let extensions = null;
|
|
if (attestedCredentialData &&
|
|
authenticatorData.length > 37 + attestedCredentialData.length) {
|
|
extensions = authenticatorData.slice(37 + attestedCredentialData.length);
|
|
}
|
|
|
|
return {
|
|
rpIdHash: authenticatorData.slice(0, 32),
|
|
flags: {
|
|
up: !!(flags & 0x01),
|
|
uv: !!(flags & 0x04),
|
|
at: !!(flags & 0x40),
|
|
ed: !!(flags & 0x80),
|
|
},
|
|
counter: (counter[0] << 24)
|
|
+ (counter[1] << 16)
|
|
+ (counter[2] << 8)
|
|
+ counter[3],
|
|
attestedCredentialData,
|
|
extensions,
|
|
};
|
|
}
|
|
|
|
// Taken from
|
|
// https://cs.chromium.org/chromium/src/chrome/browser/resources/cryptotoken/cbor.js?rcl=c9b6055cf9c158fb4119afd561a591f8fc95aefe
|
|
class Cbor {
|
|
constructor(buffer) {
|
|
this.slice = new Uint8Array(buffer);
|
|
}
|
|
get data() {
|
|
return this.slice;
|
|
}
|
|
get length() {
|
|
return this.slice.length;
|
|
}
|
|
get empty() {
|
|
return this.slice.length == 0;
|
|
}
|
|
get hex() {
|
|
return uint8ArrayToHex(this.data);
|
|
}
|
|
compare(other) {
|
|
if (this.length < other.length) {
|
|
return -1;
|
|
} else if (this.length > other.length) {
|
|
return 1;
|
|
}
|
|
for (let i = 0; i < this.length; i++) {
|
|
if (this.slice[i] < other.slice[i]) {
|
|
return -1;
|
|
} else if (this.slice[i] > other.slice[i]) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
getU8() {
|
|
if (this.empty) {
|
|
throw('Cbor: empty during getU8');
|
|
}
|
|
const byte = this.slice[0];
|
|
this.slice = this.slice.subarray(1);
|
|
return byte;
|
|
}
|
|
skip(n) {
|
|
if (this.length < n) {
|
|
throw('Cbor: too few bytes to skip');
|
|
}
|
|
this.slice = this.slice.subarray(n);
|
|
}
|
|
getBytes(n) {
|
|
if (this.length < n) {
|
|
throw('Cbor: insufficient bytes in getBytes');
|
|
}
|
|
const ret = this.slice.subarray(0, n);
|
|
this.slice = this.slice.subarray(n);
|
|
return ret;
|
|
}
|
|
getCBORHeader() {
|
|
const copy = new Cbor(this.slice);
|
|
const a = this.getU8();
|
|
const majorType = a >> 5;
|
|
const info = a & 31;
|
|
if (info < 24) {
|
|
return [majorType, info, new Cbor(copy.getBytes(1))];
|
|
} else if (info < 28) {
|
|
const lengthLength = 1 << (info - 24);
|
|
let data = this.getBytes(lengthLength);
|
|
let value = 0;
|
|
for (let i = 0; i < lengthLength; i++) {
|
|
// Javascript has problems handling uint64s given the limited range of
|
|
// a double.
|
|
if (value > 35184372088831) {
|
|
throw('Cbor: cannot represent CBOR number');
|
|
}
|
|
// Not using bitwise operations to avoid truncating to 32 bits.
|
|
value *= 256;
|
|
value += data[i];
|
|
}
|
|
switch (lengthLength) {
|
|
case 1:
|
|
if (value < 24) {
|
|
throw('Cbor: value should have been encoded in single byte');
|
|
}
|
|
break;
|
|
case 2:
|
|
if (value < 256) {
|
|
throw('Cbor: non-minimal integer');
|
|
}
|
|
break;
|
|
case 4:
|
|
if (value < 65536) {
|
|
throw('Cbor: non-minimal integer');
|
|
}
|
|
break;
|
|
case 8:
|
|
if (value < 4294967296) {
|
|
throw('Cbor: non-minimal integer');
|
|
}
|
|
break;
|
|
}
|
|
return [majorType, value, new Cbor(copy.getBytes(1 + lengthLength))];
|
|
} else {
|
|
throw('Cbor: CBOR contains unhandled info value ' + info);
|
|
}
|
|
}
|
|
getCBOR() {
|
|
const [major, value] = this.getCBORHeader();
|
|
switch (major) {
|
|
case 0:
|
|
return value;
|
|
case 1:
|
|
return 0 - (1 + value);
|
|
case 2:
|
|
return this.getBytes(value);
|
|
case 3:
|
|
return this.getBytes(value);
|
|
case 4: {
|
|
let ret = new Array(value);
|
|
for (let i = 0; i < value; i++) {
|
|
ret[i] = this.getCBOR();
|
|
}
|
|
return ret;
|
|
}
|
|
case 5:
|
|
if (value == 0) {
|
|
return {};
|
|
}
|
|
let copy = new Cbor(this.data);
|
|
const [firstKeyMajor] = copy.getCBORHeader();
|
|
if (firstKeyMajor == 3) {
|
|
// String-keyed map.
|
|
let lastKeyHeader = new Cbor(new Uint8Array(0));
|
|
let lastKeyBytes = new Cbor(new Uint8Array(0));
|
|
let ret = {};
|
|
for (let i = 0; i < value; i++) {
|
|
const [keyMajor, keyLength, keyHeader] = this.getCBORHeader();
|
|
if (keyMajor != 3) {
|
|
throw('Cbor: non-string in string-valued map');
|
|
}
|
|
const keyBytes = new Cbor(this.getBytes(keyLength));
|
|
if (i > 0) {
|
|
const headerCmp = lastKeyHeader.compare(keyHeader);
|
|
if (headerCmp > 0 ||
|
|
(headerCmp == 0 && lastKeyBytes.compare(keyBytes) >= 0)) {
|
|
throw(
|
|
'Cbor: map keys in wrong order: ' + lastKeyHeader.hex +
|
|
'/' + lastKeyBytes.hex + ' ' + keyHeader.hex + '/' +
|
|
keyBytes.hex);
|
|
}
|
|
}
|
|
lastKeyHeader = keyHeader;
|
|
lastKeyBytes = keyBytes;
|
|
ret[keyBytes.parseUTF8()] = this.getCBOR();
|
|
}
|
|
return ret;
|
|
} else if (firstKeyMajor == 0 || firstKeyMajor == 1) {
|
|
// Number-keyed map.
|
|
let lastKeyHeader = new Cbor(new Uint8Array(0));
|
|
let ret = {};
|
|
for (let i = 0; i < value; i++) {
|
|
let [keyMajor, keyValue, keyHeader] = this.getCBORHeader();
|
|
if (keyMajor != 0 && keyMajor != 1) {
|
|
throw('Cbor: non-number in number-valued map');
|
|
}
|
|
if (i > 0 && lastKeyHeader.compare(keyHeader) >= 0) {
|
|
throw(
|
|
'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + ' ' +
|
|
keyHeader.hex);
|
|
}
|
|
lastKeyHeader = keyHeader;
|
|
if (keyMajor == 1) {
|
|
keyValue = 0 - (1 + keyValue);
|
|
}
|
|
ret[keyValue] = this.getCBOR();
|
|
}
|
|
return ret;
|
|
} else {
|
|
throw('Cbor: map keyed by invalid major type ' + firstKeyMajor);
|
|
}
|
|
default:
|
|
throw('Cbor: unhandled major type ' + major);
|
|
}
|
|
}
|
|
parseUTF8() {
|
|
return (new TextDecoder('utf-8')).decode(this.slice);
|
|
}
|
|
}
|