Skip to main content
Module

x/fido2/test/validator.test.js

A node.js library for performing FIDO 2.0 / WebAuthn server functionality
Go to Latest
File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
// Testing libimport * as chai from "chai";import * as chaiAsPromised from "chai-as-promised";
// Helpersimport * as h from "./helpers/fido2-helpers.js";
// Testing subjectimport { attach, coerceToArrayBuffer, parseAttestationObject, parseAuthnrAssertionResponse, parseAuthnrAttestationResponse, parseClientResponse} from "../lib/main.js";
chai.use(chaiAsPromised.default);const assert = chai.assert;
const parser = { parseAuthnrAttestationResponse, parseAuthnrAssertionResponse, parseAttestationObject, parseClientResponse,};let attResp;
const runs = [ { functionName: "parseAuthnrAttestationResponse" }, { functionName: "parseAttestationObject" },];
describe("attestation validation", function() { runs.forEach(function(run) { describe("parsing attestation with " + run.functionName, function() { beforeEach(async function() { attResp = { request: {}, requiredExpectations: new Set([ "origin", "challenge", "flags", ]), optionalExpectations: new Set([ "rpId", "allowCredentials", ]), expectations: new Map([ ["origin", "https://localhost:8443"], [ "challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w", ], ["flags", ["UP", "AT"]], ]), clientData: parser.parseClientResponse( h.lib.makeCredentialAttestationNoneResponse, ), authnrData: run.functionName == "parseAuthnrAttestationResponse" ? await parser[run.functionName]( h.lib.makeCredentialAttestationNoneResponse, ) : await parser[run.functionName]( h.lib.makeCredentialAttestationNoneResponse .response .attestationObject, ), }; let testReq = h.functions.cloneObject(h.lib.makeCredentialAttestationNoneResponse); testReq.rawId = h.lib.makeCredentialAttestationNoneResponse.rawId; testReq.response.clientDataJSON = h.lib.makeCredentialAttestationNoneResponse.response.clientDataJSON.slice(0); testReq.response.attestationObject = h.lib.makeCredentialAttestationNoneResponse.response.attestationObject.slice(0); attResp.request = testReq;
attach(attResp); });
it("is attached", function() { assert.isFunction(attach); assert.isFunction(attResp.validateOrigin); assert.isFunction(attResp.validateAttestation); });
describe("validateExpectations", function() { it("returns true on valid expectations", async function() { const ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
it("throws if expectations aren't found", function() { delete attResp.expectations; return assert.isRejected(attResp.validateExpectations(), Error, "expectations should be of type Map"); });
it("throws if expectations aren't Map", function() { attResp.expectations = {}; return assert.isRejected(attResp.validateExpectations(), Error, "expectations should be of type Map"); });
it("throws if optionalExpectations aren't Set", function() { attResp.optionalExpectations = { rpId: true }; return assert.isRejected(attResp.validateExpectations(), Error, "optionalExpectations should be of type Set"); });
it("should not throw if optionalExpectations are array", async function() { attResp.optionalExpectations = ["rpId"]; await attResp.validateExpectations(); });
it("throws if too many expectations", function() { attResp.expectations.set("foo", "bar"); return assert.isRejected(attResp.validateExpectations(), Error, "wrong number of expectations: should have 3 but got 4"); });
it("throws if too many expectations, but expectations are valid", function() { attResp.expectations.set("prevCounter", 42); return assert.isRejected(attResp.validateExpectations(), Error, "wrong number of expectations: should have 3 but got 4"); });
it("throws if missing challenge", function() { attResp.expectations.delete("challenge"); return assert.isRejected(attResp.validateExpectations(), Error, "expectation did not contain value for 'challenge'"); });
it("throws if missing flags", function() { attResp.expectations.delete("flags"); return assert.isRejected(attResp.validateExpectations(), Error, "expectation did not contain value for 'flags'"); });
it("throws if missing origin", function() { attResp.expectations.delete("origin"); return assert.isRejected(attResp.validateExpectations(), Error, "expectation did not contain value for 'origin'"); });
it("throws if challenge is undefined", function() { attResp.expectations.set("challenge", undefined); return assert.isRejected(attResp.validateExpectations(), Error, "expected challenge should be of type String, got: undefined"); });
it("throws if challenge isn't string", function() { attResp.expectations.set("challenge", { foo: "bar" }); return assert.isRejected(attResp.validateExpectations(), Error, "expected challenge should be of type String, got: object"); });
it("throws if challenge isn't base64 encoded string", function() { attResp.expectations.set("challenge", "miles&me"); return assert.isRejected(attResp.validateExpectations(), Error, "expected challenge should be properly encoded base64url String"); });
it("calls checkOrigin");
it("returns true if flags is Set", async function() { attResp.expectations.set("flags", new Set(["UP", "AT"])); const ret = await attResp.validateExpectations(); assert.isTrue(ret); });
it("throws if Set contains non-string", function() { attResp.expectations.set("flags", new Set([3, "UP", "AT"])); return assert.isRejected(attResp.validateExpectations(), Error, "expected flag unknown: 3"); });
it("throws if Array contains non-string", function() { attResp.expectations.set("flags", [3, "UP", "AT"]); return assert.isRejected(attResp.validateExpectations(), Error, "expected flag unknown: 3"); });
it("throws on unknown flag", function() { attResp.expectations.set("flags", new Set(["foo", "UP", "AT"])); return assert.isRejected(attResp.validateExpectations(), Error, "expected flag unknown: foo"); });
it("throws on undefined flag", function() { attResp.expectations.set("flags", new Set([undefined, "UP", "AT"])); return assert.isRejected(attResp.validateExpectations(), Error, "expected flag unknown: undefined"); });
it("throws on invalid rpId type", function() { attResp.expectations.set("rpId", 123); return assert.isRejected(attResp.validateExpectations(), Error, "rpId must be a string"); });
it("throws on invalid rpId", function() { attResp.expectations.set("rpId", "test"); return assert.isRejected(attResp.validateExpectations(), Error, "rpId is not a valid eTLD+1"); });
it("works with valid rpId", async function() { attResp.expectations.set("rpId", "google.com"); const ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
it("works with localhost rpId", async function() { attResp.expectations.set("rpId", "localhost"); const ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
it("works with valid allowCredentials", async function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, type: "public-key", transports: ["usb", "nfc"] }]); let ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
it("works with null allowCredentials", async function() { attResp.expectations.set("allowCredentials", null); const ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
it("throws on wrong allowCredentials type", function() { attResp.expectations.set("allowCredentials", { type: "public-key", transports: ["usb", "nfc"] }); return assert.isRejected(attResp.validateExpectations(), Error, "expected allowCredentials to be null or array"); });
it("throws on missing id in allowCredentials", function() { attResp.expectations.set("allowCredentials", [{ type: "public-key", transports: ["usb", "nfc"] }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected id of allowCredentials[0] to be ArrayBuffer"); });
it("throws on null id in allowCredentials", function() { attResp.expectations.set("allowCredentials", [{ id: {}, type: "public-key", transports: ["usb", "nfc"] }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected id of allowCredentials[0] to be ArrayBuffer"); });
it("throws on wrong type of id in allowCredentials", function() { attResp.expectations.set("allowCredentials", [{ id: {}, type: "public-key", transports: ["usb", "nfc"] }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected id of allowCredentials[0] to be ArrayBuffer"); });
it("throws on missing type in allowCredentials element", function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, transports: ["usb", "nfc"] }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected type of allowCredentials[0] to be string with value 'public-key'"); });
it("throws on wrong type value in allowCredentials element", function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, type: "test", transports: ["usb", "nfc"] }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected type of allowCredentials[0] to be string with value 'public-key'"); });
it("throws on wrong transports type in allowCredentials element", function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, type: "public-key", transports: "test" }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected transports of allowCredentials[0] to be array or null"); });
it("throws on wrong transports value in allowCredentials element", function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, type: "public-key", transports: ["none", "nfc"] }]); return assert.isRejected(attResp.validateExpectations(), Error, "expected transports of allowCredentials[0] to be string with value 'usb', 'nfc', 'ble', 'internal' or null"); });
it("works with null transports in allowCredentials element", async function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, type: "public-key" }]); let ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
it("throws if counter is not a number"); it("throws if counter is negative"); it("throws if publicKey is not a string"); it("throws if publicKey doesn't match PEM regexp");
it("throws if requiredExpectations is undefined"); it("throws if requiredExpectations is not Array or Set"); it("passes if requiredExpectations is Array"); it("passes if requiredExpectations is Set"); it("throws if requiredExpectations field is missing"); it("throws if more expectations were passed than requiredExpectations"); });
describe("validateCreateRequest", function() { it("returns true if request is valid", function() { const ret = attResp.validateCreateRequest(); assert.isTrue(ret); assert.isTrue(attResp.audit.validRequest); });
it("returns true for U2F request", function() { const ret = attResp.validateCreateRequest(); assert.isTrue(ret); assert.isTrue(attResp.audit.validRequest); });
it("throws if request is undefined", function() { attResp.request = undefined; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected request to be Object, got undefined"); });
it("throws if response field is undefined", function() { delete attResp.request.response; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'response' field of request to be Object, got undefined"); });
it("throws if response field is non-object", function() { attResp.request.response = 3; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'response' field of request to be Object, got number"); });
it("throws if id field is undefined", function() { delete attResp.request.id; delete attResp.request.rawId; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'id' or 'rawId' field of request to be ArrayBuffer, got rawId undefined and id undefined"); });
it("throws if id field is non-string", function() { attResp.request.rawId = []; delete attResp.request.id; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'id' or 'rawId' field of request to be ArrayBuffer, got rawId object and id undefined"); });
it("throws if response.attestationObject is undefined", function() { delete attResp.request.response.attestationObject; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'response.attestationObject' to be base64 String or ArrayBuffer"); });
it("throws if response.attestationObject is non-ArrayBuffer & non-String", function() { attResp.request.response.attestationObject = {}; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'response.attestationObject' to be base64 String or ArrayBuffer"); });
it("passes with response.attestationObject as ArrayBuffer", async function() { attResp.request.response.attestationObject = new ArrayBuffer(); const ret = await attResp.validateCreateRequest(); assert.isTrue(ret); assert.isTrue(attResp.audit.validRequest); });
it("passes with response.attestationObject as String", async function() { attResp.request.response.attestationObject = ""; const ret = await attResp.validateCreateRequest(); assert.isTrue(ret); assert.isTrue(attResp.audit.validRequest); });
it("throws if response.clientDataJSON is undefined", function() { delete attResp.request.response.clientDataJSON; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'response.clientDataJSON' to be base64 String or ArrayBuffer"); });
it("throws if response.clientDataJSON is non-ArrayBuffer & non-String", function() { attResp.request.response.clientDataJSON = {}; assert.throws(() => { attResp.validateCreateRequest(); }, TypeError, "expected 'response.clientDataJSON' to be base64 String or ArrayBuffer"); });
it("passes with response.clientDataJSON as ArrayBuffer", async function() { attResp.request.response.clientDataJSON = new ArrayBuffer(); const ret = await attResp.validateCreateRequest(); assert.isTrue(ret); assert.isTrue(attResp.audit.validRequest); });
it("passes with response.clientDataJSON as String", async function() { attResp.request.response.clientDataJSON = ""; const ret = await attResp.validateCreateRequest(); assert.isTrue(ret); assert.isTrue(attResp.audit.validRequest); }); });
describe("validateRawClientDataJson", function() { it("returns true if ArrayBuffer", async function() { const ret = await attResp.validateRawClientDataJson(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("rawClientDataJson")); });
it("throws if missing", function() { attResp.clientData.delete("rawClientDataJson"); return assert.isRejected(attResp.validateRawClientDataJson(), Error, "clientData clientDataJson should be ArrayBuffer"); });
it("throws if not ArrayBuffer", function() { attResp.clientData.set("rawClientDataJson", "foo"); return assert.isRejected(attResp.validateRawClientDataJson(), Error, "clientData clientDataJson should be ArrayBuffer"); }); });
describe("validateId", function() { it("returns true on ArrayBuffer", async function() { const ret = await attResp.validateId(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("rawId")); });
it("throws on non-ArrayBuffer", function() { attResp.clientData.set("id", {}); attResp.clientData.set("rawId", {}); return assert.isRejected(attResp.validateId(), Error, "expected id to be of type ArrayBuffer"); });
it("throws on undefined", function() { attResp.clientData.set("id", undefined); attResp.clientData.set("rawId", undefined); return assert.isRejected(attResp.validateId(), Error, "expected id to be of type ArrayBuffer"); }); });
describe("validateTransports", function() { it("returns true on array<string>", async function() { const ret = await attResp.validateTransports(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("transports")); });
it("returns true on null", async function() { const ret = await attResp.validateTransports(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("transports")); });
it("throws on non-Array", function() { attResp.authnrData.set("transports", "test"); return assert.isRejected(attResp.validateTransports(), Error, "expected transports to be 'null' or 'array<string>'"); });
it("throws on non-Array<string>", function() { attResp.authnrData.set("transports", [1]); return assert.isRejected(attResp.validateTransports(), Error, "expected transports[0] to be 'string'"); }); });
describe("validateOrigin", function() { it("accepts exact match", async function() { attResp.expectations.set("origin", "https://webauthn.bin.coffee:8080"); attResp.clientData.set("origin", "https://webauthn.bin.coffee:8080"); let ret = await attResp.validateOrigin(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("origin")); });
it("throws on port mismatch", function() { attResp.expectations.set("origin", "https://webauthn.bin.coffee:8080"); attResp.clientData.set("origin", "https://webauthn.bin.coffee:8443"); return assert.isRejected(attResp.validateOrigin(), Error, "clientData origin did not match expected origin"); });
it("throws on domain mismatch", function() { attResp.expectations.set("origin", "https://webauthn.bin.coffee:8080"); attResp.clientData.set("origin", "https://bin.coffee:8080"); return assert.isRejected(attResp.validateOrigin(), Error, "clientData origin did not match expected origin"); });
it("throws on protocol mismatch", function() { attResp.expectations.set("origin", "http://webauthn.bin.coffee:8080"); attResp.clientData.set("origin", "https://webauthn.bin.coffee:8080"); return assert.isRejected(attResp.validateOrigin(), Error, "clientData origin did not match expected origin"); });
it("calls checkOrigin"); });
describe("checkOrigin", function() { });
describe("validateCreateType", function() { it("returns true when 'webauthn.create'", async function() { const ret = await attResp.validateCreateType(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("type")); });
it("throws when undefined", function() { attResp.clientData.set("type", undefined); return assert.isRejected(attResp.validateCreateType(), Error, "clientData type should be 'webauthn.create'"); });
it("throws on 'webauthn.get'", function() { attResp.clientData.set("type", "webauthn.get"); return assert.isRejected(attResp.validateCreateType(), Error, "clientData type should be 'webauthn.create'"); });
it("throws on unknown string", function() { attResp.clientData.set("type", "asdf"); return assert.isRejected(attResp.validateCreateType(), Error, "clientData type should be 'webauthn.create'"); }); });
describe("validateGetType", function() { it("returns true when 'webauthn.get'", async function() { attResp.clientData.set("type", "webauthn.get"); const ret = await attResp.validateGetType(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("type")); });
it("throws when undefined", function() { attResp.clientData.set("type", undefined); return assert.isRejected(attResp.validateGetType(), Error, "clientData type should be 'webauthn.get'"); });
it("throws on 'webauthn.create'", function() { attResp.clientData.set("type", "webauthn.create"); return assert.isRejected(attResp.validateGetType(), "clientData type should be 'webauthn.get'"); });
it("throws on unknown string", function() { attResp.clientData.set("type", "asdf"); return assert.isRejected(attResp.validateGetType(), "clientData type should be 'webauthn.get'"); }); });
describe("validateChallenge", function() { it("returns true if challenges match", async function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); let ret = await attResp.validateChallenge(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("challenge")); });
it("accepts ending equal sign (1)", async function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w="); let ret = await attResp.validateChallenge(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("challenge")); });
it("accepts ending equal signs (2)", async function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w=="); let ret = await attResp.validateChallenge(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("challenge")); });
it("throws on three equal signs", function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w==="); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge was not properly encoded base64url"); });
it("does not remove equal sign from middle of string", function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", "33EHav-jZ1v9qwH783aU-j0A=Rx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge was not properly encoded base64url"); });
it("throws if challenge is not a string", function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", ["foo"]); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge was not a string"); });
it("throws if challenge is base64url encoded", function() { attResp.expectations.set("challenge", "4BS1YJKRCeCVoLdfG_b66BuSQ-I2n34WsLFvy62fpIVFjrm32_tFRQixX9U8EBVTriTkreAp-1nDvYboRK9WFg"); attResp.clientData.set("challenge", "4BS1YJKRCeCVoLdfG/b66BuSQ+I2n34WsLFvy62fpIVFjrm32/tFRQixX9U8EBVTriTkreAp+1nDvYboRK9WFg"); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge was not properly encoded base64url"); });
it("throws if challenge is not base64 string", function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", "miles&me"); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge was not properly encoded base64url"); });
it("throws on undefined challenge", function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", undefined); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge was not a string"); });
it("throws on challenge mismatch", function() { attResp.expectations.set("challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"); attResp.clientData.set("challenge", "4BS1YJKRCeCVoLdfG_b66BuSQ-I2n34WsLFvy62fpIVFjrm32_tFRQixX9U8EBVTriTkreAp-1nDvYboRK9WFg"); return assert.isRejected(attResp.validateChallenge(), Error, "clientData challenge mismatch"); }); });
describe("validateRawAuthnrData", function() { it("returns true if ArrayBuffer", async function() { const ret = await attResp.validateRawAuthnrData(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("rawAuthnrData")); });
it("throws if missing", function() { attResp.authnrData.delete("rawAuthnrData"); return assert.isRejected( attResp.validateRawAuthnrData(), Error, "authnrData rawAuthnrData should be ArrayBuffer", ); });
it("throws if not ArrayBuffer", function() { attResp.authnrData.set("rawAuthnrData", "foo"); return assert.isRejected(attResp.validateRawAuthnrData(), Error, "authnrData rawAuthnrData should be ArrayBuffer"); }); });
describe("validateAttestation", function() { it("accepts none", async function() { const ret = await attResp.validateAttestation(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("fmt")); });
it("throws on unknown fmt", function() { attResp.authnrData.set("fmt", "asdf"); return assert.isRejected(attResp.validateAttestation(), Error, "no support for attestation format: asdf"); });
it("throws on undefined fmt", function() { attResp.authnrData.delete("fmt"); return assert.isRejected(attResp.validateAttestation(), Error, "expected 'fmt' to be string, got: undefined"); }); });
describe("validateRpIdHash", function() { afterEach(() => { attResp.expectations.delete("rpId"); });
it("returns true when matches", async function() { const ret = await attResp.validateRpIdHash(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("rpIdHash")); });
it("throws when it doesn't match", function() { attResp.expectations.set("origin", "https://google.com"); return assert.isRejected(attResp.validateRpIdHash(), Error, "authnrData rpIdHash mismatch"); });
it("throws when it doesn't match in case of invalid rpId", function() { attResp.expectations.set("origin", "localhost"); attResp.expectations.set("rpId", "google.com"); return assert.isRejected(attResp.validateRpIdHash(), Error, "authnrData rpIdHash mismatch"); });
it("throws when length mismatches", function() { attResp.authnrData.set("rpIdHash", new Uint8Array([1, 2, 3]).buffer); return assert.isRejected(attResp.validateRpIdHash(), Error, "authnrData rpIdHash length mismatch"); }); });
describe("validateAaguid", function() { it("returns true on validation", async function() { const ret = await attResp.validateAaguid(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("aaguid")); });
it("throws if too short", function() { attResp.authnrData.set("aaguid", new Uint8Array([1, 2, 3]).buffer); return assert.isRejected(attResp.validateAaguid(), Error, "authnrData AAGUID was wrong length"); }); });
describe("validateCredId", function() { it("returns true when ArrayBuffer of correct length", async function() { const ret = await attResp.validateCredId(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("credId")); assert.isTrue(attResp.audit.journal.has("credIdLen")); });
it("throws if length is undefined", function() { attResp.authnrData.delete("credIdLen"); return assert.isRejected(attResp.validateCredId(), Error, "authnrData credIdLen should be number, got undefined"); });
it("throws if length is not number", function() { attResp.authnrData.set("credIdLen", new Uint8Array()); return assert.isRejected(attResp.validateCredId(), Error, "authnrData credIdLen should be number, got object"); });
it("throws if length is wrong", function() { attResp.authnrData.set("credIdLen", 42); return assert.isRejected(attResp.validateCredId(), "authnrData credId was wrong length"); });
it("throws if credId is undefined", function() { attResp.authnrData.delete("credId"); return assert.isRejected(attResp.validateCredId(), "authnrData credId should be ArrayBuffer"); });
it("throws if not array buffer", function() { attResp.authnrData.set("credId", "foo"); return assert.isRejected(attResp.validateCredId(), "authnrData credId should be ArrayBuffer"); }); });
describe("validatePublicKey", function() { it("returns true on validation", async function() { const ret = await attResp.validatePublicKey(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("credentialPublicKeyCose")); assert.isTrue(attResp.audit.journal.has("credentialPublicKeyJwk")); assert.isTrue(attResp.audit.journal.has("credentialPublicKeyPem")); }); });
describe("validateTokenBinding", function() { it("returns true if tokenBinding is undefined", async function() { const ret = await attResp.validateTokenBinding(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("tokenBinding")); });
it("throws if tokenBinding is defined", function() { attResp.clientData.set("tokenBinding", "foo"); return assert.isRejected(attResp.validateTokenBinding(), Error, "Token binding field malformed: foo"); }); });
describe("validateFlags", function() { it("returns true on valid expectations", async function() { const ret = await attResp.validateFlags(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("flags")); });
it("throws on invalid expectations", function() { attResp.expectations.set("flags", ["ED"]); return assert.isRejected(attResp.validateFlags(), Error, "expected flag was not set: ED"); });
it("throws if UV is set but UP is not set", function() { attResp.expectations.set("flags", ["UV"]); attResp.authnrData.set("flags", new Set(["UV"])); return assert.isRejected(attResp.validateFlags(), Error, "expected User Presence (UP) flag to be set if User Verification (UV) is set"); });
it("throws if UV is not set", function() { attResp.expectations.set("flags", ["UV"]); attResp.authnrData.set("flags", new Set(["ED"])); return assert.isRejected(attResp.validateFlags(), Error, "expected flag was not set: UV"); });
it("throws if UV but only UP is set", function() { attResp.expectations.set("flags", ["UV"]); attResp.authnrData.set("flags", new Set(["UP"])); return assert.isRejected(attResp.validateFlags(), Error, "expected flag was not set: UV"); });
it("returns true on UP with UP-or-UV", async function() { attResp.expectations.set("flags", ["UP-or-UV"]); attResp.authnrData.set("flags", new Set(["UP"])); const ret = await attResp.validateFlags(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("flags")); });
it("returns true on UV with UP-or-UV", async function() { attResp.expectations.set("flags", ["UP-or-UV"]); attResp.authnrData.set("flags", new Set(["UV", "UP"])); const ret = await attResp.validateFlags(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("flags")); });
it("throws if UP-or-UV and UV is set but not UP", function() { attResp.expectations.set("flags", ["UP-or-UV"]); attResp.authnrData.set("flags", new Set(["UV"])); return assert.isRejected(attResp.validateFlags(), Error, "expected User Presence (UP) flag to be set if User Verification (UV) is set"); });
it("throws if UP-or-UV and neither is set", function() { attResp.expectations.set("flags", ["UP-or-UV"]); attResp.authnrData.set("flags", new Set(["ED"])); return assert.isRejected(attResp.validateFlags(), Error, "expected User Presence (UP) or User Verification (UV) flag to be set and neither was"); });
it("throws if any of the RFU flags are set"); });
describe("validateInitialCounter", function() { it("returns true if valid", async function() { const ret = await attResp.validateInitialCounter(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("counter")); });
it("throws if not a number", function() { attResp.authnrData.set("counter", "foo"); return assert.isRejected(attResp.validateInitialCounter(), Error, "authnrData counter wasn't a number"); }); });
describe("validateAudit for 'none' attestation", function() { it("returns on all internal checks passed", async function() { await attResp.validateExpectations(); await attResp.validateCreateRequest(); // clientData validators await attResp.validateRawClientDataJson(); await attResp.validateOrigin(); await attResp.validateCreateType(); await attResp.validateChallenge(); await attResp.validateTokenBinding(); await attResp.validateId(); await attResp.validateTransports(); // authnrData validators await attResp.validateRawAuthnrData(); await attResp.validateAttestation(); await attResp.validateRpIdHash(); await attResp.validateAaguid(); await attResp.validateCredId(); await attResp.validatePublicKey(); await attResp.validateFlags(); await attResp.validateInitialCounter();
// audit const ret = await attResp.validateAudit(); assert.isTrue(ret); assert.isTrue(attResp.audit.complete); });
it("throws on untested verifies", function() { return assert.isRejected(attResp.validateAudit(), Error, "internal audit failed: challenge was not validated"); });
it("throws on extra journal entries");
it("throws on untested expectations"); it("throws on untested request"); });
describe("validateAudit for assertion", function() { it("returns on all internal checks passed"); }); }); });});
describe("assertion validation", function() { let assnResp; beforeEach(async function() { const assertionResponseCopy = h.functions.cloneObject( h.lib.assertionResponse, ); assnResp = { request: {}, requiredExpectations: new Set([ "origin", "challenge", "flags", "counter", "publicKey", ]), optionalExpectations: new Set([ "rpId", "allowCredentials", ]), expectations: new Map([ ["origin", "https://localhost:8443"], ["challenge", "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w"], ["flags", ["UP", "AT"]], ["counter", 300], ["publicKey", h.lib.assnPublicKey], ["allowCredentials", [{ id: assertionResponseCopy.rawId, type: "public-key", }]], ]), clientData: parser.parseClientResponse(assertionResponseCopy), authnrData: new Map([ ...await parser.parseAuthnrAssertionResponse( assertionResponseCopy, ), ]), }; const testReq = assertionResponseCopy; testReq.rawId = assertionResponseCopy.rawId; testReq.response.clientDataJSON = assertionResponseCopy.response.clientDataJSON.slice(0); testReq.response.authenticatorData = assertionResponseCopy.response.authenticatorData.slice(0); testReq.response.signature = assertionResponseCopy.response.signature.slice(0); testReq.response.userHandle = assertionResponseCopy.response.userHandle.slice(0); assnResp.request = testReq;
attach(assnResp); });
describe("validateUserHandle", function() { it("returns true when undefined", async function() { const ret = await assnResp.validateUserHandle(); assert.isTrue(ret); assert.isTrue(assnResp.audit.journal.has("userHandle")); });
it("throws if not undefined", function() { assnResp.authnrData.set("userHandle", "foo"); return assert.isRejected(assnResp.validateUserHandle(), Error, "unable to validate userHandle"); }); });
describe("validateCounter", function() { it("returns true if counter has advanced", async function() { assert.strictEqual(assnResp.authnrData.get("counter"), 363); assnResp.expectations.set("prevCounter", 362); const ret = await assnResp.validateCounter(); assert.isTrue(ret); assert.isTrue(assnResp.audit.journal.has("counter")); assert.equal(assnResp.audit.info.get("counter-supported"), "true"); });
it("returns true if counter is not supported but do not add it to journal", async function() { assnResp.authnrData.set("counter", 0); assnResp.expectations.set("prevCounter", 0); const ret = await assnResp.validateCounter(); assert.isTrue(ret); assert.isTrue(assnResp.audit.journal.has("counter")); assert.equal(assnResp.audit.info.get("counter-supported"), "false"); });
it("throws if counter is the same", function() { assert.strictEqual(assnResp.authnrData.get("counter"), 363); assnResp.expectations.set("prevCounter", 363); return assert.isRejected(assnResp.validateCounter(), Error, "counter rollback detected"); });
it("throws if counter has rolled back", function() { assert.strictEqual(assnResp.authnrData.get("counter"), 363); assnResp.expectations.set("prevCounter", 364); return assert.isRejected(assnResp.validateCounter(), Error, "counter rollback detected"); }); });
describe("validateExpectations", function() { it("returns true on valid expectations", async function() { const ret = await assnResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validExpectations); }); });
describe("validateId", function() { it("returns true on ArrayBuffer", async function() { const ret = await assnResp.validateId(); assert.isTrue(ret); assert.isTrue(assnResp.audit.journal.has("rawId")); });
it("throws on non-ArrayBuffer", function() { assnResp.clientData.set("id", {}); assnResp.clientData.set("rawId", {}); return assert.isRejected(assnResp.validateId(), Error, "expected id to be of type ArrayBuffer"); });
it("throws on undefined", function() { assnResp.clientData.set("id", undefined); assnResp.clientData.set("rawId", undefined); return assert.isRejected(assnResp.validateId(), Error, "expected id to be of type ArrayBuffer"); });
it("throws on allowCredentials not includes rawId", function() { assnResp.expectations.set("allowCredentials", [{ type: "public-key", id: coerceToArrayBuffer("dGVz", "tes") }]); assnResp.clientData.set("rawId", coerceToArrayBuffer("Y2lhbw==", "ciao")); return assert.isRejected(assnResp.validateId(), Error, "Credential ID does not match any value in allowCredentials"); }); });
describe("validateAssertionSignature", function() { it("returns true on valid signature"); });
describe("validateAssertionResponse", function() { it("returns true if request is valid", async function() { const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("returns true for U2F request", async function() { const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("throws if request is undefined", function() { assnResp.request = undefined; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected request to be Object, got undefined"); });
it("throws if response field is undefined", function() { delete assnResp.request.response; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response' field of request to be Object, got undefined"); });
it("throws if response field is non-object", function() { assnResp.request.response = 3; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response' field of request to be Object, got number"); });
it("throws if id field is undefined", function() { delete assnResp.request.id; delete assnResp.request.rawId; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'id' or 'rawId' field of request to be ArrayBuffer, got rawId undefined and id undefined"); });
it("throws if rawId field is non-string", function() { assnResp.request.rawId = {}; delete assnResp.request.id; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'id' or 'rawId' field of request to be ArrayBuffer, got rawId object and id undefined"); });
it("throws if response.signature is undefined", function() { delete assnResp.request.response.signature; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.signature' to be base64 String or ArrayBuffer"); });
it("throws if response.signature is non-ArrayBuffer & non-String", function() { assnResp.request.response.signature = {}; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.signature' to be base64 String or ArrayBuffer"); });
it("passes with response.signature as ArrayBuffer", async function() { assnResp.request.response.signature = new ArrayBuffer(); const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("passes with response.signature as String", async function() { assnResp.request.response.signature = ""; const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("throws if response.authenticatorData is undefined", function() { delete assnResp.request.response.authenticatorData; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.authenticatorData' to be base64 String or ArrayBuffer"); });
it("throws if response.authenticatorData is non-ArrayBuffer & non-String", function() { assnResp.request.response.authenticatorData = {}; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.authenticatorData' to be base64 String or ArrayBuffer"); });
it("passes with response.authenticatorData as ArrayBuffer", async function() { assnResp.request.response.authenticatorData = new ArrayBuffer(); const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("passes with response.authenticatorData as String", async function() { assnResp.request.response.authenticatorData = ""; const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("returns true if response.userHandle is undefined", async function() { delete assnResp.request.response.userHandle; const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("throws if response.userHandle is non-ArrayBuffer & non-String", function() { assnResp.request.response.userHandle = {}; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.userHandle' to be base64 String, ArrayBuffer, or undefined"); });
it("passes with response.userHandle as ArrayBuffer", async function() { assnResp.request.response.userHandle = new ArrayBuffer(); const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("passes with response.userHandle as String", async function() { assnResp.request.response.userHandle = ""; const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("throws if response.clientDataJSON is undefined", function() { delete assnResp.request.response.clientDataJSON; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.clientDataJSON' to be base64 String or ArrayBuffer"); });
it("throws if response.clientDataJSON is non-ArrayBuffer & non-String", function() { assnResp.request.response.clientDataJSON = {}; assert.throws(() => { assnResp.validateAssertionResponse(); }, TypeError, "expected 'response.clientDataJSON' to be base64 String or ArrayBuffer"); });
it("passes with response.clientDataJSON as ArrayBuffer", async function() { assnResp.request.response.clientDataJSON = new ArrayBuffer(); const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); });
it("passes with response.clientDataJSON as String", async function() { assnResp.request.response.clientDataJSON = ""; const ret = await assnResp.validateAssertionResponse(); assert.isTrue(ret); assert.isTrue(assnResp.audit.validRequest); }); });});