]> git.openstreetmap.org Git - chef.git/blobdiff - cookbooks/letsencrypt/files/default/bin/check-certificate
Tell certbot to prefer the legacy "DST Root CA X3" chain
[chef.git] / cookbooks / letsencrypt / files / default / bin / check-certificate
index d292b562250e476f6218b049ebb7b3ab5c2db5f3..f24681589e13687c2c16e0fc79804ec9e7aa6391 100755 (executable)
@@ -1,36 +1,94 @@
 #!/usr/bin/ruby
 
+require "socket"
+require "openssl"
 require "net/http"
 
-domain = ARGV.first
+host = ARGV.shift
+address = ARGV.shift
+domains = ARGV
+
+context = OpenSSL::SSL::SSLContext.new
+context.verify_mode = OpenSSL::SSL::VERIFY_NONE
 
 begin
-  connection = Net::HTTP.start(domain, :use_ssl => true)
-  certificate = connection.peer_cert
+  socket = TCPSocket.new(address, 443)
+
+  ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
+  ssl.sync_close = true
+  ssl.hostname = domains.first
+  ssl.connect
+rescue StandardError => e
+  puts "Error connecting to #{host}: #{e.message}"
+end
+
+if ssl
+  certificate = ssl.peer_cert
+  chain = ssl.peer_cert_chain.drop(1)
+  issuer = chain.first
 
   if Time.now < certificate.not_before
-    puts "Certificate #{domain} not valid until #{certificate.not_before}"
-  elsif certificate.not_after - Time.now < 14 * 86400
-    puts "Certificate #{domain} expires at #{certificate.not_after}"
-  else
-    subject_alt_name = certificate.extensions.find { |e| e.oid == "subjectAltName" }
+    puts "Certificate #{domains.first} on #{host} not valid until #{certificate.not_before}"
+  elsif certificate.not_after - Time.now < 21 * 86400
+    puts "Certificate #{domains.first} on #{host} expires at #{certificate.not_after}"
+  end
 
-    if subject_alt_name.nil?
-      puts "Certificate #{domain} has no subjectAltName"
-    else
-      alt_names = subject_alt_name.value.split(/\s*,\s*/).sort
+  digest = OpenSSL::Digest::SHA1.new
+  certificate_id = OpenSSL::OCSP::CertificateId.new(certificate, issuer, digest)
+  ocsp_request = OpenSSL::OCSP::Request.new.add_certid(certificate_id)
 
-      ARGV.sort.each do |expected|
-        puts "Certificate #{domain} is missing subjectAltName #{expected}" unless alt_names.shift == "DNS:#{expected}"
-      end
+  authority_info_access = certificate.extensions.find { |ext| ext.oid == "authorityInfoAccess" }
+  ocsp = authority_info_access.value.split("\n").find { |desc| desc.start_with?("OCSP") }
+  ocsp_uri = URI(ocsp.sub(/^.* URI:/, ""))
+
+  http_response = Net::HTTP.start(ocsp_uri.hostname, ocsp_uri.port) do |http|
+    path = ocsp_uri.path
+    path = "/" if path.empty?
+    http.post(path, ocsp_request.to_der, "Content-Type" => "application/ocsp-request")
+  end
+
+  basic_response = OpenSSL::OCSP::Response.new(http_response.body).basic
+
+  store = OpenSSL::X509::Store.new
+  store.set_default_paths
+
+  unless basic_response.verify(chain, store)
+    raise "OCSP response is not signed by a trusted certificate"
+  end
 
-      alt_names.each do |name|
-        puts "Certificate #{domain} has unexpected subjectAltName #{name}"
+  single_response = basic_response.find_response(certificate_id)
+
+  unless single_response
+    raise "OCSP response does not have the status for the certificate"
+  end
+
+  unless single_response.check_validity
+    raise "OCSP response is not valid"
+  end
+
+  if single_response.cert_status == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
+    puts "Certificate #{domains.first} on #{host} has been revoked"
+  end
+
+  subject_alt_name = certificate.extensions.find { |ext| ext.oid == "subjectAltName" }
+
+  if subject_alt_name.nil?
+    puts "Certificate #{domains.first} on #{host} has no subjectAltName"
+  else
+    alt_names = subject_alt_name.value.split(/\s*,\s*/).map { |n| n.sub(/^DNS:/, "") }
+
+    domains.each do |domain|
+      if alt_names.include?(domain)
+        alt_names.delete(domain)
+      else
+        puts "Certificate #{domains.first} on #{host} is missing subjectAltName #{domain}"
       end
     end
+
+    alt_names.each do |name|
+      puts "Certificate #{domains.first} on #{host} has unexpected subjectAltName #{name}"
+    end
   end
 
-  connection.finish
-rescue StandardError => error
-  puts "Error connecting to #{domain}: #{error.message}"
+  ssl.close
 end