Skip to main content
Module

x/fido2/lib/main.js

A node.js library for performing FIDO 2.0 / WebAuthn server functionality
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
import * as utils from "./utils.js";
import { Fido2AssertionResult, Fido2AttestationResult, Fido2Result } from "./response.js";
import { MdsCollection, MdsEntry } from "./mds.js";
// add 'none' attestation formatimport { noneAttestation } from "./attestations/none.js";
// add 'packed' attestation formatimport { packedAttestation } from "./attestations/packed.js";
// add 'fidoU2F' attestation formatimport { fidoU2fAttestation } from "./attestations/fidoU2F.js";
// add 'androidSafetyNet' attestation formatimport { androidSafetyNetAttestation } from "./attestations/androidSafetyNet.js";
// add 'tpm' attestation formatimport { tpmAttestation } from "./attestations/tpm.js";
// add 'apple' attestation formatimport { appleAttestation } from "./attestations/apple.js";const { coerceToArrayBuffer, abToBuf, tools, appendBuffer,} = utils;
const globalAttestationMap = new Map();const globalExtensionMap = new Map();const globalMdsCollection = new Map();
class Fido2Lib { /** * Creates a FIDO2 server class * @param {Object} opts Options for the server * @param {Number} [opts.timeout=60000] The amount of time to wait, in milliseconds, before a call has timed out * @param {String} [opts.rpId="localhost"] The name of the server * @param {String} [opts.rpName="Anonymous Service"] The name of the server * @param {String} [opts.rpIcon] A URL for the service's icon. Can be a [RFC 2397]{@link https://tools.ietf.org/html/rfc2397} data URL. * @param {Number} [opts.challengeSize=64] The number of bytes to use for the challenge * @param {Object} [opts.authenticatorSelectionCriteria] An object describing what types of authenticators are allowed to register with the service. * See [AuthenticatorSelectionCriteria]{@link https://w3.org/TR/webauthn/#authenticatorSelection} in the WebAuthn spec for details. * @param {String} [opts.authenticatorAttachment] Indicates whether authenticators should be part of the OS ("platform"), or can be roaming authenticators ("cross-platform") * @param {Boolean} [opts.authenticatorRequireResidentKey] Indicates whether authenticators must store the key internally (true) or if they can use a KDF to generate keys * @param {String} [opts.authenticatorUserVerification] Indicates whether user verification should be performed. Options are "required", "preferred", or "discouraged". * @param {String} [opts.attestation="direct"] The preferred attestation type to be used. * See [AttestationConveyancePreference]{https://w3.org/TR/webauthn/#enumdef-attestationconveyancepreference} in the WebAuthn spec * @param {Array<Number>} [opts.cryptoParams] A list of COSE algorithm identifiers (e.g. -7) * ordered by the preference in which the authenticator should use them. */ constructor(opts) { /* eslint complexity: ["off"] */ opts = opts || {};
// set defaults this.config = {};
// timeout this.config.timeout = (opts.timeout === undefined) ? 60000 : opts.timeout; // 1 minute checkOptType(this.config, "timeout", "number"); if (!(this.config.timeout >>> 0 === parseFloat(this.config.timeout))) { throw new RangeError("timeout should be zero or positive integer"); }
// challengeSize this.config.challengeSize = opts.challengeSize || 64; checkOptType(this.config, "challengeSize", "number"); if (this.config.challengeSize < 32) { throw new RangeError( "challenge size too small, must be 32 or greater", ); }
// rpId this.config.rpId = opts.rpId; checkOptType(this.config, "rpId", "string");
// rpName this.config.rpName = opts.rpName || "Anonymous Service"; checkOptType(this.config, "rpName", "string");
// rpIcon this.config.rpIcon = opts.rpIcon; checkOptType(this.config, "rpIcon", "string");
// authenticatorRequireResidentKey this.config.authenticatorRequireResidentKey = opts.authenticatorRequireResidentKey; checkOptType(this.config, "authenticatorRequireResidentKey", "boolean");
// authenticatorAttachment this.config.authenticatorAttachment = opts.authenticatorAttachment; if ( this.config.authenticatorAttachment !== undefined && (this.config.authenticatorAttachment !== "platform" && this.config.authenticatorAttachment !== "cross-platform") ) { throw new TypeError( "expected authenticatorAttachment to be 'platform', or 'cross-platform', got: " + this.config.authenticatorAttachment, ); }
// authenticatorUserVerification this.config.authenticatorUserVerification = opts.authenticatorUserVerification; if ( this.config.authenticatorUserVerification !== undefined && (this.config.authenticatorUserVerification !== "required" && this.config.authenticatorUserVerification !== "preferred" && this.config.authenticatorUserVerification !== "discouraged") ) { throw new TypeError( "expected authenticatorUserVerification to be 'required', 'preferred', or 'discouraged', got: " + this.config.authenticatorUserVerification, ); }
// attestation this.config.attestation = opts.attestation || "direct"; if ( this.config.attestation !== "direct" && this.config.attestation !== "indirect" && this.config.attestation !== "none" ) { throw new TypeError( "expected attestation to be 'direct', 'indirect', or 'none', got: " + this.config.attestation, ); }
// cryptoParams this.config.cryptoParams = opts.cryptoParams || [-7, -257]; checkOptType(this.config, "cryptoParams", Array); if (this.config.cryptoParams.length < 1) { throw new TypeError("cryptoParams must have at least one element"); } this.config.cryptoParams.forEach((param) => { checkOptType({ cryptoParam: param }, "cryptoParam", "number"); });
this.attestationMap = globalAttestationMap; this.extSet = new Set(); // enabled extensions (all disabled by default) this.extOptMap = new Map(); // default options for extensions
// TODO: convert icon file to data-URL icon // TODO: userVerification }
/** * Creates a new {@link MdsCollection} * @param {String} collectionName The name of the collection to create. * Used to identify the source of a {@link MdsEntry} when {@link Fido2Lib#findMdsEntry} * finds multiple matching entries from different sources (e.g. FIDO MDS 1 & FIDO MDS 2) * @return {MdsCollection} The MdsCollection that was created * @see MdsCollection */ static createMdsCollection(collectionName) { return new MdsCollection(collectionName); }
/** * Adds a new {@link MdsCollection} to the global MDS collection list that will be used for {@link findMdsEntry} * @param {MdsCollection} mdsCollection The MDS collection that will be used * @see MdsCollection */ static async addMdsCollection(mdsCollection) { if (!(mdsCollection instanceof MdsCollection)) { throw new Error( "expected 'mdsCollection' to be instance of MdsCollection, got: " + mdsCollection, ); } await mdsCollection.validate(); globalMdsCollection.set(mdsCollection.name, mdsCollection); }
/** * Removes all entries from the global MDS collections list. Mostly used for testing. */ static clearMdsCollections() { globalMdsCollection.clear(); }
/** * Returns {@link MdsEntry} objects that match the requested id. The * lookup is done by calling {@link MdsCollection#findEntry} on the current global * MDS collection. If no global MDS collection has been specified using * {@link setMdsCollection}, an `Error` will be thrown. * @param {String|ArrayBuffer} id The authenticator id to look up metadata for * @return {Array.<MdsEntry>} Returns an Array of {@link MdsEntry} for the specified id. * If no entry was found, the Array will be empty. * @see MdsCollection */ static findMdsEntry(id) { if (globalMdsCollection.size < 1) { throw new Error( "must set MDS collection before attempting to find an MDS entry", ); }
const ret = []; for (const collection of globalMdsCollection.values()) { const entry = collection.findEntry(id); if (entry) ret.push(entry); }
return ret; }
/** * Adds a new global extension that will be available to all instantiations of * {@link Fido2Lib}. Note that the extension must still be enabled by calling * {@link enableExtension} for each instantiation of a Fido2Lib. * @param {String} extName The name of the extension to add. (e.g. - "appid") * @param {Function} optionGeneratorFn Extensions are included in * @param {Function} resultParserFn [description] * @param {Function} resultValidatorFn [description] */ static addExtension( extName, optionGeneratorFn, resultParserFn, resultValidatorFn, ) { if (typeof extName !== "string") { throw new Error("expected 'extName' to be String, got: " + extName); }
if (globalExtensionMap.has(extName)) { throw new Error( `the extension '${extName}' has already been added`, ); }
if (typeof optionGeneratorFn !== "function") { throw new Error( "expected 'optionGeneratorFn' to be a Function, got: " + optionGeneratorFn, ); }
if (typeof resultParserFn !== "function") { throw new Error( "expected 'resultParserFn' to be a Function, got: " + resultParserFn, ); }
if (typeof resultValidatorFn !== "function") { throw new Error( "expected 'resultValidatorFn' to be a Function, got: " + resultValidatorFn, ); }
globalExtensionMap.set(extName, { optionGeneratorFn, resultParserFn, resultValidatorFn, }); }
/** * Removes all extensions from the global extension registry. Mostly used for testing. */ static deleteAllExtensions() { globalExtensionMap.clear(); }
/** * Generates the options to send to the client for the specified extension * @private * @param {String} extName The name of the extension to generate options for. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension} * @param {String} type The type of options that are being generated. Valid options are "attestation" or "assertion". * @param {Any} [options] Optional parameters to pass to the generator function * @return {Any} The extension value that will be sent to the client. If `undefined`, this extension won't be included in the * options sent to the client. */ generateExtensionOptions(extName, type, options) { if (typeof extName !== "string") { throw new Error("expected 'extName' to be String, got: " + extName); }
if (type !== "attestation" && type !== "assertion") { throw new Error( "expected 'type' to be 'attestation' or 'assertion', got: " + type, ); }
const ext = globalExtensionMap.get(extName); if ( typeof ext !== "object" || typeof ext.optionGeneratorFn !== "function" ) { throw new Error(`valid extension for '${extName}' not found`); } const ret = ext.optionGeneratorFn(extName, type, options);
return ret; }
static parseExtensionResult(extName, clientThing, authnrThing) { if (typeof extName !== "string") { throw new Error("expected 'extName' to be String, got: " + extName); }
const ext = globalExtensionMap.get(extName); if ( typeof ext !== "object" || typeof ext.parseFn !== "function" ) { throw new Error(`valid extension for '${extName}' not found`); } const ret = ext.parseFn(extName, clientThing, authnrThing);
return ret; }
static validateExtensionResult(extName) { const ext = globalExtensionMap.get(extName); if ( typeof ext !== "object" || typeof ext.validateFn !== "function" ) { throw new Error(`valid extension for '${extName}' not found`); } const ret = ext.validateFn.call(this);
return ret; }
/** * Enables the specified extension. * @param {String} extName The name of the extension to enable. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension} */ enableExtension(extName) { if (typeof extName !== "string") { throw new Error("expected 'extName' to be String, got: " + extName); }
if (!globalExtensionMap.has(extName)) { throw new Error(`valid extension for '${extName}' not found`); }
this.extSet.add(extName); }
/** * Disables the specified extension. * @param {String} extName The name of the extension to enable. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension} */ disableExtension(extName) { if (typeof extName !== "string") { throw new Error("expected 'extName' to be String, got: " + extName); }
if (!globalExtensionMap.has(extName)) { throw new Error(`valid extension for '${extName}' not found`); }
this.extSet.delete(extName); }
/** * Specifies the options to be used for the extension * @param {String} extName The name of the extension to set the options for (e.g. - "appid". Must be a valid extension that has been registered through {@link Fido2Lib#addExtension} * @param {Any} options The parameter that will be passed to the option generator function (e.g. - "https://webauthn.org") */ setExtensionOptions(extName, options) { if (typeof extName !== "string") { throw new Error("expected 'extName' to be String, got: " + extName); }
if (!globalExtensionMap.has(extName)) { throw new Error(`valid extension for '${extName}' not found`); }
this.extOptMap.set(extName, options); }
/** * Validates an attestation response. Will be called within the context (`this`) of a {@link Fido2AttestationResult} * @private */ static async validateAttestation() { const fmt = this.authnrData.get("fmt");
// validate input if (typeof fmt !== "string") { throw new TypeError( "expected 'fmt' to be string, got: " + typeof fmt, ); }
// get from attestationMap const fmtObj = globalAttestationMap.get(fmt); if ( typeof fmtObj !== "object" || typeof fmtObj.parseFn !== "function" || typeof fmtObj.validateFn !== "function" ) { throw new Error(`no support for attestation format: ${fmt}`); }
// call fn const ret = await fmtObj.validateFn.call(this);
// validate return if (ret !== true) { throw new Error(`${fmt} validateFn did not return 'true'`); }
// return result return ret; }
/** * Adds a new attestation format that will automatically be recognized and parsed * for any future {@link Fido2CreateRequest} messages * @param {String} fmt The name of the attestation format, as it appears in the * ARIN registry and / or as it will appear in the {@link Fido2CreateRequest} * message that is received * @param {Function} parseFn The function that will be called to parse the * attestation format. It will receive the `attStmt` as a parameter and will be * called from the context (`this`) of the `Fido2CreateRequest` * @param {Function} validateFn The function that will be called to validate the * attestation format. It will receive no arguments, as all the necessary * information for validating the attestation statement will be contained in the * calling context (`this`). */ static addAttestationFormat(fmt, parseFn, validateFn) { // validate input if (typeof fmt !== "string") { throw new TypeError( "expected 'fmt' to be string, got: " + typeof fmt, ); }
if (typeof parseFn !== "function") { throw new TypeError( "expected 'parseFn' to be string, got: " + typeof parseFn, ); }
if (typeof validateFn !== "function") { throw new TypeError( "expected 'validateFn' to be string, got: " + typeof validateFn, ); }
if (globalAttestationMap.has(fmt)) { throw new Error(`can't add format: '${fmt}' already exists`); }
// add to attestationMap globalAttestationMap.set(fmt, { parseFn, validateFn, });
return true; }
/** * Deletes all currently registered attestation formats. */ static deleteAllAttestationFormats() { globalAttestationMap.clear(); }
/** * Parses an attestation statememnt of the format specified * @private * @param {String} fmt The name of the format to be parsed, as specified in the * ARIN registry of attestation formats. * @param {Object} attStmt The attestation object to be parsed. * @return {Map} A Map of all the attestation fields that were parsed. * At this point the fields have not yet been verified. * @throws {Error} when a field cannot be parsed or verified. * @throws {TypeError} when supplied parameters `fmt` or `attStmt` are of the * wrong type */ static parseAttestation(fmt, attStmt) { // validate input if (typeof fmt !== "string") { throw new TypeError( "expected 'fmt' to be string, got: " + typeof fmt, ); }
if (typeof attStmt !== "object") { throw new TypeError( "expected 'attStmt' to be object, got: " + typeof attStmt, ); }
// get from attestationMap const fmtObj = globalAttestationMap.get(fmt); if ( typeof fmtObj !== "object" || typeof fmtObj.parseFn !== "function" || typeof fmtObj.validateFn !== "function" ) { throw new Error(`no support for attestation format: ${fmt}`); }
// call fn const ret = fmtObj.parseFn.call(this, attStmt);
// validate return if (!(ret instanceof Map)) { throw new Error(`${fmt} parseFn did not return a Map`); }
// return result return new Map([ ["fmt", fmt], ...ret, ]); }
/** * Parses and validates an attestation response from the client * @param {Object} res The assertion result that was generated by the client. * See {@link https://w3.org/TR/webauthn/#authenticatorattestationresponse AuthenticatorAttestationResponse} in the WebAuthn spec. * @param {String} [res.id] The base64url encoded id returned by the client * @param {String} [res.rawId] The base64url encoded rawId returned by the client. If `res.rawId` is missing, `res.id` will be used instead. If both are missing an error will be thrown. * @param {String} res.response.clientDataJSON The base64url encoded clientDataJSON returned by the client * @param {String} res.response.authenticatorData The base64url encoded authenticatorData returned by the client * @param {Object} expected The expected parameters for the assertion response. * If these parameters don't match the recieved values, validation will fail and an error will be thrown. * @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions} * @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org" * @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either". * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.). * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button). * If "either", then either "first" or "second" is acceptable * @return {Promise<Fido2AttestationResult>} Returns a Promise that resolves to a {@link Fido2AttestationResult} * @throws {Error} If parsing or validation fails */ async attestationResult(res, expected) { expected.flags = factorToFlags(expected.factor, ["AT"]); delete expected.factor; return await Fido2AttestationResult.create(res, expected); }
/** * Parses and validates an assertion response from the client * @param {Object} res The assertion result that was generated by the client. * See {@link https://w3.org/TR/webauthn/#authenticatorassertionresponse AuthenticatorAssertionResponse} in the WebAuthn spec. * @param {String} [res.id] The base64url encoded id returned by the client * @param {String} [res.rawId] The base64url encoded rawId returned by the client. If `res.rawId` is missing, `res.id` will be used instead. If both are missing an error will be thrown. * @param {String} res.response.clientDataJSON The base64url encoded clientDataJSON returned by the client * @param {String} res.response.attestationObject The base64url encoded authenticatorData returned by the client * @param {String} res.response.signature The base64url encoded signature returned by the client * @param {String|null} [res.response.userHandle] The base64url encoded userHandle returned by the client. May be null or an empty string. * @param {Object} expected The expected parameters for the assertion response. * If these parameters don't match the recieved values, validation will fail and an error will be thrown. * @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions} * @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org" * @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either". * If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.). * If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button). * If "either", then either "first" or "second" is acceptable * @param {String} expected.publicKey A PEM encoded public key that will be used to validate the assertion response signature. * This is the public key that was returned for this user during [attestationResult]{@link Fido2Lib#attestationResult} * @param {Number} expected.prevCounter The previous value of the signature counter for this authenticator. * @param {String|null} expected.userHandle The expected userHandle, which was the user.id during registration * @return {Promise<Fido2AssertionResult>} Returns a Promise that resolves to a {@link Fido2AssertionResult} * @throws {Error} If parsing or validation fails */ // deno-lint-ignore require-await async assertionResult(res, expected) { expected.flags = factorToFlags(expected.factor, []); delete expected.factor; return Fido2AssertionResult.create(res, expected); }
/** * Gets a challenge and any other parameters for the `navigator.credentials.create()` call * The `challenge` property is an `ArrayBuffer` and will need to be encoded to be transmitted to the client. * @param {Object} [opts] An object containing various options for the option creation * @param {Object} [opts.extensionOptions] An object that contains the extensions to enable, and the options to use for each of them. * The keys of this object are the names of the extensions (e.g. - "appid"), and the value of each key is the option that will * be passed to that extension when it is generating the value to send to the client. This object overrides the extensions that * have been set with {@link enableExtension} and the options that have been set with {@link setExtensionOptions}. If an extension * was enabled with {@link enableExtension} but it isn't included in this object, the extension won't be sent to the client. Likewise, * if an extension was disabled with {@link disableExtension} but it is included in this object, it will be sent to the client. * @param {String} [extraData] Extra data to be signed by the authenticator during attestation. The challenge will be a hash: * SHA256(rawChallenge + extraData) and the `rawChallenge` will be returned as part of PublicKeyCredentialCreationOptions. * @returns {Promise<PublicKeyCredentialCreationOptions>} The options for creating calling `navigator.credentials.create()` */ async attestationOptions(opts) { opts = opts || {};
// The object being returned is described here: // https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions let challenge = tools.randomValues(this.config.challengeSize); challenge = coerceToArrayBuffer(challenge, "challenge"); const pubKeyCredParams = []; this.config.cryptoParams.forEach((coseId) => { pubKeyCredParams.push({ type: "public-key", alg: coseId, }); });
// mix extraData into challenge let rawChallenge; if (opts.extraData) { rawChallenge = challenge; const extraData = coerceToArrayBuffer(opts.extraData, "extraData"); const hash = await tools.hashDigest( appendBuffer(challenge, extraData), ); challenge = new Uint8Array(hash).buffer; }
const options = { rp: {}, user: {}, };
const extensions = createExtensions.call( this, "attestation", opts.extensionOptions, );
/** * @typedef {Object} PublicKeyCredentialCreationOptions * @description This object is returned by {@link attestationOptions} and is basially the same as * the [PublicKeyCredentialCreationOptions]{@link https://w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions} * object that is required to be passed to `navigator.credentials.create()`. With the exception of the `challenge` property, * all other properties are optional and only set if they were specified in the configuration paramater * that was passed to the constructor. * @property {Object} rp Relying party information (a.k.a. - server / service information) * @property {String} [rp.name] Relying party name (e.g. - "ACME"). This is only set if `rpName` was specified during the `new` call. * @property {String} [rp.id] Relying party ID, a domain name (e.g. - "example.com"). This is only set if `rpId` was specified during the `new` call. * @property {Object} user User information. This will be an empty object * @property {ArrayBuffer} challenge An ArrayBuffer filled with random bytes. This will be verified in {@link attestationResult} * @property {Array} [pubKeyCredParams] A list of PublicKeyCredentialParameters objects, based on the `cryptoParams` that was passed to the constructor. * @property {Number} [timeout] The amount of time that the call should take before returning an error * @property {String} [attestation] Whether the client should request attestation from the authenticator or not * @property {Object} [authenticatorSelectionCriteria] A object describing which authenticators are preferred for registration * @property {String} [authenticatorSelectionCriteria.attachment] What type of attachement is acceptable for new authenticators. * Allowed values are "platform", meaning that the authenticator is embedded in the operating system, or * "cross-platform", meaning that the authenticator is removeable (e.g. USB, NFC, or BLE). * @property {Boolean} [authenticatorSelectionCriteria.requireResidentKey] Indicates whether authenticators must store the keys internally, or if they can * store them externally (using a KDF or key wrapping) * @property {String} [authenticatorSelectionCriteria.userVerification] Indicates whether user verification is required for authenticators. User verification * means that an authenticator will validate a use through their biometrics (e.g. fingerprint) or knowledge (e.g. PIN). Allowed * values for `userVerification` are "required", meaning that registration will fail if no authenticator provides user verification; * "preferred", meaning that if multiple authenticators are available, the one(s) that provide user verification should be used; or * "discouraged", which means that authenticators that don't provide user verification are preferred. * @property {ArrayBuffer} [rawChallenge] If `extraData` was passed to {@link attestationOptions}, this * will be the original challenge used, and `challenge` will be a hash: * SHA256(rawChallenge + extraData) * @property {Object} [extensions] The values of any enabled extensions. */ setOpt(options.rp, "name", this.config.rpName); setOpt(options.rp, "id", this.config.rpId); setOpt(options.rp, "icon", this.config.rpIcon); setOpt(options, "challenge", challenge); setOpt(options, "pubKeyCredParams", pubKeyCredParams); setOpt(options, "timeout", this.config.timeout); setOpt(options, "attestation", this.config.attestation); if ( this.config.authenticatorAttachment !== undefined || this.config.authenticatorRequireResidentKey !== undefined || this.config.authenticatorUserVerification !== undefined ) { options.authenticatorSelection = {}; setOpt( options.authenticatorSelection, "authenticatorAttachment", this.config.authenticatorAttachment, ); setOpt( options.authenticatorSelection, "requireResidentKey", this.config.authenticatorRequireResidentKey, ); setOpt( options.authenticatorSelection, "userVerification", this.config.authenticatorUserVerification, ); } setOpt(options, "rawChallenge", rawChallenge);
if (Object.keys(extensions).length > 0) { options.extensions = extensions; }
return options; }
/** * Creates an assertion challenge and any other parameters for the `navigator.credentials.get()` call. * The `challenge` property is an `ArrayBuffer` and will need to be encoded to be transmitted to the client. * @param {Object} [opts] An object containing various options for the option creation * @param {Object} [opts.extensionOptions] An object that contains the extensions to enable, and the options to use for each of them. * The keys of this object are the names of the extensions (e.g. - "appid"), and the value of each key is the option that will * be passed to that extension when it is generating the value to send to the client. This object overrides the extensions that * have been set with {@link enableExtension} and the options that have been set with {@link setExtensionOptions}. If an extension * was enabled with {@link enableExtension} but it isn't included in this object, the extension won't be sent to the client. Likewise, * if an extension was disabled with {@link disableExtension} but it is included in this object, it will be sent to the client. * @param {String} [extraData] Extra data to be signed by the authenticator during attestation. The challenge will be a hash: * SHA256(rawChallenge + extraData) and the `rawChallenge` will be returned as part of PublicKeyCredentialCreationOptions. * @returns {Promise<PublicKeyCredentialRequestOptions>} The options to be passed to `navigator.credentials.get()` */ async assertionOptions(opts) { opts = opts || {};
// https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions let challenge = tools.randomValues(this.config.challengeSize); challenge = coerceToArrayBuffer(challenge, "challenge"); const options = {};
// mix extraData into challenge let rawChallenge; if (opts.extraData) { rawChallenge = challenge; const extraData = coerceToArrayBuffer(opts.extraData, "extraData"); challenge = abToBuf( await tools.hashDigest(appendBuffer(challenge, extraData)), ); }
const extensions = createExtensions.call( this, "assertion", opts.extensionOptions, );
/** * @typedef {Object} PublicKeyCredentialRequestOptions * @description This object is returned by {@link assertionOptions} and is basially the same as * the [PublicKeyCredentialRequestOptions]{@link https://w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions} * object that is required to be passed to `navigator.credentials.get()`. With the exception of the `challenge` property, * all other properties are optional and only set if they were specified in the configuration paramater * that was passed to the constructor. * @property {ArrayBuffer} challenge An ArrayBuffer filled with random bytes. This will be verified in {@link attestationResult} * @property {Number} [timeout] The amount of time that the call should take before returning an error * @property {String} [rpId] Relying party ID, a domain name (e.g. - "example.com"). This is only set if `rpId` was specified during the `new` call. * @property {String} [attestation] Whether the client should request attestation from the authenticator or not * @property {String} [userVerification] Indicates whether user verification is required for authenticators. User verification * means that an authenticator will validate a use through their biometrics (e.g. fingerprint) or knowledge (e.g. PIN). Allowed * values for `userVerification` are "required", meaning that authentication will fail if no authenticator provides user verification; * "preferred", meaning that if multiple authenticators are available, the one(s) that provide user verification should be used; or * "discouraged", which means that authenticators that don't provide user verification are preferred. * @property {ArrayBuffer} [rawChallenge] If `extraData` was passed to {@link attestationOptions}, this * will be the original challenge used, and `challenge` will be a hash: * SHA256(rawChallenge + extraData) * @property {Object} [extensions] The values of any enabled extensions. */ setOpt(options, "challenge", challenge); setOpt(options, "timeout", this.config.timeout); setOpt(options, "rpId", this.config.rpId); setOpt( options, "userVerification", this.config.authenticatorUserVerification, );
setOpt(options, "rawChallenge", rawChallenge);
if (Object.keys(extensions).length > 0) { options.extensions = extensions; }
return options; }}
function checkOptType(opts, prop, type) { if (typeof opts !== "object") return;
// undefined if (opts[prop] === undefined) return;
// native type if (typeof type === "string") { // deno-lint-ignore valid-typeof if (typeof opts[prop] !== type) { throw new TypeError( `expected ${prop} to be ${type}, got: ${opts[prop]}`, ); } }
// class type if (typeof type === "function") { if (!(opts[prop] instanceof type)) { throw new TypeError( `expected ${prop} to be ${type.name}, got: ${opts[prop]}`, ); } }}
function setOpt(obj, prop, val) { if (val !== undefined) { obj[prop] = val; }}
function factorToFlags(expectedFactor, flags) { // var flags = ["AT"]; flags = flags || [];
switch (expectedFactor) { case "first": flags.push("UP"); flags.push("UV"); break; case "second": flags.push("UP"); break; case "either": flags.push("UP-or-UV"); break; default: throw new TypeError( "expectedFactor should be 'first', 'second' or 'either'", ); }
return flags;}
function createExtensions(type, extObj) { /* eslint-disable no-invalid-this */ const extensions = {};
// default extensions let enabledExtensions = this.extSet; let extensionsOptions = this.extOptMap;
// passed in extensions if (typeof extObj === "object") { enabledExtensions = new Set(Object.keys(extObj)); extensionsOptions = new Map(); for (const key of Object.keys(extObj)) { extensionsOptions.set(key, extObj[key]); } }
// generate extension values for (const extension of enabledExtensions) { const extVal = this.generateExtensionOptions( extension, type, extensionsOptions.get(extension), ); if (extVal !== undefined) extensions[extension] = extVal; }
return extensions;}Fido2Lib.addAttestationFormat( noneAttestation.name, noneAttestation.parseFn, noneAttestation.validateFn,);Fido2Lib.addAttestationFormat( packedAttestation.name, packedAttestation.parseFn, packedAttestation.validateFn,);Fido2Lib.addAttestationFormat( fidoU2fAttestation.name, fidoU2fAttestation.parseFn, fidoU2fAttestation.validateFn,);Fido2Lib.addAttestationFormat( androidSafetyNetAttestation.name, androidSafetyNetAttestation.parseFn, androidSafetyNetAttestation.validateFn,);Fido2Lib.addAttestationFormat( tpmAttestation.name, tpmAttestation.parseFn, tpmAttestation.validateFn,);Fido2Lib.addAttestationFormat( appleAttestation.name, appleAttestation.parseFn, appleAttestation.validateFn);export { Fido2Lib };
// Export all helpers
export { arrayBufferEquals, abToBuf, abToHex, appendBuffer, coerceToArrayBuffer, coerceToBase64, coerceToBase64Url, isBase64Url, isPem, jsObjectToB64, pemToBase64, str2ab, tools} from "./utils.js";
export { parseAttestationObject, parseAuthenticatorData, parseAuthnrAssertionResponse, parseAuthnrAttestationResponse, parseClientResponse, parseExpectations} from "./parser.js";
export { Certificate, CertManager, CRL, helpers } from "./certUtils.js";export { PublicKey, coseAlgToHashStr, coseAlgToStr } from "./keyUtils.js";
// Export responseexport { Fido2AssertionResult, Fido2AttestationResult, Fido2Result };
// Export validatorexport { attach } from "./validator.js";
// Export mdsexport { MdsCollection, MdsEntry };
// Export attestationsexport { androidSafetyNetAttestation, fidoU2fAttestation, noneAttestation, packedAttestation, tpmAttestation, appleAttestation };