Skip to content

kestrel: the ASP.NET adapter has no tests, and exposes a fraction of the TLS surface #196

Description

@MDA2AV

Three bugs were fixed in ioxide.Kestrel during the second-round TLS review, and all three were found by reading rather than by a failing test - because there is no test anywhere in the repo that exercises this adapter. grep -rl Kestrel tests/ returns one file, and it is a comment in DecryptFaultTests.cs.

For the record, the three that were fixed, since they set the tone for what the missing coverage costs:

  • ITlsConnectionFeature.ClientCertificate was never assigned anywhere, and its comment still said "ioxide.tls never requests a client certificate" - untrue since mTLS shipped. An app configuring ClientCaPath plus the standard certificate authentication handler saw null for every peer and denied all of them, with the handshake having verified them perfectly.
  • ITlsHandshakeFeature returned constants: SslProtocols.Tls13 and TLS_AES_128_GCM_SHA256 for every connection. True only while kTLS-TX was the default and pinned exactly those; once KernelTx became opt-in, a real TLS 1.2 / AES-256 / ChaCha20 connection was reported to middleware and compliance logging as TLS 1.3 AES-128.
  • Both pumps in HopDuplexPipe caught every exception and completed their pipes cleanly, so a bad MAC or a truncated record reached Kestrel as an ordinary end of stream - the exact property TlsDecryptingPipeReader documents and has a test for, inverted in its Kestrel twin.

Each of those is the kind of thing one end-to-end test would have caught on the day it was written. What is left:

No coverage at all. Nothing starts Kestrel on this transport. The three fixes above are verified by inspection. A minimal suite - an ASP.NET app on UseIoxide(), one HTTPS request, and assertions on HttpContext.Connection.ClientCertificate, ITlsHandshakeFeature.Protocol and NegotiatedCipherSuite - would pin all three and cost one test project.

IoxideTlsOptions exposes a fraction of TlsOptions (src/serving/ioxide.Kestrel/IoxideTransportOptions.cs:59). It carries CertificatePath, KeyPath, KernelTx, Alpn and Ports. It does not carry mutual TLS (ClientCaPath, ClientCaPem, RequireClientCertificate), MinProtocolVersion, CipherSuites, CipherList, CertificatesByHost (so no SNI), HandshakeTimeoutMs, the in-memory PEM sources, or KernelRx. So an ASP.NET host on this transport cannot ask for mutual TLS at all - which makes the ClientCertificate fix above necessary but not yet reachable through the supported configuration surface, and it cannot state a protocol floor or a ciphersuite posture that the rest of the fleet states in TlsOptions.

The ClientCertificate fix is still worth having: IoxideTransportOptions.ConfigureServer lets a host build its own ServerConfig, and nothing stops a TlsService being started with anchors that way. But the first-class path does not offer it.

ITlsHandshakeFeature.HostName returns string.Empty (IoxideTlsFeature.cs:84). The negotiated SNI name is never surfaced, so an app cannot see which host the client asked for. TlsSession does not expose it either - the same gap the second-round review raised on the ioxide side - so this needs the session to report the name before the feature can.

A contract the adapter quietly breaks. TlsSession is documented reactor-thread-only, and HopDuplexPipe.DisposeAsync disposes it off-reactor (:258). It is correct today because await _recvPump has quiesced the only other user by that point, but the documentation and the usage disagree, and TlsSession.Dispose performs a raw send() on a bare fd with no generation check - so the invariant that makes it safe is timing, not structure. Either the contract should be widened deliberately or the disposal should be marshalled.

Filed rather than fixed because the owner's call is how much of TlsOptions this adapter should mirror - the whole surface, or a deliberate subset with the rest reachable only through ConfigureServer. That decision shapes what the tests should assert.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions