From d9bdb5174fc6588af576bbf0724d1a9b204abf66 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Mon, 11 Oct 2021 10:21:16 +0100 Subject: [PATCH 1/5] adding alpn protocol test, based on an existign one from ruby-openssl --- src/test/ruby/ssl/test_session.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/test/ruby/ssl/test_session.rb b/src/test/ruby/ssl/test_session.rb index 5f8a4c6d..c6dd31f3 100644 --- a/src/test/ruby/ssl/test_session.rb +++ b/src/test/ruby/ssl/test_session.rb @@ -30,6 +30,25 @@ def test_session end end + def test_alpn_protocol_selection_ary + advertised = ["h2", "http/1.1"] + ctx_proc = Proc.new { |ctx| + ctx.alpn_select_cb = -> (protocols) { + protocols.first + } + } + start_server0(PORT, OpenSSL::SSL::VERIFY_NONE, true, ctx_proc: ctx_proc) do |server, port| + sock = TCPSocket.new("127.0.0.1", port) + ctx = OpenSSL::SSL::SSLContext.new("TLSv1_2") + ctx.alpn_protocols = advertised + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + ssl.sync_close = true + ssl.connect + assert_equal("h2", ssl.alpn_protocol) + ssl.puts "abc"; assert_equal "abc\n", ssl.gets + end + end + def test_exposes_session_error OpenSSL::SSL::Session::SessionError end From 8605594c7d020bf7f00e9fc5f6af73e8d61eb1cd Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Mon, 11 Oct 2021 10:45:21 +0100 Subject: [PATCH 2/5] added Ruby SSLContext/SSLSocket ALPN APIs support * SSLSocket now implements `#alpn_protocol` * SSLContext onw implements `#alpn_protocols` annd `#alpn_select_cb` They will be used to set the alpn parameters when using jsse BCSSLEngine, which supports ALPN negotiation. --- .../org/jruby/ext/openssl/SSLContext.java | 91 +++++++++++++++++-- .../java/org/jruby/ext/openssl/SSLSocket.java | 7 ++ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/SSLContext.java b/src/main/java/org/jruby/ext/openssl/SSLContext.java index f57c2de1..02c5e921 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLContext.java +++ b/src/main/java/org/jruby/ext/openssl/SSLContext.java @@ -53,6 +53,7 @@ import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyClass; +import org.jruby.RubyString; import org.jruby.RubyFixnum; import org.jruby.RubyHash; import org.jruby.RubyInteger; @@ -60,6 +61,7 @@ import org.jruby.RubyNumeric; import org.jruby.RubyObject; import org.jruby.RubySymbol; +import org.jruby.RubyProc; import org.jruby.anno.JRubyMethod; import org.jruby.common.IRubyWarnings.ID; import org.jruby.runtime.Arity; @@ -242,6 +244,8 @@ public static void createSSLContext(final Ruby runtime, final RubyModule SSL) { SSLContext.addReadWriteAttribute(context, "tmp_dh_callback"); SSLContext.addReadWriteAttribute(context, "servername_cb"); SSLContext.addReadWriteAttribute(context, "renegotiation_cb"); + SSLContext.addReadWriteAttribute(context, "alpn_protocols"); + SSLContext.addReadWriteAttribute(context, "alpn_select_cb"); SSLContext.defineAlias("ssl_timeout", "timeout"); SSLContext.defineAlias("ssl_timeout=", "timeout="); @@ -451,6 +455,30 @@ public IRubyObject setup(final ThreadContext context) { // SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb); } + final String[] alpnProtocols; + + value = getInstanceVariable("@alpn_protocols"); + if ( value != null && ! value.isNil() ) { + IRubyObject[] rArray = ((RubyArray) value).toJavaArray(); + String[] protos = new String[rArray.length]; + for(int i = 0; i < protos.length; i++) { + protos[i] = ((RubyString) rArray[i]).asJavaString(); + } + + alpnProtocols = protos; + } else { + alpnProtocols = null; + } + + final RubyProc alpnSelectCb; + value = getInstanceVariable("@alpn_select_cb"); + if ( value != null && ! value.isNil() ) { + alpnSelectCb = (RubyProc) value; + } else { + alpnSelectCb = null; + } + + // NOTE: no API under javax.net to support session get/new/remove callbacks /* val = ossl_sslctx_get_sess_id_ctx(self); @@ -477,7 +505,7 @@ public IRubyObject setup(final ThreadContext context) { */ try { - internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert, verifyMode, timeout); + internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert, verifyMode, timeout, alpnProtocols, alpnSelectCb); } catch (GeneralSecurityException e) { throw newSSLError(runtime, e); @@ -505,7 +533,7 @@ private RubyArray matchedCiphersWithCache(final ThreadContext context) { private RubyArray matchedCiphers(final ThreadContext context) { final Ruby runtime = context.runtime; try { - final String[] supported = getSupportedCipherSuites(protocol); + final String[] supported = getSupportedCipherSuites(runtime, protocol); final Collection cipherDefs = CipherStrings.matchingCiphers(this.ciphers, supported, false); @@ -688,14 +716,52 @@ private static class CipherListCache { } } - private static String[] getSupportedCipherSuites(final String protocol) + void setApplicationProtocols(SSLEngine engine) { + if ( !(engine instanceof org.bouncycastle.jsse.BCSSLEngine) ) { + return; + } + if ( protocolForClient ) { + if ( internalContext.alpnProtocols != null ) { + org.bouncycastle.jsse.BCSSLParameters bcParams = new org.bouncycastle.jsse.BCSSLParameters(); + bcParams.setApplicationProtocols(internalContext.alpnProtocols); + bcParams.setCipherSuites(engine.getSupportedCipherSuites()); // TODO + ((org.bouncycastle.jsse.BCSSLEngine) engine).setParameters(bcParams); + } + } + if ( protocolForServer ) { + if ( internalContext.alpnSelectCb != null ) { + ((org.bouncycastle.jsse.BCSSLEngine) engine).setBCHandshakeApplicationProtocolSelector( + new org.bouncycastle.jsse.BCApplicationProtocolSelector() + { + public String select(SSLEngine engine, List protocols) { + Ruby runtime = internalContext.alpnSelectCb.getRuntime(); + IRubyObject[] arr = new IRubyObject[protocols.size()]; + for(int i = 0; i < arr.length; i++) { + arr[i] = runtime.newString(protocols.get(i)); + } + RubyArray array = RubyArray.newArray(runtime, arr); + + IRubyObject selectedProtocol = internalContext.alpnSelectCb.getBlock().call(runtime.getCurrentContext(), (IRubyObject) array); + if (selectedProtocol != null) { + return ((RubyString) selectedProtocol).toString(); + } + + return null; + } + }); + } + } + + } + + private static String[] getSupportedCipherSuites(Ruby runtime, final String protocol) throws GeneralSecurityException { - return dummySSLEngine(protocol).getSupportedCipherSuites(); + return dummySSLEngine(runtime, protocol).getSupportedCipherSuites(); } - private static SSLEngine dummySSLEngine(final String protocol) throws GeneralSecurityException { + private static SSLEngine dummySSLEngine(Ruby runtime, final String protocol) throws GeneralSecurityException { javax.net.ssl.SSLContext sslContext = SecurityHelper.getSSLContext(protocol); - sslContext.init(null, null, null); + sslContext.init(null, null, OpenSSL.getSecureRandom(runtime)); return sslContext.createSSLEngine(); } @@ -899,8 +965,9 @@ static RubyClass _SSLContext(final Ruby runtime) { private InternalContext createInternalContext(ThreadContext context, final X509Cert xCert, final PKey pKey, final Store store, final List clientCert, final List extraChainCert, - final int verifyMode, final int timeout) throws NoSuchAlgorithmException, KeyManagementException { - InternalContext internalContext = new InternalContext(xCert, pKey, store, clientCert, extraChainCert, verifyMode, timeout); + final int verifyMode, final int timeout, + final String[] alpnProtocols, final RubyProc alpnSelectCb) throws NoSuchAlgorithmException, KeyManagementException { + InternalContext internalContext = new InternalContext(xCert, pKey, store, clientCert, extraChainCert, verifyMode, timeout, alpnProtocols, alpnSelectCb); internalContext.initSSLContext(context); return internalContext; } @@ -917,7 +984,9 @@ private class InternalContext { final List clientCert, final List extraChainCert, final int verifyMode, - final int timeout) throws NoSuchAlgorithmException { + final int timeout, + final String[] alpnProtocols, + final RubyProc alpnSelectCb) throws NoSuchAlgorithmException { if ( pKey != null && xCert != null ) { this.privateKey = pKey.getPrivateKey(); @@ -935,6 +1004,8 @@ private class InternalContext { this.extraChainCert = extraChainCert; this.verifyMode = verifyMode; this.timeout = timeout; + this.alpnProtocols = alpnProtocols; + this.alpnSelectCb = alpnSelectCb; // initialize SSL context : @@ -981,6 +1052,8 @@ void initSSLContext(final ThreadContext context) throws KeyManagementException { final List extraChainCert; // empty assumed == null private final int timeout; + private final String[] alpnProtocols; + private final RubyProc alpnSelectCb; private final javax.net.ssl.SSLContext sslContext; diff --git a/src/main/java/org/jruby/ext/openssl/SSLSocket.java b/src/main/java/org/jruby/ext/openssl/SSLSocket.java index 147d79e1..3d2dce08 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLSocket.java +++ b/src/main/java/org/jruby/ext/openssl/SSLSocket.java @@ -238,6 +238,11 @@ private SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean serv @JRubyMethod(name = "context") public final SSLContext context() { return this.sslContext; } + @JRubyMethod(name = "alpn_protocol") + public final RubyString alpn_protocol(final ThreadContext context) { + return RubyString.newString(context.runtime, this.engine.getApplicationProtocol()); + } + @JRubyMethod(name = "sync") public IRubyObject sync(final ThreadContext context) { final CallSite[] sites = getMetaClass().getExtraCallSites(); @@ -285,6 +290,7 @@ private IRubyObject connectImpl(final ThreadContext context, final boolean block if ( ! initialHandshake ) { SSLEngine engine = ossl_ssl_setup(context, true); engine.setUseClientMode(true); + sslContext.setApplicationProtocols(engine); engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); initialHandshake = true; @@ -359,6 +365,7 @@ private IRubyObject acceptImpl(final ThreadContext context, final boolean blocki engine.setNeedClientAuth(true); } } + sslContext.setApplicationProtocols(engine); engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); initialHandshake = true; From e7a665667d6b0ab3f67c71cb3020764dd46c3dd3 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 3 Feb 2022 07:07:50 +0100 Subject: [PATCH 3/5] [refactor] get ALPN working with Java SSL APIs there's really no need to resort to BC engine --- .../org/jruby/ext/openssl/SSLContext.java | 86 +++++++++---------- .../java/org/jruby/ext/openssl/SSLSocket.java | 5 +- src/test/ruby/ssl/test_session.rb | 2 + 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/SSLContext.java b/src/main/java/org/jruby/ext/openssl/SSLContext.java index 02c5e921..cc61b17c 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLContext.java +++ b/src/main/java/org/jruby/ext/openssl/SSLContext.java @@ -45,6 +45,7 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedKeyManager; @@ -459,13 +460,12 @@ public IRubyObject setup(final ThreadContext context) { value = getInstanceVariable("@alpn_protocols"); if ( value != null && ! value.isNil() ) { - IRubyObject[] rArray = ((RubyArray) value).toJavaArray(); - String[] protos = new String[rArray.length]; - for(int i = 0; i < protos.length; i++) { - protos[i] = ((RubyString) rArray[i]).asJavaString(); + IRubyObject[] alpn_protocols = ((RubyArray) value).toJavaArrayMaybeUnsafe(); + String[] protocols = new String[alpn_protocols.length]; + for(int i = 0; i < protocols.length; i++) { + protocols[i] = alpn_protocols[i].convertToString().asJavaString(); } - - alpnProtocols = protos; + alpnProtocols = protocols; } else { alpnProtocols = null; } @@ -505,7 +505,8 @@ public IRubyObject setup(final ThreadContext context) { */ try { - internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert, verifyMode, timeout, alpnProtocols, alpnSelectCb); + internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert, + verifyMode, timeout, alpnProtocols, alpnSelectCb); } catch (GeneralSecurityException e) { throw newSSLError(runtime, e); @@ -716,42 +717,38 @@ private static class CipherListCache { } } - void setApplicationProtocols(SSLEngine engine) { - if ( !(engine instanceof org.bouncycastle.jsse.BCSSLEngine) ) { - return; - } - if ( protocolForClient ) { - if ( internalContext.alpnProtocols != null ) { - org.bouncycastle.jsse.BCSSLParameters bcParams = new org.bouncycastle.jsse.BCSSLParameters(); - bcParams.setApplicationProtocols(internalContext.alpnProtocols); - bcParams.setCipherSuites(engine.getSupportedCipherSuites()); // TODO - ((org.bouncycastle.jsse.BCSSLEngine) engine).setParameters(bcParams); - } - } - if ( protocolForServer ) { - if ( internalContext.alpnSelectCb != null ) { - ((org.bouncycastle.jsse.BCSSLEngine) engine).setBCHandshakeApplicationProtocolSelector( - new org.bouncycastle.jsse.BCApplicationProtocolSelector() - { - public String select(SSLEngine engine, List protocols) { - Ruby runtime = internalContext.alpnSelectCb.getRuntime(); - IRubyObject[] arr = new IRubyObject[protocols.size()]; - for(int i = 0; i < arr.length; i++) { - arr[i] = runtime.newString(protocols.get(i)); - } - RubyArray array = RubyArray.newArray(runtime, arr); - - IRubyObject selectedProtocol = internalContext.alpnSelectCb.getBlock().call(runtime.getCurrentContext(), (IRubyObject) array); - if (selectedProtocol != null) { - return ((RubyString) selectedProtocol).toString(); - } - - return null; - } - }); - } + void setApplicationProtocolsOrSelector(final SSLEngine engine) { + setApplicationProtocolSelector(engine); + setApplicationProtocols(engine); + } + + private void setApplicationProtocolSelector(final SSLEngine engine) { + final RubyProc alpn_select_cb = internalContext.alpnSelectCallback; + if (alpn_select_cb != null) { + engine.setHandshakeApplicationProtocolSelector((_engine, protocols) -> { + final Ruby runtime = getRuntime(); + IRubyObject[] rubyProtocols = new IRubyObject[protocols.size()]; + int i = 0; for (String protocol : protocols) { + rubyProtocols[i++] = runtime.newString(protocol); + } + + IRubyObject[] args = new IRubyObject[] { RubyArray.newArray(runtime, rubyProtocols) }; + IRubyObject selected_protocol = alpn_select_cb.call(runtime.getCurrentContext(), args); + if (selected_protocol != null && !selected_protocol.isNil()) { + return ((RubyString) selected_protocol).asJavaString(); + } + return null; // callback returned nil - none of the advertised names are acceptable + }); } + } + private void setApplicationProtocols(final SSLEngine engine) { + final String[] alpn_protocols = internalContext.alpnProtocols; + if (alpn_protocols != null) { + SSLParameters params = engine.getSSLParameters(); + params.setApplicationProtocols(alpn_protocols); + engine.setSSLParameters(params); + } } private static String[] getSupportedCipherSuites(Ruby runtime, final String protocol) @@ -986,7 +983,7 @@ private class InternalContext { final int verifyMode, final int timeout, final String[] alpnProtocols, - final RubyProc alpnSelectCb) throws NoSuchAlgorithmException { + final RubyProc alpnSelectCallback) throws NoSuchAlgorithmException { if ( pKey != null && xCert != null ) { this.privateKey = pKey.getPrivateKey(); @@ -1005,7 +1002,7 @@ private class InternalContext { this.verifyMode = verifyMode; this.timeout = timeout; this.alpnProtocols = alpnProtocols; - this.alpnSelectCb = alpnSelectCb; + this.alpnSelectCallback = alpnSelectCallback; // initialize SSL context : @@ -1052,8 +1049,9 @@ void initSSLContext(final ThreadContext context) throws KeyManagementException { final List extraChainCert; // empty assumed == null private final int timeout; + private final String[] alpnProtocols; - private final RubyProc alpnSelectCb; + private final RubyProc alpnSelectCallback; private final javax.net.ssl.SSLContext sslContext; diff --git a/src/main/java/org/jruby/ext/openssl/SSLSocket.java b/src/main/java/org/jruby/ext/openssl/SSLSocket.java index 3d2dce08..d24a334c 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLSocket.java +++ b/src/main/java/org/jruby/ext/openssl/SSLSocket.java @@ -229,6 +229,9 @@ private SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean serv dummy = ByteBuffer.allocate(0); this.engine = engine; copySessionSetupIfSet(context); + + sslContext.setApplicationProtocolsOrSelector(engine); + return engine; } @@ -290,7 +293,6 @@ private IRubyObject connectImpl(final ThreadContext context, final boolean block if ( ! initialHandshake ) { SSLEngine engine = ossl_ssl_setup(context, true); engine.setUseClientMode(true); - sslContext.setApplicationProtocols(engine); engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); initialHandshake = true; @@ -365,7 +367,6 @@ private IRubyObject acceptImpl(final ThreadContext context, final boolean blocki engine.setNeedClientAuth(true); } } - sslContext.setApplicationProtocols(engine); engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); initialHandshake = true; diff --git a/src/test/ruby/ssl/test_session.rb b/src/test/ruby/ssl/test_session.rb index c6dd31f3..71d39a3f 100644 --- a/src/test/ruby/ssl/test_session.rb +++ b/src/test/ruby/ssl/test_session.rb @@ -34,6 +34,8 @@ def test_alpn_protocol_selection_ary advertised = ["h2", "http/1.1"] ctx_proc = Proc.new { |ctx| ctx.alpn_select_cb = -> (protocols) { + assert_equal Array, protocols.class + assert_equal advertised, protocols protocols.first } } From 8946b0b9daa472c1a0b201c8a6fe8253ee824ca6 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 3 Feb 2022 07:08:11 +0100 Subject: [PATCH 4/5] [fix] alpn_protocol might return nil --- src/main/java/org/jruby/ext/openssl/SSLSocket.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/SSLSocket.java b/src/main/java/org/jruby/ext/openssl/SSLSocket.java index d24a334c..1b458d8b 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLSocket.java +++ b/src/main/java/org/jruby/ext/openssl/SSLSocket.java @@ -242,8 +242,9 @@ private SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean serv public final SSLContext context() { return this.sslContext; } @JRubyMethod(name = "alpn_protocol") - public final RubyString alpn_protocol(final ThreadContext context) { - return RubyString.newString(context.runtime, this.engine.getApplicationProtocol()); + public IRubyObject alpn_protocol(final ThreadContext context) { + final String protocol = engine.getApplicationProtocol(); + return protocol == null ? context.nil : RubyString.newString(context.runtime, protocol); } @JRubyMethod(name = "sync") From ede62c002b2271ad4ce5b87af8531ebe7db8ecc1 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 3 Feb 2022 07:08:43 +0100 Subject: [PATCH 5/5] [fix] inverse flags passed (param not used atm) --- src/main/java/org/jruby/ext/openssl/SSLSocket.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/SSLSocket.java b/src/main/java/org/jruby/ext/openssl/SSLSocket.java index 1b458d8b..c40d6d96 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLSocket.java +++ b/src/main/java/org/jruby/ext/openssl/SSLSocket.java @@ -292,7 +292,7 @@ private IRubyObject connectImpl(final ThreadContext context, final boolean block try { if ( ! initialHandshake ) { - SSLEngine engine = ossl_ssl_setup(context, true); + SSLEngine engine = ossl_ssl_setup(context, false); engine.setUseClientMode(true); engine.beginHandshake(); handshakeStatus = engine.getHandshakeStatus(); @@ -352,7 +352,7 @@ private IRubyObject acceptImpl(final ThreadContext context, final boolean blocki try { if ( ! initialHandshake ) { - final SSLEngine engine = ossl_ssl_setup(context, false); + final SSLEngine engine = ossl_ssl_setup(context, true); engine.setUseClientMode(false); final IRubyObject verify_mode = verify_mode(context); if ( verify_mode != context.nil ) {