digitalbazaar / forge

A native implementation of TLS in Javascript and tools to write crypto-based and network-heavy webapps
https://digitalbazaar.com/
Other
5.06k stars 779 forks source link

ERR_SSL_SERVER_CERT_BAD_FORMAT in Chrome for self-signed cert, but works in Firefox, Safari #660

Open AnsonT opened 5 years ago

AnsonT commented 5 years ago

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:

-----BEGIN CERTIFICATE-----
MIIDTjCCAjagAwIBAgIQHIjv6OkPFQ8iuwv7QFfw3jANBgkqhkiG9w0BAQsFADBP
MRkwFwYDVQQDExBkZXZQcm94eSB0ZXN0IENBMSAwHgYDVQQKExdkZXZQcm94eSBk
ZXZlbG9wbWVudCBDQTEQMA4GA1UECxMHdGVzdCBDQTAeFw0xOTAyMjUxNTQ2MDZa
Fw00NjA3MTIxNDQ2MDZaME8xGTAXBgNVBAMTEGRldlByb3h5IHRlc3QgQ0ExIDAe
BgNVBAoTF2RldlByb3h5IGRldmVsb3BtZW50IENBMRAwDgYDVQQLEwd0ZXN0IENB
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhyM/nwN2Pn0RWGGeEZOe
OCTGTdNmJpR85RUdudAvs7hoFKIzujcgIbHd4/ivmPVF4QbCbBQMNqXTZZ5b5Cyw
NtiHWOo+5QvRQTx+kmsZFJcLlMJEvJaEII6mBkfQ7pA+Lq9Vt+P+JjE4cnFUzjel
rKf3QjjQ9QzICT5G7Gm5hvIK9VDJKsumpLRCdU9hgBXcLC4+Tm0DYdgZJTF43hBs
bGIxIYnDOg1rb+2NciCsdoBrGy/2SsCufHVu/heD4SGfHy1mXv+49HZi65lyRHiQ
VebTRnwMjff1yweYNN9DTOZijEOZ5g4FzmsFWJmwTJEvseM5peuzsfIYsZUSWqUe
3QIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
BgkqhkiG9w0BAQsFAAOCAQEALL+xiJofb+4WATSVzrE2Pe2wYxCTrCA6Lt6CvEUW
5RbdHvS2aOkODFTjMVYJtYwjQHUSJn6oEAyG0l2HmQeuM8H1AatEF9XBkGdW5iKe
mIpX3jz9mgM/paY1h2LJmzmRUCN1bN9Gm23pNV4pyvVM5UFARCn6OpwBVPl/99YG
DP7VAWFLlBRghmeGe9H7LbTk/W3oxQLSCLuT5P865syE+a0bVEfXMhK7r62LMONg
8KP/Y4pKpQj6T9EMUUXRoaVZOhBa1yae0xEuSafHKW552Ur5b4t8JVW7fswAT6TU
KvOueH9wd8koOZm7bvshVyWlEiJMRWJ/T0jf/rDOK99RsQ==
-----END CERTIFICATE-----

Server Cert:

-----BEGIN CERTIFICATE-----
MIIDiDCCAnCgAwIBAgIQX2MkSBNgtNz/X6TAS11uYzANBgkqhkiG9w0BAQsFADBP
MRkwFwYDVQQDExBkZXZQcm94eSB0ZXN0IENBMSAwHgYDVQQKExdkZXZQcm94eSBk
ZXZlbG9wbWVudCBDQTEQMA4GA1UECxMHdGVzdCBDQTAeFw0xOTAyMjUxNTQ2MzBa
Fw0yMDAyMjUxNTQ2MzBaMD0xEDAOBgNVBAsTB3Rlc3QgQ0ExKTAnBgNVBAoTIGRl
dlByb3h5IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEApQF5l4ndqYrYoxxGVkqbbJUhXMSwF1vlenJuAByTpswZ
aEy7FwY/Um7iArXz4eBR6/tMuf/ChCR4fGYgG1mLmKjCMNsC/2Z4Y/Pr9Sccjn7O
07JQMk5HrUYQGnCaCAePYch/qWUJbsNG9odwYyKZAXPUeAxuvEUzwNQGZJoq67RZ
TV6Z4G66432fP8B7MmIv0m+vXH4i2fF++wg0OOsV43/d5TI2Ow37oOmP34Pi3SxA
FXh7L1diD6qkTwpfvekoX4ebWnnB91fzrU4St7kGqlQjiYVC17E/sfPxU+GWtjib
L8aDywvA1FYWkiWqETsWqy7ToZy/2fC/zduNHu/zAQIDAQABo3IwcDAMBgNVHRMB
Af8EAjAAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNV
HSMEGDAWgBT3ysWOK9e1QswB30In9mtFxJo/kjAaBgNVHREEEzARgg90ZXN0MS5s
b2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAAJEjA16WaPpcvCjt+nw2XvPG/tg
v+PK6WKFWxGgxH196Iol67gbrolj62/7YXLiwC30isxLFTmfXDDFxWxhOtrCfecS
4tY7VYp9ysuWWqU+eGYxSxgkEaznMYK+MUJcH1fiUQVuM6NdRW0hhZ0S5D+bDG8t
Z30sgv6PL0KSBcyiGlpG99VI0jccUh7kbsW/i0+igO1IiZ6FFZurbA98z13rU6ON
rQLXGlQ9Ru+8cSqNFnPunjxlPgdnrGG1YS52CsfKW4r06UxUuDH3m9HBQXRkA7fK
AEx3Ezcyi1PAeQdP7WXv5BIYbKVVw/DsPmb4B0B6ZIHF4DtZGzCNy9Yp8jc=
-----END CERTIFICATE-----
davidlehn commented 5 years ago
AnsonT commented 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.

x-077 commented 5 years ago

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).

Nisgrak commented 4 years ago

Hi, any updates about the possible fix for the self-signed certs?

Thanks @AnsonT for the code, it help me too much!

daguej commented 4 years ago

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.

O1dMate commented 3 years ago

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);