From fc1d3c3fc60f8c5eec7431ec386c6be3801a5cf7 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Thu, 8 Oct 2026 13:30:24 +0200 Subject: [PATCH 1/2] [fix] match BCJSSE key types when choosing the TLS credential With an explicit TLS version (e.g. SSLContext.new(:TLSv1_2)), and always in the FIPS variant, SSLContext uses BCJSSE. BCJSSE asks the key manager for "ECDHE_ECDSA" / "ECDHE_RSA" / "DHE_DSS" (TLS 1.2), "EC/" (TLS 1.3) or "Ed25519" / "Ed448", but chooseAlias compared against the plain key type ("EC", "RSA", "EdDSA"). EC and Ed25519 server keys then got no credential and every handshake failed with "found no selectable cipher suite". Map these key types to the credential's key type, and for "EC/" also check that the key is on that curve. Fixes #383 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../org/jruby/ext/openssl/SSLContext.java | 39 ++++++++++++++++++- test/ssl/test_ssl.rb | 27 +++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/SSLContext.java b/src/main/java/org/jruby/ext/openssl/SSLContext.java index 18d78430..3313eaf5 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLContext.java +++ b/src/main/java/org/jruby/ext/openssl/SSLContext.java @@ -1239,18 +1239,53 @@ public String chooseServerAlias(String keyType, java.security.Principal[] issuer return chooseAlias(keyType); } - // BCJSSE asks per negotiated key type (RSA / EC / ...) + // BCJSSE asks per negotiated key type: "RSA", "EC/secp256r1" or "Ed25519", + // or (TLS 1.2) a legacy auth type such as "ECDHE_ECDSA" or "ECDHE_RSA" private String chooseAlias(final String keyType) { if (keyType == null) return null; for (int i = 0; i < internalContext.credentials.size(); i++) { final Credential credential = internalContext.credentials.get(i); - if (credential.privateKey != null && keyType.equalsIgnoreCase(credential.keyType)) { + if (credential.privateKey != null && keyTypeMatches(keyType, credential)) { return credential.alias; // pick the first credential that matches } } return null; } + private static boolean keyTypeMatches(final String keyType, final Credential credential) { + final int slash = keyType.indexOf('/'); + final String keyAlgorithm = slash < 0 ? keyType : keyType.substring(0, slash); + switch (keyAlgorithm.toUpperCase(java.util.Locale.ROOT)) { + case "ECDHE_ECDSA": + return "EC".equalsIgnoreCase(credential.keyType); + case "ECDHE_RSA": case "DHE_RSA": + return "RSA".equalsIgnoreCase(credential.keyType); + case "DHE_DSS": + return "DSA".equalsIgnoreCase(credential.keyType); + case "ED25519": case "ED448": // credential key type is "EdDSA" + if (!"EdDSA".equalsIgnoreCase(credential.keyType)) return false; + final String algorithm = credential.privateKey.getAlgorithm(); + return keyAlgorithm.equalsIgnoreCase(algorithm) || "EdDSA".equalsIgnoreCase(algorithm); + default: + if (!keyAlgorithm.equalsIgnoreCase(credential.keyType)) return false; + return slash < 0 || isOnCurve(credential.privateKey, keyType.substring(slash + 1)); + } + } + + private static boolean isOnCurve(final java.security.PrivateKey key, final String namedGroup) { + if (!(key instanceof java.security.interfaces.ECKey)) return false; + try { + final java.security.AlgorithmParameters params = java.security.AlgorithmParameters.getInstance("EC"); + params.init(new java.security.spec.ECGenParameterSpec(namedGroup)); + final java.security.spec.ECParameterSpec group = params.getParameterSpec(java.security.spec.ECParameterSpec.class); + final java.security.spec.ECParameterSpec own = ((java.security.interfaces.ECKey) key).getParams(); + return group.getCurve().equals(own.getCurve()) && group.getOrder().equals(own.getOrder()); + } + catch (java.security.GeneralSecurityException e) { + return false; + } + } + private Credential credentialByAlias(String alias) { if (alias == null) return null; for (int i = 0; i < internalContext.credentials.size(); i++) { diff --git a/test/ssl/test_ssl.rb b/test/ssl/test_ssl.rb index bd0ef69b..815abd3d 100644 --- a/test/ssl/test_ssl.rb +++ b/test/ssl/test_ssl.rb @@ -709,6 +709,33 @@ def test_add_certificate_multiple_certs } end + # An explicit TLS version selects BCJSSE, which asks the key manager for + # "ECDHE_ECDSA" / "EC/" / "Ed25519" rather than "EC" or "EdDSA" + def test_server_key_types_with_explicit_tls_version + keys = { + "P-256" => OpenSSL::PKey::EC.generate("prime256v1"), + "P-384" => OpenSSL::PKey::EC.generate("secp384r1"), + "Ed25519" => OpenSSL::PKey.generate_key("ED25519"), + } + keys.each do |name, key| + dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=#{name}") + cert = issue_cert(dn, key, 10, [["keyUsage", "digitalSignature", true]], @ca_cert, @ca_key) + [:TLSv1_2, :TLSv1_3].each do |version| + ctx_proc = -> ctx { + ctx.ssl_version = version + ctx.cert = cert + ctx.key = key + } + start_server(OpenSSL::SSL::VERIFY_NONE, true, ctx_proc: ctx_proc) { |server, port| + server_connect(port, OpenSSL::SSL::SSLContext.new(version)) { |ssl| + assert_equal dn.to_s, ssl.peer_cert.subject.to_s, "#{name} #{version}" + ssl.puts "abc"; assert_equal "abc\n", ssl.gets + } + } + end + end + end + def test_add_certificate_validates_private_key_and_chain ctx = OpenSSL::SSL::SSLContext.new public_key = OpenSSL::PKey.read(@svr_key.public_to_der) From 6a66a9fc378d62d9ea8b46bedddab2f3c108ce9a Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Thu, 8 Oct 2026 14:41:01 +0200 Subject: [PATCH 2/2] [test] skip the Ed25519 server key in FIPS mode BCJSSE in FIPS mode does not offer the ed25519 signature scheme, so an Ed25519 server key cannot be selected there regardless of key-type matching. Co-Authored-By: Claude Opus 5.5 (1M context) --- test/ssl/test_ssl.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/ssl/test_ssl.rb b/test/ssl/test_ssl.rb index 815abd3d..b25ad4cc 100644 --- a/test/ssl/test_ssl.rb +++ b/test/ssl/test_ssl.rb @@ -715,8 +715,9 @@ def test_server_key_types_with_explicit_tls_version keys = { "P-256" => OpenSSL::PKey::EC.generate("prime256v1"), "P-384" => OpenSSL::PKey::EC.generate("secp384r1"), - "Ed25519" => OpenSSL::PKey.generate_key("ED25519"), } + # BCJSSE in FIPS mode does not offer the ed25519 signature scheme + keys["Ed25519"] = OpenSSL::PKey.generate_key("ED25519") unless fips? keys.each do |name, key| dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=#{name}") cert = issue_cert(dn, key, 10, [["keyUsage", "digitalSignature", true]], @ca_cert, @ca_key)