cheatah
Module

tls

A minimal, dependency-free TLS 1.3 implementation (RFC 8446) — both a client and a server handshake — built entirely on cheatah's own crypto modules: x25519 key exchange, the aead ChaCha20-Poly1305 (and AES-GCM) record cipher, hashlib's HKDF key schedule, and ed25519 signatures. No OpenSSL. The handshake is validated against OpenSSL as the peer in both directions (our client vs openssl s_server, our server vs openssl s_client).

Server authentication (MITM-resistant by default)

The client authenticates the server — it is not just encryption. After the handshake proves the peer holds the private key for the certificate it presents (CertificateVerify + Finished MAC), the presented X.509 chain is validated (from-scratch, x509.hpp):

  1. Chain of trust — each certificate is signed by the next, up to a certificate in the system CA trust store (/etc/ssl/certs/…, $SSL_CERT_FILE, or a caller-supplied bundle), with intermediates required to carry basicConstraints: CA. A self-signed or unknown-CA certificate is refused.

  2. Hostname — the requested host must match the leaf's subjectAltName dNSNames (RFC 6125, with single left-label wildcards). A valid certificate for the wrong host is refused.

  3. Validity — every certificate's notBefore … notAfter window must contain the current time. Expired / not-yet-valid certificates are refused.

Chain signatures are verified for RSA PKCS#1 v1.5 (SHA-256 and SHA-384), ECDSA with SHA-256 or SHA-384 under P-256 or P-384 issuer keys (the hash comes from the signature OID, the curve from the issuer's key — real CA chains mix them), and Ed25519. Algorithms cheatah does not implement yet (e.g. SHA-512, rsassa-PSS chain signatures) fail closed — the connection is refused, never accepted unverified.

Opting out (pinned / controlled peer). For a server whose identity you establish out of band, pass insecure = true to skip validation (leaf-key possession only), or ca_file to trust a specific PEM bundle (e.g. a private CA or a pinned self-signed cert):

with tls.open(sock.fd(), "example.com") as conn { … }                 # validate (default)
with tls.open(sock.fd(), "10.0.0.5", true) as conn { … }              # insecure: skip validation
with tls.open(sock.fd(), "internal.host", false, "/etc/my-ca.pem") { … }  # trust a private CA

requests and websocket ride this and inherit it: requests.get("https://…") and websocket.open_url("wss://…") validate the certificate by default (with the same insecure / ca_file options).

import socket
import tls

# tls rides an already-connected TCP socket. Both are owning guards, so nothing leaks.
with socket.open("example.com", 443) as sock {
    with tls.open(sock.fd(), "example.com") as conn {
        conn.send("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
        io.print(conn.recv(65536))
    }
}

API (cheatah-facing)

  • tls.open(fd, server_name) -> Conn — run the client handshake over a connected fd, authenticating the server; returns an owning Conn. On failure conn.is_open() is false (see tls.last_error()).

  • tls.accept(fd, cert_pem, key_pem) -> Conn — run the server handshake over an accepted fd, presenting an Ed25519 or ECDSA P-256 leaf and signing with its private key_pem (PKCS#8 Ed25519, or PKCS#8/SEC1 P-256 EC); returns an owning Conn. cert_pem may be a full chain (fullchain.pem — leaf first, then intermediates) and every block is sent, so a Let's Encrypt certificate works as issued and browsers get a path to their trust anchor. This is the "HTTPS with zero non-cheatah software" path — pair it with a socket accept loop.

  • Conn methods: send(data), recv(bufsize), shutdown(), close(), is_open(), id(). The Conn sends close_notify and erases its session automatically at scope exit — held as a plain let or in a with, it cannot leak.

  • tls.last_error() — the last error message on this thread.

The underlying TCP fd is not owned by the TLS session — guard it with a socket.Conn (as above) or close it yourself.

Note for C++ callers

The flat, handle-based API (client_connect/send/recv/close, keyed by an integer session id) is C++-only and lives in tls_lowlevel.hpp. It is intentionally not reachable from cheatah, so cheatah code cannot leak a session — it uses the tls.Conn guard instead.

Scope (v1): TLS 1.3 only, cipher suites TLS_CHACHA20_POLY1305_SHA256 and TLS_AES_128_GCM_SHA256, X25519 key share, SNI, and X.509 chain + hostname + expiry validation (RSA-PKCS1 SHA-256/384, ECDSA SHA-256/384 under P-256/P-384 keys, and Ed25519 chain signatures). The server side presents an Ed25519 or ECDSA P-256 leaf (P-256 is what public CAs issue, so a CA-trusted HTTPS server needs no other software; full-chain PEMs are sent whole), signs per RFC 8446 §4.4.3 only with an algorithm the client offered, and refuses a cert/key mismatch at startup with a precise error; the client picks the record cipher. Not yet: RSA or P-384 server certificates, SHA-512 chain signatures (refused, not accepted), certificate revocation (OCSP/CRL), and client certificates.

Classes

Functions

fn std::string expand_label_impl(std::string_view secret, std::string_view label, std::string_view context, unsigned length, bool sha384=false) source#

HKDF-Expand-Label(secret, label, context, length) with the "tls13 " prefix (RFC 8446 §7.1).

Parameters
secret

the HKDF secret.

label

the schedule label (without the "tls13 " prefix, which is added here).

context

the hash context bytes.

length

the output length in bytes.

sha384

selects the SHA-384 HKDF (for the TLS_AES_256_GCM_SHA384 key schedule); default is the SHA-256 schedule.

Returns

the expanded key material, length bytes.

Complexity

O(length) — HKDF-Expand emits ceil(length/hash) HMAC blocks.

Allocation

the returned key material plus the HkdfLabel info string.

fn std::string derive_secret_impl(std::string_view secret, std::string_view label, std::string_view transcript, bool sha384=false) source#

Derive-Secret(secret, label, transcript) = Expand-Label(secret, label, Hash(transcript), HashLen), where Hash is the negotiated suite's hash (SHA-256, or SHA-384 when sha384).

Parameters
secret

the HKDF secret.

label

the schedule label.

transcript

the handshake transcript to hash into the context.

sha384

selects the SHA-384 schedule; default is SHA-256.

Returns

the derived secret (32 or 48 bytes).

Complexity

O(|transcript|) — one transcript hash, then a fixed-size expand.

Allocation

the transcript-hash string and the returned secret.

fn std::string last_error() source#

The most recent tls error message on this thread ("" when none).

Returns

the last error text set on the calling thread, or "" if none.

Complexity

O(1).

Allocation

the returned string.

Concurrency

the error slot is thread-local — read it on the thread whose call failed.

fn Conn open(long long fd, const std::string &server_name, bool insecure=false, const std::string &ca_file="") source#

Run the TLS 1.3 client handshake over connected fd fd and return an owning Conn (the RAII, with-friendly form of client_connect()).

By default the server is AUTHENTICATED: the certificate chain is built to a trusted CA, the hostname is matched against the leaf's subjectAltName, and the validity dates are checked — so the connection resists an active MITM.

Parameters
fd

a CONNECTED TCP socket (e.g. socket::Conn::fd()).

server_name

the hostname (SNI + certificate SAN match).

insecure

when true, skip chain/hostname/expiry validation (leaf-key possession only) — for a pinned/controlled peer where identity is established out of band. Default false.

ca_file

a PEM CA bundle to trust instead of the system store (empty = system default).

Returns

an owning Conn; on handshake or validation failure its is_open() is false (see last_error()).

Warning

insecure = true drops the MITM protection: ANY peer that holds its own certificate's key is accepted, whoever it is. Use it only when the peer's identity is pinned/established out of band.

Complexity

one network round trip + O(handshake bytes) crypto (+ a one-time parse of the system CA store; a custom ca_file is parsed on every call).

Allocation

the session state (plus transient handshake buffers).

Concurrency

blocks for the handshake round trip — bound it with socket.set_timeout() on fd.

fn Conn accept(long long fd, const std::string &cert_pem, const std::string &key_pem) source#

Run the TLS 1.3 SERVER handshake over an accepted TCP fd and return an owning Conn — the with-friendly server counterpart to open().

We present cert_pem and prove possession of its key by signing the handshake, so a client that validates the certificate gets an authenticated, encrypted channel with no OpenSSL anywhere.

The server certificate is Ed25519 or ECDSA P-256 — the second is what public CAs (Let's Encrypt) actually issue, so a browser-facing HTTPS server works with an ordinary fullchain.pem/privkey.pem pair. cert_pem may carry the WHOLE chain (leaf first, then intermediates); every block is sent, giving clients a path to their trust anchor. The private key's derived public half must match the leaf's SPKI, or the handshake refuses at startup with a precise error rather than failing opaquely at the first client. Both suites (ChaCha20-Poly1305 and AES-128-GCM) and X25519 key exchange are supported; the client picks the suite, and per RFC 8446 §4.4.3 we refuse a client whose signature_algorithms do not include our certificate's algorithm.

Parameters
fd

a CONNECTED TCP socket from socket::accept()/Listener (e.g. one client of a server loop).

cert_pem

the server certificate PEM — a single leaf or a full chain, leaf first.

key_pem

the leaf's private key, PEM: PKCS#8 Ed25519, or PKCS#8/SEC1 P-256 EC.

Returns

an owning Conn; on handshake failure its is_open() is false (see last_error()).

Complexity

one network round trip + O(handshake bytes) crypto.

Allocation

the session state (plus transient handshake buffers).

Concurrency

blocks awaiting the client's handshake flights — bound it with socket.set_timeout() on fd so a silent client cannot hang the server.

fn std::string to_hex(const std::uint8_t *data, std::size_t n) source#

Lowercase-hex ENCODE overload for a raw byte buffer (the form the digest and Ed25519 paths use).

Parameters
data

pointer to the bytes.

n

the number of bytes.

Returns

the lowercase hex text (length 2*n).

Complexity

O(n).

Allocation

the returned string.

fn std::string from_hex(std::string_view hex) source#

Hex DECODE — the inverse of to_hex.

Accepts an even-length hex string in either digit case.

Parameters
hex

the hex text (both a-f and A-F accepted; no 0x prefix).

Returns

the decoded raw bytes (may contain embedded NULs).

Parameters
std::invalid_argument

on an odd length or a non-hex character.

Complexity

O(|hex|).

Allocation

the returned bytes.