]> git.openstreetmap.org Git - chef.git/blob - cookbooks/letsencrypt/templates/default/check-certificate.erb
letsencrypt: Add ECDSA key type check to check-certificate
[chef.git] / cookbooks / letsencrypt / templates / default / check-certificate.erb
1 #!<%= node[:ruby][:interpreter] %>
2
3 require "socket"
4 require "openssl"
5 require "net/http"
6
7 host = ARGV.shift
8 address = ARGV.shift
9 domains = ARGV
10
11 context = OpenSSL::SSL::SSLContext.new
12 context.verify_mode = OpenSSL::SSL::VERIFY_NONE
13
14 begin
15   socket = TCPSocket.new(address, 443)
16
17   ssl = OpenSSL::SSL::SSLSocket.new(socket, context)
18   ssl.sync_close = true
19   ssl.hostname = domains.first
20   ssl.connect
21 rescue StandardError => e
22   puts "Error connecting to #{host}: #{e.message}"
23 end
24
25 if ssl
26   certificate = ssl.peer_cert
27   chain = ssl.peer_cert_chain.drop(1)
28   issuer = chain.first
29
30   if Time.now < certificate.not_before
31     puts "Certificate #{domains.first} on #{host} not valid until #{certificate.not_before}"
32   elsif certificate.not_after - Time.now < 21 * 86400
33     puts "Certificate #{domains.first} on #{host} expires at #{certificate.not_after}"
34   end
35
36   unless certificate.public_key.is_a?(OpenSSL::PKey::EC)
37     puts "Certificate #{domains.first} on #{host} does not use ECDSA key type"
38   end
39
40   digest = OpenSSL::Digest::SHA1.new
41   certificate_id = OpenSSL::OCSP::CertificateId.new(certificate, issuer, digest)
42   ocsp_request = OpenSSL::OCSP::Request.new.add_certid(certificate_id)
43
44   authority_info_access = certificate.extensions.find { |ext| ext.oid == "authorityInfoAccess" }
45   ocsp = authority_info_access.value.split("\n").find { |desc| desc.start_with?("OCSP") }
46   ocsp_uri = URI(ocsp.sub(/^.* URI:/, ""))
47
48   http_response = Net::HTTP.start(ocsp_uri.hostname, ocsp_uri.port) do |http|
49     path = ocsp_uri.path
50     path = "/" if path.empty?
51     http.post(path, ocsp_request.to_der, "Content-Type" => "application/ocsp-request")
52   end
53
54   basic_response = OpenSSL::OCSP::Response.new(http_response.body).basic
55
56   store = OpenSSL::X509::Store.new
57   store.set_default_paths
58
59   unless basic_response.verify(chain, store)
60     raise "OCSP response is not signed by a trusted certificate"
61   end
62
63   single_response = basic_response.find_response(certificate_id)
64
65   unless single_response
66     raise "OCSP response does not have the status for the certificate"
67   end
68
69   unless single_response.check_validity
70     raise "OCSP response is not valid"
71   end
72
73   if single_response.cert_status == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
74     puts "Certificate #{domains.first} on #{host} has been revoked"
75   end
76
77   subject_alt_name = certificate.extensions.find { |ext| ext.oid == "subjectAltName" }
78
79   if subject_alt_name.nil?
80     puts "Certificate #{domains.first} on #{host} has no subjectAltName"
81   else
82     alt_names = subject_alt_name.value.split(/\s*,\s*/).map { |n| n.sub(/^DNS:/, "") }
83
84     domains.each do |domain|
85       if alt_names.include?(domain)
86         alt_names.delete(domain)
87       else
88         puts "Certificate #{domains.first} on #{host} is missing subjectAltName #{domain}"
89       end
90     end
91
92     alt_names.each do |name|
93       puts "Certificate #{domains.first} on #{host} has unexpected subjectAltName #{name}"
94     end
95   end
96
97   ssl.close
98 end