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; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables): the per-thread last_error() slot IS the documented error channel40
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/hash⌉ · (|secret| + |label| + |context| + hash)) — one HMAC per output block.80
* @alloc the returned key material, the HkdfLabel info string, and HKDF's per-block HMAC scratch.81
* @test CheatahTls.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 : std::uint8_t { 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
// The process-wide session table: handle → Session, behind one mutex.172
struct Registry {173
std::mutex mutex;174
std::map<long long, Session> sessions;175
long long next_handle = 1;176
};177
Registry& registry() {178
static Registry r;179
return r;180
}182
// ---- record I/O ---------------------------------------------------------------184
// Read exactly one TLS record (header + payload) from the socket into (type, payload).185
// Blocking, bounded by the fd's socket timeout. False on EOF/short read.186
// The socket read chunk. 64 KiB drains several TLS records per syscall when the kernel has them187
// buffered, which (with the socket's enlarged SO_RCVBUF) keeps the receive window open instead of188
// stalling one record per round-trip.189
constexpr long long kRecvChunk = 65536;191
bool read_record(long long fd, std::string& buffer, unsigned& type, std::string& payload) {192
while (buffer.size() < 5) {193
const std::string chunk = sock::recv(fd, kRecvChunk);194
if (chunk.empty()) return false;195
buffer += chunk;196
}197
type = static_cast<unsigned char>(buffer[0]);198
const unsigned len = get16(buffer, 3);199
if (len > 16384 + 256) { // RFC bound + AEAD overhead: anything bigger is malformed200
return false;201
}202
while (buffer.size() < 5 + len) {203
const std::string chunk = sock::recv(fd, kRecvChunk);204
if (chunk.empty()) return false;205
buffer += chunk;206
}207
payload = buffer.substr(5, len);208
buffer.erase(0, 5 + len);209
return true;210
}212
// True when `buffer` already holds at least one COMPLETE record — used by the drain loop to keep213
// decrypting from bytes already in hand without blocking on another recv().214
bool has_complete_record(const std::string& buffer) {215
if (buffer.size() < 5) {216
return false;217
}218
const unsigned len = get16(buffer, 3);219
return buffer.size() >= static_cast<std::size_t>(5) + len;220
}222
bool write_record(long long fd, unsigned type, std::string_view payload) {223
std::string rec;224
rec.push_back(static_cast<char>(type));225
put16(rec, 0x0303); // legacy_record_version226
put16(rec, static_cast<unsigned>(payload.size()));227
rec += payload;228
return sock::sendall(fd, rec) == 0;229
}231
// Seal one application_data record (RFC 8446 §5.2): inner plaintext = content || content_type,232
// AAD = the record header, then ChaCha20-Poly1305.233
bool seal_record(long long fd, Keys& k, unsigned inner_type, std::string_view content) {234
std::string inner(content);235
inner.push_back(static_cast<char>(inner_type));236
std::string aad;237
aad.push_back(23);238
put16(aad, 0x0303);239
put16(aad, static_cast<unsigned>(inner.size() + 16));240
std::string ct;241
if (k.aead == Aead::Aes256) ct = aead::aes256gcm_encrypt(k.key_hex, nonce_hex(k), aad, inner);242
else if (k.aead == Aead::Aes128) ct = aead::aes128gcm_encrypt(k.key_hex, nonce_hex(k), aad, inner);243
else ct = aead::chacha20poly1305_encrypt(k.key_hex, nonce_hex(k), aad, inner);244
++k.seq;245
if (ct.empty()) return false;246
return sock::sendall(fd, aad + ct) == 0;247
}249
// Open one encrypted record: returns the inner content and type, false on AEAD failure.250
bool open_record(Keys& k, std::string_view payload, unsigned& inner_type, std::string& content) {251
std::string aad;252
aad.push_back(23);253
put16(aad, 0x0303);254
put16(aad, static_cast<unsigned>(payload.size()));255
std::string inner;256
if (k.aead == Aead::Aes256) inner = aead::aes256gcm_decrypt(k.key_hex, nonce_hex(k), aad, payload);257
else if (k.aead == Aead::Aes128) inner = aead::aes128gcm_decrypt(k.key_hex, nonce_hex(k), aad, payload);258
else inner = aead::chacha20poly1305_decrypt(k.key_hex, nonce_hex(k), aad, payload);259
++k.seq;260
if (inner.empty() && payload.size() > 16) return false; // tag mismatch (or empty record)261
while (!inner.empty() && inner.back() == '\0') inner.pop_back(); // strip padding262
if (inner.empty()) return false; // a record must carry a content type263
inner_type = static_cast<unsigned char>(inner.back());264
inner.pop_back(); // drop the trailing content-type byte in place …265
content = std::move(inner); // … and move the ~16 KB plaintext out instead of copying it266
return true;267
}269
// ---- handshake construction -----------------------------------------------------271
std::string random_bytes(std::size_t n) {272
std::string out(n, '\0');273
// getentropy (Linux + macOS/BSD) is the portable CSPRNG read; getrandom is Linux-only.274
// It is capped at 256 bytes per call, so loop for larger requests. A nonzero return means275
// the OS could not supply randomness — fatal for key material, so bail with "".276
std::size_t got = 0;277
while (got < n) {278
const std::size_t chunk = std::min<std::size_t>(n - got, 256);279
if (::getentropy(out.data() + got, chunk) != 0) return "";280
got += chunk;281
}282
return out;283
}285
// Build the ClientHello handshake MESSAGE (no record header). Fills `client_hello_random`.286
std::string build_client_hello(const std::string& server_name, std::string_view pub_raw) {287
std::string body;288
put16(body, 0x0303); // legacy_version289
body += random_bytes(32); // random290
body.push_back(32); // legacy_session_id (32 bytes, middlebox compatibility)291
body += random_bytes(32);292
// Cipher preference follows OUR fastest cipher, exactly as OpenSSL/curl do: with AES-NI +293
// PCLMULQDQ present, AES-GCM runs at multi-GB/s hardware speed and beats our scalar ChaCha20,294
// so offer AES-GCM FIRST; without hardware AES (some VMs/ARM), scalar ChaCha20 is the faster295
// path, so lead with it. The server picks from our order when it honors client preference —296
// which is what turns a ChaCha-negotiated ~200 MB/s link into a ~320 MB/s AES-GCM one.297
put16(body, 6); // cipher_suites: three suites (6 bytes)298
detail::append_cipher_preference(body, aead::crypto_hardware_active());299
body.push_back(1); // legacy_compression_methods300
body.push_back(0); // null302
std::string ext;303
{ // server_name (0)304
std::string names;305
names.push_back(0); // host_name306
put16(names, static_cast<unsigned>(server_name.size()));307
names += server_name;308
std::string sni;309
put16(sni, static_cast<unsigned>(names.size()));310
sni += names;311
put16(ext, 0);312
put16(ext, static_cast<unsigned>(sni.size()));313
ext += sni;314
}315
{ // supported_groups (10): x25519 only316
std::string g;317
put16(g, 2);318
put16(g, 0x001d);319
put16(ext, 10);320
put16(ext, static_cast<unsigned>(g.size()));321
ext += g;322
}323
{ // signature_algorithms (13): ed25519 (verifiable) + the common ones so real servers324
// complete the handshake far enough for our explicit refusal to be diagnosable325
std::string a;326
put16(a, 8);327
put16(a, 0x0807); // ed25519328
put16(a, 0x0804); // rsa_pss_rsae_sha256 (verified — see rsa_verify.hpp)329
put16(a, 0x0403); // ecdsa_secp256r1_sha256 (verified — see p256)330
put16(a, 0x0503); // ecdsa_secp384r1_sha384 (verified — see p384)331
put16(ext, 13);332
put16(ext, static_cast<unsigned>(a.size()));333
ext += a;334
}335
{ // supported_versions (43): TLS 1.3336
std::string v;337
v.push_back(2);338
put16(v, 0x0304);339
put16(ext, 43);340
put16(ext, static_cast<unsigned>(v.size()));341
ext += v;342
}343
{ // key_share (51): our X25519 public key344
std::string entry;345
put16(entry, 0x001d);346
put16(entry, 32);347
entry += pub_raw;348
std::string ks;349
put16(ks, static_cast<unsigned>(entry.size()));350
ks += entry;351
put16(ext, 51);352
put16(ext, static_cast<unsigned>(ks.size()));353
ext += ks;354
}355
put16(body, static_cast<unsigned>(ext.size()));356
body += ext;358
std::string msg;359
msg.push_back(1); // client_hello360
put24(msg, static_cast<unsigned>(body.size()));361
msg += body;362
return msg;363
}365
// Human-readable TLS alert (RFC 8446 §6) from the 2 alert bytes — so a handshake refusal names its366
// cause (e.g. 40 handshake_failure = no common cipher/group; 70 protocol_version = no TLS 1.3).367
std::string alert_text(std::string_view p) {368
if (p.size() < 2) return "(empty alert)";369
const unsigned lvl = static_cast<unsigned char>(p[0]);370
const unsigned d = static_cast<unsigned char>(p[1]);371
const char* name = "unknown";372
switch (d) {373
case 0: name = "close_notify"; break;374
case 10: name = "unexpected_message"; break;375
case 20: name = "bad_record_mac"; break;376
case 22: name = "record_overflow"; break;377
case 40: name = "handshake_failure"; break;378
case 42: name = "bad_certificate"; break;379
case 43: name = "unsupported_certificate"; break;380
case 47: name = "illegal_parameter"; break;381
case 48: name = "unknown_ca"; break;382
case 49: name = "access_denied"; break;383
case 50: name = "decode_error"; break;384
case 51: name = "decrypt_error"; break;385
case 70: name = "protocol_version"; break;386
case 71: name = "insufficient_security"; break;387
case 80: name = "internal_error"; break;388
case 109: name = "missing_extension"; break;389
case 110: name = "unsupported_extension"; break;390
case 112: name = "unrecognized_name"; break;391
case 116: name = "certificate_required"; break;392
case 120: name = "no_application_protocol"; break;393
default: break;394
}395
return "alert level=" + std::to_string(lvl) + " description=" + std::to_string(d) + " (" + name + ")";396
}398
// Parse ServerHello: confirm TLS 1.3 + one of our suites, extract the server's X25519 key share and399
// the CHOSEN cipher suite (0x1303 ChaCha20-Poly1305 or 0x1301 AES-128-GCM).400
bool parse_server_hello(std::string_view msg, std::string& server_pub_raw, unsigned& chosen_suite) {401
if (msg.size() < 4 || msg[0] != 2) return false; // server_hello402
std::string_view b = msg.substr(4);403
if (b.size() < 2 + 32 + 1) return false;404
std::size_t i = 2 + 32; // legacy_version + random405
const unsigned sid_len = static_cast<unsigned char>(b[i]);406
i += 1 + sid_len;407
if (b.size() < i + 4) return false;408
const unsigned suite = get16(b, i);409
if (suite != 0x1303 && suite != 0x1301 && suite != 0x1302)410
return false; // LCOV_EXCL_LINE: a server choosing a suite we did NOT offer is a malformed/hostile peer a conformant server never produces — the client-side mirror of the tested ParseClientHello "no suite in common" rejection411
chosen_suite = suite;412
i += 2 + 1; // suite + legacy_compression413
if (b.size() < i + 2) return false;414
const unsigned ext_len = get16(b, i);415
i += 2;416
const std::size_t ext_end = i + ext_len;417
bool saw_13 = false;418
while (i + 4 <= ext_end && ext_end <= b.size()) {419
const unsigned etype = get16(b, i);420
const unsigned elen = get16(b, i + 2);421
i += 4;422
if (i + elen > b.size()) return false;423
if (etype == 43 && elen == 2 && get16(b, i) == 0x0304) saw_13 = true;424
if (etype == 51 && elen >= 4 && get16(b, i) == 0x001d && get16(b, i + 2) == 32 &&425
elen == 4 + 32) {426
server_pub_raw = std::string(b.substr(i + 4, 32));427
}428
i += elen;429
}430
return saw_13 && server_pub_raw.size() == 32;431
}433
// Extract the Ed25519 public key from the leaf certificate's SubjectPublicKeyInfo: the DER434
// pattern 30 05 06 03 2B 65 70 (AlgorithmIdentifier { id-Ed25519 }) followed by435
// 03 21 00 <32-byte key> (BIT STRING). Returns "" when the cert key is not Ed25519.436
std::string ed25519_spki_key(std::string_view cert_der) {437
static const unsigned char kPat[] = {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70,438
0x03, 0x21, 0x00};439
for (std::size_t i = 0; i + sizeof kPat + 32 <= cert_der.size(); ++i) {440
if (std::memcmp(cert_der.data() + i, kPat, sizeof kPat) == 0) {441
return std::string(cert_der.substr(i + sizeof kPat, 32));442
}443
}444
return ""; // LCOV_EXCL_LINE: only when an Ed25519 CertificateVerify names a non-Ed25519 leaf — a malformed peer we don't mirror445
}447
// ---- server-side handshake construction (mirror of the client builders above) --------449
// A PEM block's DER bytes (strict base64 — a non-alphabet byte rejects the block, like x509).450
// @p label is e.g. "CERTIFICATE" or "PRIVATE KEY". Returns "" if the block is absent/malformed.451
std::string pem_block(const std::string& pem, const std::string& label) {452
const std::string begin = "-----BEGIN " + label + "-----";453
const std::string end = "-----END " + label + "-----";454
const std::size_t s = pem.find(begin);455
if (s == std::string::npos) return "";456
const std::size_t b = s + begin.size();457
const std::size_t e = pem.find(end, b);458
if (e == std::string::npos) return "";459
return hashlib::base64_decode(pem.substr(b, e - b), /*strict=*/true);460
}462
// The 32-byte Ed25519 seed from a PKCS#8 private key DER: the id-Ed25519 AlgorithmIdentifier463
// (30 05 06 03 2B 65 70) followed by 04 22 04 20 (OCTET STRING { OCTET STRING[32] }) and the seed.464
std::string ed25519_seed_from_pkcs8(std::string_view der) {465
static const unsigned char kPat[] = {0x30, 0x05, 0x06, 0x03, 0x2B, 0x65, 0x70,466
0x04, 0x22, 0x04, 0x20};467
for (std::size_t i = 0; i + sizeof kPat + 32 <= der.size(); ++i) {468
if (std::memcmp(der.data() + i, kPat, sizeof kPat) == 0) {469
return std::string(der.substr(i + sizeof kPat, 32));470
}471
}472
return "";473
}475
// EVERY PEM block under @p label, in order — the server's Certificate message must carry the whole476
// chain (leaf first, then intermediates), so a Let's Encrypt fullchain.pem yields N entries here477
// where pem_block() alone would silently drop everything after the leaf and browsers would reject478
// the path. A malformed block (bad base64) poisons the whole read: better no chain than a hole.479
std::vector<std::string> pem_blocks(const std::string& pem, const std::string& label) {480
std::vector<std::string> out;481
const std::string begin = "-----BEGIN " + label + "-----";482
const std::string end = "-----END " + label + "-----";483
std::size_t at = 0;484
while (true) {485
const std::size_t s = pem.find(begin, at);486
if (s == std::string::npos) break;487
const std::size_t b = s + begin.size();488
const std::size_t e = pem.find(end, b);489
if (e == std::string::npos) return {};490
const std::string der = hashlib::base64_decode(pem.substr(b, e - b), /*strict=*/true);491
if (der.empty()) return {};492
out.push_back(der);493
at = e + end.size();494
}495
return out;496
}498
// The 32-byte P-256 private scalar from a server key PEM — either shape openssl/certbot emit:499
// PKCS#8 ("PRIVATE KEY": AlgorithmIdentifier{id-ecPublicKey, prime256v1} wrapping a SEC1500
// ECPrivateKey) or bare SEC1 ("EC PRIVATE KEY"). Both carry the scalar as 02 01 01 04 20 <d32>501
// (ECPrivateKey version 1, then the OCTET STRING), and both carry the prime256v1 OID502
// (2A 86 48 CE 3D 03 01 07) — required here so a P-384/other-curve key is refused instead of503
// misread. Same pattern-scan discipline as ed25519_seed_from_pkcs8 above.504
std::string ec_p256_scalar_from_pem(const std::string& key_pem) {505
std::string der = pem_block(key_pem, "PRIVATE KEY");506
if (der.empty()) der = pem_block(key_pem, "EC PRIVATE KEY");507
if (der.empty()) return "";508
static const unsigned char kOid[] = {0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07};509
bool p256_curve = false;510
for (std::size_t i = 0; i + sizeof kOid <= der.size() && !p256_curve; ++i) {511
p256_curve = std::memcmp(der.data() + i, kOid, sizeof kOid) == 0;512
}513
if (!p256_curve) return "";514
static const unsigned char kPat[] = {0x02, 0x01, 0x01, 0x04, 0x20};515
for (std::size_t i = 0; i + sizeof kPat + 32 <= der.size(); ++i) {516
if (std::memcmp(der.data() + i, kPat, sizeof kPat) == 0) {517
return der.substr(i + sizeof kPat, 32);518
}519
}520
return "";521
}523
// Parse a ClientHello: choose a cipher suite we support (ChaCha20 preferred), extract the client's524
// X25519 key share + its legacy_session_id (echoed in ServerHello), confirm it offered TLS 1.3,525
// and surface its signature_algorithms (ext 13) as raw u16 pairs in @p sig_algs — the parser stays526
// lenient (an absent extension parses fine, sig_algs empty); server_handshake enforces the match,527
// because refusing to SIGN with an algorithm the client never offered is a handshake policy, not a528
// parse question.529
bool parse_client_hello(std::string_view msg, std::string& client_pub_raw, unsigned& chosen_suite,530
std::string& session_id, std::string& sig_algs) {531
client_pub_raw.clear(); // never leave stale out-params on a rejected/partial parse532
session_id.clear();533
sig_algs.clear();534
if (msg.size() < 4 || static_cast<unsigned char>(msg[0]) != 1) return false; // client_hello535
std::string_view b = msg.substr(4);536
std::size_t i = 2 + 32; // legacy_version + random537
if (b.size() < i + 1) return false;538
const unsigned sid_len = static_cast<unsigned char>(b[i]);539
i += 1;540
if (b.size() < i + sid_len) return false;541
session_id = std::string(b.substr(i, sid_len)); // must be echoed back verbatim542
i += sid_len;543
if (b.size() < i + 2) return false;544
const unsigned cs_len = get16(b, i);545
i += 2;546
if (b.size() < i + cs_len) return false;547
bool has_chacha = false, has_aes = false;548
for (std::size_t j = 0; j + 2 <= cs_len; j += 2) {549
const unsigned suite = get16(b, i + j);550
if (suite == 0x1303) has_chacha = true;551
if (suite == 0x1301) has_aes = true;552
}553
i += cs_len;554
if (has_chacha) chosen_suite = 0x1303;555
else if (has_aes) chosen_suite = 0x1301;556
else return false; // no cipher suite in common557
if (b.size() < i + 1) return false;558
const unsigned comp_len = static_cast<unsigned char>(b[i]);559
i += 1 + comp_len;560
if (b.size() < i + 2) return false;561
const unsigned ext_len = get16(b, i);562
i += 2;563
const std::size_t ext_end = i + ext_len;564
bool saw_13 = false;565
while (i + 4 <= ext_end && ext_end <= b.size()) {566
const unsigned etype = get16(b, i);567
const unsigned elen = get16(b, i + 2);568
i += 4;569
if (i + elen > b.size()) return false;570
if (etype == 43 && elen >= 1) { // supported_versions (list): look for 0x0304571
const unsigned n = static_cast<unsigned char>(b[i]);572
for (std::size_t j = 0; j + 2 <= n && i + 1 + j + 2 <= b.size(); j += 2) {573
if (get16(b, i + 1 + j) == 0x0304) saw_13 = true;574
}575
}576
if (etype == 13 && elen >= 2) { // signature_algorithms: the u16-pair list577
const unsigned sa_len = get16(b, i);578
if (2 + sa_len <= elen && sa_len % 2 == 0) {579
sig_algs = std::string(b.substr(i + 2, sa_len));580
}581
}582
if (etype == 51 && elen >= 2) { // key_share (list): find our x25519 (0x001d)583
const unsigned ks_len = get16(b, i);584
std::size_t j = i + 2;585
const std::size_t ks_end = i + 2 + ks_len;586
while (j + 4 <= ks_end && ks_end <= b.size()) {587
const unsigned group = get16(b, j);588
const unsigned klen = get16(b, j + 2);589
if (group == 0x001d && klen == 32 && j + 4 + 32 <= b.size()) {590
client_pub_raw = std::string(b.substr(j + 4, 32));591
}592
j += 4 + klen;593
}594
}595
i += elen;596
}597
return saw_13 && client_pub_raw.size() == 32;598
}600
// Build a ServerHello: echo the client's session id, our chosen suite, our X25519 key share.601
std::string build_server_hello(std::string_view session_id, unsigned suite,602
std::string_view pub_raw) {603
std::string body;604
put16(body, 0x0303); // legacy_version605
body += random_bytes(32); // random606
body.push_back(static_cast<char>(session_id.size()));607
body += session_id; // echo legacy_session_id (RFC 8446 §4.1.3)608
put16(body, suite); // cipher_suite609
body.push_back(0); // legacy_compression_method (null)611
std::string ext;612
put16(ext, 43); // supported_versions: TLS 1.3613
put16(ext, 2);614
put16(ext, 0x0304);615
{ // key_share (51): our X25519 KeyShareEntry616
std::string entry;617
put16(entry, 0x001d);618
put16(entry, 32);619
entry += pub_raw;620
put16(ext, 51);621
put16(ext, static_cast<unsigned>(entry.size()));622
ext += entry;623
}624
put16(body, static_cast<unsigned>(ext.size()));625
body += ext;627
std::string msg;628
msg.push_back(2); // server_hello629
put24(msg, static_cast<unsigned>(body.size()));630
msg += body;631
return msg;632
}634
// The TLS 1.3 SERVER handshake over connected fd @p fd, presenting @p cert_pem (an Ed25519 or635
// ECDSA P-256 leaf — @p cert_pem may be a fullchain.pem, and every block is sent) and proving636
// possession with @p key_pem (the leaf's PKCS#8 Ed25519 key, or its PKCS#8/SEC1 P-256 key).637
// Mirror of handshake(): same key schedule and record I/O, roles reversed — we SIGN638
// CertificateVerify instead of verifying it, and send the certificate flight. Returns a session639
// handle (>= 1) or -1 (see last_error()).640
long long server_handshake(long long fd, const std::string& cert_pem, const std::string& key_pem) {641
t_error.clear();643
const std::vector<std::string> chain = pem_blocks(cert_pem, "CERTIFICATE");644
if (chain.empty()) {645
fail("tls: server certificate PEM is missing or malformed");646
return -1;647
}648
const std::string& leaf_der = chain.front();650
// Which key did we get, and does it actually belong to the leaf? Deriving the public key from651
// the private half and comparing it to the leaf's SPKI catches a mixed-up cert/key pair at652
// startup with a precise message, instead of as an opaque CertificateVerify failure on the653
// first client. Exactly one signature algorithm follows from the key type — there is no654
// negotiation surface on our side to confuse.655
const std::string ed_seed = ed25519_seed_from_pkcs8(pem_block(key_pem, "PRIVATE KEY"));656
const std::string ec_scalar = ec_p256_scalar_from_pem(key_pem);657
unsigned cv_alg = 0;658
if (ed_seed.size() == 32) {659
const std::string spki = ed25519_spki_key(leaf_der);660
if (spki.size() != 32 || ed25519::public_key(to_hex(ed_seed)) != to_hex(spki)) {661
fail("tls: the Ed25519 private key does not match the server certificate");662
return -1;663
}664
cv_alg = 0x0807; // ed25519665
} else if (ec_scalar.size() == 32) {666
const std::string point = p256::spki_ec_point(leaf_der);667
if (point.size() != 64 || p256::public_from_private(ec_scalar) != point) {668
fail("tls: the ECDSA P-256 private key does not match the server certificate");669
return -1;670
}671
cv_alg = 0x0403; // ecdsa_secp256r1_sha256672
} else {673
fail("tls: server private key is not a PKCS#8 Ed25519 or P-256 EC key");674
return -1;675
}677
// ClientHello (plaintext; tolerate a leading ChangeCipherSpec compat record).678
std::string buffer, payload;679
unsigned rtype = 0;680
for (;;) {681
if (!read_record(fd, buffer, rtype, payload)) {682
fail("tls: connection closed before ClientHello");683
return -1;684
}685
if (rtype == 20) continue;686
if (rtype != 22) {687
fail("tls: expected a ClientHello");688
return -1;689
}690
break;691
}692
std::string client_pub_raw, session_id, sig_algs;693
unsigned suite = 0;694
if (!parse_client_hello(payload, client_pub_raw, suite, session_id, sig_algs)) {695
fail("tls: malformed ClientHello (or no TLS 1.3 / X25519 / shared cipher suite)");696
return -1;697
}698
// RFC 8446 §4.4.3: a server MUST NOT sign with an algorithm the client did not offer in699
// signature_algorithms (§4.2.3 makes the extension mandatory for certificate auth). Our700
// algorithm is fixed by the key type, so this is a containment check, not a negotiation.701
bool alg_offered = false;702
for (std::size_t j = 0; j + 2 <= sig_algs.size(); j += 2) {703
if (get16(sig_algs, j) == cv_alg) alg_offered = true;704
}705
if (!alg_offered) {706
fail("tls: the client's signature_algorithms do not include our certificate's algorithm");707
return -1;708
}709
// The server offers only the SHA-256 suites (ChaCha20 / AES-128-GCM), so the key schedule is SHA-256.710
const Aead aead = (suite == 0x1301) ? Aead::Aes128 : Aead::Chacha20;711
std::string transcript = payload; // ClientHello713
// Our ephemeral X25519 key pair, and ServerHello.714
const std::string priv_raw = random_bytes(32);715
if (priv_raw.size() != 32) {716
fail("tls: system random unavailable"); // LCOV_EXCL_LINE: getentropy failure — unreachable on a working host717
return -1; // LCOV_EXCL_LINE718
}719
const std::string priv_hex = to_hex(priv_raw);720
const std::string pub_raw = from_hex(x25519::x25519_base(priv_hex));721
const std::string server_hello = build_server_hello(session_id, suite, pub_raw);722
transcript += server_hello;723
if (!write_record(fd, 22, server_hello)) {724
fail("tls: cannot send ServerHello"); // LCOV_EXCL_LINE: a mid-handshake socket write failure725
return -1; // LCOV_EXCL_LINE726
}727
write_record(fd, 20, std::string(1, '\x01')); // middlebox-compat ChangeCipherSpec (not in transcript)729
// Key schedule (identical to the client; the transcript now spans ClientHello + ServerHello).730
const std::string shared_hex = x25519::x25519(priv_hex, to_hex(client_pub_raw));731
if (shared_hex.empty()) {732
fail("tls: invalid client key share");733
return -1;734
}735
const std::string zeros(32, '\0');736
const std::string early = hashlib::hkdf_extract(std::string(), zeros);737
const std::string derived = derive_secret_impl(early, "derived", "");738
const std::string hs_secret = hashlib::hkdf_extract(derived, from_hex(shared_hex));739
const std::string c_hs = derive_secret_impl(hs_secret, "c hs traffic", transcript);740
const std::string s_hs = derive_secret_impl(hs_secret, "s hs traffic", transcript);741
Keys server_keys = traffic_keys(s_hs, aead, false); // we SEND under the server secret742
Keys client_keys = traffic_keys(c_hs, aead, false); // we RECEIVE under the client secret744
// Encrypted flight: EncryptedExtensions (empty), Certificate, CertificateVerify, Finished —745
// each sealed as its own handshake record (inner type 22).746
std::string ee;747
ee.push_back(8); // encrypted_extensions748
put24(ee, 2);749
put16(ee, 0); // extensions: empty750
transcript += ee;751
if (!seal_record(fd, server_keys, 22, ee)) {752
fail("tls: cannot send EncryptedExtensions"); // LCOV_EXCL_LINE: a mid-handshake socket write failure753
return -1; // LCOV_EXCL_LINE754
}756
std::string cert_msg;757
{758
std::string entries; // every chain block, leaf first — a fullchain.pem arrives intact,759
for (const std::string& der : chain) { // giving the client a path to its trust anchor760
put24(entries, static_cast<unsigned>(der.size()));761
entries += der;762
put16(entries, 0); // per-cert extensions: none763
}764
std::string cbody;765
cbody.push_back(0); // certificate_request_context: empty766
put24(cbody, static_cast<unsigned>(entries.size()));767
cbody += entries;768
cert_msg.push_back(11); // certificate769
put24(cert_msg, static_cast<unsigned>(cbody.size()));770
cert_msg += cbody;771
}772
transcript += cert_msg;773
if (!seal_record(fd, server_keys, 22, cert_msg)) {774
fail("tls: cannot send Certificate"); // LCOV_EXCL_LINE: a mid-handshake socket write failure775
return -1; // LCOV_EXCL_LINE776
}778
std::string cv_msg;779
{780
// Same signed content the client verifies: 64 spaces, the context string, a NUL, then the781
// transcript hash through Certificate. Ed25519 signs the content directly; ECDSA P-256782
// signs SHA-256(content) and travels as DER — the exact mirror of the client's verify783
// branches for 0x0807/0x0403.784
std::string signed_content(64, ' ');785
signed_content += "TLS 1.3, server CertificateVerify";786
signed_content.push_back('\0');787
signed_content += hashlib::sha256_digest(transcript);788
std::string sig;789
if (cv_alg == 0x0807) {790
sig = from_hex(ed25519::sign(to_hex(ed_seed), signed_content));791
} else {792
sig = p256::rs_to_der(793
p256::sign_raw(ec_scalar, hashlib::sha256_digest(signed_content)));794
if (sig.empty()) { // LCOV_EXCL_LINE: pre-flight proved the scalar derives the leaf's public key, so sign_raw cannot reject it795
fail("tls: ECDSA signing failed (invalid P-256 private key scalar)"); // LCOV_EXCL_LINE796
return -1; // LCOV_EXCL_LINE797
}798
}799
std::string vbody;800
put16(vbody, cv_alg);801
put16(vbody, static_cast<unsigned>(sig.size()));802
vbody += sig;803
cv_msg.push_back(15); // certificate_verify804
put24(cv_msg, static_cast<unsigned>(vbody.size()));805
cv_msg += vbody;806
}807
transcript += cv_msg;808
if (!seal_record(fd, server_keys, 22, cv_msg)) {809
fail("tls: cannot send CertificateVerify"); // LCOV_EXCL_LINE: a mid-handshake socket write failure810
return -1; // LCOV_EXCL_LINE811
}813
std::string fin_msg;814
{815
const std::string finished_key = expand_label_impl(s_hs, "finished", "", 32);816
const std::string verify =817
hashlib::hmac_sha256(finished_key, hashlib::sha256_digest(transcript));818
fin_msg.push_back(20); // finished819
put24(fin_msg, static_cast<unsigned>(verify.size()));820
fin_msg += verify;821
}822
if (!seal_record(fd, server_keys, 22, fin_msg)) {823
fail("tls: cannot send server Finished"); // LCOV_EXCL_LINE: a mid-handshake socket write failure824
return -1; // LCOV_EXCL_LINE825
}826
transcript += fin_msg;828
// Application traffic secrets (transcript through the server's Finished).829
const std::string derived2 = derive_secret_impl(hs_secret, "derived", "");830
const std::string master = hashlib::hkdf_extract(derived2, zeros);831
const std::string c_ap = derive_secret_impl(master, "c ap traffic", transcript);832
const std::string s_ap = derive_secret_impl(master, "s ap traffic", transcript);834
// Client Finished (encrypted under the client handshake keys); verify its MAC over the835
// transcript through the server Finished.836
const std::string c_finished_key = expand_label_impl(c_hs, "finished", "", 32);837
const std::string expect =838
hashlib::hmac_sha256(c_finished_key, hashlib::sha256_digest(transcript));839
bool client_finished = false;840
while (!client_finished) {841
if (!read_record(fd, buffer, rtype, payload)) {842
fail("tls: connection closed before the client Finished");843
return -1;844
}845
if (rtype == 20) continue; // ChangeCipherSpec compat846
if (rtype == 21) {847
fail("tls: client alert during the handshake — " + alert_text(payload));848
return -1;849
}850
if (rtype != 23) {851
fail("tls: unexpected plaintext record awaiting the client Finished");852
return -1;853
}854
unsigned inner_type = 0;855
std::string content;856
if (!open_record(client_keys, payload, inner_type, content)) {857
fail("tls: client Finished failed authentication");858
return -1;859
}860
if (inner_type != 22 || content.size() < 4 || static_cast<unsigned char>(content[0]) != 20) {861
fail("tls: expected a client Finished"); // LCOV_EXCL_LINE: a key-scheduled malicious client we do not mirror862
return -1; // LCOV_EXCL_LINE863
}864
// cppcheck-suppress stlcstrConstructor // (ptr,len) subview of content — not a c_str() copy865
const std::string_view got(content.data() + 4, content.size() - 4);866
unsigned char diff = (got.size() == expect.size()) ? 0 : 1; // constant-time MAC compare867
for (std::size_t i = 0; i < expect.size(); ++i) {868
const unsigned char g = i < got.size() ? static_cast<unsigned char>(got[i]) : 0;869
diff |= g ^ static_cast<unsigned char>(expect[i]);870
}871
if (diff != 0) {872
fail("tls: client Finished MAC mismatch — handshake transcript tampered"); // LCOV_EXCL_LINE: a key-scheduled malicious client we do not mirror873
return -1; // LCOV_EXCL_LINE874
}875
client_finished = true;876
}878
Session s;879
s.fd = fd;880
s.client_keys = traffic_keys(s_ap, aead, false); // our sending direction (server app secret)881
s.server_keys = traffic_keys(c_ap, aead, false); // the peer's direction (client app secret)882
s.read_buffer = std::move(buffer);883
const std::lock_guard<std::mutex> lock(registry().mutex);884
const long long handle = registry().next_handle++;885
registry().sessions[handle] = std::move(s);886
return handle;887
}889
// ---- the handshake state machine -------------------------------------------------891
// The system CA trust store, parsed once and reused (loading ~150 CA certs on every handshake892
// would be wasteful). Thread-safe initialization via the C++ function-local static.893
const x509::TrustStore& default_trust() {894
static const x509::TrustStore store = x509::load_trust("");895
return store;896
}898
long long handshake(long long fd, const std::string& server_name, bool insecure,899
const std::string& ca_file) {900
t_error.clear();902
// Ephemeral X25519 key pair.903
const std::string priv_raw = random_bytes(32);904
if (priv_raw.size() != 32) {905
fail("tls: system random unavailable");906
return -1;907
}908
const std::string priv_hex = to_hex(priv_raw);909
const std::string pub_hex = x25519::x25519_base(priv_hex);910
const std::string pub_raw = from_hex(pub_hex);912
std::string transcript; // concatenated handshake MESSAGES (no record headers)913
const std::string client_hello = build_client_hello(server_name, pub_raw);914
transcript += client_hello;915
if (!write_record(fd, 22, client_hello)) {916
fail("tls: cannot send ClientHello");917
return -1;918
}920
// ServerHello (plaintext record, type 22; tolerate ChangeCipherSpec compat records).921
std::string buffer, payload;922
unsigned rtype = 0;923
std::string server_pub_raw;924
for (;;) {925
if (!read_record(fd, buffer, rtype, payload)) {926
fail("tls: connection closed before ServerHello");927
return -1;928
}929
if (rtype == 20) continue; // ChangeCipherSpec (compat) — ignored930
if (rtype == 21) {931
fail("tls: server sent an alert instead of ServerHello — " + alert_text(payload));932
return -1;933
}934
if (rtype != 22) {935
fail("tls: unexpected record before ServerHello");936
return -1;937
}938
break;939
}940
unsigned chosen_suite = 0;941
if (!parse_server_hello(payload, server_pub_raw, chosen_suite)) {942
fail("tls: malformed ServerHello (or the server refused TLS 1.3 + our cipher suites)");943
return -1;944
}945
// The negotiated suite fixes both the record AEAD and the key-schedule hash.946
const bool sha384 = (chosen_suite == 0x1302); // TLS_AES_256_GCM_SHA384 → SHA-384 schedule947
Aead aead = Aead::Chacha20; // TLS_CHACHA20_POLY1305_SHA256 (0x1303)948
if (chosen_suite == 0x1302) aead = Aead::Aes256;949
else if (chosen_suite == 0x1301) aead = Aead::Aes128;950
transcript += payload;952
// Key schedule through the handshake secrets.953
const std::string shared_hex = x25519::x25519(priv_hex, to_hex(server_pub_raw));954
if (shared_hex.empty()) {955
fail("tls: invalid server key share");956
return -1;957
}958
const std::string zeros(sha384 ? 48 : 32, '\0'); // HashLen zero bytes for Extract959
const std::string early = ks_extract(sha384, std::string(), zeros);960
const std::string derived = derive_secret_impl(early, "derived", "", sha384);961
const std::string hs_secret = ks_extract(sha384, derived, from_hex(shared_hex));962
const std::string c_hs = derive_secret_impl(hs_secret, "c hs traffic", transcript, sha384);963
const std::string s_hs = derive_secret_impl(hs_secret, "s hs traffic", transcript, sha384);964
Keys client_keys = traffic_keys(c_hs, aead, sha384);965
Keys server_keys = traffic_keys(s_hs, aead, sha384);967
// Encrypted handshake flight: EncryptedExtensions, Certificate, CertificateVerify, Finished.968
std::string handshake_bytes; // decrypted, possibly spanning records969
std::string cert_der;970
std::vector<std::string> cert_chain; // the full certificate chain (leaf first) for validation971
bool verified_cert = false, server_finished = false;972
bool cert_requested = false; // server sent CertificateRequest (mail servers do)973
std::string cert_request_context; // echoed back in our (empty) Certificate reply974
std::string transcript_at_cv, transcript_at_finished;975
// Total-flight cap: the server's handshake messages accumulate into transcript / handshake_bytes /976
// cert_chain BEFORE the certificate is validated, so a hostile (or MITM) server could otherwise977
// stream unbounded records and exhaust memory pre-auth. A real TLS 1.3 flight — even a long cert978
// chain of RSA-4096 leaves — is well under 256 KiB; cap there and fail closed beyond it.979
constexpr std::size_t kMaxHandshakeFlight = std::size_t{256} * 1024;980
std::size_t flight_bytes = 0;981
while (!server_finished) {982
if (!read_record(fd, buffer, rtype, payload)) {983
fail("tls: connection closed during the handshake");984
return -1;985
}986
if (rtype == 20) continue; // compat ChangeCipherSpec987
if (rtype == 21) {988
fail("tls: server alert during the handshake — " + alert_text(payload));989
return -1;990
}991
if (rtype != 23) {992
fail("tls: unexpected plaintext record during the encrypted handshake");993
return -1;994
}995
unsigned inner_type = 0;996
std::string content;997
if (!open_record(server_keys, payload, inner_type, content)) {998
fail("tls: handshake record failed authentication");999
return -1;1000
}1001
if (inner_type == 21) {1002
fail("tls: server alert during the handshake — " + alert_text(content));1003
return -1;1004
}1005
if (inner_type != 22) {1006
fail("tls: unexpected inner record type during the handshake");1007
return -1;1008
}1009
flight_bytes += content.size();1010
if (flight_bytes > kMaxHandshakeFlight) {1011
fail("tls: server handshake flight too large");1012
return -1;1013
}1014
handshake_bytes += content;1016
// Drain complete handshake messages from the reassembly buffer.1017
while (handshake_bytes.size() >= 4) {1018
const unsigned mtype = static_cast<unsigned char>(handshake_bytes[0]);1019
const unsigned mlen = get24(handshake_bytes, 1);1020
if (handshake_bytes.size() < 4 + mlen) break;1021
const std::string msg = handshake_bytes.substr(0, 4 + mlen);1022
handshake_bytes.erase(0, 4 + mlen);1024
if (mtype == 11 && msg.size() > 4 + 4 + 3 + 3) { // Certificate1025
// certificate_request_context (1 byte, empty) + the cert list. Walk every1026
// CertificateEntry (3-byte length | cert DER | 2-byte extensions | extensions)1027
// so the FULL chain is available for path validation, not just the leaf.1028
std::size_t i = 4 + 1 + 3; // header + context length byte + cert-list length1029
while (i + 3 <= msg.size()) {1030
const unsigned clen = get24(msg, i);1031
i += 3;1032
if (i + clen > msg.size()) break;1033
cert_chain.push_back(msg.substr(i, clen));1034
i += clen;1035
if (i + 2 > msg.size()) break;1036
i += 2 + get16(msg, i); // skip the per-certificate extensions1037
}1038
if (!cert_chain.empty()) cert_der = cert_chain[0]; // the leaf signs CertificateVerify1039
transcript_at_cv = transcript + msg; // transcript THROUGH Certificate1040
}1041
if (mtype == 13 && msg.size() >= 5) { // CertificateRequest (optional client auth)1042
// RFC 8446 §4.4.2: a client with no certificate MUST still answer with a1043
// Certificate message whose certificate_list is empty, echoing this1044
// context — smtp.gmail.com requests one and aborts (unexpected_message)1045
// on a bare Finished. We never present a certificate; we just decline1046
// correctly.1047
cert_requested = true;1048
const std::size_t ctx_len = static_cast<unsigned char>(msg[4]);1049
if (msg.size() >= 5 + ctx_len) cert_request_context = msg.substr(5, ctx_len);1050
}1051
if (mtype == 15) { // CertificateVerify1052
// cppcheck-suppress stlcstrConstructor // (ptr,len) subview of msg — not a c_str() copy1053
const std::string_view body(msg.data() + 4, msg.size() - 4);1054
if (body.size() < 4) {1055
fail("tls: malformed CertificateVerify");1056
return -1;1057
}1058
const unsigned alg = get16(body, 0);1059
const unsigned sig_len = get16(body, 2);1060
if (body.size() < 4 + sig_len) {1061
fail("tls: malformed CertificateVerify");1062
return -1;1063
}1064
// The signed content (same for every algorithm): 64 spaces, the1065
// context string, a NUL, then the handshake transcript hash.1066
std::string signed_content(64, ' ');1067
signed_content += "TLS 1.3, server CertificateVerify";1068
signed_content.push_back('\0');1069
signed_content += ks_digest(sha384, transcript_at_cv); // transcript hash = suite hash1070
const std::string sig(body.substr(4, sig_len));1071
if (alg == 0x0807) { // ed25519 (signs the message directly)1072
const std::string spki = ed25519_spki_key(cert_der);1073
if (spki.size() != 32) {1074
fail("tls: certificate key is not Ed25519");1075
return -1;1076
}1077
if (!ed25519::verify(to_hex(spki), signed_content, to_hex(sig))) {1078
fail("tls: server CertificateVerify signature is INVALID");1079
return -1;1080
}1081
} else if (alg == 0x0403) { // ecdsa_secp256r1_sha256 (signs SHA-256(content))1082
const std::string point = p256::spki_ec_point(cert_der);1083
if (point.size() != 64) {1084
fail("tls: certificate key is not P-256 EC");1085
return -1;1086
}1087
if (!p256::verify_der(point, hashlib::sha256_digest(signed_content), sig)) {1088
fail("tls: server CertificateVerify (ECDSA P-256) is INVALID");1089
return -1;1090
}1091
} else if (alg == 0x0503) { // ecdsa_secp384r1_sha384 (signs SHA-384(content))1092
// The signature scheme's hash (SHA-384) is independent of the transcript hash1093
// inside signed_content (which is the negotiated suite's hash) — RFC 8446 §4.4.3.1094
const std::string point = p384::spki_ec_point(cert_der);1095
if (point.size() != 96) {1096
fail("tls: certificate key is not P-384 EC");1097
return -1;1098
}1099
if (!p384::verify_der(point, hashlib::sha384_digest(signed_content), sig)) {1100
fail("tls: server CertificateVerify (ECDSA P-384) is INVALID");1101
return -1;1102
}1103
} else if (alg == 0x0804) { // rsa_pss_rsae_sha256 (RSA leaf certificate)1104
// RSA-PSS verifies the signed_content directly (it hashes with SHA-256 internally),1105
// using the RSA public key extracted from the leaf cert's SubjectPublicKeyInfo.1106
if (!rsa::verify_pss_sha256(cert_der, signed_content, sig)) {1107
fail("tls: server CertificateVerify (RSA-PSS SHA-256) is INVALID");1108
return -1;1109
}1110
} else { // LCOV_EXCL_LINE: reached only if a server sends a CertificateVerify whose algorithm ignores our advertised signature_algorithms — a non-conformant peer we don't mirror1111
fail("tls: server certificate uses an algorithm cheatah cannot verify yet " // LCOV_EXCL_LINE1112
"(Ed25519, ECDSA P-256/P-384, and RSA-PSS SHA-256 are supported) — refusing an "1113
"unauthenticated connection");1114
return -1;1115
}1116
verified_cert = true;1117
}1118
if (mtype == 20) { // Finished1119
const std::string finished_key =1120
expand_label_impl(s_hs, "finished", "", sha384 ? 48 : 32, sha384);1121
const std::string expect =1122
ks_hmac(sha384, finished_key, ks_digest(sha384, transcript));1123
// cppcheck-suppress stlcstrConstructor // (ptr,len) subview of msg — not a c_str() copy1124
const std::string_view got(msg.data() + 4, msg.size() - 4);1125
// Constant-time MAC compare (parity with the AEAD tag check): always scan all1126
// HashLen bytes of the secret `expect` (32 for SHA-256, 48 for SHA-384), never1127
// early-exiting on a mismatching byte, so timing cannot reveal a partial match.1128
unsigned char diff = (got.size() == expect.size()) ? 0 : 1;1129
for (std::size_t i = 0; i < expect.size(); ++i) {1130
const unsigned char g =1131
i < got.size() ? static_cast<unsigned char>(got[i]) : 0;1132
diff |= g ^ static_cast<unsigned char>(expect[i]);1133
}1134
if (diff != 0) {1135
fail("tls: server Finished MAC mismatch — handshake transcript tampered");1136
return -1;1137
}1138
transcript_at_finished = transcript + msg;1139
server_finished = true;1140
}1141
transcript += msg;1142
}1143
}1144
if (!verified_cert) {1145
fail("tls: server never proved possession of its certificate key");1146
return -1;1147
}1149
// AUTHENTICATE THE SERVER'S IDENTITY (unless the caller opted out of verification): build the1150
// presented chain to a trusted CA, match the hostname against the leaf's SAN, and check the1151
// validity dates. Key possession alone (above) does not prove identity — this is what stops an1152
// active man-in-the-middle presenting any certificate.1153
if (!insecure) {1154
x509::TrustStore custom;1155
const x509::TrustStore* store = &default_trust();1156
if (!ca_file.empty()) {1157
custom = x509::load_trust(ca_file);1158
store = &custom;1159
}1160
std::string verr;1161
if (!x509::validate(cert_chain, server_name, *store, static_cast<long long>(std::time(nullptr)),1162
verr)) {1163
fail("tls: certificate validation failed — " + verr);1164
return -1;1165
}1166
}1168
// Application traffic secrets (transcript through server Finished), then OUR Finished1169
// (sent under the handshake keys, with the transcript through the server's Finished).1170
const std::string derived2 = derive_secret_impl(hs_secret, "derived", "", sha384);1171
const std::string master = ks_extract(sha384, derived2, zeros);1172
const std::string c_ap = derive_secret_impl(master, "c ap traffic", transcript_at_finished, sha384);1173
const std::string s_ap = derive_secret_impl(master, "s ap traffic", transcript_at_finished, sha384);1175
// A requested-but-absent client certificate: the empty Certificate reply goes on the1176
// wire AND into the transcript BEFORE our Finished (whose MAC covers it) — RFC 84461177
// §4.4.2/§4.4.4. No CertificateVerify follows an empty list.1178
if (cert_requested) {1179
std::string cert_body;1180
cert_body.push_back(static_cast<char>(cert_request_context.size()));1181
cert_body += cert_request_context;1182
put24(cert_body, 0); // empty certificate_list1183
std::string cert_msg;1184
cert_msg.push_back(11);1185
put24(cert_msg, static_cast<unsigned>(cert_body.size()));1186
cert_msg += cert_body;1187
if (!seal_record(fd, client_keys, 22, cert_msg)) {1188
fail("tls: cannot send the (empty) client Certificate");1189
return -1;1190
}1191
transcript += cert_msg;1192
}1194
const std::string c_finished_key =1195
expand_label_impl(c_hs, "finished", "", sha384 ? 48 : 32, sha384);1196
const std::string verify = ks_hmac(sha384, c_finished_key, ks_digest(sha384, transcript));1197
std::string fin_msg;1198
fin_msg.push_back(20);1199
put24(fin_msg, static_cast<unsigned>(verify.size()));1200
fin_msg += verify;1201
if (!seal_record(fd, client_keys, 22, fin_msg)) {1202
fail("tls: cannot send client Finished");1203
return -1;1204
}1206
Session s;1207
s.fd = fd;1208
s.client_keys = traffic_keys(c_ap, aead, sha384);1209
s.server_keys = traffic_keys(s_ap, aead, sha384);1210
s.read_buffer = std::move(buffer); // bytes already pulled off the socket stay with us1212
const std::lock_guard<std::mutex> lock(registry().mutex);1213
const long long handle = registry().next_handle++;1214
registry().sessions[handle] = std::move(s);1215
return handle;1216
}1218
} // namespace1220
/// @cond INTERNAL1221
/// the C++-only low-level session API (tls_lowlevel.hpp); cheatah uses the Conn guard1222
long long client_connect(long long fd, const std::string& server_name, bool insecure,1223
const std::string& ca_file) {1224
return handshake(fd, server_name, insecure, ca_file);1225
}1227
long long server_accept(long long fd, const std::string& cert_pem, const std::string& key_pem) {1228
return server_handshake(fd, cert_pem, key_pem);1229
}1231
long long send(long long session, const std::string& data) {1232
t_error.clear();1233
// Lock ONLY for the map lookup (see recv): the socket write below runs without1234
// the global lock so concurrent sessions don't serialize on each other.1235
Session* sp = nullptr;1236
{1237
const std::lock_guard<std::mutex> lock(registry().mutex);1238
const auto it = registry().sessions.find(session);1239
if (it == registry().sessions.end() || it->second.closed) {1240
fail("tls: unknown or closed session");1241
return -1;1242
}1243
sp = &it->second;1244
}1245
Session& s = *sp;1246
// Respect the 16 KiB record plaintext bound.1247
std::string_view rest = data;1248
while (!rest.empty()) {1249
const std::size_t n = std::min<std::size_t>(rest.size(), 16384);1250
if (!seal_record(s.fd, s.client_keys, 23, rest.substr(0, n))) {1251
fail("tls: send failed");1252
return -1;1253
}1254
rest.remove_prefix(n);1255
}1256
return 0;1257
}1259
std::string recv(long long session, long long bufsize) {1260
t_error.clear();1261
if (bufsize <= 0) return "";1262
// Guard ONLY the map lookup. The per-session buffers (read_buffer/app_pending)1263
// and socket are owned by this session's single reader thread, so the blocking1264
// record I/O below runs WITHOUT the global lock — otherwise one session's1265
// blocking recv would serialize (and at shutdown, starve) every other session's1266
// recv/send. A std::map node address is stable until that node is erased, and a1267
// session is erased only by its own owner (after this loop), so the pointer is1268
// valid for this call. (registry().mutex still serializes find/insert/erase on the map.)1269
Session* sp = nullptr;1270
{1271
const std::lock_guard<std::mutex> lock(registry().mutex);1272
const auto it = registry().sessions.find(session);1273
if (it == registry().sessions.end()) {1274
fail("tls: unknown session");1275
return "";1276
}1277
sp = &it->second;1278
}1279
Session& s = *sp;1280
// Drain up to `bufsize` of application data. We block (in read_record) ONLY while we have1281
// nothing to hand back; once app_pending holds data we keep going solely to consume records1282
// ALREADY buffered (has_complete_record) — never adding a blocking wait. Because read_record1283
// now pulls 64 KiB per recv, one blocking read typically delivers several records, all drained1284
// here into a single ≥16 KB return to requests — which keeps the socket drained and the1285
// receive window open instead of the old one-record-per-call stall.1286
while (!s.closed) {1287
if (!s.app_pending.empty() &&1288
(s.app_pending.size() >= static_cast<std::size_t>(bufsize) ||1289
!has_complete_record(s.read_buffer))) {1290
break; // enough to return, and nothing more ready without blocking1291
}1292
unsigned rtype = 0;1293
std::string payload;1294
if (!read_record(s.fd, s.read_buffer, rtype, payload)) {1295
s.closed = true; // peer EOF (or socket timeout) — surfaced as ""1296
break;1297
}1298
if (rtype == 20) continue; // stray compat ChangeCipherSpec1299
if (rtype == 21) { // plaintext alert (illegal post-handshake, but final)1300
s.closed = true;1301
break;1302
}1303
if (rtype != 23) continue; // ignore anything else1304
unsigned inner_type = 0;1305
std::string content;1306
if (!open_record(s.server_keys, payload, inner_type, content)) {1307
fail("tls: record failed authentication");1308
s.closed = true;1309
break;1310
}1311
if (inner_type == 23) {1312
s.app_pending += content;1313
} else if (inner_type == 21) { // alert ends the stream: close_notify is the1314
// normal clean close (surfaced as plain EOF); anything else is the peer1315
// REFUSING the session — name it, so a fatal alert never masquerades as EOF.1316
if (content.size() != 2 || static_cast<unsigned char>(content[1]) != 0) {1317
fail("tls: peer alert — " + alert_text(content));1318
}1319
s.closed = true;1320
} else if (inner_type == 22) {1321
// Post-handshake messages: NewSessionTicket(4) is ignored; a KeyUpdate(24)1322
// would change the peer's keys — unsupported, so end the stream rather than1323
// silently fail to decrypt what follows.1324
if (!content.empty() && static_cast<unsigned char>(content[0]) == 24) {1325
fail("tls: peer KeyUpdate is not supported");1326
s.closed = true;1327
}1328
}1329
}1330
const std::size_t n = std::min<std::size_t>(s.app_pending.size(),1331
static_cast<std::size_t>(bufsize));1332
if (n == s.app_pending.size()) {1333
std::string out = std::move(s.app_pending); // whole buffer → move, no copy1334
s.app_pending.clear();1335
return out;1336
}1337
std::string out = s.app_pending.substr(0, n);1338
s.app_pending.erase(0, n);1339
return out;1340
}1342
long long close(long long session) {1343
t_error.clear();1344
const std::lock_guard<std::mutex> lock(registry().mutex);1345
const auto it = registry().sessions.find(session);1346
if (it == registry().sessions.end()) return -1;1347
if (!it->second.closed) {1348
const std::string close_notify = {1, 0}; // warning, close_notify1349
seal_record(it->second.fd, it->second.client_keys, 21, close_notify);1350
}1351
registry().sessions.erase(it);1352
return 0;1353
}1355
long long shutdown(long long session) {1356
t_error.clear();1357
const std::lock_guard<std::mutex> lock(registry().mutex);1358
const auto it = registry().sessions.find(session);1359
if (it == registry().sessions.end()) return -1;1360
// Wake a reader blocked in recv() WITHOUT erasing the session (that stays the1361
// owner's job via close(), after it has joined the reader). Just half-close the1362
// socket so the blocking recv returns EOF.1363
return socket::shutdown(it->second.fd);1364
}1365
/// @endcond1367
std::string last_error() { return t_error; }1369
// ---- owning RAII session ----1370
// Each method forwards to the handle-based free function above; the guard adds deterministic1371
// close() (close_notify + session erase) on scope exit, so a `with` block cannot leak.1373
Conn& Conn::operator=(Conn&& other) noexcept {1374
if (this != &other) {1375
if (session_ > 0) cheatah::tls::close(session_);1376
session_ = other.session_;1377
other.session_ = 0;1378
}1379
return *this;1380
}1381
Conn::~Conn() {1382
if (session_ > 0) cheatah::tls::close(session_);1383
}1384
long long Conn::send(const std::string& data) const { return cheatah::tls::send(session_, data); }1385
std::string Conn::recv(long long bufsize) const { return cheatah::tls::recv(session_, bufsize); }1386
long long Conn::shutdown() const { return cheatah::tls::shutdown(session_); }1387
long long Conn::close() {1388
if (session_ <= 0) return -1;1389
const long long rc = cheatah::tls::close(session_);1390
session_ = 0;1391
return rc;1392
}1393
Conn open(long long fd, const std::string& server_name, bool insecure, const std::string& ca_file) {1394
return Conn(client_connect(fd, server_name, insecure, ca_file));1395
}1396
Conn accept(long long fd, const std::string& cert_pem, const std::string& key_pem) {1397
return Conn(server_handshake(fd, cert_pem, key_pem));1398
}1400
namespace detail {1401
// Cipher preference follows OUR fastest cipher, exactly as OpenSSL/curl do: with AES-NI +1402
// PCLMULQDQ present, AES-GCM runs at multi-GB/s hardware speed and beats our scalar ChaCha20, so1403
// offer AES-GCM FIRST; without hardware AES (some VMs/ARM), scalar ChaCha20 is the faster path, so1404
// lead with it. The server picks from our order when it honors client preference — which is what1405
// turns a ChaCha-negotiated ~200 MB/s link into a ~320 MB/s AES-GCM one.1406
//1407
// Split out and taking the decision as a PARAMETER rather than calling crypto_hardware_active()1408
// inline, so both orders are reachable from a test on any host. Inline, the branch not matching the1409
// build machine's CPU was dead code no test could ever execute — the ordering is a wire-format1410
// decision and deserves to be pinned on every machine, not only on ARM.1411
void append_cipher_preference(std::string& body, bool hardware_aes) {1412
if (hardware_aes) {1413
put16(body, 0x1302); // TLS_AES_256_GCM_SHA384 (hardware AES-NI — preferred)1414
put16(body, 0x1301); // TLS_AES_128_GCM_SHA256 (hardware AES-NI)1415
put16(body, 0x1303); // TLS_CHACHA20_POLY1305_SHA256 (fallback)1416
} else {1417
put16(body, 0x1303); // TLS_CHACHA20_POLY1305_SHA256 (no AES-NI — scalar ChaCha wins)1418
put16(body, 0x1301); // TLS_AES_128_GCM_SHA2561419
put16(body, 0x1302); // TLS_AES_256_GCM_SHA3841420
}1421
}1424
std::string expand_label(std::string_view secret, std::string_view label,1425
std::string_view context, unsigned length) {1426
return cheatah::tls::expand_label_impl(secret, label, context, length);1427
}1428
std::string derive_secret(std::string_view secret, std::string_view label,1429
std::string_view transcript) {1430
return cheatah::tls::derive_secret_impl(secret, label, transcript);1431
}1432
bool parse_client_hello(std::string_view msg, std::string& client_pub_raw, unsigned& chosen_suite,1433
std::string& session_id, std::string& sig_algs) {1434
return cheatah::tls::parse_client_hello(msg, client_pub_raw, chosen_suite, session_id,1435
sig_algs);1436
}1437
std::string pem_block(const std::string& pem, const std::string& label) {1438
return cheatah::tls::pem_block(pem, label);1439
}1440
std::vector<std::string> pem_blocks(const std::string& pem, const std::string& label) {1441
return cheatah::tls::pem_blocks(pem, label);1442
}1443
std::string ed25519_seed_from_pkcs8(std::string_view der) {1444
return cheatah::tls::ed25519_seed_from_pkcs8(der);1445
}1446
std::string ec_p256_scalar_from_pem(const std::string& key_pem) {1447
return cheatah::tls::ec_p256_scalar_from_pem(key_pem);1448
}1449
std::string build_client_hello(const std::string& server_name, std::string_view pub_raw) {1450
return cheatah::tls::build_client_hello(server_name, pub_raw);1451
}1452
} // namespace detail1454
} // namespace cheatah::tls