Skip to main content

Find Expiring TLS/SSL Certs

·2536 words·12 mins·
prettygoodping TLS SSL Go curl perl Let's Encrypt
Table of Contents
❀️ It's great to see you here! I'm currently available on evenings and weekends for consulting and freelance work. Let's chat about how I can help you achieve your goals.

πŸ”’ TLS/SSL is Everywhere
#

TLS/SSL certificates used to be much harder to deploy than they are today. Generally you would have either created a self-signed certificate (not great) or purchased a certificate from a trusted organization (often expensive).

If you were purchasing, you might even have had to fax some information about yourself to the company that was selling the certificate. Maybe you had to wait for up to 48 hours to get your shiny, new certificate. There was enough friction involved in the process that TLS/SSL was not the norm.

Love padlocks on the Butchers’ Bridge (Ljubljana)

This has all changed with the availability of free TLS/SSL certificates from Let’s Encrypt. There is now a much wider adoption of TLS/SSL encryption across the Internet. It is becoming less and less common to find web sites which don’t offer https. Wider adoption of TLS/SSL is a very good thing, but with this added layer of security there comes an added layer of complexity. Just like domain name registrations, TLS/SSL certificates need to be renewed regularly, otherwise they expire. This means you now have one more thing to monitor. 1

In this post we’ll look at a three different ways to automate getting TLS/SSL certificate expiration dates. 2 Before we get to that, is an easier way?

πŸš€ There Is an Easier Way
#

Getting an overview of all of your TLS/SSL certificates and their expiry dates can be cumbersome, so I created prettygoodping.com. It’s currently in a free beta.

Add Status Check Form

Within a few clicks you’ll be able to set up a dashboard of your TLS/SSL certificates, giving you an easy overview of when they expire. You may also opt in to email notifications to remind you when you a certificate is in danger of expiring. If you sign up for the free beta now, you can monitor the expiry of up to 25 TLS/SSL certificates on your own personal dashboard.

TLS/SSL Expiry Alerts

πŸ‘‰ prettygoodping.com πŸ‘ˆ

That’s the easy way, but where is the fun in that? Let’s look at doing it the hard way.

πŸ”¨ Building Our Own Solution
#

While prettygoodping.com gives us a convenient way to do this, we can also roll our own solution. Let’s have a look at how to go about obtaining TLS/SSL cert expiration dates. 3

curl
#

For our first example, let’s use curl, which is already installed in a lot of places.

curl https://www.prettygoodping.com -vI --stderr - | \
    grep "  * expire date"

Our output is:

*  expire date: Mar  9 22:27:01 2023 GMT

We can iterate on this to return just the date, by using a regex in Perl. We’ll grab everything after : and assume that’s the date.

curl https://www.prettygoodping.com -vI --stderr - | \
    grep "  * expire date" | \
    perl -nE 'm/: (.*)/;say $1'

Our output is:

Mar  9 22:27:01 2023 GMT

That’s a helpful start. We could build something more complex that would do some date comparisons directly at the command line, but this seems like a good time to create a script.

How would this look in Go?

Golang
#

In Go, we get the expiry date using only standard libraries – and with surprisingly few lines of code.

 1// main checks a certificate expiration date
 2package main
 3
 4import (
 5	"crypto/tls"
 6	"fmt"
 7	"log"
 8	"time"
 9)
10
11func main() {
12	conn, err := tls.Dial("tcp", "www.prettygoodping.com:443", nil)
13	if err != nil {
14		log.Fatal("Could not find SSL certificate: " + err.Error())
15	}
16
17	expiry := conn.ConnectionState().PeerCertificates[0].NotAfter
18	fmt.Printf("Expiry: %v\n", expiry.Format(time.RFC850))
19}

Our output is:

Expiry: Thursday, 09-Mar-23 22:27:01 UTC

Done – next language, please!

πŸ§… πŸͺ Perl
#

The Net::SSL::ExpireDate module makes this trivial to do in Perl. A minimal example looks something like this:

 1#!/usr/bin/env perl
 2
 3use 5.12.0; # use strict/use warnings/use feature qw( say )
 4
 5use Net::SSL::ExpireDate ();
 6
 7my $hostname = 'www.prettygoodping.com';
 8
 9my $ed = Net::SSL::ExpireDate->new(
10    https   => $hostname,
11    timeout => 5,
12);
13
14my $date = $ed->expire_date;
15if (!$date) {
16    warn 'Could not find expiration date for ' . $hostname;
17    exit(1);
18}
19
20say sprintf( "%s expires at %s", $hostname, $date );

Our output is:

www.prettygoodping.com expires at 2023-03-09T22:27:01

The object which is returned by the expire_date() method is a DateTime object in UTC. If we want to do some date math to see if the certificate is still valid, that’s easy enough to do. (Note that DateTime->now also returns an object in UTC). Changes begin at line 22.

 1#!/usr/bin/env perl
 2
 3use 5.12.0; # use strict/use warnings/use feature qw( say )
 4
 5use Net::SSL::ExpireDate ();
 6
 7my $hostname = 'www.prettygoodping.com';
 8
 9my $ed = Net::SSL::ExpireDate->new(
10    https   => $hostname,
11    timeout => 5,
12);
13
14my $date = $ed->expire_date;
15if (!$date) {
16    warn 'Could not find expiration date for ' . $hostname;
17    exit(1);
18}
19
20say sprintf( "%s expires at %s", $hostname, $date );
21
22if ( $ed->expire_date < DateTime->now ) {
23    say 'Your certificate has already expired';
24}
25else {
26    for my $weeks ( 1 .. 12 ) {
27        if ( $ed->expire_date < DateTime->now->add( weeks => $weeks ) ) {
28            say sprintf(
29                'Your certificate expires in less than %i weeks',
30                $weeks
31            );
32            last;
33        }
34    }
35}

Our output is:

www.prettygoodping.com expires at 2023-03-09T22:27:01
Your certificate expires in less than 9 weeks

If we want to allow for warnings, our example looks something like:

 1#!/usr/bin/env perl
 2
 3use 5.36.0;
 4
 5use Net::SSL::ExpireDate ();
 6
 7my $hostname = 'www.prettygoodping.com';
 8
 9my $ed = Net::SSL::ExpireDate->new(
10    https   => $hostname,
11    timeout => 5,
12);
13
14my ( $date, $messages ) = ssl_expiry($hostname);
15
16if ( $messages->@* ) {
17    say 'The following issues were encountered:';
18    say $_ for $messages->@*;
19}
20
21exit 1 unless $date;
22
23say $hostname . ' expires at ' . $date;
24
25if ( $ed->expire_date < DateTime->now ) {
26    say 'Your certificate has already expired';
27}
28else {
29    for my $weeks ( 1 .. 12 ) {
30        if ( $ed->expire_date < DateTime->now->add( weeks => $weeks ) ) {
31            say sprintf(
32                'Your certificate expires in less than %i weeks',
33                $weeks
34            );
35            last;
36        }
37    }
38}
39
40sub ssl_expiry ( $hostname, $timeout = 5 ) {
41    my $warning;
42
43    # expire_date could generate non-fatal warnings. We will catch them here
44    # and return them later.
45    local $SIG{__WARN__} = sub { $warning = shift };
46    my $ed = Net::SSL::ExpireDate->new(
47        https   => $hostname,
48        timeout => $timeout,
49    );
50
51    my $date = $ed->expire_date;
52    my @messages;
53
54    if ($warning) {
55        chomp $warning;
56        push @messages, $warning;
57    }
58
59    return ( $date, \@messages ) if $date;
60
61    push @messages,
62        'Could not find expiration date for a certificate at ' . $hostname;
63
64    return ( undef, \@messages );
65}

Let’s take a look at what changed:

diff --git a/02-minimal-with-date-math.pl b/03-with-error-handling.pl
old mode 100755
new mode 100644
index ac89ade..0c6b4b2
--- a/02-minimal-with-date-math.pl
+++ b/03-with-error-handling.pl
@@ -1,6 +1,6 @@
 #!/usr/bin/env perl
 
-use 5.12.0; # use strict/use warnings/use feature qw( say )
+use 5.36.0;
 
 use Net::SSL::ExpireDate ();
 
@@ -11,13 +11,16 @@ my $ed = Net::SSL::ExpireDate->new(
     timeout => 5,
 );
 
-my $date = $ed->expire_date;
-if (!$date) {
-    warn 'Could not find expiration date for ' . $hostname;
-    exit(1);
+my ( $date, $messages ) = ssl_expiry($hostname);
+
+if ( $messages->@* ) {
+    say 'The following issues were encountered:';
+    say $_ for $messages->@*;
 }
 
-say sprintf( "%s expires at %s", $hostname, $date );
+exit 1 unless $date;
+
+say $hostname . ' expires at ' . $date;
 
 if ( $ed->expire_date < DateTime->now ) {
     say 'Your certificate has already expired';
@@ -33,3 +36,30 @@ else {
         }
     }
 }
+
+sub ssl_expiry ( $hostname, $timeout = 5 ) {
+    my $warning;
+
+    # expire_date could generate non-fatal warnings. We will catch them here
+    # and return them later.
+    local $SIG{__WARN__} = sub { $warning = shift };
+    my $ed = Net::SSL::ExpireDate->new(
+        https   => $hostname,
+        timeout => $timeout,
+    );
+
+    my $date = $ed->expire_date;
+    my @messages;
+
+    if ($warning) {
+        chomp $warning;
+        push @messages, $warning;
+    }
+
+    return ( $date, \@messages ) if $date;
+
+    push @messages,
+        'Could not find expiration date for a certificate at ' . $hostname;
+
+    return ( undef, \@messages );
+}

This example is a little more robust. Note the use 5.36.0 near the top of the script. Among other things, it automatically imports the say function, turns on strict, warnings, and signatures and postfix dereferencing

I love this shorthand. I’ve been using it a lot lately.

We’re almost done. Let’s now add some command line flags so that we can avoid hardcoding the host.

 1#!/usr/bin/env perl
 2
 3use 5.36.0;
 4
 5use English                   qw( *PROGRAM_NAME );
 6use Getopt::Long::Descriptive qw( describe_options );
 7use Net::SSL::ExpireDate      ();
 8
 9my ( $opt, $usage ) = describe_options(
10    "$PROGRAM_NAME %o <some-arg>",
11    [ 'host=s',      'the server to connect to', { required => 1 } ],
12    [ 'timeout|t=i', 'timeout (in seconds)',     { default  => 5 } ],
13    [],
14    [ 'help', 'print usage message and exit', { shortcircuit => 1 } ],
15);
16
17print( $usage->text ), exit if $opt->help;
18my $hostname = $opt->host;
19
20my $ed = Net::SSL::ExpireDate->new(
21    https   => $hostname,
22    timeout => $opt->timeout,
23);
24
25my ( $date, $messages ) = ssl_expiry($hostname);
26
27if ( $messages->@* ) {
28    say 'The following issues were encountered:';
29    say $_ for $messages->@*;
30}
31
32exit 1 unless $date;
33
34say $hostname . ' expires at ' . $date;
35
36if ( $ed->expire_date < DateTime->now ) {
37    say 'Your certificate has already expired';
38}
39else {
40    for my $weeks ( 1 .. 12 ) {
41        if ( $ed->expire_date < DateTime->now->add( weeks => $weeks ) ) {
42            say sprintf(
43                'Your certificate expires in less than %i weeks',
44                $weeks
45            );
46            last;
47        }
48    }
49}
50
51sub ssl_expiry ( $hostname, $timeout = 5 ) {
52    my $warning;
53
54    # expire_date could generate non-fatal warnings. We will catch them here
55    # and return them later.
56    local $SIG{__WARN__} = sub { $warning = shift };
57    my $ed = Net::SSL::ExpireDate->new(
58        https   => $hostname,
59        timeout => $timeout,
60    );
61
62    my $date = $ed->expire_date;
63    my @messages;
64
65    if ($warning) {
66        chomp $warning;
67        push @messages, $warning;
68    }
69
70    return ( $date, \@messages ) if $date;
71
72    push @messages,
73        'Could not find expiration date for a certificate at ' . $hostname;
74
75    return ( undef, \@messages );
76}

Let’s take a look at what changed:

diff --git a/03-with-error-handling.pl b/06-lookup-script.pl
old mode 100644
new mode 100755
index 0c6b4b2..57d4e4d
--- a/03-with-error-handling.pl
+++ b/06-lookup-script.pl
@@ -2,13 +2,24 @@
 
 use 5.36.0;
 
-use Net::SSL::ExpireDate ();
+use English                   qw( *PROGRAM_NAME );
+use Getopt::Long::Descriptive qw( describe_options );
+use Net::SSL::ExpireDate      ();
 
-my $hostname = 'www.prettygoodping.com';
+my ( $opt, $usage ) = describe_options(
+    "$PROGRAM_NAME %o <some-arg>",
+    [ 'host=s',      'the server to connect to', { required => 1 } ],
+    [ 'timeout|t=i', 'timeout (in seconds)',     { default  => 5 } ],
+    [],
+    [ 'help', 'print usage message and exit', { shortcircuit => 1 } ],
+);
+
+print( $usage->text ), exit if $opt->help;
+my $hostname = $opt->host;
 
 my $ed = Net::SSL::ExpireDate->new(
     https   => $hostname,
-    timeout => 5,
+    timeout => $opt->timeout,
 );
 
 my ( $date, $messages ) = ssl_expiry($hostname);

The biggest change is the use of Getopt::Long::Descriptive. It’s not a lot of lines of code, but it gives us a few things.

  1. We can use command line args to specify host and timeout values
  2. Some rudimentary validation will happen in the argument parsing
  3. We get some help text if a command is incorrect or if --help is invoked

We can now invoke our script to look up an arbitrary host:

./06-lookup-script.pl --host www.prettygoodping.com

Let’s explore the other options as well. We can specify a timeout:

./06-lookup-script.pl --host www.prettygoodping.com --timeout 5

We can specify an arbitrary port:

./06-lookup-script.pl --host smtp.gmail.com:465

We can use an IP address rather than a host name:

./06-lookup-script.pl --host 142.250.180.14

With our error handling, we now get a helpful error message when something goes wrong. Here we have provided an invalid host:

./06-lookup-script.pl --host www.prettygoodping.comx
The following issues were encountered:
cannot create socket: Invalid argument at 06-lookup-script.pl line 62.
Could not find expiration date for a certificate at www.prettygoodping.comx

Since there was a problem, we get a non-zero exit code:

$ echo $?
1

If we want more detailed debugging information, we can use Devel::Confess. Using it via the PERL5OPT environment variable is a nice trick, as it means you don’t need to change the invocation of the script itself.

PERL5OPT="-MDevel::Confess" ./06-lookup-script.pl --host www.prettygoodping.comx
The following issues were encountered:
cannot create socket: Invalid argument at /Users/olafalders/.plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Net/SSL/ExpireDate.pm line 182.
	Net::SSL::ExpireDate::_peer_certificate("www.prettygoodping.comx", 443, 5, undef) called at /Users/olafalders/.plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Net/SSL/ExpireDate.pm line 98
	eval {...} called at /Users/olafalders/.plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Net/SSL/ExpireDate.pm line 98
	Net::SSL::ExpireDate::expire_date(Net::SSL::ExpireDate=HASH(0x7fb4ce824fc8)) called at 06-lookup-script.pl line 62
	main::ssl_expiry("www.prettygoodping.comx") called at 06-lookup-script.pl line 25
Could not find expiration date for a certificate at www.prettygoodping.comx

We also get helpful output when we call the script with no arguments or with --help.

06-lookup-script.pl [-t] [long options...] <some-arg>
	--host STR             the server to connect to
	--timeout INT (or -t)  timeout (in seconds)

	--help                 print usage message and exit

Note that the name of the script is injected into the help text via $PROGRAM_NAME, which is available when we import English. For most of us this is much clearer and easier to remember than using $0. See perlvar for more info.

Just copy the script above, name it as you see fit and the name of your script will be used in the help text.

Have Fun With It
#

By now you have enough information to be dangerous when it comes to finding and monitoring TLS/SSL certificate expiration dates. You can run the above script via a cron or even embed some of the logic into a pluggable monitoring system. If all of that seems to be too much, prettygoodping.com is available to you as an easy option as well.

The important thing is that you now have additional tools to make yourself aware of expiring certs well before there’s any danger of that happening. πŸŽ‰

Attribution
#

"File:Love padlocks on the Butchers' Bridge (Ljubljana).jpg" by Petar Miloőević is licensed under CC BY-SA 4.0 .


  1. Of course, there are ways to automate TLS/SSL cert renewals, but that doesn’t mean they don’t need to be monitored. Your automation could fail and you might not realize until it’s too late. Having an extra safeguard in place is a good plan. ↩︎

  2. I should note that if you’re using Let’s Encrypt, you can be notified by them via email of upcoming expirations. This is great, but relying on e-mail delivery alone is not very robust. This also doesn’t give you an on-demand overview of expiration dates. It’s a good thing to enable notification emails from Let’s Encrypt and you should do it, but we can do more. ↩︎

  3. Thanks to https://nickjanetakis.com/blog/using-curl-to-check-an-ssl-certificate-expiration-date-and-details for getting me started on the right path. ↩︎


Related

perlimports
·111 words·1 min
Go perl Programming
My β€œGo for Perl Hackers” Cheatsheet
·86 words·1 min
Go perl Programming
Opening Files Quickly from Inside vim
·191 words·1 min
vim perl Programming Open::This CPAN