class Heroku::Command::Certs

manage ssl endpoints for an app

Constants

SSL_DOCTOR

Public Instance Methods

add() click to toggle source

certs:add CRT KEY

Add an ssl endpoint to an app.

--bypass                 # bypass the trust chain completion step
# File lib/heroku/command/certs.rb, line 74
def add
  crt, key = read_crt_and_key
  endpoint = action("Adding SSL Endpoint to #{app}") { heroku.ssl_endpoint_add(app, crt, key) }
  display_warnings(endpoint)
  display "#{app} now served by #{endpoint['cname']}"
  display "Certificate details:"
  display_certificate_info(endpoint)
rescue UsageError
  fail("Usage: heroku certs:add CRT KEY\nMust specify CRT and KEY to add cert.")
end
chain() click to toggle source

certs:chain CRT [CRT …]

Print the ordered and complete chain for the given certificate.

Optional intermediate certificates may be given too, and will be used during chain resolution.

# File lib/heroku/command/certs.rb, line 48
def chain
  puts read_crt_through_ssl_doctor
rescue UsageError
  fail("Usage: heroku certs:chain CRT [CRT ...]\nMust specify at least one certificate file.")
end
generate() click to toggle source

certs:generate DOMAIN

Generate a key and certificate signing request (or self-signed certificate) for an app. Prompts for information to put in the certificate unless –now is used, or at least one of the –subject, –owner, –country, –area, or –city options is specified.

--selfsigned              # generate a self-signed certificate instead of a CSR
--keysize BITSIZE         # RSA key size in bits (default: 2048)
--owner NAME              # name of organization certificate belongs to
--country COUNTRY         # country of owner, as a two-letter ISO country code
--area AREA               # sub-country area (state, province, etc.) of owner
--city CITY               # city of owner
--subject SUBJECT         # specify entire certificate subject
--now                     # do not prompt for any owner information
# File lib/heroku/command/certs.rb, line 176
def generate
  request = Heroku::OpenSSL::CertificateRequest.new

  request.domain = args[0] || error("certs:generate must specify a domain")
  request.subject = cert_subject_for_domain_and_options(request.domain, options)
  request.self_signed = options[:selfsigned] || false
  request.key_size = (options[:keysize] || request.key_size).to_i

  result = request.generate

  explain_step_after_generate result

rescue Heroku::OpenSSL::NotInstalledError => ex
  error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint)

rescue Heroku::OpenSSL::GenericError => ex
  error(ex.message)
end
index() click to toggle source

certs

List ssl endpoints for an app.

# File lib/heroku/command/certs.rb, line 16
def index
  endpoints = heroku.ssl_endpoint_list(app)

  if endpoints.empty?
    display "#{app} has no SSL Endpoints."
    display "Use `heroku certs:add CRT KEY` to add one."
  else
    endpoints.map! do |endpoint|
      ssl_cert_attributes = {}
      if cert = endpoint['ssl_cert']
        ssl_cert_attributes.merge!(
          'domains'    => cert['cert_domains'].join(', '),
          'expires_at' => format_date(cert['expires_at']),
          'ca_signed?' => cert['ca_signed?'].to_s.capitalize)
      end
      { 'cname' => endpoint['cname'] }.merge(ssl_cert_attributes)
    end
    display_table(
      endpoints,
      %w( cname domains expires_at ca_signed? ),
      [ "Endpoint", "Common Name(s)", "Expires", "Trusted" ]
    )
  end
end
info() click to toggle source

certs:info

Show certificate information for an ssl endpoint.

-e, --endpoint ENDPOINT  # name of the endpoint to check info on
# File lib/heroku/command/certs.rb, line 111
def info
  cname = options[:endpoint] || current_endpoint
  endpoint = action("Fetching SSL Endpoint #{cname} info for #{app}") do
    heroku.ssl_endpoint_info(app, cname)
  end

  if endpoint
    display "Certificate details:"
    display_certificate_info(endpoint)
  else
    error "No certificate found."
  end
end
key() click to toggle source

certs:key CRT KEY [KEY …]

Print the correct key for the given certificate.

You must pass one single certificate, and one or more keys. The first key that signs the certificate will be printed back.

# File lib/heroku/command/certs.rb, line 61
def key
  _, key = read_crt_and_key_through_ssl_doctor("Testing for signing key")
  puts key
rescue UsageError
  fail("Usage: heroku certs:key CRT KEY [KEY ...]\nMust specify one certificate file and at least one key file.")
end
remove() click to toggle source

certs:remove

Remove an SSL Endpoint from an app.

-e, --endpoint ENDPOINT  # name of the endpoint to remove
# File lib/heroku/command/certs.rb, line 131
def remove
  cname = options[:endpoint] || current_endpoint
  message = "WARNING: Potentially Destructive Action\nThis command will remove the endpoint #{cname} from #{app}."
  return unless confirm_command(app, message)
  action("Removing SSL Endpoint #{cname} from #{app}") do
    heroku.ssl_endpoint_remove(app, cname)
  end
  display "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing."
end
rollback() click to toggle source

certs:rollback

Rollback an SSL Endpoint for an app.

-e, --endpoint ENDPOINT  # name of the endpoint to rollback
# File lib/heroku/command/certs.rb, line 147
def rollback
  cname = options[:endpoint] || current_endpoint

  message = "WARNING: Potentially Destructive Action\nThis command will rollback the certificate of endpoint #{cname} on #{app}."
  return unless confirm_command(app, message)

  endpoint = action("Rolling back SSL Endpoint #{cname} for #{app}") do
    heroku.ssl_endpoint_rollback(app, cname)
  end

  display "New active certificate details:"
  display_certificate_info(endpoint)
end
update() click to toggle source

certs:update CRT KEY

Update an SSL Endpoint on an app.

--bypass                 # bypass the trust chain completion step
-e, --endpoint ENDPOINT  # name of the endpoint to update
# File lib/heroku/command/certs.rb, line 92
def update
  crt, key = read_crt_and_key
  cname    = options[:endpoint] || current_endpoint
  message = "WARNING: Potentially Destructive Action\nThis command will change the certificate of endpoint #{cname} on #{app}."
  return unless confirm_command(app, message)
  endpoint = action("Updating SSL Endpoint #{cname} for #{app}") { heroku.ssl_endpoint_update(app, cname, crt, key) }
  display_warnings(endpoint)
  display "Updated certificate details:"
  display_certificate_info(endpoint)
rescue UsageError
  fail("Usage: heroku certs:update CRT KEY\nMust specify CRT and KEY to update cert.")
end

Private Instance Methods

all_endpoint_domains() click to toggle source
# File lib/heroku/command/certs.rb, line 272
def all_endpoint_domains
  endpoints = heroku.ssl_endpoint_list(app)
  endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] }                .map   { |endpoint| endpoint['ssl_cert']['cert_domains'] }                .reduce(:+)
end
cert_subject_for_domain_and_options(domain, options = {}) click to toggle source
# File lib/heroku/command/certs.rb, line 288
def cert_subject_for_domain_and_options(domain, options = {})
  raise ArgumentError, "domain cannot be empty" if domain.nil? || domain.empty?

  subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now)

  if val_empty? subject
    if !now && [country, area, city, owner].all? { |v| val_empty? v }
      owner = prompt "Owner of this certificate"
      country = prompt "Country of owner (two-letter ISO code)"
      area = prompt "State/province/etc. of owner"
      city = prompt "City of owner"
    end

    subject = ""
    subject += "/C=#{country}" unless val_empty? country
    subject += "/ST=#{area}" unless val_empty? area
    subject += "/L=#{city}" unless val_empty? city
    subject += "/O=#{owner}" unless val_empty? owner

    subject += "/CN=#{domain}"
  end

  subject
end
current_endpoint() click to toggle source
# File lib/heroku/command/certs.rb, line 197
def current_endpoint
  endpoint = heroku.ssl_endpoint_list(app).first || error("#{app} has no SSL Endpoints.")
  endpoint["cname"]
end
display(msg = "", new_line = true) click to toggle source
Calls superclass method Heroku::Helpers#display
# File lib/heroku/command/certs.rb, line 229
def display(msg = "", new_line = true)
  super if $stdout.tty?
end
display_certificate_info(endpoint) click to toggle source
# File lib/heroku/command/certs.rb, line 202
def display_certificate_info(endpoint)
  data = {
    'Common Name(s)'  => endpoint['ssl_cert']['cert_domains'],
    'Expires At'      => format_date(endpoint['ssl_cert']['expires_at']),
    'Issuer'          => endpoint['ssl_cert']['issuer'],
    'Starts At'       => format_date(endpoint['ssl_cert']['starts_at']),
    'Subject'         => endpoint['ssl_cert']['subject']
  }
  styled_hash(data)

  if endpoint["ssl_cert"]["ca_signed?"]
    display "SSL certificate is verified by a root authority."
  elsif endpoint["issuer"] == endpoint["subject"]
    display "SSL certificate is self signed."
  else
    display "SSL certificate is not trusted."
  end
end
display_warnings(endpoint) click to toggle source
# File lib/heroku/command/certs.rb, line 221
def display_warnings(endpoint)
  if endpoint["warnings"]
    endpoint["warnings"].each do |field, warning|
      display "WARNING: #{field} #{warning}"
    end
  end
end
explain_step_after_generate(result) click to toggle source
# File lib/heroku/command/certs.rb, line 313
def explain_step_after_generate(result)
  if result.csr_file.nil?
    display "Your key and self-signed certificate have been generated."
    display "Next, run:"
  else
    display "Your key and certificate signing request have been generated."
    display "Submit the CSR in '#{result.csr_file}' to your preferred certificate authority."
    display "When you've received your certificate, run:"
  end

  needs_addon = false
  command = "add"
  begin
    command = "update" if all_endpoint_domains.include? result.request.domain
  rescue RestClient::Forbidden
    needs_addon = true
  end

  display "$ heroku addons:add ssl:endpoint" if needs_addon
  display "$ heroku certs:#{command} #{result.crt_file || "CERTFILE"} #{result.key_file}"
end
post_to_ssl_doctor(path, action_text = nil) click to toggle source
# File lib/heroku/command/certs.rb, line 233
def post_to_ssl_doctor(path, action_text = nil)
  raise UsageError if args.size < 1
  action_text ||= "Resolving trust chain"
  action(action_text) do
    input = args.map { |arg|
      begin
        certbody=File.read(arg)
      rescue => e
        error("Unable to read #{arg} file: #{e}")
      end
      certbody
    }.join("\n")
    SSL_DOCTOR.post(:path => path, :body => input, :headers => {'Content-Type' => 'application/octet-stream'}, :expects => 200).body
  end

rescue Excon::Errors::BadRequest, Excon::Errors::UnprocessableEntity => e
  error(e.response.body)
end
prompt(question) click to toggle source
# File lib/heroku/command/certs.rb, line 279
def prompt(question)
  display("#{question}: ", false)
  ask
end
read_crt_and_key() click to toggle source
# File lib/heroku/command/certs.rb, line 268
def read_crt_and_key
  options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor
end
read_crt_and_key_bypassing_ssl_doctor() click to toggle source
# File lib/heroku/command/certs.rb, line 261
def read_crt_and_key_bypassing_ssl_doctor
  raise UsageError if args.size != 2
  crt = File.read(args[0]) rescue error("Unable to read #{args[0]} CRT")
  key = File.read(args[1]) rescue error("Unable to read #{args[1]} KEY")
  [crt, key]
end
read_crt_and_key_through_ssl_doctor(action_text = nil) click to toggle source
# File lib/heroku/command/certs.rb, line 252
def read_crt_and_key_through_ssl_doctor(action_text = nil)
  crt_and_key = post_to_ssl_doctor("resolve-chain-and-key", action_text)
  MultiJson.load(crt_and_key).values_at("pem", "key")
end
read_crt_through_ssl_doctor(action_text = nil) click to toggle source
# File lib/heroku/command/certs.rb, line 257
def read_crt_through_ssl_doctor(action_text = nil)
  post_to_ssl_doctor("resolve-chain", action_text)
end
val_empty?(val) click to toggle source
# File lib/heroku/command/certs.rb, line 284
def val_empty?(val)
  val.nil? or val.empty?
end