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:
parent
81c2ccb6d4
commit
4ae0de4f4c
4 changed files with 85 additions and 64 deletions
BIN
examples/puma/client-certs/keystore.jks
Normal file
BIN
examples/puma/client-certs/keystore.jks
Normal file
Binary file not shown.
BIN
examples/puma/client-certs/server.p12
Normal file
BIN
examples/puma/client-certs/server.p12
Normal file
Binary file not shown.
|
@ -13,6 +13,7 @@ import org.jruby.runtime.builtin.IRubyObject;
|
|||
import org.jruby.util.ByteList;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLEngineResult;
|
||||
|
@ -136,23 +137,36 @@ public class MiniSSL extends RubyObject {
|
|||
public IRubyObject initialize(ThreadContext threadContext, IRubyObject miniSSLContext)
|
||||
throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
|
||||
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
|
||||
char[] password = miniSSLContext.callMethod(threadContext, "keystore_pass").convertToString().asJavaString().toCharArray();
|
||||
ks.load(new FileInputStream(miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString()),
|
||||
password);
|
||||
String keystoreFile = miniSSLContext.callMethod(threadContext, "keystore").convertToString().asJavaString();
|
||||
ks.load(new FileInputStream(keystoreFile), password);
|
||||
ts.load(new FileInputStream(keystoreFile), password);
|
||||
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
|
||||
kmf.init(ks, password);
|
||||
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
|
||||
tmf.init(ts);
|
||||
|
||||
SSLContext sslCtx = SSLContext.getInstance("TLS");
|
||||
|
||||
sslCtx.init(kmf.getKeyManagers(), null, null);
|
||||
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
||||
engine = sslCtx.createSSLEngine();
|
||||
|
||||
String[] protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
|
||||
engine.setEnabledProtocols(protocols);
|
||||
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();
|
||||
inboundNetData = new MiniSSLBuffer(session.getPacketBufferSize());
|
||||
outboundAppData = new MiniSSLBuffer(session.getApplicationBufferSize());
|
||||
|
|
|
@ -132,92 +132,99 @@ class TestPumaServerSSL < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
# 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)
|
||||
@port = 3212
|
||||
@host = "127.0.0.1"
|
||||
def assert_ssl_client_error_match(error, subject=nil, &blk)
|
||||
@port = 3212
|
||||
@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.cert = File.expand_path "../../examples/puma/client-certs/server.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
|
||||
@server = Puma::Server.new @app, events
|
||||
@server.add_ssl_listener @host, @port, @ctx
|
||||
@server.run
|
||||
events = SSLEventsHelper.new STDOUT, STDERR
|
||||
@server = Puma::Server.new @app, events
|
||||
@server.add_ssl_listener @host, @port, @ctx
|
||||
@server.run
|
||||
|
||||
@http = Net::HTTP.new @host, @port
|
||||
@http.use_ssl = true
|
||||
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
@http = Net::HTTP.new @host, @port
|
||||
@http.use_ssl = true
|
||||
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
|
||||
blk.call(@http)
|
||||
blk.call(@http)
|
||||
|
||||
client_error = false
|
||||
begin
|
||||
@http.start do
|
||||
req = Net::HTTP::Get.new "/", {}
|
||||
@http.request(req)
|
||||
end
|
||||
rescue OpenSSL::SSL::SSLError
|
||||
client_error = true
|
||||
client_error = false
|
||||
begin
|
||||
@http.start do
|
||||
req = Net::HTTP::Get.new "/", {}
|
||||
@http.request(req)
|
||||
end
|
||||
rescue OpenSSL::SSL::SSLError
|
||||
client_error = true
|
||||
end
|
||||
|
||||
sleep 0.1
|
||||
assert_equal !!error, client_error
|
||||
sleep 0.1
|
||||
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_equal @host, events.addr if error
|
||||
assert_equal subject, events.cert.subject.to_s if subject
|
||||
ensure
|
||||
@server.stop(true)
|
||||
end
|
||||
ensure
|
||||
@server.stop(true)
|
||||
end
|
||||
|
||||
def test_verify_fail_if_no_client_cert
|
||||
return if DISABLE_SSL
|
||||
def test_verify_fail_if_no_client_cert
|
||||
return if DISABLE_SSL
|
||||
|
||||
assert_ssl_client_error_match 'peer did not return a certificate' do |http|
|
||||
# nothing
|
||||
end
|
||||
assert_ssl_client_error_match 'peer did not return a certificate' do |http|
|
||||
# nothing
|
||||
end
|
||||
end
|
||||
|
||||
def test_verify_fail_if_client_unknown_ca
|
||||
return if DISABLE_SSL
|
||||
def test_verify_fail_if_client_unknown_ca
|
||||
return if DISABLE_SSL
|
||||
|
||||
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__
|
||||
crt = File.expand_path "../../examples/puma/client-certs/client_unknown.crt", __FILE__
|
||||
http.key = OpenSSL::PKey::RSA.new File.read(key)
|
||||
http.cert = OpenSSL::X509::Certificate.new File.read(crt)
|
||||
http.ca_file = File.expand_path "../../examples/puma/client-certs/unknown_ca.crt", __FILE__
|
||||
end
|
||||
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__
|
||||
crt = File.expand_path "../../examples/puma/client-certs/client_unknown.crt", __FILE__
|
||||
http.key = OpenSSL::PKey::RSA.new File.read(key)
|
||||
http.cert = OpenSSL::X509::Certificate.new File.read(crt)
|
||||
http.ca_file = File.expand_path "../../examples/puma/client-certs/unknown_ca.crt", __FILE__
|
||||
end
|
||||
end
|
||||
|
||||
def test_verify_fail_if_client_expired_cert
|
||||
return if DISABLE_SSL
|
||||
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__
|
||||
crt = File.expand_path "../../examples/puma/client-certs/client_expired.crt", __FILE__
|
||||
http.key = OpenSSL::PKey::RSA.new File.read(key)
|
||||
http.cert = OpenSSL::X509::Certificate.new File.read(crt)
|
||||
http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
|
||||
end
|
||||
def test_verify_fail_if_client_expired_cert
|
||||
return if DISABLE_SSL
|
||||
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__
|
||||
crt = File.expand_path "../../examples/puma/client-certs/client_expired.crt", __FILE__
|
||||
http.key = OpenSSL::PKey::RSA.new File.read(key)
|
||||
http.cert = OpenSSL::X509::Certificate.new File.read(crt)
|
||||
http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
|
||||
end
|
||||
end
|
||||
|
||||
def test_verify_client_cert
|
||||
return if DISABLE_SSL
|
||||
assert_ssl_client_error_match(nil) do |http|
|
||||
key = File.expand_path "../../examples/puma/client-certs/client.key", __FILE__
|
||||
crt = File.expand_path "../../examples/puma/client-certs/client.crt", __FILE__
|
||||
http.key = OpenSSL::PKey::RSA.new File.read(key)
|
||||
http.cert = OpenSSL::X509::Certificate.new File.read(crt)
|
||||
http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
def test_verify_client_cert
|
||||
return if DISABLE_SSL
|
||||
assert_ssl_client_error_match(nil) do |http|
|
||||
key = File.expand_path "../../examples/puma/client-certs/client.key", __FILE__
|
||||
crt = File.expand_path "../../examples/puma/client-certs/client.crt", __FILE__
|
||||
http.key = OpenSSL::PKey::RSA.new File.read(key)
|
||||
http.cert = OpenSSL::X509::Certificate.new File.read(crt)
|
||||
http.ca_file = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue