class LECLI::CertificateBuilder

Helper class to generate certs and access the default options

Constants

YAML_FILENAME

Attributes

production[RW]

Public Class Methods

load_options(config_file:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 49
def self.load_options(config_file:)
  opts = LECLI::CertificateBuilder.runtime_defaults
  opts.merge!(YAML.load_file(config_file)) if File.file?(config_file)
  required_options = LECLI::CertificateBuilder.required_options

  # Should return nil if all required options are not present
  opts if (opts.keys & required_options).count == required_options.count
end
new() { |self| ... } click to toggle source
# File lib/lecli/certificate_builder.rb, line 13
def initialize
  @challenges = []
  @production = false

  # Pass a block to edit the new object for prod/staging or other options
  yield self if block_given?

  prod_url = 'https://acme-v02.api.letsencrypt.org/directory'
  staging_url = 'https://acme-staging-v02.api.letsencrypt.org/directory'
  @endpoint = @production ? prod_url : staging_url
end
persist_defaults_file(override:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 58
def self.persist_defaults_file(override:)
  opts = LECLI::CertificateBuilder.sample_options
  if !File.file?(YAML_FILENAME) || override
    File.write(YAML_FILENAME, opts.to_yaml)
    puts YAML_FILENAME
  else
    puts "#{YAML_FILENAME} already exists. Try `lecli help yaml`"
  end
end
required_options() click to toggle source
# File lib/lecli/certificate_builder.rb, line 25
def self.required_options
  ['domains', 'common_name', 'account_email']
end
runtime_defaults() click to toggle source
# File lib/lecli/certificate_builder.rb, line 41
def self.runtime_defaults
  {
    'request_key' => 'request.pem',
    'certificate_key' => 'certificate.pem',
    'challenges_relative_path' => 'challenges'
  }
end
sample_options() click to toggle source
# File lib/lecli/certificate_builder.rb, line 29
def self.sample_options
  {
    'domains' => ['example.com', 'test.net'],
    'common_name' => 'example.com',
    'account_email' => 'test@account.com',
    'request_key' => 'request.pem',
    'certificate_key' => 'certificate.pem',
    'challenges_relative_path' => 'challenges',
    'success_callback_script' => 'deploy.sh'
  }
end

Public Instance Methods

generate_certs(options) click to toggle source
# File lib/lecli/certificate_builder.rb, line 68
def generate_certs(options)
  success = true

  begin
    request_challenges(options: options)
    sleep(3) # We are unaware of challenge hosting, better give extra time

    request_challenge_validation
    request_key = finalize_order(
      domains: options['domains'],
      title: options['common_name']
    )

    write_certificate(
      cert: @order.certificate, relative_path: options['certificate_key']
    )
    write_certificate(
      cert: request_key, relative_path: options['request_key']
    )
  rescue Acme::Client::Error::RateLimited => e
    puts e.message
    success = false
  end

  success
end

Private Instance Methods

create_order(email:, domains:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 120
def create_order(email:, domains:)
  pkey = OpenSSL::PKey::RSA.new(4096)
  client = Acme::Client.new(private_key: pkey, directory: @endpoint)
  client.new_account(
    contact: "mailto:#{email}",
    terms_of_service_agreed: true
  )
  @order = client.new_order(identifiers: domains)
end
finalize_order(domains:, title:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 108
def finalize_order(domains:, title:)
  request_key = OpenSSL::PKey::RSA.new(4096)
  csr = Acme::Client::CertificateRequest.new(
    private_key: request_key,
    names: domains,
    subject: { common_name: title }
  )
  @order.finalize(csr: csr)
  sleep(1) while @order.status == 'processing'
  request_key
end
persist_challenge_tokens() click to toggle source
# File lib/lecli/certificate_builder.rb, line 160
def persist_challenge_tokens
  @order.authorizations.each do |authorization|
    challenge = authorization.http
    token_path = File.join(@challenges_dir, challenge.token)
    File.write(token_path, challenge.file_content)
    @challenges << challenge
  end
end
request_challenge_validation() click to toggle source
# File lib/lecli/certificate_builder.rb, line 136
def request_challenge_validation
  puts 'Requesting challenge validation and polling for confirmation...'
  wait_time = 5
  pending = true
  while pending
    @challenges.each do |challenge|
      begin
        challenge.request_validation
      rescue Acme::Client::Error::Malformed
        print '.'
      end
    end

    status = @challenges.map(&:status)
    pending = status.include?('pending')

    next unless pending
    puts "At least one challenge still pending, waiting #{wait_time}s ..."
    sleep(wait_time)
    wait_time *= 2 if wait_time < 640 # Gradually increment retry max ~10min
  end
  puts 'Challenges are all valid now!'
end
request_challenges(options:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 97
def request_challenges(options:)
  create_order(email: options['account_email'], domains: options['domains'])
  setup_challenges_dir(relative_path: options['challenges_relative_path'])
  persist_challenge_tokens
end
setup_challenges_dir(relative_path:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 130
def setup_challenges_dir(relative_path:)
  @challenges_dir = File.expand_path(relative_path)
  FileUtils.mkdir_p(@challenges_dir)
  FileUtils.rm(Dir[File.join(@challenges_dir, '*')])
end
write_certificate(cert:, relative_path:) click to toggle source
# File lib/lecli/certificate_builder.rb, line 103
def write_certificate(cert:, relative_path:)
  full_path = File.expand_path(relative_path)
  File.write(full_path, cert)
end