He-Pin opened a new issue, #3237:
URL: https://github.com/apache/pekko/issues/3237

   ### Motivation
   
   When an `SSLException` occurs during TLS unwrap/wrap in the GraphStage TLS 
engine (`TlsGraphStage`), the code calls `writeFailureAlert()` which sends a 
TLS `close_notify` alert and then gracefully completes the transport output. 
This is a behavioral regression from the legacy `TLSActor`, which does not send 
any data to the transport on SSL errors with `closeTransport = false` — it 
simply errors the user output and stops.
   
   In security-sensitive scenarios (handshake failures, certificate validation 
failures, protocol violations), the transport connection should be aborted 
immediately rather than gracefully closed with a close notification.
   
   ### Current behavior
   
   In `TlsGraphStage.scala`, when an `SSLException` is caught during `doUnwrap` 
(line 333) or `doWrap` (line 362), the code calls `failTls(ex, closeTransport = 
false)`:
   
   ```scala
   // line 584-595
   private def failTls(ex: Throwable, closeTransport: Boolean): Unit =
     if ((stateBits & StageClosedFlag) == 0) {
       userInput.cancel()
       transportInput.cancel()
       if (closeTransport) {
         stateBits |= StageClosedFlag
         failStage(ex)
       } else {
         writeFailureAlert()        // sends close_notify to transport
         userOutput.failOutput(ex)  // fails user output
       }
     }
   ```
   
   `writeFailureAlert()` (line 599-610) calls `engine.closeOutbound()` then 
`engine.wrap()` to generate and send a `close_notify` alert, silently 
swallowing any errors:
   
   ```scala
   // line 599-610
   private def writeFailureAlert(): Unit =
     if ((engine ne null) && !transportOutput.isCancelled)
       try {
         if (!engine.isOutboundDone)
           engine.closeOutbound()
         val result = engine.wrap(EmptyApplicationBuffer, transportOutBuffer)
         lastHandshakeStatus = result.getHandshakeStatus
         flushToTransport()
         EmptyApplicationBuffer.clear()
       } catch {
         case NonFatal(_) => EmptyApplicationBuffer.clear()
       }
   ```
   
   After this, `completeOrFlush()` transitions to `CompletedPhase`, and 
`pumpFinished()` calls `transportOutput.completeOutput()` — completing the 
transport output **normally** instead of failing it. The downstream TCP 
connection receives a graceful FIN rather than a connection reset.
   
   ### Expected behavior
   
   On `SSLException` during unwrap/wrap, the stage should abort the transport 
connection immediately (fail the stage with the exception) rather than sending 
a `close_notify` and gracefully completing. This matches:
   
   1. **Legacy `TLSActor` behavior**: `TLSActor.fail()` with `closeTransport = 
false` does NOT send any data to the transport. It only errors the user output 
and stops (TLSActor.scala line 491-501).
   2. **TLS spec guidance**: RFC 5246 Section 7.2 states that fatal alert 
messages MUST result in immediate connection termination. Sending a 
`close_notify` after a fatal error is redundant and deviates from this guidance.
   3. **Security best practice**: Abruptly terminating the connection on SSL 
errors prevents any additional data exchange after a security-relevant failure 
is detected.
   
   ### Code evidence
   
   - 
`stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala`:
     - Line 333-339: `SSLException` caught in `doInbound` → calls `failTls(ex, 
closeTransport = false)`
     - Line 362-365: `SSLException` caught in `doOutbound` → calls `failTls(ex, 
closeTransport = false)`
     - Line 584-595: `failTls` with `closeTransport = false` → calls 
`writeFailureAlert()` instead of `failStage(ex)`
     - Line 599-610: `writeFailureAlert()` sends `close_notify` and silently 
swallows errors
   - `stream/src/main/scala/org/apache/pekko/stream/impl/io/TLSActor.scala`:
     - Line 491-501: Legacy `fail()` does NOT send any alert — only errors user 
output and stops
   
   ### Reproduction
   
   1. Set up a TLS connection where the client's trust store does not contain 
the server's CA certificate.
   2. Use the GraphStage TLS engine (`pekko.stream.materializer.tls.engine = 
graph-stage`).
   3. Initiate a TLS handshake.
   4. Observe that the server receives a `close_notify` alert and the TCP 
connection is gracefully closed (FIN) instead of being reset.
   5. The existing test `"emit an error if the TLS handshake fails certificate 
checks"` in `TlsSpec.scala` (line 477) verifies error propagation to the user 
side but does not verify the transport-side behavior (close_notify vs abort).
   
   ### Impact
   
   - **Module**: `pekko-stream`, TLS GraphStage engine
   - **Affected scenarios**: Any SSL error during handshake or data transfer 
when using the opt-in GraphStage TLS engine — certificate validation failures, 
protocol violations, corrupted records, etc.
   - **Security impact**: Graceful close instead of abort on SSL errors may 
mask the nature of the disconnection from peers and network observers, and 
deviates from the principle of immediate termination on fatal TLS errors.
   - **Behavioral regression**: The legacy `TLSActor` does not exhibit this 
behavior; the `writeFailureAlert()` method is a new addition in `TlsGraphStage` 
that was not part of the legacy state machine migration.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to