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.2 renegotiation, server side | The server's HelloRequest is answered and the second handshake starts, 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. |
| 0-RTT, this library as the client | The early traffic secret is not the problem and neither is the record: against s_server -early_data -max_early_data 4096, OpenSSL's own CLIENT_EARLY_TRAFFIC_SECRET for the connection is the same 32 octets this client derives, and the early record decrypts under it with AAD = 17 03 03 || length in an independent check. What OpenSSL does is decline the ticket before it gets that far: with -max_early_data it issues its anti-replay tickets, which are a 32-octet session id kept on the server, and this client's resumption is refused under those whether or not it carries early data — a full handshake, resumed=0, and no early secret derived at all. The same client resumes the 192-octet stateless ticket that a server without -max_early_data hands out. The other direction is in the matrix: s_client -sess_in -early_data against this server answers Reused and Early data was accepted. |
| 0-RTT, this library as the server | Covered: the server: 0-RTT accepted case, OpenSSL's client against this server, s_client -sess_in … -early_data. The client's own half of the same handshake is refused by OpenSSL's anti-replay tickets, above. |
| # | Server | Scripts it is for |
|---|---|---|
| 0 | an RSA certificate | the general one |
| 1 | an ECDSA P-256 certificate | minerva, ecdsa-support |
| 2 | an Ed25519 certificate | eddsa |
| 3 | an RSA certificate that asks for a client certificate, under a root this run issues | the eight that answer a CertificateRequest, handed -c/-k and -s |
| 4 | every TLS version and every TLS 1.2 suite | symetric-ciphers, non-support, version-negotiation |
| 5 | a server that sends the first KeyUpdate, answering the peer's first request | keyupdate-from-server |
| 6 | a brainpoolP256r1 certificate | ecdhe-brainpool-curves, obsolete-curves |
| 7 | an RSASSA-PSS certificate | rsapss-signatures |
| 8 | a server that asks for a client certificate once the connection is open | post-handshake-auth |
| Scripts | What fails |
|---|---|
| psk_dhe_ke, psk_ke, session-resumption | The scripts present a key from a file. This library's PSK comes from a ticket it issued, so its ServerHello names no pre_shared_key and the script calls that an answer it is owed. A psk_ke-only hello also carries no key_share, which §9.2 allows only for that mode and this server requires. |
| non-support, symetric-ciphers | TLS 1.2 suites: 53 conversations end with the peer closing on the script and 31 with handshake_failure, out of 53 and 102. |
| ecdsa-in-certificate-verify, eddsa-in-certificate-verify | Five and six probes of 132 expect illegal_parameter for a forged CertificateVerify and this server answers decrypt_error. |
| connection-abort | Ten probes of 150 read ApplicationData(len=64) where the script has stopped reading: the peer echoes the request it was given. |
| mldsa-in-certificate-verify, client-certificate-compression, certificate-compression | The conversations that need a compressed certificate or an ML-DSA leaf to be judged: 14, and all of the compression scripts' cases past the hello. |
| certificate-request | One probe of five: the server names its anchors in the CertificateRequest, which makes it send certificate_authorities, and the probe expects signature_algorithms alone. |
| no-unknown-groups | Four probes of 259 expect handshake_failure for a hello whose groups are all unknown, and this server sends a HelloRetryRequest instead. |
| 0rtt-garbage, keyupdate-from-server | One probe of eleven, and one of three. |
| mlkem | The script builds its own hybrid hello with kyber-py: 127 of its 402 probes pass. |
| ecdsa-support, ecdsa-brainpool-in-certificate-verify | Unrouted for brainpool, as above; ecdsa-support also wants a server whose certificate is RSASSA-PSS. |
| lengths, dhe-shared-secret-padding | The script's own conversation runs past the five minutes it is given. |
Install
Download zipTLS 1.3 and DTLS 1.3 for MoonBit.