Created
August 18, 2023 13:26
-
-
Save wparad/0cec914b5e9f486894cbc1e1138e4544 to your computer and use it in GitHub Desktop.
Get a cert signed by KMS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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