How can I get the expiration date of an SSL certificate in Crystal?

Hi everyone,

I’m trying to get the expiration date for an SSL cert in Crystal - with the openssl command one can do this:

openssl x509 -enddate -noout -in some_certificate.crt
notAfter=Jan  7 15:46:58 2025 GMT

In Ruby, you can do: Programmatically determine SSL certificate's expiration date in Ruby - Stack Overflow

How can I do this with Crystal ? Thanks!

Looks like the certificate type is marked as nodoc, even when it is used as part of the public API, for example via OpenSSL::SSL::Socket - Crystal 1.11.2?

Also seems LibCrypto doesn’t bind the required X509_get0_notBefore method. So doesn’t seem like it’s doable out of the box via the stdlib, but surely could get it to work with some manual effort.

1 Like

How can I do this with Crystal ?

As @Blacksmoke16 said, this isn’t doable with the Crystal Standard Library.

I put together an example that retrieves the certificate from a remote host and prints the “not after” date.

require "openssl"

lib LibCrypto
  alias ASN1_TIME = ASN1_STRING

  # NOTE: output time is GMT (UTC+00:00)
  fun asn1_time_to_tm = ASN1_TIME_to_tm(s : ASN1_TIME*, tm : LibC::Tm*) : LibC::Int
  fun x509_get0_not_after = X509_get0_notAfter(x : X509) : ASN1_TIME*
end

module OpenSSL::X509
  class Certificate
    def not_after : Time
      tm = uninitialized LibC::Tm

      raise Error.new("X509_get0_notAfter") if @cert.null?

      asn1_time = LibCrypto.x509_get0_not_after(@cert)
      raise "Invalid ASN1_TIME on X509 certificate" if LibCrypto.asn1_time_to_tm(asn1_time, pointerof(tm)) == 0
      # The C tm structure has a few quirks:
      #  - tm_year provides the year as an offset from the year 1900
      #  - tm_mon  is 0-based, meaning January is 0 and December is 11
      #  - tm_sec  can contain a leap second, which Crystal doesn't support.
      Time.utc(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec >= 60 ? 59 : tm.tm_sec)
    end
  end
end

HOST = "forum.crystal-lang.org"
PORT = 443

begin
  socket = TCPSocket.new(HOST, PORT)
  ssl = OpenSSL::SSL::Socket::Client.new(socket, hostname: HOST)
  puts "Certificate Validity for #{HOST}:#{PORT}"
  puts "\tNot After:  #{ssl.peer_certificate.not_after}"
ensure
  socket.close unless socket.nil?
  ssl.close unless ssl.nil?
end

I only tested this on Linux, but it should work on most systems (dunno about Windows).

If you want to read a certificate from a path—using the same API as Ruby—define a new initialize method and use the BIO_ and PEM_ functions from OpenSSL, such as BIO_new, BIO_write, and PEM_read_bio_X509. You’ll probably have to read the Crystal source code for OpenSSL::X509::Certificate to understand what’s already provided.

Feel free to ask for clarification if something isn’t clear.

2 Likes

thank you very much both @Blacksmoke16 and @echo - I’ll try those suggestions out and let you know what worked for me!

At Heii On-Call, where we use a set of Crystal processes to continuously monitor our customers’ HTTP(S) API endpoints and websites, we do a similar monkeypatch to what @echo described, calling LibCrypto.x509_get0_notAfter. It works very well! It resulted in Fix memory leak in `OpenSSL::SSL::Socket#peer_certificate` by compumike · Pull Request #13785 · crystal-lang/crystal · GitHub but this fix was merged a while ago in 1.10.0, so you should have nothing to worry about. :slight_smile:

1 Like