зеркало из https://github.com/mozilla/gecko-dev.git
399 строки
11 KiB
JavaScript
399 строки
11 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_kty = 1;
|
|
const cose_kty_ec2 = 2;
|
|
const cose_alg = 3;
|
|
const cose_alg_ECDSA_w_SHA256 = -7;
|
|
const cose_alg_ECDSA_w_SHA512 = -36;
|
|
const cose_crv = -1;
|
|
const cose_crv_P256 = 1;
|
|
const cose_crv_x = -2;
|
|
const cose_crv_y = -3;
|
|
|
|
var { AppConstants } = SpecialPowers.Cu.import(
|
|
"resource://gre/modules/AppConstants.jsm",
|
|
{}
|
|
);
|
|
|
|
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 hasOnlyKeys(obj, ...keys) {
|
|
let okeys = new Set(Object.keys(obj));
|
|
return keys.length == okeys.size && keys.every(k => okeys.has(k));
|
|
}
|
|
|
|
function webAuthnDecodeCBORAttestation(aCborAttBuf) {
|
|
let attObj = CBOR.decode(aCborAttBuf);
|
|
console.log(":: Attestation CBOR Object ::");
|
|
if (!hasOnlyKeys(attObj, "authData", "fmt", "attStmt")) {
|
|
return Promise.reject("Invalid CBOR Attestation Object");
|
|
}
|
|
if (attObj.fmt == "fido-u2f" && !hasOnlyKeys(attObj.attStmt, "sig", "x5c")) {
|
|
return Promise.reject("Invalid CBOR Attestation Statement");
|
|
}
|
|
if (attObj.fmt == "none" && Object.keys(attObj.attStmt).length > 0) {
|
|
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,
|
|
flags,
|
|
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));
|
|
|
|
let cborPubKey = aAuthData.slice(55 + attData.credIdLen);
|
|
var pubkeyObj = CBOR.decode(cborPubKey.buffer);
|
|
if (
|
|
!(
|
|
cose_kty in pubkeyObj &&
|
|
cose_alg in pubkeyObj &&
|
|
cose_crv in pubkeyObj &&
|
|
cose_crv_x in pubkeyObj &&
|
|
cose_crv_y in pubkeyObj
|
|
)
|
|
) {
|
|
throw "Invalid CBOR Public Key Object";
|
|
}
|
|
if (pubkeyObj[cose_kty] != cose_kty_ec2) {
|
|
throw "Unexpected key type";
|
|
}
|
|
if (pubkeyObj[cose_alg] != cose_alg_ECDSA_w_SHA256) {
|
|
throw "Unexpected public key algorithm";
|
|
}
|
|
if (pubkeyObj[cose_crv] != cose_crv_P256) {
|
|
throw "Unexpected curve";
|
|
}
|
|
|
|
let pubKeyBytes = assemblePublicKeyBytesData(
|
|
pubkeyObj[cose_crv_x],
|
|
pubkeyObj[cose_crv_y]
|
|
);
|
|
console.log(":: CBOR Public Key Object Data ::");
|
|
console.log("kty: " + pubkeyObj[cose_kty] + " (EC2)");
|
|
console.log("alg: " + pubkeyObj[cose_alg] + " (ES256)");
|
|
console.log("crv: " + pubkeyObj[cose_crv] + " (P256)");
|
|
console.log("X: " + pubkeyObj[cose_crv_x]);
|
|
console.log("Y: " + pubkeyObj[cose_crv_y]);
|
|
console.log("Uncompressed (hex): " + hexEncode(pubKeyBytes));
|
|
|
|
return importPublicKey(pubKeyBytes).then(function(aKeyHandle) {
|
|
return Promise.resolve({
|
|
rpIdHash,
|
|
flags,
|
|
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,
|
|
};
|
|
});
|
|
}
|
|
|
|
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))
|
|
);
|
|
}
|
|
|
|
// Copy signature data into the current context.
|
|
let derSigCopy = new ArrayBuffer(derSig.byteLength);
|
|
new Uint8Array(derSigCopy).set(new Uint8Array(derSig));
|
|
|
|
let sigAsn1 = org.pkijs.fromBER(derSigCopy);
|
|
|
|
// pkijs.asn1 seems to erroneously set an error code when calling some
|
|
// internal function. The test suite doesn't like dangling globals.
|
|
delete window.error;
|
|
|
|
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);
|
|
}
|