1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

[jruby] support a truststore option (#2849)

* [jruby] support a truststore option

which might be a completely different file than keystore ...

due backwards compatibility we assume `truststore = keystore`
(`truststore_pass = keystore_pass`)

* [jruby] actually use truststore on initialize

* [jruby] add keystore_type and truststore_type

* [jruby] dry and simplify native bits

* [jruby] setup SSLError in native (like C part)

* [jruby] map to SSLError from native exception

* [jruby] provide peercert even if hand-shake fails
This commit is contained in:
Karol Bucek 2022-04-09 16:58:51 +02:00 committed by GitHub
parent db751ba82f
commit ceb4c56ad4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 54 deletions

View file

@ -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

Binary file not shown.

View file

@ -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<String> 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<tms.length; i++) {
final TrustManager tm = tms[i];
if (tm instanceof X509TrustManager) {
tms[i] = new TrustManagerWrapper((X509TrustManager) tm);
}
}
}
return tms;
}
private volatile transient X509Certificate[] lastCheckedChain;
private class TrustManagerWrapper implements X509TrustManager {
private final X509TrustManager delegate;
TrustManagerWrapper(X509TrustManager delegate) {
this.delegate = delegate;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
lastCheckedChain = chain.clone();
delegate.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
delegate.checkServerTrusted(chain, authType);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return delegate.getAcceptedIssuers();
}
}
@JRubyMethod
public IRubyObject inject(IRubyObject arg) {
ByteList bytes = arg.convertToString().getByteList();
@ -338,9 +402,7 @@ public class MiniSSL extends RubyObject {
return RubyString.newString(getRuntime(), appDataByteList);
} catch (SSLException e) {
RaiseException re = getRuntime().newEOFError(e.getMessage());
re.initCause(e);
throw re;
throw newSSLError(getRuntime(), e);
}
}
@ -373,19 +435,23 @@ public class MiniSSL extends RubyObject {
return RubyString.newString(context.runtime, dataByteList);
} catch (SSLException e) {
RaiseException ex = context.runtime.newRuntimeError(e.toString());
ex.initCause(e);
throw ex;
throw newSSLError(getRuntime(), e);
}
}
@JRubyMethod
public IRubyObject peercert() throws CertificateEncodingException {
public IRubyObject peercert(ThreadContext context) throws CertificateEncodingException {
Certificate peerCert;
try {
return JavaEmbedUtils.javaToRuby(getRuntime(), engine.getSession().getPeerCertificates()[0].getEncoded());
peerCert = engine.getSession().getPeerCertificates()[0];
} catch (SSLPeerUnverifiedException e) {
return getRuntime().getNil();
if (lastCheckedChain != null) {
peerCert = lastCheckedChain[0];
} else {
peerCert = null;
}
}
return peerCert == null ? context.nil : JavaEmbedUtils.javaToRuby(context.runtime, peerCert.getEncoded());
}
@JRubyMethod(name = "init?")
@ -404,4 +470,19 @@ public class MiniSSL extends RubyObject {
return getRuntime().getFalse();
}
}
private static RubyClass getSSLError(Ruby runtime) {
return (RubyClass) ((RubyModule) runtime.getModule("Puma").getConstantAt("MiniSSL")).getConstantAt("SSLError");
}
private static RaiseException newSSLError(Ruby runtime, SSLException cause) {
return newError(runtime, getSSLError(runtime), cause.toString(), cause);
}
private static RaiseException newError(Ruby runtime, RubyClass errorClass, String message, Throwable cause) {
RaiseException ex = new RaiseException(runtime, errorClass, message, true);
ex.initCause(cause);
return ex;
}
}

View file

@ -195,10 +195,6 @@ module Puma
if IS_JRUBY
OPENSSL_NO_SSL3 = false
OPENSSL_NO_TLS1 = false
class SSLError < StandardError
# Define this for jruby even though it isn't used.
end
end
class Context
@ -222,7 +218,11 @@ module Puma
if IS_JRUBY
# jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
attr_reader :keystore
attr_reader :keystore_type
attr_accessor :keystore_pass
attr_reader :truststore
attr_reader :truststore_type
attr_accessor :truststore_pass
attr_accessor :ssl_cipher_list
def keystore=(keystore)
@ -230,8 +230,24 @@ module Puma
@keystore = keystore
end
def truststore=(truststore)
raise ArgumentError, "No such truststore file '#{truststore}'" unless File.exist? truststore
@truststore = truststore
end
def keystore_type=(type)
raise ArgumentError, "Invalid keystore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
@keystore_type = type
end
def truststore_type=(type)
raise ArgumentError, "Invalid truststore type: #{type.inspect}" unless ['pkcs12', 'jks', nil].include?(type)
@truststore_type = type
end
def check
raise "Keystore not configured" unless @keystore
# @truststore defaults to @keystore due backwards compatibility
end
else

View file

@ -292,7 +292,7 @@ class TestPumaServerSSLClient < Minitest::Test
ctx.verify_mode = Puma::MiniSSL::VERIFY_PEER | Puma::MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
}
def assert_ssl_client_error_match(error, subject=nil, &blk)
def assert_ssl_client_error_match(error, subject: nil, context: CTX, &blk)
host = "localhost"
port = 0
@ -300,7 +300,7 @@ class TestPumaServerSSLClient < Minitest::Test
log_writer = SSLLogWriterHelper.new STDOUT, STDERR
server = Puma::Server.new app, log_writer
server.add_ssl_listener host, port, CTX
server.add_ssl_listener host, port, context
host_addrs = server.binder.ios.map { |io| io.to_io.addr[2] }
server.run
@ -316,34 +316,35 @@ class TestPumaServerSSLClient < Minitest::Test
req = Net::HTTP::Get.new "/", {}
http.request(req)
end
rescue OpenSSL::SSL::SSLError, EOFError, Errno::ECONNRESET
rescue OpenSSL::SSL::SSLError, EOFError, Errno::ECONNRESET => 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