UNPKG

easy-ocsp

Version:

An easy-to-use OCSP client for Node.js

510 lines (502 loc) 19.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { OCSPRevocationReason: () => OCSPRevocationReason, convertPkijsCertToPem: () => convertPkijsCertToPem, convertToPkijsCert: () => convertToPkijsCert, downloadCert: () => downloadCert, downloadIssuerCert: () => downloadIssuerCert, getCertStatus: () => getCertStatus, getCertStatusByDomain: () => getCertStatusByDomain, getCertURLs: () => getCertURLs, getRawOCSPResponse: () => getRawOCSPResponse, parseOCSPResponse: () => parseOCSPResponse }); module.exports = __toCommonJS(index_exports); var pkijs3 = __toESM(require("pkijs")); // src/convert.ts var pkijs = __toESM(require("pkijs")); var import_asn1js = require("asn1js"); var import_node_crypto = require("crypto"); function pemToCert(pem) { try { const base64 = pem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\n\r])/g, ""); const der = Buffer.from(base64, "base64"); const asn1 = (0, import_asn1js.fromBER)(new Uint8Array(der).buffer); return new pkijs.Certificate({ schema: asn1.result }); } catch { throw new Error("The certificate is not a valid PEM encoded X.509 certificate string"); } } function convertToPkijsCert(cert) { if (typeof cert === "string") { return pemToCert(cert); } if (cert instanceof import_node_crypto.X509Certificate) { return pemToCert(cert.toString()); } if (cert instanceof Buffer) { return pkijs.Certificate.fromBER(bufferToArrayBuffer(cert)); } if (cert instanceof ArrayBuffer) { return pkijs.Certificate.fromBER(cert); } if (cert instanceof pkijs.Certificate) { return cert; } throw new Error("Invalid certificate type. Expected string, Buffer, X509Certificate or pkijs.Certificate"); } function convertPkijsCertToPem(cert) { const der = Buffer.from(cert.toSchema().toBER(false)); const base64 = der.toString("base64"); return `-----BEGIN CERTIFICATE----- ${base64.match(/.{1,64}/g)?.join("\n")} -----END CERTIFICATE----- `; } function bufferToArrayBuffer(b) { if (b.buffer instanceof SharedArrayBuffer) { throw new Error("Passing a Node.js Buffer that is backed by a SharedArrayBuffer is not supported"); } return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength); } function typedArrayToBuffer(array) { if (array.buffer instanceof SharedArrayBuffer) { throw new Error("Passing a typed array that is backed by a SharedArrayBuffer is not supported"); } return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset); } // src/ocsp.ts var import_node_crypto2 = require("crypto"); var import_asn1js2 = require("asn1js"); var pkijs2 = __toESM(require("pkijs")); var cryptoEngine = new pkijs2.CryptoEngine({ crypto: import_node_crypto2.webcrypto }); pkijs2.setEngine("crypto", cryptoEngine); async function buildOCSPRequest(cert, issuerCert, config) { const ocspReq = new pkijs2.OCSPRequest(); await ocspReq.createForCertificate(cert, { hashAlgorithm: "SHA-1", issuerCertificate: issuerCert }); let nonce = null; if (config.enableNonce) { nonce = new import_asn1js2.OctetString({ valueHex: pkijs2.getRandomValues(new Uint8Array(32)) }).toBER(); ocspReq.tbsRequest.requestExtensions = [ new pkijs2.Extension({ extnID: "1.3.6.1.5.5.7.48.1.2", // nonce extnValue: nonce }) ]; } return { ocspReq: ocspReq.toSchema(true).toBER(), nonce }; } function getCAInfoUrls(cert) { if (!cert.extensions) { throw new Error("Certificate does not contain any extensions"); } const authorityInfoAccessExtension = cert.extensions.find((ext) => ext.extnID === "1.3.6.1.5.5.7.1.1"); if (!authorityInfoAccessExtension || !authorityInfoAccessExtension.parsedValue || !authorityInfoAccessExtension.parsedValue.accessDescriptions) { throw new Error("Certificate does not contain authority information access extension"); } const ocsp = authorityInfoAccessExtension.parsedValue.accessDescriptions.find( (ext) => ext.accessMethod === "1.3.6.1.5.5.7.48.1" ); if (!ocsp || !ocsp.accessLocation || !ocsp.accessLocation.value) { throw new Error("Certificate does not contain OCSP url"); } const issuer = authorityInfoAccessExtension.parsedValue.accessDescriptions.find( (ext) => ext.accessMethod === "1.3.6.1.5.5.7.48.2" ); if (!issuer || !issuer.accessLocation || !issuer.accessLocation.value) { throw new Error("Certificate does not contain issuer url"); } return { ocspUrl: ocsp.accessLocation.value, issuerUrl: issuer.accessLocation.value }; } async function parseOCSPResponse(responseData, certificate, issuerCertificate, config, nonce) { const ocspResponse = pkijs2.OCSPResponse.fromBER(responseData); const responseCode = ocspResponse.responseStatus.valueBlock.valueDec; if (responseCode !== 0) { switch (responseCode) { case 1: throw new Error("OCSP server response: malformedRequest"); case 2: throw new Error("OCSP server response: internalError"); case 3: throw new Error("OCSP server response: tryLater"); case 5: throw new Error("OCSP server response: sigRequired"); case 6: throw new Error("OCSP server response: unauthorized"); default: throw new Error("OCSP server response: unknown"); } } if (!ocspResponse.responseBytes) { throw new Error("OCSP server response does not contain response bytes"); } if (ocspResponse.responseBytes.responseType !== "1.3.6.1.5.5.7.48.1.1") { throw new Error("Unknown ocsp response type"); } const basicResponse = pkijs2.BasicOCSPResponse.fromBER(typedArrayToBuffer(ocspResponse.responseBytes.response.valueBlock.valueHexView)); if (!Array.isArray(basicResponse.tbsResponseData.responses)) { throw new Error("OCSP response does not contain any response data"); } if (basicResponse.tbsResponseData.responses.length !== 1) { throw new Error("OCSP response does not contain exactly one response"); } if (!(basicResponse.tbsResponseData.responses[0] instanceof pkijs2.SingleResponse)) { throw new Error("OCSP response is not a pkijs.SingleResponse"); } const cryptoEngine2 = pkijs2.getEngine(); if (!cryptoEngine2 || !cryptoEngine2.crypto) { throw new Error("No pkijs crypto engine"); } if (config.validateSignature) { if (!await verifySignature(basicResponse, issuerCertificate, nonce, config, cryptoEngine2)) { throw new Error("OCSP response signature verification failed"); } } const singleResponse = basicResponse.tbsResponseData.responses[0]; const hashAlgorithm = cryptoEngine2.crypto.getAlgorithmByOID( singleResponse.certID.hashAlgorithm.algorithmId, true, "CertID.hashAlgorithm" ); const certID = new pkijs2.CertID(); await certID.createForCertificate( certificate, { hashAlgorithm: hashAlgorithm.name, issuerCertificate }, cryptoEngine2.crypto ); if (!singleResponse.certID.isEqual(certID)) { throw new Error("OCSP response does not match certificate"); } let status = "unknown"; if (singleResponse.certStatus.idBlock.isConstructed) { if (singleResponse.certStatus.idBlock.tagNumber === 1) { status = "revoked"; } } else { if (singleResponse.certStatus.idBlock.tagNumber === 0) { status = "good"; } else if (singleResponse.certStatus.idBlock.tagNumber !== 2) { throw new Error(`OCSP response certStatus is not good, revoked or unknown: ${singleResponse.certStatus.idBlock.tagNumber}`); } } const result = { status, ocspUrl: config.ocspUrl }; if (basicResponse.tbsResponseData.producedAt instanceof Date) { result.producedAt = basicResponse.tbsResponseData.producedAt; } if (singleResponse.nextUpdate instanceof Date) { result.nextUpdate = singleResponse.nextUpdate; } if (singleResponse.thisUpdate instanceof Date) { result.thisUpdate = singleResponse.thisUpdate; } if (status === "revoked" && Array.isArray(singleResponse.certStatus?.valueBlock?.value)) { for (const v of singleResponse.certStatus.valueBlock.value) { if (v instanceof import_asn1js2.GeneralizedTime) { result.revocationTime = v.toDate(); } if (v instanceof import_asn1js2.UTCTime) { result.revocationTime = v.toDate(); } if (v instanceof import_asn1js2.Constructed) { if (Array.isArray(v.valueBlock.value) && v.valueBlock.value.length === 1) { const vBlock = v.valueBlock.value[0]; if (vBlock instanceof import_asn1js2.Enumerated) { result.revocationReason = vBlock.valueBlock.valueDec; } } } } } if (config.rawResponse === true) { result.rawResponse = Buffer.from(responseData); } return result; } async function verifySignature(basicOcspResponse, trustedCert, nonce, config, cryptoEngine2 = pkijs2.getEngine()) { let signatureCert = null; if (!cryptoEngine2 || !cryptoEngine2.crypto) { throw new Error("No pkijs crypto engine"); } if (config.ocspCertificate) { signatureCert = convertToPkijsCert(config.ocspCertificate); } else if (basicOcspResponse.tbsResponseData.responderID instanceof pkijs2.RelativeDistinguishedNames) { if (trustedCert.subject.isEqual(basicOcspResponse.tbsResponseData.responderID)) { signatureCert = trustedCert; } } else if (basicOcspResponse.tbsResponseData.responderID instanceof import_asn1js2.OctetString) { const hash = await import_node_crypto2.webcrypto.subtle.digest( { name: "sha-1" }, trustedCert.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView ); if (Buffer.compare(Buffer.from(hash), basicOcspResponse.tbsResponseData.responderID.valueBlock.valueHexView) === 0) { signatureCert = trustedCert; } } else { throw new Error("Responder ID is unknown"); } if (!signatureCert) { if (!Array.isArray(basicOcspResponse.certs) || !basicOcspResponse.certs.length) { throw new Error("OCSP response is not signed by trusted certificate and does not contain additional certificates"); } for (const cert of basicOcspResponse.certs) { if (basicOcspResponse.tbsResponseData.responderID instanceof pkijs2.RelativeDistinguishedNames) { if (cert.subject.isEqual(basicOcspResponse.tbsResponseData.responderID)) { signatureCert = cert; break; } } else if (basicOcspResponse.tbsResponseData.responderID instanceof import_asn1js2.OctetString) { const hash = await import_node_crypto2.webcrypto.subtle.digest( { name: "sha-1" }, cert.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView ); if (Buffer.compare(Buffer.from(hash), basicOcspResponse.tbsResponseData.responderID.valueBlock.valueHexView) === 0) { signatureCert = cert; } } } if (!signatureCert) { throw new Error("OCSP response is not signed by trusted certificate or additional response certificates"); } const chain = new pkijs2.CertificateChainValidationEngine({ certs: basicOcspResponse.certs, trustedCerts: [trustedCert] }); const verificationResult = await chain.verify({}, cryptoEngine2.crypto); if (!verificationResult.result) { throw new Error("Validation of OCSP response certificate chain failed"); } } if (config.enableNonce && nonce && Array.isArray(basicOcspResponse.tbsResponseData.responseExtensions)) { const nonceExtension = basicOcspResponse.tbsResponseData.responseExtensions.find((e) => e.extnID === "1.3.6.1.5.5.7.48.1.2"); if (nonceExtension && Buffer.compare(Buffer.from(nonce), nonceExtension.extnValue.valueBlock.valueHexView) !== 0) { throw new Error("OCSP response nonce does not match request nonce"); } } return cryptoEngine2.crypto.verifyWithPublicKey( typedArrayToBuffer(basicOcspResponse.tbsResponseData.tbsView), basicOcspResponse.signature, signatureCert.subjectPublicKeyInfo, basicOcspResponse.signatureAlgorithm ); } // src/tls.ts var import_node_tls = require("tls"); function downloadCert(hostname, timeout = 6e3) { return new Promise((resolve, reject) => { const options = { port: 443, host: hostname, servername: hostname, timeout }; const socket = (0, import_node_tls.connect)(options, () => { const cert = socket.getPeerCertificate(); if (!cert || !cert.raw) { reject(new Error(`No certificate found for host ${hostname}`)); } resolve(cert.raw); socket.end(); }); socket.on("error", (err) => { reject(err); }); socket.on("timeout", () => { reject(new Error(`Timeout while connecting to host ${hostname}`)); }); }); } // src/types.ts var OCSPRevocationReason = /* @__PURE__ */ ((OCSPRevocationReason2) => { OCSPRevocationReason2[OCSPRevocationReason2["unspecified"] = 0] = "unspecified"; OCSPRevocationReason2[OCSPRevocationReason2["keyCompromise"] = 1] = "keyCompromise"; OCSPRevocationReason2[OCSPRevocationReason2["caCompromise"] = 2] = "caCompromise"; OCSPRevocationReason2[OCSPRevocationReason2["affiliationChanged"] = 3] = "affiliationChanged"; OCSPRevocationReason2[OCSPRevocationReason2["superseded"] = 4] = "superseded"; OCSPRevocationReason2[OCSPRevocationReason2["cessationOfOperation"] = 5] = "cessationOfOperation"; OCSPRevocationReason2[OCSPRevocationReason2["certificateHold"] = 6] = "certificateHold"; OCSPRevocationReason2[OCSPRevocationReason2["removeFromCRL"] = 8] = "removeFromCRL"; OCSPRevocationReason2[OCSPRevocationReason2["privilegeWithdrawn"] = 9] = "privilegeWithdrawn"; OCSPRevocationReason2[OCSPRevocationReason2["aACompromise"] = 10] = "aACompromise"; return OCSPRevocationReason2; })(OCSPRevocationReason || {}); // src/fetchWrapper.ts async function fetchWrapper(url, options, timeout, errorPrefix) { const ac = new AbortController(); const timeoutId = setTimeout(() => ac.abort(), timeout); options.signal = ac.signal; try { return await fetch(url, options); } catch (error) { if (!(error instanceof Error)) { throw new Error(`${errorPrefix}: ${String(error)}`); } if (error.name === "AbortError") { throw new Error(`${errorPrefix}: Operation timed out after ${timeout}ms`); } throw new Error(`${errorPrefix}: ${error.message}${error.cause ? ` (${error.cause})` : ""}`); } finally { clearTimeout(timeoutId); } } // src/index.ts async function downloadIssuerCert(cert, timeout) { let _timeoutMs = defaultConfig.timeout; if (typeof timeout === "number") { _timeoutMs = timeout; } const { issuerUrl } = getCAInfoUrls(convertToPkijsCert(cert)); const res = await fetchWrapper(issuerUrl, {}, _timeoutMs, `Failed to download issuer certificate`); if (!res.ok) { throw new Error(`Issuer certificate download failed with status ${res.status} ${res.statusText} ${issuerUrl}`); } const rawResponse = Buffer.from(await res.arrayBuffer()); try { return convertToPkijsCert(rawResponse); } catch (err) { if (err instanceof pkijs3.AsnError) { const txt = rawResponse.toString("ascii"); if (txt.includes("BEGIN CERTIFICATE")) { return convertToPkijsCert(txt); } throw new Error("The issuer certificate is not a valid DER or PEM encoded X.509 certificate"); } throw err; } } var defaultConfig = { validateSignature: true, enableNonce: true, timeout: 6e3 }; async function sendOCSPRequest(cert, config) { const certificate = convertToPkijsCert(cert); if (certificate.notAfter.value.getTime() < Date.now()) { throw new Error("The certificate is already expired"); } if (!config.ocspUrl) { config.ocspUrl = getCAInfoUrls(certificate).ocspUrl; } let issuerCertificate; if (!config.ca) { issuerCertificate = await downloadIssuerCert(certificate, config.timeout); } else { issuerCertificate = convertToPkijsCert(config.ca); } const { ocspReq, nonce } = await buildOCSPRequest(certificate, issuerCertificate, config); const res = await fetchWrapper( config.ocspUrl, { method: "POST", headers: { "Content-Type": "application/ocsp-request" }, body: ocspReq }, config.timeout ?? defaultConfig.timeout, "Failed to send OCSP request" ); if (!res.ok) { throw new Error(`OCSP request failed with http status ${res.status} ${res.statusText}`); } return { response: await res.arrayBuffer(), certificate, issuerCertificate, nonce }; } async function getCertStatus(cert, config) { const _config = { ...defaultConfig, ...config }; const { response, certificate, issuerCertificate, nonce } = await sendOCSPRequest(cert, _config); return parseOCSPResponse(response, certificate, issuerCertificate, _config, nonce); } async function getCertStatusByDomain(domain, config) { let _domain = domain; let timeout = 6e3; if (config && typeof config.timeout === "number") { timeout = config.timeout; } if (_domain.includes("/")) { try { const url = new URL(_domain); _domain = url.hostname; } catch { throw new Error("Invalid URL"); } } return getCertStatus(await downloadCert(_domain, timeout), config); } async function getRawOCSPResponse(cert, config) { const _config = { ...defaultConfig, ...config }; const { response, issuerCertificate, nonce } = await sendOCSPRequest(cert, _config); return { rawResponse: Buffer.from(response), nonce: nonce ? Buffer.from(nonce) : void 0, issuerCert: convertPkijsCertToPem(issuerCertificate) }; } function getCertURLs(cert) { return getCAInfoUrls(convertToPkijsCert(cert)); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { OCSPRevocationReason, convertPkijsCertToPem, convertToPkijsCert, downloadCert, downloadIssuerCert, getCertStatus, getCertStatusByDomain, getCertURLs, getRawOCSPResponse, parseOCSPResponse });