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" leftend 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 endend How it works
OpenSSL::X509::Certificate.newreads PEM and DER;not_afteris aTimein UTC, and subtractingTime.nowgives seconds left.SSLContext#set_paramssetsVERIFY_PEER, host name verification and the system roots;ca_file:replaces the roots with your own CA.tls.hostname = hostsends SNI and is the nameverify_hostnamechecks duringconnect;peer_certis then the verified leaf.- The run prints
2035-12-31T23:59:59Zfor the test leaf, from the file and from the local server.
Gotchas
- A context from
SSLContext.newalone hasverify_mode0 (VERIFY_NONE): the run connects to the expired server without an error, and onlyverify_result(20, no trusted issuer) shows that nothing was trusted. Callset_params. - With
set_params, the expired server fails withSSL_connect returned=1 errno=0 peeraddr=127.0.0.1:9443 state=error: certificate verify failed (certificate has expired)— rescueOpenSSL::SSL::SSLErrorand report the expiry. (seconds / 86_400).floorgoes negative after expiry (-568in the run), so adays_left(...) < 30check also catches certificates that have already expired.