HTTPS 域名证书检查小功能

November 26, 2018 21:05


获取网站证书

首先加载 socket 与 openssl

require 'socket'
require 'openssl'

然后我们通过 HTTPS 443 端口获取证书

def get_cert(host)
  tcp_client = Socket.tcp(host, 443, connect_timeout: $timeout)
  ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_client)
  ssl_client.hostname = host


  begin
    ssl_client.connect_nonblock
  rescue IO::WaitReadable
    retry if IO.select([ssl_client], nil, nil, $timeout)
  rescue IO::WaitWritable
    retry if IO.select(nil, [ssl_client], nil, $timeout)
  end


  ssl_client.close
  tcp_client.close
  [ssl_client.peer_cert, ssl_client.peer_cert_chain]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ETIMEDOUT
  nil
end

验证

因为根证书的公钥已经存放到我们电脑上了,所以我们直接创建一个 cert store,通过当前主机上的证书来验证一个证书是否有效

当然,cert_store 也可以加入自定义的根证书来进行验证

$cert_store = OpenSSL::X509::Store.new
$cert_store.set_default_paths

根证书只能验证自己签名的证书,如果有中间证书的话,那么就需要一层层的验证,一直到最终实体证书。

还是以 Let's Encrypt 为例,先通过我们主机上已有的 DST Root CA X3 来验证 Let's Encrypt 中间证书的公钥是有效的,然后再通过 Let's Encrypt 这个中间证书的公钥来验证我们最终的实体证书是有效的。如果都正确,我们就可以认为这个最终证书是有效的。如果网站没有给出中间证书,或是中间证书有问题的话。那么就无法确实证书的有效性了。配置 HTTPS 需要注意一下这点

def show_cert(host, cert, cert_chain)
  # C Country Name
  # CN Common Name
  # DC Domain Component
  # O Organization Name
  # OU Organizational Unit Name
  # ST State or Province Name
  issuer = cert.issuer.to_a.find { |name, data, type| name == 'O' }[1]
  expired_at = cert.not_after

  status_str = expired_at <= Time.now ? '[expired]' : '[ok]'

  # DNS 中放了所有域名,所以我们只要检查一下里面是不是有当前的 host,就知道证书是不是和域名匹配了
  dns = cert.extensions.find { |e| e.value =~ /^DNS/ }.value.split(',').map { |d| d.split(':').last }
  status_str = '[not-match]' if dns.all? { |d| !(Regexp.new('\A' + d.gsub('*', '.+') + '\z') =~ host) }

  # 检查 cert 时需要传入中间证书 chain ,如果有的话。因为通常我们的证书都是由中间证书生成的
  status_str = '[err-chain]' unless $cert_store.verify(cert, cert_chain)

  # 证书的签发及过期时间
  times = [cert.not_before, cert.not_after].map { |t| t.strftime("%F") }.join(' - ')

  puts $format % [status_str, host, issuer, times]
end

使用

最后,我们可以使用上面的代码来检查自己网站的证书是否配置正确了

%w{
  xjz.pw
  aliyun.com
  www.qq.com
}.each do |host|
  cert, cert_chain = get_cert(host)
  if cert
    show_cert(host, cert, cert_chain)
  else
    puts $format % ['[failed]', host, nil, nil]
  end
end

运行后将会输出

[ok] xjz.pw Let's Encrypt 2018-11-20 - 2019-02-18
[ok] aliyun.com GlobalSign nv-sa 2018-08-13 - 2019-03-29
[ok] www.qq.com GlobalSign nv-sa 2018-11-12 - 2019-10-12

Refs

Comments: