diff --git a/Rakefile b/Rakefile index 4ca600c4..0b7eda83 100644 --- a/Rakefile +++ b/Rakefile @@ -72,6 +72,8 @@ else Rake::JavaExtensionTask.new("puma_http11", gemspec) do |ext| ext.lib_dir = "lib/puma" + ext.source_version = '1.8' + ext.target_version = '1.8' end end diff --git a/examples/puma/client-certs/ca_store.p12 b/examples/puma/client-certs/ca_store.p12 new file mode 100644 index 00000000..eafadd87 Binary files /dev/null and b/examples/puma/client-certs/ca_store.p12 differ diff --git a/ext/puma_http11/org/jruby/puma/MiniSSL.java b/ext/puma_http11/org/jruby/puma/MiniSSL.java index 6371e990..ece00be3 100644 --- a/ext/puma_http11/org/jruby/puma/MiniSSL.java +++ b/ext/puma_http11/org/jruby/puma/MiniSSL.java @@ -15,6 +15,7 @@ import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -22,6 +23,7 @@ import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; +import javax.net.ssl.X509TrustManager; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; @@ -32,15 +34,18 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.concurrent.ConcurrentHashMap; import java.util.Map; +import java.util.function.Supplier; import static javax.net.ssl.SSLEngineResult.Status; import static javax.net.ssl.SSLEngineResult.HandshakeStatus; -public class MiniSSL extends RubyObject { +public class MiniSSL extends RubyObject { // MiniSSL::Engine private static ObjectAllocator ALLOCATOR = new ObjectAllocator() { public IRubyObject allocate(Ruby runtime, RubyClass klass) { return new MiniSSL(runtime, klass); @@ -51,11 +56,10 @@ public class MiniSSL extends RubyObject { RubyModule mPuma = runtime.defineModule("Puma"); RubyModule ssl = mPuma.defineModuleUnder("MiniSSL"); - mPuma.defineClassUnder("SSLError", - runtime.getClass("IOError"), - runtime.getClass("IOError").getAllocator()); + // Puma::MiniSSL::SSLError + ssl.defineClassUnder("SSLError", runtime.getStandardError(), runtime.getStandardError().getAllocator()); - RubyClass eng = ssl.defineClassUnder("Engine",runtime.getObject(),ALLOCATOR); + RubyClass eng = ssl.defineClassUnder("Engine", runtime.getObject(), ALLOCATOR); eng.defineAnnotatedMethods(MiniSSL.class); } @@ -141,70 +145,89 @@ public class MiniSSL extends RubyObject { public static synchronized IRubyObject server(ThreadContext context, IRubyObject recv, IRubyObject miniSSLContext) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException { // Create the KeyManagerFactory and TrustManagerFactory for this server - String keystoreFile = miniSSLContext.callMethod(context, "keystore").convertToString().asJavaString(); - char[] password = miniSSLContext.callMethod(context, "keystore_pass").convertToString().asJavaString().toCharArray(); + String keystoreFile = asStringValue(miniSSLContext.callMethod(context, "keystore"), null); + char[] keystorePass = asStringValue(miniSSLContext.callMethod(context, "keystore_pass"), null).toCharArray(); + String keystoreType = asStringValue(miniSSLContext.callMethod(context, "keystore_type"), KeyStore::getDefaultType); - KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + String truststoreFile; + char[] truststorePass; + String truststoreType; + IRubyObject truststore = miniSSLContext.callMethod(context, "truststore"); + if (truststore.isNil()) { + truststoreFile = keystoreFile; + truststorePass = keystorePass; + truststoreType = keystoreType; + } else { + truststoreFile = truststore.convertToString().asJavaString(); + truststorePass = asStringValue(miniSSLContext.callMethod(context, "truststore_pass"), null).toCharArray(); + truststoreType = asStringValue(miniSSLContext.callMethod(context, "truststore_type"), KeyStore::getDefaultType); + } + + KeyStore ks = KeyStore.getInstance(keystoreType); InputStream is = new FileInputStream(keystoreFile); try { - ks.load(is, password); + ks.load(is, keystorePass); } finally { is.close(); } KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, password); + kmf.init(ks, keystorePass); keyManagerFactoryMap.put(keystoreFile, kmf); - KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType()); - is = new FileInputStream(keystoreFile); + KeyStore ts = KeyStore.getInstance(truststoreType); + is = new FileInputStream(truststoreFile); try { - ts.load(is, password); + ts.load(is, truststorePass); } finally { is.close(); } TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); tmf.init(ts); - trustManagerFactoryMap.put(keystoreFile, tmf); + trustManagerFactoryMap.put(truststoreFile, tmf); RubyClass klass = (RubyClass) recv; - return klass.newInstance(context, - new IRubyObject[] { miniSSLContext }, - Block.NULL_BLOCK); + return klass.newInstance(context, miniSSLContext, Block.NULL_BLOCK); + } + + private static String asStringValue(IRubyObject value, Supplier defaultValue) { + if (defaultValue != null && value.isNil()) return defaultValue.get(); + return value.convertToString().asJavaString(); } @JRubyMethod - public IRubyObject initialize(ThreadContext threadContext, IRubyObject miniSSLContext) + public IRubyObject initialize(ThreadContext context, IRubyObject miniSSLContext) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException { - String keystoreFile = miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString(); + String keystoreFile = miniSSLContext.callMethod(context, "keystore").convertToString().asJavaString(); KeyManagerFactory kmf = keyManagerFactoryMap.get(keystoreFile); - TrustManagerFactory tmf = trustManagerFactoryMap.get(keystoreFile); - if(kmf == null || tmf == null) { - throw new KeyStoreException("Could not find KeyManagerFactory/TrustManagerFactory for keystore: " + keystoreFile); + String truststoreFile = asStringValue(miniSSLContext.callMethod(context, "truststore"), () -> keystoreFile); + TrustManagerFactory tmf = trustManagerFactoryMap.get(truststoreFile); + if (kmf == null || tmf == null) { + throw new KeyStoreException("Could not find KeyManagerFactory/TrustManagerFactory for keystore: " + keystoreFile + " truststore: " + truststoreFile); } SSLContext sslCtx = SSLContext.getInstance("TLS"); - sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + sslCtx.init(kmf.getKeyManagers(), getTrustManagers(tmf), null); closed = false; handshake = false; engine = sslCtx.createSSLEngine(); String[] protocols; - if(miniSSLContext.callMethod(threadContext, "no_tlsv1").isTrue()) { + if (miniSSLContext.callMethod(context, "no_tlsv1").isTrue()) { protocols = new String[] { "TLSv1.1", "TLSv1.2" }; } else { protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; } - if(miniSSLContext.callMethod(threadContext, "no_tlsv1_1").isTrue()) { + if (miniSSLContext.callMethod(context, "no_tlsv1_1").isTrue()) { protocols = new String[] { "TLSv1.2" }; } engine.setEnabledProtocols(protocols); engine.setUseClientMode(false); - long verify_mode = miniSSLContext.callMethod(threadContext, "verify_mode").convertToInteger("to_i").getLongValue(); + long verify_mode = miniSSLContext.callMethod(context, "verify_mode").convertToInteger("to_i").getLongValue(); if ((verify_mode & 0x1) != 0) { // 'peer' engine.setWantClientAuth(true); } @@ -212,7 +235,7 @@ public class MiniSSL extends RubyObject { engine.setNeedClientAuth(true); } - IRubyObject sslCipherListObject = miniSSLContext.callMethod(threadContext, "ssl_cipher_list"); + IRubyObject sslCipherListObject = miniSSLContext.callMethod(context, "ssl_cipher_list"); if (!sslCipherListObject.isNil()) { String[] sslCipherList = sslCipherListObject.convertToString().asJavaString().split(","); engine.setEnabledCipherSuites(sslCipherList); @@ -227,6 +250,47 @@ public class MiniSSL extends RubyObject { return this; } + private TrustManager[] getTrustManagers(TrustManagerFactory factory) { + final TrustManager[] tms = factory.getTrustManagers(); + if (tms != null) { + for (int i=0; i e # Errno::ECONNRESET TruffleRuby - client_error = true + client_error = e # closes socket if open, may not close on error http.send :do_finish end sleep 0.1 - assert_equal !!error, client_error + assert_equal !!error, !!client_error, client_error # The JRuby MiniSSL implementation lacks error capturing currently, # so we can't inspect the messages here - unless Puma.jruby? - assert_match error, log_writer.error.message if error - assert_includes host_addrs, log_writer.addr if error - assert_equal subject, log_writer.cert.subject.to_s if subject - end + assert_match error, log_writer.error.message if error + assert_includes host_addrs, log_writer.addr if error + assert_equal subject, log_writer.cert.subject.to_s if subject ensure server.stop(true) if server end def test_verify_fail_if_no_client_cert - assert_ssl_client_error_match 'peer did not return a certificate' do |http| + error = Puma.jruby? ? /Empty server certificate chain/ : 'peer did not return a certificate' + assert_ssl_client_error_match(error) do |http| # nothing end end def test_verify_fail_if_client_unknown_ca - assert_ssl_client_error_match(/self[- ]signed certificate in certificate chain/, '/DC=net/DC=puma/CN=CAU') do |http| + error = Puma.jruby? ? /No trusted certificate found/ : /self[- ]signed certificate in certificate chain/ + cert_subject = Puma.jruby? ? '/DC=net/DC=puma/CN=localhost' : '/DC=net/DC=puma/CN=CAU' + assert_ssl_client_error_match(error, subject: cert_subject) do |http| key = "#{CERT_PATH}/client_unknown.key" crt = "#{CERT_PATH}/client_unknown.crt" http.key = OpenSSL::PKey::RSA.new File.read(key) @@ -353,7 +354,8 @@ class TestPumaServerSSLClient < Minitest::Test end def test_verify_fail_if_client_expired_cert - assert_ssl_client_error_match('certificate has expired', '/DC=net/DC=puma/CN=localhost') do |http| + error = Puma.jruby? ? /NotAfter:/ : 'certificate has expired' + assert_ssl_client_error_match(error, subject: '/DC=net/DC=puma/CN=localhost') do |http| key = "#{CERT_PATH}/client_expired.key" crt = "#{CERT_PATH}/client_expired.crt" http.key = OpenSSL::PKey::RSA.new File.read(key) @@ -363,7 +365,7 @@ class TestPumaServerSSLClient < Minitest::Test end def test_verify_client_cert - assert_ssl_client_error_match(nil) do |http| + assert_ssl_client_error_match(false) do |http| key = "#{CERT_PATH}/client.key" crt = "#{CERT_PATH}/client.crt" http.key = OpenSSL::PKey::RSA.new File.read(key) @@ -372,6 +374,26 @@ class TestPumaServerSSLClient < Minitest::Test http.verify_mode = OpenSSL::SSL::VERIFY_PEER end end + + def test_verify_client_cert_with_truststore + ctx = Puma::MiniSSL::Context.new + ctx.keystore = "#{CERT_PATH}/server.p12" + ctx.keystore_type = 'pkcs12' + ctx.keystore_pass = 'jruby_puma' + ctx.truststore = "#{CERT_PATH}/ca_store.p12" + ctx.truststore_type = 'pkcs12' + ctx.truststore_pass = 'jruby_puma' + + assert_ssl_client_error_match(false, context: ctx) do |http| + key = "#{CERT_PATH}/client.key" + crt = "#{CERT_PATH}/client.crt" + http.key = OpenSSL::PKey::RSA.new File.read(key) + http.cert = OpenSSL::X509::Certificate.new File.read(crt) + http.ca_file = "#{CERT_PATH}/ca.crt" + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + end + end if Puma.jruby? + end if ::Puma::HAS_SSL class TestPumaServerSSLWithCertPemAndKeyPem < Minitest::Test