#!/usr/bin/ruby # require 'net/http' # require 'json' require 'yaml' require 'openssl' require 'acme-client' require 'dnsruby' require 'time' def read_config( path = 'config.yaml' ) p "Reading config from #{path}" begin config = YAML.load_file( path ) rescue Psych::SyntaxError $stderr.puts "Parsing configfile failed: " + $!.to_s raise rescue Errno::ENOENT $stderr.puts "IO failed: " + $!.to_s end return config end def read_account_key( path = 'pkey.pem' ) p "Reading account key from #{path}" if File.readable?( path ) p "File #{path} is readable, trying to parse" privatekey_string = File.read( path ) private_key = OpenSSL::PKey::EC.new( privatekey_string ) else if File.exists?( path ) raise( "The file #{path} exists but is not readable. Make it readable or specify different path" ) else p "File #{path} does not exist, trying to create" private_key = OpenSSL::PKey::EC.generate( "prime256v1" ) File.write( path, private_key.private_to_pem ) end end return private_key end def deploy_dns01_challenge_token( domain, token, nameserver, config ) p "Creating DNS UPDATE packet" update = Dnsruby::Update.new( domain ) # TODO: delete challenge token record after validation update.delete( "_acme-challenge." + domain , 'TXT' ) update.add( "_acme-challenge." + domain, 'TXT', 10, token ) p "Creating object for contacting nameserver" res = Dnsruby::Resolver.new( nameserver ) res.dnssec = false p "Looking up TSIG parameters" tsig_name = config['domains'][domain]['tsig_key'] tsig_key = config['tsig_keys'][tsig_name]['key'] tsig_alg = config['tsig_keys'][tsig_name]['algorithm'] p "Creating TSIG object" tsig = Dnsruby::RR.create({ :name => tsig_name, :type => 'TSIG', :key => tsig_key, :algorithm => tsig_alg, }) p "Signing DNS UPDATE packet with TSIG object" tsig.apply(update) p "Sending UPDATE to nameserver" response = res.send_message(update) end def wait_for_challenge_propagation( domain, challenge ) p "Creating recursor object for checking challenge propagation" rec = Dnsruby::Recursor.new p "Getting NS records for #{domain}" domain_auth_ns = rec.query_no_validation_or_recursion( domain, "NS" ) p "Checking challenge status on all NS" domain_auth_ns.answer.each do |ns| nameserver = ns.rdata.to_s p "Creating resolver object for checking propagation on #{nameserver}" res = Dnsruby::Resolver.new( nameserver ) res.dnssec = false res.do_caching = false begin p "Querying ACME challenge record" result = res.query_no_validation_or_recursion( "_acme-challenge." + domain, "TXT" ) p result propagated = result.answer.any? do |answer| p "Checking response" p answer p answer.rdata[0] p "against challenge string" p challenge.record_content answer.rdata[0] == challenge.record_content end unless propagated p "Sleeping before checking again" sleep(1) end end until propagated end end def wait_for_challenge_validation( challenge ) p "Requesting validation of challenge" challenge.request_validation while challenge.status == 'pending' p "Sleeping because challenge validation is pending" sleep(1) p "Checking again" challenge.reload end end def get_cert_key( domain ) path = "./domains/#{domain}/" key_file = path + "current.key" p "Reading cert key from #{key_file}" if File.readable?( key_file ) p "Cert key is readable, trying to read" pkey_file = File.new( key_file ) privatekey_string = pkey_file.read domain_key = OpenSSL::PKey::EC.new( privatekey_string ) else p "Cert key is not readable, trying to create one" pkey_file = File.new( path + Time.now.to_i.to_s + ".key", 'w' ) domain_key = OpenSSL::PKey::EC.generate( "prime256v1" ) pkey_pem = domain_key.private_to_pem pkey_file.write( pkey_pem ) File.symlink( File.basename( pkey_file ), File.dirname( pkey_file ) + "/current.key" ) end return domain_key end def get_cert( order, domains, domain_key ) path = "./domains/#{domains[0]}/" crt_file = path + "cert.pem" p "Creating CSR object" csr = Acme::Client::CertificateRequest.new(private_key: domain_key, names: domains, subject: { common_name: "#{domains[0]}" }) p "Finalize cert order" order.finalize(csr: csr) while order.status == 'processing' p "Sleep while order is processing" sleep(1) p "Rechecking order status" order.reload end cert = order.certificate p "Writing cert" cert_file = File.new( path + Time.now.to_i.to_s + ".crt", 'w' ) cert_file.write( cert ) if File.symlink?( File.dirname( cert_file ) + "/current.crt" ) then File.unlink( File.dirname( cert_file ) + "/current.crt" ) File.symlink( File.basename( cert_file ), File.dirname( cert_file ) + "/current.crt" ) else raise Exception end return cert end config = read_config # iterate over configured certs # TODO: make this one thread per cert config['certs'].each_pair do |cert_name, cert_opts| p "Finding CA to use for cert #{cert_name}" acme_directory_url = config['CAs'][cert_opts['ca']['name']]['directory_url'] p "Finding account to use for cert #{cert_name} from CA #{cert_opts['ca']['name']}" account = config['ca_accounts'][cert_opts['ca']['account']] email = account['email'] private_key = read_account_key( account['keyfile'] ) p "Creating client object for communication with CA" client = Acme::Client.new( private_key: private_key, directory: acme_directory_url ) client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) p "Creating order object for cert #{cert_name}" order = client.new_order(identifiers: cert_opts['domain_names'] ) if order.status != "ready" then p "Order is not ready, we need to authorize first" p "Iterating over required authorizations" order.authorizations.each do |auth| p "Processing authorization for #{auth.domain}" p "Finding challenge type for #{auth.domain}" p config['domains'][auth.domain]['challenge'] challenge = auth.dns01 deploy_dns01_challenge_token( auth.domain, challenge.record_content, config['domains'][auth.domain]['primary_ns'], config ) wait_for_challenge_propagation( auth.domain, challenge ) wait_for_challenge_validation( challenge ) end # deploy_dns01_challenge_token( cert_opts['domain_names'][0], challenge.record_content, cert_opts['challenge']['primary_ns'], config ) else p "Order is ready, we don’t need to authorize" end domain_key = get_cert_key( cert_opts['domain_names'][0] ) get_cert( order, cert_opts['domain_names'], domain_key ) end