10 def read_config( path = 'config.yaml' )
11 p "Reading config from #{path}"
13 config = YAML.load_file( path )
14 rescue Psych::SyntaxError
15 $stderr.puts "Parsing configfile failed: " + $!.to_s
18 $stderr.puts "IO failed: " + $!.to_s
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 )
31 if File.exists?( path )
32 raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
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 )
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 )
51 if File.exists?( path )
52 raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
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" )
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 )
71 p 'Creating object for contacting nameserver'
72 res = Dnsruby::Resolver.new( nameserver )
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']
80 p 'Creating TSIG object'
81 tsig = Dnsruby::RR.create(
90 p 'Signing DNS UPDATE packet with TSIG object'
93 p 'Sending UPDATE to nameserver'
94 response = res.send_message(update)
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" )
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 )
109 res.do_caching = false
111 p 'Querying ACME challenge record'
112 result = res.query_no_validation_or_recursion( "_acme-challenge." + domain, "TXT" )
114 propagated = result.answer.any? do |answer|
115 answer.rdata[0] == challenge.record_content
118 p 'Not yet propagated, sleeping before checking again'
125 def wait_for_challenge_validation( challenge )
126 p 'Requesting validation of challenge'
127 challenge.request_validation
129 while challenge.status == 'pending'
130 p 'Sleeping because challenge validation is pending'
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,
144 subject: { common_name: "#{domains[0]}" }
146 p 'Finalize cert order'
147 order.finalize(csr: csr)
148 while order.status == 'processing'
149 p 'Sleep while order is processing'
151 p 'Rechecking order status'
154 cert = order.certificate
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" )
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']
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']
181 private_key = read_account_key( account['keyfile'] )
183 p 'Creating client object for communication with CA'
184 client = Acme::Client.new( private_key: private_key, directory: acme_directory_url )
186 client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
188 p "Creating order object for cert #{cert_name}"
189 order = client.new_order(identifiers: cert_opts['domain_names'] )
192 if order.status != 'ready'
193 p 'Order is not ready, we need to authorize first'
195 p 'Iterating over required authorizations'
196 order.authorizations.each do |auth|
197 p "Processing authorization for #{auth.domain}"
198 p "Finding challenge type for #{auth.domain}"
199 if auth.status == 'valid'
200 p "Authorization for #{auth.domain} is still valid, skipping"
204 challenge = auth.dns01
205 deploy_dns01_challenge_token( auth.domain, challenge, config['domains'][auth.domain]['primary_ns'], config )
206 wait_for_challenge_propagation( auth.domain, challenge )
207 wait_for_challenge_validation( challenge )
210 p 'Order is ready, we don’t need to authorize'
212 domain_key = read_cert_key( cert_opts['domain_names'][0] )
214 get_cert( order, cert_opts['domain_names'], domain_key )