public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/jss] f45: 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 : f45
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=f45

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] f45: 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