Skip to content

Instantly share code, notes, and snippets.

@wparad
Created August 18, 2023 13:26
Show Gist options
  • Save wparad/0cec914b5e9f486894cbc1e1138e4544 to your computer and use it in GitHub Desktop.
Save wparad/0cec914b5e9f486894cbc1e1138e4544 to your computer and use it in GitHub Desktop.
Get a cert signed by KMS
const { KMS } = require('aws-sdk');
const kmsClient = new KMS();
const keyId = 'alias/DELETE_ME';
let forge = require('node-forge');
// a hexString is considered negative if it's most significant bit is 1
// because serial numbers use ones' complement notation
// this RFC in section 4.1.2.2 requires serial numbers to be positive
// http://www.ietf.org/rfc/rfc5280.txt
function toPositiveHex(hexString) {
let mostSiginficativeHexAsInt = parseInt(hexString[0], 16);
if (mostSiginficativeHexAsInt < 8) {
return hexString;
}
mostSiginficativeHexAsInt -= 8;
return mostSiginficativeHexAsInt.toString() + hexString.substring(1);
}
/**
*
* @param {forge.pki.CertificateField[]} attrs Attributes used for subject and issuer.
* @param {object} options
* @param {number} [options.days=365] the number of days before expiration
* @param {number} [options.keySize=1024] the size for the private key in bits
* @param {object} [options.extensions] additional extensions for the certificate
* @param {string} [options.algorithm="sha1"] The signature algorithm sha256 or sha1
* @param {boolean} [options.pkcs7=false] include PKCS#7 as part of the output
* @param {boolean} [options.clientCertificate=false] generate client cert signed by the original key
* @param {string} [options.clientCertificateCN="John Doe jdoe123"] client certificate's common name
* @param {function} [done] Optional callback, if not provided the generation is synchronous
* @returns
*/
async function generate(attrs = [], options = {}) {
let generatePem = async function() {
let cert = forge.pki.createCertificate();
// eslint-disable-next-line no-sync
cert.serialNumber = toPositiveHex(forge.util.bytesToHex(forge.random.getBytesSync(9))); // the serial number can be decimal or hex (if preceded by 0x)
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + (options.days || 365));
Object.assign(attrs || [], [{
name: 'commonName',
value: 'Warren'
}, {
name: 'countryName',
value: 'US'
}, {
shortName: 'ST',
value: 'Virginia'
}, {
name: 'localityName',
value: 'Blacksburg'
}, {
name: 'organizationName',
value: 'Test'
}, {
shortName: 'OU',
value: 'Test'
}]);
cert.setSubject(attrs);
cert.setIssuer(attrs);
const publicKeyData = await kmsClient.getPublicKey({ KeyId: keyId }).promise();
const publicKey = publicKeyData.PublicKey;
cert.publicKey = forge.pki.publicKeyFromPem(`-----BEGIN PUBLIC KEY-----\n${publicKey.toString('base64')}\n-----END PUBLIC KEY-----`);
cert.setExtensions(options.extensions || [{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true
}, {
name: 'subjectAltName',
altNames: [{
type: 6, // URI
value: 'http://example.org/webid#me'
}]
}]);
cert.md = forge.md.sha256.create();
let algorithmOid = forge.pki.oids[`${cert.md.algorithm}WithRSAEncryption`];
if (!algorithmOid) {
let error = new Error('Could not compute certificate digest. '
+ 'Unknown message digest algorithm OID.');
error.algorithm = cert.md.algorithm;
throw error;
}
cert.signatureOid = cert.siginfo.algorithmOid = algorithmOid;
// get TBSCertificate, convert to DER
cert.tbsCertificate = forge.pki.getTBSCertificate(cert);
let bytes = forge.asn1.toDer(cert.tbsCertificate);
// digest and sign
cert.md.update(bytes.getBytes());
function emsaPkcs1v15encode(md) {
// get the oid for the algorithm
let oid;
if (cert.md.algorithm in forge.pki.oids) {
oid = forge.pki.oids[md.algorithm];
} else {
let error = new Error('Unknown message digest algorithm.');
error.algorithm = md.algorithm;
throw error;
}
let oidBytes = forge.asn1.oidToDer(oid).getBytes();
// create the digest info
let digestInfo = forge.asn1.create(
forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, []);
let digestAlgorithm = forge.asn1.create(
forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, []);
digestAlgorithm.value.push(forge.asn1.create(
forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, oidBytes));
digestAlgorithm.value.push(forge.asn1.create(
forge.asn1.Class.UNIVERSAL, forge.asn1.Type.NULL, false, ''));
let digest = forge.asn1.create(
forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OCTETSTRING,
false, md.digest().getBytes());
digestInfo.value.push(digestAlgorithm);
digestInfo.value.push(digest);
// encode digest info
const hexResult = forge.asn1.toDer(digestInfo).toHex().slice(38);
return new Uint8Array(hexResult.match(/../g).map(h => parseInt(h, 16))).buffer;
}
const tempPrivateKey = `-----BEGIN PRIVATE KEY-----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-----END PRIVATE KEY-----`;
cert.sign(forge.pki.privateKeyFromPem(tempPrivateKey), forge.md.sha256.create());
// const tempHexResult = '185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969';
// const temp = new Uint8Array(tempHexResult.match(/../g).map(h => parseInt(h, 16))).buffer;
// const signedResponse2 = await kmsClient.sign({ KeyId: keyId, SigningAlgorithm: 'RSASSA_PKCS1_V1_5_SHA_256', MessageType: 'DIGEST', Message: temp }).promise();
const signedResponse = await kmsClient.sign({ KeyId: keyId, SigningAlgorithm: 'RSASSA_PKCS1_V1_5_SHA_256', MessageType: 'DIGEST', Message: emsaPkcs1v15encode(cert.md) }).promise();
cert.signature = signedResponse.Signature;
const fingerprint = forge.md.sha1
.create()
.update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes())
.digest()
.toHex()
.match(/.{2}/g)
.join(':');
let pem = {
cert: forge.pki.certificateToPem(cert),
fingerprint: fingerprint
};
return pem;
};
const result = await generatePem();
return result;
}
generate();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment