TLS 1.3 client
rusty-tls is a TLS 1.3 client: it runs the handshake, validates the server's certificate chain, and hands back an encrypted stream, standing in for rustls behind fetch and https. One cipher suite (AES-128-GCM-SHA256 over P-256) completes a handshake, and it enforces CA constraints, hostname matching, and downgrade defense.
Every fetch('https://...') and https.get(...) reaches, far below the calling code, a TLS library that opens a socket, negotiates keys, checks a certificate, and hands back a stream. On most runtimes that layer is a binding to OpenSSL or a wrapper over the OS trust store, carrying a decade of interop testing and CA-ecosystem trust. Cruft ships its own instead: a TLS 1.3 client in the tls crate (rusty-tls, the Cruft stand-in for rustls). This page covers exactly what it checks and what it does not, because that determines what you can carry over it.
Alpha. This is an independent TLS 1.3 client in Cruft 0.0.10, externally unaudited, with minimal interop testing against real-world servers. Treat its security properties as young unless this page points to a concrete test. Do not rely on it in production or on adversarial networks.
Scope: a TLS 1.3 client
The crate is a TLS 1.3 client and nothing wider. tls_connect runs a client-side 1.3 state machine through complete_handshake: ClientHello, ServerHello, EncryptedExtensions, Certificate, CertificateVerify, Finished, then application data.
There is server-side scaffolding in the crate, but the client that dials out is the part meant for use.
One cipher suite and one group actually run
A rustls feature table lists many suites and groups. In this crate the presence of a constant does not mean the code path works. Exactly one cipher suite and one key-exchange group complete a handshake: AES-128-GCM-SHA256 (0x1301) over P-256. AES-256-GCM, ChaCha20-Poly1305, X25519, and P-384 exist as constants but are dead; the 1.3 path hard-rejects any negotiated suite that is not 0x1301. The result is one modern, sound suite wired end to end, plus a set of names that look like options but do nothing.
Certificate validation
Certificate checking is where a TLS library either earns its keep or opens a MITM hole. A chain walk that verifies signatures but skips the CA-constraint checks is incomplete, even though it looks like full validation. Here is what the crate checks today:
- Signature chain to a self-signed trust anchor:
chain_walkmatches issuer to subject and verifies each signature via thex509crate. - Validity dates and hostname / SAN matching, including correct wildcard and IP-SAN handling, in
validate_server_certificate. Name and date checks are unit-tested. - CA constraints. Every issuer candidate must prove typed X.509 BasicConstraints
cA:TRUE, must not exceed itspathLenConstraint, must carry KeyUsagekeyCertSignwhen KeyUsage is present, and must satisfy supported DNS/IP nameConstraints against the child's names. The leaf's Extended Key Usage must include TLSserverAuth.
That last check is the one that most often goes wrong in TLS stacks: the "any leaf can act as an issuer" class (the basicConstraints hole, the CVE-2002-0862 lineage). Cruft enforces it. chain_walk consumes the typed extension data from x509 before trusting any issuer, and the leaf/expiry/hostname checks are not skipped. Default fixtures cover the positive root -> CA -> leaf path plus negatives: non-CA issuer, missing keyCertSign, pathLen exceeded, name-constraint violation, and clientAuth-only EKU. The root cause of the class lived one crate down, in x509 not decoding those extensions at all; that is now surfaced as typed accessors (see x509).
One caller-controlled escape hatch exists, mirroring Node's rejectUnauthorized:false: TlsClientConfig::from_reject_unauthorized(false) / insecure_skip_certificate_validation disables the trust-anchor chain walk entirely for self-signed or dev certificates. It is opt-in and the default is secure (validation on), but when set it turns off the chain checks above, so a caller that flips it accepts any certificate.
Resistance to a hostile handshake
Handling a cooperating peer is one thing; surviving a peer trying to weaken or crash the client is another. The crate defends the cases an active network attacker would reach for:
- Downgrade defense. A network attacker can strip 1.3 support and try to shove the client onto TLS 1.2. RFC 8446 section 4.1.3 plants a sentinel in
ServerHello.random(DOWNGRD\x01/DOWNGRD\x00) so a 1.3-capable client can detect this. The crate classifies that sentinel and refuses the 1.2 fork when it appears. - Constant-time MAC compare. Finished-MAC and TLS 1.2 MAC comparisons route through
finished_verify_data_equal, backed by web-crypto'stiming_safe_equal, not a short-circuiting!=. - No panic on malformed input. A hostile peer cannot panic the driver via
unwrap()on peer-controlledOptions (a remote DoS). Those sites returnTlsErrordiagnostics, and the chain cache and ticket store recover poisoned mutexes instead of panicking.
The randomness underneath is real: client_random, session id, the ephemeral ECDH scalar, and server_random all draw from the platform CSPRNG via web-crypto's get_random_values (/dev/urandom, Windows BCryptGenRandom), and the AEAD nonce is the static-IV-XOR-seq construction of RFC 8446 section 5.3 with per-direction counters and no reuse under monotonic sequence. One minor note: the ephemeral P-256 scalar is bit-clear approximated rather than rejection-sampled, a bias too small to call an operative weakness but worth knowing.
Delegated crypto, in-crate protocol
tls implements protocol logic and validation policy. It does not implement crypto. Hashes, HMAC, HKDF, AES-GCM, ECDSA/RSA verify, P-256 scalar mul, and the CSPRNG are delegated to web-crypto; certificate parsing and verify_signature to x509; DER decoding to asn1-der. So the independent-crypto risk is concentrated in web-crypto, not here. This crate's risk is protocol-state and policy correctness.
Session resumption and remaining gaps
Session resumption via stored tickets is implemented: the client offers a held ticket for a host and can complete an abbreviated/resumed handshake, backed by an RFC 5077 ticket store shared across consumers. Still absent, by design: no 0-RTT, no PSK-only authentication, no client authentication (a CertificateRequest is fatal), no KeyUpdate (a received KeyUpdate is fatal, not processed), no OCSP/CRL revocation checking. EncryptedExtensions contents, including any negotiated ALPN, are parsed and then ignored; ALPN is plumbed but off unless the caller opts in. SNI is sent.
How this compares to rustls
rustls is years of production traffic, external audits, a broad suite matrix, resumption, revocation hooks, and a fuzzing and interop history against OpenSSL and BoringSSL. tls is a single suite, a single group, client-only, with ticket-based resumption but no 0-RTT, and a young test surface. rustls leads on maturity, coverage, and adversarial hardening by a wide margin. Where tls is defensible is scope: it does one modern handshake, and the security-policy checks a stack most often botches (chain constraints, downgrade, constant-time compare, non-panic on malformed input) are present and tested.
Limitations
The test surface is thin. Most modules carry no inline unit tests; the tested evidence lives in a dedicated verifier suite. The one live-socket OpenSSL interop test is ignored for cost. A deterministic in-memory test drives the real client complete_handshake over a server-generated flight with a synthetic parseable EC leaf whose SPKI matches the server signing key, checks CertificateVerify and Finished through the transcript, checks the client's Finished against the server side, and checks that both application traffic keys match. That evidence is in-memory and deterministic: it is not a live socket handshake against OpenSSL or rustls. So "completes a TLS 1.3 handshake" is covered against Cruft's own deterministic harness; interoperability with the real internet is not yet covered and stays off by default.
The whole thing is externally unaudited alpha. The constant-time Finished compare is in place, but the underlying web-crypto asymmetric primitives are documented as not fully side-channel hardened. Do not put this in front of adversarial input you cannot afford to lose.