Skip to content

Commit

Permalink
(PUP-11439) Support Ed25519 keys/certificates
Browse files Browse the repository at this point in the history
The generic interface usage was added by 78712fe, which improved key format support.
ruby-openssl 3.0, shipped in Ruby 3.0, supports Ed25519 keys using the generic interface and returns a OpenSSL::PKey::PKey.
The only thing preventing these from working is a simple type check. Update it to only check key types that aren't supported.
  • Loading branch information
tambry authored and Raul Tambre committed Mar 13, 2024
1 parent f5906a2 commit 0988068
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 17 deletions.
8 changes: 5 additions & 3 deletions lib/puppet/ssl/ssl_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def create_system_context(cacerts:, path: Puppet[:ssl_trust_store], include_clie
#
# @param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs
# @param crls [Array<OpenSSL::X509::CRL>] Array of CRLs
# @param private_key [OpenSSL::PKey::RSA, OpenSSL::PKey::EC] client's private key
# @param private_key [OpenSSL::PKey::PKey] client's private key
# @param client_cert [OpenSSL::X509::Certificate] client's cert whose public
# key matches the `private_key`
# @param revocation [:chain, :leaf, false] revocation mode
Expand Down Expand Up @@ -199,7 +199,7 @@ def load_context(certname: Puppet[:certname], revocation: Puppet[:certificate_re
# of the private key, and that it hasn't been tampered with since.
#
# @param csr [OpenSSL::X509::Request] certificate signing request
# @param public_key [OpenSSL::PKey::RSA, OpenSSL::PKey::EC] public key
# @param public_key [OpenSSL::PKey::PKey] public key
# @raise [Puppet::SSL:SSLError] The private_key for the given `public_key` was
# not used to sign the CSR.
# @api private
Expand Down Expand Up @@ -281,7 +281,9 @@ def revocation_mode(mode)
def resolve_client_chain(store, client_cert, private_key)
client_chain = verify_cert_with_store(store, client_cert)

if !private_key.is_a?(OpenSSL::PKey::RSA) && !private_key.is_a?(OpenSSL::PKey::EC)
if !private_key.is_a?(OpenSSL::PKey::RSA) && \
!private_key.is_a?(OpenSSL::PKey::EC) && \
!(private_key.is_a?(OpenSSL::PKey::PKey) && private_key.respond_to?(:oid) && private_key.oid == 'ED25519')
raise Puppet::SSL::SSLError, _("Unsupported key '%{type}'") % { type: private_key.class.name }
end

Expand Down
6 changes: 3 additions & 3 deletions lib/puppet/x509/cert_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def ca_last_update=(time)
# historical reasons, names are case insensitive.
#
# @param name [String] The private key identity
# @param key [OpenSSL::PKey::RSA] private key
# @param key [OpenSSL::PKey::PKey] private key
# @param password [String, nil] If non-nil, derive an encryption key
# from the password, and use that to encrypt the private key. If nil,
# save the private key unencrypted.
Expand Down Expand Up @@ -227,7 +227,7 @@ def load_private_key(name, required: false, password: nil)
# @param password [String, nil] If the private key is encrypted, decrypt
# it using the password. If the key is encrypted, but a password is
# not specified, then the key cannot be loaded.
# @return [OpenSSL::PKey::RSA, OpenSSL::PKey::EC] The private key
# @return [OpenSSL::PKey::PKey] The private key
# @raise [OpenSSL::PKey::PKeyError] The `pem` text does not contain a valid key
#
# @api private
Expand Down Expand Up @@ -299,7 +299,7 @@ def load_client_cert_from_pem(pem)
# Create a certificate signing request (CSR).
#
# @param name [String] the request identity
# @param private_key [OpenSSL::PKey::RSA] private key
# @param private_key [OpenSSL::PKey::PKey] private key
# @return [Puppet::X509::Request] The request
#
# @api private
Expand Down
4 changes: 3 additions & 1 deletion spec/lib/puppet/test_ca.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ def generate(name, opts)
private

def build_cert(name, issuer, opts = {})
key = if opts[:key_type] == :ec
key = if opts[:key_type] == :ed25519
key = OpenSSL::PKey.generate('ed25519')
elsif opts[:key_type] == :ec
key = OpenSSL::PKey::EC.generate('prime256v1')
elsif opts[:reuse_key]
key = opts[:reuse_key]
Expand Down
26 changes: 26 additions & 0 deletions spec/unit/x509/cert_provider_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,32 @@ def expects_private_file(path)
}.to raise_error(OpenSSL::PKey::PKeyError, /(unknown|invalid) curve name|Could not parse PKey: (no start line|bad decrypt)/)
end
end

context 'using Ed25519', if: RUBY_VERSION.to_f >= 3 && OpenSSL::OPENSSL_VERSION_NUMBER > 0x10101000 do
it 'returns a generic key' do
expect(provider.load_private_key('ed25519-key')).to be_a(OpenSSL::PKey::PKey)
end

it 'returns a generic key from PKCS#8 format' do
expect(provider.load_private_key('ed25519-key-pk8')).to be_a(OpenSSL::PKey::PKey)
end

it 'returns a generic key from openssl format' do
expect(provider.load_private_key('ed25519-key-openssl')).to be_a(OpenSSL::PKey::PKey)
end

it 'decrypts a generic key using the password' do
pkey = provider.load_private_key('encrypted-ed25519-key', password: password)
expect(pkey).to be_a(OpenSSL::PKey::PKey)
end

it 'raises without a password' do
# password is 74695716c8b6
expect {
provider.load_private_key('encrypted-ed25519-key')
}.to raise_error(OpenSSL::PKey::PKeyError, /(unknown|invalid) curve name|Could not parse PKey: no start line/)
end
end
end

context 'certs' do
Expand Down
26 changes: 16 additions & 10 deletions tasks/generate_cert_fixtures.rake
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ task(:gen_cert_fixtures) do
end
end

def generate(type, inter)
# Create an EC key and cert, issued by "Test CA Subauthority"
cert = ca.create_cert(type, inter[:cert], inter[:private_key], key_type: :type)
save(dir, "#{type}.pem", cert[:cert])
save(dir, "#{type}-key.pem", cert[:private_key])

# Create an encrypted version of the above private key.
save(dir, "encrypted-#{type}-key.pem", cert[:private_key]) do |x509|
# private key password was chosen at random
x509.to_pem(OpenSSL::Cipher::AES.new(128, :CBC), '74695716c8b6')
end
end

# This task generates a PKI consisting of a root CA, intermediate CA and
# several leaf certs. A CRL is generated for each CA. The root CA CRL is
# empty, while the intermediate CA CRL contains the revoked cert's serial
Expand Down Expand Up @@ -125,16 +138,9 @@ task(:gen_cert_fixtures) do
save(dir, 'revoked.pem', revoked[:cert])
save(dir, 'revoked-key.pem', revoked[:private_key])

# Create an EC key and cert, issued by "Test CA Subauthority"
ec = ca.create_cert('ec', inter[:cert], inter[:private_key], key_type: :ec)
save(dir, 'ec.pem', ec[:cert])
save(dir, 'ec-key.pem', ec[:private_key])

# Create an encrypted version of the above private key for host "ec"
save(dir, 'encrypted-ec-key.pem', ec[:private_key]) do |x509|
# private key password was chosen at random
x509.to_pem(OpenSSL::Cipher::AES.new(128, :CBC), '74695716c8b6')
end
# Generate certificate and key sets for various algorithms.
generate('ec', inter)
generate('ed25519', inter)

# Update intermediate CRL now that we've revoked
save(dir, 'intermediate-crl.pem', inter_crl)
Expand Down

0 comments on commit 0988068

Please sign in to comment.