Open AnsonT opened 5 years ago
Instead of importing the cert itself, I added the CA's cert to MacOS keychain:
security add-trusted-cert -d -p ssl -r trustroot -k "/Library/Keychains/System.keychain" "rootCA.pem"
The certs generated appears to be valid for everything except for Chrome. When Chrome browses to a site using this cert, I get the ERR_SSL_SERVER_CERT_BAD_FORMAT error. The server & cert chain works in Firefox and Safari.
The following code was used to generate the certs:
import forge, { pki, rsa, sha256 } from 'node-forge'
const isIp = require('is-ip')
// 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) {
var mostSiginficativeHexAsInt = parseInt(hexString[0], 16)
if (mostSiginficativeHexAsInt < 8) {
return hexString
}
mostSiginficativeHexAsInt -= 8
return mostSiginficativeHexAsInt.toString() + hexString.substring(1)
}
function randomSerialNumber () {
return toPositiveHex(forge.util.bytesToHex(forge.random.getBytesSync(16)))
}
// export function mkCertCA () {
export async function createCA ({ commonName, organization, organizationalUnit, validityDays }) {
const rootCertAuthorityAttr = [
{ name: 'commonName', value: commonName || organization },
{ name: 'organizationName', value: organization },
{ shortName: 'OU', value: organizationalUnit }
]
const certAuthorityExtensions = [
{
name: 'basicConstraints',
critical: true,
cA: true,
pathLenConstraint: 0
}, {
name: 'keyUsage',
critical: true,
keyCertSign: true,
cRLSign: true
}
]
const caKeyPair = rsa.generateKeyPair(2048)
const caCert = pki.createCertificate()
caCert.publicKey = caKeyPair.publicKey
caCert.serialNumber = randomSerialNumber() // see Appendix for implementation
caCert.validity.notBefore = new Date()
caCert.validity.notAfter = new Date()
caCert.validity.notAfter.setDate(caCert.validity.notAfter.getDate() + validityDays)
caCert.setSubject(rootCertAuthorityAttr)
caCert.setIssuer(rootCertAuthorityAttr)
caCert.setExtensions(certAuthorityExtensions)
caCert.sign(caKeyPair.privateKey, sha256.create())
return {
pem: {
key: pki.privateKeyToPem(caKeyPair.privateKey),
cert: pki.certificateToPem(caCert)
},
cert: caCert
}
}
export async function createCert ({ domains, validityDays, caKey, caCert }) {
const ca = pki.certificateFromPem(caCert)
const serverAttr = [
{ shortName: 'OU', value: ca.subject.getField('OU').value },
{ name: 'organizationName', value: 'devProxy development certificate' }
]
const serverExtensions = [
{
name: 'basicConstraints',
critical: true,
cA: false
},
{
name: 'keyUsage',
critical: true,
digitalSignature: true,
keyEncipherment: true
},
{
name: 'extKeyUsage',
serverAuth: true
}, {
name: 'authorityKeyIdentifier',
keyIdentifier: ca.generateSubjectKeyIdentifier().getBytes()
},
{
name: 'subjectAltName',
altNames: domains.map(domain => (
isIp(domain)
? { type: 7, ip: domain }
: { type: 2, value: domain }
))
}
]
const serverKeyPair = await rsa.generateKeyPair(2048)
const serverCert = pki.createCertificate()
serverCert.publicKey = serverKeyPair.publicKey
serverCert.serialNumber = randomSerialNumber()
serverCert.validity.notBefore = new Date()
serverCert.validity.notAfter = new Date()
serverCert.validity.notAfter.setDate(serverCert.validity.notAfter.getDate() + validityDays)
serverCert.setSubject(serverAttr)
serverCert.setIssuer(ca.subject.attributes)
serverCert.setExtensions(serverExtensions)
const signingKey = pki.privateKeyFromPem(caKey)
serverCert.sign(signingKey, sha256.create())
const serverKeyPem = pki.privateKeyToPem(serverKeyPair.privateKey)
const serverCertPem = pki.certificateToPem(serverCert)
return {
pem: {
key: serverKeyPem,
cert: serverCertPem
},
cert: serverCert
}
}
As a comparison, I used the CA cert and server cert generated by the mkcert (GoLang) project, and that was able to be used by Chrome, with the same set of properties/extensions.
For Firefox, I imported the CA cert as a trusted authority (or use nss's certutil). For Safari, it picks up the CA cert from Mac keychain, and was able to validate the https site.
Hello @AnsonT, you saved me a lot of time, thanks
@davidlehn not sure if you that could be possible but this example is working perfectly on Chrome Version 73.0.3683.103 with self sign certificat.
Maybe it could be good to link this post it in the TLS section as few people are wondering how to do so.
I was looking for the similar problem and end up using @AnsonT functions to create the CA and the server certificates.
At the end you just need to save what's return from those functions (in the pem object).
Hi, any updates about the possible fix for the self-signed certs?
Thanks @AnsonT for the code, it help me too much!
FWIW, In my case, Chrome was giving me ERR_SSL_SERVER_CERT_BAD_FORMAT
for self-issued certs because my CA cert had an @
in its CN. Reissued the CA cert without the @
and it started working.
Hey @AnsonT,
I had the same issue with the cert working in Firefox but I was getting ERR_SSL_SERVER_CERT_BAD_FORMAT error in Chrome.
It turns out my issue was because of the authorityKeyIdentifier
extension.
Added the following fix the issue for
{
name: 'authorityKeyIdentifier',
authorityCertIssuer: true,
serialNumber: caCert.serialNumber
};
The following is my working code for generating a CA, then a certificate for a host signed by my CA:
(P.S. @AnsonT I used your toPositiveHex
function, that was one of the reasons mine wasn't working initially)
const forge = require('node-forge');
const path = require('path');
const fs = require('fs');
const makeNumberPositive = (hexString) => {
let mostSiginficativeHexDigitAsInt = parseInt(hexString[0], 16);
if (mostSiginficativeHexDigitAsInt < 8) return hexString;
mostSiginficativeHexDigitAsInt -= 8
return mostSiginficativeHexDigitAsInt.toString() + hexString.substring(1)
}
const randomSerialNumber = () => {
return makeNumberPositive(forge.util.bytesToHex(forge.random.getBytesSync(20)));
}
const createCA = () => {
const keypair = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.privateKey = keypair.privateKey;
cert.serialNumber = randomSerialNumber();
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setYear(cert.validity.notAfter.getFullYear() + 100);
const attributes = [{
shortName: 'C',
value: 'AU'
}, {
shortName: 'ST',
value: 'Victoria'
}, {
shortName: 'L',
value: 'Melbourne'
}, {
shortName: 'CN',
value: 'Cert Gen Root CA'
}];
const extensions = [{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
cRLSign: true
}];
cert.setSubject(attributes);
cert.setIssuer(attributes);
cert.setExtensions(extensions);
// Self-sign certificate
cert.sign(keypair.privateKey, forge.md.sha512.create());
const pemCert = forge.pki.certificateToPem(cert);
const pemKey = forge.pki.privateKeyToPem(keypair.privateKey);
fs.writeFileSync(path.join(__dirname, 'ca.crt'), pemCert);
fs.writeFileSync(path.join(__dirname, 'ca.key'), pemKey);
return { cert: cert, privateKey: keypair.privateKey };
}
const createCert = (hostname, CA) => {
const hostKeys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
const csr = createCSR(hostname, hostKeys);
cert.publicKey = csr.publicKey;
cert.serialNumber = randomSerialNumber();
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setYear(cert.validity.notAfter.getFullYear() + 1);
const extensions = [{
name: 'basicConstraints',
cA: false
}, {
name: 'nsCertType',
server: true
}, {
name: 'subjectKeyIdentifier'
}, {
name: 'authorityKeyIdentifier',
authorityCertIssuer: true,
serialNumber: CA.cert.serialNumber
}, {
name: 'keyUsage',
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true
}, {
name: 'extKeyUsage',
serverAuth: true
}, {
name: 'subjectAltName',
altNames: [{
type: 2, // 2 is DNS type
value: hostname
}]
}];
cert.setSubject(csr.subject.attributes);
cert.setIssuer(CA.cert.subject.attributes);
cert.setExtensions(extensions);
// Self-sign certificate
cert.sign(CA.privateKey, forge.md.sha512.create());
const pemHostCert = forge.pki.certificateToPem(cert);
const pemHostKey = forge.pki.privateKeyToPem(hostKeys.privateKey);
fs.writeFileSync(path.join(__dirname, 'host.crt'), pemHostCert);
fs.writeFileSync(path.join(__dirname, 'host.key'), pemHostKey);
}
const createCSR = (hostname, hostKeys) => {
// Create a Certification Signing Request (CSR)
const csr = forge.pki.createCertificationRequest();
csr.publicKey = hostKeys.publicKey;
const subject = [{
shortName: 'C',
value: 'AU'
}, {
shortName: 'ST',
value: 'Victoria'
}, {
shortName: 'L',
value: 'Melbourne'
}, {
shortName: 'O',
value: 'whatever'
}, {
shortName: 'OU',
value: 'whatever'
}, {
shortName: 'CN',
value: 'whatever'
}];
const attributes = [{
name: 'extensionRequest',
extensions: [{
name: 'subjectAltName',
altNames: [{
type: 2, // 2 is DNS type
value: hostname
}]
}]
}];
csr.setSubject(subject);
csr.setAttributes(attributes);
// Sign the CSR using the host private key
csr.sign(hostKeys.privateKey, forge.md.sha512.create());
const csrPem = forge.pki.certificationRequestToPem(csr);
fs.writeFileSync(path.join(__dirname, 'host.csr'), csrPem);
return csr;
}
let CA = createCA();
createCert('testing.com', CA);
I generated the following self-signed CA cert and Server cert for local https development server. The certificates are added to MacOS/Firefox trust stores. The certificate works in FireFox and Safari but is reported by Chrome 72.0.3626.109 (Official Build) (64-bit) as ERR_SSL_SERVER_CERT_BAD_FORMAT.
Cert Authority Cert:
Server Cert: