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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
// 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', 'cable', 'internal' or null"); });
it("works with all allowed transports in allowCredentials element", async function() { attResp.expectations.set("allowCredentials", [{ id: h.lib.assertionResponse.rawId, type: "public-key", transports: ["nfc","ble","cable","internal","usb"] }]); let ret = await attResp.validateExpectations(); assert.isTrue(ret); assert.isTrue(attResp.audit.validExpectations); });
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); }); });});