UNPKG

mockttp-mvs

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

192 lines 9.14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CA = exports.getCA = exports.generateSPKIFingerprint = exports.generateCACertificate = void 0; const _ = require("lodash"); const fs = require("fs/promises"); const uuid_1 = require("uuid"); const forge = require("node-forge"); const { pki, md, util: { encode64 } } = forge; ; /** * Generate a CA certificate for mocking HTTPS. * * Returns a promise, for an object with key and cert properties, * containing the generated private key and certificate in PEM format. * * These can be saved to disk, and their paths passed * as HTTPS options to a Mockttp server. */ async function generateCACertificate(options = {}) { options = _.defaults({}, options, { commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY', organizationName: 'Mockttp', countryName: 'XX', bits: 2048, }); const keyPair = await new Promise((resolve, reject) => { pki.rsa.generateKeyPair({ bits: options.bits }, (error, keyPair) => { if (error) reject(error); else resolve(keyPair); }); }); const cert = pki.createCertificate(); cert.publicKey = keyPair.publicKey; cert.serialNumber = generateSerialNumber(); cert.validity.notBefore = new Date(); // Make it valid for the last 24h - helps in cases where clocks slightly disagree cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1); cert.validity.notAfter = new Date(); // Valid for the next year by default. cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1); cert.setSubject([ // All of these are required for a fully valid CA cert that will be accepted when imported anywhere: { name: 'commonName', value: options.commonName }, { name: 'countryName', value: options.countryName }, { name: 'organizationName', value: options.organizationName } ]); cert.setExtensions([ { name: 'basicConstraints', cA: true, critical: true }, { name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, cRLSign: true, critical: true }, { name: 'subjectKeyIdentifier' } ]); // Self-issued too cert.setIssuer(cert.subject.attributes); // Self-sign the certificate - we're the root cert.sign(keyPair.privateKey, md.sha256.create()); return { key: pki.privateKeyToPem(keyPair.privateKey), cert: pki.certificateToPem(cert) }; } exports.generateCACertificate = generateCACertificate; function generateSPKIFingerprint(certPem) { let cert = pki.certificateFromPem(certPem.toString('utf8')); return encode64(pki.getPublicKeyFingerprint(cert.publicKey, { type: 'SubjectPublicKeyInfo', md: md.sha256.create(), encoding: 'binary' })); } exports.generateSPKIFingerprint = generateSPKIFingerprint; // Generates a unique serial number for a certificate as a hex string: function generateSerialNumber() { return 'A' + (0, uuid_1.v4)().replace(/-/g, ''); // We add a leading 'A' to ensure it's always positive (not 'F') and always // valid (e.g. leading 000 is bad padding, and would be unparseable). } async function getCA(options) { let certOptions; if ('key' in options && 'cert' in options) { certOptions = options; } else if ('keyPath' in options && 'certPath' in options) { certOptions = await Promise.all([ fs.readFile(options.keyPath, 'utf8'), fs.readFile(options.certPath, 'utf8') ]).then(([keyContents, certContents]) => ({ ..._.omit(options, ['keyPath', 'certPath']), key: keyContents, cert: certContents })); } else { throw new Error('Unrecognized https options: you need to provide either a keyPath & certPath, or a key & cert.'); } return new CA(certOptions); } exports.getCA = getCA; // We share a single keypair across all certificates in this process, and // instantiate it once when the first CA is created, because it can be // expensive (depending on the key length). // This would be a terrible idea for a real server, but for a mock server // it's ok - if anybody can steal this, they can steal the CA cert anyway. let KEY_PAIR; class CA { constructor(options) { this.caKey = pki.privateKeyFromPem(options.key.toString()); this.caCert = pki.certificateFromPem(options.cert.toString()); this.certCache = {}; this.options = options ?? {}; const keyLength = options.keyLength || 2048; if (!KEY_PAIR || KEY_PAIR.length < keyLength) { // If we have no key, or not a long enough one, generate one. KEY_PAIR = Object.assign(pki.rsa.generateKeyPair(keyLength), { length: keyLength }); } } generateCertificate(domain) { // TODO: Expire domains from the cache? Based on their actual expiry? if (this.certCache[domain]) return this.certCache[domain]; if (domain.includes('_')) { // TLS certificates cannot cover domains with underscores, bizarrely. More info: // https://www.digicert.com/kb/ssl-support/underscores-not-allowed-in-fqdns.htm // To fix this, we use wildcards instead. This is only possible for one level of // certificate, and only for subdomains, so our options are a little limited, but // this should be very rare (because it's not supported elsewhere either). const [, ...otherParts] = domain.split('.'); if (otherParts.length <= 1 || // *.com is never valid otherParts.some(p => p.includes('_'))) { throw new Error(`Cannot generate certificate for domain due to underscores: ${domain}`); } // Replace the first part with a wildcard to solve the problem: domain = `*.${otherParts.join('.')}`; } let cert = pki.createCertificate(); cert.publicKey = KEY_PAIR.publicKey; cert.serialNumber = generateSerialNumber(); cert.validity.notBefore = new Date(); // Make it valid for the last 24h - helps in cases where clocks slightly disagree. cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1); cert.validity.notAfter = new Date(); // Valid for the next year by default. TODO: Shorten (and expire the cache) automatically. cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1); cert.setSubject([ ...(domain[0] === '*' ? [] // We skip the CN (deprecated, rarely used) for wildcards, since they can't be used here. : [{ name: 'commonName', value: domain }]), { name: 'countryName', value: this.options?.countryName ?? 'XX' }, { name: 'localityName', value: this.options?.localityName ?? 'Unknown' }, { name: 'organizationName', value: this.options?.organizationName ?? 'Mockttp Cert - DO NOT TRUST' } ]); cert.setIssuer(this.caCert.subject.attributes); const policyList = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [ forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [ forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('2.5.29.32.0').getBytes() // Mark all as Domain Verified ) ]) ]); cert.setExtensions([ { name: 'basicConstraints', cA: false, critical: true }, { name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true }, { name: 'extKeyUsage', serverAuth: true, clientAuth: true }, { name: 'subjectAltName', altNames: [{ type: 2, value: domain }] }, { name: 'certificatePolicies', value: policyList }, { name: 'subjectKeyIdentifier' }, { name: 'authorityKeyIdentifier', // We have to calculate this ourselves due to // https://github.com/digitalbazaar/forge/issues/462 keyIdentifier: this.caCert // generateSubjectKeyIdentifier is missing from node-forge types .generateSubjectKeyIdentifier().getBytes() } ]); cert.sign(this.caKey, md.sha256.create()); const generatedCertificate = { key: pki.privateKeyToPem(KEY_PAIR.privateKey), cert: pki.certificateToPem(cert), ca: pki.certificateToPem(this.caCert) }; this.certCache[domain] = generatedCertificate; return generatedCertificate; } } exports.CA = CA; //# sourceMappingURL=tls.js.map