Packages

An independent Elixir/OTP TLS stack with programmable ClientHello wire profiles

Current section

Files

Jump to
ex_ssl README.md
Raw

README.md

# 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 under validation for an opt-in Manifold backend. The public API supports authenticated connections, passive binary/raw application traffic, and TCP-to-TLS upgrades. See the [compatibility matrix](docs/COMPATIBILITY.md) for the exact scope, test evidence, restrictions, and consumer gates. OTP `:ssl` remains the default recommendation.
## Installation
Once published, add the `ex_ssl` package to your dependencies:
```elixir
def deps do
[
{:ex_ssl, "~> 0.1.0"}
]
end
```
The Hex package name is [`ex_ssl`](https://hex.pm/packages/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
```text
Package / repository: ex_ssl
OTP application: :ex_ssl
Public API: SSL
Internal namespace: SSL.*
```
Migration is intended to look like:
```elixir
# OTP
:ssl.connect(host, port, options)
# ex_ssl
SSL.connect(host, port, options)
```
Custom behavior is additive and namespaced:
```elixir
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.
## 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`, and
`close/1`. Calls return success only after CertificateVerify and Finished
verification and transmission of client Finished. 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](docs/COMPATIBILITY.md).
The wider roadmap (not implemented API) includes:
```text
close/2
shutdown/2
setopts/2
getopts/2
controlling_process/2
peername/1
sockname/1
peercert/1
negotiated_protocol/1
connection_information/1,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:
```elixir
{:ssl, socket, data}
{:ssl_closed, socket}
{:ssl_error, socket, reason}
{:ssl_passive, socket}
```
## Protocol scope
The first protocol target is **TLS 1.3 client only**, using RFC 9846 as the normative specification.
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:
```text
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
```
Initial target groups:
```text
X25519
secp256r1
```
Runtime support depends on the crypto provider available to OTP.
## Architecture
```text
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:
```text
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.
### TLS 1.2 comes later
Many real clients advertise both TLS 1.3 and TLS 1.2. A profile must not normally advertise a protocol version that `ex_ssl` cannot negotiate. TLS 1.2 client support is therefore a planned follow-up required for broader faithful profile coverage.
### 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 foundation gate, the Phase 3D/4A deterministic wire/parsing milestone, and the pure Phase 4 authenticated server-flight gate are complete. Development continues through these staged gates:
1. first authenticated TLS 1.3 connection;
2. application data;
3. OTP active/passive compatibility;
4. STARTTLS;
5. broader OTP API/options;
6. KeyUpdate/exporters/resumption;
7. verified real-world profiles and hardening.
See:
- [ARCHITECTURE.md](docs/ARCHITECTURE.md)
- [DESIGN.md](docs/DESIGN.md)
- [PRD.md](docs/PRD.md)
- [IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md)
- [AGENTS.md](AGENTS.md)
## 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.
### Planned interoperability tests
The compatibility harness is currently a skeleton. Future phases will connect
`ex_ssl` to independent TLS implementations such as OTP `:ssl` and OpenSSL.
### Planned differential compatibility tests
Future phases will run equivalent socket/API scenarios against `:ssl` and `SSL`,
comparing externally visible behavior.
A compatibility claim is not complete until covered by tests.
## Standards and references
Primary references:
- TLS 1.3: RFC 9846 — https://www.rfc-editor.org/info/rfc9846/
- GREASE: RFC 8701 — https://www.rfc-editor.org/info/rfc8701/
- TLS service identity: RFC 9525 — https://www.rfc-editor.org/info/rfc9525/
- Erlang/OTP `:ssl`: https://www.erlang.org/doc/apps/ssl/ssl.html
- Erlang/OTP `:crypto`: https://www.erlang.org/doc/apps/crypto/crypto.html
- Erlang/OTP `:public_key`: https://www.erlang.org/doc/apps/public_key/public_key.html
- JA4 technical details: https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md
## License
Licensed under the [Apache License 2.0](LICENSE).