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

support TLS client auth (verify_mode) in jruby

Adds support for `verify_mode` to configure client authentication when running under JRuby.

Things to note:

- Assumes the CA used to verify client certs is in the same java
  keystore file that is used when setting up the HTTPS TLS listener. We
could split this out, but not sure if it's necessary.
- Friendly/helpful error messages explaining why the verification failed
  are not present in the same way they are in the CRuby/OpenSSL code
path. I'm not sure how to make them available.
- I did not include any code to create the `keystore.jks` file in the
  `examples/puma/client-certs` directory because I didn't see any
existing code to create the `examples/puma/keystore.jks` file. The
commands to create this keystore would be:

```
cd examples/puma/client-certs
  openssl pkcs12 -chain -CAfile ./ca.crt -export -password pass:blahblah -inkey server.key -in server.crt -name server -out server.p12
  keytool -importkeystore -srckeystore server.p12 -srcstoretype pkcs12 -srcstorepass blahblah -destkeystore keystore.jks -deststoretype JKS -storepass blahblah
  keytool -importcert -alias ca -noprompt -trustcacerts -file ca.crt -keystore keystore.jks -storepass blahblah
```
This commit is contained in:
joe miller 2015-11-28 18:17:01 -08:00
parent 81c2ccb6d4
commit 4ae0de4f4c
4 changed files with 85 additions and 64 deletions

Binary file not shown.

Binary file not shown.

View file

@ -13,6 +13,7 @@ import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList; import org.jruby.util.ByteList;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult;
@ -136,23 +137,36 @@ public class MiniSSL extends RubyObject {
public IRubyObject initialize(ThreadContext threadContext, IRubyObject miniSSLContext) public IRubyObject initialize(ThreadContext threadContext, IRubyObject miniSSLContext)
throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException { throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
char[] password = miniSSLContext.callMethod(threadContext, "keystore_pass").convertToString().asJavaString().toCharArray(); char[] password = miniSSLContext.callMethod(threadContext, "keystore_pass").convertToString().asJavaString().toCharArray();
ks.load(new FileInputStream(miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString()), String keystoreFile = miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString();
password); ks.load(new FileInputStream(keystoreFile), password);
ts.load(new FileInputStream(keystoreFile), password);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, password); kmf.init(ks, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
SSLContext sslCtx = SSLContext.getInstance("TLS"); SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(kmf.getKeyManagers(), null, null); sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
engine = sslCtx.createSSLEngine(); engine = sslCtx.createSSLEngine();
String[] protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; String[] protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
engine.setEnabledProtocols(protocols); engine.setEnabledProtocols(protocols);
engine.setUseClientMode(false); engine.setUseClientMode(false);
long verify_mode = miniSSLContext.callMethod(threadContext, "verify_mode").convertToInteger().getLongValue();
if ((verify_mode & 0x1) != 0) { // 'peer'
engine.setWantClientAuth(true);
}
if ((verify_mode & 0x2) != 0) { // 'force_peer'
engine.setNeedClientAuth(true);
}
SSLSession session = engine.getSession(); SSLSession session = engine.getSession();
inboundNetData = new MiniSSLBuffer(session.getPacketBufferSize()); inboundNetData = new MiniSSLBuffer(session.getPacketBufferSize());
outboundAppData = new MiniSSLBuffer(session.getApplicationBufferSize()); outboundAppData = new MiniSSLBuffer(session.getApplicationBufferSize());

View file

@ -132,92 +132,99 @@ class TestPumaServerSSL < Test::Unit::TestCase
end end
# client-side TLS authentication tests # client-side TLS authentication tests
unless defined?(JRUBY_VERSION) class TestPumaServerSSLClient < Test::Unit::TestCase
class TestPumaServerSSLClient < Test::Unit::TestCase
def assert_ssl_client_error_match(error, subject=nil, &blk) def assert_ssl_client_error_match(error, subject=nil, &blk)
@port = 3212 @port = 3212
@host = "127.0.0.1" @host = "127.0.0.1"
@app = lambda { |env| [200, {}, [env['rack.url_scheme']]] } @app = lambda { |env| [200, {}, [env['rack.url_scheme']]] }
@ctx = Puma::MiniSSL::Context.new @ctx = Puma::MiniSSL::Context.new
if defined?(JRUBY_VERSION)
@ctx.keystore = File.expand_path "../../examples/puma/client-certs/keystore.jks", __FILE__
@ctx.keystore_pass = 'blahblah'
else
@ctx.key = File.expand_path "../../examples/puma/client-certs/server.key", __FILE__ @ctx.key = File.expand_path "../../examples/puma/client-certs/server.key", __FILE__
@ctx.cert = File.expand_path "../../examples/puma/client-certs/server.crt", __FILE__ @ctx.cert = File.expand_path "../../examples/puma/client-certs/server.crt", __FILE__
@ctx.ca = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__ @ctx.ca = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
@ctx.verify_mode = Puma::MiniSSL::VERIFY_PEER | Puma::MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT end
@ctx.verify_mode = Puma::MiniSSL::VERIFY_PEER | Puma::MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
events = SSLEventsHelper.new STDOUT, STDERR events = SSLEventsHelper.new STDOUT, STDERR
@server = Puma::Server.new @app, events @server = Puma::Server.new @app, events
@server.add_ssl_listener @host, @port, @ctx @server.add_ssl_listener @host, @port, @ctx
@server.run @server.run
@http = Net::HTTP.new @host, @port @http = Net::HTTP.new @host, @port
@http.use_ssl = true @http.use_ssl = true
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
blk.call(@http) blk.call(@http)
client_error = false client_error = false
begin begin
@http.start do @http.start do
req = Net::HTTP::Get.new "/", {} req = Net::HTTP::Get.new "/", {}
@http.request(req) @http.request(req)
end
rescue OpenSSL::SSL::SSLError
client_error = true
end end
rescue OpenSSL::SSL::SSLError
client_error = true
end
sleep 0.1 sleep 0.1
assert_equal !!error, client_error assert_equal !!error, client_error
# The JRuby MiniSSL implementation lacks error capturing currently, so we can't inspect the
# messages here
unless defined?(JRUBY_VERSION)
assert_match error, events.error.message if error assert_match error, events.error.message if error
assert_equal @host, events.addr if error assert_equal @host, events.addr if error
assert_equal subject, events.cert.subject.to_s if subject assert_equal subject, events.cert.subject.to_s if subject
ensure
@server.stop(true)
end end
ensure
@server.stop(true)
end
def test_verify_fail_if_no_client_cert def test_verify_fail_if_no_client_cert
return if DISABLE_SSL return if DISABLE_SSL
assert_ssl_client_error_match 'peer did not return a certificate' do |http| assert_ssl_client_error_match 'peer did not return a certificate' do |http|
# nothing # nothing
end
end end
end
def test_verify_fail_if_client_unknown_ca def test_verify_fail_if_client_unknown_ca
return if DISABLE_SSL return if DISABLE_SSL
assert_ssl_client_error_match('self signed certificate in certificate chain', '/DC=net/DC=puma/CN=ca-unknown') do |http| assert_ssl_client_error_match('self signed certificate in certificate chain', '/DC=net/DC=puma/CN=ca-unknown') do |http|
key = File.expand_path "../../examples/puma/client-certs/client_unknown.key", __FILE__ key = File.expand_path "../../examples/puma/client-certs/client_unknown.key", __FILE__
crt = File.expand_path "../../examples/puma/client-certs/client_unknown.crt", __FILE__ crt = File.expand_path "../../examples/puma/client-certs/client_unknown.crt", __FILE__
http.key = OpenSSL::PKey::RSA.new File.read(key) http.key = OpenSSL::PKey::RSA.new File.read(key)
http.cert = OpenSSL::X509::Certificate.new File.read(crt) http.cert = OpenSSL::X509::Certificate.new File.read(crt)
http.ca_file = File.expand_path "../../examples/puma/client-certs/unknown_ca.crt", __FILE__ http.ca_file = File.expand_path "../../examples/puma/client-certs/unknown_ca.crt", __FILE__
end
end end
end
def test_verify_fail_if_client_expired_cert def test_verify_fail_if_client_expired_cert
return if DISABLE_SSL return if DISABLE_SSL
assert_ssl_client_error_match('certificate has expired', '/DC=net/DC=puma/CN=client-expired') do |http| assert_ssl_client_error_match('certificate has expired', '/DC=net/DC=puma/CN=client-expired') do |http|
key = File.expand_path "../../examples/puma/client-certs/client_expired.key", __FILE__ key = File.expand_path "../../examples/puma/client-certs/client_expired.key", __FILE__
crt = File.expand_path "../../examples/puma/client-certs/client_expired.crt", __FILE__ crt = File.expand_path "../../examples/puma/client-certs/client_expired.crt", __FILE__
http.key = OpenSSL::PKey::RSA.new File.read(key) http.key = OpenSSL::PKey::RSA.new File.read(key)
http.cert = OpenSSL::X509::Certificate.new File.read(crt) http.cert = OpenSSL::X509::Certificate.new File.read(crt)
http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__ http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
end
end end
end
def test_verify_client_cert def test_verify_client_cert
return if DISABLE_SSL return if DISABLE_SSL
assert_ssl_client_error_match(nil) do |http| assert_ssl_client_error_match(nil) do |http|
key = File.expand_path "../../examples/puma/client-certs/client.key", __FILE__ key = File.expand_path "../../examples/puma/client-certs/client.key", __FILE__
crt = File.expand_path "../../examples/puma/client-certs/client.crt", __FILE__ crt = File.expand_path "../../examples/puma/client-certs/client.crt", __FILE__
http.key = OpenSSL::PKey::RSA.new File.read(key) http.key = OpenSSL::PKey::RSA.new File.read(key)
http.cert = OpenSSL::X509::Certificate.new File.read(crt) http.cert = OpenSSL::X509::Certificate.new File.read(crt)
http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__ http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end
end end
end end
end end