From 7bd0583bfdf32938393a61176df7325732d4ed12 Mon Sep 17 00:00:00 2001 From: Hendrik Jäger Date: Fri, 9 Feb 2024 13:49:07 +0100 Subject: dirty --- macir.rb | 487 +++++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 393 insertions(+), 94 deletions(-) diff --git a/macir.rb b/macir.rb index f923839..cdb9b61 100644 --- a/macir.rb +++ b/macir.rb @@ -68,8 +68,18 @@ def read_cert_key(cert_name) return private_key end +def lookup_ns(domain) + p "Domain #{domain}: Creating recursor object for checking challenge propagation" + rec = Dnsruby::Resolver.new + p "Domain #{domain}: Getting NS records for #{domain}" + rec.query_no_validation_or_recursion(domain, 'NS') +rescue StandardError => e + warn "Domain #{domain}: NS lookup during deploy failed: #{e}" + raise +end + def lookup_soa(domain) - rec = Dnsruby::Recursor.new + rec = Dnsruby::Resolver.new p "Domain #{domain}: Getting SOA records for #{domain}" rec.query_no_validation_or_recursion(domain, 'SOA') rescue StandardError => e @@ -86,6 +96,53 @@ def find_apex_domain(domain) end end +def build_dns_update_packet(apex, authzs) + update = Dnsruby::Update.new(apex) + + authzs.each do |auth| + chal = auth.dns01 + update.delete("#{chal.record_name}.#{auth.domain}", chal.record_type) + update.add("#{chal.record_name}.#{auth.domain}", chal.record_type, 3, chal.record_content) + end + return update +end + +def build_tsig_object(apex, config) + p "Domain #{apex}: Looking up TSIG parameters" + tsig_name = config.dig('domains', apex, 'tsig_key') || config.dig('defaults', 'domains', 'tsig_key') + tsig_key = config.dig('tsig_keys', tsig_name, 'key') + tsig_alg = config.dig('tsig_keys', tsig_name, 'algorithm') + + p "Domain #{apex}: Creating TSIG object" + tsig = Dnsruby::RR.create( + { + name: tsig_name, + type: 'TSIG', + key: tsig_key, + algorithm: tsig_alg, + } + ) +end + +def deploy_dns_tokens_on_apex(apex, authzs, nameserver, config) + update_packet = build_dns_update_packet(apex, authzs) + + p "Domain #{apex}: Creating object for contacting nameserver" + res = Dnsruby::Resolver.new(nameserver) + res.dnssec = false + + tsig = build_tsig_object(apex, config) + + p "Domain #{apex}: Signing DNS UPDATE packet with TSIG object" + tsig.apply(update_packet) + + p "Domain #{apex}: Sending UPDATE to nameserver" + res.send_message(update_packet) +rescue StandardError => e + warn "Domain #{apex}: DNS Update failed: #{e}" + raise +end + def deploy_dns01_challenge_token(domain, challenge, nameserver, config) p "Domain #{domain}: Creating DNS UPDATE packet" @@ -100,20 +157,7 @@ def deploy_dns01_challenge_token(domain, challenge, nameserver, config) res = Dnsruby::Resolver.new(nameserver) res.dnssec = false - p "Domain #{domain}: Looking up TSIG parameters" - tsig_name = config.dig('domains', domain, 'tsig_key') || config.dig('defaults', 'domains', 'tsig_key') - tsig_key = config.dig('tsig_keys', tsig_name, 'key') - tsig_alg = config.dig('tsig_keys', tsig_name, 'algorithm') - - p "Domain #{domain}: Creating TSIG object" - tsig = Dnsruby::RR.create( - { - name: tsig_name, - type: 'TSIG', - key: tsig_key, - algorithm: tsig_alg, - } - ) + tsig = build_tsig_object(domain, config) p "Domain #{domain}: Signing DNS UPDATE packet with TSIG object" tsig.apply(update) @@ -126,29 +170,8 @@ rescue StandardError => e end def wait_for_challenge_propagation(domain, challenge) - rec = Dnsruby::Recursor.new - p "Domain #{domain}: Getting SOA records for #{domain}" - begin - domain_soa_resp = rec.query_no_validation_or_recursion(domain, 'SOA') - rescue StandardError => e - warn "Domain #{domain}: SOA lookup during propagation wait failed: #{e}" - raise - end - apex_domain = if domain_soa_resp.answer.empty? - domain_soa_resp.authority[0].name - else - domain_soa_resp.answer[0].name - end - - p "Domain #{domain}: Creating recursor object for checking challenge propagation" - rec = Dnsruby::Recursor.new - p "Domain #{domain}: Getting NS records for #{apex_domain}" - begin - domain_auth_ns = rec.query_no_validation_or_recursion(apex_domain, 'NS') - rescue StandardError => e - warn "Domain #{domain}: NS lookup failed: #{e}" - raise - end + apex_domain = find_apex_domain(domain) + domain_auth_ns = lookup_ns(apex_domain) p "Domain #{domain}: Checking challenge status on all NS" @@ -186,7 +209,7 @@ def wait_for_challenge_propagation(domain, challenge) end def acme_request_with_retries(retries: 5, &block) - p "Retries: #{retries}" + # p "Retries: #{retries}" block.call(self) rescue Acme::Client::Error::BadNonce raise unless retries.positive? @@ -200,11 +223,11 @@ def wait_for_challenge_validation(challenge, cert_name) acme_request_with_retries { challenge.request_validation } while challenge.status == 'pending' - p "Cert #{cert_name}: Sleeping because challenge validation is pending" + p "Cert #{cert_name}: challenge validation is pending, sleeping before checking again" sleep(0.1) - p 'Checking again' acme_request_with_retries { challenge.reload } end + # pp challenge end def get_cert(order, cert_name, domains, domain_key) @@ -217,7 +240,9 @@ def get_cert(order, cert_name, domains, domain_key) subject: { common_name: domains[0] } ) p "Cert #{cert_name}: Finalize cert order" - acme_request_with_retries { order.reload } + # pp order + # TODO: this seems unnecessary? + # acme_request_with_retries { order.reload } acme_request_with_retries { order.finalize(csr: csr) } while order.status == 'processing' p "Cert #{cert_name}: Sleep while order is processing" @@ -241,6 +266,67 @@ def get_cert(order, cert_name, domains, domain_key) return cert end +def find_ca_for_cert(cert_name, cert_opts, config) + cert_ca_account = cert_opts['ca_account'] || config.dig('defaults', 'certs', 'ca_account') + # cert_ca_name = config.dig('ca_accounts', cert_ca_account, 'ca') + # cert_ca_identity = config.dig('ca_accounts', cert_ca_account, 'identity') + # + # p "Cert #{cert_name}: Finding directory URL for CA" + # acme_directory_url = config.dig('CAs', cert_ca_name, 'directory_url') + # + # p "Cert #{cert_name}: Finding account to use for cert #{cert_name} from CA #{cert_ca_name}" + # account = config.dig('identities', cert_ca_account_name) + # email = account['email'] + { cert_name => cert_ca_account } + # { + # 'ca' => cert_ca_name, + # 'account' => cert_ca_account_name, + # }, + # } +end + +def make_client_for_ca_account(ca, id) + p "CA #{ca['name']}: Finding directory URL for CA" + acme_directory_url = ca['directory_url'] + + private_key = read_account_key(id['keyfile']) + + p "CA #{ca['name']}: Creating client object for communication with CA" + client = Acme::Client.new(private_key: private_key, directory: acme_directory_url) + + email = id['email'] + + account_object = acme_request_with_retries { client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) } + client +end + +def handle_apex_authzs(apex, authzs, config) + p "apex: #{apex}" + primary_ns = config.dig('domains', authzs[0].domain, 'primary_ns') || config.dig('defaults', 'domains', 'primary_ns') + deploy_dns_tokens_on_apex(apex, authzs, primary_ns, config) +end + + +# domain_to_apex = {} +# +# domain_apex_threads = [] +# domains_with_apex = domains.map do |domain| +# apex_thread = Thread.new(domain) do |d| +# find_apex_domain(d).to_s +# end +# domain_apex_threads << apex_thread +# domain_to_apex[domain] = apex_thread.value +# { apex_thread.value => [domain] } +# end +# domain_apex_threads.each(&:join) +# +# domains_per_apex = domains_with_apex[0].merge(*domains_with_apex[1..]) do |_key, old, new| +# old + new +# end +# +# p "domains_per_apex" +# pp domains_per_apex + config = read_config @@ -248,68 +334,281 @@ cert_dir = config.dig('global', 'cert_dir') || './certs/' ensure_cert_dir(cert_dir) -acme_threads = [] -# iterate over configured certs -# TODO: make this one thread per cert -# TODO: check all domains for apex domain, deploy challenges for one apex_domain all at once -config['certs'].each_pair do |cert_name, cert_opts| - acme_threads << Thread.new(cert_name, cert_opts) do |cert_name, cert_opts| - ensure_cert_dir(cert_dir + cert_name) +# acme_threads = [] +# # iterate over configured certs +# config['certs'].each_pair do |cert_name, cert_opts| +# acme_threads << Thread.new(cert_name, cert_opts) do |cert_name, cert_opts| +# ensure_cert_dir(cert_dir + cert_name) +# +# p "Cert #{cert_name}: Finding CA to use for cert" +# cert_ca = cert_opts['ca'] || config.dig('defaults', 'certs', 'ca') +# cert_ca_name = cert_ca['name'] +# cert_ca_account_name = cert_ca['account'] +# +# p "Cert #{cert_name}: Finding directory URL for CA" +# acme_directory_url = config.dig('CAs', cert_ca_name, 'directory_url') +# +# p "Cert #{cert_name}: Finding account to use for cert #{cert_name} from CA #{cert_ca_name}" +# account = config.dig('ca_accounts', cert_ca_account_name) +# email = account['email'] +# +# private_key = read_account_key(account['keyfile']) +# +# p "Cert #{cert_name}: Creating client object for communication with CA" +# client = Acme::Client.new(private_key: private_key, directory: acme_directory_url) +# +# acme_request_with_retries { client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) } +# +# p "Cert #{cert_name}: Creating order object for cert #{cert_name}" +# order = acme_request_with_retries { client.new_order(identifiers: cert_opts['domain_names']) } +# +# p "Cert #{cert_name}: order status" +# p order.status +# +# if order.status != 'ready' +# p "Cert #{cert_name}: Order is not ready, we need to authorize first" +# +# # TODO: collect dns modifications per primary NS, update all at once +# p "Cert #{cert_name}: Iterating over required authorizations" +# auths = acme_request_with_retries { order.authorizations } +# auths.each do |auth| +# p "Cert #{cert_name}: Processing authorization for #{auth.domain}" +# p "Cert #{cert_name}: Finding challenge type for #{auth.domain}" +# # p "Cert #{cert_name}: auth is:" +# # pp auth +# if auth.status == 'valid' +# p "Cert #{cert_name}: Authorization for #{auth.domain} is still valid, skipping" +# next +# end +# +# challenge = auth.dns01 +# primary_ns = config.dig('domains', auth.domain, 'primary_ns') || config.dig('defaults', 'domains', 'primary_ns') +# deploy_dns01_challenge_token(auth.domain, challenge, primary_ns, config) +# wait_for_challenge_propagation(auth.domain, challenge) +# wait_for_challenge_validation(challenge, cert_name) +# end +# else +# p "Cert #{cert_name}: Order is ready, we don’t need to authorize" +# end +# domain_key = read_cert_key(cert_name) +# +# get_cert(order, cert_name, cert_opts['domain_names'], domain_key) +# end +# end +# +# acme_threads.each(&:join) + + +# TODO: restructure process +# DELAY THIS FOR NOW: check all certs’ lifetimes +# DELAY THIS FOR NOW: decide which ones to renew +# collect domain names for certs to be renewed +# group domains in need of validation by apex_domain +# collect ca_accounts that need to be used +# THREAD PER ACCOUNT: create account object +# hash: ca_account => account object +# THREAD PER CA_ACCOUNT: create orders +# THREAD PER DOMAIN: check validation status and collect domains needing validation +# THREAD PER APEX DOMAIN: + # THREAD PER DOMAIN: request challenge + # deploy challenges with one DNS UPDATE per apex_domain + # THREAD PER NS: check propagation + # THREAD PER DOMAIN: + # request validation + # report back to main +# THREAD PER CERT: when all domains for any cert are validated, finalize order + + +domains = config['certs'].map { |_certname, cert_opts| cert_opts['domain_names'] }.flatten +domain_attrs = domains.to_h { |d| [d, {}] } + +domain_apex_threads = {} +domains.map do |d| + domain_apex_threads[d] = Thread.new(d) do |d| + p "finding apex for domain #{d}" + find_apex_domain(d).to_s + end +end +domain_apex_threads.each(&:join) + +domain_attrs.keys.each do |domain| + domain_attrs[domain][:apex] = domain_apex_threads[domain].value +end - p "Cert #{cert_name}: Finding CA to use for cert" - cert_ca = cert_opts['ca'] || config.dig('defaults', 'certs', 'ca') - cert_ca_name = cert_ca['name'] - cert_ca_account = cert_ca['account'] +apex_domains = domain_attrs.map do |_, v| + apex = v[:apex] + domains_under_apex = domain_attrs.filter { |d,v| v[:apex] == apex }.keys + [apex, { :domains => domains_under_apex }] +end.uniq.to_h - p "Cert #{cert_name}: Finding directory URL for CA" - acme_directory_url = config.dig('CAs', cert_ca_name, 'directory_url') - p "Cert #{cert_name}: Finding account to use for cert #{cert_name} from CA #{cert_ca_name}" - account = config.dig('ca_accounts', cert_ca_account) - email = account['email'] +apex_domain_ns_threads = {} +apex_domains.keys.map do |d| + apex_domain_ns_threads[d] = Thread.new(d) do |d| + p "finding ns for apex domain #{d}" + res = lookup_ns(d) + ns_names = res.answer.map do |answer| + answer.rdata.to_s + end + end +end +apex_domain_ns_threads.each(&:join) - private_key = read_account_key(account['keyfile']) +apex_domains.keys.each do |domain| + apex_domains[domain][:ns] = apex_domain_ns_threads[domain].value + # p "#{domain}: #{apex_domains[domain][:ns]}" +end +# p apex_domains - p "Cert #{cert_name}: Creating client object for communication with CA" - client = Acme::Client.new(private_key: private_key, directory: acme_directory_url) - acme_request_with_retries { client.new_account(contact: "mailto:#{email}", terms_of_service_agreed: true) } +certs = config['certs'] +certs.each_pair do |name, opts| + opts['ca_account'] || certs[name]['ca_account'] = config.dig('defaults', 'certs', 'ca_account') +end +# # iterate over configured certs to find CA and account to use +# cert_to_CA_account = config['certs'].map do |cert_name, cert_opts| +# # ensure_cert_dir(cert_dir + cert_name) +# +# p "Cert #{cert_name}: Finding CA to use for cert" +# find_ca_for_cert(cert_name, cert_opts, config) +# end +# +# # p "cert_to_CA_account" +# # pp cert_to_CA_account +# +# certs_to_CA = cert_to_CA_account[0].merge(*cert_to_CA_account[1..]) +# # p "certs_to_CA" +# # p certs_to_CA + + +# unique_CA_accounts_to_use = certs_to_CA.values.uniq +# p "unique_CA_accounts_to_use: #{unique_CA_accounts_to_use}" + +ca_accounts = certs.map { |_, opts| opts['ca_account'] }.uniq + + +account_threads = [] +ca_accounts.each do |ca_account| + account_threads << Thread.new(ca_account) do |ca_account| + ca_name = config.dig('ca_accounts', ca_account, 'ca') + ca = config.dig('CAs', ca_name) + ca_identity = config.dig('ca_accounts', ca_account, 'identity') + identity = config.dig('identities', ca_identity) + client = make_client_for_ca_account(ca, identity) + { + ca_account => client + } + end +end +account_threads.each(&:join) + +ca_clients = account_threads.map { |t| t.value } +ca_clients = ca_clients[0].merge(*ca_clients[1..]) + +# ca_clients = {} +# account_threads.each do |t| +# ca_account_to_client = t.value +# ca_clients[ca_account_to_client['account']] = ca_account_to_client['client'] +# end + + +# acme_threads = [] +certs.each_pair do |cert_name, cert_opts| + # p cert_opts + # p cert_opts['ca_account'] + client = ca_clients[cert_opts['ca_account']] + # p "client" + # p client + domains = cert_opts['domain_names'] + certs[cert_name]['order_thread'] = Thread.new(cert_name, client, domains) do |cert_name, client, domains| p "Cert #{cert_name}: Creating order object for cert #{cert_name}" - order = acme_request_with_retries { client.new_order(identifiers: cert_opts['domain_names']) } - - p "Cert #{cert_name}: order status" - p order.status - - if order.status != 'ready' - p "Cert #{cert_name}: Order is not ready, we need to authorize first" - - # TODO: collect dns modifications per primary NS, update all at once - p "Cert #{cert_name}: Iterating over required authorizations" - auths = acme_request_with_retries { order.authorizations } - auths.each do |auth| - p "Cert #{cert_name}: Processing authorization for #{auth.domain}" - p "Cert #{cert_name}: Finding challenge type for #{auth.domain}" - # p "Cert #{cert_name}: auth is:" - # pp auth - if auth.status == 'valid' - p "Cert #{cert_name}: Authorization for #{auth.domain} is still valid, skipping" - next - end + acme_request_with_retries { client.new_order(identifiers: domains) } + end +end - challenge = auth.dns01 - primary_ns = config.dig('domains', auth.domain, 'primary_ns') || config.dig('defaults', 'domains', 'primary_ns') - deploy_dns01_challenge_token(auth.domain, challenge, primary_ns, config) - wait_for_challenge_propagation(auth.domain, challenge) - wait_for_challenge_validation(challenge, cert_name) - end - else - p "Cert #{cert_name}: Order is ready, we don’t need to authorize" - end - domain_key = read_cert_key(cert_name) +certs.each_pair do |cert_name, cert_opts| + t = cert_opts['order_thread'] + t.join + certs[cert_name]['order'] = t.value +end + +# pp certs + +# certs_to_CA.each_pair do |cert_name, ca| +# # p "ca_clients[ca]: #{ca_clients[ca]}" +# acme_threads << Thread.new(cert_name, ca_clients[ca], config) do |cert_name, client, config| +# p "Cert #{cert_name}: Creating order object for cert #{cert_name}" +# cert_opts = config['certs'][cert_name] +# order = acme_request_with_retries { client.new_order(identifiers: cert_opts['domain_names']) } +# { cert_name => order } +# end +# end +# +# acme_threads.each(&:join) +# orders = acme_threads.map(&:value) +# orders = orders[0].merge(*orders[1..]) +# p "orders:" +# pp orders + +# order_threads = [] +# # for each order do +# # if its ready skip to getting cert +# # THREADS: get authorizations +# authorizations = {} +# orders.each_pair do |cert_name, order| +# order_authorizations = acme_request_with_retries { order.authorizations } +# authorizations[cert_name] = order_authorizations +# end + +certs.each_pair do |cert, opts| + order_authorizations = acme_request_with_retries { opts['order'].authorizations } + order_authorizations.each do |auth| + pp auth + # pp auth.domain + # pp auth.challenges + # pp auth.url + domain_attrs[auth.domain]['auth'] = auth + end +end - get_cert(order, cert_name, cert_opts['domain_names'], domain_key) +all_authorizations = domain_attrs.map do |d, attrs| + attrs['auth'] +end + +authzs_by_apex = all_authorizations.group_by do |auth| + domain_attrs[auth.domain][:apex] +end + +authzs_by_apex.each_pair do |apex, authzs| + handle_apex_authzs(apex, authzs, config) +end + +propagation_threads = [] +domain_attrs.each_pair do |d, attrs| + domain_attrs[d][:propagation_thread] = Thread.new(attrs['auth']) do |auth| + wait_for_challenge_propagation(auth.domain, auth.dns01) end + propagation_threads << domain_attrs[d][:propagation_thread] end -acme_threads.each(&:join) +propagation_threads.each(&:join) + +validation_threads = {} +domain_attrs.each_pair do |d, attrs| + # domain_attrs[d][:auth_thread] = Thread.new(attrs) do |attrs| + validation_threads[d] = Thread.new(attrs) do |attrs| + # wait_for_challenge_validation(auth.domain, auth.dns01) + # pp auth.dns01 + # pp auth.dns01 + wait_for_challenge_validation(attrs['auth'].dns01, attrs['auth'].domain) + end + # validation_threads[d] = domain_attrs[d][:auth_thread] +end + +validation_threads.each_pair { |d, t| t.join } + +certs.each_pair do |name, opts| + cert_key = read_cert_key(name) + get_cert(opts['order'], name, opts['domain_names'], cert_key) +end -- cgit v1.2.3