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
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 )
30 if File.exists?( path )
31 raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
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 )
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 )
50 if File.exists?( path )
51 raise( "The file #{path} exists but is not readable. Make it readable or specify different path" )
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" )
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 )
70 p "Creating object for contacting nameserver"
71 res = Dnsruby::Resolver.new( nameserver )
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']
79 p "Creating TSIG object"
80 tsig = Dnsruby::RR.create({
84 :algorithm => tsig_alg,
87 p "Signing DNS UPDATE packet with TSIG object"
90 p "Sending UPDATE to nameserver"
91 response = res.send_message(update)
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" )
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 )
106 res.do_caching = false
108 p "Querying ACME challenge record"
109 result = res.query_no_validation_or_recursion( "_acme-challenge." + domain, "TXT" )
111 propagated = result.answer.any? do |answer|
112 answer.rdata[0] == challenge.record_content
115 p "Not yet propagated, sleeping before checking again"
122 def wait_for_challenge_validation( challenge )
123 p "Requesting validation of challenge"
124 challenge.request_validation
126 while challenge.status == 'pending'
127 p "Sleeping because challenge validation is pending"
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"
144 p "Rechecking order status"
147 cert = order.certificate
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" )
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']
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']
174 private_key = read_account_key( account['keyfile'] )
176 p "Creating client object for communication with CA"
177 client = Acme::Client.new( private_key: private_key, directory: acme_directory_url )
179 client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
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"
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 )
196 # deploy_dns01_challenge_token( cert_opts['domain_names'][0], challenge.record_content, cert_opts['challenge']['primary_ns'], config )
199 p "Order is ready, we don’t need to authorize"
201 domain_key = read_cert_key( cert_opts['domain_names'][0] )
203 get_cert( order, cert_opts['domain_names'], domain_key )