Skip to content

TLS peer-close handling does not follow the negotiated protocol version #177

Description

@kentbull

Problem

When TLS receive reaches authenticated peer close_notify, HIO records only generic receive cutoff. It does not consult SSLSocket.version(), even though TLS 1.2 and TLS 1.3 require different treatment of the local transmit direction.

Using one policy for both versions either discards valid TLS 1.3 output or permits output that TLS 1.2 requires the recipient to abandon. Missing or unsupported negotiated versions also need an explicit fail-closed outcome rather than silently inheriting the permissive TLS 1.3 path.

This issue starts only after #173 has distinguished authenticated close_notify from abrupt TCP EOF.

Protocol contract

TLS 1.2 requires the recipient of close_notify to send its own notification, close immediately, and discard pending writes.

TLS 1.3 changes that rule. Peer close_notify closes the peer's write direction but has no effect on the local write direction. Accepted local output may continue before local closure.

The policy must use the version negotiated for this connection, returned by SSLSocket.version(), not the versions enabled on the context. A return value of None after authenticated closure indicates inconsistent endpoint state; an unknown value has no defined HIO policy. Both must fail closed.

Required behavior

TLS 1.3

  • Mark local receive closed.
  • Keep transmit available.
  • Preserve and continue sending accepted output.
  • Do not start local TLS shutdown solely because the peer sent close_notify.
  • Record no transport error.

TLS 1.2

  • Mark both directions terminal for application I/O.
  • Preserve queued bytes for inspection.
  • If bytes are stranded, retain a TransmitClosedError with the exact count.
  • Enter the recurrent reciprocal shutdown path from TLS endpoints lack a recurrent close_notify shutdown path #176 immediately.
  • Reject later application output.

Missing or unsupported version

  • Mark both directions terminal.
  • Retain a VersionError identifying the missing or unsupported value.
  • Force-close and surface the error.
  • Attempt no further application transmission.

Reproduction

  • Establish a TLS 1.2 connection, queue output, and receive peer close_notify.
  • Verify that current main records only generic receive cutoff instead of abandoning application transmission and starting reciprocal shutdown.
  • Repeat under TLS 1.3 and verify that the endpoint has no explicit guarantee that transmit remains available.
  • Repeat deterministically with version() returning None and an unsupported value.

Equivalent regressions are required for ClientTls and RemoterTls, including zero and nonzero queued-output cases.

Proposed direction

Route authenticated peer closure through a version-aware handler:

def _receiveClosed(self):
    version = self.cs.version()
    self.cutoff = True

    if version == "TLSv1.3":
        return bytes()

    if version == "TLSv1.2":
        remaining = len(self.txbs)
        self.txCutoff = True
        if remaining:
            self.error = TransmitClosedError(
                f"TLSv1.2 peer close_notify stranded {remaining} bytes"
            )
        if not self._closing:
            self.serviceClose()
        return bytes()

    self.txCutoff = True
    self.error = VersionError(
        f"unsupported negotiated TLS version {version!r}"
    )
    self.close()
    raise self.error

The exact state names may differ, but the negotiated-version decision, directional behavior, exact stranded-byte accounting, and retained failure must remain observable.

Buffered plaintext and WANT_READ recurrence belong to #176. The close service must drain pending plaintext before the first local unwrap() and wait for receive processing to consume peer close_notify before retrying it.

Output accounting

TLS 1.2's required discard is an application-transmit decision, not permission to erase diagnostics. Bytes already accepted into txbs should remain available for inspection even though they can no longer be sent. The retained error must report the exact byte count so owners can distinguish clean reciprocal closure from accepted output loss.

Dependencies

Acceptance criteria

  • Both TLS endpoint roles branch on SSLSocket.version().
  • TLS 1.3 peer close ends only local receive and preserves transmission.
  • TLS 1.2 peer close starts reciprocal shutdown and rejects further output.
  • TLS 1.2 preserves stranded bytes and reports their exact count without inventing an error for zero bytes.
  • Missing and unsupported versions fail closed with retained VersionError.
  • Real TLS 1.2 and TLS 1.3 tests cover both endpoint roles.
  • Deterministic tests cover missing versions, unknown versions, exact byte counts, and recurrent-shutdown interaction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions