Repository: aws/s2n-tls (C)

Issue:
End-of-data signals not compliant with RFC
### Problem:
According to https://www.rfc-editor.org/rfc/rfc8446.html#section-6.1, we should not be indicating End-of-Data to the application without receiving a close_notify alert. If we simply get an end-of-data indicator from our transport, it could mean that the application data was actually truncated.

### Solution:

We need to rewrite [this special error handling](https://github.com/aws/s2n-tls/blob/bcaaaeb8dd6eed8e9fe4c51ec4a9d2a3f3101162/tls/s2n_recv.c#L130-L137) to not return 0 for S2N_ERR_CLOSED. We need to either always return an error, discarding the information about bytes read, or at least return an error if no bytes were read.

If we go with the latter (and maybe anyway), we should also revisit[ the logic that returns 0 from a closed connection](https://github.com/aws/s2n-tls/blob/bcaaaeb8dd6eed8e9fe4c51ec4a9d2a3f3101162/tls/s2n_recv.c#L117-L119). We should only return 0 if the connection was closed gracefully, with a close_notify. We SHOULD be able to do that by checking conn->close_notify_received, except that [my testing ran into this issue](https://github.com/aws/s2n-tls/issues/3935).

* **Does this change what S2N sends over the wire?** If yes, explain.
* **Does this change any public APIs?** If yes, explain.
* **Which versions of TLS will this impact?**

### Requirements / Acceptance Criteria:

What must a solution address in order to solve the problem? How do we know the solution is complete?

* **RFC links:** Links to relevant RFC(s)
* **Related Issues:** Link any relevant issues
* **Will the Usage Guide or other documentation need to be updated?**
* **Testing:** How will this change be tested? Call out new integration tests, functional tests, or particularly interesting/important unit tests.
  * **Will this change trigger SAW changes?** Changes to the state machine, the s2n_handshake_io code that controls state transitions, the DRBG, or the corking/uncorking logic could trigger SAW failures.
  * **Should this change be fuzz tested?** Will it handle untrusted input? Create a separate issue to track the fuzzing work.

### Out of scope:

Is there anything the solution will intentionally NOT address?

[//]: #  (NOTE: If you believe this might be a security issue, please email aws-security@amazon.com instead of creating a GitHub issue. For more details, see the AWS Vulnerability Reporting Guide: https://aws.amazon.com/security/vulnerability-reporting/ )




Fix (patch):
diff --git a/bin/echo.c b/bin/echo.c
index 7243c4e8190..53affc0167a 100644
--- a/bin/echo.c
+++ b/bin/echo.c
@@ -261,8 +261,10 @@ int negotiate(struct s2n_connection *conn, int fd)
             fprintf(stderr, "Failed to negotiate: '%s'. %s\n",
                     s2n_strerror(s2n_errno, "EN"),
                     s2n_strerror_debug(s2n_errno, "EN"));
-            fprintf(stderr, "Alert: %d\n",
-                    s2n_connection_get_alert(conn));
+            if (s2n_error_get_type(s2n_errno) == S2N_ERR_T_ALERT) {
+                fprintf(stderr, "Alert: %d\n",
+                        s2n_connection_get_alert(conn));
+            }
             S2N_ERROR_PRESERVE_ERRNO();
         }
 
@@ -375,12 +377,17 @@ int echo(struct s2n_connection *conn, int sockfd, bool *stop_echo)
                     return 0;
                 }
                 if (bytes_read < 0) {
-                    if (s2n_error_get_type(s2n_errno) == S2N_ERR_T_BLOCKED) {
-                        /* Wait until poll tells us data is ready */
-                        continue;
+                    switch (s2n_error_get_type(s2n_errno)) {
+                        case S2N_ERR_T_BLOCKED:
+                            /* Wait until poll tells us data is ready */
+                            continue;
+                        case S2N_ERR_T_ALERT:
+                            fprintf(stderr, "Received alert: %d\n", s2n_connection_get_alert(conn));
+                            break;
+                        default:
+                            fprintf(stderr, "Error reading from connection: '%s'\n", s2n_strerror(s2n_errno, "EN"));
+                            break;
                     }
-
-                    fprintf(stderr, "Error reading from connection: '%s' %d\n", s2n_strerror(s2n_errno, "EN"), s2n_connection_get_alert(conn));
                     exit(1);
                 }
 
diff --git a/tls/s2n_recv.c b/tls/s2n_recv.c
index d1f04814dc8..0f6256844f1 100644
--- a/tls/s2n_recv.c
+++ b/tls/s2n_recv.c
@@ -115,8 +115,23 @@ ssize_t s2n_recv_impl(struct s2n_connection *conn, void *buf, ssize_t size, s2n_
     POSIX_GUARD(s2n_blob_init(&out, (uint8_t *) buf, 0));
 
     if (!s2n_connection_check_io_status(conn, S2N_IO_READABLE)) {
+        /*
+         *= https://tools.ietf.org/rfc/rfc8446#6.1
+         *# If a transport-level close
+         *# is received prior to a "close_notify", the receiver cannot know that
+         *# all the data that was sent has been received.
+         *
+         *= https://tools.ietf.org/rfc/rfc8446#6.1
+         *# If the application protocol using TLS provides that any data may be
+         *# carried over the underlying transport after the TLS connection is
+         *# closed, the TLS implementation MUST receive a "close_notify" alert
+         *# before indicating end-of-data to the application layer.
+         */
+        POSIX_ENSURE(conn->close_notify_received, S2N_ERR_CLOSED);
+        *blocked = S2N_NOT_BLOCKED;
         return 0;
     }
+
     *blocked = S2N_BLOCKED_ON_READ;
 
     POSIX_ENSURE(!s2n_connection_is_quic_enabled(conn), S2N_ERR_UNSUPPORTED_WITH_QUIC);
@@ -127,13 +142,11 @@ ssize_t s2n_recv_impl(struct s2n_connection *conn, void *buf, ssize_t size, s2n_
         uint8_t record_type;
         int r = s2n_read_full_record(conn, &record_type, &isSSLv2);
         if (r < 0) {
-            if (s2n_errno == S2N_ERR_CLOSED) {
-                *blocked = S2N_NOT_BLOCKED;
-                if (!bytes_read) {
-                    return 0;
-                } else {
-                    return bytes_read;
-                }
+            /* Don't propagate the error if we already read some bytes.
+             * We'll report S2N_ERR_CLOSED on the next call.
+             */
+            if (s2n_errno == S2N_ERR_CLOSED && bytes_read) {
+                return bytes_read;
             }
 
             /* Don't propagate the error if we already read some bytes */
diff --git a/tls/s2n_shutdown.c b/tls/s2n_shutdown.c
index 4afe783d615..e547839284b 100644
--- a/tls/s2n_shutdown.c
+++ b/tls/s2n_shutdown.c
@@ -73,6 +73,7 @@ int s2n_shutdown(struct s2n_connection *conn, s2n_blocked_status *blocked)
      */
     if (!s2n_handshake_is_complete(conn)) {
         POSIX_GUARD_RESULT(s2n_connection_set_closed(conn));
+        *blocked = S2N_NOT_BLOCKED;
         return S2N_SUCCESS;
     }