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
- Options (
clause_ssl_options):connect,listenandhandshakesplit their list as OTP does into ssl options, the socket options ssl emulates (mode,packet,packet_size,header,active,line_delimiter) and the transport's own. The ssl options are checked in OTP's order with OTP's errors:{options, {Key, Value}},{options, incompatible, [...]}(a client withverify_peerand nocacerts),{option, server_only | client_only, Key},{options, missing_version, ...},{options, {option_not_a_key_value_tuple, X}}, and{error, nooptions}forlisten(Port, []). When an option repeats, the last one wins. A client'sserver_name_indicationdefaults to the host name it connects to.cipherstakes suite maps, tuples, codes, names or an OpenSSL string and keeps the suites its versions take. Certificate files are read when a server listens or a client has connected; their errors are{options, {certfile, {File, Reason}}}. Options the checks do not cover are kept as given. - Processes (
clause_ssl_connection): onegen_serverper connection undertls_connection_supowns the transport socket (in{active, once}only while data is wanted) and the engine. Thesslsocketrecord keeps OTP's fields.connectruns the handshake before returning.transport_acceptreturns a server connection over the listening socket's engine context, andhandshake/1,2,3runs it, with a context of its own when handshake options change the configuration.connect/2,3andhandshake/2,3upgrade a connectedgen_tcpsocket, whose emulated options carry over. - Data:
sendframes packets of type 1, 2 and 4. Passiverecvtakes an exact length in raw mode,einvalin an active mode andtimeoutafter its timeout. Active modes deliver{ssl, S, Data}as each packet completes (erlang:decode_packet/3for packet types).onceand counts turn passive, a count with{ssl_passive, S}, and{active, N}adds to a count.headerreturns the first bytes of a binary as a list. - Hello pause (
clause_ssl_hello): with{handshake, hello}the connection holds the peer's first bytes until its hello message (a ClientHello to a server, a ServerHello to a client) is complete, before libssl sees them. The handshake then returns{ok, SslSocket, Extensions}, the hello's extensions as OTP decodes them (builtins).handshake_continue/2,3feeds the held bytes to libssl under the updated options: a server gets a new engine when they change, a client's engine moves to a new context for its certificate and verification. A refused option returns its error and ends the handshake with aninternal_erroralert;handshake_cancel/1sendsuser_canceled. Both alerts go out as plaintext records, since no keys are in use yet. The peer's extensions are libssl's when the peer is a Clause connection (differences). - Reading ahead: a passive connection reads from the transport while its buffer is empty, as OTP's does, so it answers renegotiations and key updates of an idle peer. Like OTP's, it acts on the peer's close_notify only once it is read from or turns active.
- Closing:
close/1sends close_notify and closes the transport; the owner's exit does the same.close/2with a timeout closes the same way.close(S, {NewOwner, Timeout})sends close_notify, waits for the peer's, and hands the transport socket toNewOwnerin passive binary mode as{ok, Socket}, or{ok, Socket, Data}with the bytes that came after the peer's close_notify;{error, timeout}when it does not come in time. A passive connection that already has the peer's close_notify hands the socket over at once.shutdown(S, read | write)shuts the transport down without close_notify, as OTP's does, andread_writesends close_notify first; a listening socket gives{error, enotconn}. A handshake whose peer closes the transport after an alert libssl ignored (TLS 1.3'suser_canceled) fails with that alert. When the peer closes, the connection answers its close_notify; an active owner then gets{ssl_closed, S}, or{ssl_error, S, {tls_alert, ...}}after an alert, and a passive reader gets{error, closed}once the buffered data is read. A finished connection answers every call with{error, closed}. - Information:
peername,socknameandgetstatask the transport.getoptslists the transport's options and then the emulated ones, the last asked first, as OTP's does.connection_information/1,2gives the negotiated items (a server also givessni_hostname) and the main options;connection_information/2lists them in OTP's order and adds the secrets asked for.peercertgives the peer's certificate, andnegotiated_protocolgives the ALPN protocol or{error, protocol_not_negotiated}. The internaltls_version/1maps a version tuple to its TLS version, a DTLS one to its counterpart. - Alerts (
clause_ssl_alert): a failed handshake returns{error, {tls_alert, {Description, Text}}}with OTP's description atoms and texts, and logs the alert atnoticeunlesslog_levelis above it. The text leaves out OTP's source location (differences).
Renegotiation, key updates and exporters
Over the engine's feature builtins.
- Renegotiation (TLS 1.2):
renegotiate/1starts a new full handshake, a client's with a ClientHello, a server's with a HelloRequest, and returnsokwhen it is done; the peer's chain is verified again.{error, already_renegotiating}while one is running,{error, notsup}on TLS 1.3,{error, enotconn}on a listening socket. Sends wait until the handshake lets libssl write again. A server withclient_renegotiation(the default) accepts a client's renegotiation, then refuses others for 12 seconds as OTP's does, with ano_renegotiationwarning; the client gets{error, renegotiation_rejected}(libssl then ends its connection, differences). - Key updates (TLS 1.3):
update_keys(S, write | read_write)sends a KeyUpdate;read_writeasks the peer to update its keys too, which the peer does when it reads it. Another type gives{error, {illegal_parameter, Type}}and a listening socket{error, not_supported}. - Exporters:
export_key_materials/4,5run libssl's exporter (RFC 5705 for TLS 1.2, RFC 8446 for TLS 1.3, whereno_contextis an empty context). TLS 1.3 takes lists of labels, contexts and lengths, gives{error, bad_input}for lists of other lengths or bad contexts, and withConsumeSecret(the default of/4) leaves no exporter for later calls:{error, exporter_master_secret_already_consumed}. TLS 1.2 exports one label, unwrapped, as OTP's does; other requests end the connection with{error, closed}. The deprecatedprf/5exports withmaster_secretand[client_random, server_random | [Context]], and runs TLS 1.2's PRF on its arguments under the negotiated suite's hash otherwise.
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).
connection_information/2givesclient_random,server_randomand the TLS 1.2master_secretwhen one of them orkeylogis asked for, as OTP's does, even withoutkeep_secrets.keylogneeds{keep_secrets, true}: OTP's NSS lines in upper-case hexadecimal,CLIENT_RANDOMfor TLS 1.2, and for TLS 1.3 the handshake secrets (until the peer updates its keys) and both sides' currentCLIENT_TRAFFIC_SECRET_NandSERVER_TRAFFIC_SECRET_N. With a keylog fun, asking for a secret raises{case_clause, {keep_secrets, Value}}in the caller, as OTP's does.{keylog, Fun}gets#{items => Lines, client_random => Random}from the connection process: the TLS 1.3 handshake secrets together, then each traffic secret and each one after a key update, or the TLS 1.2 line after every handshake. A failing fun is logged as a warning.{keylog_hs, Fun}gets the TLS 1.3 secrets a handshake had when it failed with a fatal alert.
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).
- Client cache: one process, registered as
ssl_manageras OTP's, keys sessions by{{Host, Port}, SessionId},Hostbeing theserver_name_indicationsent, else the host connected to (the peer's address for an upgraded socket). A client that verified the server (verify_peer) saves its new session unlessreuse_sessionsisfalse: withtrue(the default) only when no equivalent session is cached, withsaveat once, before the handshake returns. A client whose versions are not only TLS 1.3 offers a session in its ClientHello: the session of{reuse_session, {Id, Data}}(data libssl cannot read gives a new session, as OTP's does with data it cannot decode), the saved session{reuse_session, Id}names, or withreuse_sessionstruea cached session to the same server whose suite is still inciphersand whose own certificate is still configured. - Server caches: each listening socket starts one, keyed by session id, which ends with
ssl:close/1of the listening socket or with its owner; upgraded server sockets sharessl_unknown_listener's, started underssl_supon first use. A server withreuse_sessionstrue(the default unless it verifies clients) saves its new sessions. A ClientHello offering a session it may resume (not one that also offers TLS 1.3 to a server that takes it) pauses the handshake while the connection looks it up: the session is reused when it has not expired, the server still has its certificate, a server that requires a client certificate has one, the requested server name is the session's, and thereuse_sessionfun (fun(SuggestedId, PeerCert, Compression, CipherSuite)) agrees. Like OTP's, the fun gets the suite's two-byte code, not a suite map. Every server names its sessions, also withreuse_sessionsfalse. - Stores: each cache keeps its sessions in an
ssl_session_cache_apimodule,client_session_cbandserver_session_cbof the application environment (init/1gets[{role, client | server} | Args], Args fromclient_session_cb_init_args,server_session_cb_init_argsorsession_cb_init_args). OTP's defaultssl_client_session_cache_dbandssl_server_session_cache_dbname Clause'sclause_ssl_session_db, an ETS table.session_lifetime(seconds, 24 hours by default) bounds a session's age, also in libssl's check, andsession_cache_client_maxandsession_cache_server_max(1000) the number of entries, the oldest going first. - Information:
connection_information/1,2givesession_id,session_data(the DER encoding, which{reuse_session, {Id, Data}}takes back) andsession_resumption. A resumed connection'speercert/1is the certificate its session kept. - Options:
reuse_sessionsandreuse_sessionare checked as OTP checks them, with its errors; given with versions that are only TLS 1.3 they are{options, incompatible, [Key, {versions, ['tlsv1.3']}]}.
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).
- Clients: with
{session_tickets, auto}each ticket the server sends goes to the storeclause_ssl_ticketsregisters astls_client_ticket_store, as OTP's. A new connection takes the oldest ticket for the same server name whose suite it offers (of the first hash of its suites that has one), and each ticket is used once; tickets expire afterclient_session_ticket_lifetimeseconds (7200) and the store keepsclient_session_ticket_store_size(1000). Since the store is keyed by server name, connections to another server of the same name try its tickets first, as OTP's do. With{session_tickets, manual}the owner gets{ssl, session_ticket, Ticket}for each ticket, and{use_ticket, Tickets}offers one of them. - Servers:
session_ticketsstatelessandstateless_with_certissue libssl's encrypted tickets, keyed bystateless_tickets_seedwhen it is given;statefulandstateful_with_certissue ids of sessions the listening socket's context keeps (server_session_ticket_store_size, 1000) and takes once. A server sendsserver_session_tickets_amounttickets (3) after each handshake, livingserver_session_ticket_lifetimeseconds (7200). TLS 1.2 connections of a ticket server get no tickets. - Resumed connections:
session_resumptionistrue. As OTP's, a resumed client has nopeercert/1({error, no_peercert}), and a server has its client's certificate only with the_with_certmodes. - Options:
session_tickets,use_ticket(tickets for the server the client names),early_data,anti_replayandstateless_tickets_seedare checked in OTP's order with its errors; they need TLS 1.3 among the versions.
Early data
TLS 1.3 early data (0-RTT) as OTP's.
- Clients:
{early_data, Data}goes with the ClientHello of a connection that resumes with a ticket whose limit takes it. After the handshake the owner gets{ssl, S, {early_data, accepted | rejected}}; rejected data is the application's to send again. Data larger than the ticket's limit is not sent and counts as rejected. - Servers:
{early_data, enabled}takes up toserver_session_ticket_max_early_databytes (16384), which its tickets name; the connection delivers the early data before the data that follows the handshake. A stateful server's tickets are used once. A stateless server'santi_replay('10k','100k'or{Window, Hashes, Bits}) records the PSK binders of the ClientHellos that brought early data and refuses early data whose binders it saw in the current or the last window; withoutanti_replayit logs OTP's warning when it listens.
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
- Cipher suites:
cipher_suites/2,3,suite_to_str/1,suite_to_openssl_str/1andstr_to_suite/1are builtins in the core runtime (runtime/src/builtins/ssl/, builtins). The table holds OTP's 129 listed suites with their codes and the 30 lists of each version (tlsv1.3totlsv1,dtlsv1.2,dtlsv1) and description (default,all,exclusive,anonymous,exclusive_anonymous). As in OTP, these lists do not depend on crypto. - Names: the name builtins follow
ssl_cipher_formatclause by clause, so odd maps and names get OTP's results. Like OTP,str_to_suite/1parses a name without checking it against the table: any name whose parts are existing atoms becomes a map, and anything else is{error, {not_recognized, Name}}. - Crypto-dependent lists:
filter_cipher_suites/2,prepend_cipher_suites/2andappend_cipher_suites/2apply the user's filter funs and then crypto's support, part by part.signature_algs/2,eccs/0,1,groups/0,1andversions/0filter OTP's lists bycrypto:supports/1(clause_ssl_algorithms). - Versions:
versions/0reads theprotocol_versionanddtls_protocol_versionapplication variables as OTP does. When a variable is unset, it stores the default versions there. - Errors:
format_error/1returns OTP's texts for alerts, options, files and POSIX reasons.
Clause