tls / testing certificate chains / easycert
The openssl client is a very versatile tool, but also a bit cryptic. The easycert utility from the ossobv/vcutil scripts makes validating/managing certificates easier.
easycert from ossobv/vcutil has a
few modes of operation: CLI, CGI, generating certificates and testing
certificates. Nowadays we mostly use the testing mode: -T
The utility is a convenient wrapper around openssl s_client
and x509
calls. Get it from github.com/ossobv/vcutil
easycert.
Usage
Run it like this:
$ easycert -T HOSTNAME PORT
or like this:
$ easycert -T LOCAL_CERT_CHAIN
For example, checking the https://google.com certificate chain might look like this:
$ easycert -T google.com 443
The list below should be logically ordered,
and end with a self-signed root certificate.
(Although the last one is optional and only
overhead.)
Certificate chain
0 s: {96:65:7B:C2:08:15:03:E1:C3:F8:50:DD:8F:B6:73:65:43:DF:8C:80} [d5b02a29] C = US, ST = California, L = Mountain View, O = Google LLC, CN = *.google.com
i: {98:D1:F8:6E:10:EB:CF:9B:EC:60:9F:18:90:1B:A0:EB:7D:09:FD:2B} [99bdd351] C = US, O = Google Trust Services, CN = GTS CA 1O1
1 s: {98:D1:F8:6E:10:EB:CF:9B:EC:60:9F:18:90:1B:A0:EB:7D:09:FD:2B} [99bdd351] C = US, O = Google Trust Services, CN = GTS CA 1O1
i: {9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign
---
Expires in 67 days
There are a couple of things to note in the above output:
-
As the comment already mentions: the issuer
i
(signer) of the first certificate must be the subjects
of the next certificate. Certificate 0 is signed by certificate 1, an intermediate. (The chain may be longer.) -
When your web browser (or other application) validates the SSL/TLS certificate, it has (at least) the self signed root key. In this case:
4a6481c9
.
On a *nix system, this file will generally be located in the/etc/ssl/certs
directory, as a symlink to the actual certificate:$ ls -l /etc/ssl/certs/4a6481c9.0 lrwxrwxrwx 1 root root 27 mrt 12 2018 /etc/ssl/certs/4a6481c9.0 -> GlobalSign_Root_CA_-_R2.pem $ easycert -T /etc/ssl/certs/4a6481c9.0 ... Certificate chain 0 s: {9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign i: {9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign --- Expires in 459 days
-
The
{96:65:7B:C2:08:15:03:E1:C3:F8:50:DD:8F:B6:73:65:43:DF:8C:80}
is the X509v3 Subject Key Identifier (or Authority Key ~).
Where the certificate-subject_hash
and-issuer_hash
simply are based on the subject (and can have duplicates), the Subject Key identifier and its Authority counterpart uniquely identify a specific certificate. (More about this below.)
As you can see, easycert makes inspecting certificate chains easy.
Examples
You can also see easycert in action on various badssl.com tests:
$ easycert -T expired.badssl.com 443
...
Certificate chain
0 s: {9D:EE:C1:7B:81:0B:3A:47:69:71:18:7D:11:37:93:BC:A5:1B:3F:FB} [c98795d1] OU = Domain Control Validated, OU = PositiveSSL Wildcard, CN = *.badssl.com
i: {90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7} [8d28ae65] C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO RSA Domain Validation Secure Server CA
1 s: {90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7} [8d28ae65] C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO RSA Domain Validation Secure Server CA
i: {BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4} [d6325660] C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO RSA Certification Authority
2 s: {BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4} [d6325660] C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO RSA Certification Authority
i: {AD:BD:98:7A:34:B4:26:F7:FA:C4:26:54:EF:03:BD:E0:24:CB:54:1A} [157753a5] C = SE, O = AddTrust AB, OU = AddTrust External TTP Network, CN = AddTrust External CA Root
---
Expires in -1978 days
$ easycert -T incomplete-chain.badssl.com 443
...
Certificate chain
0 s: {9D:EE:C1:7B:81:0B:3A:47:69:71:18:7D:11:37:93:BC:A5:1B:3F:FB} [34383cd7] C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com
i: {0F:80:61:1C:82:31:61:D5:2F:28:E7:8D:46:38:B4:2C:E1:C6:D9:E2} [85cf5865] C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA
---
Expires in 612 days
X509v3 Subject Key Identifier
About the X509v3 Subject Key Identifiers and X509v3 Authority Key
Identifiers: here’s what would happen if you created a different
certificate with the same subject (and consequently the same
4a6481c9
hash), but did not supply said identifiers.
(We use the -config
option to skip the openssl default extensions.)
$ openssl genrsa -out GlobalSign-bogus.key 2048 >&2
Generating RSA private key, 2048 bit long modulus (2 primes)
.....+++++
..........+++++
e is 65537 (0x010001)
$ openssl req -batch -new -x509 \
-key GlobalSign-bogus.key -out GlobalSign-bogus.crt \
-subj '/OU=GlobalSign Root CA - R2/O=GlobalSign/CN=GlobalSign' \
-config <(printf '[req]\ndistinguished_name = req_distinguished_name\n[req_distinguished_name]\n')
$ easycert -T ./GlobalSign-bogus.crt
...
Certificate chain
0 s: {x509v3-subject-key-not-provided} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign
i: {x509v3-issuer-key--not-provided} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign
---
Expires in 29 days
Observe how it has the same 4a6481c9
hash (and it’s missing the
identifiers). Watch what happens when we try to use it for validation:
$ curl https://google.com/ --capath /dev/null \
--cacert ./GlobalSign-bogus.crt
curl: (35) error:0407008A:rsa
routines:RSA_padding_check_PKCS1_type_1:invalid padding
curl is not happy. And shows an obscure error. Obviously it’s good that it fails. It should, as the RSA key doesn’t match. But if you accidentally have multiple CA root certificates with the same hash, this can be very confusing, and a mess to sort out.
Let’s create a new one, this time adding subjectKeyIdentifier
and
authorityKeyIdentifier
:
$ rm GlobalSign-bogus.crt
$ openssl req -batch -new -x509 \
-key GlobalSign-bogus.key -out GlobalSign-bogus.crt \
-subj '/OU=GlobalSign Root CA - R2/O=GlobalSign/CN=GlobalSign' \
-config <(printf '[req]\ndistinguished_name = req_distinguished_name\n[req_distinguished_name]\n') \
-addext keyUsage=critical,cRLSign,keyCertSign \
-addext basicConstraints=critical,CA:true \
-addext subjectKeyIdentifier=hash \
-addext authorityKeyIdentifier=keyid:always,issuer
$ easycert -T ./GlobalSign-bogus.crt
...
Certificate chain
0 s: {02:40:B3:7E:46:F4:E1:32:18:8B:DF:60:F1:90:74:A7:0A:CB:1A:E8} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign
i: {02:40:B3:7E:46:F4:E1:32:18:8B:DF:60:F1:90:74:A7:0A:CB:1A:E8} [4a6481c9] OU = GlobalSign Root CA - R2, O = GlobalSign, CN = GlobalSign
---
Expires in 29 days
This time curl (in fact libssl) will reject it before complaining about invalid RSA padding.
$ curl https://google.com/ --capath /dev/null \
--cacert ./GlobalSign-bogus.crt
curl: (60) SSL certificate problem: unable to get local issuer certificate
Whereas when we manually supply the right certificate, everything works as intended:
$ curl https://google.com/ --capath /dev/null \
--cacert /etc/ssl/certs/4a6481c9.0
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
...
As a quick aside: curl will not accept an intermediate certificate to
validate against, when that does not have CA:true
flag set (which an
intermediate doesn’t have):
$ curl https://google.com/ \
--capath /dev/null --cacert ./99bdd351.crt
curl: (60) SSL certificate problem: unable to get issuer certificate
As an aside to this aside: this requirement was also observed with the
3CX phone system that needed the root
certificate. (You can check the
details of a certificate by doing
openssl x509 -in CERT -noout -text
. You’ll see the CA:TRUE
on
the root cert.)
In any case: you can coerce openssl into accepting an intermediate
certificate, if you’re explicit with the -partial_chain
flag:
$ openssl s_client -connect google.com:443 \
-CAfile ./99bdd351.crt -partial_chain
...
Verify return code: 0 (ok)
How to deal with services that don’t send intermediates
And if you’re dealing with SSL/TLS services that only supply their own certificate, you now know what to do. Put both the intermediate(s) and the root certificate in your local chain:
$ curl https://incomplete-chain.badssl.com:443/ \
--capath /dev/null --cacert ./85cf5865.crt
curl: (60) SSL certificate problem: unable to get issuer certificate
$ easycert -T ./85cf5865.crt
...
Certificate chain
0 s: {0F:80:61:1C:82:31:61:D5:2F:28:E7:8D:46:38:B4:2C:E1:C6:D9:E2} [85cf5865] C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA
i: {03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55} [3513523f] C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
---
Expires in 907 days
And after finding and adding 3513523f
:
$ curl https://incomplete-chain.badssl.com:443/ \
--capath /dev/null --cacert ./85cf5865+3513523f.crt
<!DOCTYPE html>
...
Validation succesful!