Skip to content

Commit 4b24ac3

Browse files
authored
Merge pull request #732 from johnnyshields/2.x-cert-pkey-objects
[READY] v2.x - Allow SP certificates to be OpenSSL::X509::Certificate / private_key to be OpenSSL::PKey::PKey
2 parents 4ce10b3 + 50a2e89 commit 4b24ac3

File tree

5 files changed

+154
-7
lines changed

5 files changed

+154
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* [#715](https://github.com/SAML-Toolkits/ruby-saml/pull/715) Fix typo in error when SPNameQualifier value does not match the SP entityID.
1515
* [#718](https://github.com/SAML-Toolkits/ruby-saml/pull/718) Add support to retrieve from SAMLResponse the AuthnInstant and AuthnContextClassRef values
1616
* [#711](https://github.com/SAML-Toolkits/ruby-saml/pull/711) Standardize how RubySaml reads and formats certificate and private_key PEM values, including the `RubySaml::Util#format_cert` and `#format_private_key` methods.
17+
* [#732](https://github.com/SAML-Toolkits/ruby-saml/pull/732) Return original certificate and private key objects from `RubySaml::Utils` build functions.
18+
* [#732](https://github.com/SAML-Toolkits/ruby-saml/pull/732) Allow SP `certificate`, `certificate_new`, and `private_key` settings to be `OpenSSL::X509::Certificate` and `OpenSSL::PKey::PKey` objects respectively.
1719
* [#733](https://github.com/SAML-Toolkits/ruby-saml/pull/733) Allow `RubySaml::Utils.is_cert_expired` and `is_cert_active` to accept an optional time argument.
1820
* [#731](https://github.com/SAML-Toolkits/ruby-saml/pull/731) Add CI coverage for Ruby 3.4. Remove CI coverage for Ruby 1.x and 2.x.
1921

lib/ruby_saml/settings.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,24 @@ def compress_deprecation(old_param, new_param)
363363
'"HTTP-Redirect" will always be compressed, and "HTTP-POST" will always be uncompressed.'
364364
end
365365

366+
# Quick check if a certificate value is present as a string or OpenSSL::X509::Certificate.
367+
# Does not check if the string can actually be parsed.
368+
#
369+
# @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
370+
# @return [true|false] Whether the certificate value is present.
371+
def cert?(cert)
372+
!!cert && (cert.is_a?(OpenSSL::X509::Certificate) || !cert.empty?)
373+
end
374+
375+
# Quick check if a private key value is present as a string or OpenSSL::PKey::PKey.
376+
# Does not check if the string can actually be parsed.
377+
#
378+
# @param private_key [OpenSSL::PKey::PKey|String] The private key.
379+
# @return [true|false] Whether the private key value is present.
380+
def private_key?(private_key)
381+
!!private_key && (private_key.is_a?(OpenSSL::PKey::PKey) || !private_key.empty?)
382+
end
383+
366384
# @return [Hash<Symbol, Array<Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>>>]
367385
# Build the SP certificates and private keys from the settings. Returns all
368386
# certificates and private keys, even if they are expired.
@@ -373,11 +391,8 @@ def get_all_sp_certs
373391

374392
# Validate certificate, certificate_new, private_key, and sp_cert_multi params.
375393
def validate_sp_certs_params!
376-
multi = sp_cert_multi && !sp_cert_multi.empty?
377-
cert = certificate && !certificate.empty?
378-
cert_new = certificate_new && !certificate_new.empty?
379-
pk = private_key && !private_key.empty?
380-
if multi && (cert || cert_new || pk)
394+
has_multi = sp_cert_multi && !sp_cert_multi.empty?
395+
if has_multi && (cert?(certificate) || cert?(certificate_new) || private_key?(private_key))
381396
raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters")
382397
end
383398
end

lib/ruby_saml/utils.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ module Utils # rubocop:disable Metrics/ModuleLength
3939
# @return [true|false] Whether the certificate is expired.
4040
def is_cert_expired(cert, now = Time.now)
4141
now = Time.at(now) if now.is_a?(Integer)
42-
cert = build_cert_object(cert) if cert.is_a?(String)
42+
cert = build_cert_object(cert)
4343
cert.not_after < now
4444
end
4545

@@ -50,7 +50,7 @@ def is_cert_expired(cert, now = Time.now)
5050
# @return [true|false] Whether the certificate is currently active.
5151
def is_cert_active(cert, now = Time.now)
5252
now = Time.at(now) if now.is_a?(Integer)
53-
cert = build_cert_object(cert) if cert.is_a?(String)
53+
cert = build_cert_object(cert)
5454
cert.not_before <= now && cert.not_after >= now
5555
end
5656

@@ -119,6 +119,7 @@ def format_private_key(key, multi: false)
119119
# @param pem [String] The original certificate
120120
# @return [OpenSSL::X509::Certificate] The certificate object
121121
def build_cert_object(pem)
122+
return pem if pem.is_a?(OpenSSL::X509::Certificate)
122123
return unless (pem = PemFormatter.format_cert(pem, multi: false))
123124

124125
OpenSSL::X509::Certificate.new(pem)
@@ -129,6 +130,7 @@ def build_cert_object(pem)
129130
# @param pem [String] The original private key.
130131
# @return [OpenSSL::PKey::PKey] The private key object.
131132
def build_private_key_object(pem)
133+
return pem if pem.is_a?(OpenSSL::PKey::PKey)
132134
return unless (pem = PemFormatter.format_private_key(pem, multi: false))
133135

134136
error = nil

test/settings_test.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,78 @@ class SettingsTest < Minitest::Test
515515
end
516516
end
517517

518+
it 'handles OpenSSL::X509::Certificate objects for single case' do
519+
@settings.certificate = OpenSSL::X509::Certificate.new(cert_text1)
520+
@settings.private_key = key_text1
521+
522+
actual = @settings.get_sp_certs
523+
expected = [[cert_text1, key_text1]]
524+
assert_equal [:signing, :encryption], actual.keys
525+
assert_equal expected, actual[:signing].map { |ary| ary.map(&:to_pem) }
526+
assert_equal expected, actual[:encryption].map { |ary| ary.map(&:to_pem) }
527+
end
528+
529+
it 'handles OpenSSL::X509::Certificate objects for single case with new cert' do
530+
@settings.certificate = cert_text1
531+
@settings.certificate_new = OpenSSL::X509::Certificate.new(cert_text2)
532+
@settings.private_key = key_text1
533+
534+
actual = @settings.get_sp_certs
535+
expected = [[cert_text1, key_text1], [cert_text2, key_text1]]
536+
assert_equal [:signing, :encryption], actual.keys
537+
assert_equal expected, actual[:signing].map { |ary| ary.map(&:to_pem) }
538+
assert_equal expected, actual[:encryption].map { |ary| ary.map(&:to_pem) }
539+
end
540+
541+
it 'handles OpenSSL::X509::Certificate objects for multi case' do
542+
x509_certificate1 = OpenSSL::X509::Certificate.new(cert_text1)
543+
x509_certificate2 = OpenSSL::X509::Certificate.new(cert_text2)
544+
@settings.sp_cert_multi = {
545+
signing: [{ certificate: x509_certificate1, private_key: key_text1 },
546+
{ certificate: cert_text2, private_key: key_text1 }],
547+
encryption: [{ certificate: x509_certificate2, private_key: key_text1 },
548+
{ certificate: cert_text3, private_key: key_text2 }]
549+
}
550+
551+
actual = @settings.get_sp_certs
552+
expected_signing = [[cert_text1, key_text1], [cert_text2, key_text1]]
553+
expected_encryption = [[cert_text2, key_text1], [cert_text3, key_text2]]
554+
assert_equal [:signing, :encryption], actual.keys
555+
assert_equal expected_signing, actual[:signing].map { |ary| ary.map(&:to_pem) }
556+
assert_equal expected_encryption, actual[:encryption].map { |ary| ary.map(&:to_pem) }
557+
end
558+
559+
560+
it 'handles OpenSSL::PKey::PKey objects for single case' do
561+
@settings.certificate = cert_text1
562+
@settings.private_key = OpenSSL::PKey::RSA.new(key_text1)
563+
564+
actual = @settings.get_sp_certs
565+
expected = [[cert_text1, key_text1]]
566+
assert_equal [:signing, :encryption], actual.keys
567+
assert_equal expected, actual[:signing].map { |ary| ary.map(&:to_pem) }
568+
assert_equal expected, actual[:encryption].map { |ary| ary.map(&:to_pem) }
569+
end
570+
571+
it 'handles OpenSSL::PKey::PKey objects for multi case' do
572+
pkey2 = OpenSSL::PKey::RSA.new(key_text2)
573+
pkey3 = CertificateHelper.generate_private_key(:dsa)
574+
pkey4 = CertificateHelper.generate_private_key(:ecdsa)
575+
@settings.sp_cert_multi = {
576+
signing: [{ certificate: cert_text1, private_key: pkey3 },
577+
{ certificate: cert_text2, private_key: pkey4 }],
578+
encryption: [{ certificate: cert_text2, private_key: key_text1 },
579+
{ certificate: cert_text3, private_key: pkey2 }]
580+
}
581+
582+
actual = @settings.get_sp_certs
583+
expected_signing = [[cert_text1, pkey3.to_pem], [cert_text2, pkey4.to_pem]]
584+
expected_encryption = [[cert_text2, key_text1], [cert_text3, key_text2]]
585+
assert_equal [:signing, :encryption], actual.keys
586+
assert_equal expected_signing, actual[:signing].map { |ary| ary.map(&:to_pem) }
587+
assert_equal expected_encryption, actual[:encryption].map { |ary| ary.map(&:to_pem) }
588+
end
589+
518590
it "sp_cert_multi allows sending only signing" do
519591
@settings.sp_cert_multi = {
520592
signing: [{ certificate: cert_text1, private_key: key_text1 },
@@ -920,5 +992,49 @@ class SettingsTest < Minitest::Test
920992
end
921993
end
922994
end
995+
996+
describe '#cert?' do
997+
it 'returns true for a valid OpenSSL::X509::Certificate object' do
998+
cert = CertificateHelper.generate_cert
999+
assert @settings.send(:cert?, cert)
1000+
end
1001+
1002+
it 'returns true for a non-empty certificate string' do
1003+
cert_string = '-----BEGIN CERTIFICATE-----\nVALID_CERTIFICATE\n-----END CERTIFICATE-----'
1004+
assert @settings.send(:cert?, cert_string)
1005+
end
1006+
1007+
it 'returns false for an empty certificate string' do
1008+
cert_string = ''
1009+
refute @settings.send(:cert?, cert_string)
1010+
end
1011+
1012+
it 'returns false for nil' do
1013+
cert = nil
1014+
refute @settings.send(:cert?, cert)
1015+
end
1016+
end
1017+
1018+
describe '#private_key?' do
1019+
it 'returns true for a valid OpenSSL::PKey::PKey object' do
1020+
private_key = CertificateHelper.generate_private_key
1021+
assert @settings.send(:private_key?, private_key)
1022+
end
1023+
1024+
it 'returns true for a non-empty private key string' do
1025+
private_key_string = '-----BEGIN PRIVATE KEY-----\nVALID_PRIVATE_KEY\n-----END PRIVATE KEY-----'
1026+
assert @settings.send(:private_key?, private_key_string)
1027+
end
1028+
1029+
it 'returns false for an empty private key string' do
1030+
private_key_string = ''
1031+
refute @settings.send(:private_key?, private_key_string)
1032+
end
1033+
1034+
it 'returns false for nil' do
1035+
private_key = nil
1036+
refute @settings.send(:private_key?, private_key)
1037+
end
1038+
end
9231039
end
9241040
end

test/utils_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ def result(duration, reference = 0)
156156
end
157157
end
158158

159+
it 'returns the original certificate when an OpenSSL::X509::Certificate is given' do
160+
certificate = OpenSSL::X509::Certificate.new
161+
assert_same certificate, RubySaml::Utils.build_cert_object(certificate)
162+
end
163+
159164
it 'returns nil for nil certificate string' do
160165
assert_nil RubySaml::Utils.build_cert_object(nil)
161166
end
@@ -180,6 +185,13 @@ def result(duration, reference = 0)
180185
end
181186
end
182187

188+
[OpenSSL::PKey::RSA, OpenSSL::PKey::DSA, OpenSSL::PKey::EC].each do |key_class|
189+
it 'returns the original private key when an instance of OpenSSL::PKey::PKey is given' do
190+
private_key = key_class.new
191+
assert_same private_key, RubySaml::Utils.build_private_key_object(private_key)
192+
end
193+
end
194+
183195
it 'returns nil for nil private key string' do
184196
assert_nil RubySaml::Utils.build_private_key_object(nil)
185197
end

0 commit comments

Comments
 (0)