package org.jruby.puma; import org.jruby.Ruby; import org.jruby.RubyClass; import org.jruby.RubyModule; import org.jruby.RubyObject; import org.jruby.RubyString; import org.jruby.anno.JRubyMethod; import org.jruby.exceptions.RaiseException; import org.jruby.javasupport.JavaEmbedUtils; import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; 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; 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; import java.nio.Buffer; import java.nio.ByteBuffer; import java.security.KeyManagementException; 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 { // MiniSSL::Engine private static ObjectAllocator ALLOCATOR = new ObjectAllocator() { public IRubyObject allocate(Ruby runtime, RubyClass klass) { return new MiniSSL(runtime, klass); } }; public static void createMiniSSL(Ruby runtime) { RubyModule mPuma = runtime.defineModule("Puma"); RubyModule ssl = mPuma.defineModuleUnder("MiniSSL"); // Puma::MiniSSL::SSLError ssl.defineClassUnder("SSLError", runtime.getStandardError(), runtime.getStandardError().getAllocator()); RubyClass eng = ssl.defineClassUnder("Engine", runtime.getObject(), ALLOCATOR); eng.defineAnnotatedMethods(MiniSSL.class); } /** * Fairly transparent wrapper around {@link java.nio.ByteBuffer} which adds the enhancements we need */ private static class MiniSSLBuffer { ByteBuffer buffer; private MiniSSLBuffer(int capacity) { buffer = ByteBuffer.allocate(capacity); } private MiniSSLBuffer(byte[] initialContents) { buffer = ByteBuffer.wrap(initialContents); } public void clear() { buffer.clear(); } public void compact() { buffer.compact(); } public void flip() { ((Buffer) buffer).flip(); } public boolean hasRemaining() { return buffer.hasRemaining(); } public int position() { return buffer.position(); } public ByteBuffer getRawBuffer() { return buffer; } /** * Writes bytes to the buffer after ensuring there's room */ private void put(byte[] bytes, final int offset, final int length) { if (buffer.remaining() < length) { resize(buffer.limit() + length); } buffer.put(bytes, offset, length); } /** * Ensures that newCapacity bytes can be written to this buffer, only re-allocating if necessary */ public void resize(int newCapacity) { if (newCapacity > buffer.capacity()) { ByteBuffer dstTmp = ByteBuffer.allocate(newCapacity); flip(); dstTmp.put(buffer); buffer = dstTmp; } else { buffer.limit(newCapacity); } } /** * Drains the buffer to a ByteList, or returns null for an empty buffer */ public ByteList asByteList() { flip(); if (!buffer.hasRemaining()) { buffer.clear(); return null; } byte[] bss = new byte[buffer.limit()]; buffer.get(bss); buffer.clear(); return new ByteList(bss, false); } @Override public String toString() { return buffer.toString(); } } private SSLEngine engine; private boolean closed; private boolean handshake; private MiniSSLBuffer inboundNetData; private MiniSSLBuffer outboundAppData; private MiniSSLBuffer outboundNetData; public MiniSSL(Ruby runtime, RubyClass klass) { super(runtime, klass); } private static Map keyManagerFactoryMap = new ConcurrentHashMap(); private static Map trustManagerFactoryMap = new ConcurrentHashMap(); @JRubyMethod(meta = true) 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 = 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); 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, keystorePass); } finally { is.close(); } KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, keystorePass); keyManagerFactoryMap.put(keystoreFile, kmf); KeyStore ts = KeyStore.getInstance(truststoreType); is = new FileInputStream(truststoreFile); try { ts.load(is, truststorePass); } finally { is.close(); } TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); tmf.init(ts); trustManagerFactoryMap.put(truststoreFile, tmf); RubyClass klass = (RubyClass) recv; 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 context, IRubyObject miniSSLContext) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException { String keystoreFile = miniSSLContext.callMethod(context, "keystore").convertToString().asJavaString(); KeyManagerFactory kmf = keyManagerFactoryMap.get(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(), getTrustManagers(tmf), null); closed = false; handshake = false; engine = sslCtx.createSSLEngine(); String[] protocols; 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(context, "no_tlsv1_1").isTrue()) { protocols = new String[] { "TLSv1.2" }; } engine.setEnabledProtocols(protocols); engine.setUseClientMode(false); long verify_mode = miniSSLContext.callMethod(context, "verify_mode").convertToInteger("to_i").getLongValue(); if ((verify_mode & 0x1) != 0) { // 'peer' engine.setWantClientAuth(true); } if ((verify_mode & 0x2) != 0) { // 'force_peer' engine.setNeedClientAuth(true); } IRubyObject sslCipherListObject = miniSSLContext.callMethod(context, "ssl_cipher_list"); if (!sslCipherListObject.isNil()) { String[] sslCipherList = sslCipherListObject.convertToString().asJavaString().split(","); engine.setEnabledCipherSuites(sslCipherList); } SSLSession session = engine.getSession(); inboundNetData = new MiniSSLBuffer(session.getPacketBufferSize()); outboundAppData = new MiniSSLBuffer(session.getApplicationBufferSize()); outboundAppData.flip(); outboundNetData = new MiniSSLBuffer(session.getPacketBufferSize()); return this; } private TrustManager[] getTrustManagers(TrustManagerFactory factory) { final TrustManager[] tms = factory.getTrustManagers(); if (tms != null) { for (int i=0; i