Socket.Tls
TLS (Transport Layer Security) encrypts a TCP connection and lets a client check that it is
talking to the server it meant, and this module makes TLS connections and listeners. Once made,
a TLS connection is an ordinary Socket.Connection whose streams carry the
plaintext. The encryption happens underneath, and the program uses the connection through
Socket and its streams as it would any other.
A TLS connection starts with a handshake, in which the two ends agree on a TLS version and a
cipher, and the client, unless told not to, verifies the server's certificate. Verification
checks that the certificate was signed by an authority the client trusts and that it names the
server the client meant. Verification says which authorities are trusted, and the
serverName of ClientOptions says which name the certificate must hold.
Two further things travel in the handshake. SNI (Server Name Indication) is the client telling
the server the host name it wants, so that a server with several certificates can choose one.
ALPN (Application-Layer Protocol Negotiation) is the client offering a list of application
protocols, such as "h2" and "http/1.1", from which the server chooses one.
connect takes an address, not a host name, so resolving the name is the caller's
job, for example with Socket.lookup. The server name is used only for TLS,
never to find the server. For example:
Socket.lookup "example.com"
|> Task.andThen
(\addresses ->
case addresses of
address :: _ ->
Socket.Tls.connect
(Socket.Tls.defaultClientOptions "example.com")
(Socket.Tcp.defaultConnectOptions address 443)
[] ->
Task.fail ...
)A server listens and gets its connections from Socket.accept or
Socket.onConnection, by the rule the Socket module describes. A
connection reaches that rule only once its handshake has succeeded, so a client whose handshake
is slow can be overtaken by clients that connected after it.
TLS is a protocol layered on a TCP connection, so this module has its own
Error, which says in which layer a failure happened. A failure of the TCP connection
underneath is a SocketError holding a Socket.Error, as Socket.Tcp would
report it; a failure of TLS itself is a TlsError holding a Problem. A certificate
that fails verification makes connect fail with
TlsError (CertificateRejected problem), where the
CertificateProblem says what was wrong with it. A server that closes the
connection during the handshake is a failure of the connection, not of TLS: a SocketError
whose kind is Socket.ConnectionReset.
Once a connection has been made, failures to read or write arrive on its streams, as for any
connection, and fromStreamFailure reads them back as an Error.
Clients
type alias ClientOptions =
{ serverName : String
, verification : Verification
, alpn : List String
}type Verification
= SystemCertificates
| TrustedCertificates String
| NoVerificationWhich authorities a client trusts to vouch for a server's certificate.
SystemCertificates trusts the operating system's certificate authorities. On the native
backend a non-empty SSL_CERT_FILE environment variable replaces them with the certificates in
the file it names, and a non-empty SSL_CERT_DIR replaces them with a directory of certificates
under OpenSSL's hashed file names (as openssl rehash makes them), together with the file when
both are set. On the JavaScript backend a non-empty SSL_CERT_FILE likewise replaces them with
the certificates in the file it names, and a non-empty SSL_CERT_DIR set as well adds back the
authorities Node reports as the system's. The variables and the system's authorities are read
when a connection first needs them, not for every connection.
TrustedCertificates pem trusts only the PEM-encoded certificates in pem, which may hold one
or more; the system's authorities are not consulted. On the native backend, reading stops at the
first certificate block that cannot be read, and the certificates after it are not trusted. When
no certificate can be read from pem, connect fails with
TlsError (CertificateUnusable reason) before it contacts the server.
NoVerification accepts any certificate and checks no name. The connection is encrypted, but
the server is not authenticated, so anyone able to intercept the connection can pose as it. It
is meant for testing.
defaultClientOptions : String -> ClientOptionsReturns options for a server named serverName, trusting the system's certificate
authorities (SystemCertificates) and offering no application protocols.
connect : ClientOptions -> ConnectOptions -> Task Error ConnectionOpens a TCP connection and secures it with TLS, succeeding once the handshake has finished and, unless verification is off, the server's certificate has been accepted.
The TCP side is made from the same options as by Socket.Tcp.connect, and fails in the same
ways, as a SocketError. Its timeout covers the handshake too: the task fails with a
SocketError whose kind is Socket.TimedOut when connecting and the
handshake together take longer. The alpn names, and the certificates of TrustedCertificates,
are checked before anything connects, so a problem with them fails the task without contacting
the server.
A failure of TLS itself is a TlsError: CertificateRejected when the server's certificate is
rejected, and HandshakeFailed when the handshake fails for another reason, for example when the
two ends share no TLS version or cipher. A server that closes the connection during the handshake
is a SocketError with the kind Socket.ConnectionReset.
Servers
type alias ServerOptions =
{ certificateChain : String
, privateKey : String
, alpn : List String
}What a TLS listener presents to its clients: its certificate and private key, and the application protocols it supports.
certificateChain is PEM text holding the server's own certificate first, followed by any
intermediate certificates that link it to an authority. privateKey is the PEM-encoded key that
belongs to that certificate.
alpn is the list of protocols the server supports, in its order of preference. Of the
protocols a client offers, the server chooses the first in its own list, and when the client
offers none of them the handshake fails. A client that offers no protocols is served without
one, and [] ignores what clients offer. Http.Server does not use this field; it chooses its
protocols itself.
listen : ServerOptions -> ListenOptions -> Task Error ListenerStarts listening for TLS connections, succeeding with the listener once it is bound and
listening. The TCP side is set up from the same options as by Socket.Tcp.listen, and fails in
the same ways, as a SocketError.
The certificate, the key, and whether the two belong together are checked before anything
listens. When they cannot be used, the task fails with TlsError (CertificateUnusable reason).
An alpn name that is empty or longer than 255 bytes fails it with a SocketError whose kind
is Socket.InvalidInput.
A client whose handshake fails, or has not finished after 120 seconds, is closed without being handed to the program. On the native backend at most 64 handshakes are under way at once on one listener; further clients wait in the operating system's listen queue.
Connection information
type alias Info =
{ version : TlsVersion, alpn : Maybe String, cipher : String }What a connection's TLS handshake agreed on.
version is the TLS version, a TlsVersion. alpn is the application
protocol agreed, such as Just "h2", and Nothing when none was agreed. cipher is the cipher
suite, in OpenSSL's name, such as "TLS_AES_256_GCM_SHA384".
type TlsVersion
= Tls1_0
| Tls1_1
| Tls1_2
| Tls1_3
| UnknownTlsVersion StringA TLS version, as the handshake agreed on it.
The backends report the version by its OpenSSL name, and the names map to the cases as follows:
Tls1_0:"TLSv1";Tls1_1:"TLSv1.1";Tls1_2:"TLSv1.2";Tls1_3:"TLSv1.3";UnknownTlsVersion name: any other name, as it was reported.
info : Connection -> Task Error InfoReturns what the handshake of a TLS connection agreed on.
Fails with a SocketError whose kind is Socket.InvalidInput for a
connection that is not a TLS connection. Once the connection has ended, it may fail in the same
way.
Errors
type Error
= SocketError Error
| TlsError ProblemA failed TLS operation, by the layer it failed in.
SocketError error: the TCP connection under TLS failed, or an argument of the TCP side could not be used;erroris aSocket.Error, whoseSocket.errorKindsays what went wrong, as forSocket.Tcp. A peer that drops the connection during the handshake is aSocketErrorwith the kindSocket.ConnectionReset.TlsError problem: TLS itself failed; theProblemsays how.
Only failures produced by TLS itself, such as a certificate check or an error that the TLS
library reports, are a TlsError.
type Problem
= CertificateRejected CertificateProblem
| CertificateUnusable String
| HandshakeFailed StringHow TLS itself failed.
CertificateRejected problem:connectrejected the server's certificate; theCertificateProblemsays why.CertificateUnusable reason: certificates or a key given to this module cannot be used: thecertificateChainorprivateKeyoflisten, the two not belonging together, or the certificates ofTrustedCertificatesforconnect.reasonis the TLS library's reason in upper case with underscores, such as"NO_CERTIFICATES"; the backend reports it as the codeERR_SSL_<reason>.HandshakeFailed reason: the handshake, or the TLS layer of an open connection, failed for another reason, such as the two ends sharing no TLS version or cipher.reasonis the TLS library's reason as forCertificateUnusable, or"UNKNOWN"when the library gave none.
The reasons stay text because the TLS library has hundreds of them.
type CertificateProblem
= Expired
| NotYetValid
| Revoked
| SelfSigned
| SelfSignedInChain
| IssuerNotTrusted
| LeafNotVerifiable
| NameMismatch
| VerificationFailedWhy a client rejected a server's certificate. The code each case stands for, as the backends report it, is in parentheses.
Expired: the certificate has expired (CERT_HAS_EXPIRED).NotYetValid: the certificate is not valid yet (CERT_NOT_YET_VALID).Revoked: the certificate was revoked (CERT_REVOKED).SelfSigned: the certificate is self-signed and not trusted (DEPTH_ZERO_SELF_SIGNED_CERT).SelfSignedInChain: the chain ends in a self-signed certificate that is not trusted (SELF_SIGNED_CERT_IN_CHAIN).IssuerNotTrusted: the certificate of the authority that signed it is not available or not trusted (UNABLE_TO_GET_ISSUER_CERT_LOCALLY).LeafNotVerifiable: the server's own certificate cannot be verified, usually because the server did not send the intermediate certificates (UNABLE_TO_VERIFY_LEAF_SIGNATURE).NameMismatch: the certificate does not name theserverNameconnected to, as a DNS name or an IP address (ERR_TLS_CERT_ALTNAME_INVALID).VerificationFailed: verification failed for another reason (CERT_VERIFY_FAILED on the native backend; on the JavaScript backend, any other verification code Node reports).
errorToString : Error -> StringReturns a description of the error for people to read. For a SocketError it is that of
Socket.errorToString. For a TlsError it is the code the backend
reports for the problem, ": ", and a description of it, as in
"CERT_HAS_EXPIRED: the certificate has expired" or
"ERR_SSL_NO_CERTIFICATES: the certificates cannot be used".
fromStreamFailure : Failure -> Maybe ErrorReads the failure of a TLS connection's stream back as an Error, or returns
Nothing for a failure that some other kind of stream made.
A read or write on the streams of a TLS connection that fails because of the connection fails
with Stream.Failed. This gives a TlsError when TLS itself failed, for example with
HandshakeFailed when the peer sent data TLS cannot accept, and a SocketError when the
connection under TLS failed, with the same Socket.Error that
Socket.fromStreamFailure gives. One call covers both, so a
program reading a TLS connection needs only this one.
How a client secures a connection: the name the server's certificate must hold, the authorities it trusts, and the application protocols it offers.
serverNameis used only for TLS, never to find the server. A DNS name is sent as SNI and must match the certificate. An IP address literal, such as"192.0.2.1", is not sent and must match one of the certificate's IP addresses. On the native backend an IPv6 literal with a zone, such as"fe80::1%eth0", counts as a DNS name. The empty string sends no name, and the certificate is then checked against the trusted authorities but not against any name. WithNoVerificationnothing is checked, though a DNS name is still sent.alpnis the list of protocols to offer, such as[ "h2", "http/1.1" ], and[]offers none. The server chooses which one is used, andinforeports it. Each name must be 1 to 255 bytes of UTF-8, orconnectfails with aSocketErrorwhose kind isSocket.InvalidInput.