Class: LetsCert::Certificate

Inherits:
Object
  • Object
show all
Includes:
Loggable
Defined in:
lib/letscert/certificate.rb

Overview

Class to handle ACME operations on certificates

Author:

  • Sylvain Daubert

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Loggable

included, #logger

Constructor Details

#initialize(cert) ⇒ Certificate

Returns a new instance of Certificate.

Parameters:

  • cert (OpenSSL::X509::Certificate, nil)


44
45
46
47
# File 'lib/letscert/certificate.rb', line 44

def initialize(cert)
  @cert = cert
  @chain = []
end

Instance Attribute Details

#certOpenSSL::X509::Certificate? (readonly)

Returns:

  • (OpenSSL::X509::Certificate, nil)


36
37
38
# File 'lib/letscert/certificate.rb', line 36

def cert
  @cert
end

#chainArray<OpenSSL::X509::Certificate> (readonly)

Certification chain. Only set by #get.

Returns:

  • (Array<OpenSSL::X509::Certificate>)


39
40
41
# File 'lib/letscert/certificate.rb', line 39

def chain
  @chain
end

#clientAcme::Client? (readonly)

Returns:

  • (Acme::Client, nil)


41
42
43
# File 'lib/letscert/certificate.rb', line 41

def client
  @client
end

Instance Method Details

#get(account_key, key, options) ⇒ void

This method returns an undefined value.

Get a new certificate, or renew an existing one

Parameters:

  • account_key (OpenSSL::PKey::PKey, nil)

    private key to authenticate to ACME server

  • key (OpenSSL::PKey::PKey, nil)

    private key from which make a certificate. If nil, generate a new one with options[:cert_key_size] bits.

  • options (Hash)

    option hash

Options Hash (options):

  • :account_key_size (Fixnum)

    ACME account private key size in bits

  • :cert_key_size (Fixnum)

    private key size used to generate a certificate

  • :email (String)

    e-mail used as ACME account

  • :files (Array<String>)

    plugin names to use

  • :reuse_key (Boolean)

    reuse private key when getting a new certificate

  • :roots (Hash)

    hash associating domains as keys to web roots as values

  • :server (String)

    ACME servel URL

Raises:

  • (Acme::Client::Error)

    error in protocol ACME with server

  • (Error)

    issue with domain name, challenge fails,…



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/letscert/certificate.rb', line 70

def get(, key, options)
  logger.info { 'create key/cert/chain...' }
  check_roots(options[:roots])
  logger.debug { "webroots are: #{options[:roots].inspect}" }

   = (, options[:account_key_type],
                                options[:account_key_size])

  client = get_acme_client(, options)

  do_challenges client, options[:roots]

  pkey = if options[:reuse_key]
           raise Error, 'cannot reuse a non-existing key' if key.nil?
           logger.info { 'Reuse existing private key' }
           generate_certificate_from_pkey options[:roots].keys, key
         else
           logger.info { 'Generate new private key' }
           generate_certificate options[:roots].keys,
                                options
         end

  options[:files] ||= []
  options[:files].each do |plugname|
    IOPlugin.registered[plugname].save(account_key: ,
                                       key: pkey, cert: @cert,
                                       chain: @chain)
  end
end

#get_acme_client(account_key, options) {|@client| ... } ⇒ Acme::Client

Get ACME client.

Client is only created on first call, then it is cached.

Parameters:

  • account_key (Hash)
  • options (Hash)

Yields:

Returns:

  • (Acme::Client)


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/letscert/certificate.rb', line 160

def get_acme_client(, options)
  return @client if @client

  logger.debug { "connect to #{options[:server]}" }
  @client = Acme::Client.new(private_key: , endpoint: options[:server])

  yield @client if block_given?

  if options[:email].nil?
    logger.warn { '--email was not provided. ACME CA will have no way to ' \
                  'contact you!' }
  end

  begin
    logger.debug { "register with #{options[:email]}" }
    registration = @client.register(contact: "mailto:#{options[:email]}")
  rescue Acme::Client::Error::Malformed => ex
    raise if ex.message != 'Registration key is already in use'
  else
    # Requesting ToS make acme-client throw an exception: Connection reset
    # by peer (Faraday::ConnectionFailed). To investigate...
    #if registration.term_of_service_uri
    #  @logger.debug { "get terms of service" }
    #  terms = registration.get_terms
    #  if !terms.nil?
    #    tos_digest = OpenSSL::Digest::SHA256.digest(terms)
    #    if tos_digest != @options[:tos_sha256]
    #      raise Error, 'Terms Of Service mismatch'
    #    end
         @logger.debug { 'agree terms of service' }
         registration.agree_terms
    #  end
    #end
  end

  @client
end

#revoke(account_key, options = {}) ⇒ Boolean

Revoke certificate

Parameters:

  • account_key (OpenSSL::PKey::PKey)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :account_key_size (Fixnum)

    ACME account private key size in bits

  • :email (String)

    e-mail used as ACME account

  • :server (String)

    ACME servel URL

Returns:

  • (Boolean)

Raises:

  • (Error)

    no certificate to revole.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/letscert/certificate.rb', line 109

def revoke(, options = {})
  raise Error, 'no certification data to revoke' if @cert.nil?

  client = get_acme_client(, options)
  result = client.revoke_certificate(@cert)

  if result
    logger.info { 'certificate is revoked' }
  else
    logger.warn { 'certificate is not revoked!' }
  end

  result
end

#valid?(domains, valid_min = 0) ⇒ Boolean

Check if certificate is still valid for at least valid_min seconds. Also checks that domains are certified by certificate.

Parameters:

  • domains (Array<String>)

    list of certificate domains

  • valid_min (Integer) (defaults to: 0)

    minimum number of seconds of validity under which a renewal is necessary.

Returns:

  • (Boolean)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/letscert/certificate.rb', line 130

def valid?(domains, valid_min = 0)
  if @cert.nil?
    logger.debug { 'no existing certificate' }
    return false
  end

  subjects = []
  @cert.extensions.each do |ext|
    if ext.oid == 'subjectAltName'
      subjects += ext.value.split(/,\s*/).map { |s| s.sub(/DNS:/, '') }
    end
  end
  logger.debug { "cert SANs: #{subjects.join(', ')}" }

  # Check all domains are subjects of certificate
  unless domains.all? { |domain| subjects.include? domain }
    msg = 'At least one domain is not declared as a certificate subject. ' \
          'Backup and remove existing cert if you want to proceed.'
    raise Error, msg
  end

  !renewal_necessary?(valid_min)
end