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 key17
#include "aead.hpp" // chacha20poly1305_{en,de}crypt — the record cipher18
#include "ed25519.hpp" // verify — CertificateVerify for Ed25519 server certs19
#include "hashlib.hpp" // sha256_digest, hmac_sha256, hkdf_extract/expand — the key schedule20
#include "p256.hpp" // verify — CertificateVerify for ECDSA P-256 server certs21
#include "p384.hpp" // verify — CertificateVerify for ECDSA P-384 server certs22
#include "rsa_verify.hpp" // verify_pss_sha256 — CertificateVerify for RSA (rsa_pss_rsae_sha256) certs23
#include "socket.hpp" // raw fd I/O underneath the record layer24
#include "x25519.hpp" // the key exchange25
#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 HKDF30
// key schedule, then the handshake state machine. Every secret derives through hashlib's31
// HKDF; every record seals/opens through the aead module; the ephemeral key is x25519.33
namespace cheatah::tls {34
namespace {36
namespace sock = cheatah::socket;38
thread_local std::string t_error; // last_error() text for this thread40
void 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 lowercase44
// x25519 hex, so the canonical from_hex's odd-length/non-hex throws are never reached here.45
using hashlib::to_hex; // bytes -> lowercase hex (string_view / (uint8_t*, n) overloads).46
using hashlib::from_hex; // hex -> bytes (throws on odd length / non-hex).48
// 16/24-bit big-endian helpers for the wire format.49
void put16(std::string& out, unsigned v) {50
out.push_back(static_cast<char>(v >> 8));51
out.push_back(static_cast<char>(v));52
}53
void 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));57
}58
unsigned 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]);60
}61
unsigned 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]);64
}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.ExpandLabel82
*/83
std::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);94
}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.KeySchedule107
*/108
std::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);113
}115
namespace { // resume the file-local helpers117
// The negotiated record cipher: ChaCha20-Poly1305 (0x1303), AES-128-GCM (0x1301), or AES-256-GCM (0x1302).118
enum class Aead { Chacha20, Aes128, Aes256 };120
// Key-schedule hash dispatch: the SHA-256 schedule by default, the SHA-384 schedule for the121
// TLS_AES_256_GCM_SHA384 suite. (RFC 8446 §7.1: the schedule's Hash is the cipher suite's hash.)122
std::string ks_digest(bool sha384, std::string_view d) {123
return sha384 ? hashlib::sha384_digest(d) : hashlib::sha256_digest(d);124
}125
std::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);127
}128
std::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);130
}132
// One traffic direction: AEAD key + iv + record sequence number.133
struct Keys {134
std::string key_hex; // AEAD key (hex): 32 bytes for ChaCha20 / AES-256-GCM, 16 for AES-128-GCM135
std::string iv; // 12-byte raw iv; per-record nonce = iv XOR seq136
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).142
Keys 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;148
}150
// The per-record nonce: the 12-byte iv with the 8-byte big-endian sequence XORed into its tail.151
std::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);158
}160
// ---- one TLS session ----------------------------------------------------------162
struct Session {163
long long fd = -1;164
Keys client_keys; // our sending direction165
Keys server_keys; // the peer's direction166
std::string read_buffer; // raw bytes from the socket not yet framed into records167
std::string app_pending; // decrypted application data not yet handed to recv()168
bool closed = false; // close_notify seen (either direction)169
};171
std::mutex g_mutex;172
std::map<long long, Session> g_sessions;173
long 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 them180
// buffered, which (with the socket's enlarged SO_RCVBUF) keeps the receive window open instead of181
// stalling one record per round-trip.182
constexpr long long kRecvChunk = 65536;184
bool 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 malformed193
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;203
}205
// True when `buffer` already holds at least one COMPLETE record — used by the drain loop to keep206
// decrypting from bytes already in hand without blocking on another recv().207
bool 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;213
}215
bool 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_version219
put16(rec, static_cast<unsigned>(payload.size()));220
rec += payload;221
return sock::sendall(fd, rec) == 0;222
}224
// Seal one application_data record (RFC 8446 §5.2): inner plaintext = content || content_type,225
// AAD = the record header, then ChaCha20-Poly1305.226
bool 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;240
}242
// Open one encrypted record: returns the inner content and type, false on AEAD failure.243
bool 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 padding255
if (inner.empty()) return false; // a record must carry a content type256
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 it259
return true;260
}262
// ---- handshake construction -----------------------------------------------------264
std::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 means268
// 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;276
}278
// Build the ClientHello handshake MESSAGE (no record header). Fills `client_hello_random`.279
std::string build_client_hello(const std::string& server_name, std::string_view pub_raw) {280
std::string body;281
put16(body, 0x0303); // legacy_version282
body += random_bytes(32); // random283
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 faster288
// 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_methods293
body.push_back(0); // null295
std::string ext;296
{ // server_name (0)297
std::string names;298
names.push_back(0); // host_name299
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 only309
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 servers317
// complete the handshake far enough for our explicit refusal to be diagnosable318
std::string a;319
put16(a, 8);320
put16(a, 0x0807); // ed25519321
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.3329
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 key337
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_hello353
put24(msg, static_cast<unsigned>(body.size()));354
msg += body;355
return msg;356
}358
// Human-readable TLS alert (RFC 8446 §6) from the 2 alert bytes — so a handshake refusal names its359
// cause (e.g. 40 handshake_failure = no common cipher/group; 70 protocol_version = no TLS 1.3).360
std::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 + ")";389
}391
// Parse ServerHello: confirm TLS 1.3 + one of our suites, extract the server's X25519 key share and392
// the CHOSEN cipher suite (0x1303 ChaCha20-Poly1305 or 0x1301 AES-128-GCM).393
bool 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_hello395
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 + random398
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" rejection404
chosen_suite = suite;405
i += 2 + 1; // suite + legacy_compression406
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;424
}426
// Extract the Ed25519 public key from the leaf certificate's SubjectPublicKeyInfo: the DER427
// pattern 30 05 06 03 2B 65 70 (AlgorithmIdentifier { id-Ed25519 }) followed by428
// 03 21 00 <32-byte key> (BIT STRING). Returns "" when the cert key is not Ed25519.429
std::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 mirror438
}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.444
std::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);453
}455
// The 32-byte Ed25519 seed from a PKCS#8 private key DER: the id-Ed25519 AlgorithmIdentifier456
// (30 05 06 03 2B 65 70) followed by 04 22 04 20 (OCTET STRING { OCTET STRING[32] }) and the seed.457
std::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 "";466
}468
// EVERY PEM block under @p label, in order — the server's Certificate message must carry the whole469
// chain (leaf first, then intermediates), so a Let's Encrypt fullchain.pem yields N entries here470
// where pem_block() alone would silently drop everything after the leaf and browsers would reject471
// the path. A malformed block (bad base64) poisons the whole read: better no chain than a hole.472
std::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;489
}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 SEC1493
// 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 OID495
// (2A 86 48 CE 3D 03 01 07) — required here so a P-384/other-curve key is refused instead of496
// misread. Same pattern-scan discipline as ed25519_seed_from_pkcs8 above.497
std::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 "";514
}516
// Parse a ClientHello: choose a cipher suite we support (ChaCha20 preferred), extract the client's517
// 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 stays519
// 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 a521
// parse question.522
bool 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 parse525
session_id.clear();526
sig_algs.clear();527
if (msg.size() < 4 || static_cast<unsigned char>(msg[0]) != 1) return false; // client_hello528
std::string_view b = msg.substr(4);529
std::size_t i = 2 + 32; // legacy_version + random530
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 verbatim535
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 common550
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 0x0304564
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 list570
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;591
}593
// Build a ServerHello: echo the client's session id, our chosen suite, our X25519 key share.594
std::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_version598
body += random_bytes(32); // random599
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_suite602
body.push_back(0); // legacy_compression_method (null)604
std::string ext;605
put16(ext, 43); // supported_versions: TLS 1.3606
put16(ext, 2);607
put16(ext, 0x0304);608
{ // key_share (51): our X25519 KeyShareEntry609
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_hello622
put24(msg, static_cast<unsigned>(body.size()));623
msg += body;624
return msg;625
}627
// The TLS 1.3 SERVER handshake over connected fd @p fd, presenting @p cert_pem (an Ed25519 or628
// ECDSA P-256 leaf — @p cert_pem may be a fullchain.pem, and every block is sent) and proving629
// 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 SIGN631
// CertificateVerify instead of verifying it, and send the certificate flight. Returns a session632
// handle (>= 1) or -1 (see last_error()).633
long 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 from644
// the private half and comparing it to the leaf's SPKI catches a mixed-up cert/key pair at645
// startup with a precise message, instead of as an opaque CertificateVerify failure on the646
// first client. Exactly one signature algorithm follows from the key type — there is no647
// 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; // ed25519658
} 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_sha256665
} 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 in692
// signature_algorithms (§4.2.3 makes the extension mandatory for certificate auth). Our693
// 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; // ClientHello706
// 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 host710
return -1; // LCOV_EXCL_LINE711
}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 failure718
return -1; // LCOV_EXCL_LINE719
}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 secret735
Keys client_keys = traffic_keys(c_hs, aead, false); // we RECEIVE under the client secret737
// 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_extensions741
put24(ee, 2);742
put16(ee, 0); // extensions: empty743
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 failure746
return -1; // LCOV_EXCL_LINE747
}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 anchor753
put24(entries, static_cast<unsigned>(der.size()));754
entries += der;755
put16(entries, 0); // per-cert extensions: none756
}757
std::string cbody;758
cbody.push_back(0); // certificate_request_context: empty759
put24(cbody, static_cast<unsigned>(entries.size()));760
cbody += entries;761
cert_msg.push_back(11); // certificate762
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 failure768
return -1; // LCOV_EXCL_LINE769
}771
std::string cv_msg;772
{773
// Same signed content the client verifies: 64 spaces, the context string, a NUL, then the774
// transcript hash through Certificate. Ed25519 signs the content directly; ECDSA P-256775
// signs SHA-256(content) and travels as DER — the exact mirror of the client's verify776
// 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 it788
fail("tls: ECDSA signing failed (invalid P-256 private key scalar)"); // LCOV_EXCL_LINE789
return -1; // LCOV_EXCL_LINE790
}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_verify797
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 failure803
return -1; // LCOV_EXCL_LINE804
}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); // finished812
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 failure817
return -1; // LCOV_EXCL_LINE818
}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 the828
// 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 compat839
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 mirror855
return -1; // LCOV_EXCL_LINE856
}857
// cppcheck-suppress stlcstrConstructor // (ptr,len) subview of content — not a c_str() copy858
const std::string_view got(content.data() + 4, content.size() - 4);859
unsigned char diff = (got.size() == expect.size()) ? 0 : 1; // constant-time MAC compare860
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 mirror866
return -1; // LCOV_EXCL_LINE867
}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;880
}882
// ---- the handshake state machine -------------------------------------------------884
// The system CA trust store, parsed once and reused (loading ~150 CA certs on every handshake885
// would be wasteful). Thread-safe initialization via the C++ function-local static.886
const x509::TrustStore& default_trust() {887
static const x509::TrustStore store = x509::load_trust("");888
return store;889
}891
long 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) — ignored923
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 schedule940
const Aead aead = chosen_suite == 0x1302 ? Aead::Aes256941
: chosen_suite == 0x1301 ? Aead::Aes128942
: 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 Extract952
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 records962
std::string cert_der;963
std::vector<std::string> cert_chain; // the full certificate chain (leaf first) for validation964
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 reply967
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 otherwise970
// stream unbounded records and exhaust memory pre-auth. A real TLS 1.3 flight — even a long cert971
// 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 ChangeCipherSpec980
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;1001
}1002
flight_bytes += content.size();1003
if (flight_bytes > kMaxHandshakeFlight) {1004
fail("tls: server handshake flight too large");1005
return -1;1006
}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) { // Certificate1018
// certificate_request_context (1 byte, empty) + the cert list. Walk every1019
// 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 length1022
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 extensions1030
}1031
if (!cert_chain.empty()) cert_der = cert_chain[0]; // the leaf signs CertificateVerify1032
transcript_at_cv = transcript + msg; // transcript THROUGH Certificate1033
}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 a1036
// Certificate message whose certificate_list is empty, echoing this1037
// context — smtp.gmail.com requests one and aborts (unexpected_message)1038
// on a bare Finished. We never present a certificate; we just decline1039
// 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);1043
}1044
if (mtype == 15) { // CertificateVerify1045
// cppcheck-suppress stlcstrConstructor // (ptr,len) subview of msg — not a c_str() copy1046
const std::string_view body(msg.data() + 4, msg.size() - 4);1047
if (body.size() < 4) {1048
fail("tls: malformed CertificateVerify");1049
return -1;1050
}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;1056
}1057
// The signed content (same for every algorithm): 64 spaces, the1058
// 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 hash1063
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;1069
}1070
if (!ed25519::verify(to_hex(spki), signed_content, to_hex(sig))) {1071
fail("tls: server CertificateVerify signature is INVALID");1072
return -1;1073
}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;1079
}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;1083
}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 hash1086
// 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;1091
}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;1095
}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;1102
}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 mirror1104
fail("tls: server certificate uses an algorithm cheatah cannot verify yet " // LCOV_EXCL_LINE1105
"(Ed25519, ECDSA P-256/P-384, and RSA-PSS SHA-256 are supported) — refusing an "1106
"unauthenticated connection");1107
return -1;1108
}1109
verified_cert = true;1110
}1111
if (mtype == 20) { // Finished1112
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() copy1117
const std::string_view got(msg.data() + 4, msg.size() - 4);1118
// Constant-time MAC compare (parity with the AEAD tag check): always scan all1119
// HashLen bytes of the secret `expect` (32 for SHA-256, 48 for SHA-384), never1120
// 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]);1126
}1127
if (diff != 0) {1128
fail("tls: server Finished MAC mismatch — handshake transcript tampered");1129
return -1;1130
}1131
transcript_at_finished = transcript + msg;1132
server_finished = true;1133
}1134
transcript += msg;1135
}1136
}1137
if (!verified_cert) {1138
fail("tls: server never proved possession of its certificate key");1139
return -1;1140
}1142
// AUTHENTICATE THE SERVER'S IDENTITY (unless the caller opted out of verification): build the1143
// presented chain to a trusted CA, match the hostname against the leaf's SAN, and check the1144
// validity dates. Key possession alone (above) does not prove identity — this is what stops an1145
// 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;1152
}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;1158
}1159
}1161
// Application traffic secrets (transcript through server Finished), then OUR Finished1162
// (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 the1169
// wire AND into the transcript BEFORE our Finished (whose MAC covers it) — RFC 84461170
// §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_list1176
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;1183
}1184
transcript += cert_msg;1185
}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;1197
}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 us1205
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;1209
}1211
} // namespace1213
/// @cond INTERNAL — the C++-only low-level session API (tls_lowlevel.hpp); cheatah uses the Conn guard1214
long 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);1217
}1219
long 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);1221
}1223
long 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 without1226
// the global lock so concurrent sessions don't serialize on each other.1227
Session* sp = nullptr;1228
{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;1234
}1235
sp = &it->second;1236
}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;1245
}1246
rest.remove_prefix(n);1247
}1248
return 0;1249
}1251
std::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 blocking1256
// record I/O below runs WITHOUT the global lock — otherwise one session's1257
// blocking recv would serialize (and at shutdown, starve) every other session's1258
// recv/send. A std::map node address is stable until that node is erased, and a1259
// session is erased only by its own owner (after this loop), so the pointer is1260
// valid for this call. (g_mutex still serializes find/insert/erase on the map.)1261
Session* sp = nullptr;1262
{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 "";1268
}1269
sp = &it->second;1270
}1271
Session& s = *sp;1272
// Drain up to `bufsize` of application data. We block (in read_record) ONLY while we have1273
// nothing to hand back; once app_pending holds data we keep going solely to consume records1274
// ALREADY buffered (has_complete_record) — never adding a blocking wait. Because read_record1275
// now pulls 64 KiB per recv, one blocking read typically delivers several records, all drained1276
// here into a single ≥16 KB return to requests — which keeps the socket drained and the1277
// 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 blocking1283
}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;1289
}1290
if (rtype == 20) continue; // stray compat ChangeCipherSpec1291
if (rtype == 21) { // plaintext alert (illegal post-handshake, but final)1292
s.closed = true;1293
break;1294
}1295
if (rtype != 23) continue; // ignore anything else1296
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;1302
}1303
if (inner_type == 23) {1304
s.app_pending += content;1305
} else if (inner_type == 21) { // alert ends the stream: close_notify is the1306
// normal clean close (surfaced as plain EOF); anything else is the peer1307
// 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));1310
}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 than1315
// 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;1319
}1320
}1321
}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 copy1326
s.app_pending.clear();1327
return out;1328
}1329
std::string out = s.app_pending.substr(0, n);1330
s.app_pending.erase(0, n);1331
return out;1332
}1334
long 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_notify1341
seal_record(it->second.fd, it->second.client_keys, 21, close_notify);1342
}1343
g_sessions.erase(it);1344
return 0;1345
}1347
long 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 the1353
// owner's job via close(), after it has joined the reader). Just half-close the1354
// socket so the blocking recv returns EOF.1355
return socket::shutdown(it->second.fd);1356
}1357
/// @endcond1359
std::string last_error() { return t_error; }1361
// ---- owning RAII session ----1362
// Each method forwards to the handle-based free function above; the guard adds deterministic1363
// close() (close_notify + session erase) on scope exit, so a `with` block cannot leak.1365
Conn& 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;1370
}1371
return *this;1372
}1373
Conn::~Conn() {1374
if (session_ > 0) cheatah::tls::close(session_);1375
}1376
long long Conn::send(const std::string& data) { return cheatah::tls::send(session_, data); }1377
std::string Conn::recv(long long bufsize) { return cheatah::tls::recv(session_, bufsize); }1378
long long Conn::shutdown() { return cheatah::tls::shutdown(session_); }1379
long long Conn::close() {1380
if (session_ <= 0) return -1;1381
const long long rc = cheatah::tls::close(session_);1382
session_ = 0;1383
return rc;1384
}1385
Conn 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));1387
}1388
Conn accept(long long fd, const std::string& cert_pem, const std::string& key_pem) {1389
return Conn(server_handshake(fd, cert_pem, key_pem));1390
}1392
namespace 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, so1395
// offer AES-GCM FIRST; without hardware AES (some VMs/ARM), scalar ChaCha20 is the faster path, so1396
// lead with it. The server picks from our order when it honors client preference — which is what1397
// turns a ChaCha-negotiated ~200 MB/s link into a ~320 MB/s AES-GCM one.1398
//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 the1401
// build machine's CPU was dead code no test could ever execute — the ordering is a wire-format1402
// decision and deserves to be pinned on every machine, not only on ARM.1403
void 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_SHA2561411
put16(body, 0x1302); // TLS_AES_256_GCM_SHA3841412
}1413
}1416
std::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);1419
}1420
std::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);1423
}1424
bool 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);1428
}1429
std::string pem_block(const std::string& pem, const std::string& label) {1430
return cheatah::tls::pem_block(pem, label);1431
}1432
std::vector<std::string> pem_blocks(const std::string& pem, const std::string& label) {1433
return cheatah::tls::pem_blocks(pem, label);1434
}1435
std::string ed25519_seed_from_pkcs8(std::string_view der) {1436
return cheatah::tls::ed25519_seed_from_pkcs8(der);1437
}1438
std::string ec_p256_scalar_from_pem(const std::string& key_pem) {1439
return cheatah::tls::ec_p256_scalar_from_pem(key_pem);1440
}1441
std::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);1443
}1444
} // namespace detail1446
} // namespace cheatah::tls