Clause
← All documentation

ssl

Clause's ssl application exports the OTP 29 interface of the ssl module. The utility functions are implemented, and so are TLS 1.2 and 1.3 connections over the libssl engine, with the {handshake, hello} pause, close/2, shutdown/2 and tls_version/1, and renegotiation, key updates, exporters and secrets, TLS 1.2 session reuse, TLS 1.3 session tickets, early data and the server's server name options, custom transports and automatic rekeys, CRL checks with ssl_crl_cache and TLS 1.2 pre-shared keys. OCSP stapling is not done (optional), SRP is declined, and OTP's debug protocol traces of log_level are not produced. See the function inventory and differences.

Design

Each TLS connection is an Erlang process with OTP's semantics: an owner, controlling_process/2, active and passive modes, the {ssl, Socket, Data}, {ssl_closed, Socket} and {ssl_error, Socket, Reason} messages, and OTP's option validation, error terms and alerts. OpenSSL's libssl, linked from the same OpenSSL as crypto, runs the record layer and the handshake over memory buffers. The process moves bytes between libssl and the transport of the cb_info option, gen_tcp by default. Certificates are verified as OTP verifies them: libssl pauses the handshake in its verify callback, and the process runs public_key:pkix_path_validation/3 with the verify_fun, partial_chain and CRL options. TLS 1.2 and 1.3 come first; TLS 1.0, 1.1 and DTLS are optional.

Connections

Renegotiation, key updates and exporters

Over the engine's feature builtins.

Secrets

clause_ssl_secrets keeps the secrets of a connection whose keep_secrets option is not false. libssl logs them as NSS key log lines, which the engine queues as events; the TLS 1.3 traffic secrets after a KeyUpdate are derived in Erlang (RFC 8446 section 7.2).

Session reuse

TLS 1.2 sessions are saved and reused as OTP's ssl saves and reuses them. clause_ssl_session runs the caches; a session is libssl's session in its DER encoding, with the facts OTP's choices read (suite, own and peer certificates, server name, time stamp).

Session tickets

TLS 1.3 sessions resume with tickets as OTP's do. A ticket is a map: cipher_suite ({Cipher, Hash}), sni (the server_name_indication the client sent), timestamp and ticket (libssl's session in DER).

Early data

TLS 1.3 early data (0-RTT) as OTP's.

Automatic rekeys

As OTP's sender does: a TLS 1.3 connection sends a KeyUpdate that asks the peer to update its keys too before the data it has sent under its keys would pass key_update_at bytes (OTP's AES-GCM limit by default), and splits a larger send at that size, a KeyUpdate before each part. ChaCha20 connections do not count bytes. A TLS 1.2 connection renegotiates once renegotiate_at records (at most 2^28, the default) went out under its keys, the Finished message counting as the first; the send waits for the renegotiation. A server refuses another client renegotiation within 12 seconds, which ends an OTP or Clause client's connection. key_update_at needs TLS 1.3 among the versions; both take positive integers.

Transports

cb_info ({Module, DataTag, ClosedTag, ErrorTag}, or with a PassiveTag) names a gen_tcp-like module: the connection calls its connect, listen, accept, send, close, shutdown, controlling_process, setopts, getopts, peername, sockname and getstat, and reads its messages by the tags. Other values are {options, {cb_info, Value}}.

Server name indication

A server's sni_hosts ([{HostName, Options}]) or sni_fun (fun(HostName) -> Options | undefined | unrecognized) choose its options by the name a client sends. The handshake pauses in libssl's ClientHello callback while the connection asks them; options replace the server's (as handshake/2 options do: a cert and key replace its certificates) and the connection goes on under a context of them, with their certificates, verification, versions, suites and libssl options. undefined, an unlisted name or no name keep the server's options. unrecognized, an empty name and a name with a trailing dot end the handshake with an unrecognized_name alert; options that fail and a failing fun with handshake_failure, logged as OTP's. A server name option that turns verify_peer on takes the reuse_sessions default of verify_peer, as OTP's. The options are checked as OTP checks them: entries with names and option lists without sni_hosts, sni_fun of arity 1, not both.

CRL checks

crl_check (true, peer, best_effort or false, the default) checks the revocation status of the peer's certificate (peer) or of every certificate of its path, as OTP's ssl does. CRLs come from the crl_cache module ({Module, {Handle, Args}}, ssl_crl_cache by default): first by the certificate's distribution points (Module:lookup/3, else lookup/2), then by its issuer (Module:select/2); public_key:pkix_crls_validate/3 decides, its issuer_fun finding the CRL's signer by name and signature among the peer's chain and cacerts, and Module:fresh_crl/2 updating CRLs. A revoked certificate ends the handshake with certificate_revoked, an undetermined status with bad_certificate, except with best_effort. A verify_fun that accepts a valid certificate is then shown a failed check ({bad_cert, {revoked, Reason}}), and decides again. Callback results of {logger, {Level, Report, Metadata}, Value} are logged.

ssl_crl_cache keeps CRLs in clause_ssl_crls, an ETS store under ssl_sup: insert(URI, {der, CRLs} | {file, PemFile}) by the http distribution point URI (and by issuer), insert(Source) by issuer only, and delete/1 of a URI or of a source. Other URIs are {error, {only_http_distribution_points_supported, URI}}. An insert past crl_cache_max_size empties the store first, as OTP's. As in OTP 29.1.1, CRLs inserted by issuer are stored under the issuer name of their encoding, which the lookup by a certificate's issuer does not match. Clause has no inets: CRLs are not fetched over HTTP, and the {http, Timeout} argument of the internal cache does nothing.

Pre-shared keys

TLS 1.2 PSK suites (psk, dhe_psk, ecdhe_psk, rsa_psk key exchanges) take their keys from user_lookup_fun ({fun(psk, Name, UserState) -> {ok, Key} | error, UserState}), as OTP's do. A server's fun gets the client's identity, a client's fun the server's hint (psk_identity of the server; undefined without a ServerKeyExchange, <<>> for an empty one). A client sends its psk_identity, else "psk_identity". libssl asks for keys in a synchronous callback, so the engine reads the peer's plaintext handshake first: a server holds the bytes of the ClientKeyExchange, a client of a PSK suite those of the ServerHelloDone, while the connection asks the fun. A fun without a key ends the handshake with illegal_parameter. The options are checked as OTP's: psk_identity is a string shorter than 64 KiB, user_lookup_fun a pair with a fun of arity 3, neither with versions that are only TLS 1.3. libssl has no ECDHE_PSK suites with AES-GCM or AES-CCM, so they are never negotiated. SRP suites are declined: libssl's SRP interface is deprecated; srp_identity is checked but no SRP suite is negotiated.

Verification

clause_ssl_certificate checks a peer's chain when the handshake pauses, as OTP's ssl does. A sent root found in cacerts, or the issuer of the chain's top in cacerts (by name and signature), becomes the trusted certificate. A single certificate is first completed from cacerts. partial_chain may trust a certificate of the chain instead. Then public_key:pkix_path_validation/3 runs with max_path_length from depth and ssl's verify fun. That fun fails on bad certificates and checks a client's server name against the peer's certificate (server_name_indication, else the host, with customize_hostname_check); a user verify_fun sees every event, valid_peer replaced by the host name failure. Failures become OTP's alerts: unknown_ca, certificate_expired, bad_certificate (host names, signatures, self-signed peers), unsupported_certificate, certificate_revoked, else handshake_failure. Servers send the certificate chain built from cacerts, and their cacerts name the certificate authorities of a certificate request.

Engine

clause_ssl_engine runs the TLS side of one connection over libssl (builtins). Its builtins live in a separate clause_runtime_ssl archive, linked with libssl of the OpenSSL the crypto archive uses. Only programs that call the engine link it. A context holds the options ssl.erl validated, and an engine holds one connection over memory buffers. Each handshake, read, write, shutdown or key update step returns the bytes to send. A client continuing a paused handshake moves its engine to a new context. When it pauses for verification, the peer's chain is available, and Erlang answers with ok or the alert to send. libssl 4.1 and later pause on both sides. A server linked with an older libssl verifies the client's chain after its handshake is done, and a rejection then closes the connection without sending the alert (and does not verify it again in a renegotiation). libssl's callbacks queue events that Erlang takes after each step: the secrets it logs, KeyUpdates sent and received, and TLS 1.2 renegotiations started, finished or refused.

Application

ssl.app names clause_ssl_app as the start module. clau adds an application's start module to every program that uses a module of that application, because no module calls it. ssl:start/0,1 runs application:ensure_all_started(ssl, Type), which also starts crypto, asn1 and public_key. The top supervisor is registered as ssl_sup. Its children are clause_ssl_pem_cache, registered as ssl_pem_cache as in OTP, the client session cache registered as ssl_manager (session reuse), the client ticket store tls_client_ticket_store (session tickets), and tls_connection_sup, whose temporary children are the connection processes. Connections started without the application run unsupervised. ssl:clear_pem_cache/0 calls it the way OTP does: it sets the caller's ssl_pem_cache process dictionary key, and it exits with {noproc, {gen_server, call, [ssl_pem_cache, {unconditionally_clear_pem_cache, Caller}, infinity]}} when ssl is not started.

Utility functions