Skip to content
ecdsa.com

Certificate expiry · Ruby

How to check SSL certificate expiration date in Ruby

To check SSL certificate expiration date in Ruby, read not_after from an OpenSSL::X509::Certificate — parsed from a file, or taken from OpenSSL::SSL::SSLSocket#peer_cert after connecting. This recipe does both with the openssl library, trusts a private test root, and shows why a bare SSLContext would connect to an expired server without complaint.

Tested with openssl (Ruby gem) 4.0.2 (Ruby 4.0.7, OpenSSL 3.5.7) and OpenSSL 3.5.7 (openssl s_server as the local TLS servers).

expiry.rb — notAfter and days left for PEM or DER files
require "openssl"
def days_left(path)
cert = OpenSSL::X509::Certificate.new(File.binread(path)) # PEM or DER
left = ((cert.not_after - Time.now) / 86_400).floor # not_after is a Time in UTC
puts "#{path}: notAfter #{cert.not_after.utc.iso8601}, #{left} days left"
left
end
server.rb — the expiry of the certificate a TLS server presents
require "openssl"
require "socket"
def server_not_after(host, port = 443, ca_file = nil)
context = OpenSSL::SSL::SSLContext.new
# set_params turns on VERIFY_PEER and host name checking; ca_file: nil keeps the system roots
context.set_params(ca_file ? { ca_file: ca_file } : {})
TCPSocket.open(host, port, connect_timeout: 10) do |tcp|
tls = OpenSSL::SSL::SSLSocket.new(tcp, context)
tls.hostname = host # SNI, and the name verify_hostname checks
tls.connect
not_after = tls.peer_cert.not_after
tls.close
not_after
end
end

How it works

  1. OpenSSL::X509::Certificate.new reads PEM and DER; not_after is a Time in UTC, and subtracting Time.now gives seconds left.
  2. SSLContext#set_params sets VERIFY_PEER, host name verification and the system roots; ca_file: replaces the roots with your own CA.
  3. tls.hostname = host sends SNI and is the name verify_hostname checks during connect; peer_cert is then the verified leaf.
  4. The run prints 2035-12-31T23:59:59Z for the test leaf, from the file and from the local server.

Gotchas

  • A context from SSLContext.new alone has verify_mode 0 (VERIFY_NONE): the run connects to the expired server without an error, and only verify_result (20, no trusted issuer) shows that nothing was trusted. Call set_params.
  • With set_params, the expired server fails with SSL_connect returned=1 errno=0 peeraddr=127.0.0.1:9443 state=error: certificate verify failed (certificate has expired) — rescue OpenSSL::SSL::SSLError and report the expiry.
  • (seconds / 86_400).floor goes negative after expiry (-568 in the run), so a days_left(...) < 30 check also catches certificates that have already expired.

Related recipes