Skip to main content
Module

x/fido2/lib/validator.js

A node.js library for performing FIDO 2.0 / WebAuthn server functionality
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
// deno-lint-ignore-fileimport { arrayBufferEquals, appendBuffer, coerceToArrayBuffer, coerceToBase64Url, isBase64Url, isPem, isPositiveInteger, tools} from "./utils.js";
import { Fido2Lib } from "./main.js";

async function validateExpectations() { /* eslint complexity: ["off"] */ let req = this.requiredExpectations; let opt = this.optionalExpectations; let exp = this.expectations;
if (!(exp instanceof Map)) { throw new Error("expectations should be of type Map"); }
if (Array.isArray(req)) { req = new Set([req]); }
if (!(req instanceof Set)) { throw new Error("requiredExpectaions should be of type Set"); }
if (Array.isArray(opt)) { opt = new Set([opt]); }
if (!(opt instanceof Set)) { throw new Error("optionalExpectations should be of type Set"); }
for (let field of req) { if (!exp.has(field)) { throw new Error(`expectation did not contain value for '${field}'`); } }
let optCount = 0; for (const [field] of exp) { if (opt.has(field)) { optCount++; } }
if (req.size !== exp.size - optCount) { throw new Error( `wrong number of expectations: should have ${req.size} but got ${exp.size - optCount}`, ); }
// origin - isValid if (req.has("origin")) { let expectedOrigin = exp.get("origin");
tools.checkOrigin(expectedOrigin); }
// rpId - optional, isValid if (exp.has("rpId")) { let expectedRpId = exp.get("rpId");
tools.checkRpId(expectedRpId); }
// challenge - is valid base64url string if (exp.has("challenge")) { let challenge = exp.get("challenge"); if (typeof challenge !== "string") { throw new Error("expected challenge should be of type String, got: " + typeof challenge); }
if (!isBase64Url(challenge)) { throw new Error("expected challenge should be properly encoded base64url String"); } }
// flags - is Array or Set if (req.has("flags")) { let validFlags = new Set(["UP", "UV", "UP-or-UV", "AT", "ED"]); let flags = exp.get("flags");
for (let flag of flags) { if (!validFlags.has(flag)) { throw new Error(`expected flag unknown: ${flag}`); } } }
// prevCounter if (req.has("prevCounter")) { let prevCounter = exp.get("prevCounter");
if (!isPositiveInteger(prevCounter)) { throw new Error("expected counter to be positive integer"); } }
// publicKey if (req.has("publicKey")) { let publicKey = exp.get("publicKey"); if (!isPem(publicKey)) { throw new Error("expected publicKey to be in PEM format"); } }
// userHandle if (req.has("userHandle")) { let userHandle = exp.get("userHandle"); if (userHandle !== null && typeof userHandle !== "string") { throw new Error("expected userHandle to be null or string"); } }

// allowCredentials if (exp.has("allowCredentials")) { let allowCredentials = exp.get("allowCredentials"); if (allowCredentials != null) { if (!Array.isArray(allowCredentials)) { throw new Error("expected allowCredentials to be null or array"); } else { for (const index in allowCredentials) { if (typeof allowCredentials[index].id === "string") { allowCredentials[index].id = coerceToArrayBuffer(allowCredentials[index].id, "allowCredentials[" + index + "].id"); } if (allowCredentials[index].id == null || !(allowCredentials[index].id instanceof ArrayBuffer)) { throw new Error("expected id of allowCredentials[" + index + "] to be ArrayBuffer"); } if (allowCredentials[index].type == null || allowCredentials[index].type !== "public-key") { throw new Error("expected type of allowCredentials[" + index + "] to be string with value 'public-key'"); } if (allowCredentials[index].transports != null && !Array.isArray(allowCredentials[index].transports)) { throw new Error("expected transports of allowCredentials[" + index + "] to be array or null"); } else if (allowCredentials[index].transports != null && !allowCredentials[index].transports.every(el => ["usb", "nfc", "ble", "internal"].includes(el))) { throw new Error("expected transports of allowCredentials[" + index + "] to be string with value 'usb', 'nfc', 'ble', 'internal' or null"); } } } }
}
this.audit.validExpectations = true;
return true;}
function validateCreateRequest() { let req = this.request;
if (typeof req !== "object") { throw new TypeError("expected request to be Object, got " + typeof req); }
if (!(req.rawId instanceof ArrayBuffer) && !(req.id instanceof ArrayBuffer)) { throw new TypeError("expected 'id' or 'rawId' field of request to be ArrayBuffer, got rawId " + typeof req.rawId + " and id " + typeof req.id); }
if (typeof req.response !== "object") { throw new TypeError("expected 'response' field of request to be Object, got " + typeof req.response); }
if (typeof req.response.attestationObject !== "string" && !(req.response.attestationObject instanceof ArrayBuffer)) { throw new TypeError("expected 'response.attestationObject' to be base64 String or ArrayBuffer"); }
if (typeof req.response.clientDataJSON !== "string" && !(req.response.clientDataJSON instanceof ArrayBuffer)) { throw new TypeError("expected 'response.clientDataJSON' to be base64 String or ArrayBuffer"); }
this.audit.validRequest = true;
return true;}
function validateAssertionResponse() { let req = this.request;
if (typeof req !== "object") { throw new TypeError("expected request to be Object, got " + typeof req); }
if (!(req.rawId instanceof ArrayBuffer) && !(req.id instanceof ArrayBuffer)) { throw new TypeError("expected 'id' or 'rawId' field of request to be ArrayBuffer, got rawId " + typeof req.rawId + " and id " + typeof req.id); }
if (typeof req.response !== "object") { throw new TypeError("expected 'response' field of request to be Object, got " + typeof req.response); }
if (typeof req.response.clientDataJSON !== "string" && !(req.response.clientDataJSON instanceof ArrayBuffer)) { throw new TypeError("expected 'response.clientDataJSON' to be base64 String or ArrayBuffer"); }
if (typeof req.response.authenticatorData !== "string" && !(req.response.authenticatorData instanceof ArrayBuffer)) { throw new TypeError("expected 'response.authenticatorData' to be base64 String or ArrayBuffer"); }
if (typeof req.response.signature !== "string" && !(req.response.signature instanceof ArrayBuffer)) { throw new TypeError("expected 'response.signature' to be base64 String or ArrayBuffer"); }
if (typeof req.response.userHandle !== "string" && !(req.response.userHandle instanceof ArrayBuffer) && req.response.userHandle !== undefined) { throw new TypeError("expected 'response.userHandle' to be base64 String, ArrayBuffer, or undefined"); }
this.audit.validRequest = true;
return true;}
async function validateRawClientDataJson() { // XXX: this isn't very useful, since this has already been parsed... let rawClientDataJson = this.clientData.get("rawClientDataJson");
if (!(rawClientDataJson instanceof ArrayBuffer)) { throw new Error("clientData clientDataJson should be ArrayBuffer"); }
this.audit.journal.add("rawClientDataJson");
return true;}
async function validateTransports() { let transports = this.authnrData.get("transports");
if (transports != null && !Array.isArray(transports)) { throw new Error("expected transports to be 'null' or 'array<string>'"); }
for (const index in transports) { if (typeof transports[index] !== "string") { throw new Error("expected transports[" + index + "] to be 'string'"); } }
this.audit.journal.add("transports");
return true;}
async function validateId() { let rawId = this.clientData.get("rawId");
if (!(rawId instanceof ArrayBuffer)) { throw new Error("expected id to be of type ArrayBuffer"); }
let credId = this.authnrData.get("credId"); if (credId !== undefined && !arrayBufferEquals(rawId, credId)) { throw new Error("id and credId were not the same"); }
let allowCredentials = this.expectations.get("allowCredentials");
if (allowCredentials != undefined) { if (!allowCredentials.some((cred) => { let result = arrayBufferEquals(rawId, cred.id); return result; })) { throw new Error("Credential ID does not match any value in allowCredentials"); } }
this.audit.journal.add("rawId");
return true;}

async function validateOrigin() { let expectedOrigin = this.expectations.get("origin"); let clientDataOrigin = this.clientData.get("origin");
let origin = tools.checkOrigin(clientDataOrigin);
if (origin !== expectedOrigin) { throw new Error("clientData origin did not match expected origin"); }
this.audit.journal.add("origin");
return true;}
async function validateCreateType() { let type = this.clientData.get("type");
if (type !== "webauthn.create") { throw new Error("clientData type should be 'webauthn.create', got: " + type); }
this.audit.journal.add("type");
return true;}
async function validateGetType() { let type = this.clientData.get("type");
if (type !== "webauthn.get") { throw new Error("clientData type should be 'webauthn.get'"); }
this.audit.journal.add("type");
return true;}
async function validateChallenge() { let expectedChallenge = this.expectations.get("challenge"); let challenge = this.clientData.get("challenge");
if (typeof challenge !== "string") { throw new Error("clientData challenge was not a string"); }
if (!isBase64Url(challenge)) { throw new TypeError("clientData challenge was not properly encoded base64url"); }
challenge = challenge.replace(/={1,2}$/, "");
// console.log("challenge", challenge); // console.log("expectedChallenge", expectedChallenge); if (challenge !== expectedChallenge) { throw new Error("clientData challenge mismatch"); }
this.audit.journal.add("challenge");
return true;}
async function validateTokenBinding() { // TODO: node.js can't support token binding right now :( let tokenBinding = this.clientData.get("tokenBinding");
if (typeof tokenBinding === "object") { if (tokenBinding.status !== "not-supported" && tokenBinding.status !== "supported") { throw new Error("tokenBinding status should be 'not-supported' or 'supported', got: " + tokenBinding.status); }
if (Object.keys(tokenBinding).length != 1) { throw new Error("tokenBinding had too many keys"); } } else if (tokenBinding !== undefined) { throw new Error("Token binding field malformed: " + tokenBinding); }
// TODO: add audit.info for token binding status so that it can be used for policies, risk, etc. this.audit.journal.add("tokenBinding");
return true;}
async function validateRawAuthnrData() { // XXX: this isn't very useful, since this has already been parsed... let rawAuthnrData = this.authnrData.get("rawAuthnrData"); if (!(rawAuthnrData instanceof ArrayBuffer)) { throw new Error("authnrData rawAuthnrData should be ArrayBuffer"); }
this.audit.journal.add("rawAuthnrData");
return true;}

async function validateAttestation() { return Fido2Lib.validateAttestation.call(this);}
async function validateAssertionSignature() { let expectedSignature = this.authnrData.get("sig"); let publicKey = this.expectations.get("publicKey"); let rawAuthnrData = this.authnrData.get("rawAuthnrData"); let rawClientData = this.clientData.get("rawClientDataJson");
// console.log("publicKey", publicKey); // printHex("expectedSignature", expectedSignature); // printHex("rawAuthnrData", rawAuthnrData); // printHex("rawClientData", rawClientData);

let clientDataHashBuf = await tools.hashDigest(rawClientData); let clientDataHash = new Uint8Array(clientDataHashBuf).buffer;
let res = await tools.verifySignature( publicKey, expectedSignature, appendBuffer(rawAuthnrData, clientDataHash), "SHA-256", ); if (!res) { throw new Error("signature validation failed"); }
this.audit.journal.add("sig");
return true;}
async function validateRpIdHash() { let rpIdHash = this.authnrData.get("rpIdHash");
if (typeof Buffer !== "undefined" && rpIdHash instanceof Buffer) { rpIdHash = new Uint8Array(rpIdHash).buffer; }
if (!(rpIdHash instanceof ArrayBuffer)) { throw new Error("couldn't coerce clientData rpIdHash to ArrayBuffer"); }
let domain = this.expectations.has("rpId") ? this.expectations.get("rpId") : tools.getHostname(this.expectations.get("origin"));
let createdHash = new Uint8Array(await tools.hashDigest(domain)).buffer;
// wouldn't it be weird if two SHA256 hashes were different lengths...? if (rpIdHash.byteLength !== createdHash.byteLength) { throw new Error("authnrData rpIdHash length mismatch"); }
rpIdHash = new Uint8Array(rpIdHash); createdHash = new Uint8Array(createdHash); for (let i = 0; i < rpIdHash.byteLength; i++) { if (rpIdHash[i] !== createdHash[i]) { throw new TypeError("authnrData rpIdHash mismatch"); } }
this.audit.journal.add("rpIdHash");
return true;}
async function validateFlags() { let expectedFlags = this.expectations.get("flags"); let flags = this.authnrData.get("flags");
for (let expFlag of expectedFlags) { if (expFlag === "UP-or-UV") { if (flags.has("UV")) { if (flags.has("UP")) { continue; } else { throw new Error("expected User Presence (UP) flag to be set if User Verification (UV) is set"); } } else if (flags.has("UP")) { continue; } else { throw new Error("expected User Presence (UP) or User Verification (UV) flag to be set and neither was"); } }
if (expFlag === "UV") { if (flags.has("UV")) { if (flags.has("UP")) { continue; } else { throw new Error("expected User Presence (UP) flag to be set if User Verification (UV) is set"); } } else { throw new Error(`expected flag was not set: ${expFlag}`); } }
if (!flags.has(expFlag)) { throw new Error(`expected flag was not set: ${expFlag}`); } }
this.audit.journal.add("flags");
return true;}
async function validateInitialCounter() { let counter = this.authnrData.get("counter");
// TODO: does counter need to be zero initially? probably not... I guess.. if (typeof counter !== "number") { throw new Error("authnrData counter wasn't a number"); }
this.audit.journal.add("counter");
return true;}
async function validateAaguid() { let aaguid = this.authnrData.get("aaguid");
if (!(aaguid instanceof ArrayBuffer)) { throw new Error("authnrData AAGUID is not ArrayBuffer"); }
if (aaguid.byteLength !== 16) { throw new Error("authnrData AAGUID was wrong length"); }
this.audit.journal.add("aaguid");
return true;}
async function validateCredId() { let credId = this.authnrData.get("credId"); let credIdLen = this.authnrData.get("credIdLen");
if (!(credId instanceof ArrayBuffer)) { throw new Error("authnrData credId should be ArrayBuffer"); }
if (typeof credIdLen !== "number") { throw new Error("authnrData credIdLen should be number, got " + typeof credIdLen); }
if (credId.byteLength !== credIdLen) { throw new Error("authnrData credId was wrong length"); }
this.audit.journal.add("credId"); this.audit.journal.add("credIdLen");
return true;}
async function validatePublicKey() { // XXX: the parser has already turned this into PEM at this point // if something were malformatted or wrong, we probably would have // thrown an error well before this. // Maybe we parse the ASN.1 and make sure attributes are correct? // Doesn't seem very worthwhile...
let cbor = this.authnrData.get("credentialPublicKeyCose"); let jwk = this.authnrData.get("credentialPublicKeyJwk"); let pem = this.authnrData.get("credentialPublicKeyPem");
// cbor if (!(cbor instanceof ArrayBuffer)) { throw new Error("authnrData credentialPublicKeyCose isn't of type ArrayBuffer"); } this.audit.journal.add("credentialPublicKeyCose");
// jwk if (typeof jwk !== "object") { throw new Error("authnrData credentialPublicKeyJwk isn't of type Object"); }
if (typeof jwk.kty !== "string") { throw new Error("authnrData credentialPublicKeyJwk.kty isn't of type String"); }
if (typeof jwk.alg !== "string") { throw new Error("authnrData credentialPublicKeyJwk.alg isn't of type String"); }
switch (jwk.kty) { case "EC": if (typeof jwk.crv !== "string") { throw new Error("authnrData credentialPublicKeyJwk.crv isn't of type String"); } break; case "RSA": if (typeof jwk.n !== "string") { throw new Error("authnrData credentialPublicKeyJwk.n isn't of type String");
}
if (typeof jwk.e !== "string") { throw new Error("authnrData credentialPublicKeyJwk.e isn't of type String"); } break; default: throw new Error("authnrData unknown JWK key type: " + jwk.kty); }
this.audit.journal.add("credentialPublicKeyJwk");
// pem if (typeof pem !== "string") { throw new Error("authnrData credentialPublicKeyPem isn't of type String"); }
if (!isPem(pem)) { throw new Error("authnrData credentialPublicKeyPem was malformatted"); } this.audit.journal.add("credentialPublicKeyPem");
return true;}
async function validateUserHandle() { let userHandle = this.authnrData.get("userHandle");
if (userHandle === undefined || userHandle === null || userHandle === "") { this.audit.journal.add("userHandle"); return true; }
userHandle = coerceToBase64Url(userHandle, "userHandle"); let expUserHandle = this.expectations.get("userHandle"); if (typeof userHandle === "string" && userHandle === expUserHandle) { this.audit.journal.add("userHandle"); return true; }
throw new Error("unable to validate userHandle");}
async function validateCounter() { let prevCounter = this.expectations.get("prevCounter"); let counter = this.authnrData.get("counter"); let counterSupported = !(counter === 0 && prevCounter === 0);
if (counter <= prevCounter && counterSupported) { throw new Error("counter rollback detected"); }
this.audit.journal.add("counter"); this.audit.info.set("counter-supported", "" + counterSupported);
return true;}
async function validateAudit() { let journal = this.audit.journal; let clientData = this.clientData; let authnrData = this.authnrData;
for (let kv of clientData) { let val = kv[0]; if (!journal.has(val)) { throw new Error(`internal audit failed: ${val} was not validated`); } }
for (let kv of authnrData) { let val = kv[0]; if (!journal.has(val)) { throw new Error(`internal audit failed: ${val} was not validated`); } }
if (journal.size !== (clientData.size + authnrData.size)) { throw new Error(`internal audit failed: ${journal.size} fields checked; expected ${clientData.size + authnrData.size}`); }
if (!this.audit.validExpectations) { throw new Error("internal audit failed: expectations not validated"); }
if (!this.audit.validRequest) { throw new Error("internal audit failed: request not validated"); }
this.audit.complete = true;
return true;}
function attach(o) { let mixins = { validateExpectations, validateCreateRequest, // clientData validators validateRawClientDataJson, validateOrigin, validateId, validateCreateType, validateGetType, validateChallenge, validateTokenBinding, validateTransports, // authnrData validators validateRawAuthnrData, validateAttestation, validateAssertionSignature, validateRpIdHash, validateAaguid, validateCredId, validatePublicKey, validateFlags, validateUserHandle, validateCounter, validateInitialCounter, validateAssertionResponse, // audit structures audit: { validExpectations: false, validRequest: false, complete: false, journal: new Set(), warning: new Map(), info: new Map(), }, validateAudit, };
for (let key of Object.keys(mixins)) { o[key] = mixins[key]; }}
export { attach };