From 0e380681b7a3185c9bf73236de07d30bb0ad1233 Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Thu, 22 Oct 2026 16:18:09 +0900 Subject: [PATCH 1/3] [fix] support AES128, AES256 and CHACHA20 cipher aliases These were not registered, so ciphers = 'AES128' raised "no cipher match", and in a "+" rule the unknown name was skipped: EECDH+CHACHA20 and EECDH+AES128 both selected every EECDH suite. AES128 and AES256 match on key size. An unknown name in a "+" rule now drops the whole rule, as libssl's ssl_cipher_process_rulestr does. --- .../org/jruby/ext/openssl/CipherStrings.java | 29 +++++++++++++----- .../jruby/ext/openssl/CipherStringsTest.java | 30 ++++++++++++++++++- test/ssl/test_context.rb | 16 ++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/CipherStrings.java b/src/main/java/org/jruby/ext/openssl/CipherStrings.java index 6f69517b..2c85abbe 100644 --- a/src/main/java/org/jruby/ext/openssl/CipherStrings.java +++ b/src/main/java/org/jruby/ext/openssl/CipherStrings.java @@ -494,6 +494,7 @@ static final class Def implements Comparable, Cloneable { private final long mask; private final long algStrengthMask; private final long requiredAlgorithms; + private int requiredStrengthBits; // 0 matches any // OpenSSL 1.1.1 private long algorithm_mkey; @@ -607,6 +608,17 @@ Def setCipherSuite(final String suite) { } } + Def withStrengthBits(final int bits) { + try { + Def clone = (Def) super.clone(); + clone.requiredStrengthBits = bits; + return clone; + } + catch (CloneNotSupportedException e) { + throw new AssertionError(e); // won't happen + } + } + @Override public int hashCode() { return name.hashCode(); @@ -636,6 +648,7 @@ public String toString() { // from ssl_cipher_apply_rule public boolean matches(Def current) { if ( (current.algorithms & requiredAlgorithms) != requiredAlgorithms ) return false; + if ( requiredStrengthBits != 0 && current.algStrengthBits != requiredStrengthBits ) return false; // ma = mask & cp->algorithms; // ma_s = mask_strength & cp->algo_strength; // @@ -760,13 +773,12 @@ private static Collection matching(final String[] defs, final String[] all, Collection matching = null; for ( final String name : defs ) { final Def pattern = Definitions.get(name); - if ( pattern != null ) { - if ( matching == null ) { - matching = matchingPattern(pattern, all, true, setSuite); - } - else { - matching.retainAll( matchingPattern(pattern, all, false, setSuite) ); - } + if ( pattern == null ) return null; // unknown name drops the rule (libssl ssl_cipher_process_rulestr) + if ( matching == null ) { + matching = matchingPattern(pattern, all, true, setSuite); + } + else { + matching.retainAll( matchingPattern(pattern, all, false, setSuite) ); } } return matching; @@ -847,6 +859,9 @@ static Def suiteToDef(final String cipherSuite) { Definitions.put(SSL_TXT_eNULL,new Def(0,SSL_TXT_eNULL,0,SSL_eNULL,0,0,0,0,SSL_ENC_MASK,0)); Definitions.put(SSL_TXT_eFZA,new Def(0,SSL_TXT_eFZA,0,SSL_eFZA, 0,0,0,0,SSL_ENC_MASK,0)); Definitions.put(SSL_TXT_AES,new Def(0,SSL_TXT_AES, 0,SSL_AES, 0,0,0,0,SSL_ENC_MASK,0)); + Definitions.put(SSL_TXT_AES128,new Def(0,SSL_TXT_AES128,0,SSL_AES,0,0,0,0,SSL_ENC_MASK,0).withStrengthBits(128)); + Definitions.put(SSL_TXT_AES256,new Def(0,SSL_TXT_AES256,0,SSL_AES,0,0,0,0,SSL_ENC_MASK,0).withStrengthBits(256)); + Definitions.put(SSL_TXT_CHACHA20,new Def(0,SSL_TXT_CHACHA20,0,SSL_CHACHA20,0,0,0,0,SSL_ENC_MASK,0)); Definitions.put(SSL_TXT_MD5,new Def(0,SSL_TXT_MD5, 0,SSL_MD5, 0,0,0,0,SSL_MAC_MASK,0)); Definitions.put(SSL_TXT_SHA1,new Def(0,SSL_TXT_SHA1,0,SSL_SHA1, 0,0,0,0,SSL_MAC_MASK,0)); Definitions.put(SSL_TXT_SHA,new Def(0,SSL_TXT_SHA, 0,SSL_SHA, 0,0,0,0,SSL_MAC_MASK,0)); diff --git a/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java b/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java index a6ca67d2..f9ac679f 100644 --- a/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java +++ b/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java @@ -54,8 +54,36 @@ public void ecdheSelectorsMatchOpenSSLAuthenticationRules() { ), javaNames); } + private static final String[] ENC_SUITES = { + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + }; + + @Test + public void encryptionSelectorsMatchKeySize() { + assertEquals(Arrays.asList("ECDHE-RSA-AES128-GCM-SHA256", "DHE-RSA-AES128-SHA"), names("AES128", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-AES256-GCM-SHA384"), names("AES256", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-CHACHA20-POLY1305", "DHE-RSA-CHACHA20-POLY1305"), names("CHACHA20", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-CHACHA20-POLY1305"), names("EECDH+CHACHA20", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-AES128-GCM-SHA256"), names("EECDH+AES128", ENC_SUITES)); + } + + @Test + public void unknownAliasVoidsCombinedRule() { + assertEquals(Arrays.asList(), names("EECDH+FOO", ENC_SUITES)); + assertEquals(Arrays.asList(), names("FOO+EECDH", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-AES256-GCM-SHA384"), names("EECDH+FOO:EECDH+AES256", ENC_SUITES)); + } + private static List names(final String selector) { - final Collection ciphers = CipherStrings.matchingCiphers(selector, SUITES, false); + return names(selector, SUITES); + } + + private static List names(final String selector, final String[] suites) { + final Collection ciphers = CipherStrings.matchingCiphers(selector, suites, false); final List names = new ArrayList<>(ciphers.size()); for (final CipherStrings.Def cipher : ciphers) names.add(cipher.name); return names; diff --git a/test/ssl/test_context.rb b/test/ssl/test_context.rb index cd4cb2a8..26290083 100644 --- a/test/ssl/test_context.rb +++ b/test/ssl/test_context.rb @@ -200,6 +200,22 @@ def test_set_ciphers_by_ecdh_group_name end end + def test_set_ciphers_by_encryption_alias + context = OpenSSL::SSL::SSLContext.new + context.ciphers = 'EECDH+CHACHA20:EECDH+AES128' + selected = context.ciphers.map(&:first) + assert_include selected, 'ECDHE-RSA-CHACHA20-POLY1305' + assert_include selected, 'ECDHE-RSA-AES128-GCM-SHA256' + assert_not_include selected, 'ECDHE-RSA-AES256-GCM-SHA384' + + context.ciphers = 'AES256' + selected = context.ciphers.map(&:first) + assert_include selected, 'ECDHE-RSA-AES256-GCM-SHA384' + assert_not_include selected, 'ECDHE-RSA-AES128-GCM-SHA256' + + assert_raise(OpenSSL::SSL::SSLError) { context.ciphers = 'EECDH+FOO' } + end + def test_set_ciphers_by_cipher_name context = OpenSSL::SSL::SSLContext.new context.ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384" From 1b8830377529cf13fc22fdfcab905822861c52f9 Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Thu, 22 Oct 2026 18:01:06 +0900 Subject: [PATCH 2/3] Pass AES128/AES256 key size through the constructor instead of a clone --- .../org/jruby/ext/openssl/CipherStrings.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/CipherStrings.java b/src/main/java/org/jruby/ext/openssl/CipherStrings.java index 2c85abbe..c79b0518 100644 --- a/src/main/java/org/jruby/ext/openssl/CipherStrings.java +++ b/src/main/java/org/jruby/ext/openssl/CipherStrings.java @@ -494,7 +494,7 @@ static final class Def implements Comparable, Cloneable { private final long mask; private final long algStrengthMask; private final long requiredAlgorithms; - private int requiredStrengthBits; // 0 matches any + private final int requiredStrengthBits; // 0 matches any // OpenSSL 1.1.1 private long algorithm_mkey; @@ -519,9 +519,14 @@ static final class Def implements Comparable, Cloneable { this.mask = mask; this.algStrengthMask = maskStrength; this.requiredAlgorithms = 0; + this.requiredStrengthBits = 0; } Def(String name, long algorithms, long mask, long requiredAlgorithms) { + this(name, algorithms, mask, requiredAlgorithms, 0); + } + + Def(String name, long algorithms, long mask, long requiredAlgorithms, int requiredStrengthBits) { this.valid = false; this.name = name; this.id = 0; @@ -532,6 +537,7 @@ static final class Def implements Comparable, Cloneable { this.mask = mask; this.algStrengthMask = 0; this.requiredAlgorithms = requiredAlgorithms; + this.requiredStrengthBits = requiredStrengthBits; } Def(String name, long algorithms, long algo_strength, int strength_bits, int alg_bits, long mask, long maskStrength) { @@ -545,6 +551,7 @@ static final class Def implements Comparable, Cloneable { this.mask = mask; this.algStrengthMask = maskStrength; this.requiredAlgorithms = 0; + this.requiredStrengthBits = 0; } Def(int valid, String name, String stdname, /* RFC name */ @@ -579,6 +586,7 @@ static final class Def implements Comparable, Cloneable { this.mask = 0; this.algStrengthMask = 0; this.requiredAlgorithms = 0; + this.requiredStrengthBits = 0; } @@ -608,17 +616,6 @@ Def setCipherSuite(final String suite) { } } - Def withStrengthBits(final int bits) { - try { - Def clone = (Def) super.clone(); - clone.requiredStrengthBits = bits; - return clone; - } - catch (CloneNotSupportedException e) { - throw new AssertionError(e); // won't happen - } - } - @Override public int hashCode() { return name.hashCode(); @@ -859,8 +856,8 @@ static Def suiteToDef(final String cipherSuite) { Definitions.put(SSL_TXT_eNULL,new Def(0,SSL_TXT_eNULL,0,SSL_eNULL,0,0,0,0,SSL_ENC_MASK,0)); Definitions.put(SSL_TXT_eFZA,new Def(0,SSL_TXT_eFZA,0,SSL_eFZA, 0,0,0,0,SSL_ENC_MASK,0)); Definitions.put(SSL_TXT_AES,new Def(0,SSL_TXT_AES, 0,SSL_AES, 0,0,0,0,SSL_ENC_MASK,0)); - Definitions.put(SSL_TXT_AES128,new Def(0,SSL_TXT_AES128,0,SSL_AES,0,0,0,0,SSL_ENC_MASK,0).withStrengthBits(128)); - Definitions.put(SSL_TXT_AES256,new Def(0,SSL_TXT_AES256,0,SSL_AES,0,0,0,0,SSL_ENC_MASK,0).withStrengthBits(256)); + Definitions.put(SSL_TXT_AES128,new Def(SSL_TXT_AES128,SSL_AES,SSL_ENC_MASK,SSL_AES,128)); + Definitions.put(SSL_TXT_AES256,new Def(SSL_TXT_AES256,SSL_AES,SSL_ENC_MASK,SSL_AES,256)); Definitions.put(SSL_TXT_CHACHA20,new Def(0,SSL_TXT_CHACHA20,0,SSL_CHACHA20,0,0,0,0,SSL_ENC_MASK,0)); Definitions.put(SSL_TXT_MD5,new Def(0,SSL_TXT_MD5, 0,SSL_MD5, 0,0,0,0,SSL_MAC_MASK,0)); Definitions.put(SSL_TXT_SHA1,new Def(0,SSL_TXT_SHA1,0,SSL_SHA1, 0,0,0,0,SSL_MAC_MASK,0)); From 0a8ed4ca80f4031ad5fdfb4eee4a4be14037dca2 Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Thu, 22 Oct 2026 20:19:50 +0900 Subject: [PATCH 3/3] Accept cipher names, including RFC names, inside "+" rules --- .../org/jruby/ext/openssl/CipherStrings.java | 14 ++++++++------ .../jruby/ext/openssl/CipherStringsTest.java | 13 +++++++++++++ test/ssl/test_context.rb | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/CipherStrings.java b/src/main/java/org/jruby/ext/openssl/CipherStrings.java index c79b0518..073d1eb2 100644 --- a/src/main/java/org/jruby/ext/openssl/CipherStrings.java +++ b/src/main/java/org/jruby/ext/openssl/CipherStrings.java @@ -750,11 +750,13 @@ private static Collection matchingExact(final String name, final String[] a return matchingPattern(pattern, all, true, setSuite); } - final Def def = CipherNames.get(name); + // an OpenSSL cipher name, or an RFC (Java) suite name + final String ossl = CipherNames.containsKey(name) ? name : SuiteToOSSL.get(name); + final Def def = ossl == null ? null : CipherNames.get(ossl); if (def != null) { if (setSuite) { for (final String entry : all) { - if (name.equals(SuiteToOSSL.get(entry))) { + if (ossl.equals(SuiteToOSSL.get(entry))) { return Collections.singleton(def.setCipherSuite(entry)); } } @@ -769,13 +771,13 @@ private static Collection matching(final String[] defs, final String[] all, final boolean setSuite) { Collection matching = null; for ( final String name : defs ) { - final Def pattern = Definitions.get(name); - if ( pattern == null ) return null; // unknown name drops the rule (libssl ssl_cipher_process_rulestr) + final Collection found = matchingExact(name, all, setSuite); + if ( found == null ) return null; // unknown name drops the rule (libssl ssl_cipher_process_rulestr) if ( matching == null ) { - matching = matchingPattern(pattern, all, true, setSuite); + matching = new LinkedHashSet(found); } else { - matching.retainAll( matchingPattern(pattern, all, false, setSuite) ); + matching.retainAll(found); } } return matching; diff --git a/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java b/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java index f9ac679f..d17ccb35 100644 --- a/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java +++ b/src/test/java/org/jruby/ext/openssl/CipherStringsTest.java @@ -78,6 +78,19 @@ public void unknownAliasVoidsCombinedRule() { assertEquals(Arrays.asList("ECDHE-RSA-AES256-GCM-SHA384"), names("EECDH+FOO:EECDH+AES256", ENC_SUITES)); } + @Test + public void combinedRuleAcceptsCipherNames() { + assertEquals(Arrays.asList("ECDHE-RSA-AES128-GCM-SHA256"), names("AES128+ECDHE-RSA-AES128-GCM-SHA256", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-AES128-GCM-SHA256"), names("ECDHE-RSA-AES128-GCM-SHA256+AES128", ENC_SUITES)); + assertEquals(Arrays.asList("ECDHE-RSA-AES128-GCM-SHA256"), names("AES128+TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", ENC_SUITES)); + assertEquals(Arrays.asList(), names("AES256+ECDHE-RSA-AES128-GCM-SHA256", ENC_SUITES)); + } + + @Test + public void rfcCipherName() { + assertEquals(Arrays.asList("ECDHE-RSA-AES128-GCM-SHA256"), names("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", ENC_SUITES)); + } + private static List names(final String selector) { return names(selector, SUITES); } diff --git a/test/ssl/test_context.rb b/test/ssl/test_context.rb index 26290083..481db529 100644 --- a/test/ssl/test_context.rb +++ b/test/ssl/test_context.rb @@ -216,6 +216,23 @@ def test_set_ciphers_by_encryption_alias assert_raise(OpenSSL::SSL::SSLError) { context.ciphers = 'EECDH+FOO' } end + def test_set_ciphers_by_cipher_name_in_combined_rule + context = OpenSSL::SSL::SSLContext.new + ['AES128+ECDHE-RSA-AES128-GCM-SHA256', 'AES128+TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'].each do |rule| + context.ciphers = rule + selected = context.ciphers.reject { |_, version, *| version == 'TLSv1.3' }.map(&:first) + assert_equal ['ECDHE-RSA-AES128-GCM-SHA256'], selected, rule + end + + assert_raise(OpenSSL::SSL::SSLError) { context.ciphers = 'AES256+ECDHE-RSA-AES128-GCM-SHA256' } + end + + def test_set_ciphers_by_rfc_name + context = OpenSSL::SSL::SSLContext.new + context.ciphers = 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' + assert_equal ['ECDHE-RSA-AES128-GCM-SHA256'], context.ciphers.reject { |_, version, *| version == 'TLSv1.3' }.map(&:first) + end + def test_set_ciphers_by_cipher_name context = OpenSSL::SSL::SSLContext.new context.ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384"