public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/jss] rawhide: Backport PR #1116 to fix post-handshake auth hang (FreeIPA replication) (#2490607)
@ 2026-09-02 19:01 Adam Williamson
0 siblings, 0 replies; only message in thread
From: Adam Williamson @ 2026-09-02 19:01 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/jss
Branch : rawhide
Commit : 570c971fefcddb421a33a70d0abd2d1ebccdc15e
Author : Adam Williamson <adamwill@fedoraproject.org>
Date : 2026-09-02T12:00:49-07:00
Stats : +894/-1 in 4 file(s)
URL : https://src.fedoraproject.org/rpms/jss/c/570c971fefcddb421a33a70d0abd2d1ebccdc15e?branch=rawhide
Log:
Backport PR #1116 to fix post-handshake auth hang (FreeIPA replication) (#2490607)
---
diff --git a/0001-Fix-TLS-1.3-post-handshake-authentication-hanging-at.patch b/0001-Fix-TLS-1.3-post-handshake-authentication-hanging-at.patch
new file mode 100644
index 0000000..3f1af86
--- /dev/null
+++ b/0001-Fix-TLS-1.3-post-handshake-authentication-hanging-at.patch
@@ -0,0 +1,127 @@
+From a4406095c2ab74b713c39ecc8d8b9881d8d9cdd9 Mon Sep 17 00:00:00 2001
+From: Thomas Woerner <twoerner@redhat.com>
+Date: Mon, 17 Aug 2026 10:54:20 +0200
+Subject: [PATCH 1/3] Fix TLS 1.3 post-handshake authentication hanging at 100%
+ CPU
+
+When a TLS 1.3 server sends a post-handshake CertificateRequest to a
+JSS client, the unwrap() do-while loop spins indefinitely. PR.Read()
+processes the request and queues the Certificate response in write_buf,
+but the loop never breaks to let the caller call wrap() to flush it.
+
+In JSSEngineReferenceImpl, detect pending write data after a completed
+handshake and signal NEED_WRAP to break the loop.
+
+In JSSSocketChannel, add flushPostHandshake() to drive wrap() and
+send the Certificate response. Handle non-blocking channels by
+preserving pending output when writeChannel.write() returns 0 and
+retrying on the next read() or write() call.
+
+Resolves: https://bugzilla.redhat.com/show_bug.cgi?id=2490607
+
+Signed-off-by: Thomas Woerner <twoerner@redhat.com>
+---
+ .../jss/ssl/javax/JSSEngineReferenceImpl.java | 10 +++++
+ .../jss/ssl/javax/JSSSocketChannel.java | 38 +++++++++++++++++++
+ 2 files changed, 48 insertions(+)
+
+diff --git a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
+index 0121aca0..7cd151c8 100644
+--- a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
++++ b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
+@@ -1382,6 +1382,16 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+ seen_exception = true;
+ }
+ }
++
++ // TLS 1.3 post-handshake auth: PR.Read() may have processed a
++ // CertificateRequest, putting the Certificate response in write_buf.
++ // Break so the caller can call wrap() to send it.
++ if (handshake_already_complete && !seen_exception
++ && Buffer.ReadCapacity(write_buf) > 0) {
++ handshake_state = SSLEngineResult.HandshakeStatus.NEED_WRAP;
++ break;
++ }
++
+ } while (this_src_write != 0 || this_dst_write != 0);
+
+ SSLException checkException = checkSSLAlerts();
+diff --git a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java
+index a5e57407..e9b3e6a3 100644
+--- a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java
++++ b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java
+@@ -47,6 +47,7 @@ public class JSSSocketChannel extends SocketChannel {
+ private ByteBuffer writeBuffer;
+
+ private boolean handshakeCompleted = false;
++ private boolean pendingPostHandshake = false;
+
+ public JSSSocketChannel(JSSSocket sslSocket, SocketChannel parent, Socket parentSocket, ReadableByteChannel readChannel, WritableByteChannel writeChannel, JSSEngine engine) throws IOException {
+ super(null);
+@@ -251,6 +252,10 @@ public class JSSSocketChannel extends SocketChannel {
+ return -1;
+ }
+
++ if (pendingPostHandshake) {
++ flushPostHandshake();
++ }
++
+ long unwrapped = 0;
+ long decrypted = 0;
+
+@@ -298,6 +303,12 @@ public class JSSSocketChannel extends SocketChannel {
+
+ readBuffer.compact();
+
++ // Handle TLS 1.3 post-handshake auth (CertificateRequest)
++ if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP
++ && handshakeCompleted) {
++ flushPostHandshake();
++ }
++
+ // If we consumed bytes, there is now room in readBuffer for some
+ // more. Even if dsts are full, we may be able to consume more
+ // bytes in another call to unwrap().
+@@ -316,12 +327,39 @@ public class JSSSocketChannel extends SocketChannel {
+ return (int) write(new ByteBuffer[] { src });
+ }
+
++ private void flushPostHandshake() throws IOException {
++ if (!pendingPostHandshake) {
++ writeBuffer.clear();
++ }
++ SSLEngineResult wr;
++ do {
++ wr = engine.wrap(new ByteBuffer[0], 0, 0, writeBuffer);
++ writeBuffer.flip();
++ while (writeBuffer.hasRemaining()) {
++ int n = writeChannel.write(writeBuffer);
++ if (n == 0) {
++ writeBuffer.compact();
++ pendingPostHandshake = true;
++ return;
++ }
++ }
++ writeBuffer.compact();
++ } while (wr.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP);
++ pendingPostHandshake = false;
++ }
++
+ @Override
+ public synchronized long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
+ if (outboundClosed) {
+ return -1;
+ }
+
++ if (pendingPostHandshake) {
++ flushPostHandshake();
++ if (pendingPostHandshake) {
++ return 0;
++ }
++ }
+ writeBuffer.clear();
+
+ ByteBuffer dst = writeBuffer;
+--
+2.55.0
+
diff --git a/0002-Integrate-post-handshake-auth-into-the-handshake-sta.patch b/0002-Integrate-post-handshake-auth-into-the-handshake-sta.patch
new file mode 100644
index 0000000..21f5101
--- /dev/null
+++ b/0002-Integrate-post-handshake-auth-into-the-handshake-sta.patch
@@ -0,0 +1,324 @@
+From a89689df6d701f116b7d3380ae15d4e653ae5700 Mon Sep 17 00:00:00 2001
+From: Marco Fargetta <mfargett@redhat.com>
+Date: Mon, 17 Aug 2026 19:36:44 +0200
+Subject: [PATCH 2/3] Integrate post-handshake auth into the handshake state
+ machine
+
+The previous fix broke out of the unwrap() do-while loop as a
+special case when write_buf grew after a completed handshake, and
+added flushPostHandshake() as a separate, parallel path outside the
+normal handshake state machine. This left several gaps: NEED_TASK
+was not fully driven during flush, wrap() had no status check and
+could spin on BUFFER_OVERFLOW, and write_buf growth alone could not
+distinguish genuine post-handshake auth from ordinary leftover
+ciphertext.
+
+Fold post-handshake auth into updateHandshakeState() so it is
+handled by the same NEED_WRAP/NEED_TASK machinery as the initial
+handshake, add a post_handshake_auth_pending flag (set only when
+PR.Read() demonstrably grows write_buf) to disambiguate it from
+leftover ciphertext, and harden flushPostHandshake() with status
+checks, stall detection, and full NEED_TASK handling so non-blocking
+channels retry correctly instead of spinning.
+
+Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
+---
+ .../org/mozilla/jss/ssl/javax/JSSEngine.java | 11 ++
+ .../jss/ssl/javax/JSSEngineReferenceImpl.java | 50 ++++++++-
+ .../jss/ssl/javax/JSSSocketChannel.java | 100 ++++++++++++++----
+ 3 files changed, 138 insertions(+), 23 deletions(-)
+
+diff --git a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngine.java b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngine.java
+index 329dcd1c..245c018b 100644
+--- a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngine.java
++++ b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngine.java
+@@ -200,6 +200,13 @@ public abstract class JSSEngine extends javax.net.ssl.SSLEngine {
+ */
+ protected boolean is_inbound_closed;
+
++ /**
++ * Whether PR.Read() inside unwrap() detected a TLS 1.3 post-handshake
++ * auth event (e.g. CertificateRequest) that produced new data in
++ * write_buf. Cleared after wrap() drains the response.
++ */
++ protected boolean post_handshake_auth_pending;
++
+ /**
+ * Set of configuration options to enable via SSL_OptionSet(...).
+ */
+@@ -1066,6 +1073,10 @@ public abstract class JSSEngine extends javax.net.ssl.SSLEngine {
+ return is_outbound_closed;
+ }
+
++ public boolean isPostHandshakeAuthPending() {
++ return post_handshake_auth_pending;
++ }
++
+ /**
+ * Gets the current security status of this JSSEngine instance.
+ *
+diff --git a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
+index 7cd151c8..14086cba 100644
+--- a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
++++ b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSEngineReferenceImpl.java
+@@ -1126,6 +1126,14 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+
+ ssl_exception = checkSSLAlerts();
+ seen_exception = (ssl_exception != null);
++
++ // TLS 1.3 post-handshake: NSS may have queued response data
++ // (e.g. Certificate for a CertificateRequest).
++ if (!seen_exception && Buffer.ReadCapacity(write_buf) > 0) {
++ debug("JSSEngine.updateHandshakeState() - post-handshake NEED_WRAP");
++ handshake_state = SSLEngineResult.HandshakeStatus.NEED_WRAP;
++ }
++
+ return;
+ }
+
+@@ -1152,6 +1160,26 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+ return;
+ }
+
++ // Post-handshake state: the initial handshake completed but
++ // handshake_state is not NOT_HANDSHAKING or FINISHED (e.g. set to
++ // NEED_WRAP by the write_buf check above). Transition back to
++ // NOT_HANDSHAKING once write_buf is drained; do NOT fall through
++ // to ForceHandshake/fireHandshakeComplete.
++ if (!step_handshake && ssl_fd.handshakeComplete) {
++ debug("JSSEngine.updateHandshakeState() - post-handshake, write_buf.read=" + Buffer.ReadCapacity(write_buf));
++ unknown_state_count = 0;
++
++ ssl_exception = checkSSLAlerts();
++ seen_exception = (ssl_exception != null);
++
++ if (!seen_exception && Buffer.ReadCapacity(write_buf) > 0) {
++ handshake_state = SSLEngineResult.HandshakeStatus.NEED_WRAP;
++ } else {
++ handshake_state = SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
++ }
++ return;
++ }
++
+ // Since we're not obviously done handshaking, and the last time we
+ // were called, we were still handshaking, step the handshake.
+ debug("JSSEngine.updateHandshakeState() - forcing handshake");
+@@ -1363,6 +1391,7 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+ updateHandshakeState();
+
+ int max_dst_size = computeSize(dsts, offset, length);
++ long write_buf_before = Buffer.ReadCapacity(write_buf);
+ byte[] app_buffer = PR.Read(ssl_fd, max_dst_size);
+ int error = PR.GetError();
+ debug("JSSEngine.unwrap() - " + app_buffer + " error=" + errorText(error));
+@@ -1383,12 +1412,22 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+ }
+ }
+
++ // TLS 1.3 post-handshake auth: PR.Read() may have triggered the
++ // async cert-auth callback (e.g. validating a client's
++ // post-handshake certificate) without producing any write_buf
++ // output. Report NEED_TASK here since result.getHandshakeStatus()
++ // (unlike engine.getHandshakeStatus()) won't otherwise reflect it.
++ if (checkNeedCertValidation()) {
++ break;
++ }
++
+ // TLS 1.3 post-handshake auth: PR.Read() may have processed a
+ // CertificateRequest, putting the Certificate response in write_buf.
+- // Break so the caller can call wrap() to send it.
++ // Detect this by checking if PR.Read() increased write_buf.
+ if (handshake_already_complete && !seen_exception
+- && Buffer.ReadCapacity(write_buf) > 0) {
++ && Buffer.ReadCapacity(write_buf) > write_buf_before) {
+ handshake_state = SSLEngineResult.HandshakeStatus.NEED_WRAP;
++ post_handshake_auth_pending = true;
+ break;
+ }
+
+@@ -1417,7 +1456,8 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+ if (is_inbound_closed) {
+ debug("Socket is currently closed.");
+ handshake_status = SSLEngineResult.Status.CLOSED;
+- } else if (handshake_already_complete && src_capacity > 0 && app_data == 0) {
++ } else if (handshake_already_complete && src_capacity > 0 && app_data == 0
++ && handshake_state != SSLEngineResult.HandshakeStatus.NEED_WRAP) {
+ debug("Underflowed: produced no application data when we expected to.");
+ handshake_status = SSLEngineResult.Status.BUFFER_UNDERFLOW;
+ }
+@@ -1706,6 +1746,10 @@ public class JSSEngineReferenceImpl extends JSSEngine {
+ }
+ } while (this_src_write != 0 || this_dst_write != 0);
+
++ if (post_handshake_auth_pending && Buffer.ReadCapacity(write_buf) == 0) {
++ post_handshake_auth_pending = false;
++ }
++
+ // Check for new outbound alerts to the peer and fire the related events
+ SSLException newSSLException = checkSSLAlerts();
+ if (!seen_exception && ssl_exception == null && newSSLException != null) {
+diff --git a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java
+index e9b3e6a3..5a71ec3a 100644
+--- a/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java
++++ b/base/src/main/java/org/mozilla/jss/ssl/javax/JSSSocketChannel.java
+@@ -47,7 +47,7 @@ public class JSSSocketChannel extends SocketChannel {
+ private ByteBuffer writeBuffer;
+
+ private boolean handshakeCompleted = false;
+- private boolean pendingPostHandshake = false;
++ private boolean postHandshakePending = false;
+
+ public JSSSocketChannel(JSSSocket sslSocket, SocketChannel parent, Socket parentSocket, ReadableByteChannel readChannel, WritableByteChannel writeChannel, JSSEngine engine) throws IOException {
+ super(null);
+@@ -252,14 +252,18 @@ public class JSSSocketChannel extends SocketChannel {
+ return -1;
+ }
+
+- if (pendingPostHandshake) {
+- flushPostHandshake();
+- }
+-
+ long unwrapped = 0;
+ long decrypted = 0;
++ int postHandshakeOps = 0;
+
+ try {
++ if (postHandshakePending) {
++ flushPostHandshake();
++ if (postHandshakePending) {
++ return 0;
++ }
++ }
++
+ SSLEngineResult result;
+ do {
+ int n = remoteRead();
+@@ -303,10 +307,39 @@ public class JSSSocketChannel extends SocketChannel {
+
+ readBuffer.compact();
+
+- // Handle TLS 1.3 post-handshake auth (CertificateRequest)
+- if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP
+- && handshakeCompleted) {
+- flushPostHandshake();
++ // Handle NEED_WRAP and NEED_TASK after handshake completion.
++ // NEED_WRAP may come from post-handshake auth (CertificateRequest)
++ // or from leftover ciphertext in write_buf; both need flushing,
++ // but only genuine post-handshake auth counts toward the safety limit.
++ if (handshakeCompleted) {
++ SSLEngineResult.HandshakeStatus hsStatus = result.getHandshakeStatus();
++ if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) {
++ boolean isPostHandshake = engine.isPostHandshakeAuthPending();
++ flushPostHandshake();
++ if (postHandshakePending) {
++ return decrypted;
++ }
++ if (isPostHandshake) {
++ postHandshakeOps++;
++ }
++ } else if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) {
++ Runnable task = engine.getDelegatedTask();
++ if (task != null) {
++ task.run();
++ }
++ postHandshakeOps++;
++ // After task, a NEED_WRAP may follow (e.g. to send
++ // the auth result). Flush it now like finishConnect().
++ if (engine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) {
++ flushPostHandshake();
++ if (postHandshakePending) {
++ return decrypted;
++ }
++ }
++ }
++ if (postHandshakeOps > 10) {
++ throw new IOException("Exceeded maximum post-handshake operations during read");
++ }
+ }
+
+ // If we consumed bytes, there is now room in readBuffer for some
+@@ -322,30 +355,56 @@ public class JSSSocketChannel extends SocketChannel {
+ return decrypted;
+ }
+
+- @Override
+- public int write(ByteBuffer src) throws IOException {
+- return (int) write(new ByteBuffer[] { src });
+- }
+-
+ private void flushPostHandshake() throws IOException {
+- if (!pendingPostHandshake) {
++ if (!postHandshakePending) {
+ writeBuffer.clear();
+ }
++
+ SSLEngineResult wr;
++ SSLEngineResult.HandshakeStatus hsStatus;
+ do {
+ wr = engine.wrap(new ByteBuffer[0], 0, 0, writeBuffer);
++
++ if (wr.getStatus() != SSLEngineResult.Status.OK
++ && wr.getStatus() != SSLEngineResult.Status.CLOSED) {
++ postHandshakePending = false;
++ throw new IOException("Unexpected status from post-handshake wrap: " + wr);
++ }
++
+ writeBuffer.flip();
+ while (writeBuffer.hasRemaining()) {
+ int n = writeChannel.write(writeBuffer);
+ if (n == 0) {
+ writeBuffer.compact();
+- pendingPostHandshake = true;
++ postHandshakePending = true;
+ return;
+ }
+ }
++
++ if (wr.bytesProduced() == 0
++ && wr.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) {
++ postHandshakePending = false;
++ throw new IOException("Post-handshake wrap stalled, producing no data");
++ }
++
++ hsStatus = wr.getHandshakeStatus();
++ if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) {
++ Runnable task = engine.getDelegatedTask();
++ if (task != null) {
++ task.run();
++ }
++ hsStatus = engine.getHandshakeStatus();
++ }
++
+ writeBuffer.compact();
+- } while (wr.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP);
+- pendingPostHandshake = false;
++ } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP);
++
++ postHandshakePending = false;
++ }
++
++ @Override
++ public int write(ByteBuffer src) throws IOException {
++ return (int) write(new ByteBuffer[] { src });
+ }
+
+ @Override
+@@ -354,12 +413,13 @@ public class JSSSocketChannel extends SocketChannel {
+ return -1;
+ }
+
+- if (pendingPostHandshake) {
++ if (postHandshakePending) {
+ flushPostHandshake();
+- if (pendingPostHandshake) {
++ if (postHandshakePending) {
+ return 0;
+ }
+ }
++
+ writeBuffer.clear();
+
+ ByteBuffer dst = writeBuffer;
+--
+2.55.0
+
diff --git a/0003-Add-JSSSocketChannel-TLS-1.3-post-handshake-auth-tes.patch b/0003-Add-JSSSocketChannel-TLS-1.3-post-handshake-auth-tes.patch
new file mode 100644
index 0000000..0e76eee
--- /dev/null
+++ b/0003-Add-JSSSocketChannel-TLS-1.3-post-handshake-auth-tes.patch
@@ -0,0 +1,431 @@
+From 8d2eb4b0357f64561f7d4bfd570810aae1a6e08c Mon Sep 17 00:00:00 2001
+From: Fraser Tweedale <ftweedal@redhat.com>
+Date: Mon, 31 Aug 2026 09:37:28 -0400
+Subject: [PATCH 3/3] Add JSSSocketChannel TLS 1.3 post-handshake auth test
+
+Add TestJSSSocketChannel with two test methods:
+
+- testPostHandshakeAuth: server and client perform an initial TLS 1.3
+ handshake, exchange data, then the server triggers post-handshake
+ client authentication via startHandshake(). Verifies that the
+ client's certificate is presented transparently through
+ JSSSocketChannel.read() -> flushPostHandshake(), and that data
+ exchange continues normally afterward. Includes a 32KB transfer
+ after post-handshake auth as general coverage for large writes on a
+ connection that has been through post-handshake auth.
+
+- testMultipleMessagesAfterPostHandshakeAuth: same setup, then
+ performs 5 additional request/response round trips to verify the
+ connection remains fully functional after post-handshake auth.
+
+Both tests use JSSSocket over loopback with JSSNativeTrustManager and
+TLS 1.3 only. The server sends a "done" acknowledgment before the
+client closes, preventing a close_notify race.
+
+Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
+---
+ .../jss/tests/TestJSSSocketChannel.java | 375 ++++++++++++++++++
+ cmake/JSSTests.cmake | 5 +
+ 2 files changed, 380 insertions(+)
+ create mode 100644 base/src/test/java/org/mozilla/jss/tests/TestJSSSocketChannel.java
+
+diff --git a/base/src/test/java/org/mozilla/jss/tests/TestJSSSocketChannel.java b/base/src/test/java/org/mozilla/jss/tests/TestJSSSocketChannel.java
+new file mode 100644
+index 00000000..595fc857
+--- /dev/null
++++ b/base/src/test/java/org/mozilla/jss/tests/TestJSSSocketChannel.java
+@@ -0,0 +1,375 @@
++package org.mozilla.jss.tests;
++
++import java.io.IOException;
++import java.io.InputStream;
++import java.io.OutputStream;
++import java.net.InetAddress;
++import java.net.ServerSocket;
++import java.net.Socket;
++import java.security.KeyStore;
++import java.util.Arrays;
++
++import javax.net.ssl.KeyManager;
++import javax.net.ssl.KeyManagerFactory;
++import javax.net.ssl.SSLContext;
++import javax.net.ssl.SSLSession;
++import javax.net.ssl.TrustManager;
++import javax.net.ssl.X509KeyManager;
++import javax.net.ssl.X509TrustManager;
++
++import org.mozilla.jss.CryptoManager;
++import org.mozilla.jss.provider.javax.crypto.JSSNativeTrustManager;
++import org.mozilla.jss.ssl.javax.JSSParameters;
++import org.mozilla.jss.ssl.javax.JSSSocket;
++
++public class TestJSSSocketChannel {
++
++ static X509KeyManager[] keyManagers;
++ static X509TrustManager[] trustManagers;
++
++ public static void initialize(String[] args) throws Exception {
++ CryptoManager cm = CryptoManager.getInstance();
++ cm.setPasswordCallback(new FilePasswordCallback(args[1]));
++
++ KeyStore ks = KeyStore.getInstance("PKCS11", "Mozilla-JSS");
++ ks.load(null, null);
++ KeyManagerFactory kmf = KeyManagerFactory.getInstance("NssX509", "Mozilla-JSS");
++ kmf.init(ks, null);
++
++ KeyManager[] kms = kmf.getKeyManagers();
++ keyManagers = new X509KeyManager[kms.length];
++ for (int i = 0; i < kms.length; i++) {
++ keyManagers[i] = (X509KeyManager) kms[i];
++ }
++
++ trustManagers = new X509TrustManager[] { new JSSNativeTrustManager() };
++ }
++
++ static JSSSocket createJSSSocket(SSLContext ctx, Socket raw, String alias, boolean clientMode, int port) throws Exception {
++ JSSSocket sock = new JSSSocket();
++ sock.consumeSocket(raw);
++ sock.setSSLContext(ctx);
++ if (clientMode) {
++ sock.initEngine("localhost", port);
++ } else {
++ sock.initEngine();
++ }
++ JSSParameters params = new JSSParameters();
++ params.setAliases(Arrays.asList(alias.split(",")));
++ params.setHostname("localhost");
++ sock.setSSLParameters(params);
++ sock.setUseClientMode(clientMode);
++ sock.setKeyManagers(keyManagers);
++ sock.setTrustManagers(trustManagers);
++ return sock;
++ }
++
++ static byte[] readWithRetry(InputStream in, long timeoutMs) throws Exception {
++ byte[] buf = new byte[4096];
++ long deadline = System.currentTimeMillis() + timeoutMs;
++ int total = 0;
++ while (total == 0) {
++ if (System.currentTimeMillis() > deadline) {
++ throw new IOException("Read timed out after " + timeoutMs + "ms");
++ }
++ int n = in.read(buf, total, buf.length - total);
++ if (n < 0) {
++ throw new IOException("Unexpected EOF");
++ }
++ total += n;
++ if (total == 0) {
++ Thread.sleep(50);
++ }
++ }
++ return Arrays.copyOf(buf, total);
++ }
++
++ static byte[] readExactly(InputStream in, int count, long timeoutMs) throws Exception {
++ byte[] buf = new byte[count];
++ long deadline = System.currentTimeMillis() + timeoutMs;
++ int total = 0;
++ while (total < count) {
++ if (System.currentTimeMillis() > deadline) {
++ throw new IOException("Read timed out after " + timeoutMs + "ms (got " + total + " of " + count + " bytes)");
++ }
++ int n = in.read(buf, total, count - total);
++ if (n < 0) {
++ throw new IOException("Unexpected EOF after " + total + " of " + count + " bytes");
++ }
++ if (n == 0) {
++ Thread.sleep(50);
++ }
++ total += n;
++ }
++ return buf;
++ }
++
++ static void assertEqual(String expected, byte[] actual, String context) {
++ String actualStr = new String(actual);
++ if (!expected.equals(actualStr)) {
++ throw new RuntimeException(context + ": expected '" + expected + "', got '" + actualStr + "'");
++ }
++ }
++
++ public static void testPostHandshakeAuth(SSLContext ctx, String clientAlias, String serverAlias) throws Exception {
++ System.out.println("TestJSSSocketChannel: testPostHandshakeAuth");
++
++ final Exception[] serverError = { null };
++
++ ServerSocket ss = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
++ int port = ss.getLocalPort();
++
++ Thread serverThread = new Thread(() -> {
++ try {
++ Socket rawServer = ss.accept();
++ JSSSocket server = createJSSSocket(ctx, rawServer, serverAlias, false, 0);
++ server.setEnabledProtocols(new String[] { "TLSv1.3" });
++
++ server.startHandshake();
++ System.out.println(" server: initial handshake complete");
++
++ OutputStream sOut = server.getOutputStream();
++ InputStream sIn = server.getInputStream();
++
++ sOut.write("hello from server".getBytes());
++ sOut.flush();
++
++ assertEqual("hello from client", readWithRetry(sIn, 10000), "server initial read");
++ System.out.println(" server: initial data exchange OK");
++
++ // Enable client auth and trigger post-handshake auth
++ server.setWantClientAuth(true);
++ server.setNeedClientAuth(true);
++ server.startHandshake();
++
++ sOut.write("post-auth data".getBytes());
++ sOut.flush();
++ System.out.println(" server: sent post-handshake-auth data");
++
++ assertEqual("post-auth reply", readWithRetry(sIn, 10000), "server post-auth read");
++
++ SSLSession session = server.getSession();
++ assert session.getPeerCertificates() != null : "Expected peer certificates";
++ assert session.getPeerCertificates().length > 0 : "Expected at least one peer certificate";
++ System.out.println(" server: verified " + session.getPeerCertificates().length + " peer cert(s)");
++
++ // Send >18KB to verify large writes work after
++ // post-handshake auth.
++ byte[] largeData = new byte[32 * 1024];
++ for (int i = 0; i < largeData.length; i++) {
++ largeData[i] = (byte) (i & 0xFF);
++ }
++ sOut.write(largeData);
++ sOut.flush();
++ System.out.println(" server: sent " + largeData.length + " bytes post-auth");
++
++ assertEqual("large-data-ok", readWithRetry(sIn, 10000), "server large data ack");
++
++ // Signal the client it is OK to close now. This prevents
++ // the client's close_notify from arriving while the server
++ // is still processing the post-handshake auth response.
++ sOut.write("done".getBytes());
++ sOut.flush();
++
++ server.close();
++ } catch (Exception e) {
++ serverError[0] = e;
++ }
++ });
++ serverThread.setDaemon(true);
++ serverThread.start();
++
++ try {
++ Socket rawClient = new Socket(InetAddress.getLoopbackAddress(), port);
++ JSSSocket client = createJSSSocket(ctx, rawClient, clientAlias, true, port);
++ client.setEnabledProtocols(new String[] { "TLSv1.3" });
++
++ client.startHandshake();
++ System.out.println(" client: initial handshake complete");
++
++ OutputStream cOut = client.getOutputStream();
++ InputStream cIn = client.getInputStream();
++
++ assertEqual("hello from server", readWithRetry(cIn, 10000), "client initial read");
++
++ cOut.write("hello from client".getBytes());
++ cOut.flush();
++ System.out.println(" client: initial data exchange OK");
++
++ // This read triggers post-handshake auth processing
++ // in JSSSocketChannel.read() → flushPostHandshake()
++ assertEqual("post-auth data", readWithRetry(cIn, 10000), "client post-auth read");
++ System.out.println(" client: post-handshake auth completed transparently");
++
++ cOut.write("post-auth reply".getBytes());
++ cOut.flush();
++
++ // Receive the large (>18KB) post-auth transfer
++ int largeSize = 32 * 1024;
++ byte[] largeReceived = readExactly(cIn, largeSize, 10000);
++ for (int i = 0; i < largeSize; i++) {
++ if (largeReceived[i] != (byte) (i & 0xFF)) {
++ throw new RuntimeException("Large data mismatch at byte " + i);
++ }
++ }
++ System.out.println(" client: received and verified " + largeSize + " bytes post-auth");
++ cOut.write("large-data-ok".getBytes());
++ cOut.flush();
++
++ // Wait for server ack before closing, so close_notify doesn't
++ // race with the server's read of the post-handshake auth response.
++ assertEqual("done", readWithRetry(cIn, 10000), "client done-ack read");
++
++ client.close();
++ } finally {
++ serverThread.join(30000);
++ ss.close();
++ }
++
++ if (serverError[0] != null) {
++ throw new RuntimeException("Server thread failed", serverError[0]);
++ }
++
++ if (serverThread.isAlive()) {
++ throw new RuntimeException("Server thread did not finish in time");
++ }
++
++ System.out.println("TestJSSSocketChannel: testPostHandshakeAuth PASSED");
++ }
++
++ public static void testMultipleMessagesAfterPostHandshakeAuth(SSLContext ctx, String clientAlias, String serverAlias) throws Exception {
++ System.out.println("TestJSSSocketChannel: testMultipleMessagesAfterPostHandshakeAuth");
++
++ final Exception[] serverError = { null };
++
++ ServerSocket ss = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
++ int port = ss.getLocalPort();
++
++ Thread serverThread = new Thread(() -> {
++ try {
++ Socket rawServer = ss.accept();
++ JSSSocket server = createJSSSocket(ctx, rawServer, serverAlias, false, 0);
++ server.setEnabledProtocols(new String[] { "TLSv1.3" });
++
++ server.startHandshake();
++
++ OutputStream sOut = server.getOutputStream();
++ InputStream sIn = server.getInputStream();
++
++ // Initial data exchange to settle the connection
++ sOut.write("ping".getBytes());
++ sOut.flush();
++ assertEqual("pong", readWithRetry(sIn, 10000), "server initial read");
++
++ // Trigger post-handshake auth
++ server.setWantClientAuth(true);
++ server.setNeedClientAuth(true);
++ server.startHandshake();
++
++ sOut.write("auth-trigger".getBytes());
++ sOut.flush();
++
++ assertEqual("ack", readWithRetry(sIn, 10000), "server ack read");
++
++ // Multiple round trips after post-handshake auth
++ for (int i = 0; i < 5; i++) {
++ String msg = "server-msg-" + i;
++ sOut.write(msg.getBytes());
++ sOut.flush();
++
++ String expected = "client-msg-" + i;
++ assertEqual(expected, readWithRetry(sIn, 10000), "server round " + i);
++ }
++
++ sOut.write("done".getBytes());
++ sOut.flush();
++
++ server.close();
++ } catch (Exception e) {
++ serverError[0] = e;
++ }
++ });
++ serverThread.setDaemon(true);
++ serverThread.start();
++
++ try {
++ Socket rawClient = new Socket(InetAddress.getLoopbackAddress(), port);
++ JSSSocket client = createJSSSocket(ctx, rawClient, clientAlias, true, port);
++ client.setEnabledProtocols(new String[] { "TLSv1.3" });
++
++ client.startHandshake();
++
++ OutputStream cOut = client.getOutputStream();
++ InputStream cIn = client.getInputStream();
++
++ assertEqual("ping", readWithRetry(cIn, 10000), "client initial read");
++ cOut.write("pong".getBytes());
++ cOut.flush();
++
++ assertEqual("auth-trigger", readWithRetry(cIn, 10000), "client auth-trigger read");
++
++ cOut.write("ack".getBytes());
++ cOut.flush();
++
++ // Multiple round trips after post-handshake auth
++ for (int i = 0; i < 5; i++) {
++ String expected = "server-msg-" + i;
++ assertEqual(expected, readWithRetry(cIn, 10000), "client round " + i);
++
++ String msg = "client-msg-" + i;
++ cOut.write(msg.getBytes());
++ cOut.flush();
++ }
++
++ assertEqual("done", readWithRetry(cIn, 10000), "client done-ack read");
++
++ client.close();
++ } finally {
++ serverThread.join(30000);
++ ss.close();
++ }
++
++ if (serverError[0] != null) {
++ throw new RuntimeException("Server thread failed", serverError[0]);
++ }
++
++ if (serverThread.isAlive()) {
++ throw new RuntimeException("Server thread did not finish in time");
++ }
++
++ System.out.println("TestJSSSocketChannel: testMultipleMessagesAfterPostHandshakeAuth PASSED");
++ }
++
++ public static void main(String[] args) throws Exception {
++ System.out.println("Initializing CryptoManager...");
++ initialize(args);
++
++ if (!org.mozilla.jss.JSSProvider.ENABLE_JSSENGINE) {
++ System.out.println("JSSEngine not enabled, skipping.");
++ return;
++ }
++
++ String clientAlias = args[2];
++ String serverAlias = args[3];
++
++ // Check TLS 1.3 support
++ SSLContext ctx = SSLContext.getInstance("TLS", "Mozilla-JSS");
++ ctx.init(keyManagers, trustManagers, null);
++
++ String[] supported = ctx.createSSLEngine().getSupportedProtocols();
++ boolean tls13 = false;
++ for (String p : supported) {
++ if ("TLSv1.3".equals(p)) {
++ tls13 = true;
++ break;
++ }
++ }
++ if (!tls13) {
++ System.out.println("TLS 1.3 not supported, skipping.");
++ return;
++ }
++
++ testPostHandshakeAuth(ctx, clientAlias, serverAlias);
++ testMultipleMessagesAfterPostHandshakeAuth(ctx, clientAlias, serverAlias);
++ }
++}
+diff --git a/cmake/JSSTests.cmake b/cmake/JSSTests.cmake
+index b82d421f..f2b9db1f 100644
+--- a/cmake/JSSTests.cmake
++++ b/cmake/JSSTests.cmake
+@@ -327,6 +327,11 @@ macro(jss_tests)
+ DEPENDS "Generate_known_RSA_cert_pair"
+ )
+
++ jss_test_java(
++ NAME "JSSSocketChannel_PostHandshakeAuth"
++ COMMAND "org.mozilla.jss.tests.TestJSSSocketChannel" "${RESULTS_NSSDB_OUTPUT_DIR}" "${PASSWORD_FILE}" "Client_RSA" "Server_RSA"
++ DEPENDS "SSLEngine_RSA"
++ )
+
+ if(NOT FIPS_ENABLED)
+ jss_test_java(
+--
+2.55.0
+
diff --git a/jss.spec b/jss.spec
index 205ab24..5c68c60 100644
--- a/jss.spec
+++ b/jss.spec
@@ -13,7 +13,7 @@ Name: jss
# Downstream release number:
# - development/stabilization (unsupported): 0.<n> where n >= 1
# - GA/update (supported): <n> where n >= 1
-%global release_number 1
+%global release_number 2
# Development phase:
# - development (unsupported): alpha<n> where n >= 1
@@ -41,6 +41,14 @@ Release: %{release_number}%{?phase:.}%{?phase}%{?timestamp:.}%{?timestamp
# tarball.
Source: https://github.com/dogtagpki/jss/archive/v%{version}%{?phase:-}%{?phase}/jss-%{version}%{?phase:-}%{?phase}.tar.gz
+# https://bugzilla.redhat.com/show_bug.cgi?id=2490607
+# https://github.com/dogtagpki/jss/pull/1116
+# Fix post-handshake auth hang with TLS 1.3
+# Fixes FreeIPA replication with OpenSSL 4
+Patch: 0001-Fix-TLS-1.3-post-handshake-authentication-hanging-at.patch
+Patch: 0002-Integrate-post-handshake-auth-into-the-handshake-sta.patch
+Patch: 0003-Add-JSSSocketChannel-TLS-1.3-post-handshake-auth-tes.patch
+
# To create a patch for all changes since a version tag:
# $ git format-patch \
# --stdout \
@@ -420,6 +428,9 @@ cp base/target/jss-tests.jar %{buildroot}%{_datadir}/jss/tests/lib
################################################################################
%changelog
+* Wed Sep 02 2026 Adam Williamson <adamwill@fedoraproject.org> -5.10.1-2
+- Backport PR #1116 to fix post-handshake auth hang (FreeIPA replication) (#2490607)
+
* Tue Jul 28 2026 Dogtag PKI Team <devel@lists.dogtagpki.org> - 5.10.1-1
- Rebase to JSS 5.10.1
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-09-02 19:01 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-02 19:01 [rpms/jss] rawhide: Backport PR #1116 to fix post-handshake auth hang (FreeIPA replication) (#2490607) Adam Williamson
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox