swift-tls-nio

0.1.1

Protocol-neutral SwiftNIO transport adapter for swift-tls
1amageek/swift-tls-nio

What's New

swift-tls-nio 0.1.1

2026-09-07T02:16:40Z

Complete authenticated TLS close-notify reads after application inbound consumption stops.

Preserve application-data ordering/backpressure and typed stalled or unclean-peer failures.

Verified by 24 owner tests across three suites, including real TCP, and downstream SwiftWeb HTTPS/WSS shutdown. No protocol-version or broad zero-copy claim.

swift-tls-nio

CI Swift 6.4 License

swift-tls-nio is a protocol-neutral SwiftNIO channel adapter for the TLS 1.3 stream sessions provided by swift-tls. It turns a NIO byte-stream channel into a plaintext channel while preserving the TLS session, ownership, and failure contracts of swift-tls.

Important

swift-tls-nio 0.1.0 is the current tagged release. Use a versioned dependency for applications. The API may change in future releases, and production deployment requires an independent security review of the complete application stack.

Architecture

flowchart LR
    TCP["NIO byte-stream channel"] --> Adapter["TLSNIOHandler\nrecord framing + lifecycle"]
    Adapter --> Session["swift-tls\nTLS 1.3 session contract"]
    Session --> SSL["swift-ssl\ncryptography + PKI + TLS mechanisms"]
    Adapter --> Plaintext["Plaintext NIO pipeline"]
    Plaintext --> Protocol["HTTP / WebSocket / gRPC / custom protocol"]
Loading

TLSNIO owns NIO channel lifecycle, record framing, bounded transport buffers, timeouts, plaintext delivery, and orderly transport shutdown. It does not own a second TLS state machine. HTTP, WebSocket, socket bootstrap policy, certificate storage, and application policy remain with the consuming package.

Features

  • Client and server TLS 1.3 channel handling backed by swift-tls
  • TLS record framing across fragmented and arbitrarily coalesced ByteBuffer reads
  • Bounded pre-handshake outbound buffering and bounded suspended inbound buffering
  • Typed handshake, framing, capability, buffer, and shutdown failures
  • Caller-owned trust, credential selection, and signing through resumable capability events
  • TLS 1.3 NewSessionTicket issuance and one-shot session resumption state
  • Graceful close_notify exchange with a shutdown deadline
  • Direct destination-buffer application-data path into final NIO owners
  • Real TCP integration tests in addition to NIOEmbedded behavioral tests

Requirements

  • Swift development snapshot swift-6.4.x-DEVELOPMENT-SNAPSHOT-2026-08-14-a
  • SwiftNIO 2.101.0 or newer
  • macOS 26+, iOS 26+, tvOS 26+, watchOS 26+, or visionOS 26+

The repository pins the matching Swift selector in .swift-version. Do not mix the package with a different Swift 6.4 snapshot.

Installation

Add the current 0.1.0 release:

dependencies: [
    .package(
        url: "https://github.com/1amageek/swift-tls-nio.git",
        from: "0.1.0"
    ),
]

Add the TLSNIO product to the target that owns the NIO pipeline:

.target(
    name: "YourTarget",
    dependencies: [
        .product(name: "TLSNIO", package: "swift-tls-nio"),
    ]
)

TLSNIO reuses the TLS product internally. Import TLS in application code when constructing identities, trust roots, or TLSConfiguration values.

Add the handler

Create one handler per channel and install it before every handler that expects plaintext. syncOperations must run on the channel's event loop; a bootstrap's channel initializer is the usual installation point.

Client

import NIOCore
import TLS
import TLSNIO

var tlsConfiguration = TLSConfiguration.client(
    serverName: "example.com",
    alpn: ["http/1.1"]
)
tlsConfiguration.trustRoots = TLSTrustRoots(
    x509Roots: trustedRootCertificates
)

let tlsHandler = try TLSNIOHandler(
    clientConfiguration: tlsConfiguration
)
try channel.pipeline.syncOperations.addHandler(tlsHandler)

trustedRootCertificates is an application-owned [Certificate] containing the DER trust anchors for the peer. A custom certificateValidator may be used instead. verifyPeer defaults to true; construction fails explicitly if no trust roots or validator are configured.

Server

import NIOCore
import TLS
import TLSNIO

let tlsConfiguration = TLSConfiguration.server(
    identity: serverIdentity,
    alpn: ["http/1.1"]
)
let tlsHandler = try TLSNIOHandler(
    serverConfiguration: tlsConfiguration
)

try channel.pipeline.syncOperations.addHandlers(
    tlsHandler,
    applicationProtocolHandler
)

serverIdentity is an application-owned TLSIdentity. Later inbound handlers receive plaintext ByteBuffer values. Outbound ByteBuffer writes from those handlers are encrypted into TLS records.

WSS and other application protocols

WSS is ordinary WebSocket framing carried over a TLS-protected TCP channel. swift-tls-nio supplies the TLS layer; the HTTP upgrade and WebSocket handlers remain standard SwiftNIO application handlers placed after it.

flowchart LR
    TCP["TCP"] --> TLS["TLSNIOHandler"]
    TLS --> HTTP["NIOHTTP1 codec + WebSocket upgrader"]
    HTTP --> WS["WebSocket frame handlers"]
Loading

For HTTP/1.1 WebSocket upgrades, configure http/1.1 in the TLS ALPN list. The same ordering applies to HTTP, gRPC, or a custom byte protocol: TLS first, then the handler stack that consumes and produces plaintext.

Lifecycle and events

stateDiagram-v2
    [*] --> Handshaking: channelActive
    Handshaking --> Active: authenticated handshake
    Handshaking --> Failed: timeout / TLS failure
    Active --> Closing: close(.all)
    Active --> OutputClosed: close(.output)
    OutputClosed --> Closing: close(.all)
    Closing --> Closed: peer close_notify
    Closing --> Failed: shutdown timeout / EOF
    Failed --> Closed: transport close
Loading

channelActive remains visible to downstream handlers immediately. Plaintext writes made before authentication are retained in order under the configured byte budget. TLSNIOUserEvent.handshakeCompleted is emitted before those writes are drained.

The handler emits these typed inbound user events:

Event Meaning
handshakeCompleted(negotiatedProtocol:) Authentication and key establishment succeeded
capabilityRequested(_:) The TLS transition is suspended for caller-owned trust, credential, or signing work
sessionTicketReceived A client accepted a NewSessionTicket; call takeResumptionState() once
sessionTicketCreated(requestID:resumptionState:) A server created a ticket and its paired state
shutdownCompleted Both peers exchanged authenticated close_notify alerts

Resume a capability request through NIO's outbound user-event path with TLSNIOOutboundEvent.resumeCapability(response). The response token must match the exact pending request. Servers issue a ticket with TLSNIOOutboundEvent.issueSessionTicket(request).

Unknown user events are forwarded unchanged through the pipeline.

Session resumption

Resumption state is intentionally one-shot:

  1. The server sends issueSessionTicket with an opaque ticket value and a correlation requestID. When sessionTicketCreated arrives, it stores the returned state under those ticket bytes.
  2. The client receives sessionTicketReceived and takes its state exactly once with takeResumptionState().
  3. Each side supplies its corresponding state to the next handler initializer.
let resumedClientHandler = try TLSNIOHandler(
    clientConfiguration: clientConfiguration,
    resumptionState: clientResumptionState
)

let resumedServerHandler = try TLSNIOHandler(
    serverConfiguration: serverConfiguration,
    resumptionState: serverResumptionState
)

The application owns ticket storage, expiry policy, lookup, and replay policy. A state can be consumed by only one subsequent session.

Limits and failure contract

TLSNIOHandlerConfiguration.defaults applies these transport bounds:

Setting Default
handshakeTimeout 10 seconds
shutdownTimeout 5 seconds
maximumBufferedOutboundBytes 1 MiB while TLS is suspended
maximumBufferedInboundBytesWhileSuspended 1 MiB

Custom values must be positive and are validated by the throwing configuration initializer. TLSCiphertext payload length is capped at RFC 8446's 2^14 + 256 bytes. Bound violations, mismatched capability tokens, timeouts, interrupted handshakes, and unclean shutdowns are surfaced as TLSNIOError; they are never silently converted into success.

The outbound buffer bound covers plaintext retained before handshake completion or during a capability request. Once active, the handler recordizes writes immediately and relies on SwiftNIO's downstream writability and backpressure; the suspension budget is not a per-message size limit.

close(mode: .all) sends close_notify and waits for the authenticated peer response until the shutdown deadline. EOF without a peer close_notify is reported as TLSNIOError.uncleanShutdown. Input-only close is unsupported.

Data-path ownership

Complete TLS records are borrowed as zero-copy ByteBuffer slices. A fragmented record uses one bounded assembly owner because it must outlive the source read callback. Coalesced records are streamed one at a time, so the framer does not retain an unbounded batch.

For application traffic, the handler allocates the final destination ByteBuffer first and lends its writable tail through swift-tls to swift-ssl. Outbound plaintext is borrowed while authenticated encryption writes directly into the final TLS record owner. Inbound ciphertext is borrowed while authenticated plaintext is written directly into the final downstream owner. No compatibility [UInt8] payload materialization is used on this path.

The tracked-allocator test for one application send and receive asserts exactly two final NIO allocations, zero reallocations, and zero allocator copy operations. This is the measured NIO application-data path; bounded control messages, session tickets, and fragmented-record assembly retain owners because their lifetimes cross state-machine or read-callback boundaries.

Development

CI installs the pinned Swift snapshot, rejects local package dependencies, and runs the complete test suite with Xcode. See CONTRIBUTING.md for the local verification command.

Security reports should follow SECURITY.md.

License

Licensed under the Apache License, Version 2.0. See LICENSE.

Description

  • Swift Tools 6.4.0
View More Packages from this Author

Dependencies

Last updated: Tue Sep 29 2026 12:12:24 GMT-0900 (Hawaii-Aleutian Daylight Time)