Rethinking SSL and protocol transforms in lori #156
Replies: 4 comments
-
Directions A and B are complementary, not alternativesAfter discussion, directions A (SSL as first-class connection concern) and B (two-level architecture with protocol adapters) are complementary rather than competing options. The combined model has three fixed layers:
Ordering is fixed by architecture — no user composition, no ordering footguns. SSL can never be put in the wrong place. Protocol adapters always operate on decrypted data. The actor always operates on protocol-parsed data. This is the direction to explore further. |
Beta Was this translation helpful? Give feedback.
-
|
Discussion #157 explores making SSL a first-class TCPConnection concern (Direction A). Four explicit constructors ( |
Beta Was this translation helpful? Give feedback.
-
TLS Alternatives ResearchFull research document exploring alternative TLS approaches for lori, evaluating five options along a spectrum from least to most ambitious. Alternative TLS Approaches for LoriCurrent StateLori uses the Key pain points with the current approach: Version ifdef complexity. The FFI surface area. The Global initialization. The Memory management. Two classes use finalizers ( Unused API surface. Lori uses a small fraction of the ApproachesApproach 1: Stay with OpenSSL, Drop 0.9.0 SupportWhat it is. Keep OpenSSL as the underlying TLS library but drop support for OpenSSL 0.9.0 in the How it would work with lori. No changes needed in lori. The What changes. In
Advantages.
Disadvantages.
Effort estimate. Small. This is removing code, not adding it. Could be done in a single PR to Risk factors. Minimal technical risk. The main risk is community pushback if any users still depend on 0.9.0, but that seems unlikely given the version has been EOL for over a decade. Approach 2: Switch to rustls via rustls-ffiWhat it is. Replace OpenSSL with rustls, using its C API (rustls-ffi). Rustls is a memory-safe TLS implementation written in Rust, backed by the ISRG/Prossimo memory safety initiative. Its C API is explicitly designed for caller-managed transport -- it never touches a socket. How it would work with lori. Rustls's I/O model maps directly to
The What changes. In Advantages.
Disadvantages.
Effort estimate. Medium. The Risk factors. Build toolchain complexity is the primary risk. The Pony ecosystem's build system ( Approach 3: Switch to mbedTLSWhat it is. Replace OpenSSL with mbedTLS (maintained by ARM via the TrustedFirmware project). MbedTLS is a C library designed for embedded systems with a small footprint, clean modular API, and no external dependencies. How it would work with lori. MbedTLS supports custom transport callbacks via What changes. In Advantages.
Disadvantages.
Effort estimate. Medium to large. Rewriting the Risk factors. The FFI callback model is the main technical risk. Pony's FFI system can create C-callable function pointers, but managing the context pointer and ensuring memory safety across the FFI boundary during callbacks requires careful implementation. The threading constraints need validation against Pony's scheduler. Lack of FIPS certification may matter for some users. Approach 4: Switch to s2n-tlsWhat it is. Replace OpenSSL with Amazon's s2n-tls, a TLS implementation focused on simplicity and security. s2n-tls is designed as a minimal TLS library that delegates cryptographic operations to a separate How it would work with lori. S2n-tls supports custom send/receive callbacks via What changes. In Advantages.
Disadvantages.
Effort estimate. Medium to large. Similar to mbedTLS in implementation complexity (callback-based I/O model). The additional complexity of managing a Risk factors. The per-thread state and global initialization are concerns for Pony's scheduler, which manages its own thread pool. Thread-local cleanup must happen on each scheduler thread, which is not straightforward to arrange in Pony. The Approach 5: Implement TLS Protocol in Pony, Use C Only for Crypto PrimitivesWhat it is. Implement the TLS record layer, handshake state machine, and protocol logic in Pony, calling out to C only for cryptographic primitives (AES, SHA, RSA, ECDH, X25519, etc.). This is the approach taken by Erlang/OTP, Haskell's How it would work with lori. A Pony TLS library would be a natural The crypto primitives would be the only FFI layer. These could come from OpenSSL's What changes. A new Pony library implementing TLS 1.2 and 1.3 protocol logic:
The existing Advantages.
Disadvantages.
Effort estimate. Large. This is a multi-month project for an experienced developer, plus ongoing maintenance. Implementing TLS 1.3 alone is more tractable than TLS 1.2+1.3 together, since TLS 1.3 is a significantly cleaner protocol. A phased approach (TLS 1.3 first, TLS 1.2 later) is possible. Risk factors. The primary risk is sustainability: can the Pony community maintain a TLS implementation long-term? Security bugs in a less-scrutinized implementation. Scope creep (TLS has many extensions, cipher suites, and edge cases). The effort may not be justified given Pony's community size unless there is a clear path to shared maintenance. Erlang's lesson about deadlock risk (the DiscussionThe approaches form a clear spectrum from least to most ambitious: Approach 1 (drop 0.9.0) is pure upside with near-zero risk. It should be done regardless of which other approach is pursued. The question is whether it goes far enough. Approaches 2-4 (alternative C libraries) trade one set of FFI complexity for another. The key differentiator is the I/O model:
Approach 5 (Pony TLS) is the most architecturally clean but the most expensive. The cross-language precedent is strong -- Erlang, Haskell, OCaml, Go, and Zig all chose this path, each for reasons that apply equally to Pony (actor model impedance mismatch, memory model conflicts, desire for transport-agnostic TLS). The question is whether the Pony community can sustain the maintenance. Key questions that would drive the decision:
|
Beta Was this translation helpful? Give feedback.
-
|
Direction A (SSL as a first-class TCPConnection concern) was implemented:
Protocol transformations (Direction B / composable protocol layers) remains an open area for future research if real use cases emerge. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Context
This discussion captures research and design exploration for rethinking how lori handles SSL/TLS and protocol-level data transforms. The goal is to explore whether better APIs exist that avoid the footguns inherent in interceptor composition, potentially requiring changes to the
ponylang/ssllibrary.Related:
Why lori exists: the mixin pattern advantage
Lori uses the mixin pattern rather than the notifier pattern used by the stdlib
netpackage. The key limitation of the notifier pattern (from the pattern documentation):In lori, the user's actor implements traits (
TCPConnectionActor,ServerLifecycleEventReceiver/ClientLifecycleEventReceiver) directly — the actor IS the connection handler. This means:The
ponylang/postgreslibrary demonstrates why this matters. ItsSessionactor simultaneously manages TCP connection lifecycle, PostgreSQL authentication state, query dispatch, wire protocol parsing, and result delivery — all in one actor with a trait-based state machine that makes illegal state transitions unreachable at compile time. None of this is possible with the notifier pattern.The problem: interceptors reintroduce notifier-pattern problems
The
DataInterceptortrait (from Discussion #149) solves the data ordering problem — read and write paths need opposite orderings for protocol layering. But it introduces a separate object into the mixin pattern's data path, and that object is essentially a notifier:For a single interceptor (just SSL), this is manageable. But composition of multiple interceptors (SSL + compression, SSL + custom framing) introduces serious footguns:
Ordering mistakes —
ChainedInterceptor(ssl, compression)vsChainedInterceptor(compression, ssl)silently corrupts data. The user must get this right manually.Setup/ready signaling with multiple handshaking interceptors — If two interceptors both need handshakes, who calls
signal_ready()when? TheChainedInterceptorproposal hand-waved this as "harmless double-signal."Error propagation across chains — If an inner interceptor fails during
incoming(), how does that flow back through the chain? The push model doesn't have a natural error return path.The interceptor is a notifier-like object inside a mixin architecture — It reintroduces the fundamental limitation that lori was created to avoid. The interceptor can't receive messages, can't be part of the actor's type-safe state machine, and composition creates a chain of objects with the same problems as notifier wrapping.
Current SSL architecture
How lori uses the ssl library today
Lori's
SSLClientInterceptorandSSLServerInterceptorimplementDataInterceptor, wrapping theponylang/ssllibrary'sSSLclass. TheSSLclass uses OpenSSL's memory BIO abstraction — it's a state machine where the caller feeds in encrypted bytes and reads out decrypted bytes:SSL.receive(data)— feed encrypted bytes from the wireSSL.read(expect)— extract decrypted plaintextSSL.write(data)— encrypt plaintext for sendingSSL.can_send()/SSL.send()— extract encrypted bytes for the wireSSL.state()— check handshake state (SSLReady | SSLAuthFail | SSLError)The interceptors bridge between this API and lori's
DataInterceptorinterface, managing handshake signaling, pending write buffering, and error handling.Pain points with ponylang/ssl
The
ponylang/ssllibrary wraps OpenSSL via FFI with approximately 75-80 distinct FFI calls. Key pain points:Version complexity. Three OpenSSL version families (0.9.0, 1.1.x, 3.0.x) with three-way
ifdefbranches in 10+ locations. Theopenssl_0.9.0flag is actually used for LibreSSL compatibility (see below), not ancient OpenSSL.LibreSSL misidentification. LibreSSL users build with
-Dopenssl_0.9.0, which forces them through a code path that disables ALPN (which LibreSSL supports), runs unnecessary locking ceremony (which is a no-op in LibreSSL), and uses legacy API names (which LibreSSL has modern equivalents for). LibreSSL's actual API is mostly OpenSSL 1.1.x compatible with a few holdovers. This is tracked as a separate issue.Global initialization.
_SSLInit._init()runs at program startup. For the 0.9.0/LibreSSL path, this involvesCRYPTO_set_locking_callbackreaching into the Pony runtime — fragile coupling.Memory management. Finalizers coordinate between Pony's GC and OpenSSL's C memory. BIO buffers are OpenSSL-allocated and freed by
SSL_free. TheDigestclass can leak its EVP context.Large FFI surface. ~55 OpenSSL FFI calls in the networking layer alone.
Alternative TLS libraries evaluated
Nine TLS libraries were evaluated as potential replacements. Key findings:
rustls-ffi — Best I/O model fit (no socket ownership, maps directly to DataInterceptor). Memory-safe TLS in Rust. Rejected: requires a Rust toolchain to build, which is not acceptable as a dependency for the Pony ecosystem.
mbedTLS — Self-contained C library, no external deps, clean API. Uses I/O callbacks where the callee performs data transfer — actually harder to integrate than OpenSSL's memory BIOs since it requires FFI callbacks from C into Pony. No FIPS certification.
s2n-tls — Battle-tested (Amazon S3), FIPS-capable. But requires a
libcryptodependency (doesn't eliminate OpenSSL), has global init (s2n_init()), and per-thread state that's awkward for Pony's scheduler.wolfSSL — Comprehensive features, FIPS certified. GPLv2 license is problematic for ponylang projects.
BearSSL — Ideal I/O model (pure state machine, zero allocation). Disqualified: unmaintained since 2018, no TLS 1.3.
picotls — Clean buffer-oriented API. TLS 1.3 only — can't connect to older servers.
BoringSSL, LibreSSL — Same memory BIO model as OpenSSL, no meaningful improvement for lori's use case. BoringSSL has no stable API/ABI.
GnuTLS — Heavy dependency chain (libnettle, GMP), global init requirements.
Conclusion: No alternative C TLS library is clearly better than a cleaned-up OpenSSL wrapper for lori's architecture. The callback-based libraries (mbedTLS, s2n-tls) are actually harder to integrate than OpenSSL's memory BIOs.
Cross-language patterns for TLS in actor-model languages
Research into how other actor-model and GC'd languages handle TLS revealed a strong pattern:
gen_statem)tlspackage)cryptonFFIocaml-tls)nocryptoaws-lc-rsThe consensus approach for languages with GC and/or actor models is: implement TLS protocol logic in the host language, use C only for crypto primitives. This eliminates the impedance mismatch between C TLS state machines and the host language's concurrency/memory model.
Key lessons:
tls_sendersplit in OTP 21).tlslibrary was motivated by exactly lori's problem: C TLS libraries are "extremely stateful" and wrapping them creates "IO mess to do essentially pure things."crypto/tlsrequires at least 5 named core maintainers — the maintenance burden is significant.nqsb-TLSachieved a trusted code base 25x smaller than OpenSSL with matching handshake performance.Design directions being explored
The fundamental tension: lori's mixin pattern gives the actor compile-time safety and message-receiving ability, but protocol transforms need to sit between the wire and the actor. How do you get protocol transforms without reintroducing notifier-pattern problems?
Direction A: SSL as a first-class connection concern
TCPConnectionhas built-in SSL support. The actor never sees encrypted data, never deals with handshake signaling:The connection manages the SSL session internally.
_on_connectedfires only after handshake completes._on_receiveddelivers decrypted data.send()encrypts automatically.For simpler transforms (compression, framing), the actor handles them in its callback implementations — no separate object needed, no composition headaches.
Implication for ssl library: The
SSLclass's memory BIO API (receive/read/write/send/state) would be used internally byTCPConnection. The ssl library might need minimal changes — theSSLclass is already transport-independent.Direction B: Two-level architecture
At most one "protocol adapter" (SSL) that lives inside the connection and handles complex lifecycle concerns (handshake, ready signaling, errors). Plus simple data transforms expressed as trait methods on the actor — no separate objects, no composition.
Ordering is fixed by architecture: wire → protocol adapter → actor transforms. No user-controlled ordering to get wrong.
Direction C: Transform as mixin traits
Protocol transforms expressed as traits the actor mixes in, keeping everything on the actor and preserving compile-time safety. But this has Pony trait composition challenges when multiple transforms want to override the same callback.
Open questions
ponylang/ssllibrary — does it need to change, or is its currentSSLclass API sufficient for whatever lori does internally?Beta Was this translation helpful? Give feedback.
All reactions