UNPKG

fido2-lib

Version:

A library for performing FIDO 2.0 / WebAuthn functionality

1,180 lines (1,004 loc) 48.2 kB
// Testing lib import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; // Helpers import * as h from "./helpers/fido2-helpers.js"; // Testing subject import { 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("validateExtensions", function() { // original test data does not contain extensions it("returns true on validation without extensions", async function() { const ret = attResp.validateExtensions(); assert.isTrue(ret); assert.isFalse(attResp.audit.journal.has("webAuthnExtensions")); }); it("returns true on validation with extensions", async function() { attResp.authnrData.get("flags").add("ED"); attResp.authnrData.set("webAuthnExtensions", [{ credProtect: 1 }]); const ret = attResp.validateExtensions(); assert.isTrue(ret); assert.isTrue(attResp.audit.journal.has("webAuthnExtensions")); }); it("throws on invalid extensions", async function() { attResp.authnrData.get("flags").add("ED"); attResp.authnrData.set("webAuthnExtensions", [42]); assert.throws(() => attResp.validateExtensions(), Error, "webAuthnExtensions aren't valid"); }); it("throws on unexpected extensions", async function() { attResp.authnrData.set("webAuthnExtensions", [{ credProtect: 1 }]); assert.throws(() => attResp.validateExtensions(), Error, "unexpected webAuthnExtensions found"); }); }); 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.validateExtensions(); 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); }); }); });