summaryrefslogtreecommitdiff
path: root/test/openssl/utils.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/openssl/utils.rb')
-rw-r--r--test/openssl/utils.rb411
1 files changed, 330 insertions, 81 deletions
diff --git a/test/openssl/utils.rb b/test/openssl/utils.rb
index c923705b86..7e6fe8b163 100644
--- a/test/openssl/utils.rb
+++ b/test/openssl/utils.rb
@@ -1,91 +1,55 @@
-require "openssl"
+# frozen_string_literal: true
+begin
+ require "openssl"
+rescue LoadError
+end
+
require "test/unit"
+require "core_assertions"
+require "tempfile"
+require "socket"
+
+if defined?(OpenSSL)
module OpenSSL::TestUtils
- TEST_KEY_RSA1024 = OpenSSL::PKey::RSA.new <<-_end_of_pem_
------BEGIN RSA PRIVATE KEY-----
-MIICXgIBAAKBgQDLwsSw1ECnPtT+PkOgHhcGA71nwC2/nL85VBGnRqDxOqjVh7Cx
-aKPERYHsk4BPCkE3brtThPWc9kjHEQQ7uf9Y1rbCz0layNqHyywQEVLFmp1cpIt/
-Q3geLv8ZD9pihowKJDyMDiN6ArYUmZczvW4976MU3+l54E6lF/JfFEU5hwIDAQAB
-AoGBAKSl/MQarye1yOysqX6P8fDFQt68VvtXkNmlSiKOGuzyho0M+UVSFcs6k1L0
-maDE25AMZUiGzuWHyaU55d7RXDgeskDMakD1v6ZejYtxJkSXbETOTLDwUWTn618T
-gnb17tU1jktUtU67xK/08i/XodlgnQhs6VoHTuCh3Hu77O6RAkEA7+gxqBuZR572
-74/akiW/SuXm0SXPEviyO1MuSRwtI87B02D0qgV8D1UHRm4AhMnJ8MCs1809kMQE
-JiQUCrp9mQJBANlt2ngBO14us6NnhuAseFDTBzCHXwUUu1YKHpMMmxpnGqaldGgX
-sOZB3lgJsT9VlGf3YGYdkLTNVbogQKlKpB8CQQDiSwkb4vyQfDe8/NpU5Not0fII
-8jsDUCb+opWUTMmfbxWRR3FBNu8wnym/m19N4fFj8LqYzHX4KY0oVPu6qvJxAkEA
-wa5snNekFcqONLIE4G5cosrIrb74sqL8GbGb+KuTAprzj5z1K8Bm0UW9lTjVDjDi
-qRYgZfZSL+x1P/54+xTFSwJAY1FxA/N3QPCXCjPh5YqFxAMQs2VVYTfg+t0MEcJD
-dPMQD5JX6g5HKnHFg2mZtoXQrWmJSn7p8GJK8yNTopEErA==
------END RSA PRIVATE KEY-----
- _end_of_pem_
-
- TEST_KEY_RSA2048 = OpenSSL::PKey::RSA.new <<-_end_of_pem_
------BEGIN RSA PRIVATE KEY-----
-MIIEpAIBAAKCAQEAuV9ht9J7k4NBs38jOXvvTKY9gW8nLICSno5EETR1cuF7i4pN
-s9I1QJGAFAX0BEO4KbzXmuOvfCpD3CU+Slp1enenfzq/t/e/1IRW0wkJUJUFQign
-4CtrkJL+P07yx18UjyPlBXb81ApEmAB5mrJVSrWmqbjs07JbuS4QQGGXLc+Su96D
-kYKmSNVjBiLxVVSpyZfAY3hD37d60uG+X8xdW5v68JkRFIhdGlb6JL8fllf/A/bl
-NwdJOhVr9mESHhwGjwfSeTDPfd8ZLE027E5lyAVX9KZYcU00mOX+fdxOSnGqS/8J
-DRh0EPHDL15RcJjV2J6vZjPb0rOYGDoMcH+94wIDAQABAoIBAAzsamqfYQAqwXTb
-I0CJtGg6msUgU7HVkOM+9d3hM2L791oGHV6xBAdpXW2H8LgvZHJ8eOeSghR8+dgq
-PIqAffo4x1Oma+FOg3A0fb0evyiACyrOk+EcBdbBeLo/LcvahBtqnDfiUMQTpy6V
-seSoFCwuN91TSCeGIsDpRjbG1vxZgtx+uI+oH5+ytqJOmfCksRDCkMglGkzyfcl0
-Xc5CUhIJ0my53xijEUQl19rtWdMnNnnkdbG8PT3LZlOta5Do86BElzUYka0C6dUc
-VsBDQ0Nup0P6rEQgy7tephHoRlUGTYamsajGJaAo1F3IQVIrRSuagi7+YpSpCqsW
-wORqorkCgYEA7RdX6MDVrbw7LePnhyuaqTiMK+055/R1TqhB1JvvxJ1CXk2rDL6G
-0TLHQ7oGofd5LYiemg4ZVtWdJe43BPZlVgT6lvL/iGo8JnrncB9Da6L7nrq/+Rvj
-XGjf1qODCK+LmreZWEsaLPURIoR/Ewwxb9J2zd0CaMjeTwafJo1CZvcCgYEAyCgb
-aqoWvUecX8VvARfuA593Lsi50t4MEArnOXXcd1RnXoZWhbx5rgO8/ATKfXr0BK/n
-h2GF9PfKzHFm/4V6e82OL7gu/kLy2u9bXN74vOvWFL5NOrOKPM7Kg+9I131kNYOw
-Ivnr/VtHE5s0dY7JChYWE1F3vArrOw3T00a4CXUCgYEA0SqY+dS2LvIzW4cHCe9k
-IQqsT0yYm5TFsUEr4sA3xcPfe4cV8sZb9k/QEGYb1+SWWZ+AHPV3UW5fl8kTbSNb
-v4ng8i8rVVQ0ANbJO9e5CUrepein2MPL0AkOATR8M7t7dGGpvYV0cFk8ZrFx0oId
-U0PgYDotF/iueBWlbsOM430CgYEAqYI95dFyPI5/AiSkY5queeb8+mQH62sdcCCr
-vd/w/CZA/K5sbAo4SoTj8dLk4evU6HtIa0DOP63y071eaxvRpTNqLUOgmLh+D6gS
-Cc7TfLuFrD+WDBatBd5jZ+SoHccVrLR/4L8jeodo5FPW05A+9gnKXEXsTxY4LOUC
-9bS4e1kCgYAqVXZh63JsMwoaxCYmQ66eJojKa47VNrOeIZDZvd2BPVf30glBOT41
-gBoDG3WMPZoQj9pb7uMcrnvs4APj2FIhMU8U15LcPAj59cD6S6rWnAxO8NFK7HQG
-4Jxg3JNNf8ErQoCHb1B3oVdXJkmbJkARoDpBKmTCgKtP8ADYLmVPQw==
------END RSA PRIVATE KEY-----
- _end_of_pem_
-
- TEST_KEY_DSA256 = OpenSSL::PKey::DSA.new <<-_end_of_pem_
------BEGIN DSA PRIVATE KEY-----
-MIH3AgEAAkEAhk2libbY2a8y2Pt21+YPYGZeW6wzaW2yfj5oiClXro9XMR7XWLkE
-9B7XxLNFCS2gmCCdMsMW1HulaHtLFQmB2wIVAM43JZrcgpu6ajZ01VkLc93gu/Ed
-AkAOhujZrrKV5CzBKutKLb0GVyVWmdC7InoNSMZEeGU72rT96IjM59YzoqmD0pGM
-3I1o4cGqg1D1DfM1rQlnN1eSAkBq6xXfEDwJ1mLNxF6q8Zm/ugFYWR5xcX/3wFiT
-b4+EjHP/DbNh9Vm5wcfnDBJ1zKvrMEf2xqngYdrV/3CiGJeKAhRvL57QvJZcQGvn
-ISNX5cMzFHRW3Q==
------END DSA PRIVATE KEY-----
- _end_of_pem_
-
- TEST_KEY_DSA512 = OpenSSL::PKey::DSA.new <<-_end_of_pem_
------BEGIN DSA PRIVATE KEY-----
-MIH4AgEAAkEA5lB4GvEwjrsMlGDqGsxrbqeFRh6o9OWt6FgTYiEEHaOYhkIxv0Ok
-RZPDNwOG997mDjBnvDJ1i56OmS3MbTnovwIVAJgub/aDrSDB4DZGH7UyarcaGy6D
-AkB9HdFw/3td8K4l1FZHv7TCZeJ3ZLb7dF3TWoGUP003RCqoji3/lHdKoVdTQNuR
-S/m6DlCwhjRjiQ/lBRgCLCcaAkEAjN891JBjzpMj4bWgsACmMggFf57DS0Ti+5++
-Q1VB8qkJN7rA7/2HrCR3gTsWNb1YhAsnFsoeRscC+LxXoXi9OAIUBG98h4tilg6S
-55jreJD3Se3slps=
------END DSA PRIVATE KEY-----
- _end_of_pem_
+ module Fixtures
+ module_function
+
+ def pkey(name)
+ OpenSSL::PKey.read(read_file("pkey", name))
+ end
+
+ def read_file(category, name)
+ @file_cache ||= {}
+ @file_cache[[category, name]] ||=
+ File.read(File.join(__dir__, "fixtures", category, name + ".pem"))
+ end
+ end
module_function
- def issue_cert(dn, key, serial, not_before, not_after, extensions,
- issuer, issuer_key, digest)
+ def generate_cert(dn, key, serial, issuer,
+ not_before: nil, not_after: nil)
cert = OpenSSL::X509::Certificate.new
issuer = cert unless issuer
- issuer_key = key unless issuer_key
cert.version = 2
cert.serial = serial
cert.subject = dn
cert.issuer = issuer.subject
- cert.public_key = key.public_key
- cert.not_before = not_before
- cert.not_after = not_after
+ cert.public_key = key
+ now = Time.now
+ cert.not_before = not_before || now - 3600
+ cert.not_after = not_after || now + 3600
+ cert
+ end
+
+
+ def issue_cert(dn, key, serial, extensions, issuer, issuer_key,
+ not_before: nil, not_after: nil, digest: "sha256")
+ cert = generate_cert(dn, key, serial, issuer,
+ not_before: not_before, not_after: not_after)
+ issuer = cert unless issuer
+ issuer_key = key unless issuer_key
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = issuer
@@ -96,16 +60,16 @@ Q1VB8qkJN7rA7/2HrCR3gTsWNb1YhAsnFsoeRscC+LxXoXi9OAIUBG98h4tilg6S
cert
end
- def issue_crl(revoke_info, serial, lastup, nextup, extensions,
+ def issue_crl(revoke_info, serial, lastup, nextup, extensions,
issuer, issuer_key, digest)
crl = OpenSSL::X509::CRL.new
crl.issuer = issuer.subject
crl.version = 1
crl.last_update = lastup
crl.next_update = nextup
- revoke_info.each{|serial, time, reason_code|
+ revoke_info.each{|rserial, time, reason_code|
revoked = OpenSSL::X509::Revoked.new
- revoked.serial = serial
+ revoked.serial = rserial
revoked.time = time
enum = OpenSSL::ASN1::Enumerated(reason_code)
ext = OpenSSL::X509::Extension.new("CRLReason", enum)
@@ -124,12 +88,297 @@ Q1VB8qkJN7rA7/2HrCR3gTsWNb1YhAsnFsoeRscC+LxXoXi9OAIUBG98h4tilg6S
crl
end
- def get_subject_key_id(cert)
+ def get_subject_key_id(cert, hex: true)
asn1_cert = OpenSSL::ASN1.decode(cert)
tbscert = asn1_cert.value[0]
pkinfo = tbscert.value[6]
publickey = pkinfo.value[1]
pkvalue = publickey.value
- OpenSSL::Digest::SHA1.hexdigest(pkvalue).scan(/../).join(":").upcase
+ digest = OpenSSL::Digest.digest('SHA1', pkvalue)
+ if hex
+ digest.unpack("H2"*20).join(":").upcase
+ else
+ digest
+ end
+ end
+
+ def openssl?(major = nil, minor = nil, fix = nil, patch = 0, status = 0)
+ return false if OpenSSL::OPENSSL_VERSION.include?("LibreSSL") || OpenSSL::OPENSSL_VERSION.include?("AWS-LC")
+ return true unless major
+ OpenSSL::OPENSSL_VERSION_NUMBER >=
+ major * 0x10000000 + minor * 0x100000 + fix * 0x1000 + patch * 0x10 +
+ status * 0x1
+ end
+
+ def libressl?(major = nil, minor = nil, fix = nil)
+ version = OpenSSL::OPENSSL_VERSION.scan(/LibreSSL (\d+)\.(\d+)\.(\d+).*/)[0]
+ return false unless version
+ !major || (version.map(&:to_i) <=> [major, minor, fix]) >= 0
+ end
+
+ def aws_lc?
+ OpenSSL::OPENSSL_VERSION.include?("AWS-LC")
end
end
+
+class OpenSSL::TestCase < Test::Unit::TestCase
+ include OpenSSL::TestUtils
+ extend OpenSSL::TestUtils
+ include Test::Unit::CoreAssertions
+
+ def setup
+ if ENV["OSSL_GC_STRESS"] == "1"
+ GC.stress = true
+ end
+ end
+
+ def teardown
+ if ENV["OSSL_GC_STRESS"] == "1"
+ GC.stress = false
+ end
+ # OpenSSL error stack must be empty
+ assert_equal([], OpenSSL.errors)
+ end
+
+ # Omit the tests in FIPS.
+ #
+ # For example, the password based encryption used in the PEM format uses MD5
+ # for deriving the encryption key from the password, and MD5 is not
+ # FIPS-approved.
+ #
+ # See https://github.com/openssl/openssl/discussions/21830#discussioncomment-6865636
+ # for details.
+ def omit_on_fips
+ return unless OpenSSL.fips_mode
+
+ omit <<~MESSAGE
+ Only for OpenSSL non-FIPS with the following possible reasons:
+ * A testing logic is non-FIPS specific.
+ * An encryption used in the test is not FIPS-approved.
+ MESSAGE
+ end
+
+ def omit_on_non_fips
+ return if OpenSSL.fips_mode
+
+ omit "Only for OpenSSL FIPS"
+ end
+end
+
+class OpenSSL::SSLTestCase < OpenSSL::TestCase
+ RUBY = EnvUtil.rubybin
+ ITERATIONS = ($0 == __FILE__) ? 100 : 10
+
+ def setup
+ super
+ @ca_key = Fixtures.pkey("rsa-1")
+ @svr_key = Fixtures.pkey("rsa-2")
+ @cli_key = Fixtures.pkey("rsa-3")
+ @ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
+ @svr = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=localhost")
+ @cli = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=localhost")
+ @ca_exts = [
+ ["basicConstraints","CA:TRUE",true],
+ ["keyUsage","cRLSign,keyCertSign",true],
+ ]
+ @ee_exts = [
+ ["keyUsage","keyEncipherment,digitalSignature",true],
+ ]
+ @ca_cert = issue_cert(@ca, @ca_key, 1, @ca_exts, nil, nil)
+ @svr_cert = issue_cert(@svr, @svr_key, 2, @ee_exts, @ca_cert, @ca_key)
+ @cli_cert = issue_cert(@cli, @cli_key, 3, @ee_exts, @ca_cert, @ca_key)
+ @server = nil
+ end
+
+ def readwrite_loop(ctx, ssl)
+ while line = ssl.gets
+ ssl.write(line)
+ end
+ end
+
+ def start_server(verify_mode: OpenSSL::SSL::VERIFY_NONE,
+ ctx_proc: nil, server_proc: method(:readwrite_loop),
+ accept_proc: proc{},
+ ignore_listener_error: false, &block)
+ IO.pipe {|stop_pipe_r, stop_pipe_w|
+ ctx = OpenSSL::SSL::SSLContext.new
+ ctx.cert = @svr_cert
+ ctx.key = @svr_key
+ ctx.verify_mode = verify_mode
+ ctx_proc.call(ctx) if ctx_proc
+
+ Socket.do_not_reverse_lookup = true
+ tcps = TCPServer.new("127.0.0.1", 0)
+ port = tcps.connect_address.ip_port
+
+ ssls = OpenSSL::SSL::SSLServer.new(tcps, ctx)
+
+ threads = []
+ begin
+ server_thread = Thread.new do
+ Thread.current.report_on_exception = false
+
+ begin
+ loop do
+ begin
+ readable, = IO.select([ssls, stop_pipe_r])
+ break if readable.include? stop_pipe_r
+ ssl = ssls.accept
+ accept_proc.call(ssl)
+ rescue OpenSSL::SSL::SSLError, IOError, Errno::EBADF, Errno::EINVAL,
+ Errno::ECONNABORTED, Errno::ENOTSOCK, Errno::ECONNRESET
+ retry if ignore_listener_error
+ raise
+ end
+
+ th = Thread.new do
+ Thread.current.report_on_exception = false
+
+ begin
+ server_proc.call(ctx, ssl)
+ ensure
+ ssl.close
+ end
+ true
+ end
+ threads << th
+ end
+ ensure
+ tcps.close
+ end
+ end
+
+ client_thread = Thread.new do
+ Thread.current.report_on_exception = false
+
+ begin
+ block.call(port)
+ ensure
+ # Stop accepting new connection
+ stop_pipe_w.close
+ server_thread.join
+ end
+ end
+ threads.unshift client_thread
+ ensure
+ # Terminate existing connections. If a thread did 'pend', re-raise it.
+ pend = nil
+ threads.each { |th|
+ begin
+ timeout = EnvUtil.apply_timeout_scale(30)
+ th.join(timeout) or
+ th.raise(RuntimeError, "[start_server] thread did not exit in #{timeout} secs")
+ rescue Test::Unit::PendedError
+ pend = $!
+ rescue Exception
+ end
+ }
+ raise pend if pend
+ assert_join_threads(threads)
+ end
+ }
+ end
+end
+
+class OpenSSL::PKeyTestCase < OpenSSL::TestCase
+ def check_component(base, test, keys)
+ keys.each { |comp|
+ assert_equal base.send(comp), test.send(comp)
+ }
+ end
+
+ def assert_sign_verify_false_or_error
+ ret = yield
+ rescue => e
+ assert_kind_of(OpenSSL::PKey::PKeyError, e)
+ else
+ assert_equal(false, ret)
+ end
+
+ def der_to_pem(der, pem_header)
+ # RFC 7468
+ <<~EOS
+ -----BEGIN #{pem_header}-----
+ #{[der].pack("m0").scan(/.{1,64}/).join("\n")}
+ -----END #{pem_header}-----
+ EOS
+ end
+
+ def der_to_encrypted_pem(der, pem_header, password)
+ # OpenSSL encryption, non-standard
+ iv = 16.times.to_a.pack("C*")
+ encrypted = OpenSSL::Cipher.new("aes-128-cbc").encrypt.then { |cipher|
+ cipher.key = OpenSSL::Digest.digest("MD5", password + iv[0, 8])
+ cipher.iv = iv
+ cipher.update(der) << cipher.final
+ }
+ <<~EOS
+ -----BEGIN #{pem_header}-----
+ Proc-Type: 4,ENCRYPTED
+ DEK-Info: AES-128-CBC,#{iv.unpack1("H*").upcase}
+
+ #{[encrypted].pack("m0").scan(/.{1,64}/).join("\n")}
+ -----END #{pem_header}-----
+ EOS
+ end
+end
+
+module OpenSSL::Certs
+ include OpenSSL::TestUtils
+
+ module_function
+
+ def ca_cert
+ ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=Timestamp Root CA")
+
+ ca_exts = [
+ ["basicConstraints","CA:TRUE,pathlen:1",true],
+ ["keyUsage","keyCertSign, cRLSign",true],
+ ["subjectKeyIdentifier","hash",false],
+ ["authorityKeyIdentifier","keyid:always",false],
+ ]
+ OpenSSL::TestUtils.issue_cert(ca, Fixtures.pkey("rsa2048"), 1, ca_exts, nil, nil)
+ end
+
+ def ts_cert_direct(key, ca_cert)
+ dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/OU=Timestamp/CN=Server Direct")
+
+ exts = [
+ ["basicConstraints","CA:FALSE",true],
+ ["keyUsage","digitalSignature, nonRepudiation", true],
+ ["subjectKeyIdentifier", "hash",false],
+ ["authorityKeyIdentifier","keyid,issuer", false],
+ ["extendedKeyUsage", "timeStamping", true]
+ ]
+
+ OpenSSL::TestUtils.issue_cert(dn, key, 2, exts, ca_cert, Fixtures.pkey("rsa2048"))
+ end
+
+ def intermediate_cert(key, ca_cert)
+ dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/OU=Timestamp/CN=Timestamp Intermediate CA")
+
+ exts = [
+ ["basicConstraints","CA:TRUE,pathlen:0",true],
+ ["keyUsage","keyCertSign, cRLSign",true],
+ ["subjectKeyIdentifier","hash",false],
+ ["authorityKeyIdentifier","keyid:always",false],
+ ]
+
+ OpenSSL::TestUtils.issue_cert(dn, key, 3, exts, ca_cert, Fixtures.pkey("rsa2048"))
+ end
+
+ def ts_cert_ee(key, intermediate, im_key)
+ dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/OU=Timestamp/CN=Server End Entity")
+
+ exts = [
+ ["keyUsage","digitalSignature, nonRepudiation", true],
+ ["subjectKeyIdentifier", "hash",false],
+ ["authorityKeyIdentifier","keyid,issuer", false],
+ ["extendedKeyUsage", "timeStamping", true]
+ ]
+
+ OpenSSL::TestUtils.issue_cert(dn, key, 4, exts, intermediate, im_key)
+ end
+end
+
+end