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