]> git.netwichtig.de Git - user/henk/code/ruby/macir.git/blob - macir.rb
tidying
[user/henk/code/ruby/macir.git] / macir.rb
1 #!/usr/bin/ruby
2
3 require 'yaml'
4 require 'openssl'
5 require 'acme-client'
6 require 'dnsruby'
7 require 'time'
8
9
10 def read_config( path = 'config.yaml' )
11   p "Reading config from #{path}"
12   begin
13     config = YAML.load_file( path )
14   rescue Psych::SyntaxError
15     $stderr.puts "Parsing configfile failed: " + $!.to_s
16     raise
17   rescue Errno::ENOENT
18     $stderr.puts "IO failed: " + $!.to_s
19   end
20   return config
21 end
22
23 def read_account_key( path = 'pkey.pem' )
24   p "Reading account key from #{path}"
25   if File.readable?( path )
26     p "File #{path} is readable, trying to parse"
27     privatekey_string = File.read( path )
28     private_key = OpenSSL::PKey::EC.new( privatekey_string )
29   else
30     if File.exists?( path )
31       raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
32     else
33       p "File #{path} does not exist, trying to create"
34       private_key = OpenSSL::PKey::EC.generate( "prime256v1" )
35       File.write( path, private_key.private_to_pem )
36     end
37   end
38   return private_key
39 end
40
41 def read_cert_key( domain )
42   folder = "./certs/#{domain}/"
43   path = folder + "current.key"
44   p "Reading cert key from #{path}"
45   if File.readable?( path )
46     p "File #{path} is readable, trying to parse"
47     privatekey_string = File.read( path )
48     private_key = OpenSSL::PKey::EC.new( privatekey_string )
49   else
50     if File.exists?( path )
51       raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
52     else
53       p "File #{path} does not exist, trying to create"
54       private_key = OpenSSL::PKey::EC.generate( "prime256v1" )
55       pkey_file = File.new( folder + Time.now.to_i.to_s + ".key", 'w' )
56       pkey_file.write( private_key.private_to_pem )
57       File.symlink( File.basename( pkey_file ), File.dirname( pkey_file ) + "/current.key" )
58     end
59   end
60   return private_key
61 end
62
63 def deploy_dns01_challenge_token( domain, challenge, nameserver, config )
64   p "Creating DNS UPDATE packet"
65   update = Dnsruby::Update.new( domain )
66   # TODO: delete challenge token record after validation
67   update.delete( challenge.record_name + "." + domain, challenge.record_type )
68   update.add( challenge.record_name + "." + domain, challenge.record_type, 10, challenge.record_content )
69
70   p "Creating object for contacting nameserver"
71   res = Dnsruby::Resolver.new( nameserver )
72   res.dnssec = false
73
74   p "Looking up TSIG parameters"
75   tsig_name = config['domains'][domain]['tsig_key']
76   tsig_key = config['tsig_keys'][tsig_name]['key']
77   tsig_alg = config['tsig_keys'][tsig_name]['algorithm']
78
79   p "Creating TSIG object"
80   tsig = Dnsruby::RR.create({
81     :name      => tsig_name,
82     :type      => 'TSIG',
83     :key       => tsig_key,
84     :algorithm => tsig_alg,
85   })
86
87   p "Signing DNS UPDATE packet with TSIG object"
88   tsig.apply(update)
89
90   p "Sending UPDATE to nameserver"
91   response = res.send_message(update)
92 end
93
94 def wait_for_challenge_propagation( domain, challenge )
95   p "Creating recursor object for checking challenge propagation"
96   rec = Dnsruby::Recursor.new
97   p "Getting NS records for #{domain}"
98   domain_auth_ns = rec.query_no_validation_or_recursion( domain, "NS" )
99
100   p "Checking challenge status on all NS"
101   domain_auth_ns.answer.each do |ns|
102     nameserver = ns.rdata.to_s
103     p "Creating resolver object for checking propagation on #{nameserver}"
104     res = Dnsruby::Resolver.new( nameserver )
105     res.dnssec = false
106     res.do_caching = false
107     begin
108       p "Querying ACME challenge record"
109       result = res.query_no_validation_or_recursion( "_acme-challenge." + domain, "TXT" )
110       p result
111       propagated = result.answer.any? do |answer|
112         answer.rdata[0] == challenge.record_content
113       end
114       unless propagated
115         p "Not yet propagated, sleeping before checking again"
116         sleep(1)
117       end
118     end until propagated
119   end
120 end
121
122 def wait_for_challenge_validation( challenge )
123   p "Requesting validation of challenge"
124   challenge.request_validation
125
126   while challenge.status == 'pending'
127     p "Sleeping because challenge validation is pending"
128     sleep(1)
129     p "Checking again"
130     challenge.reload
131   end
132 end
133
134 def get_cert( order, domains, domain_key )
135   path = "./certs/#{domains[0]}/"
136   crt_file = path + "cert.pem"
137   p "Creating CSR object"
138   csr = Acme::Client::CertificateRequest.new(private_key: domain_key, names: domains, subject: { common_name: "#{domains[0]}" })
139   p "Finalize cert order"
140   order.finalize(csr: csr)
141   while order.status == 'processing'
142     p "Sleep while order is processing"
143     sleep(1)
144     p "Rechecking order status"
145     order.reload
146   end
147   cert = order.certificate
148
149   p "Writing cert"
150   cert_file = File.new( path + Time.now.to_i.to_s + ".crt", 'w' )
151   cert_file.write( cert )
152   if File.symlink?( File.dirname( cert_file ) + "/current.crt" ) then
153     File.unlink( File.dirname( cert_file ) + "/current.crt" )
154     File.symlink( File.basename( cert_file ), File.dirname( cert_file ) + "/current.crt" )
155   else
156     raise Exception
157   end
158   return cert
159 end
160
161
162 config = read_config
163
164 # iterate over configured certs
165 # TODO: make this one thread per cert
166 config['certs'].each_pair do |cert_name, cert_opts|
167   p "Finding CA to use for cert #{cert_name}"
168   acme_directory_url = config['CAs'][cert_opts['ca']['name']]['directory_url']
169
170   p "Finding account to use for cert #{cert_name} from CA #{cert_opts['ca']['name']}"
171   account = config['ca_accounts'][cert_opts['ca']['account']]
172   email = account['email']
173
174   private_key = read_account_key( account['keyfile'] )
175
176   p "Creating client object for communication with CA"
177   client = Acme::Client.new( private_key: private_key, directory: acme_directory_url )
178
179   client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
180
181   p "Creating order object for cert #{cert_name}"
182   order = client.new_order(identifiers: cert_opts['domain_names'] )
183   if order.status != "ready" then
184     p "Order is not ready, we need to authorize first"
185
186     p "Iterating over required authorizations"
187     order.authorizations.each do |auth|
188       p "Processing authorization for #{auth.domain}"
189       p "Finding challenge type for #{auth.domain}"
190       challenge = auth.dns01
191       deploy_dns01_challenge_token( auth.domain, challenge, config['domains'][auth.domain]['primary_ns'], config )
192       wait_for_challenge_propagation( auth.domain, challenge )
193       wait_for_challenge_validation( challenge )
194     end
195
196     # deploy_dns01_challenge_token( cert_opts['domain_names'][0], challenge.record_content, cert_opts['challenge']['primary_ns'], config )
197
198   else
199     p "Order is ready, we don’t need to authorize"
200   end
201   domain_key = read_cert_key( cert_opts['domain_names'][0] )
202
203   get_cert( order, cert_opts['domain_names'], domain_key )
204 end