Skip to content

Commit

Permalink
Merge branch 'openssl-3'.
Browse files Browse the repository at this point in the history
  • Loading branch information
philr committed Oct 15, 2022
2 parents e3947c8 + 3850439 commit 0585f7a
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 50 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ jobs:
name_suffix: ['']
experimental: [false]
include:
- os: ubuntu-22.04
ruby: '3.1'
name_suffix: ''
experimental: false
- os: ubuntu-20.04
ruby: head
name_suffix: ' (experimental)'
Expand All @@ -20,6 +24,10 @@ jobs:
ruby: jruby-head
name_suffix: ' (experimental)'
experimental: true
- os: ubuntu-22.04
ruby: head
name_suffix: ' (experimental)'
experimental: true
- os: windows-2019
ruby: head
name_suffix: ' (experimental)'
Expand Down Expand Up @@ -60,6 +68,8 @@ jobs:
- run: ruby --version
- run: gem --version
- run: bundle --version
- name: OpenSSL Version
run: ruby -ropenssl -e'puts OpenSSL::OPENSSL_VERSION'
- run: bundle exec rake test
env:
TESTOPTS: --verbose
Expand Down
306 changes: 260 additions & 46 deletions lib/putty/key/openssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,266 @@ module OpenSSL

private_constant :SSH_CURVES

# OpenSSL version helper methods.
#
# @private
module Version
class << self
# Determines if the Ruby OpenSSL wrapper is using the OpenSSL library
# (not LibreSSL and not JRuby) and if the version matches the required
# version.
#
# @param major [Integer] The required major version. `nil` if any
# version of OpenSSL is sufficient.
# @param minor [Integer] The required minor version.
# @param fix [Integer] The required fix version.
# @param patch [Integer] The required patch version.
# @return [Boolean] `true` if the requirements are met, otherwise
# `false`.
def openssl?(major = nil, minor = 0, fix = 0, patch = 0)
return false if ::OpenSSL::OPENSSL_VERSION.include?('LibreSSL')
return false if ::OpenSSL::OPENSSL_VERSION.include?('JRuby')
return true unless major
required_version = major * 0x10000000 + minor * 0x100000 + fix * 0x1000 + patch * 0x10
::OpenSSL::OPENSSL_VERSION_NUMBER >= required_version
end
end
end
private_constant :Version

# Methods to build OpenSSL private keys from a {PPK}.
#
# @private
module PKeyBuilding
class << self
# Creates a new OpenSSL DSA private key for the given DSA {PPK}.
#
# @param ppk [PPK] A DSA {PPK}.
# @return [::OpenSSL::PKey::DSA] The OpenSSL DSA private key.
def ppk_to_dsa(ppk)
_, p, q, g, pub_key = Util.ssh_unpack(ppk.public_blob, :string, :mpint, :mpint, :mpint, :mpint)
priv_key = Util.ssh_unpack(ppk.private_blob, :mpint).first
dsa_from_params(p, q, g, pub_key, priv_key)
end

# Creates a new OpenSSL RSA private key for the given RSA {PPK}.
#
# @param ppk [PPK] An RSA {PPK}.
# @return [::OpenSSL::PKey::RSA] The OpenSSL RSA private key.
def ppk_to_rsa(ppk)
_, e, n = Util.ssh_unpack(ppk.public_blob, :string, :mpint, :mpint)
d, p, q, iqmp = Util.ssh_unpack(ppk.private_blob, :mpint, :mpint, :mpint, :mpint)
dmp1 = d % (p - 1)
dmq1 = d % (q - 1)
rsa_from_params(e, n, d, p, q, iqmp, dmp1, dmq1)
end

# Creates a new OpenSSL EC private key for the given EC {PPK}.
#
# @param ppk [PPK] An EC {PPK}.
# @param ppk_curve [String] The PPK curve name extracted from the
# PPK algorithm name.
# @return [::OpenSSL::PKey::EC] The OpenSSL EC private key.
def ppk_to_ec(ppk, ppk_curve)
curve = OPENSSL_CURVES[ppk_curve]
_, _, pub_key = Util.ssh_unpack(ppk.public_blob, :string, :string, :mpint)
priv_key = Util.ssh_unpack(ppk.private_blob, :mpint).first
ec_from_params(curve, pub_key, priv_key)
end

private

if Version.openssl?(3)
# OpenSSL v3 private keys are immutable. The Ruby OpenSSL wrapper
# doesn't provide a method to construct private keys using the
# parameters. Build DER (ASN.1) encoded versions of the keys.
#
# In theory this should be usable universally. However
# ::OpenSSL::PKey::EC::Point#to_octet_string is only supported from
# Ruby >= 2.4 and there are issues with JRuby's OpenSSL library
# (that doesn't make use of OpenSSL).

# :nocov_no_openssl3:

# Creates a new OpenSSL DSA private key with the given parameters.
#
# @param p [::OpenSSL::BN] The p parameter (prime).
# @param q [::OpenSSL::BN] The q parameter (prime).
# @param g [::OpenSSL::BN] The g parameter.
# @param pub_key [::OpenSSL::BN] The public key.
# @param priv_key [::OpenSSL::BN] The private key.
# @return [::OpenSSL::PKey::DSA] The OpenSSL DSA private key.
def dsa_from_params(p, q, g, pub_key, priv_key)
# https://www.openssl.org/docs/man3.0/man1/openssl-dsa.html (outform parameter).
sequence = [
::OpenSSL::ASN1::Integer.new(0),
::OpenSSL::ASN1::Integer.new(p),
::OpenSSL::ASN1::Integer.new(q),
::OpenSSL::ASN1::Integer.new(g),
::OpenSSL::ASN1::Integer.new(pub_key),
::OpenSSL::ASN1::Integer.new(priv_key)
]

::OpenSSL::PKey::DSA.new(::OpenSSL::ASN1::Sequence.new(sequence).to_der)
end

# Creates a new OpenSSL RSA private key with the given parameters.
#
# @param e [::OpenSSL::BN] The public key exponent.
# @param n [::OpenSSL::BN] The modulus.
# @param d [::OpenSSL::BN] The private key exponent.
# @param p [::OpenSSL::BN] The p prime.
# @param q [::OpenSSL::BN] The q prime.
# @param iqmp [::OpenSSL::BN] The inverse of q, mod p.
# @param dmp1 [::OpenSSL::BN] `d` mod (`p` - 1).
# @param dmq1 [::OpenSSL::BN] `d` mod (`q` - 1).
# @return [::OpenSSL::PKey::RSA] The OpenSSL RSA private key.
def rsa_from_params(e, n, d, p, q, iqmp, dmp1, dmq1)
# RFC 3447 Appendix A.1.2
sequence = [
::OpenSSL::ASN1::Integer.new(0),
::OpenSSL::ASN1::Integer.new(n),
::OpenSSL::ASN1::Integer.new(e),
::OpenSSL::ASN1::Integer.new(d),
::OpenSSL::ASN1::Integer.new(p),
::OpenSSL::ASN1::Integer.new(q),
::OpenSSL::ASN1::Integer.new(dmp1),
::OpenSSL::ASN1::Integer.new(dmq1),
::OpenSSL::ASN1::Integer.new(iqmp)
]

::OpenSSL::PKey::RSA.new(::OpenSSL::ASN1::Sequence.new(sequence).to_der)
end

# Creates a new OpenSSL EC private key with the given parameters.
#
# @param curve [String] The name of the OpenSSL EC curve.
# @param pub_key [::OpenSSL::BN] The public key.
# @param priv_key [::OpenSSL::BN] The private key.
# @return [::OpenSSL::PKey::EC] The OpenSSL EC private key.
def ec_from_params(curve, pub_key, priv_key)
group = ::OpenSSL::PKey::EC::Group.new(curve)
point = ::OpenSSL::PKey::EC::Point.new(group, pub_key)
point_string = point.to_octet_string(:uncompressed)

# RFC 5915 Section 3
sequence = [
::OpenSSL::ASN1::Integer.new(1),
::OpenSSL::ASN1::OctetString.new(priv_key.to_s(2)),
::OpenSSL::ASN1::ObjectId.new(curve, 0, :EXPLICIT),
::OpenSSL::ASN1::BitString.new(point_string, 1, :EXPLICIT)
]

::OpenSSL::PKey::EC.new(::OpenSSL::ASN1::Sequence.new(sequence).to_der)
end
# :nocov_no_openssl3:
else
# :nocov_openssl3:
if ::OpenSSL::PKey::DSA.new.respond_to?(:set_key)
# :nocov_no_openssl_pkey_dsa_set_key:

# Creates a new OpenSSL DSA private key with the given parameters.
#
# @param p [::OpenSSL::BN] The p parameter.
# @param q [::OpenSSL::BN] The q parameter.
# @param g [::OpenSSL::BN] The g parameter.
# @param pub_key [::OpenSSL::BN] The public key.
# @param priv_key [::OpenSSL::BN] The private key.
# @return [::OpenSSL::PKey::DSA] The OpenSSL DSA private key.
def dsa_from_params(p, q, g, pub_key, priv_key)
::OpenSSL::PKey::DSA.new.tap do |pkey|
pkey.set_key(pub_key, priv_key)
pkey.set_pqg(p, q, g)
end
end
# :nocov_no_openssl_pkey_dsa_set_key:
else
# :nocov_openssl_pkey_dsa_set_key:
# Creates a new OpenSSL DSA private key with the given parameters.
#
# @param p [::OpenSSL::BN] The p parameter.
# @param q [::OpenSSL::BN] The q parameter.
# @param g [::OpenSSL::BN] The g parameter.
# @param pub_key [::OpenSSL::BN] The public key.
# @param priv_key [::OpenSSL::BN] The private key.
# @return [::OpenSSL::PKey::DSA] The OpenSSL DSA private key.
def dsa_from_params(p, q, g, pub_key, priv_key)
::OpenSSL::PKey::DSA.new.tap do |pkey|
pkey.p, pkey.q, pkey.g, pkey.pub_key, pkey.priv_key = p, q, g, pub_key, priv_key
end
end
# :nocov_openssl_pkey_dsa_set_key:
end

if ::OpenSSL::PKey::RSA.new.respond_to?(:set_factors)
# :nocov_no_openssl_pkey_rsa_set_factors:

# Creates a new OpenSSL RSA private key with the given parameters.
#
# @param e [::OpenSSL::BN] The public key exponent.
# @param n [::OpenSSL::BN] The modulus.
# @param d [::OpenSSL::BN] The private key exponent.
# @param p [::OpenSSL::BN] The p prime.
# @param q [::OpenSSL::BN] The q prime.
# @param iqmp [::OpenSSL::BN] The inverse of q, mod p.
# @param dmp1 [::OpenSSL::BN] `d` mod (`p` - 1).
# @param dmq1 [::OpenSSL::BN] `d` mod (`q` - 1).
# @return [::OpenSSL::PKey::RSA] The OpenSSL RSA private key.
def rsa_from_params(e, n, d, p, q, iqmp, dmp1, dmq1)
::OpenSSL::PKey::RSA.new.tap do |pkey|
pkey.set_factors(p, q)
pkey.set_key(n, e, d)
pkey.set_crt_params(dmp1, dmq1, iqmp)
end
end
# :nocov_no_openssl_pkey_rsa_set_factors:
else
# :nocov_openssl_pkey_rsa_set_factors:

# Creates a new OpenSSL RSA private key with the given parameters.
#
# @param e [::OpenSSL::BN] The public key exponent.
# @param n [::OpenSSL::BN] The modulus.
# @param d [::OpenSSL::BN] The private key exponent.
# @param p [::OpenSSL::BN] The p prime.
# @param q [::OpenSSL::BN] The q prime.
# @param iqmp [::OpenSSL::BN] The inverse of q, mod p.
# @param dmp1 [::OpenSSL::BN] `d` mod (`p` - 1).
# @param dmq1 [::OpenSSL::BN] `d` mod (`q` - 1).
# @return [::OpenSSL::PKey::RSA] The OpenSSL RSA private key.
def rsa_from_params(e, n, d, p, q, iqmp, dmp1, dmq1)
::OpenSSL::PKey::RSA.new.tap do |pkey|
pkey.e, pkey.n, pkey.d, pkey.p, pkey.q, pkey.iqmp, pkey.dmp1, pkey.dmq1 = e, n, d, p, q, iqmp, dmp1, dmq1
end
end
# :nocov_openssl_pkey_rsa_set_factors:
end

# Creates a new OpenSSL EC private key with the given parameters.
#
# @param curve [String] The name of the OpenSSL EC curve.
# @param pub_key [::OpenSSL::BN] The public key.
# @param priv_key [::OpenSSL::BN] The private key.
# @return [::OpenSSL::PKey::EC] The OpenSSL EC private key.
def ec_from_params(curve, pub_key, priv_key)
# Old versions of jruby-openssl don't include an EC class (version 0.9.16).
ec_class = (::OpenSSL::PKey::EC rescue raise ArgumentError, "Unsupported algorithm: #{ppk.algorithm}")

ec_class.new(curve).tap do |pkey|
group = pkey.group || ::OpenSSL::PKey::EC::Group.new(curve)
pkey.public_key = ::OpenSSL::PKey::EC::Point.new(group, pub_key)
pkey.private_key = priv_key
end
end
# :nocov_openssl3:
end
end
end
private_constant :PKeyBuilding

# The {ClassMethods} module is used to extend `OpenSSL::PKey` when
# using the PuTTY::Key refinement or calling {PuTTY::Key.global_install}.
# This adds a `from_ppk` class method to `OpenSSL::PKey`.
#
module ClassMethods
# Creates a new `OpenSSL::PKey` from a PuTTY private key (instance of
# {PPK}).
Expand All @@ -50,53 +306,11 @@ def from_ppk(ppk)

case ppk.algorithm
when 'ssh-dss'
::OpenSSL::PKey::DSA.new.tap do |pkey|
_, p, q, g, pub_key = Util.ssh_unpack(ppk.public_blob, :string, :mpint, :mpint, :mpint, :mpint)
priv_key = Util.ssh_unpack(ppk.private_blob, :mpint).first

if pkey.respond_to?(:set_key)
# :nocov_no_openssl_pkey_dsa_set_key:
pkey.set_key(pub_key, priv_key)
pkey.set_pqg(p, q, g)
# :nocov_no_openssl_pkey_dsa_set_key:
else
# :nocov_openssl_pkey_dsa_set_key:
pkey.p, pkey.q, pkey.g, pkey.pub_key, pkey.priv_key = p, q, g, pub_key, priv_key
# :nocov_openssl_pkey_dsa_set_key:
end
end
PKeyBuilding.ppk_to_dsa(ppk)
when 'ssh-rsa'
::OpenSSL::PKey::RSA.new.tap do |pkey|
_, e, n = Util.ssh_unpack(ppk.public_blob, :string, :mpint, :mpint)
d, p, q, iqmp = Util.ssh_unpack(ppk.private_blob, :mpint, :mpint, :mpint, :mpint)

dmp1 = d % (p - 1)
dmq1 = d % (q - 1)

if pkey.respond_to?(:set_factors)
# :nocov_no_openssl_pkey_rsa_set_factors:
pkey.set_factors(p, q)
pkey.set_key(n, e, d)
pkey.set_crt_params(dmp1, dmq1, iqmp)
# :nocov_no_openssl_pkey_rsa_set_factors:
else
# :nocov_openssl_pkey_rsa_set_factors:
pkey.e, pkey.n, pkey.d, pkey.p, pkey.q, pkey.iqmp, pkey.dmp1, pkey.dmq1 = e, n, d, p, q, iqmp, dmp1, dmq1
# :nocov_openssl_pkey_rsa_set_factors:
end
end
PKeyBuilding.ppk_to_rsa(ppk)
when /\Aecdsa-sha2-(nistp(?:256|384|521))\z/
curve = OPENSSL_CURVES[$1]

# Old versions of jruby-openssl don't include an EC class (version 0.9.16).
ec_class = (::OpenSSL::PKey::EC rescue raise ArgumentError, "Unsupported algorithm: #{ppk.algorithm}")

ec_class.new(curve).tap do |pkey|
_, _, point = Util.ssh_unpack(ppk.public_blob, :string, :string, :mpint)
group = pkey.group || ::OpenSSL::PKey::EC::Group.new(curve)
pkey.public_key = ::OpenSSL::PKey::EC::Point.new(group, point)
pkey.private_key = Util.ssh_unpack(ppk.private_blob, :mpint).first
end
PKeyBuilding.ppk_to_ec(ppk, $1)
else
raise ArgumentError, "Unsupported algorithm: #{ppk.algorithm}"
end
Expand Down
14 changes: 13 additions & 1 deletion lib/putty/key/ppk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,19 @@ def get_argon2_type(key_derivation)
def derive_keys(format, cipher = nil, passphrase = nil, argon2_params = nil)
if format >= 3
return derive_format_3_keys(cipher, passphrase, argon2_params) if cipher
return [''.b, nil, nil, nil]

# An empty string should work for the MAC, but ::OpenSSL::HMAC fails
# when used with OpenSSL 3:
#
# EVP_PKEY_new_mac_key: malloc failure (OpenSSL::HMACError).
#
# See https://github.com/ruby/openssl/pull/538 and
# https://github.com/openssl/openssl/issues/13089.
#
# Ruby 3.1.3 should contain the workaround from ruby/openssl PR 538.
#
# Use "\0" as the MAC key for a workaround for Ruby < 3.1.3.
return ["\0".b, nil, nil, nil]
end

mac_key = derive_format_2_mac_key(passphrase)
Expand Down
Loading

0 comments on commit 0585f7a

Please sign in to comment.