@@ -41,6 +41,7 @@ actor \nodoc\ Main is TestList
4141 test (_TestSSLReadPendingExceedsExpect )
4242 test (_TestSSLReadOnAuthFail )
4343 test (_TestSSLALPNSelectedOnAuthFail )
44+ test (_TestSSLALPNSelectedOnError )
4445 test (_TestSSLReceiveOnAuthFail )
4546 test (_TestSSLReceiveOnError )
4647 test (_TestSSLCloseFromReady )
@@ -52,6 +53,9 @@ actor \nodoc\ Main is TestList
5253 test (_TestSSLReceiveInClosed )
5354 test (_TestSSLWriteBlockedInClosed )
5455 test (_TestSSLBidirectionalCloseRoundtrip )
56+ test (_TestSSLCorruptRecordInClosed )
57+ test (_TestSSLCanSendOnFailedSession )
58+ test (_TestSSLWriteBlockedInClosing )
5559 test (_TestSSLDisposeBeforeHandshake )
5660 test (_TestSSLDisposeTwice )
5761 test (_TestSSLReadAfterDispose )
@@ -2515,6 +2519,82 @@ class \nodoc\ iso _TestSSLALPNSelectedOnAuthFail is UnitTest
25152519 client.dispose ()
25162520 server.dispose ()
25172521
2522+ class \nodoc\ iso _TestSSLALPNSelectedOnError is UnitTest
2523+ """
2524+ `alpn_selected` on a session in `SSLError` returns `None`, even though
2525+ the session negotiated a protocol before it failed.
2526+ """
2527+ fun name (): String => " net/ssl/SSL.alpn_selected/on_error"
2528+
2529+ fun apply (h: TestHelper ) =>
2530+ let auth = FileAuth (h.env .root)
2531+ let client_ctx =
2532+ try
2533+ recover val
2534+ SSLContext
2535+ .> set_cert (
2536+ FilePath (auth, " assets/cert.pem" ),
2537+ FilePath (auth, " assets/key.pem" ))?
2538+ .> set_authority (FilePath (auth, " assets/cert.pem" ))?
2539+ .> alpn_set_client_protocols ([" h2" ])
2540+ end
2541+ else
2542+ h.fail (" client ssl context setup failed" )
2543+ return
2544+ end
2545+ let server_ctx =
2546+ try
2547+ recover val
2548+ SSLContext
2549+ .> set_cert (
2550+ FilePath (auth, " assets/cert.pem" ),
2551+ FilePath (auth, " assets/key.pem" ))?
2552+ .> set_authority (FilePath (auth, " assets/cert.pem" ))?
2553+ .> alpn_set_resolver (ALPNStandardProtocolResolver ([" h2" ]))
2554+ end
2555+ else
2556+ h.fail (" server ssl context setup failed" )
2557+ return
2558+ end
2559+
2560+ (let client, let server) =
2561+ try
2562+ _TestSSLSessionPair .attempt (h, client_ctx, server_ctx)?
2563+ else
2564+ h.fail (" could not create an SSL session pair" )
2565+ return
2566+ end
2567+
2568+ h.assert_true (
2569+ client.state () is SSLReady ,
2570+ " the client should be in SSLReady after a successful handshake" )
2571+
2572+ h.assert_true (
2573+ try (server.alpn_selected () as String ) == " h2" else false end ,
2574+ " the server should have negotiated h2 before the failure" )
2575+
2576+ try
2577+ _TestSSLCorruptRecord (client, server, " hello" )?
2578+ else
2579+ h.fail (" could not send a corrupted record" )
2580+ client.dispose ()
2581+ server.dispose ()
2582+ return
2583+ end
2584+
2585+ server.read ()
2586+
2587+ h.assert_true (
2588+ server.state () is SSLError ,
2589+ " a corrupted record should put the server in SSLError" )
2590+
2591+ h.assert_true (
2592+ server.alpn_selected () is None ,
2593+ " alpn_selected() on an SSLError session should return None" )
2594+
2595+ client.dispose ()
2596+ server.dispose ()
2597+
25182598class \nodoc\ iso _TestSSLReceiveOnAuthFail is UnitTest
25192599 """
25202600 `receive` on a session in `SSLAuthFail` does nothing.
@@ -4479,6 +4559,178 @@ class \nodoc\ iso _TestSSLBidirectionalCloseRoundtrip is UnitTest
44794559 client.dispose ()
44804560 server.dispose ()
44814561
4562+ class \nodoc\ iso _TestSSLCorruptRecordInClosed is UnitTest
4563+ """
4564+ A corrupted record received while in `_Closed` transitions to `SSLError`.
4565+
4566+ The session has already sent its own `close_notify` and is in `_Closed`.
4567+ A corrupted record arriving now is a genuine error. `_Closed.read`
4568+ preserves `_Closed` on a clean `zero_return` but lets `_Errored`
4569+ through.
4570+ """
4571+ fun name (): String => " net/ssl/SSL.read/corrupt_record_in_closed"
4572+
4573+ fun apply (h: TestHelper ) =>
4574+ (let client, let server) =
4575+ try
4576+ _TestSSLSessionPair (h)?
4577+ else
4578+ h.fail (" could not establish an SSL session pair" )
4579+ return
4580+ end
4581+
4582+ // Encrypt a record from the server before the close flow starts.
4583+ // This record belongs to the same TLS session, so corrupting it triggers
4584+ // a MAC failure the client's session can detect.
4585+ var saved_record: Array [U8 ] iso = recover iso Array [U8 ] end
4586+ try
4587+ server.write (" payload" )?
4588+ saved_record = server.send ()?
4589+ else
4590+ h.fail (" could not produce a record to corrupt" )
4591+ client.dispose ()
4592+ server.dispose ()
4593+ return
4594+ end
4595+
4596+ // Get the client into _Closed by calling close()
4597+ client.close ()
4598+
4599+ h.assert_true (
4600+ client.state () is SSLClosed ,
4601+ " client should be SSLClosed after close()" )
4602+
4603+ // Feed the corrupted record — same session, so the MAC failure is real
4604+ try
4605+ let last = saved_record.size () - 1
4606+ saved_record (last)? = saved_record (last)? xor 0xFF
4607+ client.receive (consume saved_record)
4608+ else
4609+ h.fail (" could not corrupt the saved record" )
4610+ client.dispose ()
4611+ server.dispose ()
4612+ return
4613+ end
4614+
4615+ client.read ()
4616+
4617+ h.assert_true (
4618+ client.state () is SSLError ,
4619+ " a corrupted record in _Closed should transition to SSLError" )
4620+
4621+ client.dispose ()
4622+ server.dispose ()
4623+
4624+ class \nodoc\ iso _TestSSLCanSendOnFailedSession is UnitTest
4625+ """
4626+ `can_send` and `send` work on a failed session, allowing retrieval of
4627+ alert bytes that OpenSSL queued in the output BIO before the failure.
4628+
4629+ A handshake failure from non-TLS input produces an alert. `can_send` returns
4630+ `true` while those bytes are in the BIO, and `send` retrieves them.
4631+ """
4632+ fun name (): String => " net/ssl/SSL.can_send/on_failed_session"
4633+
4634+ fun apply (h: TestHelper ) =>
4635+ (let client, let server) =
4636+ try
4637+ _TestSSLSessionPair .fresh (h)?
4638+ else
4639+ h.fail (" could not create a fresh SSL session pair" )
4640+ return
4641+ end
4642+
4643+ // Drive the handshake far enough that the server has state, then corrupt it
4644+ try
4645+ _TestSSLTransfer (client, server)?
4646+ _TestSSLTransfer (server, client)?
4647+ else
4648+ h.fail (" could not drive initial handshake exchange" )
4649+ client.dispose ()
4650+ server.dispose ()
4651+ return
4652+ end
4653+
4654+ // Feed non-TLS bytes to the server to trigger an error with an alert
4655+ server.receive (" NOT TLS DATA\r\n " )
4656+ server.read ()
4657+
4658+ h.assert_true (
4659+ server.state () is SSLError ,
4660+ " server should be in SSLError after receiving non-TLS bytes" )
4661+
4662+ h.assert_true (
4663+ server.can_send (),
4664+ " failed session should have alert bytes to send" )
4665+
4666+ try
4667+ let alert = server.send ()?
4668+ h.assert_true (
4669+ alert.size () > 0 ,
4670+ " alert bytes from a failed session should not be empty" )
4671+ else
4672+ h.fail (" send() on a failed session with queued alert raised an error" )
4673+ end
4674+
4675+ h.assert_false (
4676+ server.can_send (),
4677+ " can_send should be false after sending the alert" )
4678+
4679+ client.dispose ()
4680+ server.dispose ()
4681+
4682+ class \nodoc\ iso _TestSSLWriteBlockedInClosing is UnitTest
4683+ """
4684+ `write` raises an error in `_Closing`.
4685+
4686+ The peer has sent `close_notify`, and we have not responded yet. Writing
4687+ application data is not allowed — the only valid next step is `close` to
4688+ send our own `close_notify`.
4689+ """
4690+ fun name (): String => " net/ssl/SSL.write/blocked_in_closing"
4691+
4692+ fun apply (h: TestHelper ) =>
4693+ (let client, let server) =
4694+ try
4695+ _TestSSLSessionPair (h)?
4696+ else
4697+ h.fail (" could not establish an SSL session pair" )
4698+ return
4699+ end
4700+
4701+ // Get server into _Closing: client sends close_notify, server reads it
4702+ client.close ()
4703+
4704+ try
4705+ _TestSSLTransfer (client, server)?
4706+ else
4707+ h.fail (" could not transfer close_notify" )
4708+ client.dispose ()
4709+ server.dispose ()
4710+ return
4711+ end
4712+
4713+ server.read ()
4714+
4715+ h.assert_true (
4716+ server.state () is SSLClosed ,
4717+ " server should report SSLClosed after receiving peer's close_notify" )
4718+
4719+ let write_succeeded =
4720+ try
4721+ server.write (" should fail" )?
4722+ true
4723+ else
4724+ false
4725+ end
4726+
4727+ h.assert_false (
4728+ write_succeeded,
4729+ " write should raise an error in _Closing" )
4730+
4731+ client.dispose ()
4732+ server.dispose ()
4733+
44824734class \nodoc\ iso _TestMatchNameEmptyName is UnitTest
44834735 fun name (): String => " net/ssl/X509._match_name/empty_name"
44844736
0 commit comments