X-Git-Url: https://git.openstreetmap.org./chef.git/blobdiff_plain/51a7079eab309ad3c6cf0b05623680076e56687c..135b0061b49861418cfb01083c1fe42e9f11c1db:/cookbooks/letsencrypt/files/default/bin/check-certificate diff --git a/cookbooks/letsencrypt/files/default/bin/check-certificate b/cookbooks/letsencrypt/files/default/bin/check-certificate index d292b5622..f24681589 100755 --- a/cookbooks/letsencrypt/files/default/bin/check-certificate +++ b/cookbooks/letsencrypt/files/default/bin/check-certificate @@ -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