UNPKG

@certd/acme-client

Version:
441 lines (383 loc) 12.6 kB
/** * Legacy node-forge crypto interface * * DEPRECATION WARNING: This crypto interface is deprecated and will be removed from acme-client in a future * major release. Please migrate to the new `acme.crypto` interface at your earliest convenience. * * @namespace forge */ import net from 'net'; import { promisify } from 'util'; import forge from 'node-forge'; import { createPrivateEcdsaKey } from './index.js'; const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair); /** * Attempt to parse forge object from PEM encoded string * * @private * @param {string} input PEM string * @return {object} */ function forgeObjectFromPem(input) { const msg = forge.pem.decode(input)[0]; let result; switch (msg.type) { case 'PRIVATE KEY': case 'RSA PRIVATE KEY': result = forge.pki.privateKeyFromPem(input); break; case 'PUBLIC KEY': case 'RSA PUBLIC KEY': result = forge.pki.publicKeyFromPem(input); break; case 'CERTIFICATE': case 'X509 CERTIFICATE': case 'TRUSTED CERTIFICATE': result = forge.pki.certificateFromPem(input).publicKey; break; case 'CERTIFICATE REQUEST': result = forge.pki.certificationRequestFromPem(input).publicKey; break; default: throw new Error('Unable to detect forge message type'); } return result; } /** * Parse domain names from a certificate or CSR * * @private * @param {object} obj Forge certificate or CSR * @returns {object} {commonName, altNames} */ function parseDomains(obj) { let commonName = null; let altNames = []; let altNamesDict = []; const commonNameObject = (obj.subject.attributes || []).find((a) => a.name === 'commonName'); const rootAltNames = (obj.extensions || []).find((e) => 'altNames' in e); const rootExtensions = (obj.attributes || []).find((a) => 'extensions' in a); if (rootAltNames && rootAltNames.altNames && rootAltNames.altNames.length) { altNamesDict = rootAltNames.altNames; } else if (rootExtensions && rootExtensions.extensions && rootExtensions.extensions.length) { const extAltNames = rootExtensions.extensions.find((e) => 'altNames' in e); if (extAltNames && extAltNames.altNames && extAltNames.altNames.length) { altNamesDict = extAltNames.altNames; } } if (commonNameObject) { commonName = commonNameObject.value; } if (altNamesDict) { altNames = altNamesDict.map((a) => a.value); } return { commonName, altNames, }; } /** * Generate a private RSA key * * @param {number} [size] Size of the key, default: `2048` * @returns {Promise<buffer>} PEM encoded private RSA key * * @example Generate private RSA key * ```js * const privateKey = await acme.forge.createPrivateKey(); * ``` * * @example Private RSA key with defined size * ```js * const privateKey = await acme.forge.createPrivateKey(4096); * ``` */ export async function createPrivateKey(size = 2048) { const keyPair = await generateKeyPair({ bits: size }); const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey); return Buffer.from(pemKey); } /** * Create public key from a private RSA key * * @param {buffer|string} key PEM encoded private RSA key * @returns {Promise<buffer>} PEM encoded public RSA key * * @example Create public key * ```js * const publicKey = await acme.forge.createPublicKey(privateKey); * ``` */ export const createPublicKey = async (key) => { const privateKey = forge.pki.privateKeyFromPem(key); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); const pemKey = forge.pki.publicKeyToPem(publicKey); return Buffer.from(pemKey); }; /** * Parse body of PEM encoded object from buffer or string * If multiple objects are chained, the first body will be returned * * @param {buffer|string} str PEM encoded buffer or string * @returns {string} PEM body */ export const getPemBody = (str) => { const msg = forge.pem.decode(str)[0]; return forge.util.encode64(msg.body); }; /** * Split chain of PEM encoded objects from buffer or string into array * * @param {buffer|string} str PEM encoded buffer or string * @returns {string[]} Array of PEM bodies */ export const splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode); /** * Get modulus * * @param {buffer|string} input PEM encoded private key, certificate or CSR * @returns {Promise<buffer>} Modulus * * @example Get modulus * ```js * const m1 = await acme.forge.getModulus(privateKey); * const m2 = await acme.forge.getModulus(certificate); * const m3 = await acme.forge.getModulus(certificateRequest); * ``` */ export const getModulus = async (input) => { if (!Buffer.isBuffer(input)) { input = Buffer.from(input); } const obj = forgeObjectFromPem(input); return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary'); }; /** * Get public exponent * * @param {buffer|string} input PEM encoded private key, certificate or CSR * @returns {Promise<buffer>} Exponent * * @example Get public exponent * ```js * const e1 = await acme.forge.getPublicExponent(privateKey); * const e2 = await acme.forge.getPublicExponent(certificate); * const e3 = await acme.forge.getPublicExponent(certificateRequest); * ``` */ export const getPublicExponent = async (input) => { if (!Buffer.isBuffer(input)) { input = Buffer.from(input); } const obj = forgeObjectFromPem(input); return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary'); }; /** * Read domains from a Certificate Signing Request * * @param {buffer|string} csr PEM encoded Certificate Signing Request * @returns {Promise<object>} {commonName, altNames} * * @example Read Certificate Signing Request domains * ```js * const { commonName, altNames } = await acme.forge.readCsrDomains(certificateRequest); * * console.log(`Common name: ${commonName}`); * console.log(`Alt names: ${altNames.join(', ')}`); * ``` */ export const readCsrDomains = async (csr) => { if (!Buffer.isBuffer(csr)) { csr = Buffer.from(csr); } const obj = forge.pki.certificationRequestFromPem(csr); return parseDomains(obj); }; /** * Read information from a certificate * * @param {buffer|string} cert PEM encoded certificate * @returns {Promise<object>} Certificate info * * @example Read certificate information * ```js * const info = await acme.forge.readCertificateInfo(certificate); * const { commonName, altNames } = info.domains; * * console.log(`Not after: ${info.notAfter}`); * console.log(`Not before: ${info.notBefore}`); * * console.log(`Common name: ${commonName}`); * console.log(`Alt names: ${altNames.join(', ')}`); * ``` */ export const readCertificateInfo = async (cert) => { if (!Buffer.isBuffer(cert)) { cert = Buffer.from(cert); } const obj = forge.pki.certificateFromPem(cert); const issuerCn = (obj.issuer.attributes || []).find((a) => a.name === 'commonName'); return { issuer: { commonName: issuerCn ? issuerCn.value : null, }, domains: parseDomains(obj), notAfter: obj.validity.notAfter, notBefore: obj.validity.notBefore, }; }; /** * Determine ASN.1 type for CSR subject short name * Note: https://datatracker.ietf.org/doc/html/rfc5280 * * @private * @param {string} shortName CSR subject short name * @returns {forge.asn1.Type} ASN.1 type */ function getCsrValueTagClass(shortName) { switch (shortName) { case 'C': return forge.asn1.Type.PRINTABLESTRING; case 'E': return forge.asn1.Type.IA5STRING; default: return forge.asn1.Type.UTF8; } } /** * Create array of short names and values for Certificate Signing Request subjects * * @private * @param {object} subjectObj Key-value of short names and values * @returns {object[]} Certificate Signing Request subject array */ function createCsrSubject(subjectObj) { return Object.entries(subjectObj).reduce((result, [shortName, value]) => { if (value) { const valueTagClass = getCsrValueTagClass(shortName); result.push({ shortName, value, valueTagClass }); } return result; }, []); } /** * Create array of alt names for Certificate Signing Requests * Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454 * * @private * @param {string[]} altNames Alt names * @returns {object[]} Certificate Signing Request alt names array */ function formatCsrAltNames(altNames) { return altNames.map((value) => { const type = net.isIP(value) ? 7 : 2; return { type, value }; }); } /** * Create a Certificate Signing Request * * @param {object} data * @param {number} [data.keySize] Size of newly created private key, default: `2048` * @param {string} [data.commonName] * @param {string[]} [data.altNames] default: `[]` * @param {string} [data.country] * @param {string} [data.state] * @param {string} [data.locality] * @param {string} [data.organization] * @param {string} [data.organizationUnit] * @param {string} [data.emailAddress] * @param {buffer|string} [key] CSR private key * @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest] * * @example Create a Certificate Signing Request * ```js * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * altNames: ['test.example.com'], * }); * ``` * * @example Certificate Signing Request with both common and alternative names * > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf). * ```js * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * keySize: 4096, * commonName: 'test.example.com', * altNames: ['foo.example.com', 'bar.example.com'], * }); * ``` * * @example Certificate Signing Request with additional information * ```js * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * altNames: ['test.example.com'], * country: 'US', * state: 'California', * locality: 'Los Angeles', * organization: 'The Company Inc.', * organizationUnit: 'IT Department', * emailAddress: 'contact@example.com', * }); * ``` * * @example Certificate Signing Request with predefined private key * ```js * const certificateKey = await acme.forge.createPrivateKey(); * * const [, certificateRequest] = await acme.forge.createCsr({ * altNames: ['test.example.com'], * }, certificateKey); */ export const createCsr = async (data, keyType = null) => { let key = null; if (keyType === 'ec') { key = await createPrivateEcdsaKey(); } else { key = await createPrivateKey(data.keySize); } // else if (!Buffer.isBuffer(key)) { // key = Buffer.from(key); // } if (typeof data.altNames === 'undefined') { data.altNames = []; } const csr = forge.pki.createCertificationRequest(); /* Public key */ const privateKey = forge.pki.privateKeyFromPem(key); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); csr.publicKey = publicKey; // const privateKey = key; // csr.publicKey = getPublicKey(key); /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */ if (data.commonName && !data.altNames.includes(data.commonName)) { data.altNames.unshift(data.commonName); } /* Subject */ const subject = createCsrSubject({ CN: data.commonName, C: data.country, ST: data.state, L: data.locality, O: data.organization, OU: data.organizationUnit, E: data.emailAddress, }); csr.setSubject(subject); /* SAN extension */ if (data.altNames.length) { csr.setAttributes([{ name: 'extensionRequest', extensions: [{ name: 'subjectAltName', altNames: formatCsrAltNames(data.altNames), }], }]); } /* Sign CSR using SHA-256 */ csr.sign(privateKey, forge.md.sha256.create()); /* Done */ const pemCsr = forge.pki.certificationRequestToPem(csr); return [key, Buffer.from(pemCsr)]; };