tls / testing certificate chains / easycert

tls / testing certificate chains / easycert

  • Written by
    Walter Doekes
  • Published on

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:

  1. As the comment already mentions: the issuer i (signer) of the first certificate must be the subject s of the next certificate. Certificate 0 is signed by certificate 1, an intermediate. (The chain may be longer.)

  2. 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
    
  3. 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!


Back to overview Newer post: pgp on yubikey / refresh expiry Older post: excel / generate sheet password collision