Module: Chef::Mixin::OpenSSLHelper

Overview

various helpers for use with openssl. Currently used by the openssl_* resources

Instance Method Summary collapse

Instance Method Details

#cert_need_renewal?(cert_file, renew_before_expiry) ⇒ true, false Also known as: cert_need_renewall?

Return true if a certificate need to be renewed (or doesn’t exist) according to the number of days before expiration given

Parameters:

  • cert_file (string)

    path of the cert file or cert content

  • renew_before_expiry (integer)

    number of days before expiration

Returns:

  • (true, false)


412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/chef/mixin/openssl_helper.rb', line 412

def cert_need_renewal?(cert_file, renew_before_expiry)
  resp = true
  cert_content = ::File.exist?(cert_file) ? File.read(cert_file) : cert_file
  begin
    cert = OpenSSL::X509::Certificate.new cert_content
  rescue ::OpenSSL::X509::CertificateError
    return resp
  end

  unless cert.not_after <= Time.now + 3600 * 24 * renew_before_expiry
    resp = false
  end

  resp
end

#crl_file_valid?(crl_file) ⇒ Boolean

given a crl file path see if it’s actually a crl

Parameters:

  • crl_file (String)

    the path to the crlfile

Returns:

  • (Boolean)

    is the key valid?



77
78
79
80
81
82
83
84
# File 'lib/chef/mixin/openssl_helper.rb', line 77

def crl_file_valid?(crl_file)
  begin
    ::OpenSSL::X509::CRL.new ::File.read(crl_file)
  rescue ::OpenSSL::X509::CRLError, Errno::ENOENT
    return false
  end
  true
end

#dhparam_pem_valid?(dhparam_pem_path) ⇒ Boolean

validate a dhparam file from path

Parameters:

  • dhparam_pem_path (String)

    the path to the pem file

Returns:

  • (Boolean)

    is the key valid



42
43
44
45
46
47
48
49
# File 'lib/chef/mixin/openssl_helper.rb', line 42

def dhparam_pem_valid?(dhparam_pem_path)
  # Check if the dhparam.pem file exists
  # Verify the dhparam.pem file contains a key
  return false unless ::File.exist?(dhparam_pem_path)

  dhparam = ::OpenSSL::PKey::DH.new File.read(dhparam_pem_path)
  dhparam.params_ok?
end

#encrypt_ec_key(ec_key, key_password, key_cipher) ⇒ String

generate a pem file given a cipher, key, an optional key_password

Parameters:

  • ec_key (OpenSSL::PKey::EC)

    the private key object

  • key_password (String)

    the password for the private key

  • key_cipher (String)

    the cipher to use

Returns:

Raises:

  • (TypeError)


192
193
194
195
196
197
198
199
200
# File 'lib/chef/mixin/openssl_helper.rb', line 192

def encrypt_ec_key(ec_key, key_password, key_cipher)
  raise TypeError, "ec_key must be a Ruby OpenSSL::PKey::EC object" unless ec_key.is_a?(::OpenSSL::PKey::EC)
  raise TypeError, "key_password must be a string" unless key_password.is_a?(String)
  raise TypeError, "key_cipher must be a string" unless key_cipher.is_a?(String)
  raise ArgumentError, "Specified key_cipher is not available on this system" unless ::OpenSSL::Cipher.ciphers.include?(key_cipher)

  cipher = ::OpenSSL::Cipher.new(key_cipher)
  ec_key.to_pem(cipher, key_password)
end

#encrypt_rsa_key(rsa_key, key_password, key_cipher) ⇒ String

generate a pem file given a cipher, key, an optional key_password

Parameters:

  • rsa_key (OpenSSL::PKey::RSA)

    the private key object

  • key_password (String)

    the password for the private key

  • key_cipher (String)

    the cipher to use

Returns:

Raises:

  • (TypeError)


143
144
145
146
147
148
149
150
151
# File 'lib/chef/mixin/openssl_helper.rb', line 143

def encrypt_rsa_key(rsa_key, key_password, key_cipher)
  raise TypeError, "rsa_key must be a Ruby OpenSSL::PKey::RSA object" unless rsa_key.is_a?(::OpenSSL::PKey::RSA)
  raise TypeError, "key_password must be a string" unless key_password.is_a?(String)
  raise TypeError, "key_cipher must be a string" unless key_cipher.is_a?(String)
  raise ArgumentError, "Specified key_cipher is not available on this system" unless ::OpenSSL::Cipher.ciphers.include?(key_cipher)

  cipher = ::OpenSSL::Cipher.new(key_cipher)
  rsa_key.to_pem(cipher, key_password)
end

#gen_dhparam(key_length, generator) ⇒ OpenSSL::PKey::DH

generate a dhparam file

Parameters:

  • key_length (String)

    the length of the key

  • generator (Integer)

    the dhparam generator to use

Returns:

  • (OpenSSL::PKey::DH)

Raises:

  • (ArgumentError)


110
111
112
113
114
115
# File 'lib/chef/mixin/openssl_helper.rb', line 110

def gen_dhparam(key_length, generator)
  raise ArgumentError, "Key length must be a power of 2 greater than or equal to 1024" unless key_length_valid?(key_length)
  raise TypeError, "Generator must be an integer" unless generator.is_a?(Integer)

  ::OpenSSL::PKey::DH.new(key_length, generator)
end

#gen_ec_priv_key(curve) ⇒ OpenSSL::PKey::DH

generate an ec private key given curve type

Parameters:

  • curve (String)

    the kind of curve to use

Returns:

  • (OpenSSL::PKey::DH)

Raises:

  • (TypeError)


156
157
158
159
160
161
# File 'lib/chef/mixin/openssl_helper.rb', line 156

def gen_ec_priv_key(curve)
  raise TypeError, "curve must be a string" unless curve.is_a?(String)
  raise ArgumentError, "Specified curve is not available on this system" unless %w{prime256v1 secp384r1 secp521r1}.include?(curve)

  ::OpenSSL::PKey::EC.new(curve).generate_key
end

#gen_ec_pub_key(priv_key, priv_key_password = nil) ⇒ String

generate pem format of the public key given a private key

Parameters:

  • priv_key (String)

    either the contents of the private key or the path to the file

  • priv_key_password (String) (defaults to: nil)

    optional password for the private key

Returns:

  • (String)

    pem format of the public key



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/chef/mixin/openssl_helper.rb', line 167

def gen_ec_pub_key(priv_key, priv_key_password = nil)
  # if the file exists try to read the content
  # if not assume we were passed the key and set the string to the content
  key_content = ::File.exist?(priv_key) ? File.read(priv_key) : priv_key
  key = ::OpenSSL::PKey::EC.new key_content, priv_key_password

  # Get curve type (prime256v1...)
  group = ::OpenSSL::PKey::EC::Group.new(key.group.curve_name)
  # Get Generator point & public point (priv * generator)
  generator = group.generator
  pub_point = generator.mul(key.private_key)
  key.public_key = pub_point

  # Public Key in pem
  public_key = ::OpenSSL::PKey::EC.new
  public_key.group = group
  public_key.public_key = pub_point
  public_key.to_pem
end

#gen_rsa_priv_key(key_length) ⇒ OpenSSL::PKey::DH

generate an RSA private key given key length

Parameters:

  • key_length (Integer)

    the key length of the private key

Returns:

  • (OpenSSL::PKey::DH)

Raises:

  • (ArgumentError)


120
121
122
123
124
# File 'lib/chef/mixin/openssl_helper.rb', line 120

def gen_rsa_priv_key(key_length)
  raise ArgumentError, "Key length must be a power of 2 greater than or equal to 1024" unless key_length_valid?(key_length)

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

#gen_rsa_pub_key(priv_key, priv_key_password = nil) ⇒ String

generate pem format of the public key given a private key

Parameters:

  • priv_key (String)

    either the contents of the private key or the path to the file

  • priv_key_password (String) (defaults to: nil)

    optional password for the private key

Returns:

  • (String)

    pem format of the public key



130
131
132
133
134
135
136
# File 'lib/chef/mixin/openssl_helper.rb', line 130

def gen_rsa_pub_key(priv_key, priv_key_password = nil)
  # if the file exists try to read the content
  # if not assume we were passed the key and set the string to the content
  key_content = ::File.exist?(priv_key) ? File.read(priv_key) : priv_key
  key = ::OpenSSL::PKey::RSA.new key_content, priv_key_password
  key.public_key.to_pem
end

#gen_serialInteger

generate a random Serial

Returns:

  • (Integer)


242
243
244
# File 'lib/chef/mixin/openssl_helper.rb', line 242

def gen_serial
  ::OpenSSL::BN.generate_prime(160)
end

#gen_x509_cert(request, extension, info, key) ⇒ OpenSSL::X509::Certificate

generate a Certificate given a X509 request

Parameters:

  • request (OpenSSL::X509::Request)

    X509 Certificate Request

  • extension (Array)

    Array of X509 Certificate Extension

  • info (Hash)

    issuer & validity

  • key (OpenSSL::PKey::EC, OpenSSL::PKey::RSA)

    private key to sign with

Returns:

  • (OpenSSL::X509::Certificate)

Raises:

  • (TypeError)


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/chef/mixin/openssl_helper.rb', line 252

def gen_x509_cert(request, extension, info, key)
  raise TypeError, "request must be a Ruby OpenSSL::X509::Request" unless request.is_a?(::OpenSSL::X509::Request)
  raise TypeError, "extension must be a Ruby Array" unless extension.is_a?(Array)
  raise TypeError, "info must be a Ruby Hash" unless info.is_a?(Hash)
  raise TypeError, "key must be a Ruby OpenSSL::PKey::EC object or a Ruby OpenSSL::PKey::RSA object" unless key.is_a?(::OpenSSL::PKey::EC) || key.is_a?(::OpenSSL::PKey::RSA)

  raise ArgumentError, "info must contain a validity" unless info.key?("validity")
  raise TypeError, "info['validity'] must be a Ruby Integer object" unless info["validity"].is_a?(Integer)

  cert = ::OpenSSL::X509::Certificate.new
  ef = ::OpenSSL::X509::ExtensionFactory.new

  cert.serial = gen_serial
  cert.version = 2
  cert.subject = request.subject
  cert.public_key = request.public_key
  cert.not_before = Time.now
  cert.not_after = cert.not_before + info["validity"] * 24 * 60 * 60

  if info["issuer"].nil?
    cert.issuer = request.subject
    ef.issuer_certificate = cert
    extension << ef.create_extension("basicConstraints", "CA:TRUE", true)
  else
    raise TypeError, "info['issuer'] must be a Ruby OpenSSL::X509::Certificate object" unless info["issuer"].is_a?(::OpenSSL::X509::Certificate)

    cert.issuer = info["issuer"].subject
    ef.issuer_certificate = info["issuer"]
  end
  ef.subject_certificate = cert
  if openssl_config = __openssl_config
    ef.config = openssl_config
  end

  cert.extensions = extension
  cert.add_extension ef.create_extension("subjectKeyIdentifier", "hash")
  cert.add_extension ef.create_extension("authorityKeyIdentifier",
    "keyid:always,issuer:always")

  cert.sign(key, ::OpenSSL::Digest.new("SHA256"))
  cert
end

#gen_x509_crl(ca_private_key, info) ⇒ OpenSSL::X509::CRL

generate a X509 CRL given a CA

Parameters:

  • ca_private_key (OpenSSL::PKey::EC, OpenSSL::PKey::RSA)

    private key from the CA

  • info (Hash)

    issuer & validity

Returns:

  • (OpenSSL::X509::CRL)

Raises:

  • (TypeError)


299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/chef/mixin/openssl_helper.rb', line 299

def gen_x509_crl(ca_private_key, info)
  raise TypeError, "ca_private_key must be a Ruby OpenSSL::PKey::EC object or a Ruby OpenSSL::PKey::RSA object" unless ca_private_key.is_a?(::OpenSSL::PKey::EC) || ca_private_key.is_a?(::OpenSSL::PKey::RSA)
  raise TypeError, "info must be a Ruby Hash" unless info.is_a?(Hash)

  raise ArgumentError, "info must contain a issuer and a validity" unless info.key?("issuer") && info.key?("validity")
  raise TypeError, "info['issuer'] must be a Ruby OpenSSL::X509::Certificate object" unless info["issuer"].is_a?(::OpenSSL::X509::Certificate)
  raise TypeError, "info['validity'] must be a Ruby Integer object" unless info["validity"].is_a?(Integer)

  crl = ::OpenSSL::X509::CRL.new
  ef = ::OpenSSL::X509::ExtensionFactory.new

  crl.version = 1
  crl.issuer = info["issuer"].subject
  crl.last_update = Time.now
  crl.next_update = Time.now + 3600 * 24 * info["validity"]

  if openssl_config = __openssl_config
    ef.config = openssl_config
  end
  ef.issuer_certificate = info["issuer"]

  crl.add_extension ::OpenSSL::X509::Extension.new("crlNumber", ::OpenSSL::ASN1::Integer(1))
  crl.add_extension ef.create_extension("authorityKeyIdentifier",
    "keyid:always,issuer:always")
  crl.sign(ca_private_key, ::OpenSSL::Digest.new("SHA256"))
  crl
end

#gen_x509_extensions(extensions) ⇒ Array

generate an array of X509 Extensions given a hash of extensions

Parameters:

  • extensions (Hash)

    hash of extensions

Returns:

  • (Array)

Raises:

  • (TypeError)


225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/chef/mixin/openssl_helper.rb', line 225

def gen_x509_extensions(extensions)
  raise TypeError, "extensions must be a Ruby Hash object" unless extensions.is_a?(Hash)

  exts = []
  extensions.each do |ext_name, ext_prop|
    raise TypeError, "#{ext_name} must contain a Ruby Hash" unless ext_prop.is_a?(Hash)
    raise ArgumentError, "keys in #{ext_name} must be 'values' and 'critical'" unless ext_prop.key?("values") && ext_prop.key?("critical")
    raise TypeError, "the key 'values' must contain a Ruby Arrays" unless ext_prop["values"].is_a?(Array)
    raise TypeError, "the key 'critical' must be a Ruby Boolean true/false" unless ext_prop["critical"].is_a?(TrueClass) || ext_prop["critical"].is_a?(FalseClass)

    exts << ::OpenSSL::X509::ExtensionFactory.new.create_extension(ext_name, ext_prop["values"].join(","), ext_prop["critical"])
  end
  exts
end

#gen_x509_request(subject, key) ⇒ OpenSSL::X509::Request

generate a csr pem file given a subject and a private key

Parameters:

  • subject (OpenSSL::X509::Name)

    the x509 subject object

  • key (OpenSSL::PKey::EC, OpenSSL::PKey::RSA)

    the private key object

Returns:

  • (OpenSSL::X509::Request)

Raises:

  • (TypeError)


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/chef/mixin/openssl_helper.rb', line 206

def gen_x509_request(subject, key)
  raise TypeError, "subject must be a Ruby OpenSSL::X509::Name object" unless subject.is_a?(::OpenSSL::X509::Name)
  raise TypeError, "key must be a Ruby OpenSSL::PKey::EC or a Ruby OpenSSL::PKey::RSA object" unless key.is_a?(::OpenSSL::PKey::EC) || key.is_a?(::OpenSSL::PKey::RSA)

  request = ::OpenSSL::X509::Request.new
  request.version = 0
  request.subject = subject
  request.public_key = key

  # Chef 12 backward compatibility
  ::OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?)

  request.sign(key, ::OpenSSL::Digest.new("SHA256"))
  request
end

#get_key_filename(cert_filename) ⇒ String

determine the key filename from the cert filename

Parameters:

  • cert_filename (String)

    the path to the certfile

Returns:

  • (String)

    the path to the keyfile



26
27
28
29
30
# File 'lib/chef/mixin/openssl_helper.rb', line 26

def get_key_filename(cert_filename)
  cert_file_path, cert_filename = ::File.split(cert_filename)
  cert_filename = ::File.basename(cert_filename, ::File.extname(cert_filename))
  cert_file_path + ::File::SEPARATOR + cert_filename + ".key"
end

#get_next_crl_number(crl) ⇒ Integer

generate the next CRL number available for a X509 CRL given

Parameters:

  • crl (OpenSSL::X509::CRL)

    x509 CRL

Returns:

  • (Integer)

Raises:

  • (TypeError)


330
331
332
333
334
335
336
337
338
# File 'lib/chef/mixin/openssl_helper.rb', line 330

def get_next_crl_number(crl)
  raise TypeError, "crl must be a Ruby OpenSSL::X509::CRL object" unless crl.is_a?(::OpenSSL::X509::CRL)

  crlnum = 1
  crl.extensions.each do |e|
    crlnum = e.value if e.oid == "crlNumber"
  end
  crlnum.to_i + 1
end

#key_length_valid?(number) ⇒ Boolean

is the key length a valid key length

Parameters:

  • number (Integer)

Returns:

  • (Boolean)

    is length valid



35
36
37
# File 'lib/chef/mixin/openssl_helper.rb', line 35

def key_length_valid?(number)
  number >= 1024 && ( number & (number - 1) == 0 )
end

#priv_key_file_valid?(key_file, key_password = nil) ⇒ Boolean

given either a key file path or key file content see if it’s actually a private key

Parameters:

  • key_file (String)

    the path to the keyfile or the key contents

  • key_password (String) (defaults to: nil)

    optional password to the keyfile

Returns:

  • (Boolean)

    is the key valid?



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/chef/mixin/openssl_helper.rb', line 56

def priv_key_file_valid?(key_file, key_password = nil)
  # if the file exists try to read the content
  # if not assume we were passed the key and set the string to the content
  key_content = ::File.exist?(key_file) ? File.read(key_file) : key_file

  begin
    key = ::OpenSSL::PKey.read key_content, key_password
  rescue ::OpenSSL::PKey::PKeyError, ArgumentError
    return false
  end

  if key.is_a?(::OpenSSL::PKey::EC)
    key.private_key?
  else
    key.private?
  end
end

#renew_x509_crl(crl, ca_private_key, info) ⇒ OpenSSL::X509::CRL

renew a X509 crl given

Parameters:

  • crl (OpenSSL::X509::CRL)

    CRL to renew

  • ca_private_key (OpenSSL::PKey::EC, OpenSSL::PKey::RSA)

    private key from the CA

  • info (Hash)

    issuer & validity

Returns:

  • (OpenSSL::X509::CRL)

Raises:

  • (TypeError)


381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/chef/mixin/openssl_helper.rb', line 381

def renew_x509_crl(crl, ca_private_key, info)
  raise TypeError, "crl must be a Ruby OpenSSL::X509::CRL object" unless crl.is_a?(::OpenSSL::X509::CRL)
  raise TypeError, "ca_private_key must be a Ruby OpenSSL::PKey::EC object or a Ruby OpenSSL::PKey::RSA object" unless ca_private_key.is_a?(::OpenSSL::PKey::EC) || ca_private_key.is_a?(::OpenSSL::PKey::RSA)
  raise TypeError, "info must be a Ruby Hash" unless info.is_a?(Hash)

  raise ArgumentError, "info must contain a issuer and a validity" unless info.key?("issuer") && info.key?("validity")
  raise TypeError, "info['issuer'] must be a Ruby OpenSSL::X509::Certificate object" unless info["issuer"].is_a?(::OpenSSL::X509::Certificate)
  raise TypeError, "info['validity'] must be a Ruby Integer object" unless info["validity"].is_a?(Integer)

  crl.last_update = Time.now
  crl.next_update = crl.last_update + 3600 * 24 * info["validity"]

  ef = ::OpenSSL::X509::ExtensionFactory.new
  if openssl_config = __openssl_config
    ef.config = openssl_config
  end
  ef.issuer_certificate = info["issuer"]

  crl.extensions = [ ::OpenSSL::X509::Extension.new("crlNumber",
    ::OpenSSL::ASN1::Integer(get_next_crl_number(crl)))]
  crl.add_extension ef.create_extension("authorityKeyIdentifier",
    "keyid:always,issuer:always")
  crl.sign(ca_private_key, ::OpenSSL::Digest.new("SHA256"))
  crl
end

#revoke_x509_crl(revoke_info, crl, ca_private_key, info) ⇒ OpenSSL::X509::CRL

add a serial given in the crl given

Parameters:

  • revoke_info (Hash)

    serial to revoke & revocation reason

  • crl (OpenSSL::X509::CRL)

    X509 CRL

  • ca_private_key (OpenSSL::PKey::EC, OpenSSL::PKey::RSA)

    private key from the CA

  • info (Hash)

    issuer & validity

Returns:

  • (OpenSSL::X509::CRL)

Raises:

  • (TypeError)


346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/chef/mixin/openssl_helper.rb', line 346

def revoke_x509_crl(revoke_info, crl, ca_private_key, info)
  raise TypeError, "revoke_info must be a Ruby Hash object" unless revoke_info.is_a?(Hash)
  raise TypeError, "crl must be a Ruby OpenSSL::X509::CRL object" unless crl.is_a?(::OpenSSL::X509::CRL)
  raise TypeError, "ca_private_key must be a Ruby OpenSSL::PKey::EC object or a Ruby OpenSSL::PKey::RSA object" unless ca_private_key.is_a?(::OpenSSL::PKey::EC) || ca_private_key.is_a?(::OpenSSL::PKey::RSA)
  raise TypeError, "info must be a Ruby Hash" unless info.is_a?(Hash)

  raise ArgumentError, "revoke_info must contain a serial and a reason" unless revoke_info.key?("serial") && revoke_info.key?("reason")
  raise TypeError, "revoke_info['serial'] must be a Ruby String or Integer object" unless revoke_info["serial"].is_a?(String) || revoke_info["serial"].is_a?(Integer)
  raise TypeError, "revoke_info['reason'] must be a Ruby Integer object" unless revoke_info["reason"].is_a?(Integer)

  raise ArgumentError, "info must contain a issuer and a validity" unless info.key?("issuer") && info.key?("validity")
  raise TypeError, "info['issuer'] must be a Ruby OpenSSL::X509::Certificate object" unless info["issuer"].is_a?(::OpenSSL::X509::Certificate)
  raise TypeError, "info['validity'] must be a Ruby Integer object" unless info["validity"].is_a?(Integer)

  revoked = ::OpenSSL::X509::Revoked.new
  revoked.serial = if revoke_info["serial"].is_a?(String)
                     revoke_info["serial"].to_i(16)
                   else
                     revoke_info["serial"]
                   end
  revoked.time = Time.now

  ext = ::OpenSSL::X509::Extension.new("CRLReason",
    ::OpenSSL::ASN1::Enumerated(revoke_info["reason"]))
  revoked.add_extension(ext)
  crl.add_revoked(revoked)

  renew_x509_crl(crl, ca_private_key, info)
end

#serial_revoked?(crl, serial) ⇒ true, false

check is a serial given is revoked in a crl given

Parameters:

  • crl (OpenSSL::X509::CRL)

    X509 CRL to check

  • serial (String, Integer)

    X509 Certificate Serial Number

Returns:

  • (true, false)

Raises:

  • (TypeError)


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/chef/mixin/openssl_helper.rb', line 90

def serial_revoked?(crl, serial)
  raise TypeError, "crl must be a Ruby OpenSSL::X509::CRL object" unless crl.is_a?(::OpenSSL::X509::CRL)
  raise TypeError, "serial must be a Ruby String or Integer object" unless serial.is_a?(String) || serial.is_a?(Integer)

  serial_to_verify = if serial.is_a?(String)
                       serial.to_i(16)
                     else
                       serial
                     end
  status = false
  crl.revoked.each do |revoked|
    status = true if revoked.serial == serial_to_verify
  end
  status
end