gecko-dev/dom/webauthn/tests/u2futil.js

314 строки
9.3 KiB
JavaScript

// Used by local_addTest() / local_completeTest()
var _countCompletions = 0;
var _expectedCompletions = 0;
const flag_TUP = 0x01;
const flag_UV = 0x04;
const flag_AT = 0x40;
const cose_alg_ECDSA_w_SHA256 = -7;
const cose_alg_ECDSA_w_SHA512 = -36;
function handleEventMessage(event) {
if ("test" in event.data) {
let summary = event.data.test + ": " + event.data.msg;
log(event.data.status + ": " + summary);
ok(event.data.status, summary);
} else if ("done" in event.data) {
SimpleTest.finish();
} else {
ok(false, "Unexpected message in the test harness: " + event.data)
}
}
function log(msg) {
console.log(msg)
let logBox = document.getElementById("log");
if (logBox) {
logBox.textContent += "\n" + msg;
}
}
function local_is(value, expected, message) {
if (value === expected) {
local_ok(true, message);
} else {
local_ok(false, message + " unexpectedly: " + value + " !== " + expected);
}
}
function local_isnot(value, expected, message) {
if (value !== expected) {
local_ok(true, message);
} else {
local_ok(false, message + " unexpectedly: " + value + " === " + expected);
}
}
function local_ok(expression, message) {
let body = {"test": this.location.pathname, "status":expression, "msg": message}
parent.postMessage(body, "http://mochi.test:8888");
}
function local_doesThrow(fn, name) {
let gotException = false;
try {
fn();
} catch (ex) { gotException = true; }
local_ok(gotException, name);
};
function local_expectThisManyTests(count) {
if (_expectedCompletions > 0) {
local_ok(false, "Error: local_expectThisManyTests should only be called once.");
}
_expectedCompletions = count;
}
function local_completeTest() {
_countCompletions += 1
if (_countCompletions == _expectedCompletions) {
log("All tests completed.")
local_finished();
}
if (_countCompletions > _expectedCompletions) {
local_ok(false, "Error: local_completeTest called more than local_addTest.");
}
}
function local_finished() {
parent.postMessage({"done":true}, "http://mochi.test:8888");
}
function string2buffer(str) {
return (new Uint8Array(str.length)).map((x, i) => str.charCodeAt(i));
}
function buffer2string(buf) {
let str = "";
if (!(buf.constructor === Uint8Array)) {
buf = new Uint8Array(buf);
}
buf.map(function(x){ return str += String.fromCharCode(x) });
return str;
}
function bytesToBase64(u8a){
let CHUNK_SZ = 0x8000;
let c = [];
let array = new Uint8Array(u8a);
for (let i = 0; i < array.length; i += CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, array.subarray(i, i + CHUNK_SZ)));
}
return window.btoa(c.join(""));
}
function base64ToBytes(b64encoded) {
return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
return c.charCodeAt(0);
}));
}
function bytesToBase64UrlSafe(buf) {
return bytesToBase64(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function base64ToBytesUrlSafe(str) {
if (str.length % 4 == 1) {
throw "Improper b64 string";
}
var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/");
while (b64.length % 4 != 0) {
b64 += "=";
}
return base64ToBytes(b64);
}
function hexEncode(buf) {
return Array.from(buf)
.map(x => ("0"+x.toString(16)).substr(-2))
.join("");
}
function hexDecode(str) {
return new Uint8Array(str.match(/../g).map(x => parseInt(x, 16)));
}
function webAuthnDecodeCBORAttestation(aCborAttBuf) {
let attObj = CBOR.decode(aCborAttBuf);
console.log(":: Attestation CBOR Object ::");
if (!("authData" in attObj && "fmt" in attObj && "attStmt" in attObj)) {
return Promise.reject("Invalid CBOR Attestation Object");
}
if (!("sig" in attObj.attStmt && "x5c" in attObj.attStmt)) {
return Promise.reject("Invalid CBOR Attestation Statement");
}
return webAuthnDecodeAuthDataArray(new Uint8Array(attObj.authData))
.then(function (aAuthDataObj) {
attObj.authDataObj = aAuthDataObj;
return Promise.resolve(attObj);
});
}
function webAuthnDecodeAuthDataArray(aAuthData) {
let rpIdHash = aAuthData.slice(0, 32);
let flags = aAuthData.slice(32, 33);
let counter = aAuthData.slice(33, 37);
console.log(":: Authenticator Data ::");
console.log("RP ID Hash: " + hexEncode(rpIdHash));
console.log("Counter: " + hexEncode(counter) + " Flags: " + flags);
if ((flags & flag_AT) == 0x00) {
// No Attestation Data, so we're done.
return Promise.resolve({
rpIdHash: rpIdHash,
flags: flags,
counter: counter,
});
}
if (aAuthData.length < 38) {
return Promise.reject("Authenticator Data flag was set, but not enough data passed in!");
}
let attData = {};
attData.aaguid = aAuthData.slice(37, 53);
attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54];
attData.credId = aAuthData.slice(55, 55 + attData.credIdLen);
console.log(":: Authenticator Data ::");
console.log("AAGUID: " + hexEncode(attData.aaguid));
cborPubKey = aAuthData.slice(55 + attData.credIdLen);
var pubkeyObj = CBOR.decode(cborPubKey.buffer);
if (!("alg" in pubkeyObj && "x" in pubkeyObj && "y" in pubkeyObj)) {
return Promise.reject("Invalid CBOR Public Key Object");
}
if (pubkeyObj.alg != "ES256") {
return Promise.reject("Unexpected public key algorithm");
}
let pubKeyBytes = assemblePublicKeyBytesData(pubkeyObj.x, pubkeyObj.y);
console.log(":: CBOR Public Key Object Data ::");
console.log("Algorithm: " + pubkeyObj.alg);
console.log("X: " + pubkeyObj.x);
console.log("Y: " + pubkeyObj.y);
console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes));
return importPublicKey(pubKeyBytes)
.then(function(aKeyHandle) {
return Promise.resolve({
rpIdHash: rpIdHash,
flags: flags,
counter: counter,
attestationAuthData: attData,
publicKeyBytes: pubKeyBytes,
publicKeyHandle: aKeyHandle,
});
});
}
function importPublicKey(keyBytes) {
if (keyBytes[0] != 0x04 || keyBytes.byteLength != 65) {
throw "Bad public key octet string";
}
var jwk = {
kty: "EC",
crv: "P-256",
x: bytesToBase64UrlSafe(keyBytes.slice(1, 33)),
y: bytesToBase64UrlSafe(keyBytes.slice(33))
};
return crypto.subtle.importKey("jwk", jwk, {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"])
}
function deriveAppAndChallengeParam(appId, clientData, attestation) {
var appIdBuf = string2buffer(appId);
return Promise.all([
crypto.subtle.digest("SHA-256", appIdBuf),
crypto.subtle.digest("SHA-256", clientData)
])
.then(function(digests) {
return {
appParam: new Uint8Array(digests[0]),
challengeParam: new Uint8Array(digests[1]),
attestation: attestation
};
});
}
function assemblePublicKeyBytesData(xCoord, yCoord) {
// Produce an uncompressed EC key point. These start with 0x04, and then
// two 32-byte numbers denoting X and Y.
if (xCoord.length != 32 || yCoord.length != 32) {
throw ("Coordinates must be 32 bytes long");
}
let keyBytes = new Uint8Array(65);
keyBytes[0] = 0x04;
xCoord.map((x, i) => keyBytes[1 + i] = x);
yCoord.map((x, i) => keyBytes[33 + i] = x);
return keyBytes;
}
function assembleSignedData(appParam, flags, counter, challengeParam) {
let signedData = new Uint8Array(32 + 1 + 4 + 32);
new Uint8Array(appParam).map((x, i) => signedData[0 + i] = x);
signedData[32] = new Uint8Array(flags)[0];
new Uint8Array(counter).map((x, i) => signedData[33 + i] = x);
new Uint8Array(challengeParam).map((x, i) => signedData[37 + i] = x);
return signedData;
}
function assembleRegistrationSignedData(appParam, challengeParam, keyHandle, pubKey) {
let signedData = new Uint8Array(1 + 32 + 32 + keyHandle.length + 65);
signedData[0] = 0x00;
new Uint8Array(appParam).map((x, i) => signedData[1 + i] = x);
new Uint8Array(challengeParam).map((x, i) => signedData[33 + i] = x);
new Uint8Array(keyHandle).map((x, i) => signedData[65 + i] = x);
new Uint8Array(pubKey).map((x, i) => signedData[65 + keyHandle.length + i] = x);
return signedData;
}
function sanitizeSigArray(arr) {
// ECDSA signature fields into WebCrypto must be exactly 32 bytes long, so
// this method strips leading padding bytes, if added, and also appends
// padding zeros, if needed.
if (arr.length > 32) {
arr = arr.slice(arr.length - 32)
}
let ret = new Uint8Array(32);
ret.set(arr, ret.length - arr.length);
return ret;
}
function verifySignature(key, data, derSig) {
if (derSig.byteLength < 68) {
return Promise.reject("Invalid signature (length=" + derSig.byteLength +
"): " + hexEncode(new Uint8Array(derSig)));
}
let sigAsn1 = org.pkijs.fromBER(derSig);
let sigR = new Uint8Array(sigAsn1.result.value_block.value[0].value_block.value_hex);
let sigS = new Uint8Array(sigAsn1.result.value_block.value[1].value_block.value_hex);
// The resulting R and S values from the ASN.1 Sequence must be fit into 32
// bytes. Sometimes they have leading zeros, sometimes they're too short, it
// all depends on what lib generated the signature.
let R = sanitizeSigArray(sigR);
let S = sanitizeSigArray(sigS);
console.log("Verifying these bytes: " + bytesToBase64UrlSafe(data));
let sigData = new Uint8Array(R.length + S.length);
sigData.set(R);
sigData.set(S, R.length);
let alg = {name: "ECDSA", hash: "SHA-256"};
return crypto.subtle.verify(alg, key, sigData, data);
}