12 def read_config( path = 'config.yaml' )
13 p "Reading config from #{path}"
15 config = YAML.load_file( path )
16 rescue Psych::SyntaxError
17 $stderr.puts "Parsing configfile failed: " + $!.to_s
19 rescue Errno::ENOENT => ex
20 $stderr.puts "IO failed: " + $!.to_s
26 def read_account_key( path = 'pkey.pem' )
27 p "Reading account key from #{path}"
28 if File.readable?( path )
29 p "Key exists, trying to parse"
30 pkey_file = File.new( path )
31 privatekey_string = pkey_file.read
32 private_key = OpenSSL::PKey::EC.new( privatekey_string )
34 p "Key does not exist, trying to create"
35 pkey_file = File.new( path, 'w' )
36 private_key = OpenSSL::PKey::EC.generate( "prime256v1" )
37 pkey_pem = private_key.private_to_pem
38 pkey_file.write( pkey_pem )
42 def deploy_dns01_challenge_token( domain, token, nameserver, config )
43 p "Creating DNS UPDATE packet"
44 update = Dnsruby::Update.new( domain )
45 # TODO: delete challenge token record after validation
46 update.delete( "_acme-challenge." + domain , 'TXT' )
47 update.add( "_acme-challenge." + domain, 'TXT', 10, token )
49 p "Creating object for contacting nameserver"
50 res = Dnsruby::Resolver.new( nameserver )
53 p "Looking up TSIG parameters"
54 tsig_name = config['domains'][domain]['tsig_key']
55 tsig_key = config['tsig_keys'][tsig_name]['key']
56 tsig_alg = config['tsig_keys'][tsig_name]['algorithm']
57 p "Creating TSIG object"
58 tsig = Dnsruby::RR.create({
62 :algorithm => tsig_alg,
65 p "Signing DNS UPDATE packet with TSIG object"
68 p "Sending UPDATE to nameserver"
69 response = res.send_message(update)
72 def wait_for_challenge_propagation( domain, challenge )
73 p "Creating recursor object for checking challenge propagation"
74 rec = Dnsruby::Recursor.new
75 p "Getting NS records for #{domain}"
76 domain_auth_ns = rec.query_no_validation_or_recursion( domain, "NS" )
78 p "Checking challenge status on all NS"
79 domain_auth_ns.answer.each do |ns|
80 nameserver = ns.rdata.to_s
81 p "Creating resolver object for checking propagation on #{nameserver}"
82 res = Dnsruby::Resolver.new( nameserver )
84 res.do_caching = false
86 p "Querying ACME challenge record"
87 result = res.query_no_validation_or_recursion( "_acme-challenge." + domain, "TXT" )
89 propagated = result.answer.any? do |answer|
93 p "against challenge string"
94 p challenge.record_content
95 answer.rdata[0] == challenge.record_content
98 p "Sleeping before checking again"
105 def wait_for_challenge_validation( challenge )
106 p "Requesting validation of challenge"
107 challenge.request_validation
109 while challenge.status == 'pending'
110 p "Sleeping because challenge validation is pending"
117 def get_cert_key( domain )
118 path = "./domains/#{domain}/"
119 key_file = path + "current.key"
120 p "Reading cert key from #{key_file}"
121 if File.readable?( key_file )
122 p "Cert key is readable, trying to read"
123 pkey_file = File.new( key_file )
124 privatekey_string = pkey_file.read
125 domain_key = OpenSSL::PKey::EC.new( privatekey_string )
127 p "Cert key is not readable, trying to create one"
128 pkey_file = File.new( path + Time.now.to_i.to_s + ".key", 'w' )
129 domain_key = OpenSSL::PKey::EC.generate( "prime256v1" )
130 pkey_pem = domain_key.private_to_pem
131 pkey_file.write( pkey_pem )
132 File.symlink( File.basename( pkey_file ), File.dirname( pkey_file ) + "/current.key" )
137 def get_cert( order, domains, domain_key )
138 path = "./domains/#{domains[0]}/"
139 crt_file = path + "cert.pem"
140 p "Creating CSR object"
141 csr = Acme::Client::CertificateRequest.new(private_key: domain_key, names: domains, subject: { common_name: "#{domains[0]}" })
142 p "Finalize cert order"
143 order.finalize(csr: csr)
144 while order.status == 'processing'
145 p "Sleep while order is processing"
147 p "Rechecking order status"
150 cert = order.certificate
153 cert_file = File.new( path + Time.now.to_i.to_s + ".crt", 'w' )
154 cert_file.write( cert )
155 if File.symlink?( File.dirname( cert_file ) + "/current.crt" ) then
156 File.unlink( File.dirname( cert_file ) + "/current.crt" )
157 File.symlink( File.basename( cert_file ), File.dirname( cert_file ) + "/current.crt" )
167 # iterate over configured certs
168 # TODO: make this one thread per cert
169 config['certs'].each_pair do |cert_name, cert_opts|
170 p "Finding CA to use for cert #{cert_name}"
171 acme_directory_url = config['CAs'][cert_opts['ca']['name']]['directory_url']
173 p "Finding account to use for cert #{cert_name} from CA #{cert_opts['ca']['name']}"
174 account = config['ca_accounts'][cert_opts['ca']['account']]
175 email = account['email']
177 private_key = read_account_key( account['keyfile'] )
179 p "Creating client object for communication with CA"
180 client = Acme::Client.new( private_key: private_key, directory: acme_directory_url )
182 client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true)
184 p "Creating order object for cert #{cert_name}"
185 order = client.new_order(identifiers: cert_opts['domain_names'] )
186 if order.status != "ready" then
187 p "Order is not ready, we need to authorize first"
189 p "Iterating over required authorizations"
190 order.authorizations.each do |auth|
191 p "Processing authorization for #{auth.domain}"
192 p "Finding challenge type for #{auth.domain}"
193 p config['domains'][auth.domain]['challenge']
194 challenge = auth.dns01
195 deploy_dns01_challenge_token( auth.domain, challenge.record_content, config['domains'][auth.domain]['primary_ns'], config )
196 wait_for_challenge_propagation( auth.domain, challenge )
197 wait_for_challenge_validation( challenge )
200 # deploy_dns01_challenge_token( cert_opts['domain_names'][0], challenge.record_content, cert_opts['challenge']['primary_ns'], config )
203 p "Order is ready, we don’t need to authorize"
205 domain_key = get_cert_key( cert_opts['domain_names'][0] )
207 get_cert( order, cert_opts['domain_names'], domain_key )