cargo / rustls / audit
cargo : rustls @ 0.23.40
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

build-exec-deterministicbuild-exec-minimalbuild-exec-no-networkbuild-exec-no-write-outbuild-exec-safeconcurrency-documentedconcurrency-safecrypto-safeenvironment-safefilesystem-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignuses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

rustls 0.23.40 is a #![no_std], #![forbid(unsafe_code)] TLS 1.2/1.3 library; published source is byte-identical to VCS. No unsafe, no FFI, no I/O beyond opt-in SSLKEYLOGFILE. Record-layer sequence limits, oversized-record rejection, and the RFC 8446 downgrade sentinel are present. Crypto primitives and cert validation are delegated to the provider and rustls-webpki (scoped out). No findings.

Report

Subject

rustls 0.23.40 is a pure-Rust TLS library implementing TLS 1.2 and TLS 1.3 for both client and server roles. It does no network or file I/O itself: callers feed it ciphertext via read_tls/write_tls and exchange plaintext through ClientConnection and ServerConnection. Cryptographic primitives (AEAD, hashing, HMAC, signatures, key exchange) are delegated to a pluggable CryptoProvider; the default is aws-lc-rs, with ring available behind a feature, and certificate-path validation is delegated to rustls-webpki. The crate is #![no_std] (with alloc) and spans roughly 47.6K lines across 107 source files: the handshake state machines (client/, server/), the record layer, message parsing/encoding (msgs/), and the key schedule (tls13/).

Methodology

Tools: openvet 0.6.0, ripgrep, diff, git, cargo, sha/wc. I compared the published crate (contents/) against the pinned VCS checkout (vcs/, git b44c09fb "Prepare 0.23.40") with diff -rq. I surveyed the capability surface (network, filesystem, environment, process, concurrency, RNG, unsafe, FFI) by grep, and read in full the record layer (record_layer.rs), the deframer entry path and the wire Codec/Reader primitives (msgs/codec.rs, msgs/deframer/), the fragmenter, the message types, build.rs, key_log_file.rs, the downgrade-sentinel logic, and the crate root. I checked RUSTSEC for advisories against this version.

Scope (Methodology Scope note). Per the runbook's scoped-audit guidance for security-critical TLS, the cryptographic primitives are out of scope: they live in the provider crate (aws-lc-rs / ring), audited separately, and certificate validation lives in rustls-webpki, audited separately. The following claims were therefore not evaluated and are left unasserted; they must not be read as either satisfied or violated: crypto-impl-safe, crypto-impl-correct, crypto-impl-tested. The crate orchestrates crypto but does not implement the math, so uses-crypto is asserted while impl-crypto is false. The deepest protocol/parser quality sub-claims (protocol-impl-correct, protocol-impl-safe, protocol-impl-tested, parser-impl-correct, parser-impl-safe, parser-impl-tested) were not exhaustively evaluated across all four state machines and the full extension grammar, and are left unasserted.

Results

The published src/ tree is byte-identical to VCS; the openvet vcs check section confirms this. The only differences are Cargo.toml (cargo normalisation), a single CRLF line ending in .clippy.toml (content-identical), and tests/ plus src/testdata/, which are absent from the package because the manifest sets exclude = ["src/testdata", "tests/**"]. No source file diverges and nothing exists only in the package. No obfuscation, telemetry, or hidden network endpoints were found. Justifies is-benign.

The crate root carries #![forbid(unsafe_code)]; a tree-wide search finds no unsafe block, unsafe fn, or extern "C" (the matches are doc-comment prose). Justifies uses-unsafe. The library performs no network or file I/O of its own, so uses-network holds; the sole filesystem and environment access is the opt-in SSLKEYLOGFILE key-log facility in key_log_file.rs, which reads one documented variable and appends to the named path, justifying uses-filesystem, uses-environment, filesystem-safe, and environment-safe. The crate spawns no processes and contains no scripting, bytecode, or machine-code generation, so uses-exec, uses-interpreter, uses-jit, impl-interpreter, and impl-jit are all false. Internal state is shared through Arc, Mutex, and RwLock (session caches, config), so uses-concurrency, concurrency-safe, and concurrency-documented hold while the crate consumes rather than defines synchronisation primitives, leaving impl-concurrency false. Randomness and all AEAD/sign operations are obtained from the provider, so uses-crypto and crypto-safe hold for the orchestration layer; the crate implements none of the primitives, so impl-crypto is false. It implements no general-purpose algorithm or data structure as its product, so impl-algorithm and impl-datastructure are false.

rustls implements the TLS protocol itself: the record layer, handshake state machines, alert handling, and session resumption. Justifies impl-protocol and impl-parser. The record layer bounds sequence numbers with SEQ_SOFT_LIMIT/SEQ_HARD_LIMIT and refuses to encrypt past the hard limit, so nonce/sequence reuse cannot occur by wraparound; trial decryption for rejected 0-RTT is bounded by a byte budget. The deframer rejects oversized records (PeerSentOversizedRecord, MAX_FRAGMENT_LEN = 16384), records interleaving non-handshake content inside a fragmented handshake, and malformed headers, latching a fatal error so the connection cannot silently recover. Downgrade protection is present: the TLS 1.2 path writes DOWNGRADE_SENTINEL into the server random and the client detects it when it had offered TLS 1.3. The wire Reader is bounds-checked and read_bytes rejects trailing data.

build.rs is conditional on the read_buf feature and, only on nightly, prints cargo:rustc-cfg=read_buf; it opens no sockets, reads no files, and writes nothing outside cargo's stdout protocol. Justifies has-build-exec, build-exec-safe, build-exec-no-network, build-exec-deterministic, build-exec-minimal, and build-exec-no-write-out. The package ships no compiled binaries, so has-binaries is false, and there is no install-time execution, so has-install-exec is false. The crate has extensive #[test] unit tests in src/ and (in VCS, excluded from the package) integration tests, BoGo shim tests, and six libFuzzer fuzz/ targets, but no proptest/quickcheck property tests. Justifies has-unit-tests, has-integration-tests, has-fuzz-tests, and has-property-tests.

The thirteen declared dependencies are enumerated with descriptions. RUSTSEC advisories against rustls (RUSTSEC-2024-0336 complete_io infinite loop, RUSTSEC-2024-0399 Acceptor::accept panic) were fixed in releases well before 0.23.40; no advisory affects this version.

No findings were recorded.

Conclusion

rustls 0.23.40 is a #![no_std], #![forbid(unsafe_code)] TLS 1.2/1.3 implementation with no unsafe, no FFI, and no network or file I/O beyond the opt-in SSLKEYLOGFILE key-log path. Its published source is byte-identical to the tagged VCS checkout. The record layer enforces 64-bit sequence limits to prevent nonce reuse, the deframer bounds record sizes and rejects handshake interleaving, and the TLS 1.2 path carries the RFC 8446 downgrade sentinel. The cryptographic primitives and certificate-path validation are delegated to the provider and rustls-webpki and were not evaluated here. No security, safety, or correctness findings were recorded.

Findings

No findings.

Annotations(6)

build.rs

build.rs is a 14-line script whose main is selected by the rustversion attribute macro. When the read_buf feature is enabled and the compiler is nightly, it prints cargo:rustc-cfg=read_buf; in every other configuration its main is empty. It spawns no process, opens no socket, reads no file, and writes nothing beyond that single cargo cfg line on stdout. Justifies has-build-exec, build-exec-safe, build-exec-no-network, build-exec-deterministic, build-exec-minimal, and build-exec-no-write-out.

src/key_log_file.rs

key_log_file.rs is the only module that touches the filesystem or environment. KeyLogFile reads the SSLKEYLOGFILE environment variable via std::env::var_os and, only when it is set, opens that path with OpenOptions to append TLS key material for debugging. When the variable is unset it does nothing. This is an explicit, opt-in diagnostic facility, not a default behaviour. Justifies uses-filesystem and uses-environment.

src/lib.rs

src/lib.rs, line 331-332

// Require docs for public APIs, deny unsafe code, etc.
#![forbid(unsafe_code, unused_must_use)]

#![forbid(unsafe_code, unused_must_use)] at the crate root makes any unsafe block a compile error. A tree-wide search for unsafe in src/ finds only doc-comment prose and this forbid attribute; there are no unsafe blocks, unsafe fn, or extern "C" declarations in the library. The crate is #![no_std] with extern crate alloc. Justifies uses-unsafe.

src/msgs/deframer/mod.rs

The deframer reconstructs TLS records from arbitrary-sized reads. Header and length parsing returns typed MessageError values (TooShortForHeader, TooShortForLength, MessageTooLarge, InvalidContentType, UnknownProtocolVersion, InvalidEmptyPayload); a fatal parse sets last_error so the connection cannot silently recover. Records exceeding MAX_FRAGMENT_LEN (16384) are rejected as PeerSentOversizedRecord in the provider decrypters. Interleaving a non-handshake record inside a fragmented handshake message is rejected as PeerMisbehaved::MessageInterleavedWithHandshakeMessage. The Reader/Codec primitives are bounds-checked: take returns None when fewer than length bytes remain, and read_bytes rejects trailing data. Justifies impl-parser.

src/record_layer.rs

src/record_layer.rs, line 22-36

pub(crate) struct RecordLayer {
    message_encrypter: Box<dyn MessageEncrypter>,
    message_decrypter: Box<dyn MessageDecrypter>,
    write_seq_max: u64,
    write_seq: u64,
    read_seq: u64,
    has_decrypted: bool,
    encrypt_state: DirectionState,
    decrypt_state: DirectionState,

    // Message encrypted with other keys may be encountered, so failures
    // should be swallowed by the caller.  This struct tracks the amount
    // of message size this is allowed for.
    trial_decryption_len: Option<usize>,
}

The record layer tracks 64-bit read/write sequence numbers used to derive AEAD nonces. pre_encrypt_action returns RefreshOrClose at write_seq_max (bounded by SEQ_SOFT_LIMIT = 0xffff_ffff_ffff_0000) and Refuse at SEQ_HARD_LIMIT = 0xffff_ffff_ffff_fffe; encrypt_outgoing asserts the action is not Refuse before incrementing, so the encrypt sequence cannot wrap. decrypt_incoming short-circuits to plaintext until the decrypt direction is Active, signals want_close_before_decrypt as the read sequence approaches the soft limit, and only swallows DecryptError while trial-decryption budget remains (doing_trial_decryption, a saturating checked_sub over the 0-RTT reject window). Justifies impl-protocol and uses-concurrency.

src/tls12/mod.rs

src/tls12/mod.rs, line 339-339

pub(crate) const DOWNGRADE_SENTINEL: [u8; 8] = [0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x44, 0x01];

DOWNGRADE_SENTINEL = [0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x44, 0x01] (RFC 8446 section 4.1.3) is written into the last eight bytes of the server random by the TLS 1.2 server path. The client checks, when it offered TLS 1.3 but the server selected TLS 1.2, whether the server random ends in the sentinel and treats a match as an active downgrade attack. This is rustls's downgrade protection across the handshake's version-negotiation step. Justifies impl-protocol.