summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webauthn/resources/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webauthn/resources/utils.js')
-rw-r--r--testing/web-platform/tests/webauthn/resources/utils.js340
1 files changed, 340 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webauthn/resources/utils.js b/testing/web-platform/tests/webauthn/resources/utils.js
new file mode 100644
index 0000000000..50c39605a1
--- /dev/null
+++ b/testing/web-platform/tests/webauthn/resources/utils.js
@@ -0,0 +1,340 @@
+"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);
+ }
+}