Skip to content

Repository files navigation

ex_ssl

ex_ssl is an experimental TLS implementation for Elixir/OTP with two primary goals:

  1. provide a client API that is behaviorally compatible with Erlang/OTP :ssl where implemented;
  2. provide programmable TLS ClientHello wire profiles for applications that need precise control over observable TLS handshake behavior.

The OTP application is :ex_ssl. The public compatibility module is SSL (Elixir.SSL), which does not conflict with Erlang's built-in :ssl module.

Status: experimental TLS 1.3 client runtime with authenticated connections, passive and active-once binary/raw traffic, application ownership transfer, ALPN lookup, bounded streaming writes, initial-handshake client authentication, and TCP-to-TLS upgrades. See the compatibility matrix for exact restrictions. OTP :ssl remains the default recommendation.

Version 0.4 also supports ordered TLS 1.3 algorithm options, explicit certificate-chain signature restrictions, one client identity, and a validated TCP option allowlist. These additions require ex_ssl 0.4 or newer; see compatibility and the implementation ledger for executed gates.

Installation

Add the ex_ssl package to your dependencies:

def deps do
  [
    {:ex_ssl, "~> 0.5.0"}
  ]
end

The Hex package name is ex_ssl.

Why ex_ssl?

OTP :ssl is the correct default TLS implementation for normal Erlang/Elixir applications. ex_ssl exists for cases where an application also needs deterministic or profile-driven control of the ClientHello wire representation, including characteristics such as:

  • cipher-suite ordering;
  • extension ordering;
  • supported versions/groups;
  • KeyShare shape and placement;
  • signature algorithm ordering;
  • ALPN ordering;
  • GREASE placement/policy;
  • legacy session-id/compatibility behavior;
  • padding and selected record-shaping behavior.

The library treats these as a wire profile, not as a JA3 configuration. JA3 and JA4 are derived fingerprints of the emitted/parsed ClientHello.

Naming

Package / repository: ex_ssl
OTP application:      :ex_ssl
Public API:            SSL
Internal namespace:    SSL.*

Migration is intended to look like:

# OTP
:ssl.connect(host, port, options)

# ex_ssl
SSL.connect(host, port, options)

Custom behavior is additive and namespaced:

SSL.connect(host, port,
  verify: :verify_peer,
  server_name_indication: host,
  ex_ssl: [
    profile: :default
  ]
)

Runtime baseline

The supported runtime baselines are Elixir 1.18 on Erlang/OTP 28, Elixir 1.19 on Erlang/OTP 28, and Elixir 1.20 on Erlang/OTP 29. CI covers all three tuples. OTP 29 remains the behavioral reference target for the implemented :ssl-compatible client feature subset.

The validation and security evidence map documents mandatory peer/runtime gates and the reproducible pinned downstream source-candidate/package check. Independent human security review remains incomplete.

Compatibility target

The initial compatibility baseline is the Erlang/OTP 29 :ssl client API.

Implemented client functions are connect/2,3,4, send/2, recv/2,3, close/1, setopts/2, controlling_process/2, and negotiated_protocol/1, connection_information/1,2, peercert/1, peername/1, and sockname/1. Full handshakes verify CertificateVerify and Finished; resumed handshakes verify the ticket-bound Finished. Connect succeeds after the client Finished is transmitted. The connection runs as a temporary supervised :gen_statem; a failed session is never restarted.

Defaults are deliberately restricted to binary, passive, raw, verified TLS 1.3. They differ from OTP's defaults. Supported options and receive/upgrade ownership rules are documented in the compatibility matrix.

TLS 1.3 resumption is opt-in with session_tickets: :auto; the default is :disabled. Auto requires TLS 1.3-only versions and no configured client identity. Tickets remain in a bounded in-memory cache, partitioned by endpoint and loaded trust/security policy. Each use revalidates the saved peer certificate chain and performs fresh ECDHE. A server declining PSK continues normal full authentication on that connection. No early data, automatic reconnect, or request replay occurs. See the resumption policy and compatibility details.

The wider roadmap (not implemented API) includes:

close/2
shutdown/2
getopts/2
getstat/1,2
update_keys/2
export_key_materials/4,5
format_error/1
versions/0

Compatibility means observable behavior, not only matching names and arities.

For implemented features, ex_ssl aims to match:

  • success/error tuple forms;
  • timeout semantics;
  • active/passive socket semantics;
  • controlling-process behavior;
  • standard TLS/socket option behavior;
  • STARTTLS-style upgrade of an existing TCP socket;
  • active message forms.

The active message contract targets OTP forms:

{:ssl, socket, data}
{:ssl_closed, socket}
{:ssl_error, socket, reason}
{:ssl_passive, socket}

Protocol scope

The TCP protocol target is a TLS client, using RFC 9846 as the normative TLS 1.3 specification. The separate SSL.QUIC engine also implements the record-free TLS 1.3 server role; its acceptance status is documented separately.

Initial target features:

  • TLS 1.3 ClientHello/ServerHello;
  • HelloRetryRequest;
  • EncryptedExtensions;
  • server Certificate / CertificateVerify / Finished;
  • client Finished;
  • X.509 path and service-identity verification;
  • ALPN;
  • TLS alerts and close_notify;
  • NewSessionTicket parsing;
  • KeyUpdate;
  • passive and OTP-style active receive modes;
  • STARTTLS upgrade;
  • programmable ClientHello WireProfile;
  • GREASE (RFC 8701);
  • JA3 and JA4 introspection.

Initial target cipher suites:

TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256

Initial target groups:

X25519
secp256r1
secp384r1

Runtime support depends on the crypto provider available to OTP. Implemented TLS 1.3 handshake signatures are P-256/P-384 ECDSA, Ed25519, and RSA-PSS-RSAE / RSA-PSS-PSS SHA-256/384/512, with strict key and parameter checks. See the compatibility matrix for the bounded subset; client certificates and explicit bounded TLS 1.2 are implemented in version 0.4.

Architecture

SSL.QUIC provides an experimental record-free TLS 1.3 client/server handshake API with caller-owned state and ordered traffic-secret actions. The TCP client shares its ClientHello/HRR and authentication core. This adds neither QUIC networking nor TCP server APIs. See the interface and implementation ledger. The pinned aioquic comparison exercises both roles and compares traffic secrets without QUIC networking.

SSL.Fingerprint.client_hello(bytes, :tcp | :quic) computes JA3/JA4 from actual naked ClientHello bytes; new/1 and feed/2 support bounded fragmented observation. Unknown IDs and wire ordering remain available separately from the analytical projections. See fingerprint API and references.

Application
    │
    ▼
   SSL                     OTP-compatible facade
    │
    ▼
SSL.Connection             :gen_statem per connection
    │
    ├── SSL.Protocol.*     records / handshake / transcript
    ├── SSL.Crypto.*       HKDF / AEAD / key schedule / ECDHE
    ├── SSL.PKIX.*         certificate + identity verification
    ├── SSL.ClientHello.*  WireProfile / GREASE / serializer
    ├── SSL.Fingerprint.*  JA3 / JA4 / wire inspection
    └── SSL.Packet         application packet modes
    │
    ▼
:gen_tcp

The raw TCP socket is controlled by SSL.Connection, normally using internal active: :once. Application-visible active mode is implemented above TLS so handshake/record processing cannot be disabled by application socket mode.

Wire profiles

The profile is the source of truth for what ex_ssl attempts to emit.

Conceptually:

WireProfile
  │
  ▼
validate against TLS/runtime capabilities
  │
  ▼
materialize per-connection randomness, GREASE and fresh KeyShare
  │
  ▼
ordered ClientHello AST
  │
  ├──► JA3
  ├──► JA4
  └──► exact-wire inspection
  │
  ▼
serialize

Profiles may describe KeyShare group/order/position but may not reuse actual ephemeral private/public key material between real connections.

Important limitations

Not a complete device impersonation layer

A matching TLS ClientHello does not guarantee that a remote service will see a client as identical to a browser or mobile application. Other observable layers can include TCP/IP behavior, HTTP/2 SETTINGS and framing, header behavior, QUIC/HTTP/3 behavior, application protocol details, and timing.

ex_ssl focuses on the TLS layer.

Explicit bounded TLS 1.2

Version 0.4 accepts explicit TLS1.2-only or mixed version lists while keeping TLS1.3 as the default. TLS1.2 requires ECDHE, AES-GCM, Extended Master Secret and secure-renegotiation indication; renegotiation is disabled. Peers without EMS, including the observed local OTP28 TLS1.2 server, fail explicitly. OpenSSL full/mTLS exchanges validate the bounded subset. See the compatibility matrix and progress ledger for evidence and remaining gates.

BEAM secret zeroization

Elixir/Erlang immutable binaries and garbage collection do not provide deterministic memory zeroization. ex_ssl minimizes secret lifetime and scope but does not claim guaranteed erasure of every in-memory copy.

Security model

Fingerprint fidelity never overrides TLS authentication.

ex_ssl must fail closed for:

  • invalid certificate chains;
  • service identity mismatch;
  • invalid CertificateVerify signatures;
  • invalid Finished values;
  • AEAD authentication failures;
  • invalid protocol state;
  • record/sequence limit violations.

The implementation uses OTP :crypto and :public_key for cryptographic primitives and X.509 functionality rather than reimplementing those primitives in Elixir.

Development status

The authenticated TLS 1.3 client, passive/active-once raw application traffic, STARTTLS, ownership transfer, public ALPN, KeyUpdate handling, and bounded multi-record writes are implemented. Development continues toward broader OTP API/options, active modes, packet modes, exporters, verified real-world profiles, performance work, and independent security review.

See:

Testing philosophy

The project uses four complementary test classes:

Protocol/vector tests

Cryptographic and transcript operations are checked against independent known results.

Property/fragmentation tests

Parsers are tested across arbitrary TCP, record, and handshake fragmentation boundaries.

Interoperability tests

Integration tests connect ex_ssl to generated local OTP :ssl peers and OpenSSL. The dedicated Caddy workflow validates fingerprint behavior.

Differential compatibility tests

Focused scenarios compare OTP :ssl and SSL behavior for implemented receive, path-depth, ownership, timeout, and active-mode semantics.

A compatibility claim is not complete until covered by tests.

Standards and references

Primary references:

License

Licensed under the Apache License 2.0.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages