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