Skip to main content
Module

x/fido2/lib/toolbox.js

A node.js library for performing FIDO 2.0 / WebAuthn server functionality
Go to Latest
File
// External dependenciesimport { URL } from "url";import { parse as tldtsParse } from "tldts";import punycode from "punycode";import { decodeProtectedHeader, exportSPKI, importJWK, importSPKI, jwtVerify } from "jose";import { Certificate as PkijsCertificate, CertificateChainValidationEngine, CertificateRevocationList, CryptoEngine, PublicKeyInfo, setEngine} from "pkijs";import { fromBER } from "asn1js";import * as cbor from "cbor-x";import base64 from "@hexagon/base64";
// Internal dependenciesimport { Certificate } from "./certUtils.js";import { Key } from "./keyUtils.js";
// Import webcryptoimport * as platformCrypto from "crypto";import * as peculiarCrypto from "@peculiar/webcrypto";let webcrypto;if ((typeof self !== "undefined") && "crypto" in self) { // Always use crypto if available natively (browser / Deno) console.warn("[FIDO2-LIB] Native crypto is enabled"); webcrypto = self.crypto;
} else { // Always use node webcrypto if available ( >= 16.0 ) if(platformCrypto && platformCrypto.webcrypto) { console.warn("[FIDO2-LIB] Native crypto is enabled"); webcrypto = platformCrypto.webcrypto;
} else { // Fallback to @peculiar/webcrypto webcrypto = new peculiarCrypto.Crypto(); }}
// Set up pkijsconst pkijs = { setEngine, CryptoEngine, Certificate: PkijsCertificate, CertificateRevocationList, CertificateChainValidationEngine, PublicKeyInfo,};pkijs.setEngine( "newEngine", webcrypto, new pkijs.CryptoEngine({ name: "", crypto: webcrypto, subtle: webcrypto.subtle, }),);
/* Convert signature from DER to raw Expects Uint8Array*/function derToRaw(signature) { const rStart = signature[4] === 0 ? 5 : 4; const rEnd = rStart + 32; const sStart = signature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2; return new Uint8Array([ ...signature.slice(rStart, rEnd), ...signature.slice(sStart), ]);}
function checkOrigin(str) { const originUrl = new URL(str); const origin = originUrl.origin;
if (origin !== str) { throw new Error("origin was malformatted"); }
const isLocalhost = (originUrl.hostname == "localhost" || originUrl.hostname.endsWith(".localhost"));
if (originUrl.protocol !== "https:" && !isLocalhost) { throw new Error("origin should be https"); }
if ( (!validDomainName(originUrl.hostname) || !validEtldPlusOne(originUrl.hostname)) && !isLocalhost ) { throw new Error("origin is not a valid eTLD+1"); }
return origin;}
function checkUrl(value, name, rules = {}) { if (!name) { throw new TypeError("name not specified in checkUrl"); }
if (typeof value !== "string") { throw new Error(`${name} must be a string`); }
let urlValue = null; try { urlValue = new URL(value); } catch (_err) { throw new Error(`${name} is not a valid eTLD+1/url`); }
if (!value.startsWith("http")) { throw new Error(`${name} must be http protocol`); }
if (!rules.allowHttp && urlValue.protocol !== "https:") { throw new Error(`${name} should be https`); }
// origin: base url without path including / if ( !rules.allowPath && (value.endsWith("/") || urlValue.pathname !== "/") ) { // urlValue adds / in path always throw new Error(`${name} should not include path in url`); }
if (!rules.allowHash && urlValue.hash) { throw new Error(`${name} should not include hash in url`); }
if (!rules.allowCred && (urlValue.username || urlValue.password)) { throw new Error(`${name} should not include credentials in url`); }
if (!rules.allowQuery && urlValue.search) { throw new Error(`${name} should not include query string in url`); }
return value;}
function validEtldPlusOne(value) { // Parse domain name const result = tldtsParse(value, { allowPrivateDomains: true });
// Require valid public suffix if (result.publicSuffix === null) { return false; }
// Require valid hostname if (result.domainWithoutSuffix === null) { return false; }
return true;}
function validDomainName(value) { // Before we can validate we need to take care of IDNs with unicode chars. const ascii = punycode.toASCII(value);
if (ascii.length < 1) { // return 'DOMAIN_TOO_SHORT'; return false; } if (ascii.length > 255) { // return 'DOMAIN_TOO_LONG'; return false; }
// Check each part's length and allowed chars. const labels = ascii.split("."); let label;
for (let i = 0; i < labels.length; ++i) { label = labels[i]; if (!label.length) { // LABEL_TOO_SHORT return false; } if (label.length > 63) { // LABEL_TOO_LONG return false; } if (label.charAt(0) === "-") { // LABEL_STARTS_WITH_DASH return false; } /* if (label.charAt(label.length - 1) === '-') { // LABEL_ENDS_WITH_DASH return false; } */ if (!/^[a-z0-9-]+$/.test(label)) { // LABEL_INVALID_CHARS return false; } }
return true;}
function checkDomainOrUrl(value, name, rules = {}) { if (!name) { throw new TypeError("name not specified in checkDomainOrUrl"); }
if (typeof value !== "string") { throw new Error(`${name} must be a string`); }
if (validEtldPlusOne(value, name) && validDomainName(value, name)) { return value; // if valid domain no need for futher checks }
return checkUrl(value, name, rules);}
function checkRpId(rpId) { if (typeof rpId !== "string") { throw new Error("rpId must be a string"); }
const isLocalhost = (rpId === "localhost" || rpId.endsWith(".localhost"));
if (isLocalhost) return rpId;
return checkDomainOrUrl(rpId, "rpId");}
async function verifySignature(publicKey, expectedSignature, data, hashName) { let publicKeyInst; if (publicKey instanceof Key) { publicKeyInst = publicKey;
// Check for Public CryptoKey } else if (publicKey && publicKey.type === "public") { publicKeyInst = new Key(publicKey);
// Try importing from PEM } else { publicKeyInst = new Key(); await publicKeyInst.fromPem(publicKey, hashName); } const alg = publicKeyInst.getAlgorithm(); if (typeof alg === "undefined") { throw new Error("verifySignature: Algoritm missing."); }
// Use supplied hashName if (hashName) { alg.hash = { name: hashName, }; } if (!alg.hash) { throw new Error("verifySignature: Hash name missing."); }
try { let uSignature = new Uint8Array(expectedSignature); if (alg.name === "ECDSA") { uSignature = await derToRaw(uSignature); } return await webcrypto.subtle.verify(alg, publicKeyInst.getKey(), uSignature, new Uint8Array(data)); } catch (_e) { console.error(_e); }}
async function hashDigest(o, alg) { if (typeof o === "string") { o = new TextEncoder().encode(o); } const result = await webcrypto.subtle.digest(alg || "SHA-256", o); return result;}
function randomValues(n) { const byteArray = new Uint8Array(n); webcrypto.getRandomValues(byteArray); return byteArray;}
function getHostname(urlIn) { return new URL(urlIn).hostname;}
async function getEmbeddedJwk(jwsHeader, alg) { let publicKeyJwk;
// Use JWK from header if (jwsHeader.jwk) { publicKeyJwk = jwsHeader.jwk;
// Extract JWK from first x509 certificate in header } else if (jwsHeader.x5c) { const x5c0 = jwsHeader.x5c[0]; const cert = new Certificate(x5c0); publicKeyJwk = await cert.getPublicKeyJwk();
// Use common name as kid if missing publicKeyJwk.kid = publicKeyJwk.kid || cert.getCommonName(); }
if (!publicKeyJwk) { throw new Error("getEmbeddedJwk: JWK not found in JWS."); }
// Use alg from header if not present, use passed alg as default publicKeyJwk.alg = publicKeyJwk.alg || jwsHeader.alg || alg;
return publicKeyJwk;}
export { base64, cbor, checkDomainOrUrl, checkOrigin, checkRpId, checkUrl, decodeProtectedHeader, exportSPKI, fromBER, getEmbeddedJwk, getHostname, hashDigest, importJWK, importSPKI, jwtVerify, pkijs, randomValues, verifySignature, webcrypto};