Skip to content

Commit 167b0d6

Browse files
[feat] support alpn negotiation in ssl context (#247)
* added Ruby SSLContext/SSLSocket ALPN APIs support * adding alpn protocol test, based on an existign one from ruby-openssl Co-authored-by: HoneyryderChuck <[email protected]>
1 parent 23fabf1 commit 167b0d6

File tree

3 files changed

+112
-11
lines changed

3 files changed

+112
-11
lines changed

src/main/java/org/jruby/ext/openssl/SSLContext.java

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
import javax.net.ssl.KeyManager;
4747
import javax.net.ssl.SSLEngine;
48+
import javax.net.ssl.SSLParameters;
4849
import javax.net.ssl.SSLSessionContext;
4950
import javax.net.ssl.TrustManager;
5051
import javax.net.ssl.X509ExtendedKeyManager;
@@ -53,13 +54,15 @@
5354
import org.jruby.Ruby;
5455
import org.jruby.RubyArray;
5556
import org.jruby.RubyClass;
57+
import org.jruby.RubyString;
5658
import org.jruby.RubyFixnum;
5759
import org.jruby.RubyHash;
5860
import org.jruby.RubyInteger;
5961
import org.jruby.RubyModule;
6062
import org.jruby.RubyNumeric;
6163
import org.jruby.RubyObject;
6264
import org.jruby.RubySymbol;
65+
import org.jruby.RubyProc;
6366
import org.jruby.anno.JRubyMethod;
6467
import org.jruby.common.IRubyWarnings.ID;
6568
import org.jruby.runtime.Arity;
@@ -242,6 +245,8 @@ public static void createSSLContext(final Ruby runtime, final RubyModule SSL) {
242245
SSLContext.addReadWriteAttribute(context, "tmp_dh_callback");
243246
SSLContext.addReadWriteAttribute(context, "servername_cb");
244247
SSLContext.addReadWriteAttribute(context, "renegotiation_cb");
248+
SSLContext.addReadWriteAttribute(context, "alpn_protocols");
249+
SSLContext.addReadWriteAttribute(context, "alpn_select_cb");
245250

246251
SSLContext.defineAlias("ssl_timeout", "timeout");
247252
SSLContext.defineAlias("ssl_timeout=", "timeout=");
@@ -451,6 +456,29 @@ public IRubyObject setup(final ThreadContext context) {
451456
// SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb);
452457
}
453458

459+
final String[] alpnProtocols;
460+
461+
value = getInstanceVariable("@alpn_protocols");
462+
if ( value != null && ! value.isNil() ) {
463+
IRubyObject[] alpn_protocols = ((RubyArray) value).toJavaArrayMaybeUnsafe();
464+
String[] protocols = new String[alpn_protocols.length];
465+
for(int i = 0; i < protocols.length; i++) {
466+
protocols[i] = alpn_protocols[i].convertToString().asJavaString();
467+
}
468+
alpnProtocols = protocols;
469+
} else {
470+
alpnProtocols = null;
471+
}
472+
473+
final RubyProc alpnSelectCb;
474+
value = getInstanceVariable("@alpn_select_cb");
475+
if ( value != null && ! value.isNil() ) {
476+
alpnSelectCb = (RubyProc) value;
477+
} else {
478+
alpnSelectCb = null;
479+
}
480+
481+
454482
// NOTE: no API under javax.net to support session get/new/remove callbacks
455483
/*
456484
val = ossl_sslctx_get_sess_id_ctx(self);
@@ -477,7 +505,8 @@ public IRubyObject setup(final ThreadContext context) {
477505
*/
478506

479507
try {
480-
internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert, verifyMode, timeout);
508+
internalContext = createInternalContext(context, cert, key, store, clientCert, extraChainCert,
509+
verifyMode, timeout, alpnProtocols, alpnSelectCb);
481510
}
482511
catch (GeneralSecurityException e) {
483512
throw newSSLError(runtime, e);
@@ -505,7 +534,7 @@ private RubyArray matchedCiphersWithCache(final ThreadContext context) {
505534
private RubyArray matchedCiphers(final ThreadContext context) {
506535
final Ruby runtime = context.runtime;
507536
try {
508-
final String[] supported = getSupportedCipherSuites(protocol);
537+
final String[] supported = getSupportedCipherSuites(runtime, protocol);
509538
final Collection<CipherStrings.Def> cipherDefs =
510539
CipherStrings.matchingCiphers(this.ciphers, supported, false);
511540

@@ -688,14 +717,48 @@ private static class CipherListCache {
688717
}
689718
}
690719

691-
private static String[] getSupportedCipherSuites(final String protocol)
720+
void setApplicationProtocolsOrSelector(final SSLEngine engine) {
721+
setApplicationProtocolSelector(engine);
722+
setApplicationProtocols(engine);
723+
}
724+
725+
private void setApplicationProtocolSelector(final SSLEngine engine) {
726+
final RubyProc alpn_select_cb = internalContext.alpnSelectCallback;
727+
if (alpn_select_cb != null) {
728+
engine.setHandshakeApplicationProtocolSelector((_engine, protocols) -> {
729+
final Ruby runtime = getRuntime();
730+
IRubyObject[] rubyProtocols = new IRubyObject[protocols.size()];
731+
int i = 0; for (String protocol : protocols) {
732+
rubyProtocols[i++] = runtime.newString(protocol);
733+
}
734+
735+
IRubyObject[] args = new IRubyObject[] { RubyArray.newArray(runtime, rubyProtocols) };
736+
IRubyObject selected_protocol = alpn_select_cb.call(runtime.getCurrentContext(), args);
737+
if (selected_protocol != null && !selected_protocol.isNil()) {
738+
return ((RubyString) selected_protocol).asJavaString();
739+
}
740+
return null; // callback returned nil - none of the advertised names are acceptable
741+
});
742+
}
743+
}
744+
745+
private void setApplicationProtocols(final SSLEngine engine) {
746+
final String[] alpn_protocols = internalContext.alpnProtocols;
747+
if (alpn_protocols != null) {
748+
SSLParameters params = engine.getSSLParameters();
749+
params.setApplicationProtocols(alpn_protocols);
750+
engine.setSSLParameters(params);
751+
}
752+
}
753+
754+
private static String[] getSupportedCipherSuites(Ruby runtime, final String protocol)
692755
throws GeneralSecurityException {
693-
return dummySSLEngine(protocol).getSupportedCipherSuites();
756+
return dummySSLEngine(runtime, protocol).getSupportedCipherSuites();
694757
}
695758

696-
private static SSLEngine dummySSLEngine(final String protocol) throws GeneralSecurityException {
759+
private static SSLEngine dummySSLEngine(Ruby runtime, final String protocol) throws GeneralSecurityException {
697760
javax.net.ssl.SSLContext sslContext = SecurityHelper.getSSLContext(protocol);
698-
sslContext.init(null, null, null);
761+
sslContext.init(null, null, OpenSSL.getSecureRandom(runtime));
699762
return sslContext.createSSLEngine();
700763
}
701764

@@ -899,8 +962,9 @@ static RubyClass _SSLContext(final Ruby runtime) {
899962
private InternalContext createInternalContext(ThreadContext context,
900963
final X509Cert xCert, final PKey pKey, final Store store,
901964
final List<X509AuxCertificate> clientCert, final List<X509AuxCertificate> extraChainCert,
902-
final int verifyMode, final int timeout) throws NoSuchAlgorithmException, KeyManagementException {
903-
InternalContext internalContext = new InternalContext(xCert, pKey, store, clientCert, extraChainCert, verifyMode, timeout);
965+
final int verifyMode, final int timeout,
966+
final String[] alpnProtocols, final RubyProc alpnSelectCb) throws NoSuchAlgorithmException, KeyManagementException {
967+
InternalContext internalContext = new InternalContext(xCert, pKey, store, clientCert, extraChainCert, verifyMode, timeout, alpnProtocols, alpnSelectCb);
904968
internalContext.initSSLContext(context);
905969
return internalContext;
906970
}
@@ -917,7 +981,9 @@ private class InternalContext {
917981
final List<X509AuxCertificate> clientCert,
918982
final List<X509AuxCertificate> extraChainCert,
919983
final int verifyMode,
920-
final int timeout) throws NoSuchAlgorithmException {
984+
final int timeout,
985+
final String[] alpnProtocols,
986+
final RubyProc alpnSelectCallback) throws NoSuchAlgorithmException {
921987

922988
if ( pKey != null && xCert != null ) {
923989
this.privateKey = pKey.getPrivateKey();
@@ -935,6 +1001,8 @@ private class InternalContext {
9351001
this.extraChainCert = extraChainCert;
9361002
this.verifyMode = verifyMode;
9371003
this.timeout = timeout;
1004+
this.alpnProtocols = alpnProtocols;
1005+
this.alpnSelectCallback = alpnSelectCallback;
9381006

9391007
// initialize SSL context :
9401008

@@ -982,6 +1050,9 @@ void initSSLContext(final ThreadContext context) throws KeyManagementException {
9821050

9831051
private final int timeout;
9841052

1053+
private final String[] alpnProtocols;
1054+
private final RubyProc alpnSelectCallback;
1055+
9851056
private final javax.net.ssl.SSLContext sslContext;
9861057

9871058
// part of ssl_verify_cert_chain

src/main/java/org/jruby/ext/openssl/SSLSocket.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ private SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean serv
229229
dummy = ByteBuffer.allocate(0);
230230
this.engine = engine;
231231
copySessionSetupIfSet(context);
232+
233+
sslContext.setApplicationProtocolsOrSelector(engine);
234+
232235
return engine;
233236
}
234237

@@ -238,6 +241,12 @@ private SSLEngine ossl_ssl_setup(final ThreadContext context, final boolean serv
238241
@JRubyMethod(name = "context")
239242
public final SSLContext context() { return this.sslContext; }
240243

244+
@JRubyMethod(name = "alpn_protocol")
245+
public IRubyObject alpn_protocol(final ThreadContext context) {
246+
final String protocol = engine.getApplicationProtocol();
247+
return protocol == null ? context.nil : RubyString.newString(context.runtime, protocol);
248+
}
249+
241250
@JRubyMethod(name = "sync")
242251
public IRubyObject sync(final ThreadContext context) {
243252
final CallSite[] sites = getMetaClass().getExtraCallSites();
@@ -283,7 +292,7 @@ private IRubyObject connectImpl(final ThreadContext context, final boolean block
283292

284293
try {
285294
if ( ! initialHandshake ) {
286-
SSLEngine engine = ossl_ssl_setup(context, true);
295+
SSLEngine engine = ossl_ssl_setup(context, false);
287296
engine.setUseClientMode(true);
288297
engine.beginHandshake();
289298
handshakeStatus = engine.getHandshakeStatus();
@@ -343,7 +352,7 @@ private IRubyObject acceptImpl(final ThreadContext context, final boolean blocki
343352

344353
try {
345354
if ( ! initialHandshake ) {
346-
final SSLEngine engine = ossl_ssl_setup(context, false);
355+
final SSLEngine engine = ossl_ssl_setup(context, true);
347356
engine.setUseClientMode(false);
348357
final IRubyObject verify_mode = verify_mode(context);
349358
if ( verify_mode != context.nil ) {

src/test/ruby/ssl/test_session.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ def test_session
3030
end
3131
end
3232

33+
def test_alpn_protocol_selection_ary
34+
advertised = ["h2", "http/1.1"]
35+
ctx_proc = Proc.new { |ctx|
36+
ctx.alpn_select_cb = -> (protocols) {
37+
assert_equal Array, protocols.class
38+
assert_equal advertised, protocols
39+
protocols.first
40+
}
41+
}
42+
start_server0(PORT, OpenSSL::SSL::VERIFY_NONE, true, ctx_proc: ctx_proc) do |server, port|
43+
sock = TCPSocket.new("127.0.0.1", port)
44+
ctx = OpenSSL::SSL::SSLContext.new("TLSv1_2")
45+
ctx.alpn_protocols = advertised
46+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
47+
ssl.sync_close = true
48+
ssl.connect
49+
assert_equal("h2", ssl.alpn_protocol)
50+
ssl.puts "abc"; assert_equal "abc\n", ssl.gets
51+
end
52+
end
53+
3354
def test_exposes_session_error
3455
OpenSSL::SSL::Session::SessionError
3556
end

0 commit comments

Comments
 (0)