class ApartmentAcmeClient::Encryption

Public Class Methods

new() click to toggle source
# File lib/apartment_acme_client/encryption.rb, line 25
def initialize
  @certificate_storage = ApartmentAcmeClient::CertificateStorage::Proxy.singleton
end

Public Instance Methods

authorize_domain_with_http(domain_authorization) click to toggle source

authorizes a single domain with letsencrypt server returns true on success, false otherwise.

from github.com/unixcharles/acme-client/tree/master#authorize-for-domain

# File lib/apartment_acme_client/encryption.rb, line 114
def authorize_domain_with_http(domain_authorization)
  challenge = domain_authorization.http

  puts "authorizing Domain: #{domain_authorization.domain}"
  # The http method will require you to respond to a HTTP request.

  # You can retrieve the challenge token
  challenge.token # => "some_token"

  # You can retrieve the expected path for the file.
  challenge.filename # => ".well-known/acme-challenge/:some_token"

  # You can generate the body of the expected response.
  challenge.file_content # => 'string token and JWK thumbprint'

  # You are not required to send a Content-Type. This method will return the right Content-Type should you decide to include one.
  challenge.content_type

  # Save the file. We'll create a public directory to serve it from, and inside it we'll create the challenge file.
  FileUtils.mkdir_p(File.join(ApartmentAcmeClient.public_folder, File.dirname(challenge.filename)))

  # We'll write the content of the file
  full_challenge_filename = File.join(ApartmentAcmeClient.public_folder, challenge.filename)
  File.write(full_challenge_filename, challenge.file_content)

  # Optionally save the challenge for use at another time (eg: by a background job processor)
  #  File.write('challenge', challenge.to_h.to_json)

  # The challenge file can be served with a Ruby webserver.
  # You can run a webserver in another console for that purpose. You may need to forward ports on your router.
  #
  # $ ruby -run -e httpd public -p 8080 --bind-address 0.0.0.0

  # Load a saved challenge. This is only required if you need to reuse a saved challenge as outlined above.
  #  challenge = client.challenge_from_hash(JSON.parse(File.read('challenge')))

  # Once you are ready to serve the confirmation request you can proceed.
  challenge.request_validation # => true

  30.times do
    # may be 'pending' initially
    if challenge.status == 'valid'
      puts "authorized!"
      break
    end

    puts "Waiting for letsencrypt to authorize the single domain. Status: #{challenge.status}"

    # Wait a bit for the server to make the request, or just blink. It should be fast.
    sleep(2)
    challenge.reload
  end
  File.delete(full_challenge_filename)

  challenge.status == 'valid'
end
authorize_domains_with_dns(authorizations, wildcard_domain:) click to toggle source

Authorize a wildcard cert domain. to do this, we have to write to the Amazon Route53 DNS entry params:

- authorizations - a list of authorizations, which may be http or dns based (ignore the non-wildcard ones)
- wildcard_domain - the url of the wildcard's base domain (e.g. "site.example.com")
# File lib/apartment_acme_client/encryption.rb, line 51
def authorize_domains_with_dns(authorizations, wildcard_domain:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  label = nil
  record_type = nil
  values = []

  dns_authorizations = []
  authorizations.each do |domain_authorization|
    next unless domain_authorization.wildcard || domain_authorization.http.nil?

    dns_authorizations << domain_authorization.dns
  end

  dns_authorizations.each do |authorization|
    label         = "#{authorization.record_name}.#{wildcard_domain}"
    record_type   = authorization.record_type
    value         = authorization.record_content
    values << value
  end

  return true unless values.any?

  route53 = ApartmentAcmeClient::DnsApi::Route53.new(
    requested_domain: wildcard_domain,
    dns_record_label: label,
    record_type: record_type,
    values: values
  )

  puts "writing #{label} to Route53"
  route53.write_record

  check_dns = ApartmentAcmeClient::DnsApi::CheckDns.new(wildcard_domain, label)

  check_dns.wait_for_present(values.first)
  puts "waiting 60 seconds before requesting DNS check from LetsEncrypt"
  sleep(60)

  if check_dns.check_dns(values.first)
    # DNS is updated, proceed with cert request
    dns_authorizations.each do |domain_authorization|
      domain_authorization.request_validation

      60.times do
        # may be 'pending' initially
        break if domain_authorization.status == 'valid'

        puts "Waiting for LetsEncrypt to authorize the domain. Status #{domain_authorization.status}"

        # Wait a bit for the server to make the request, or just blink. It should be fast.
        sleep(2)
        domain_authorization.reload
      end
    end
  else
    # ERROR, DNS not updated in time
    Rollbar.error("DNS Entry not found in timeout")
  end
end
csr_private_key_string() click to toggle source

for use in order to store this on the machine for NGINX use

# File lib/apartment_acme_client/encryption.rb, line 200
def csr_private_key_string
  csr_private_key.to_s
end
register_new(email) click to toggle source

Largely based on github.com/unixcharles/acme-client documentation

# File lib/apartment_acme_client/encryption.rb, line 30
def register_new(email)
  raise StandardError.new('Private key already exists') unless @certificate_storage.private_key.nil?

  private_key = create_private_key

  # Initialize the client
  new_client = ApartmentAcmeClient::AcmeClient::Proxy.singleton(
    acme_client_private_key: private_key,
    csr_private_key: nil, # not needed for 'register' call
  )

  new_client.register(email)

  @certificate_storage.save_private_key(private_key)
end
request_certificate(common_name:, domains:, wildcard_domain: nil) click to toggle source

Create an order, perform authorization for each domain, and then request the certificate.

  • common name is used so that there is continuity of requests over time

  • domains are the list of individual http-based domains to be authorized

  • wildcard_domain is an optional wildcard domain to be authorized via DNS Record

Returns the certificate

# File lib/apartment_acme_client/encryption.rb, line 178
def request_certificate(common_name:, domains:, wildcard_domain: nil)
  domain_names_requested = domains
  domain_names_requested += [wildcard_domain, "*.#{wildcard_domain}"] if wildcard_domain.present?
  order = client.new_order(identifiers: domain_names_requested)

  # Do the HTTP authorizations
  order.authorizations.each do |authorization|
    next if authorization.wildcard || authorization.http.nil?

    authorize_domain_with_http(authorization)
  end

  # Do the DNS (wildcard) authorizations
  if authorize_domains_with_dns(order.authorizations, wildcard_domain: wildcard_domain)
    client.request_certificate(common_name: common_name, names: domain_names_requested, order: order)
  else
    # error, not authorized
    nil
  end
end

Private Instance Methods

acme_client_private_key() click to toggle source

Returns a private key

# File lib/apartment_acme_client/encryption.rb, line 214
def acme_client_private_key
  private_key = @certificate_storage.private_key
  return nil unless private_key

  OpenSSL::PKey::RSA.new(private_key)
end
client() click to toggle source
# File lib/apartment_acme_client/encryption.rb, line 206
def client
  @client ||= ApartmentAcmeClient::AcmeClient::Proxy.singleton(
    acme_client_private_key: acme_client_private_key,
    csr_private_key: csr_private_key,
  )
end
create_private_key() click to toggle source
# File lib/apartment_acme_client/encryption.rb, line 233
def create_private_key
  OpenSSL::PKey::RSA.new(4096)
end
csr_private_key() click to toggle source
# File lib/apartment_acme_client/encryption.rb, line 221
def csr_private_key
  private_key = @certificate_storage.csr_private_key

  # create a new private key if one is not found
  if private_key.nil?
    private_key = create_private_key
    @certificate_storage.save_csr_private_key(private_key)
  end

  OpenSSL::PKey::RSA.new(private_key)
end