TLS 1.3 and DTLS 1.3 for MoonBit.
// A client: put what the socket gives into `receive`, send what `take` gives.
let config = @tls.Client::new(@roots.mozilla(), alpn=["h2", "http/1.1"])
let conn = @tls.Conn::client(config, "example.com", rand~, now~)
socket.write(conn.take()) // the ClientHello
while !conn.is_connected() {
conn.receive(socket.read()[:])
socket.write(conn.take())
}
conn.alpn() // Some("h2")
conn.write(request[:])
// A server: the same three calls. The key is any PEM file `openssl` writes.
let chain = @x509.pems(cert_pem) // leaf first
let config = @tls.Server::new(chain, @x509.PrivateKey::pem(key_pem), alpn=["h2"])
let server = @tls.Conn::server(config, rand~)test {
let early = @keys.early() // no PSK: the zero secret
let shared = b"\x8b\xd4\x05\x4f\xb5\x5b\x9d\x63\xfd\xfb\xac\xf9\xf0\x4b\x9f\x0d\x35\xe6\xd6\x3f\x53\x75\x63\xef\xd4\x62\x72\x90\x0f\x89\x49\x2d"
let handshake = @keys.handshake(early[:], shared[:]) // salted by the key exchange
let transcript = @keys.Transcript::new()
transcript.add(b"\x01\x00\x00\x00"[:]) // a ClientHello, for the shape of it
let secret = @keys.client_handshake(handshake[:], transcript.hash()[:])
assert_eq(
@base16.encode(secret[:]),
"ab4fc9e2370723165814c074381ab62cbb2c32fd421b15db6910f5df5b1756a3",
)
}test {
let suite = @record.Aes128Gcm
let secret = @base16.decode("ab4fc9e2370723165814c074381ab62cbb2c32fd421b15db6910f5df5b1756a3")
let writer = @record.Cipher::new(secret[:], suite~)
let record = writer.seal(@record.Handshake, b"hello"[:])
assert_eq(@base16.encode(record[:]), "1703030016ea041db0127f9d1812962e71632919c00bfbe245872d")
// The same secret on the other side opens it: the content type moved inside, and came back.
let reader = @record.Cipher::new(secret[:], suite~)
let (kind, content) = reader.open(record[:])
assert_eq(kind, @record.Handshake)
assert_eq(content, b"hello")
}// A DTLS 1.3 client: datagrams in and out, and a clock you supply.
let conn = @dtls.Conn13::client(config, "example.com", rand~, now~, wall~)
socket.send(conn.take())
while !conn.is_connected() {
conn.receive(socket.recv()[:], now~=monotonic_ms())
socket.send(conn.take())
if conn.deadline() is Some(due) && monotonic_ms() >= due {
conn.tick(now~=monotonic_ms())
}
}| Specification | State | |
|---|---|---|
| wire | §3 integer widths, the frozen legacy_version, Reader, the cursor every decoder reads through, and Refused, what a decoder raises when the octets are not the structure their own lengths describe | 0.3.0 |
| alert | §6 the alert protocol | 0.2.0 |
| keys | §7.1 key schedule, §4.4.1 transcript hash, §4.4.4 Finished | 0.1.0 |
| record | §5 record layer under any of the three suites, §7.3 traffic keys; TLS 1.2's suite table and record protection | 0.3.0 |
| ext | §4.2 extensions: server_name, supported_versions, supported_groups, signature_algorithms, key_share, ALPN, cookie, use_srtp | 0.4.0 |
| msg | §4 the handshake messages: ClientHello, ServerHello, EncryptedExtensions, Certificate, CertificateVerify, Finished | 0.4.0 |
| hs | Appendix A the state machine, §4.1.1 negotiation, §4.1.4 HelloRetryRequest, and the driver | 0.5.0 |
| dtls | RFC 9147 what datagrams change — both record shapes, record number encryption, the replay window, fragmentation and reassembly, the ACK message and the flight driver — and Conn13 and Conn12, the two connections over it, with RFC 6347's DTLS 1.2 record layer beside 1.3's | 0.3.0 |
| tls | The connection: client and server, X25519MLKEM768, X25519, P-256, P-384, P-521 and X448, and SecP256r1MLKEM768, SecP384r1MLKEM1024, ML-KEM alone and the RFC 7919 groups on request, the three suites, ECDSA, RSA-PSS and Ed25519 certificates, HelloRetryRequest, SNI, ALPN, KeyUpdate, close_notify, middlebox compatibility mode, declined 0-RTT data read past; TLS 1.2 beside 1.3 and the session resumption, client certificates, renegotiation and stapling that come with them; the server's chain judged by mooncred | 0.1.0 |
| srtp | RFC 5764 the SRTP protection profiles a DTLS handshake negotiates, and the keying material it exports for them | 0.1.0 |
@keys.early() // SHA-256, the suite RFC 8446 §9.1 makes mandatory
@keys.early(digest=Sha384) // 48 octets instead of 32, all the way down| Case | What happens |
|---|---|
| TLS 1.0 and 1.1, this library as the client | bad_record_mac on the first protected record, both against OpenSSL and between two instances of this library. The handshake itself completes; the two ends disagree about the record's MAC. The server side works. |
| 0-RTT, this library as the client | The early data is accepted — OpenSSL echoes it — while is_resumed() and early_accepted() both say no. |
| record_size_limit, this library as the client | A 512-octet limit is advertised and OpenSSL's ServerHello runs past it, which this library then refuses with record_overflow. Its own comment says a ServerHello carrying a post-quantum share may exceed the limit; the check does not allow it. |
| TLS 1.2 renegotiation, server side | The server's HelloRequest is answered and the second handshake starts (it used to be refused outright: the ClientHello carried both verify_data where RFC 5746 §3.4 allows only the client's), but OpenSSL rejects the first record sealed under the new keys — bad record mac, or a provider error with ChaCha20. The two ends of this library agree with each other, which is why nothing noticed. A client cannot start one at all: Conn::renegotiate is the server's HelloRequest. |
| DTLS 1.2, both ways | The handshake does not complete with an external peer. This library's server sends no flight of its own after the client's key exchange, so openssl s_client retransmits and pion's client waits; and this library's client's second ClientHello — the one carrying the HelloVerifyRequest's cookie — is refused by pion with illegal_parameter. OpenSSL's s_server cannot serve application data over DTLS at all (-rev and -www are both rejected), so an earlier version of this table blamed OpenSSL for a case in which its server had never started. |
| Scripts | What fails |
|---|---|
| certificate-request, certificate-verify, post-handshake-auth, ecdsa-in-certificate-verify, eddsa-in-certificate-verify, mldsa-in-certificate-verify, client-certificate-compression | These ask the server for a client certificate. The peer starts without ask=, so no CertificateRequest is sent and the script's own client certificate never arrives. The matrix covers both sides of client authentication against OpenSSL and Go; this is the peer's default, not the library's. |
| session-resumption, psk_dhe_ke, psk_ke, certificate-compression, 0rtt-garbage | A resumed handshake finishes, and these scripts then expect the server to accept a second one under the same ticket, or to send the ticket a resumption needs at the point they expect it. psk_ke is the mode this library does not speak at all: every ticket it issues names psk_dhe_ke. |
| ecdhe-brainpool-curves, obsolete-curves, no-unknown-groups | Group negotiation: the peer's certificate cannot sign for a brainpool curve, and the alerts for an obsolete or unknown group are not the ones the scripts expect. |
| version-negotiation, non-support, mlkem | 193 of version-negotiation's 202 cases are TLS 1.2 and below, which this server does not offer by default; mlkem needs kyber-py, which the run does not install. |
| rsapss-signatures | This library refuses an RSASSA-PSS certificate at configuration, as Go and rustls do. |
| keyupdate-from-server | The peer's server never sends a KeyUpdate of its own: no option asks it to. |
| dhe-shared-secret-padding, lengths, serverhello-random | These do not finish inside the time the runner gives them. |
Install
Download zipTLS 1.3 and DTLS 1.3 for MoonBit.