cheatah
Source

stdlib/tls/tls.cpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3#include "tls.hpp"
4#include "tls_lowlevel.hpp" // the C++-only raw handle API this module implements (+ tls::Conn uses)
6#include <algorithm>
7#include <cstdint>
8#include <cstring>
9#include <ctime>
10#include <map>
11#include <mutex>
12#include <string_view>
13#include <vector>
15#include <sys/random.h> // getentropy: client random + ephemeral X25519 key
17#include "aead.hpp" // chacha20poly1305_{en,de}crypt — the record cipher
18#include "ed25519.hpp" // verify — CertificateVerify for Ed25519 server certs
19#include "hashlib.hpp" // sha256_digest, hmac_sha256, hkdf_extract/expand — the key schedule
20#include "p256.hpp" // verify — CertificateVerify for ECDSA P-256 server certs
21#include "p384.hpp" // verify — CertificateVerify for ECDSA P-384 server certs
22#include "rsa_verify.hpp" // verify_pss_sha256 — CertificateVerify for RSA (rsa_pss_rsae_sha256) certs
23#include "socket.hpp" // raw fd I/O underneath the record layer
24#include "x25519.hpp" // the key exchange
25#include "x509.hpp" // certificate chain / hostname / expiry validation (server AUTHENTICATION)
27// A from-scratch TLS 1.3 client (RFC 8446); cipher suites ChaCha20-Poly1305, AES-128-GCM,
28// and AES-256-GCM-SHA384, offered in hardware-preference order (see append_cipher_preference).
29// The implementation walks the RFC top to bottom: record layer, transcript hash, the HKDF
30// key schedule, then the handshake state machine. Every secret derives through hashlib's
31// HKDF; every record seals/opens through the aead module; the ephemeral key is x25519.
33namespace cheatah::tls {
34namespace {
36namespace sock = cheatah::socket;
38thread_local std::string t_error; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables): the per-thread last_error() slot IS the documented error channel
40void fail(std::string_view what) { t_error = std::string(what); }
42// ---- hex <-> bytes: the ONE canonical implementation lives in hashlib -----------
43// The crypto modules speak hex for keys; tls feeds from_hex only valid, even-length lowercase
44// x25519 hex, so the canonical from_hex's odd-length/non-hex throws are never reached here.
45using hashlib::to_hex; // bytes -> lowercase hex (string_view / (uint8_t*, n) overloads).
46using hashlib::from_hex; // hex -> bytes (throws on odd length / non-hex).
48// 16/24-bit big-endian helpers for the wire format.
49void put16(std::string& out, unsigned v) {
50 out.push_back(static_cast<char>(v >> 8));
51 out.push_back(static_cast<char>(v));
53void put24(std::string& out, unsigned v) {
54 out.push_back(static_cast<char>(v >> 16));
55 out.push_back(static_cast<char>(v >> 8));
56 out.push_back(static_cast<char>(v));
58unsigned get16(std::string_view s, std::size_t i) {
59 return (static_cast<unsigned char>(s[i]) << 8) | static_cast<unsigned char>(s[i + 1]);
61unsigned get24(std::string_view s, std::size_t i) {
62 return (static_cast<unsigned char>(s[i]) << 16) | (static_cast<unsigned char>(s[i + 1]) << 8) |
63 static_cast<unsigned char>(s[i + 2]);
66// ---- the TLS 1.3 key schedule (RFC 8446 §7.1) over hashlib's HKDF ------------
68} // namespace (pause: the key-schedule impls are namespace-level so detail:: can reach them)
70/**
71 * HKDF-Expand-Label(secret, label, context, length) with the "tls13 " prefix (RFC 8446 §7.1).
72 * @param secret the HKDF secret.
73 * @param label the schedule label (without the "tls13 " prefix, which is added here).
74 * @param context the hash context bytes.
75 * @param length the output length in bytes.
76 * @param sha384 selects the SHA-384 HKDF (for the TLS_AES_256_GCM_SHA384 key schedule);
77 * default is the SHA-256 schedule.
78 * @return the expanded key material, @p length bytes.
79 * @complexity O(⌈length/hash⌉ · (|secret| + |label| + |context| + hash)) — one HMAC per output block.
80 * @alloc the returned key material, the HkdfLabel info string, and HKDF's per-block HMAC scratch.
81 * @test CheatahTls.ExpandLabel
82 */
83std::string expand_label_impl(std::string_view secret, std::string_view label,
84 std::string_view context, unsigned length, bool sha384 = false) {
85 std::string info;
86 put16(info, length);
87 info.push_back(static_cast<char>(6 + label.size()));
88 info += "tls13 ";
89 info += label;
90 info.push_back(static_cast<char>(context.size()));
91 info += context;
92 return sha384 ? hashlib::hkdf_expand_sha384(secret, info, length)
93 : hashlib::hkdf_expand(secret, info, length);
96/**
97 * Derive-Secret(secret, label, transcript) = Expand-Label(secret, label, Hash(transcript), HashLen),
98 * where Hash is the negotiated suite's hash (SHA-256, or SHA-384 when @p sha384).
99 * @param secret the HKDF secret.
100 * @param label the schedule label.
101 * @param transcript the handshake transcript to hash into the context.
102 * @param sha384 selects the SHA-384 schedule; default is SHA-256.
103 * @return the derived secret (32 or 48 bytes).
104 * @complexity O(|transcript|) — one transcript hash, then a fixed-size expand.
105 * @alloc the transcript-hash string and the returned secret.
106 * @test CheatahTls.KeySchedule
107 */
108std::string derive_secret_impl(std::string_view secret, std::string_view label,
109 std::string_view transcript, bool sha384 = false) {
110 const std::string th =
111 sha384 ? hashlib::sha384_digest(transcript) : hashlib::sha256_digest(transcript);
112 return expand_label_impl(secret, label, th, sha384 ? 48 : 32, sha384);
115namespace { // resume the file-local helpers
117// The negotiated record cipher: ChaCha20-Poly1305 (0x1303), AES-128-GCM (0x1301), or AES-256-GCM (0x1302).
118enum class Aead : std::uint8_t { Chacha20, Aes128, Aes256 };
120// Key-schedule hash dispatch: the SHA-256 schedule by default, the SHA-384 schedule for the
121// TLS_AES_256_GCM_SHA384 suite. (RFC 8446 §7.1: the schedule's Hash is the cipher suite's hash.)
122std::string ks_digest(bool sha384, std::string_view d) {
123 return sha384 ? hashlib::sha384_digest(d) : hashlib::sha256_digest(d);
125std::string ks_extract(bool sha384, std::string_view salt, std::string_view ikm) {
126 return sha384 ? hashlib::hkdf_extract_sha384(salt, ikm) : hashlib::hkdf_extract(salt, ikm);
128std::string ks_hmac(bool sha384, std::string_view key, std::string_view data) {
129 return sha384 ? hashlib::hmac_sha384(key, data) : hashlib::hmac_sha256(key, data);
132// One traffic direction: AEAD key + iv + record sequence number.
133struct Keys {
134 std::string key_hex; // AEAD key (hex): 32 bytes for ChaCha20 / AES-256-GCM, 16 for AES-128-GCM
135 std::string iv; // 12-byte raw iv; per-record nonce = iv XOR seq
136 std::uint64_t seq = 0;
137 Aead aead = Aead::Chacha20;
138};
140// Derive a direction's record keys from its traffic secret. Key length follows the AEAD (16 for AES-128,
141// 32 for AES-256 / ChaCha20); @p sha384 selects the SHA-384 key schedule (the 256 suite).
142Keys traffic_keys(std::string_view secret, Aead aead, bool sha384) {
143 Keys k;
144 k.aead = aead;
145 k.key_hex = to_hex(expand_label_impl(secret, "key", "", aead == Aead::Aes128 ? 16 : 32, sha384));
146 k.iv = expand_label_impl(secret, "iv", "", 12, sha384);
147 return k;
150// The per-record nonce: the 12-byte iv with the 8-byte big-endian sequence XORed into its tail.
151std::string nonce_hex(const Keys& k) {
152 std::string n = k.iv;
153 for (int i = 0; i < 8; ++i) {
154 n[4 + i] = static_cast<char>(static_cast<unsigned char>(n[4 + i]) ^
155 static_cast<unsigned char>(k.seq >> (8 * (7 - i))));
156 }
157 return to_hex(n);
160// ---- one TLS session ----------------------------------------------------------
162struct Session {
163 long long fd = -1;
164 Keys client_keys; // our sending direction
165 Keys server_keys; // the peer's direction
166 std::string read_buffer; // raw bytes from the socket not yet framed into records
167 std::string app_pending; // decrypted application data not yet handed to recv()
168 bool closed = false; // close_notify seen (either direction)
169};
171// The process-wide session table: handle → Session, behind one mutex.
172struct Registry {
173 std::mutex mutex;
174 std::map<long long, Session> sessions;
175 long long next_handle = 1;
176};
177Registry& registry() {
178 static Registry r;
179 return r;
182// ---- record I/O ---------------------------------------------------------------
184// Read exactly one TLS record (header + payload) from the socket into (type, payload).
185// Blocking, bounded by the fd's socket timeout. False on EOF/short read.
186// The socket read chunk. 64 KiB drains several TLS records per syscall when the kernel has them
187// buffered, which (with the socket's enlarged SO_RCVBUF) keeps the receive window open instead of
188// stalling one record per round-trip.
189constexpr long long kRecvChunk = 65536;
191bool read_record(long long fd, std::string& buffer, unsigned& type, std::string& payload) {
192 while (buffer.size() < 5) {
193 const std::string chunk = sock::recv(fd, kRecvChunk);
194 if (chunk.empty()) return false;
195 buffer += chunk;
196 }
197 type = static_cast<unsigned char>(buffer[0]);
198 const unsigned len = get16(buffer, 3);
199 if (len > 16384 + 256) { // RFC bound + AEAD overhead: anything bigger is malformed
200 return false;
201 }
202 while (buffer.size() < 5 + len) {
203 const std::string chunk = sock::recv(fd, kRecvChunk);
204 if (chunk.empty()) return false;
205 buffer += chunk;
206 }
207 payload = buffer.substr(5, len);
208 buffer.erase(0, 5 + len);
209 return true;
212// True when `buffer` already holds at least one COMPLETE record — used by the drain loop to keep
213// decrypting from bytes already in hand without blocking on another recv().
214bool has_complete_record(const std::string& buffer) {
215 if (buffer.size() < 5) {
216 return false;
217 }
218 const unsigned len = get16(buffer, 3);
219 return buffer.size() >= static_cast<std::size_t>(5) + len;
222bool write_record(long long fd, unsigned type, std::string_view payload) {
223 std::string rec;
224 rec.push_back(static_cast<char>(type));
225 put16(rec, 0x0303); // legacy_record_version
226 put16(rec, static_cast<unsigned>(payload.size()));
227 rec += payload;
228 return sock::sendall(fd, rec) == 0;
231// Seal one application_data record (RFC 8446 §5.2): inner plaintext = content || content_type,
232// AAD = the record header, then ChaCha20-Poly1305.
233bool seal_record(long long fd, Keys& k, unsigned inner_type, std::string_view content) {
234 std::string inner(content);
235 inner.push_back(static_cast<char>(inner_type));
236 std::string aad;
237 aad.push_back(23);
238 put16(aad, 0x0303);
239 put16(aad, static_cast<unsigned>(inner.size() + 16));
240 std::string ct;
241 if (k.aead == Aead::Aes256) ct = aead::aes256gcm_encrypt(k.key_hex, nonce_hex(k), aad, inner);
242 else if (k.aead == Aead::Aes128) ct = aead::aes128gcm_encrypt(k.key_hex, nonce_hex(k), aad, inner);
243 else ct = aead::chacha20poly1305_encrypt(k.key_hex, nonce_hex(k), aad, inner);
244 ++k.seq;
245 if (ct.empty()) return false;
246 return sock::sendall(fd, aad + ct) == 0;
249// Open one encrypted record: returns the inner content and type, false on AEAD failure.
250bool open_record(Keys& k, std::string_view payload, unsigned& inner_type, std::string& content) {
251 std::string aad;
252 aad.push_back(23);
253 put16(aad, 0x0303);
254 put16(aad, static_cast<unsigned>(payload.size()));
255 std::string inner;
256 if (k.aead == Aead::Aes256) inner = aead::aes256gcm_decrypt(k.key_hex, nonce_hex(k), aad, payload);
257 else if (k.aead == Aead::Aes128) inner = aead::aes128gcm_decrypt(k.key_hex, nonce_hex(k), aad, payload);
258 else inner = aead::chacha20poly1305_decrypt(k.key_hex, nonce_hex(k), aad, payload);
259 ++k.seq;
260 if (inner.empty() && payload.size() > 16) return false; // tag mismatch (or empty record)
261 while (!inner.empty() && inner.back() == '\0') inner.pop_back(); // strip padding
262 if (inner.empty()) return false; // a record must carry a content type
263 inner_type = static_cast<unsigned char>(inner.back());
264 inner.pop_back(); // drop the trailing content-type byte in place …
265 content = std::move(inner); // … and move the ~16 KB plaintext out instead of copying it
266 return true;
269// ---- handshake construction -----------------------------------------------------
271std::string random_bytes(std::size_t n) {
272 std::string out(n, '\0');
273 // getentropy (Linux + macOS/BSD) is the portable CSPRNG read; getrandom is Linux-only.
274 // It is capped at 256 bytes per call, so loop for larger requests. A nonzero return means
275 // the OS could not supply randomness — fatal for key material, so bail with "".
276 std::size_t got = 0;
277 while (got < n) {
278 const std::size_t chunk = std::min<std::size_t>(n - got, 256);
279 if (::getentropy(out.data() + got, chunk) != 0) return "";
280 got += chunk;
281 }
282 return out;
285// Build the ClientHello handshake MESSAGE (no record header). Fills `client_hello_random`.
286std::string build_client_hello(const std::string& server_name, std::string_view pub_raw) {
287 std::string body;
288 put16(body, 0x0303); // legacy_version
289 body += random_bytes(32); // random
290 body.push_back(32); // legacy_session_id (32 bytes, middlebox compatibility)
291 body += random_bytes(32);
292 // Cipher preference follows OUR fastest cipher, exactly as OpenSSL/curl do: with AES-NI +
293 // PCLMULQDQ present, AES-GCM runs at multi-GB/s hardware speed and beats our scalar ChaCha20,
294 // so offer AES-GCM FIRST; without hardware AES (some VMs/ARM), scalar ChaCha20 is the faster
295 // path, so lead with it. The server picks from our order when it honors client preference —
296 // which is what turns a ChaCha-negotiated ~200 MB/s link into a ~320 MB/s AES-GCM one.
297 put16(body, 6); // cipher_suites: three suites (6 bytes)
298 detail::append_cipher_preference(body, aead::crypto_hardware_active());
299 body.push_back(1); // legacy_compression_methods
300 body.push_back(0); // null
302 std::string ext;
303 { // server_name (0)
304 std::string names;
305 names.push_back(0); // host_name
306 put16(names, static_cast<unsigned>(server_name.size()));
307 names += server_name;
308 std::string sni;
309 put16(sni, static_cast<unsigned>(names.size()));
310 sni += names;
311 put16(ext, 0);
312 put16(ext, static_cast<unsigned>(sni.size()));
313 ext += sni;
314 }
315 { // supported_groups (10): x25519 only
316 std::string g;
317 put16(g, 2);
318 put16(g, 0x001d);
319 put16(ext, 10);
320 put16(ext, static_cast<unsigned>(g.size()));
321 ext += g;
322 }
323 { // signature_algorithms (13): ed25519 (verifiable) + the common ones so real servers
324 // complete the handshake far enough for our explicit refusal to be diagnosable
325 std::string a;
326 put16(a, 8);
327 put16(a, 0x0807); // ed25519
328 put16(a, 0x0804); // rsa_pss_rsae_sha256 (verified — see rsa_verify.hpp)
329 put16(a, 0x0403); // ecdsa_secp256r1_sha256 (verified — see p256)
330 put16(a, 0x0503); // ecdsa_secp384r1_sha384 (verified — see p384)
331 put16(ext, 13);
332 put16(ext, static_cast<unsigned>(a.size()));
333 ext += a;
334 }
335 { // supported_versions (43): TLS 1.3
336 std::string v;
337 v.push_back(2);
338 put16(v, 0x0304);
339 put16(ext, 43);
340 put16(ext, static_cast<unsigned>(v.size()));
341 ext += v;
342 }
343 { // key_share (51): our X25519 public key
344 std::string entry;
345 put16(entry, 0x001d);
346 put16(entry, 32);
347 entry += pub_raw;
348 std::string ks;
349 put16(ks, static_cast<unsigned>(entry.size()));
350 ks += entry;
351 put16(ext, 51);
352 put16(ext, static_cast<unsigned>(ks.size()));
353 ext += ks;
354 }
355 put16(body, static_cast<unsigned>(ext.size()));
356 body += ext;
358 std::string msg;
359 msg.push_back(1); // client_hello
360 put24(msg, static_cast<unsigned>(body.size()));
361 msg += body;
362 return msg;
365// Human-readable TLS alert (RFC 8446 §6) from the 2 alert bytes — so a handshake refusal names its
366// cause (e.g. 40 handshake_failure = no common cipher/group; 70 protocol_version = no TLS 1.3).
367std::string alert_text(std::string_view p) {
368 if (p.size() < 2) return "(empty alert)";
369 const unsigned lvl = static_cast<unsigned char>(p[0]);
370 const unsigned d = static_cast<unsigned char>(p[1]);
371 const char* name = "unknown";
372 switch (d) {
373 case 0: name = "close_notify"; break;
374 case 10: name = "unexpected_message"; break;
375 case 20: name = "bad_record_mac"; break;
376 case 22: name = "record_overflow"; break;
377 case 40: name = "handshake_failure"; break;
378 case 42: name = "bad_certificate"; break;
379 case 43: name = "unsupported_certificate"; break;
380 case 47: name = "illegal_parameter"; break;
381 case 48: name = "unknown_ca"; break;
382 case 49: name = "access_denied"; break;
383 case 50: name = "decode_error"; break;
384 case 51: name = "decrypt_error"; break;
385 case 70: name = "protocol_version"; break;
386 case 71: name = "insufficient_security"; break;
387 case 80: name = "internal_error"; break;
388 case 109: name = "missing_extension"; break;
389 case 110: name = "unsupported_extension"; break;
390 case 112: name = "unrecognized_name"; break;
391 case 116: name = "certificate_required"; break;
392 case 120: name = "no_application_protocol"; break;
393 default: break;
394 }
395 return "alert level=" + std::to_string(lvl) + " description=" + std::to_string(d) + " (" + name + ")";
398// Parse ServerHello: confirm TLS 1.3 + one of our suites, extract the server's X25519 key share and
399// the CHOSEN cipher suite (0x1303 ChaCha20-Poly1305 or 0x1301 AES-128-GCM).
400bool parse_server_hello(std::string_view msg, std::string& server_pub_raw, unsigned& chosen_suite) {
401 if (msg.size() < 4 || msg[0] != 2) return false; // server_hello
402 std::string_view b = msg.substr(4);
403 if (b.size() < 2 + 32 + 1) return false;
404 std::size_t i = 2 + 32; // legacy_version + random
405 const unsigned sid_len = static_cast<unsigned char>(b[i]);
406 i += 1 + sid_len;
407 if (b.size() < i + 4) return false;
408 const unsigned suite = get16(b, i);
409 if (suite != 0x1303 && suite != 0x1301 && suite != 0x1302)
410 return false; // LCOV_EXCL_LINE: a server choosing a suite we did NOT offer is a malformed/hostile peer a conformant server never produces — the client-side mirror of the tested ParseClientHello "no suite in common" rejection
411 chosen_suite = suite;
412 i += 2 + 1; // suite + legacy_compression
413 if (b.size() < i + 2) return false;
414 const unsigned ext_len = get16(b, i);
415 i += 2;
416 const std::size_t ext_end = i + ext_len;
417 bool saw_13 = false;
418 while (i + 4 <= ext_end && ext_end <= b.size()) {
419 const unsigned etype = get16(b, i);
420 const unsigned elen = get16(b, i + 2);
421 i += 4;
422 if (i + elen > b.size()) return false;
423 if (etype == 43 && elen == 2 && get16(b, i) == 0x0304) saw_13 = true;
424 if (etype == 51 && elen >= 4 && get16(b, i) == 0x001d && get16(b, i + 2) == 32 &&
425 elen == 4 + 32) {
426 server_pub_raw = std::string(b.substr(i + 4, 32));
427 }
428 i += elen;
429 }
430 return saw_13 && server_pub_raw.size() == 32;
433// Extract the Ed25519 public key from the leaf certificate's SubjectPublicKeyInfo: the DER
434// pattern 30 05 06 03 2B 65 70 (AlgorithmIdentifier { id-Ed25519 }) followed by
435// 03 21 00 <32-byte key> (BIT STRING). Returns "" when the cert key is not Ed25519.
436std::string ed25519_spki_key(std::string_view cert_der) {
437 static const unsigned char kPat[] = {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70,
438 0x03, 0x21, 0x00};
439 for (std::size_t i = 0; i + sizeof kPat + 32 <= cert_der.size(); ++i) {
440 if (std::memcmp(cert_der.data() + i, kPat, sizeof kPat) == 0) {
441 return std::string(cert_der.substr(i + sizeof kPat, 32));
442 }
443 }
444 return ""; // LCOV_EXCL_LINE: only when an Ed25519 CertificateVerify names a non-Ed25519 leaf — a malformed peer we don't mirror
447// ---- server-side handshake construction (mirror of the client builders above) --------
449// A PEM block's DER bytes (strict base64 — a non-alphabet byte rejects the block, like x509).
450// @p label is e.g. "CERTIFICATE" or "PRIVATE KEY". Returns "" if the block is absent/malformed.
451std::string pem_block(const std::string& pem, const std::string& label) {
452 const std::string begin = "-----BEGIN " + label + "-----";
453 const std::string end = "-----END " + label + "-----";
454 const std::size_t s = pem.find(begin);
455 if (s == std::string::npos) return "";
456 const std::size_t b = s + begin.size();
457 const std::size_t e = pem.find(end, b);
458 if (e == std::string::npos) return "";
459 return hashlib::base64_decode(pem.substr(b, e - b), /*strict=*/true);
462// The 32-byte Ed25519 seed from a PKCS#8 private key DER: the id-Ed25519 AlgorithmIdentifier
463// (30 05 06 03 2B 65 70) followed by 04 22 04 20 (OCTET STRING { OCTET STRING[32] }) and the seed.
464std::string ed25519_seed_from_pkcs8(std::string_view der) {
465 static const unsigned char kPat[] = {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70,
466 0x04, 0x22, 0x04, 0x20};
467 for (std::size_t i = 0; i + sizeof kPat + 32 <= der.size(); ++i) {
468 if (std::memcmp(der.data() + i, kPat, sizeof kPat) == 0) {
469 return std::string(der.substr(i + sizeof kPat, 32));
470 }
471 }
472 return "";
475// EVERY PEM block under @p label, in order — the server's Certificate message must carry the whole
476// chain (leaf first, then intermediates), so a Let's Encrypt fullchain.pem yields N entries here
477// where pem_block() alone would silently drop everything after the leaf and browsers would reject
478// the path. A malformed block (bad base64) poisons the whole read: better no chain than a hole.
479std::vector<std::string> pem_blocks(const std::string& pem, const std::string& label) {
480 std::vector<std::string> out;
481 const std::string begin = "-----BEGIN " + label + "-----";
482 const std::string end = "-----END " + label + "-----";
483 std::size_t at = 0;
484 while (true) {
485 const std::size_t s = pem.find(begin, at);
486 if (s == std::string::npos) break;
487 const std::size_t b = s + begin.size();
488 const std::size_t e = pem.find(end, b);
489 if (e == std::string::npos) return {};
490 const std::string der = hashlib::base64_decode(pem.substr(b, e - b), /*strict=*/true);
491 if (der.empty()) return {};
492 out.push_back(der);
493 at = e + end.size();
494 }
495 return out;
498// The 32-byte P-256 private scalar from a server key PEM — either shape openssl/certbot emit:
499// PKCS#8 ("PRIVATE KEY": AlgorithmIdentifier{id-ecPublicKey, prime256v1} wrapping a SEC1
500// ECPrivateKey) or bare SEC1 ("EC PRIVATE KEY"). Both carry the scalar as 02 01 01 04 20 <d32>
501// (ECPrivateKey version 1, then the OCTET STRING), and both carry the prime256v1 OID
502// (2A 86 48 CE 3D 03 01 07) — required here so a P-384/other-curve key is refused instead of
503// misread. Same pattern-scan discipline as ed25519_seed_from_pkcs8 above.
504std::string ec_p256_scalar_from_pem(const std::string& key_pem) {
505 std::string der = pem_block(key_pem, "PRIVATE KEY");
506 if (der.empty()) der = pem_block(key_pem, "EC PRIVATE KEY");
507 if (der.empty()) return "";
508 static const unsigned char kOid[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07};
509 bool p256_curve = false;
510 for (std::size_t i = 0; i + sizeof kOid <= der.size() && !p256_curve; ++i) {
511 p256_curve = std::memcmp(der.data() + i, kOid, sizeof kOid) == 0;
512 }
513 if (!p256_curve) return "";
514 static const unsigned char kPat[] = {0x02, 0x01, 0x01, 0x04, 0x20};
515 for (std::size_t i = 0; i + sizeof kPat + 32 <= der.size(); ++i) {
516 if (std::memcmp(der.data() + i, kPat, sizeof kPat) == 0) {
517 return der.substr(i + sizeof kPat, 32);
518 }
519 }
520 return "";
523// Parse a ClientHello: choose a cipher suite we support (ChaCha20 preferred), extract the client's
524// X25519 key share + its legacy_session_id (echoed in ServerHello), confirm it offered TLS 1.3,
525// and surface its signature_algorithms (ext 13) as raw u16 pairs in @p sig_algs — the parser stays
526// lenient (an absent extension parses fine, sig_algs empty); server_handshake enforces the match,
527// because refusing to SIGN with an algorithm the client never offered is a handshake policy, not a
528// parse question.
529bool parse_client_hello(std::string_view msg, std::string& client_pub_raw, unsigned& chosen_suite,
530 std::string& session_id, std::string& sig_algs) {
531 client_pub_raw.clear(); // never leave stale out-params on a rejected/partial parse
532 session_id.clear();
533 sig_algs.clear();
534 if (msg.size() < 4 || static_cast<unsigned char>(msg[0]) != 1) return false; // client_hello
535 std::string_view b = msg.substr(4);
536 std::size_t i = 2 + 32; // legacy_version + random
537 if (b.size() < i + 1) return false;
538 const unsigned sid_len = static_cast<unsigned char>(b[i]);
539 i += 1;
540 if (b.size() < i + sid_len) return false;
541 session_id = std::string(b.substr(i, sid_len)); // must be echoed back verbatim
542 i += sid_len;
543 if (b.size() < i + 2) return false;
544 const unsigned cs_len = get16(b, i);
545 i += 2;
546 if (b.size() < i + cs_len) return false;
547 bool has_chacha = false, has_aes = false;
548 for (std::size_t j = 0; j + 2 <= cs_len; j += 2) {
549 const unsigned suite = get16(b, i + j);
550 if (suite == 0x1303) has_chacha = true;
551 if (suite == 0x1301) has_aes = true;
552 }
553 i += cs_len;
554 if (has_chacha) chosen_suite = 0x1303;
555 else if (has_aes) chosen_suite = 0x1301;
556 else return false; // no cipher suite in common
557 if (b.size() < i + 1) return false;
558 const unsigned comp_len = static_cast<unsigned char>(b[i]);
559 i += 1 + comp_len;
560 if (b.size() < i + 2) return false;
561 const unsigned ext_len = get16(b, i);
562 i += 2;
563 const std::size_t ext_end = i + ext_len;
564 bool saw_13 = false;
565 while (i + 4 <= ext_end && ext_end <= b.size()) {
566 const unsigned etype = get16(b, i);
567 const unsigned elen = get16(b, i + 2);
568 i += 4;
569 if (i + elen > b.size()) return false;
570 if (etype == 43 && elen >= 1) { // supported_versions (list): look for 0x0304
571 const unsigned n = static_cast<unsigned char>(b[i]);
572 for (std::size_t j = 0; j + 2 <= n && i + 1 + j + 2 <= b.size(); j += 2) {
573 if (get16(b, i + 1 + j) == 0x0304) saw_13 = true;
574 }
575 }
576 if (etype == 13 && elen >= 2) { // signature_algorithms: the u16-pair list
577 const unsigned sa_len = get16(b, i);
578 if (2 + sa_len <= elen && sa_len % 2 == 0) {
579 sig_algs = std::string(b.substr(i + 2, sa_len));
580 }
581 }
582 if (etype == 51 && elen >= 2) { // key_share (list): find our x25519 (0x001d)
583 const unsigned ks_len = get16(b, i);
584 std::size_t j = i + 2;
585 const std::size_t ks_end = i + 2 + ks_len;
586 while (j + 4 <= ks_end && ks_end <= b.size()) {
587 const unsigned group = get16(b, j);
588 const unsigned klen = get16(b, j + 2);
589 if (group == 0x001d && klen == 32 && j + 4 + 32 <= b.size()) {
590 client_pub_raw = std::string(b.substr(j + 4, 32));
591 }
592 j += 4 + klen;
593 }
594 }
595 i += elen;
596 }
597 return saw_13 && client_pub_raw.size() == 32;
600// Build a ServerHello: echo the client's session id, our chosen suite, our X25519 key share.
601std::string build_server_hello(std::string_view session_id, unsigned suite,
602 std::string_view pub_raw) {
603 std::string body;
604 put16(body, 0x0303); // legacy_version
605 body += random_bytes(32); // random
606 body.push_back(static_cast<char>(session_id.size()));
607 body += session_id; // echo legacy_session_id (RFC 8446 §4.1.3)
608 put16(body, suite); // cipher_suite
609 body.push_back(0); // legacy_compression_method (null)
611 std::string ext;
612 put16(ext, 43); // supported_versions: TLS 1.3
613 put16(ext, 2);
614 put16(ext, 0x0304);
615 { // key_share (51): our X25519 KeyShareEntry
616 std::string entry;
617 put16(entry, 0x001d);
618 put16(entry, 32);
619 entry += pub_raw;
620 put16(ext, 51);
621 put16(ext, static_cast<unsigned>(entry.size()));
622 ext += entry;
623 }
624 put16(body, static_cast<unsigned>(ext.size()));
625 body += ext;
627 std::string msg;
628 msg.push_back(2); // server_hello
629 put24(msg, static_cast<unsigned>(body.size()));
630 msg += body;
631 return msg;
634// The TLS 1.3 SERVER handshake over connected fd @p fd, presenting @p cert_pem (an Ed25519 or
635// ECDSA P-256 leaf — @p cert_pem may be a fullchain.pem, and every block is sent) and proving
636// possession with @p key_pem (the leaf's PKCS#8 Ed25519 key, or its PKCS#8/SEC1 P-256 key).
637// Mirror of handshake(): same key schedule and record I/O, roles reversed — we SIGN
638// CertificateVerify instead of verifying it, and send the certificate flight. Returns a session
639// handle (>= 1) or -1 (see last_error()).
640long long server_handshake(long long fd, const std::string& cert_pem, const std::string& key_pem) {
641 t_error.clear();
643 const std::vector<std::string> chain = pem_blocks(cert_pem, "CERTIFICATE");
644 if (chain.empty()) {
645 fail("tls: server certificate PEM is missing or malformed");
646 return -1;
647 }
648 const std::string& leaf_der = chain.front();
650 // Which key did we get, and does it actually belong to the leaf? Deriving the public key from
651 // the private half and comparing it to the leaf's SPKI catches a mixed-up cert/key pair at
652 // startup with a precise message, instead of as an opaque CertificateVerify failure on the
653 // first client. Exactly one signature algorithm follows from the key type — there is no
654 // negotiation surface on our side to confuse.
655 const std::string ed_seed = ed25519_seed_from_pkcs8(pem_block(key_pem, "PRIVATE KEY"));
656 const std::string ec_scalar = ec_p256_scalar_from_pem(key_pem);
657 unsigned cv_alg = 0;
658 if (ed_seed.size() == 32) {
659 const std::string spki = ed25519_spki_key(leaf_der);
660 if (spki.size() != 32 || ed25519::public_key(to_hex(ed_seed)) != to_hex(spki)) {
661 fail("tls: the Ed25519 private key does not match the server certificate");
662 return -1;
663 }
664 cv_alg = 0x0807; // ed25519
665 } else if (ec_scalar.size() == 32) {
666 const std::string point = p256::spki_ec_point(leaf_der);
667 if (point.size() != 64 || p256::public_from_private(ec_scalar) != point) {
668 fail("tls: the ECDSA P-256 private key does not match the server certificate");
669 return -1;
670 }
671 cv_alg = 0x0403; // ecdsa_secp256r1_sha256
672 } else {
673 fail("tls: server private key is not a PKCS#8 Ed25519 or P-256 EC key");
674 return -1;
675 }
677 // ClientHello (plaintext; tolerate a leading ChangeCipherSpec compat record).
678 std::string buffer, payload;
679 unsigned rtype = 0;
680 for (;;) {
681 if (!read_record(fd, buffer, rtype, payload)) {
682 fail("tls: connection closed before ClientHello");
683 return -1;
684 }
685 if (rtype == 20) continue;
686 if (rtype != 22) {
687 fail("tls: expected a ClientHello");
688 return -1;
689 }
690 break;
691 }
692 std::string client_pub_raw, session_id, sig_algs;
693 unsigned suite = 0;
694 if (!parse_client_hello(payload, client_pub_raw, suite, session_id, sig_algs)) {
695 fail("tls: malformed ClientHello (or no TLS 1.3 / X25519 / shared cipher suite)");
696 return -1;
697 }
698 // RFC 8446 §4.4.3: a server MUST NOT sign with an algorithm the client did not offer in
699 // signature_algorithms (§4.2.3 makes the extension mandatory for certificate auth). Our
700 // algorithm is fixed by the key type, so this is a containment check, not a negotiation.
701 bool alg_offered = false;
702 for (std::size_t j = 0; j + 2 <= sig_algs.size(); j += 2) {
703 if (get16(sig_algs, j) == cv_alg) alg_offered = true;
704 }
705 if (!alg_offered) {
706 fail("tls: the client's signature_algorithms do not include our certificate's algorithm");
707 return -1;
708 }
709 // The server offers only the SHA-256 suites (ChaCha20 / AES-128-GCM), so the key schedule is SHA-256.
710 const Aead aead = (suite == 0x1301) ? Aead::Aes128 : Aead::Chacha20;
711 std::string transcript = payload; // ClientHello
713 // Our ephemeral X25519 key pair, and ServerHello.
714 const std::string priv_raw = random_bytes(32);
715 if (priv_raw.size() != 32) {
716 fail("tls: system random unavailable"); // LCOV_EXCL_LINE: getentropy failure — unreachable on a working host
717 return -1; // LCOV_EXCL_LINE
718 }
719 const std::string priv_hex = to_hex(priv_raw);
720 const std::string pub_raw = from_hex(x25519::x25519_base(priv_hex));
721 const std::string server_hello = build_server_hello(session_id, suite, pub_raw);
722 transcript += server_hello;
723 if (!write_record(fd, 22, server_hello)) {
724 fail("tls: cannot send ServerHello"); // LCOV_EXCL_LINE: a mid-handshake socket write failure
725 return -1; // LCOV_EXCL_LINE
726 }
727 write_record(fd, 20, std::string(1, '\x01')); // middlebox-compat ChangeCipherSpec (not in transcript)
729 // Key schedule (identical to the client; the transcript now spans ClientHello + ServerHello).
730 const std::string shared_hex = x25519::x25519(priv_hex, to_hex(client_pub_raw));
731 if (shared_hex.empty()) {
732 fail("tls: invalid client key share");
733 return -1;
734 }
735 const std::string zeros(32, '\0');
736 const std::string early = hashlib::hkdf_extract(std::string(), zeros);
737 const std::string derived = derive_secret_impl(early, "derived", "");
738 const std::string hs_secret = hashlib::hkdf_extract(derived, from_hex(shared_hex));
739 const std::string c_hs = derive_secret_impl(hs_secret, "c hs traffic", transcript);
740 const std::string s_hs = derive_secret_impl(hs_secret, "s hs traffic", transcript);
741 Keys server_keys = traffic_keys(s_hs, aead, false); // we SEND under the server secret
742 Keys client_keys = traffic_keys(c_hs, aead, false); // we RECEIVE under the client secret
744 // Encrypted flight: EncryptedExtensions (empty), Certificate, CertificateVerify, Finished —
745 // each sealed as its own handshake record (inner type 22).
746 std::string ee;
747 ee.push_back(8); // encrypted_extensions
748 put24(ee, 2);
749 put16(ee, 0); // extensions: empty
750 transcript += ee;
751 if (!seal_record(fd, server_keys, 22, ee)) {
752 fail("tls: cannot send EncryptedExtensions"); // LCOV_EXCL_LINE: a mid-handshake socket write failure
753 return -1; // LCOV_EXCL_LINE
754 }
756 std::string cert_msg;
757 {
758 std::string entries; // every chain block, leaf first — a fullchain.pem arrives intact,
759 for (const std::string& der : chain) { // giving the client a path to its trust anchor
760 put24(entries, static_cast<unsigned>(der.size()));
761 entries += der;
762 put16(entries, 0); // per-cert extensions: none
763 }
764 std::string cbody;
765 cbody.push_back(0); // certificate_request_context: empty
766 put24(cbody, static_cast<unsigned>(entries.size()));
767 cbody += entries;
768 cert_msg.push_back(11); // certificate
769 put24(cert_msg, static_cast<unsigned>(cbody.size()));
770 cert_msg += cbody;
771 }
772 transcript += cert_msg;
773 if (!seal_record(fd, server_keys, 22, cert_msg)) {
774 fail("tls: cannot send Certificate"); // LCOV_EXCL_LINE: a mid-handshake socket write failure
775 return -1; // LCOV_EXCL_LINE
776 }
778 std::string cv_msg;
779 {
780 // Same signed content the client verifies: 64 spaces, the context string, a NUL, then the
781 // transcript hash through Certificate. Ed25519 signs the content directly; ECDSA P-256
782 // signs SHA-256(content) and travels as DER — the exact mirror of the client's verify
783 // branches for 0x0807/0x0403.
784 std::string signed_content(64, ' ');
785 signed_content += "TLS 1.3, server CertificateVerify";
786 signed_content.push_back('\0');
787 signed_content += hashlib::sha256_digest(transcript);
788 std::string sig;
789 if (cv_alg == 0x0807) {
790 sig = from_hex(ed25519::sign(to_hex(ed_seed), signed_content));
791 } else {
792 sig = p256::rs_to_der(
793 p256::sign_raw(ec_scalar, hashlib::sha256_digest(signed_content)));
794 if (sig.empty()) { // LCOV_EXCL_LINE: pre-flight proved the scalar derives the leaf's public key, so sign_raw cannot reject it
795 fail("tls: ECDSA signing failed (invalid P-256 private key scalar)"); // LCOV_EXCL_LINE
796 return -1; // LCOV_EXCL_LINE
797 }
798 }
799 std::string vbody;
800 put16(vbody, cv_alg);
801 put16(vbody, static_cast<unsigned>(sig.size()));
802 vbody += sig;
803 cv_msg.push_back(15); // certificate_verify
804 put24(cv_msg, static_cast<unsigned>(vbody.size()));
805 cv_msg += vbody;
806 }
807 transcript += cv_msg;
808 if (!seal_record(fd, server_keys, 22, cv_msg)) {
809 fail("tls: cannot send CertificateVerify"); // LCOV_EXCL_LINE: a mid-handshake socket write failure
810 return -1; // LCOV_EXCL_LINE
811 }
813 std::string fin_msg;
814 {
815 const std::string finished_key = expand_label_impl(s_hs, "finished", "", 32);
816 const std::string verify =
817 hashlib::hmac_sha256(finished_key, hashlib::sha256_digest(transcript));
818 fin_msg.push_back(20); // finished
819 put24(fin_msg, static_cast<unsigned>(verify.size()));
820 fin_msg += verify;
821 }
822 if (!seal_record(fd, server_keys, 22, fin_msg)) {
823 fail("tls: cannot send server Finished"); // LCOV_EXCL_LINE: a mid-handshake socket write failure
824 return -1; // LCOV_EXCL_LINE
825 }
826 transcript += fin_msg;
828 // Application traffic secrets (transcript through the server's Finished).
829 const std::string derived2 = derive_secret_impl(hs_secret, "derived", "");
830 const std::string master = hashlib::hkdf_extract(derived2, zeros);
831 const std::string c_ap = derive_secret_impl(master, "c ap traffic", transcript);
832 const std::string s_ap = derive_secret_impl(master, "s ap traffic", transcript);
834 // Client Finished (encrypted under the client handshake keys); verify its MAC over the
835 // transcript through the server Finished.
836 const std::string c_finished_key = expand_label_impl(c_hs, "finished", "", 32);
837 const std::string expect =
838 hashlib::hmac_sha256(c_finished_key, hashlib::sha256_digest(transcript));
839 bool client_finished = false;
840 while (!client_finished) {
841 if (!read_record(fd, buffer, rtype, payload)) {
842 fail("tls: connection closed before the client Finished");
843 return -1;
844 }
845 if (rtype == 20) continue; // ChangeCipherSpec compat
846 if (rtype == 21) {
847 fail("tls: client alert during the handshake — " + alert_text(payload));
848 return -1;
849 }
850 if (rtype != 23) {
851 fail("tls: unexpected plaintext record awaiting the client Finished");
852 return -1;
853 }
854 unsigned inner_type = 0;
855 std::string content;
856 if (!open_record(client_keys, payload, inner_type, content)) {
857 fail("tls: client Finished failed authentication");
858 return -1;
859 }
860 if (inner_type != 22 || content.size() < 4 || static_cast<unsigned char>(content[0]) != 20) {
861 fail("tls: expected a client Finished"); // LCOV_EXCL_LINE: a key-scheduled malicious client we do not mirror
862 return -1; // LCOV_EXCL_LINE
863 }
864 // cppcheck-suppress stlcstrConstructor // (ptr,len) subview of content — not a c_str() copy
865 const std::string_view got(content.data() + 4, content.size() - 4);
866 unsigned char diff = (got.size() == expect.size()) ? 0 : 1; // constant-time MAC compare
867 for (std::size_t i = 0; i < expect.size(); ++i) {
868 const unsigned char g = i < got.size() ? static_cast<unsigned char>(got[i]) : 0;
869 diff |= g ^ static_cast<unsigned char>(expect[i]);
870 }
871 if (diff != 0) {
872 fail("tls: client Finished MAC mismatch — handshake transcript tampered"); // LCOV_EXCL_LINE: a key-scheduled malicious client we do not mirror
873 return -1; // LCOV_EXCL_LINE
874 }
875 client_finished = true;
876 }
878 Session s;
879 s.fd = fd;
880 s.client_keys = traffic_keys(s_ap, aead, false); // our sending direction (server app secret)
881 s.server_keys = traffic_keys(c_ap, aead, false); // the peer's direction (client app secret)
882 s.read_buffer = std::move(buffer);
883 const std::lock_guard<std::mutex> lock(registry().mutex);
884 const long long handle = registry().next_handle++;
885 registry().sessions[handle] = std::move(s);
886 return handle;
889// ---- the handshake state machine -------------------------------------------------
891// The system CA trust store, parsed once and reused (loading ~150 CA certs on every handshake
892// would be wasteful). Thread-safe initialization via the C++ function-local static.
893const x509::TrustStore& default_trust() {
894 static const x509::TrustStore store = x509::load_trust("");
895 return store;
898long long handshake(long long fd, const std::string& server_name, bool insecure,
899 const std::string& ca_file) {
900 t_error.clear();
902 // Ephemeral X25519 key pair.
903 const std::string priv_raw = random_bytes(32);
904 if (priv_raw.size() != 32) {
905 fail("tls: system random unavailable");
906 return -1;
907 }
908 const std::string priv_hex = to_hex(priv_raw);
909 const std::string pub_hex = x25519::x25519_base(priv_hex);
910 const std::string pub_raw = from_hex(pub_hex);
912 std::string transcript; // concatenated handshake MESSAGES (no record headers)
913 const std::string client_hello = build_client_hello(server_name, pub_raw);
914 transcript += client_hello;
915 if (!write_record(fd, 22, client_hello)) {
916 fail("tls: cannot send ClientHello");
917 return -1;
918 }
920 // ServerHello (plaintext record, type 22; tolerate ChangeCipherSpec compat records).
921 std::string buffer, payload;
922 unsigned rtype = 0;
923 std::string server_pub_raw;
924 for (;;) {
925 if (!read_record(fd, buffer, rtype, payload)) {
926 fail("tls: connection closed before ServerHello");
927 return -1;
928 }
929 if (rtype == 20) continue; // ChangeCipherSpec (compat) — ignored
930 if (rtype == 21) {
931 fail("tls: server sent an alert instead of ServerHello — " + alert_text(payload));
932 return -1;
933 }
934 if (rtype != 22) {
935 fail("tls: unexpected record before ServerHello");
936 return -1;
937 }
938 break;
939 }
940 unsigned chosen_suite = 0;
941 if (!parse_server_hello(payload, server_pub_raw, chosen_suite)) {
942 fail("tls: malformed ServerHello (or the server refused TLS 1.3 + our cipher suites)");
943 return -1;
944 }
945 // The negotiated suite fixes both the record AEAD and the key-schedule hash.
946 const bool sha384 = (chosen_suite == 0x1302); // TLS_AES_256_GCM_SHA384 → SHA-384 schedule
947 Aead aead = Aead::Chacha20; // TLS_CHACHA20_POLY1305_SHA256 (0x1303)
948 if (chosen_suite == 0x1302) aead = Aead::Aes256;
949 else if (chosen_suite == 0x1301) aead = Aead::Aes128;
950 transcript += payload;
952 // Key schedule through the handshake secrets.
953 const std::string shared_hex = x25519::x25519(priv_hex, to_hex(server_pub_raw));
954 if (shared_hex.empty()) {
955 fail("tls: invalid server key share");
956 return -1;
957 }
958 const std::string zeros(sha384 ? 48 : 32, '\0'); // HashLen zero bytes for Extract
959 const std::string early = ks_extract(sha384, std::string(), zeros);
960 const std::string derived = derive_secret_impl(early, "derived", "", sha384);
961 const std::string hs_secret = ks_extract(sha384, derived, from_hex(shared_hex));
962 const std::string c_hs = derive_secret_impl(hs_secret, "c hs traffic", transcript, sha384);
963 const std::string s_hs = derive_secret_impl(hs_secret, "s hs traffic", transcript, sha384);
964 Keys client_keys = traffic_keys(c_hs, aead, sha384);
965 Keys server_keys = traffic_keys(s_hs, aead, sha384);
967 // Encrypted handshake flight: EncryptedExtensions, Certificate, CertificateVerify, Finished.
968 std::string handshake_bytes; // decrypted, possibly spanning records
969 std::string cert_der;
970 std::vector<std::string> cert_chain; // the full certificate chain (leaf first) for validation
971 bool verified_cert = false, server_finished = false;
972 bool cert_requested = false; // server sent CertificateRequest (mail servers do)
973 std::string cert_request_context; // echoed back in our (empty) Certificate reply
974 std::string transcript_at_cv, transcript_at_finished;
975 // Total-flight cap: the server's handshake messages accumulate into transcript / handshake_bytes /
976 // cert_chain BEFORE the certificate is validated, so a hostile (or MITM) server could otherwise
977 // stream unbounded records and exhaust memory pre-auth. A real TLS 1.3 flight — even a long cert
978 // chain of RSA-4096 leaves — is well under 256 KiB; cap there and fail closed beyond it.
979 constexpr std::size_t kMaxHandshakeFlight = std::size_t{256} * 1024;
980 std::size_t flight_bytes = 0;
981 while (!server_finished) {
982 if (!read_record(fd, buffer, rtype, payload)) {
983 fail("tls: connection closed during the handshake");
984 return -1;
985 }
986 if (rtype == 20) continue; // compat ChangeCipherSpec
987 if (rtype == 21) {
988 fail("tls: server alert during the handshake — " + alert_text(payload));
989 return -1;
990 }
991 if (rtype != 23) {
992 fail("tls: unexpected plaintext record during the encrypted handshake");
993 return -1;
994 }
995 unsigned inner_type = 0;
996 std::string content;
997 if (!open_record(server_keys, payload, inner_type, content)) {
998 fail("tls: handshake record failed authentication");
999 return -1;
1001 if (inner_type == 21) {
1002 fail("tls: server alert during the handshake — " + alert_text(content));
1003 return -1;
1005 if (inner_type != 22) {
1006 fail("tls: unexpected inner record type during the handshake");
1007 return -1;
1009 flight_bytes += content.size();
1010 if (flight_bytes > kMaxHandshakeFlight) {
1011 fail("tls: server handshake flight too large");
1012 return -1;
1014 handshake_bytes += content;
1016 // Drain complete handshake messages from the reassembly buffer.
1017 while (handshake_bytes.size() >= 4) {
1018 const unsigned mtype = static_cast<unsigned char>(handshake_bytes[0]);
1019 const unsigned mlen = get24(handshake_bytes, 1);
1020 if (handshake_bytes.size() < 4 + mlen) break;
1021 const std::string msg = handshake_bytes.substr(0, 4 + mlen);
1022 handshake_bytes.erase(0, 4 + mlen);
1024 if (mtype == 11 && msg.size() > 4 + 4 + 3 + 3) { // Certificate
1025 // certificate_request_context (1 byte, empty) + the cert list. Walk every
1026 // CertificateEntry (3-byte length | cert DER | 2-byte extensions | extensions)
1027 // so the FULL chain is available for path validation, not just the leaf.
1028 std::size_t i = 4 + 1 + 3; // header + context length byte + cert-list length
1029 while (i + 3 <= msg.size()) {
1030 const unsigned clen = get24(msg, i);
1031 i += 3;
1032 if (i + clen > msg.size()) break;
1033 cert_chain.push_back(msg.substr(i, clen));
1034 i += clen;
1035 if (i + 2 > msg.size()) break;
1036 i += 2 + get16(msg, i); // skip the per-certificate extensions
1038 if (!cert_chain.empty()) cert_der = cert_chain[0]; // the leaf signs CertificateVerify
1039 transcript_at_cv = transcript + msg; // transcript THROUGH Certificate
1041 if (mtype == 13 && msg.size() >= 5) { // CertificateRequest (optional client auth)
1042 // RFC 8446 §4.4.2: a client with no certificate MUST still answer with a
1043 // Certificate message whose certificate_list is empty, echoing this
1044 // context — smtp.gmail.com requests one and aborts (unexpected_message)
1045 // on a bare Finished. We never present a certificate; we just decline
1046 // correctly.
1047 cert_requested = true;
1048 const std::size_t ctx_len = static_cast<unsigned char>(msg[4]);
1049 if (msg.size() >= 5 + ctx_len) cert_request_context = msg.substr(5, ctx_len);
1051 if (mtype == 15) { // CertificateVerify
1052 // cppcheck-suppress stlcstrConstructor // (ptr,len) subview of msg — not a c_str() copy
1053 const std::string_view body(msg.data() + 4, msg.size() - 4);
1054 if (body.size() < 4) {
1055 fail("tls: malformed CertificateVerify");
1056 return -1;
1058 const unsigned alg = get16(body, 0);
1059 const unsigned sig_len = get16(body, 2);
1060 if (body.size() < 4 + sig_len) {
1061 fail("tls: malformed CertificateVerify");
1062 return -1;
1064 // The signed content (same for every algorithm): 64 spaces, the
1065 // context string, a NUL, then the handshake transcript hash.
1066 std::string signed_content(64, ' ');
1067 signed_content += "TLS 1.3, server CertificateVerify";
1068 signed_content.push_back('\0');
1069 signed_content += ks_digest(sha384, transcript_at_cv); // transcript hash = suite hash
1070 const std::string sig(body.substr(4, sig_len));
1071 if (alg == 0x0807) { // ed25519 (signs the message directly)
1072 const std::string spki = ed25519_spki_key(cert_der);
1073 if (spki.size() != 32) {
1074 fail("tls: certificate key is not Ed25519");
1075 return -1;
1077 if (!ed25519::verify(to_hex(spki), signed_content, to_hex(sig))) {
1078 fail("tls: server CertificateVerify signature is INVALID");
1079 return -1;
1081 } else if (alg == 0x0403) { // ecdsa_secp256r1_sha256 (signs SHA-256(content))
1082 const std::string point = p256::spki_ec_point(cert_der);
1083 if (point.size() != 64) {
1084 fail("tls: certificate key is not P-256 EC");
1085 return -1;
1087 if (!p256::verify_der(point, hashlib::sha256_digest(signed_content), sig)) {
1088 fail("tls: server CertificateVerify (ECDSA P-256) is INVALID");
1089 return -1;
1091 } else if (alg == 0x0503) { // ecdsa_secp384r1_sha384 (signs SHA-384(content))
1092 // The signature scheme's hash (SHA-384) is independent of the transcript hash
1093 // inside signed_content (which is the negotiated suite's hash) — RFC 8446 §4.4.3.
1094 const std::string point = p384::spki_ec_point(cert_der);
1095 if (point.size() != 96) {
1096 fail("tls: certificate key is not P-384 EC");
1097 return -1;
1099 if (!p384::verify_der(point, hashlib::sha384_digest(signed_content), sig)) {
1100 fail("tls: server CertificateVerify (ECDSA P-384) is INVALID");
1101 return -1;
1103 } else if (alg == 0x0804) { // rsa_pss_rsae_sha256 (RSA leaf certificate)
1104 // RSA-PSS verifies the signed_content directly (it hashes with SHA-256 internally),
1105 // using the RSA public key extracted from the leaf cert's SubjectPublicKeyInfo.
1106 if (!rsa::verify_pss_sha256(cert_der, signed_content, sig)) {
1107 fail("tls: server CertificateVerify (RSA-PSS SHA-256) is INVALID");
1108 return -1;
1110 } else { // LCOV_EXCL_LINE: reached only if a server sends a CertificateVerify whose algorithm ignores our advertised signature_algorithms — a non-conformant peer we don't mirror
1111 fail("tls: server certificate uses an algorithm cheatah cannot verify yet " // LCOV_EXCL_LINE
1112 "(Ed25519, ECDSA P-256/P-384, and RSA-PSS SHA-256 are supported) — refusing an "
1113 "unauthenticated connection");
1114 return -1;
1116 verified_cert = true;
1118 if (mtype == 20) { // Finished
1119 const std::string finished_key =
1120 expand_label_impl(s_hs, "finished", "", sha384 ? 48 : 32, sha384);
1121 const std::string expect =
1122 ks_hmac(sha384, finished_key, ks_digest(sha384, transcript));
1123 // cppcheck-suppress stlcstrConstructor // (ptr,len) subview of msg — not a c_str() copy
1124 const std::string_view got(msg.data() + 4, msg.size() - 4);
1125 // Constant-time MAC compare (parity with the AEAD tag check): always scan all
1126 // HashLen bytes of the secret `expect` (32 for SHA-256, 48 for SHA-384), never
1127 // early-exiting on a mismatching byte, so timing cannot reveal a partial match.
1128 unsigned char diff = (got.size() == expect.size()) ? 0 : 1;
1129 for (std::size_t i = 0; i < expect.size(); ++i) {
1130 const unsigned char g =
1131 i < got.size() ? static_cast<unsigned char>(got[i]) : 0;
1132 diff |= g ^ static_cast<unsigned char>(expect[i]);
1134 if (diff != 0) {
1135 fail("tls: server Finished MAC mismatch — handshake transcript tampered");
1136 return -1;
1138 transcript_at_finished = transcript + msg;
1139 server_finished = true;
1141 transcript += msg;
1144 if (!verified_cert) {
1145 fail("tls: server never proved possession of its certificate key");
1146 return -1;
1149 // AUTHENTICATE THE SERVER'S IDENTITY (unless the caller opted out of verification): build the
1150 // presented chain to a trusted CA, match the hostname against the leaf's SAN, and check the
1151 // validity dates. Key possession alone (above) does not prove identity — this is what stops an
1152 // active man-in-the-middle presenting any certificate.
1153 if (!insecure) {
1154 x509::TrustStore custom;
1155 const x509::TrustStore* store = &default_trust();
1156 if (!ca_file.empty()) {
1157 custom = x509::load_trust(ca_file);
1158 store = &custom;
1160 std::string verr;
1161 if (!x509::validate(cert_chain, server_name, *store, static_cast<long long>(std::time(nullptr)),
1162 verr)) {
1163 fail("tls: certificate validation failed — " + verr);
1164 return -1;
1168 // Application traffic secrets (transcript through server Finished), then OUR Finished
1169 // (sent under the handshake keys, with the transcript through the server's Finished).
1170 const std::string derived2 = derive_secret_impl(hs_secret, "derived", "", sha384);
1171 const std::string master = ks_extract(sha384, derived2, zeros);
1172 const std::string c_ap = derive_secret_impl(master, "c ap traffic", transcript_at_finished, sha384);
1173 const std::string s_ap = derive_secret_impl(master, "s ap traffic", transcript_at_finished, sha384);
1175 // A requested-but-absent client certificate: the empty Certificate reply goes on the
1176 // wire AND into the transcript BEFORE our Finished (whose MAC covers it) — RFC 8446
1177 // §4.4.2/§4.4.4. No CertificateVerify follows an empty list.
1178 if (cert_requested) {
1179 std::string cert_body;
1180 cert_body.push_back(static_cast<char>(cert_request_context.size()));
1181 cert_body += cert_request_context;
1182 put24(cert_body, 0); // empty certificate_list
1183 std::string cert_msg;
1184 cert_msg.push_back(11);
1185 put24(cert_msg, static_cast<unsigned>(cert_body.size()));
1186 cert_msg += cert_body;
1187 if (!seal_record(fd, client_keys, 22, cert_msg)) {
1188 fail("tls: cannot send the (empty) client Certificate");
1189 return -1;
1191 transcript += cert_msg;
1194 const std::string c_finished_key =
1195 expand_label_impl(c_hs, "finished", "", sha384 ? 48 : 32, sha384);
1196 const std::string verify = ks_hmac(sha384, c_finished_key, ks_digest(sha384, transcript));
1197 std::string fin_msg;
1198 fin_msg.push_back(20);
1199 put24(fin_msg, static_cast<unsigned>(verify.size()));
1200 fin_msg += verify;
1201 if (!seal_record(fd, client_keys, 22, fin_msg)) {
1202 fail("tls: cannot send client Finished");
1203 return -1;
1206 Session s;
1207 s.fd = fd;
1208 s.client_keys = traffic_keys(c_ap, aead, sha384);
1209 s.server_keys = traffic_keys(s_ap, aead, sha384);
1210 s.read_buffer = std::move(buffer); // bytes already pulled off the socket stay with us
1212 const std::lock_guard<std::mutex> lock(registry().mutex);
1213 const long long handle = registry().next_handle++;
1214 registry().sessions[handle] = std::move(s);
1215 return handle;
1218} // namespace
1220/// @cond INTERNAL
1221/// the C++-only low-level session API (tls_lowlevel.hpp); cheatah uses the Conn guard
1222long long client_connect(long long fd, const std::string& server_name, bool insecure,
1223 const std::string& ca_file) {
1224 return handshake(fd, server_name, insecure, ca_file);
1227long long server_accept(long long fd, const std::string& cert_pem, const std::string& key_pem) {
1228 return server_handshake(fd, cert_pem, key_pem);
1231long long send(long long session, const std::string& data) {
1232 t_error.clear();
1233 // Lock ONLY for the map lookup (see recv): the socket write below runs without
1234 // the global lock so concurrent sessions don't serialize on each other.
1235 Session* sp = nullptr;
1237 const std::lock_guard<std::mutex> lock(registry().mutex);
1238 const auto it = registry().sessions.find(session);
1239 if (it == registry().sessions.end() || it->second.closed) {
1240 fail("tls: unknown or closed session");
1241 return -1;
1243 sp = &it->second;
1245 Session& s = *sp;
1246 // Respect the 16 KiB record plaintext bound.
1247 std::string_view rest = data;
1248 while (!rest.empty()) {
1249 const std::size_t n = std::min<std::size_t>(rest.size(), 16384);
1250 if (!seal_record(s.fd, s.client_keys, 23, rest.substr(0, n))) {
1251 fail("tls: send failed");
1252 return -1;
1254 rest.remove_prefix(n);
1256 return 0;
1259std::string recv(long long session, long long bufsize) {
1260 t_error.clear();
1261 if (bufsize <= 0) return "";
1262 // Guard ONLY the map lookup. The per-session buffers (read_buffer/app_pending)
1263 // and socket are owned by this session's single reader thread, so the blocking
1264 // record I/O below runs WITHOUT the global lock — otherwise one session's
1265 // blocking recv would serialize (and at shutdown, starve) every other session's
1266 // recv/send. A std::map node address is stable until that node is erased, and a
1267 // session is erased only by its own owner (after this loop), so the pointer is
1268 // valid for this call. (registry().mutex still serializes find/insert/erase on the map.)
1269 Session* sp = nullptr;
1271 const std::lock_guard<std::mutex> lock(registry().mutex);
1272 const auto it = registry().sessions.find(session);
1273 if (it == registry().sessions.end()) {
1274 fail("tls: unknown session");
1275 return "";
1277 sp = &it->second;
1279 Session& s = *sp;
1280 // Drain up to `bufsize` of application data. We block (in read_record) ONLY while we have
1281 // nothing to hand back; once app_pending holds data we keep going solely to consume records
1282 // ALREADY buffered (has_complete_record) — never adding a blocking wait. Because read_record
1283 // now pulls 64 KiB per recv, one blocking read typically delivers several records, all drained
1284 // here into a single ≥16 KB return to requests — which keeps the socket drained and the
1285 // receive window open instead of the old one-record-per-call stall.
1286 while (!s.closed) {
1287 if (!s.app_pending.empty() &&
1288 (s.app_pending.size() >= static_cast<std::size_t>(bufsize) ||
1289 !has_complete_record(s.read_buffer))) {
1290 break; // enough to return, and nothing more ready without blocking
1292 unsigned rtype = 0;
1293 std::string payload;
1294 if (!read_record(s.fd, s.read_buffer, rtype, payload)) {
1295 s.closed = true; // peer EOF (or socket timeout) — surfaced as ""
1296 break;
1298 if (rtype == 20) continue; // stray compat ChangeCipherSpec
1299 if (rtype == 21) { // plaintext alert (illegal post-handshake, but final)
1300 s.closed = true;
1301 break;
1303 if (rtype != 23) continue; // ignore anything else
1304 unsigned inner_type = 0;
1305 std::string content;
1306 if (!open_record(s.server_keys, payload, inner_type, content)) {
1307 fail("tls: record failed authentication");
1308 s.closed = true;
1309 break;
1311 if (inner_type == 23) {
1312 s.app_pending += content;
1313 } else if (inner_type == 21) { // alert ends the stream: close_notify is the
1314 // normal clean close (surfaced as plain EOF); anything else is the peer
1315 // REFUSING the session — name it, so a fatal alert never masquerades as EOF.
1316 if (content.size() != 2 || static_cast<unsigned char>(content[1]) != 0) {
1317 fail("tls: peer alert — " + alert_text(content));
1319 s.closed = true;
1320 } else if (inner_type == 22) {
1321 // Post-handshake messages: NewSessionTicket(4) is ignored; a KeyUpdate(24)
1322 // would change the peer's keys — unsupported, so end the stream rather than
1323 // silently fail to decrypt what follows.
1324 if (!content.empty() && static_cast<unsigned char>(content[0]) == 24) {
1325 fail("tls: peer KeyUpdate is not supported");
1326 s.closed = true;
1330 const std::size_t n = std::min<std::size_t>(s.app_pending.size(),
1331 static_cast<std::size_t>(bufsize));
1332 if (n == s.app_pending.size()) {
1333 std::string out = std::move(s.app_pending); // whole buffer → move, no copy
1334 s.app_pending.clear();
1335 return out;
1337 std::string out = s.app_pending.substr(0, n);
1338 s.app_pending.erase(0, n);
1339 return out;
1342long long close(long long session) {
1343 t_error.clear();
1344 const std::lock_guard<std::mutex> lock(registry().mutex);
1345 const auto it = registry().sessions.find(session);
1346 if (it == registry().sessions.end()) return -1;
1347 if (!it->second.closed) {
1348 const std::string close_notify = {1, 0}; // warning, close_notify
1349 seal_record(it->second.fd, it->second.client_keys, 21, close_notify);
1351 registry().sessions.erase(it);
1352 return 0;
1355long long shutdown(long long session) {
1356 t_error.clear();
1357 const std::lock_guard<std::mutex> lock(registry().mutex);
1358 const auto it = registry().sessions.find(session);
1359 if (it == registry().sessions.end()) return -1;
1360 // Wake a reader blocked in recv() WITHOUT erasing the session (that stays the
1361 // owner's job via close(), after it has joined the reader). Just half-close the
1362 // socket so the blocking recv returns EOF.
1363 return socket::shutdown(it->second.fd);
1365/// @endcond
1367std::string last_error() { return t_error; }
1369// ---- owning RAII session ----
1370// Each method forwards to the handle-based free function above; the guard adds deterministic
1371// close() (close_notify + session erase) on scope exit, so a `with` block cannot leak.
1373Conn& Conn::operator=(Conn&& other) noexcept {
1374 if (this != &other) {
1375 if (session_ > 0) cheatah::tls::close(session_);
1376 session_ = other.session_;
1377 other.session_ = 0;
1379 return *this;
1381Conn::~Conn() {
1382 if (session_ > 0) cheatah::tls::close(session_);
1384long long Conn::send(const std::string& data) const { return cheatah::tls::send(session_, data); }
1385std::string Conn::recv(long long bufsize) const { return cheatah::tls::recv(session_, bufsize); }
1386long long Conn::shutdown() const { return cheatah::tls::shutdown(session_); }
1387long long Conn::close() {
1388 if (session_ <= 0) return -1;
1389 const long long rc = cheatah::tls::close(session_);
1390 session_ = 0;
1391 return rc;
1393Conn open(long long fd, const std::string& server_name, bool insecure, const std::string& ca_file) {
1394 return Conn(client_connect(fd, server_name, insecure, ca_file));
1396Conn accept(long long fd, const std::string& cert_pem, const std::string& key_pem) {
1397 return Conn(server_handshake(fd, cert_pem, key_pem));
1400namespace detail {
1401// Cipher preference follows OUR fastest cipher, exactly as OpenSSL/curl do: with AES-NI +
1402// PCLMULQDQ present, AES-GCM runs at multi-GB/s hardware speed and beats our scalar ChaCha20, so
1403// offer AES-GCM FIRST; without hardware AES (some VMs/ARM), scalar ChaCha20 is the faster path, so
1404// lead with it. The server picks from our order when it honors client preference — which is what
1405// turns a ChaCha-negotiated ~200 MB/s link into a ~320 MB/s AES-GCM one.
1407// Split out and taking the decision as a PARAMETER rather than calling crypto_hardware_active()
1408// inline, so both orders are reachable from a test on any host. Inline, the branch not matching the
1409// build machine's CPU was dead code no test could ever execute — the ordering is a wire-format
1410// decision and deserves to be pinned on every machine, not only on ARM.
1411void append_cipher_preference(std::string& body, bool hardware_aes) {
1412 if (hardware_aes) {
1413 put16(body, 0x1302); // TLS_AES_256_GCM_SHA384 (hardware AES-NI — preferred)
1414 put16(body, 0x1301); // TLS_AES_128_GCM_SHA256 (hardware AES-NI)
1415 put16(body, 0x1303); // TLS_CHACHA20_POLY1305_SHA256 (fallback)
1416 } else {
1417 put16(body, 0x1303); // TLS_CHACHA20_POLY1305_SHA256 (no AES-NI — scalar ChaCha wins)
1418 put16(body, 0x1301); // TLS_AES_128_GCM_SHA256
1419 put16(body, 0x1302); // TLS_AES_256_GCM_SHA384
1424std::string expand_label(std::string_view secret, std::string_view label,
1425 std::string_view context, unsigned length) {
1426 return cheatah::tls::expand_label_impl(secret, label, context, length);
1428std::string derive_secret(std::string_view secret, std::string_view label,
1429 std::string_view transcript) {
1430 return cheatah::tls::derive_secret_impl(secret, label, transcript);
1432bool parse_client_hello(std::string_view msg, std::string& client_pub_raw, unsigned& chosen_suite,
1433 std::string& session_id, std::string& sig_algs) {
1434 return cheatah::tls::parse_client_hello(msg, client_pub_raw, chosen_suite, session_id,
1435 sig_algs);
1437std::string pem_block(const std::string& pem, const std::string& label) {
1438 return cheatah::tls::pem_block(pem, label);
1440std::vector<std::string> pem_blocks(const std::string& pem, const std::string& label) {
1441 return cheatah::tls::pem_blocks(pem, label);
1443std::string ed25519_seed_from_pkcs8(std::string_view der) {
1444 return cheatah::tls::ed25519_seed_from_pkcs8(der);
1446std::string ec_p256_scalar_from_pem(const std::string& key_pem) {
1447 return cheatah::tls::ec_p256_scalar_from_pem(key_pem);
1449std::string build_client_hello(const std::string& server_name, std::string_view pub_raw) {
1450 return cheatah::tls::build_client_hello(server_name, pub_raw);
1452} // namespace detail
1454} // namespace cheatah::tls