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
    }

How a client secures a connection: the name the server's certificate must hold, the authorities it trusts, and the application protocols it offers.

serverName is 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. With NoVerification nothing is checked, though a DNS name is still sent.

alpn is the list of protocols to offer, such as [ "h2", "http/1.1" ], and [] offers none. The server chooses which one is used, and info reports it. Each name must be 1 to 255 bytes of UTF-8, or connect fails with a SocketError whose kind is Socket.InvalidInput.

type Verification
    = SystemCertificates
    | TrustedCertificates String
    | NoVerification

Which 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 -> ClientOptions

Returns options for a server named serverName, trusting the system's certificate authorities (SystemCertificates) and offering no application protocols.

connect : ClientOptions -> ConnectOptions -> Task Error Connection

Opens 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 Listener

Starts 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 String

A 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 Info

Returns 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 Problem

A 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; error is a Socket.Error, whose Socket.errorKind says what went wrong, as for Socket.Tcp. A peer that drops the connection during the handshake is a SocketError with the kind Socket.ConnectionReset.
  • TlsError problem: TLS itself failed; the Problem says 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 String

How TLS itself failed.

  • CertificateRejected problem: connect rejected the server's certificate; the CertificateProblem says why.
  • CertificateUnusable reason: certificates or a key given to this module cannot be used: the certificateChain or privateKey of listen, the two not belonging together, or the certificates of TrustedCertificates for connect. reason is the TLS library's reason in upper case with underscores, such as "NO_CERTIFICATES"; the backend reports it as the code ERR_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. reason is the TLS library's reason as for CertificateUnusable, 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
    | VerificationFailed

Why 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 the serverName connected 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 -> String

Returns 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 Error

Reads 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.