command line, networking, web

Check SSL certificate expiration date with curl & openssl

In this example I will use my own domain https://michalwojcik.com.pl

Certificate can be obtained through the browser, but one has to be awared that browsers can cache certificates. So if we change them, we will have to wait some time to invalidate cache and let the browser retrieve cert one more time. Partial solution is doing this check in incognito window.

$ curl https://michalwojcik.com.pl
...
lots of output
...

$ curl -s https://michalwojcik.com.pl | wc -l
773

Hence we want to see only details of certificate, we have to use some flags.

$ curl -I https://michalwojcik.com.pl
HTTP/2 200
date: Tue, 15 Feb 2022 21:29:28 GMT
content-type: text/html; charset=UTF-8
link: <https://michalwojcik.com.pl/wp-json/>; rel="https://api.w.org/"
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=bhSYKngOkqycDBNX%2B96aUqJURULp3AvWZjIKl6%2FYM%2F1AbHJIEkA8%2B0L0gQ%2BphLDfhUYid68pK3IjTOAomPg1%2FDsv%2BwcmCbCQj2nzFM9x4Dhr91cwmwlXp0iHstWcT2znPSCcQ0yW"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
strict-transport-security: max-age=7776000
server: cloudflare
cf-ray: 6de1a1c2ca587803-VIE
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400

-I stands for HEAD request – there is no body and we can see headers only. By adding verbose -v flag, we will see even more details.

$ curl -Iv https://michalwojcik.com.pl
*   Trying 188.114.96.20:443...
* Connected to michalwojcik.com.pl (188.114.96.20) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Jul 11 00:00:00 2021 GMT
*  expire date: Jul 10 23:59:59 2022 GMT
*  subjectAltName: host "michalwojcik.com.pl" matched cert's "michalwojcik.com.pl"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fb54a80da00)
> HEAD / HTTP/2
> Host: michalwojcik.com.pl
> user-agent: curl/7.77.0
> accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 200
HTTP/2 200
< date: Tue, 15 Feb 2022 21:30:55 GMT
date: Tue, 15 Feb 2022 21:30:55 GMT
< content-type: text/html; charset=UTF-8
content-type: text/html; charset=UTF-8
< link: <https://michalwojcik.com.pl/wp-json/>; rel="https://api.w.org/"
link: <https://michalwojcik.com.pl/wp-json/>; rel="https://api.w.org/"
< cf-cache-status: DYNAMIC
cf-cache-status: DYNAMIC
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NF1A%2FxHyeNwzVfnCUDQDNd2NYoauGzJCahrMTwDw06v99QDskzY5u%2Bpq1bc94leMfkrD6W%2FvHXjmtqZpZIwst1ciB%2FQGvvtjLcjafTCxtjkNZU8FBIoD3TMQIPg5cQwefqE5bw2N"}],"group":"cf-nel","max_age":604800}
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NF1A%2FxHyeNwzVfnCUDQDNd2NYoauGzJCahrMTwDw06v99QDskzY5u%2Bpq1bc94leMfkrD6W%2FvHXjmtqZpZIwst1ciB%2FQGvvtjLcjafTCxtjkNZU8FBIoD3TMQIPg5cQwefqE5bw2N"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< strict-transport-security: max-age=7776000
strict-transport-security: max-age=7776000
< server: cloudflare
server: cloudflare
< cf-ray: 6de1a3e07fc07803-VIE
cf-ray: 6de1a3e07fc07803-VIE
< alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400

<
* Connection #0 to host michalwojcik.com.pl left intact

Above we can see details of TLS handshake and information aboot TLS cert. Hence curl writes response to stderr we are unable to grep over it. Instead of it, use following flags:

$ curl -Iv --stderr - https://michalwojcik.com.pl | grep "expire date"
*  expire date: Jul 10 23:59:59 2022 GMT

$ curl -Iv 2>&1 https://michalwojcik.com.pl | grep "expire date"
*  expire date: Jul 10 23:59:59 2022 GMT

--stderr - will direct output from curl to stdout, which is equivalent to 2>&1

openssl

The same we can achieve using openssl

$ openssl s_client -connect michalwojcik.com.pl:443 | openssl x509 -noout -dates
depth=2 C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
verify return:1
depth=1 C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3
verify return:1
depth=0 C = US, ST = California, L = San Francisco, O = "Cloudflare, Inc.", CN = sni.cloudflaressl.com
verify return:1
notBefore=Jun 10 00:00:00 2022 GMT
notAfter=Jun 10 23:59:59 2023 GMT