Fixing ERR_CERT_INVALID in Chrome

I’ve finished my first quarter as a Software Engineer on the Local team, which means I now have a couple of sprints under my belt!

Because Local is an Electron app, I pictured myself learning and hacking away at Javascript. While I’ve been learning more about the nuances of Typescript and React in this role, the first couple of sprints were spent investigating an ERR_CERT_INVALID error in Chrome. The specific issue was first reported in the Local Community Forums: SSL Not working on Chrome with NET::ERR_CERT_INVALID

This sort of error was difficult to troubleshoot for a few reasons:

  1. Chrome doesn’t tell you what exactly is invalid about a certain certificate.
  2. Searching the internet for more info about this error led me to results like the answers in this StackOverflow question, which basically boil down to bypassing HTTPS:

Bypassing security isn’t a solution; it’s a temporary workaround and certainly isn’t an option when you’re the dev who’s making a tool that generates SSL certificates.

I learned a lot while working on this issue and thought it best to document some of the tools and resources I found while investigating this error!

Example of the ERR_CERT_INVALID error encountered in the Chrome browser.
An example of the ERR_CERT_INVALID error in Chrome.

Not all ERR_CERT_INVALID errors are the same.

Because there are many potential reasons for a certificate to be invalid, there isn’t an easy-to-find solution for every case. I couldn’t find a definitive list of what Chrome looks for, but the main reason seems to be incorrect dates.

Invalid Cert due to Invalid Dates

When a certificate is generated, it has a Valid Before and a Valid After date:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            9b:6b:3d:a3:b9:a3:a4:b4
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=local.mydomain.com
        Validity
            Not Before: Nov 19 13:27:30 2016 GMT
            Not After : Nov 19 13:27:30 2017 GMT
        Subject: CN=local.mydomain.com

The above cert will certainly fail on most systems today because most systems think they are ahead of 2017.

Note that I mentioned “most systems think” — remember that a computer needs to set its notion of time from someplace. If the dates for a certificate seem valid, one possible avenue to explore is if a computer’s Date/Time settings are correct. Pay particular attention to if a system is oddly connecting to the network (like a VPN or a “helpful-IT” work network.)

Something about the chain is invalid.

I know that heading isn’t helpful, but I encourage you to think creatively about what sort of things would make a browser angry. In my case, the issue was caused by a doubling up of the certificate. In particular, Chrome was reporting two of the same PEM blocks.

In my case, I zeroed in on why Chrome was complaining but now needed to determine how the sites came to be in this state.

Note the doubled-up PEM certificate.

Tools for Investigation

Investigating an SSL Certificate using Chrome

I was able to get a general idea of why Chrome didn’t like the site’s certificate by clicking the area to the left of the address bar. You’ll see a lock icon for sites where SSL is working correctly. Sites with miss-configured SSL certificates will be obvious — Chrome won’t let you do anything!

This screencast covers the basics of examining the certificate and drilling down to specific pieces of data. In particular, note that:

  • You can click the icon next to the address bar for a window to drill down to specifics.
  • For most SSL-related errors, an “Advanced” button will show additional details about what went wrong.
  • The actual error (in this case ERR_CERT_INVALID) is clickable and allows you to see the raw PEM-encoded certificate.

Investigating a certificate using OpenSSL

I like polished GUIs like anyone else, but Chrome’s certificate navigator wasn’t doing what I needed to do. At this point, I turned to OpenSSL to query and decode the certificate that the server was giving. Here’s a breakdown of the commands that helped me along the way.

Using OpenSSL as a TLS Client

openssl s_client -showcerts -connect example.local:443  -servername example.local

This command allows you to make and test a connection over HTTPS using OpenSSL. You can read the parts of the above command as:

  • openssl s_client — Use s_client, which is OpenSSL’s generic SSL/TLS client for connecting to hosts.
  • -showcerts — Include the server’s certificate in the output.
  • -connect example.local:443 — Where the connection should be made. In this case, specifying the specific HTTPS port ensures that we are working with a secure connection.
  • -servername example.local — This indicates what domain you want the cert for. This is most useful when the server responds to more than one domain.

The above command should give an output similar to this:

depth=0 CN = invalid-cert-c1.local, C = XX, ST = XX, L = Fake Locality, O = "Super Fake Company, Fake.", OU = Fake Organizational Unit
verify error:num=18:self signed certificate
verify return:1
depth=0 CN = invalid-cert-c1.local, C = XX, ST = XX, L = Fake Locality, O = "Super Fake Company, Fake.", OU = Fake Organizational Unit
verify return:1
CONNECTED(00000005)
---
Certificate chain
0 s:/CN=invalid-cert-c1.local/C=XX/ST=XX/L=Fake Locality/O=Super Fake Company, Fake./OU=Fake Organizational Unit
    i:/CN=invalid-cert-c1.local/C=XX/ST=XX/L=Fake Locality/O=Super Fake Company, Fake./OU=Fake Organizational Unit
-----BEGIN CERTIFICATE-----
MIIEfTCCA2WgAwIBAgIJHCV7Ifs31UxeMA0GCSqGSIb3DQEBCwUAMIGZMR4wHAYD
VQQDExVpbnZhbGlkLWNlcnQtYzEubG9jYWwxCzAJBgNVBAYTAlhYMQswCQYDVQQI
EwJYWDEWMBQGA1UEBxMNRmFrZSBMb2NhbGl0eTEiMCAGA1UEChMZU3VwZXIgRmFr
ZSBDb21wYW55LCBGYWtlLjEhMB8GA1UECxMYRmFrZSBPcmdhbml6YXRpb25hbCBV
bml0MB4XDTIyMDIwNDE2MDUxMFoXDTMyMDIwNDE2MDUxMFowgZkxHjAcBgNVBAMT
FWludmFsaWQtY2VydC1jMS5sb2NhbDELMAkGA1UEBhMCWFgxCzAJBgNVBAgTAlhY
MRYwFAYDVQQHEw1GYWtlIExvY2FsaXR5MSIwIAYDVQQKExlTdXBlciBGYWtlIENv
bXBhbnksIEZha2UuMSEwHwYDVQQLExhGYWtlIE9yZ2FuaXphdGlvbmFsIFVuaXQw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/BGKuaQTetBNytk54co3u
E7/hbK5b2PnwpWBO7j37+qpqtiyh5iy5RMlXcjEpnTj5oeSGuZppEa0onDO04rFo
Yz9PLawcGlH2NgZqNqAHczVEl5Q5wElqAQNCnXRA3bjvgWzV2VU5bTgVwIMF3ppw
J6eVPfrxT7GBzVzjAXdOMlJVNLETq+yiniEkN1+nEmc2tIq3ypN4YQJlwNryQ6e6
2zEebgnaPDQaQyDkEeOpQM+EYqeqpDjRqR46rG3gGRALlJs1kKigNAI4ozk0eZQK
/bTRNCtIOjcjcTu7wwQNRgqWoeFLR3+d9qA/Sdq0d2xC8tOC/7ua+u1pYYNMIJu1
AgMBAAGjgcUwgcIwCQYDVR0TBAIwADALBgNVHQ8EBAMCAvQwOwYDVR0lBDQwMgYI
KwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUFBwMI
MBEGCWCGSAGG+EIBAQQEAwIA9zAdBgNVHQ4EFgQUssqVADjlsz0/t+bpBK1j2+qy
2hwwOQYDVR0RBDIwMIIVaW52YWxpZC1jZXJ0LWMxLmxvY2FsghcqLmludmFsaWQt
Y2VydC1jMS5sb2NhbDANBgkqhkiG9w0BAQsFAAOCAQEAsjSC8Vlndbz58eZqd/7/
f7j86m3NjZxVzzOKMXWSAnoY+3Uy1z0DT+u4uxDMuY6qx+OskmsUW4nVlidicpye
T2EZUQuykCTJh/aqP6wdPoPuNJ0kYaG+cq/b+T1gDG0eVWT1jER3PmPl1hpOdWpd
kzY1ErUQjN+hQXQ4a31rNUkS7hdMOsu1l+Xwz5n21SkAPc14rIoQfssny0SnCvCQ
kV27Pm5OSAgybmVsD/2WnbIC5aEgcGX2rVgLk4kOB+25REXbwkm1KKggW+1Ae5qN
ptEZ69kacQj2DZxcNyBZEBZbSu9EUFbvrtHkj+boQcZ7AKYdgWGyq8EkuI+okLAl
CA==
-----END CERTIFICATE-----
---
Server certificate
subject=/CN=invalid-cert-c1.local/C=XX/ST=XX/L=Fake Locality/O=Super Fake Company, Fake./OU=Fake Organizational Unit
issuer=/CN=invalid-cert-c1.local/C=XX/ST=XX/L=Fake Locality/O=Super Fake Company, Fake./OU=Fake Organizational Unit
---
No client certificate CA names sent
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 1859 bytes and written 352 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: BA84087FF3DEA801A256A4F0B89D30DDA63098C995B3C1D8770215A2B1DC29D7
    Session-ID-ctx: 
    Master-Key: BF6930C37EABB95DFD70A50780956197743BC23BBE6BA3FCAACA8452C36236365734CE4C7244A6110763675CF954CF64
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - c7 48 17 29 cf 87 1f 51-0c d2 f8 78 cc 64 6e 9e   .H.)...Q...x.dn.
    0010 - 7f b7 c6 52 cb 71 48 aa-5a a0 54 13 44 7c c2 ba   ...R.qH.Z.T.D|..
    0020 - 00 8e 61 d0 d8 42 21 67-4b 3f 0c 87 64 c0 64 48   ..a..B!gK?..d.dH
    0030 - 91 73 90 30 5f 25 4f 89-56 ea a6 55 85 4f c8 3d   .s.0_%O.V..U.O.=
    0040 - 89 18 b2 bd 81 c3 a9 62-12 7d 76 51 6b 5f 33 90   .......b.}vQk_3.
    0050 - 2b 38 45 38 ce d2 af 91-cf 49 4f 99 0a d9 6c 4c   +8E8.....IO...lL
    0060 - 93 8a 79 fb be 77 03 d8-1a b4 08 bf 77 27 36 b5   ..y..w......w'6.
    0070 - 2e 98 bd 2c 95 e9 ba 79-9f 05 95 0b e6 3c e2 43   ...,...y.....<.C
    0080 - f1 64 b5 25 00 8c f8 b4-3d 01 3f 87 5f dd 97 b4   .d.%....=.?._...
    0090 - f7 12 61 ff 61 c4 39 13-5c fc dc 1d fa 37 11 0f   ..a.a.9.\....7..
    00a0 - c5 d4 10 df cf 9a 87 05-c3 eb 6e 19 97 f6 1f d1   ..........n.....
    00b0 - b6 2b 0b 66 f0 a1 f9 fa-7a ce c1 3d 5a 77 80 48   .+.f....z..=Zw.H
    00c0 - c4 f8 a8 5f 65 f2 7e c3-f9 a3 06 e8 e7 a8 87 be   ..._e.~.........

    Start Time: 1644343457
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---
DONE

Verifying a certificate with OpenSSL

openssl verify -purpose any example.local.pem

Making a secure connection using the s_client command is useful, but what if you wanted to verify and work with the certificate from the above output? The above openssl verify command examines a certificate and determines if it is valid.

As a concrete example, you can save the PEM-encoded certificate in the above output to a file and receive output that looks something like this:

★  openssl verify -purpose any daniela.pem
daniela.pem: CN = danielaorg.local, C = XX, ST = XX, L = Fake Locality, O = "Super Fake Company, Fake.", OU = Fake Organizational Unit
error 18 at 0 depth lookup:self signed certificate
OK

For me, on my journey of understanding ERR_CERT_INVALID, verifying the cert ended up not helping. It confirmed what I knew, which was that this certificate was self-signed. But my next question was: “What exactly is in that certificate?”

Viewing the certificate contents with OpenSSL

openssl x509 -text -in example.local.pem

The above command decrypts the PEM-formated cert to get something more human-readable. Calling out a few items specifically:

  1. Lines 8-10 — Date validity. Double-check that the certificate is for a date range that makes sense.
  2. Lines 7,11 — The cert’s Subject and Issuer fields are usually different “things”. In the output below, they are the same organization, which implies that this is a self-signed certificate.
  3. Lines 36-37 — CA:FALSE means that this certificate can’t be considered a Certificate Authority. This ended up being the source of my bug. The certificate wasn’t authorized to be a Certificate Authority, but it was a self-signed cert. Making it so these certificates could be considered CAs made Chrome stop complaining!
  4. Lines 39,41 — These lines detail how this certificate can be used. In the previous openssl verify command, we used the -purpose any flag to ask if this certificate was verifiable for ANY use.
Certificate:
    Data:
	Version: 3 (0x2)
	Serial Number:
	    59:c4:b6:1f:0a:8c:81:f6:5b
    Signature Algorithm: sha256WithRSAEncryption
	Issuer: CN=brayanname.local, C=XX, ST=XX, L=Fake Locality, O=Super Fake Company, Fake., OU=Fake Organizational Unit
	Validity
	    Not Before: Feb 15 22:41:37 2022 GMT
	    Not After : Feb 15 22:41:37 2032 GMT
	Subject: CN=brayanname.local, C=XX, ST=XX, L=Fake Locality, O=Super Fake Company, Fake., OU=Fake Organizational Unit
	Subject Public Key Info:
	    Public Key Algorithm: rsaEncryption
		Public-Key: (2048 bit)
		Modulus:
		    00:e8:08:84:51:84:fb:f0:97:4a:c2:4c:d7:87:0e:
		    d5:10:57:41:06:92:08:8a:cd:9e:8e:a1:59:ef:b6:
		    d0:8b:22:a5:1b:ea:4d:77:7d:24:59:d2:1e:2a:54:
		    1d:9a:7f:d2:92:a6:0a:4e:33:63:69:fb:f5:1a:3a:
		    41:0e:11:34:7f:42:62:58:58:ee:a4:f9:88:a1:ed:
		    bf:ba:9e:b3:b9:00:16:77:6c:92:5b:ed:bf:ed:79:
		    89:7b:cd:cc:26:b5:fb:ce:d2:4f:9a:1b:18:01:2b:
		    02:65:f7:f6:90:7e:22:19:ff:f7:d4:3c:8c:09:c8:
		    4f:d1:84:ac:a2:1c:ae:55:1a:87:73:79:63:c2:8d:
		    6b:57:fa:9f:cc:c1:e4:5a:3c:ef:38:17:76:3c:5f:
		    c3:bc:b9:c5:0f:50:dd:4a:7e:bc:00:bc:ca:64:53:
		    af:b1:8d:d2:ad:2e:b6:47:0a:27:a7:03:7b:bb:c0:
		    f4:74:f9:a5:88:9f:ff:59:aa:e1:02:31:ef:db:04:
		    dc:34:a2:49:d9:8f:c8:87:8c:6a:0a:2c:ee:8f:34:
		    b3:7c:d2:98:b5:9e:26:38:8c:33:7c:ab:3b:1f:e7:
		    1d:55:0b:be:76:ac:00:9a:c5:01:f1:1b:47:67:90:
		    d4:37:c2:64:24:38:4e:0c:94:14:ce:3d:a1:77:68:
		    51:d1
		Exponent: 65537 (0x10001)
	X509v3 extensions:
	    X509v3 Basic Constraints: 
		CA:FALSE
	    X509v3 Key Usage: 
		Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment, Certificate Sign
	    X509v3 Extended Key Usage: 
		TLS Web Server Authentication, TLS Web Client Authentication, Code Signing, E-mail Protection, Time Stamping
	    Netscape Cert Type: 
		SSL Client, SSL Server, S/MIME, Object Signing, SSL CA, S/MIME CA, Object Signing CA
	    X509v3 Subject Key Identifier: 
		05:CC:36:C8:0C:42:06:58:5D:DF:DC:47:58:3D:B3:A7:C6:30:75:06
	    X509v3 Subject Alternative Name: 
		DNS:brayanname.local, DNS:*.brayanname.local
    Signature Algorithm: sha256WithRSAEncryption
	 5b:d9:bf:b8:53:7d:2c:48:8c:f4:01:3b:6f:2a:e6:06:c0:07:
	 13:60:82:02:f7:f8:f0:88:4d:c4:20:ca:c3:77:e9:29:21:ea:
	 5b:87:1e:57:12:54:51:c5:ef:32:77:06:3b:78:f8:bd:d7:a9:
	 b4:e2:5c:34:bd:b1:af:b3:ce:54:b3:c1:7a:7e:d9:b6:d4:0a:
	 45:ef:00:6b:78:30:ca:89:f8:f7:d5:91:5c:2d:c6:0f:ce:69:
	 f4:3f:98:17:c2:43:8f:9a:5b:e3:dd:46:81:07:0b:5e:a5:40:
	 fb:81:28:7b:90:20:3a:8d:37:01:d1:93:16:9d:fd:e8:e5:fd:
	 9d:73:b4:5b:73:f7:e6:89:50:ca:74:90:4d:c6:d0:1b:f5:c7:
	 d6:65:12:2c:f2:63:cb:74:34:64:ad:1e:dc:49:9c:c8:74:56:
	 f2:3b:92:df:2f:12:a8:38:5b:22:f0:96:ef:e4:8a:39:b3:64:
	 ed:4f:67:c7:db:38:74:64:58:2c:cc:3b:f9:75:ee:66:81:7c:
	 44:88:07:a9:cf:4c:1c:03:9c:38:90:5e:31:97:ba:2f:5e:0e:
	 30:db:8c:16:5b:01:ec:c3:0d:7d:92:88:da:1a:97:fa:59:d9:
	 b5:9e:4e:b7:5e:7c:20:5d:df:77:6c:8c:73:ff:f4:59:6a:e3:
	 a8:d6:be:02
-----BEGIN CERTIFICATE-----
MIIEaTCCA1GgAwIBAgIJWcS2HwqMgfZbMA0GCSqGSIb3DQEBCwUAMIGUMRkwFwYD
VQQDExBicmF5YW5uYW1lLmxvY2FsMQswCQYDVQQGEwJYWDELMAkGA1UECBMCWFgx
FjAUBgNVBAcTDUZha2UgTG9jYWxpdHkxIjAgBgNVBAoTGVN1cGVyIEZha2UgQ29t
cGFueSwgRmFrZS4xITAfBgNVBAsTGEZha2UgT3JnYW5pemF0aW9uYWwgVW5pdDAe
Fw0yMjAyMTUyMjQxMzdaFw0zMjAyMTUyMjQxMzdaMIGUMRkwFwYDVQQDExBicmF5
YW5uYW1lLmxvY2FsMQswCQYDVQQGEwJYWDELMAkGA1UECBMCWFgxFjAUBgNVBAcT
DUZha2UgTG9jYWxpdHkxIjAgBgNVBAoTGVN1cGVyIEZha2UgQ29tcGFueSwgRmFr
ZS4xITAfBgNVBAsTGEZha2UgT3JnYW5pemF0aW9uYWwgVW5pdDCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAOgIhFGE+/CXSsJM14cO1RBXQQaSCIrNno6h
We+20IsipRvqTXd9JFnSHipUHZp/0pKmCk4zY2n79Ro6QQ4RNH9CYlhY7qT5iKHt
v7qes7kAFndsklvtv+15iXvNzCa1+87ST5obGAErAmX39pB+Ihn/99Q8jAnIT9GE
rKIcrlUah3N5Y8KNa1f6n8zB5Fo87zgXdjxfw7y5xQ9Q3Up+vAC8ymRTr7GN0q0u
tkcKJ6cDe7vA9HT5pYif/1mq4QIx79sE3DSiSdmPyIeMagos7o80s3zSmLWeJjiM
M3yrOx/nHVULvnasAJrFAfEbR2eQ1DfCZCQ4TgyUFM49oXdoUdECAwEAAaOBuzCB
uDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIC9DA7BgNVHSUENDAyBggrBgEFBQcDAQYI
KwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgwEQYJYIZIAYb4
QgEBBAQDAgD3MB0GA1UdDgQWBBQFzDbIDEIGWF3f3EdYPbOnxjB1BjAvBgNVHREE
KDAmghBicmF5YW5uYW1lLmxvY2FsghIqLmJyYXlhbm5hbWUubG9jYWwwDQYJKoZI
hvcNAQELBQADggEBAFvZv7hTfSxIjPQBO28q5gbABxNgggL3+PCITcQgysN36Skh
6luHHlcSVFHF7zJ3Bjt4+L3XqbTiXDS9sa+zzlSzwXp+2bbUCkXvAGt4MMqJ+PfV
kVwtxg/OafQ/mBfCQ4+aW+PdRoEHC16lQPuBKHuQIDqNNwHRkxad/ejl/Z1ztFtz
9+aJUMp0kE3G0Bv1x9ZlEizyY8t0NGStHtxJnMh0VvI7kt8vEqg4WyLwlu/kijmz
ZO1PZ8fbOHRkWCzMO/l17maBfESIB6nPTBwDnDiQXjGXui9eDjDbjBZbAezDDX2S
iNoal/pZ2bWeTrdefCBd33dsjHP/9Flq46jWvgI=
-----END CERTIFICATE-----