mirror of
https://github.com/certd/certd.git
synced 2026-04-23 19:57:27 +08:00
🔱: [acme] sync upgrade with 21 commits [trident-sync]
Bump v5.0.0
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const { promisify } = require('util');
|
||||
const forge = require('node-forge');
|
||||
|
||||
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);
|
||||
* ```
|
||||
*/
|
||||
|
||||
async function createPrivateKey(size = 2048) {
|
||||
const keyPair = await generateKeyPair({ bits: size });
|
||||
const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey);
|
||||
return Buffer.from(pemKey);
|
||||
}
|
||||
|
||||
exports.createPrivateKey = createPrivateKey;
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.createPublicKey = async function(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
|
||||
*/
|
||||
|
||||
exports.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
|
||||
*/
|
||||
|
||||
exports.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);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.getModulus = async function(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);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.getPublicExponent = async function(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(', ')}`);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.readCsrDomains = async function(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(', ')}`);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.readCertificateInfo = async function(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://tools.ietf.org/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 {array} [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({
|
||||
* commonName: 'test.example.com'
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Certificate Signing Request with both common and alternative names
|
||||
* ```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({
|
||||
* commonName: '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({
|
||||
* commonName: 'test.example.com'
|
||||
* }, certificateKey);
|
||||
*/
|
||||
|
||||
exports.createCsr = async function(data, key = null) {
|
||||
if (!key) {
|
||||
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;
|
||||
|
||||
/* 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)];
|
||||
};
|
||||
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* Native Node.js crypto interface
|
||||
*
|
||||
* @namespace crypto
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const { promisify } = require('util');
|
||||
const crypto = require('crypto');
|
||||
const jsrsasign = require('jsrsasign');
|
||||
|
||||
const generateKeyPair = promisify(crypto.generateKeyPair);
|
||||
|
||||
|
||||
/**
|
||||
* Determine key type and info by attempting to derive public key
|
||||
*
|
||||
* @private
|
||||
* @param {buffer|string} keyPem PEM encoded private or public key
|
||||
* @returns {object}
|
||||
*/
|
||||
|
||||
function getKeyInfo(keyPem) {
|
||||
const result = {
|
||||
isRSA: false,
|
||||
isECDSA: false,
|
||||
signatureAlgorithm: null,
|
||||
publicKey: crypto.createPublicKey(keyPem)
|
||||
};
|
||||
|
||||
if (result.publicKey.asymmetricKeyType === 'rsa') {
|
||||
result.isRSA = true;
|
||||
result.signatureAlgorithm = 'SHA256withRSA';
|
||||
}
|
||||
else if (result.publicKey.asymmetricKeyType === 'ec') {
|
||||
result.isECDSA = true;
|
||||
result.signatureAlgorithm = 'SHA256withECDSA';
|
||||
}
|
||||
else {
|
||||
throw new Error('Unable to parse key information, unknown format');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a private RSA key
|
||||
*
|
||||
* @param {number} [modulusLength] Size of the keys modulus in bits, default: `2048`
|
||||
* @returns {Promise<buffer>} PEM encoded private RSA key
|
||||
*
|
||||
* @example Generate private RSA key
|
||||
* ```js
|
||||
* const privateKey = await acme.crypto.createPrivateRsaKey();
|
||||
* ```
|
||||
*
|
||||
* @example Private RSA key with modulus size 4096
|
||||
* ```js
|
||||
* const privateKey = await acme.crypto.createPrivateRsaKey(4096);
|
||||
* ```
|
||||
*/
|
||||
|
||||
async function createPrivateRsaKey(modulusLength = 2048) {
|
||||
const pair = await generateKeyPair('rsa', {
|
||||
modulusLength,
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
});
|
||||
|
||||
return Buffer.from(pair.privateKey);
|
||||
}
|
||||
|
||||
exports.createPrivateRsaKey = createPrivateRsaKey;
|
||||
|
||||
|
||||
/**
|
||||
* Alias of `createPrivateRsaKey()`
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
|
||||
exports.createPrivateKey = createPrivateRsaKey;
|
||||
|
||||
|
||||
/**
|
||||
* Generate a private ECDSA key
|
||||
*
|
||||
* @param {string} [namedCurve] ECDSA curve name (P-256, P-384 or P-521), default `P-256`
|
||||
* @returns {Promise<buffer>} PEM encoded private ECDSA key
|
||||
*
|
||||
* @example Generate private ECDSA key
|
||||
* ```js
|
||||
* const privateKey = await acme.crypto.createPrivateEcdsaKey();
|
||||
* ```
|
||||
*
|
||||
* @example Private ECDSA key using P-384 curve
|
||||
* ```js
|
||||
* const privateKey = await acme.crypto.createPrivateEcdsaKey('P-384');
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.createPrivateEcdsaKey = async (namedCurve = 'P-256') => {
|
||||
const pair = await generateKeyPair('ec', {
|
||||
namedCurve,
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
});
|
||||
|
||||
return Buffer.from(pair.privateKey);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a public key derived from a RSA or ECDSA key
|
||||
*
|
||||
* @param {buffer|string} keyPem PEM encoded private or public key
|
||||
* @returns {buffer} PEM encoded public key
|
||||
*
|
||||
* @example Get public key
|
||||
* ```js
|
||||
* const publicKey = acme.crypto.getPublicKey(privateKey);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.getPublicKey = (keyPem) => {
|
||||
const info = getKeyInfo(keyPem);
|
||||
|
||||
const publicKey = info.publicKey.export({
|
||||
type: info.isECDSA ? 'spki' : 'pkcs1',
|
||||
format: 'pem'
|
||||
});
|
||||
|
||||
return Buffer.from(publicKey);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a JSON Web Key derived from a RSA or ECDSA key
|
||||
*
|
||||
* https://datatracker.ietf.org/doc/html/rfc7517
|
||||
*
|
||||
* @param {buffer|string} keyPem PEM encoded private or public key
|
||||
* @returns {object} JSON Web Key
|
||||
*
|
||||
* @example Get JWK
|
||||
* ```js
|
||||
* const jwk = acme.crypto.getJwk(privateKey);
|
||||
* ```
|
||||
*/
|
||||
|
||||
function getJwk(keyPem) {
|
||||
const jwk = crypto.createPublicKey(keyPem).export({
|
||||
format: 'jwk'
|
||||
});
|
||||
|
||||
/* Sort keys */
|
||||
return Object.keys(jwk).sort().reduce((result, k) => {
|
||||
result[k] = jwk[k];
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
exports.getJwk = getJwk;
|
||||
|
||||
|
||||
/**
|
||||
* Fix missing support for NIST curve names in jsrsasign
|
||||
*
|
||||
* @private
|
||||
* @param {string} crv NIST curve name
|
||||
* @returns {string} SECG curve name
|
||||
*/
|
||||
|
||||
function convertNistCurveNameToSecg(nistName) {
|
||||
switch (nistName) {
|
||||
case 'P-256':
|
||||
return 'secp256r1';
|
||||
case 'P-384':
|
||||
return 'secp384r1';
|
||||
case 'P-521':
|
||||
return 'secp521r1';
|
||||
default:
|
||||
return nistName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Split chain of PEM encoded objects from string into array
|
||||
*
|
||||
* @param {buffer|string} chainPem PEM encoded object chain
|
||||
* @returns {array} Array of PEM objects including headers
|
||||
*/
|
||||
|
||||
function splitPemChain(chainPem) {
|
||||
if (Buffer.isBuffer(chainPem)) {
|
||||
chainPem = chainPem.toString();
|
||||
}
|
||||
|
||||
return chainPem
|
||||
/* Split chain into chunks, starting at every header */
|
||||
.split(/\s*(?=-----BEGIN [A-Z0-9- ]+-----\r?\n?)/g)
|
||||
/* Match header, PEM body and footer */
|
||||
.map((pem) => pem.match(/\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\S\s]+)\r?\n?-----END \1-----/))
|
||||
/* Filter out non-matches or empty bodies */
|
||||
.filter((pem) => pem && pem[2] && pem[2].replace(/[\r\n]+/g, '').trim())
|
||||
/* Decode to hex, and back to PEM for formatting etc */
|
||||
.map(([pem, header]) => jsrsasign.hextopem(jsrsasign.pemtohex(pem, header), header));
|
||||
}
|
||||
|
||||
exports.splitPemChain = splitPemChain;
|
||||
|
||||
|
||||
/**
|
||||
* Parse body of PEM encoded object and return a Base64URL string
|
||||
* If multiple objects are chained, the first body will be returned
|
||||
*
|
||||
* @param {buffer|string} pem PEM encoded chain or object
|
||||
* @returns {string} Base64URL-encoded body
|
||||
*/
|
||||
|
||||
exports.getPemBodyAsB64u = (pem) => {
|
||||
const chain = splitPemChain(pem);
|
||||
|
||||
if (!chain.length) {
|
||||
throw new Error('Unable to parse PEM body from string');
|
||||
}
|
||||
|
||||
/* First object, hex and back to b64 without new lines */
|
||||
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parse common name from a subject object
|
||||
*
|
||||
* @private
|
||||
* @param {object} subj Subject returned from jsrsasign
|
||||
* @returns {string} Common name value
|
||||
*/
|
||||
|
||||
function parseCommonName(subj) {
|
||||
const subjectArr = (subj && subj.array) ? subj.array : [];
|
||||
const cnArr = subjectArr.find((s) => (s[0] && s[0].type && s[0].value && (s[0].type === 'CN')));
|
||||
return (cnArr && cnArr.length && cnArr[0].value) ? cnArr[0].value : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse domains from a certificate or CSR
|
||||
*
|
||||
* @private
|
||||
* @param {object} params Certificate or CSR params returned from jsrsasign
|
||||
* @returns {object} {commonName, altNames}
|
||||
*/
|
||||
|
||||
function parseDomains(params) {
|
||||
const commonName = parseCommonName(params.subject);
|
||||
const extensionArr = (params.ext || params.extreq || []);
|
||||
let altNames = [];
|
||||
|
||||
if (extensionArr && extensionArr.length) {
|
||||
const altNameExt = extensionArr.find((e) => (e.extname && (e.extname === 'subjectAltName')));
|
||||
const altNameArr = (altNameExt && altNameExt.array && altNameExt.array.length) ? altNameExt.array : [];
|
||||
altNames = altNameArr.map((a) => Object.values(a)[0] || null).filter((a) => a);
|
||||
}
|
||||
|
||||
return {
|
||||
commonName,
|
||||
altNames
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read domains from a Certificate Signing Request
|
||||
*
|
||||
* @param {buffer|string} csrPem PEM encoded Certificate Signing Request
|
||||
* @returns {object} {commonName, altNames}
|
||||
*
|
||||
* @example Read Certificate Signing Request domains
|
||||
* ```js
|
||||
* const { commonName, altNames } = acme.crypto.readCsrDomains(certificateRequest);
|
||||
*
|
||||
* console.log(`Common name: ${commonName}`);
|
||||
* console.log(`Alt names: ${altNames.join(', ')}`);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.readCsrDomains = (csrPem) => {
|
||||
if (Buffer.isBuffer(csrPem)) {
|
||||
csrPem = csrPem.toString();
|
||||
}
|
||||
|
||||
/* Parse CSR */
|
||||
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem);
|
||||
return parseDomains(params);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Read information from a certificate
|
||||
* If multiple certificates are chained, the first will be read
|
||||
*
|
||||
* @param {buffer|string} certPem PEM encoded certificate or chain
|
||||
* @returns {object} Certificate info
|
||||
*
|
||||
* @example Read certificate information
|
||||
* ```js
|
||||
* const info = acme.crypto.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(', ')}`);
|
||||
* ```
|
||||
*/
|
||||
|
||||
exports.readCertificateInfo = (certPem) => {
|
||||
const chain = splitPemChain(certPem);
|
||||
|
||||
if (!chain.length) {
|
||||
throw new Error('Unable to parse PEM body from string');
|
||||
}
|
||||
|
||||
/* Parse certificate */
|
||||
const obj = new jsrsasign.X509();
|
||||
obj.readCertPEM(chain[0]);
|
||||
const params = obj.getParam();
|
||||
|
||||
return {
|
||||
issuer: {
|
||||
commonName: parseCommonName(params.issuer)
|
||||
},
|
||||
domains: parseDomains(params),
|
||||
notBefore: jsrsasign.zulutodate(params.notbefore),
|
||||
notAfter: jsrsasign.zulutodate(params.notafter)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine ASN.1 character string type for CSR subject field
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc5280
|
||||
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/x509-1.1.js#L2404-L2412
|
||||
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/asn1x509-1.0.js#L3526-L3535
|
||||
*
|
||||
* @private
|
||||
* @param {string} field CSR subject field
|
||||
* @returns {string} ASN.1 jsrsasign character string type
|
||||
*/
|
||||
|
||||
function getCsrAsn1CharStringType(field) {
|
||||
switch (field) {
|
||||
case 'C':
|
||||
return 'prn';
|
||||
case 'E':
|
||||
return 'ia5';
|
||||
default:
|
||||
return 'utf8';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create array of subject fields for a Certificate Signing Request
|
||||
*
|
||||
* @private
|
||||
* @param {object} input Key-value of subject fields
|
||||
* @returns {object[]} Certificate Signing Request subject array
|
||||
*/
|
||||
|
||||
function createCsrSubject(input) {
|
||||
return Object.entries(input).reduce((result, [type, value]) => {
|
||||
if (value) {
|
||||
const ds = getCsrAsn1CharStringType(type);
|
||||
result.push([{ type, value, ds }]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create array of alt names for Certificate Signing Requests
|
||||
*
|
||||
* https://github.com/kjur/jsrsasign/blob/3edc0070846922daea98d9588978e91d855577ec/src/x509-1.1.js#L1355-L1410
|
||||
*
|
||||
* @private
|
||||
* @param {string[]} altNames Array of alt names
|
||||
* @returns {object[]} Certificate Signing Request alt names array
|
||||
*/
|
||||
|
||||
function formatCsrAltNames(altNames) {
|
||||
return altNames.map((value) => {
|
||||
const key = net.isIP(value) ? 'ip' : 'dns';
|
||||
return { [key]: value };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a Certificate Signing Request
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048`
|
||||
* @param {string} [data.commonName] FQDN of your server
|
||||
* @param {array} [data.altNames] SAN (Subject Alternative Names), default: `[]`
|
||||
* @param {string} [data.country] 2 letter country code
|
||||
* @param {string} [data.state] State or province
|
||||
* @param {string} [data.locality] City
|
||||
* @param {string} [data.organization] Organization name
|
||||
* @param {string} [data.organizationUnit] Organizational unit name
|
||||
* @param {string} [data.emailAddress] Email address
|
||||
* @param {string} [keyPem] PEM encoded CSR private key
|
||||
* @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest]
|
||||
*
|
||||
* @example Create a Certificate Signing Request
|
||||
* ```js
|
||||
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||
* commonName: 'test.example.com'
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Certificate Signing Request with both common and alternative names
|
||||
* ```js
|
||||
* const [certificateKey, certificateRequest] = await acme.crypto.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.crypto.createCsr({
|
||||
* commonName: '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 ECDSA private key
|
||||
* ```js
|
||||
* const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
||||
*
|
||||
* const [, certificateRequest] = await acme.crypto.createCsr({
|
||||
* commonName: 'test.example.com'
|
||||
* }, certificateKey);
|
||||
*/
|
||||
|
||||
exports.createCsr = async (data, keyPem = null) => {
|
||||
if (!keyPem) {
|
||||
keyPem = await createPrivateRsaKey(data.keySize);
|
||||
}
|
||||
else if (!Buffer.isBuffer(keyPem)) {
|
||||
keyPem = Buffer.from(keyPem);
|
||||
}
|
||||
|
||||
if (typeof data.altNames === 'undefined') {
|
||||
data.altNames = [];
|
||||
}
|
||||
|
||||
/* Get key info and JWK */
|
||||
const info = getKeyInfo(keyPem);
|
||||
const jwk = getJwk(keyPem);
|
||||
const extensionRequests = [];
|
||||
|
||||
/* Missing support for NIST curve names in jsrsasign - https://github.com/kjur/jsrsasign/blob/master/src/asn1x509-1.0.js#L4388-L4393 */
|
||||
if (jwk.crv && (jwk.kty === 'EC')) {
|
||||
jwk.crv = convertNistCurveNameToSecg(jwk.crv);
|
||||
}
|
||||
|
||||
/* 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
|
||||
});
|
||||
|
||||
/* SAN extension */
|
||||
if (data.altNames.length) {
|
||||
extensionRequests.push({
|
||||
extname: 'subjectAltName',
|
||||
array: formatCsrAltNames(data.altNames)
|
||||
});
|
||||
}
|
||||
|
||||
/* Create CSR */
|
||||
const csr = new jsrsasign.KJUR.asn1.csr.CertificationRequest({
|
||||
subject: { array: subject },
|
||||
sigalg: info.signatureAlgorithm,
|
||||
sbjprvkey: keyPem.toString(),
|
||||
sbjpubkey: jwk,
|
||||
extreq: extensionRequests
|
||||
});
|
||||
|
||||
/* Sign CSR, get PEM */
|
||||
csr.sign();
|
||||
const pem = csr.getPEM();
|
||||
|
||||
/* Done */
|
||||
return [keyPem, Buffer.from(pem)];
|
||||
};
|
||||
Reference in New Issue
Block a user