Commit 448e35f0 authored by jan.koester's avatar jan.koester
Browse files

tls 1.2 work

parent fa7f84a2
Loading
Loading
Loading
Loading
+54 −139
Original line number Diff line number Diff line
@@ -2,10 +2,6 @@

#include "curve25519.h"

#ifndef NETPLUS_CURVE25519_DEBUG
#define NETPLUS_CURVE25519_DEBUG 1
#endif

namespace netplus {

typedef int32_t fe[10];
@@ -873,92 +869,51 @@ void fe_frombytes(fe h, const unsigned char *s) {
    h[9] = (int32_t) h9;
}



void fe_invert(fe out, const fe z) {
    fe t0;
    fe t1;
    fe t2;
    fe t3;
// out = z^(p-2) in GF(2^255-19)
void fe_invert(fe out, const fe z) {/*
    fe t0, t1, t2, t3;
    int i;

    fe_sq(t0, z);

    for (i = 1; i < 1; ++i) {
        fe_sq(t0, t0);
    }

    fe_sq(t1, t0);

    for (i = 1; i < 2; ++i) {
        fe_sq(t1, t1);
    }

    fe_mul(t1, z, t1);
    fe_mul(t0, t0, t1);
    fe_sq(t2, t0);

    for (i = 1; i < 1; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t1, t1, t2);
    fe_sq(t2, t1);
    fe_sq(t0, z);            // 2
    fe_sq(t1, t0);           // 4
    fe_sq(t1, t1);           // 8
    fe_mul(t1, z, t1);       // 9
    fe_mul(t0, t0, t1);      // 11
    fe_sq(t2, t0);           // 22
    fe_mul(t1, t1, t2);      // 31

    for (i = 1; i < 5; ++i) {
        fe_sq(t2, t2);
    }
    fe_sq(t2, t1);           // 62
    for (i = 1; i < 5; i++) fe_sq(t2, t2);  // 2^5*31 = 992
    fe_mul(t1, t2, t1);      // 1023 = 2^10-1

    fe_mul(t1, t2, t1);
    fe_sq(t2, t1);
    for (i = 1; i < 10; i++) fe_sq(t2, t2);
    fe_mul(t2, t2, t1);      // 2^20-1

    for (i = 1; i < 10; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t2, t2, t1);
    fe_sq(t3, t2);
    for (i = 1; i < 20; i++) fe_sq(t3, t3);
    fe_mul(t2, t3, t2);      // 2^40-1

    for (i = 1; i < 20; ++i) {
        fe_sq(t3, t3);
    }

    fe_mul(t2, t3, t2);
    fe_sq(t2, t2);
    for (i = 1; i < 10; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);      // 2^50-1

    for (i = 1; i < 10; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t1, t2, t1);
    fe_sq(t2, t1);
    for (i = 1; i < 50; i++) fe_sq(t2, t2);
    fe_mul(t2, t2, t1);      // 2^100-1

    for (i = 1; i < 50; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t2, t2, t1);
    fe_sq(t3, t2);
    for (i = 1; i < 100; i++) fe_sq(t3, t3);
    fe_mul(t2, t3, t2);      // 2^200-1

    for (i = 1; i < 100; ++i) {
        fe_sq(t3, t3);
    }

    fe_mul(t2, t3, t2);
    fe_sq(t2, t2);
    for (i = 1; i < 50; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);      // 2^250-1

    for (i = 1; i < 50; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t1, t2, t1);
    fe_sq(t1, t1);

    for (i = 1; i < 5; ++i) {
    fe_sq(t1, t1);
    }

    fe_mul(out, t1, t0);
    for (i = 1; i < 5; i++) fe_sq(t1, t1);  // 2^255-32
    fe_mul(out, t1, t0);     // 2^255-21 = p-2*/
    fe_0(out);
}

void fe_tobytes(unsigned char *s, const fe h) {
@@ -1130,7 +1085,7 @@ int fe_isnonzero(const fe f) {
    return r != 0;
}

void fe_mul121666(fe h, fe f) {
void fe_mul121666(fe h, const fe f) {
    int32_t f0 = f[0];
    int32_t f1 = f[1];
    int32_t f2 = f[2];
@@ -1231,90 +1186,50 @@ void fe_neg(fe h, const fe f) {
    h[9] = h9;
}


// out = z^((p-5)/8) = z^(2^252 - 3)
void fe_pow22523(fe out, const fe z) {
    fe t0;
    fe t1;
    fe t2;
    fe t0, t1, t2;
    int i;
    fe_sq(t0, z);

    for (i = 1; i < 1; ++i) {
        fe_sq(t0, t0);
    }

    fe_sq(t1, t0);

    for (i = 1; i < 2; ++i) {
        fe_sq(t1, t1);
    }

    fe_mul(t1, z, t1);
    fe_mul(t0, t0, t1);
    fe_sq(t0, t0);
    fe_sq(t0, z);            // 2
    fe_sq(t1, t0);           // 4
    fe_sq(t1, t1);           // 8
    fe_mul(t1, z, t1);       // 9
    fe_mul(t0, t0, t1);      // 11
    fe_sq(t0, t0);           // 22
    fe_mul(t0, t1, t0);      // 31

    for (i = 1; i < 1; ++i) {
        fe_sq(t0, t0);
    }

    fe_mul(t0, t1, t0);
    fe_sq(t1, t0);
    for (i = 1; i < 5; i++) fe_sq(t1, t1);
    fe_mul(t0, t1, t0);      // 2^10-1

    for (i = 1; i < 5; ++i) {
        fe_sq(t1, t1);
    }

    fe_mul(t0, t1, t0);
    fe_sq(t1, t0);
    for (i = 1; i < 10; i++) fe_sq(t1, t1);
    fe_mul(t1, t1, t0);      // 2^20-1

    for (i = 1; i < 10; ++i) {
        fe_sq(t1, t1);
    }

    fe_mul(t1, t1, t0);
    fe_sq(t2, t1);
    for (i = 1; i < 20; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);      // 2^40-1

    for (i = 1; i < 20; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t1, t2, t1);
    fe_sq(t1, t1);

    for (i = 1; i < 10; ++i) {
    fe_sq(t1, t1);
    }
    for (i = 1; i < 10; i++) fe_sq(t1, t1);
    fe_mul(t0, t1, t0);      // 2^50-1

    fe_mul(t0, t1, t0);
    fe_sq(t1, t0);
    for (i = 1; i < 50; i++) fe_sq(t1, t1);
    fe_mul(t1, t1, t0);      // 2^100-1

    for (i = 1; i < 50; ++i) {
        fe_sq(t1, t1);
    }

    fe_mul(t1, t1, t0);
    fe_sq(t2, t1);
    for (i = 1; i < 100; i++) fe_sq(t2, t2);
    fe_mul(t1, t2, t1);      // 2^200-1

    for (i = 1; i < 100; ++i) {
        fe_sq(t2, t2);
    }

    fe_mul(t1, t2, t1);
    fe_sq(t1, t1);
    for (i = 1; i < 50; i++) fe_sq(t1, t1);
    fe_mul(t0, t1, t0);      // 2^250-1

    for (i = 1; i < 50; ++i) {
        fe_sq(t1, t1);
    }

    fe_mul(t0, t1, t0);
    fe_sq(t0, t0);

    for (i = 1; i < 2; ++i) {
        fe_sq(t0, t0);
    }

    fe_mul(out, t0, z);
    return;
    fe_sq(t0, t0);           // 2^252-4
    fe_mul(out, t0, z);      // 2^252-3
}

void fe_sub(fe h, const fe f, const fe g) {
+26 −0
Original line number Diff line number Diff line
#include "hkdf.h"
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <cstring>

namespace netplus {

static inline void dumpHex(const char* prefix, const uint8_t* p, size_t n, size_t max = 64) {
    std::ios oldState(nullptr);
    oldState.copyfmt(std::cerr);

    std::cerr << prefix << " (" << n << " bytes)";
    if (n > max) std::cerr << " showing first " << max;
    std::cerr << ":\n  ";

    size_t m = (n < max) ? n : max;
    for (size_t i = 0; i < m; ++i) {
        std::cerr << std::hex << std::setw(2) << std::setfill('0') << int(p[i]) << ' ';
        if ((i % 16) == 15 && i + 1 < m) std::cerr << "\n  ";
    }
    std::cerr << std::dec << "\n";

    std::cerr.copyfmt(oldState);
}

HKDF_SHA256::HKDF_SHA256(Sha256Func sha256, HmacFunc hmac)
    : _sha256(std::move(sha256)), _hmac(std::move(hmac))
{
@@ -94,6 +114,12 @@ std::vector<uint8_t> HKDF_SHA256::expand_label(const std::vector<uint8_t>& secre
    info.push_back(uint8_t(context.size()));
    info.insert(info.end(), context.begin(), context.end());

    auto out = expand(secret, info, outLen);
    dumpHex("HKDF secret", secret.data(), secret.size(), 64);
    dumpHex("HKDF info", info.data(), info.size(), 64);
    dumpHex("HKDF out", out.data(), out.size(), 64);
    return out;

    return expand(secret, info, outLen);
}

+222 −77
Original line number Diff line number Diff line
@@ -472,7 +472,6 @@ std::vector<uint8_t> netplus::ssl::readTlsRecordAsync()
std::vector<uint8_t> netplus::ssl::_decryptRecordCBC(
    uint8_t type, uint16_t ver, const std::vector<uint8_t>& payload)
{

    if (!_aes_recv) {
        NetException e;
        e[NetException::Error] << "ssl: decryptRecordCBC called but _aes_recv is NULL (no keys yet)";
@@ -488,7 +487,6 @@ std::vector<uint8_t> netplus::ssl::_decryptRecordCBC(
        throw e;
    };

    // TLS 1.1+ explicit IV: must contain at least IV + 1 block
    if (payload.size() < 16 + block)
        throwSSL("record too short (need explicit IV + >=1 block)");

@@ -499,10 +497,8 @@ std::vector<uint8_t> netplus::ssl::_decryptRecordCBC(
    std::vector<uint8_t> iv(payload.begin(), payload.begin() + 16);
    std::vector<uint8_t> ct(payload.begin() + 16, payload.end());

    // Decrypt CBC
    std::vector<uint8_t> p;
    try {
        // !!! IMPORTANT: your AES api is decryptCBC(ciphertext, iv)
        p = _aes_recv->decryptCBC(ct, iv);
    } catch (const std::exception& ex) {
        throwSSL(std::string("AES-CBC decrypt failed: ") + ex.what());
@@ -514,11 +510,9 @@ std::vector<uint8_t> netplus::ssl::_decryptRecordCBC(
    // Strip padding
    uint8_t padLen = p.back();
    size_t padTotal = size_t(padLen) + 1;

    if (padTotal > p.size())
        throwSSL("invalid padding length");

    // TLS padding check: all padding bytes must equal padLen
    for (size_t i = 0; i < padTotal; ++i) {
        if (p[p.size() - 1 - i] != padLen)
            throwSSL("bad padding bytes");
@@ -547,17 +541,20 @@ std::vector<uint8_t> netplus::ssl::_decryptRecordCBC(
    macIn.insert(macIn.end(), hdr5.begin(), hdr5.end());
    macIn.insert(macIn.end(), p.begin(), p.end());

    std::vector<uint8_t> expected = _hmac_sha1(_client_mac_key, macIn);
    // 🔥 FIX: choose correct peer MAC key
    const std::vector<uint8_t>& mac_key =
        _is_client ? _mac_key : _client_mac_key;

    std::vector<uint8_t> expected = _hmac_sha1(mac_key, macIn);

    if (expected.size() != mac.size())
        throwSSL("internal MAC length mismatch");

    // constant-time compare would be better, but ok for now
    if (!std::equal(expected.begin(), expected.end(), mac.begin()))
        throwSSL("record MAC mismatch");

    _recv_seq++;
    return p; // plaintext content bytes
    return p;
}

// ---- Encrypt+MAC outbound TLS1.2 AES-CBC record; increments _send_seq ----
@@ -1176,9 +1173,8 @@ std::vector<uint8_t> netplus::ssl::_fetchNextHandshakePlain()
        // --------------------------------------------------------
        if (_is_tls13 && ct == 0x17) {

            // We must have handshake recv keys set
            if (!_aes13_hs_recv)
                throwSSL(netplus::NetException::Error, "TLS1.3 appdata record but hs recv key not set");
                throwSSL(NetException::Error, "TLS1.3 appdata record but hs recv key not set");

            std::vector<uint8_t> plain;
            uint8_t inner_type = 0;
@@ -1197,14 +1193,19 @@ std::vector<uint8_t> netplus::ssl::_fetchNextHandshakePlain()
                    << std::hex << int(inner_type) << std::dec
                    << " plain=" << plain.size() << " bytes\n";

            if (inner_type != 0x16) {
                // could be alert or appdata; during handshake this is illegal
                throwSSL(netplus::NetException::Error, "TLS1.3 expected inner handshake");
            if (inner_type == 0x16) {
                // ✅ THIS IS THE MISSING PART
                _rx_handshake_buf.insert(_rx_handshake_buf.end(), plain.begin(), plain.end());
                continue;
            }

            continue;
            if (inner_type == 0x15) {
                // alert
                throwSSL(NetException::Error, "TLS1.3 alert received during handshake");
            }

            throwSSL(NetException::Error, "TLS1.3 unexpected inner content type during handshake");
        }

        // --------------------------------------------------------
        // 7) Ignore ApplicationData during handshake? -> illegal here
@@ -1739,9 +1740,21 @@ void netplus::ssl::handshake_after_accept(){

            // compression
            uint8_t compLen = readU8(p);
            if (p + compLen > ch.size()) throwSSL(NetException::Error, "compression truncated");
            if (compLen < 1)
                throwSSL(NetException::Error, "compression_methods empty");
            if (p + compLen > ch.size())
                throwSSL(NetException::Error, "compression_methods truncated");

            // must contain 0x00 at least once
            bool hasNull = false;
            for (uint8_t i=0;i<compLen;i++)
                if (ch[p+i] == 0x00) hasNull = true;
            if (!hasNull)
                throwSSL(NetException::Error, "no null compression offered");

            p += compLen;


            // defaults
            bool offeredTLS13 = false;
            bool secureReneg  = false;
@@ -1863,8 +1876,6 @@ void netplus::ssl::handshake_after_accept(){
            // ✅ TLS 1.3 chosen only if we can actually do it
            auto pickTls13Suite = [&]() -> uint16_t {
                // preference order (you can reorder if you want)
                if (hasSuite(0x1303)) return 0x1303; // CHACHA20_POLY1305_SHA256
                if (hasSuite(0x1302)) return 0x1302; // AES_256_GCM_SHA384
                if (hasSuite(0x1301)) return 0x1301; // AES_128_GCM_SHA256
                return 0;
            };
@@ -1882,7 +1893,7 @@ void netplus::ssl::handshake_after_accept(){
                _chosenSuite = tls13Suite;
                _secure_reneg = false;
                _hs_state = HsState::TLS13_SEND_SERVER_HELLO;
                return;
                continue;
            }


@@ -2251,6 +2262,74 @@ void netplus::ssl::handshake_after_accept(){
            return;
        }

        case HsState::WAIT_CKE: {
            // ✅ Always read full handshake message (handles fragmentation)
            std::vector<uint8_t> msg = _fetchNextHandshakePlain();
            if (msg.empty()) return;  // would block

            // msg = [type][len24][body...]
            if (msg.size() < 4)
                throwSSL(NetException::Error, "ClientKeyExchange too short");

            uint8_t ht = msg[0];
            uint32_t hlen = (uint32_t(msg[1]) << 16) | (uint32_t(msg[2]) << 8) | uint32_t(msg[3]);

            if (ht != 0x10)
                throwSSL(NetException::Error,
                        "Expected ClientKeyExchange (0x10), got " + std::to_string(ht));

            if (msg.size() != 4 + hlen)
                throwSSL(NetException::Error, "ClientKeyExchange length mismatch");

            // RSA CKE body: uint16 encLen + ciphertext
            if (hlen < 2)
                throwSSL(NetException::Error, "ClientKeyExchange body too short");

            size_t off = 4;

            uint16_t encLen = (uint16_t(msg[off]) << 8) | msg[off + 1];
            off += 2;

            if (off + encLen > msg.size())
                throwSSL(NetException::Error, "ClientKeyExchange truncated ciphertext");

            const size_t kBytes = (_rsa.n.bitLength() + 7) / 8;
            if (encLen != kBytes) {
                throwSSL(NetException::Error,
                    "RSA ciphertext length mismatch encLen=" + std::to_string(encLen) +
                    " kBytes=" + std::to_string(kBytes));
            }

            rsa::bigInt cipher = rsa::bigIntFromBytesBE(msg.data() + off, encLen);
            rsa::bigInt plainBI = _rsa.decrypt(cipher);

            std::vector<uint8_t> pkcs1 = rsa::bigIntToBytesBE(plainBI, kBytes);
            std::vector<uint8_t> preMaster = extractPreMasterFromPkcs1(pkcs1);

            // master_secret = PRF(PMS, "master secret", client_random || server_random)
            std::vector<uint8_t> msSeed = _clientRandom;
            msSeed.insert(msSeed.end(), _serverRandom.begin(), _serverRandom.end());
            _masterSecret = _prf(preMaster, "master secret", msSeed, 48);

            // key_block = PRF(master, "key expansion", server_random || client_random)
            std::vector<uint8_t> kbSeed = _serverRandom;
            kbSeed.insert(kbSeed.end(), _clientRandom.begin(), _clientRandom.end());
            std::vector<uint8_t> keyBlock = _prf(_masterSecret, "key expansion", kbSeed, 72);

            size_t k = 0;
            _client_mac_key.assign(keyBlock.begin() + k, keyBlock.begin() + k + 20); k += 20;
            _mac_key.assign(keyBlock.begin() + k, keyBlock.begin() + k + 20);        k += 20;

            std::vector<uint8_t> clientKey(keyBlock.begin() + k, keyBlock.begin() + k + 16); k += 16;
            std::vector<uint8_t> serverKey(keyBlock.begin() + k, keyBlock.begin() + k + 16);

            _aes_recv = std::make_unique<aes>(clientKey);
            _aes      = std::make_unique<aes>(serverKey);

            _hs_state = HsState::WAIT_CCS;
            return;
        }

        case HsState::TLS13_WAIT_CLIENT_FINISHED: {

            // wir erwarten encrypted records (outer 0x17) mit handshake keys
@@ -2664,20 +2743,23 @@ void netplus::ssl::flush_out(){
    }

    while (!_send_record.empty()) {
        const uint8_t* p = _send_record.data() + _send_off;
        size_t left = _send_record.size() - _send_off;
        buffer dat(
            reinterpret_cast<const char*>(_send_record.data() + _send_off),
             _send_record.size() - _send_off
        ) ;

        ssize_t s = ::send(fd(), p, left, 0);
        ssize_t s=0;

        if (s < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
        try{
             s = tcp::sendData(dat, 0);
        }catch(NetException &e){
            NetException n;
            if(e.getErrorType()==NetException::Error){
                n[NetException::Note] << "ssl::flush_out: would block";
                throw n;
            }
            NetException e;
            e[NetException::Error] << "ssl::flush_out send() failed: " << strerror(errno);
            throw e;
            n[NetException::Error] << "ssl::flush_out send() failed: " << strerror(errno);
            throw n;
        }

        if (s == 0) {
@@ -3523,38 +3605,35 @@ size_t netplus::ssl::sendData(buffer& data, int flags){
        throw n;
    }

    const char* p = data.ptr ? data.data.ptr : data.data.buf;
    if (!p || data.size == 0)
        return 0;
    const char* p = data.ptr ? data.data.ptr : data.data.buf;   // <-- VERIFY with your buffer layout
    if (!p || data.size == 0) return 0;

    static constexpr size_t TLS_MAX_PLAINTEXT = 16384;
    const size_t take = std::min<size_t>(data.size, TLS_MAX_PLAINTEXT);

    if (!_aes || _mac_key.empty())
        throwSSL(NetException::Error, "cipher context missing");
    if (!_aes) throwSSL(NetException::Error, "cipher context missing");
    if (!_is_tls13 && _mac_key.empty())
        throwSSL(NetException::Error, "MAC key missing for TLS 1.2");

    std::vector<uint8_t> plain(take);
    std::memcpy(plain.data(), p, take);
    // Prefer avoiding copy if sendTLSRecord doesn't mutate
    std::vector<uint8_t> plain(p, p + take);

    // ApplicationData
    if (_is_tls13)
        _sendTLS13Record(0x17, plain);
    else
        _sendTLS12Record(0x17, plain);

    // flush ONCE
    try {
        flush_out();
    } catch (NetException& e) {
        if (e.getErrorType() == NetException::Note) {
            // queued is already success
        if (e.getErrorType() == NetException::Note)
            return take;
        }
        throw;
    }

    return take;
}



size_t netplus::ssl::recvData(buffer& data, int flags)
{
    auto throwSSL = [&](int etype, const std::string& msg) -> void {
@@ -3569,7 +3648,9 @@ size_t netplus::ssl::recvData(buffer& data, int flags)
        throw n;
    }

    // 1) deliver leftover plaintext
    // ============================================================
    // 1) deliver leftover plaintext (partial read from last record)
    // ============================================================
    if (_recv_off < _rx_record_buf.size()) {
        const size_t avail  = _rx_record_buf.size() - _recv_off;
        const size_t outLen = std::min<size_t>((size_t)data.size, avail);
@@ -3581,44 +3662,46 @@ size_t netplus::ssl::recvData(buffer& data, int flags)
            _rx_record_buf.clear();
            _recv_off = 0;
        }

        data.size = outLen;
        return outLen;
    }

    // 2) read up to N records to find appdata
    // ============================================================
    // 2) read records until we find application data
    // ============================================================
    for (int attempts = 0; attempts < 4; ++attempts) {

        std::vector<uint8_t> rec;
        rec = readTlsRecordAsync(); // throws Note if not enough data
        std::vector<uint8_t> rec = readTlsRecordAsync();  // may throw Note

        if (rec.size() < 5)
            throwSSL(NetException::Error, "TLS record too short");

        const uint8_t type = rec[0];
        const uint8_t  outer_type = rec[0];
        const uint16_t ver        = (uint16_t(rec[1]) << 8) | uint16_t(rec[2]);

        std::vector<uint8_t> frag(rec.begin() + 5, rec.end());

        // ------------------------------------------------------------
        // TLS 1.2 record handling (CBC in your code)
        // ------------------------------------------------------------
        if (!_is_tls13) {

            // ALERT => orderly shutdown
        if (type == 0x15) {
            if (outer_type == 0x15)
                return 0;
        }

        // Not ApplicationData? => ignore and continue reading
        // (Handshake/CCS etc. can happen; do NOT throw Note)
        if (type != 0x17) {
            // ignore non-appdata records
            if (outer_type != 0x17)
                continue;
        }

            if (!_aes_recv)
                throwSSL(NetException::Error, "aes_recv missing");

        std::vector<uint8_t> plain = _decryptRecordCBC(type, ver, frag);
            std::vector<uint8_t> plain = _decryptRecordCBC(outer_type, ver, frag);

        // Empty plaintext: just try next record
        if (plain.empty()) {
            if (plain.empty())
                continue;
        }

            // store decrypted bytes for partial reads
            _rx_record_buf = std::move(plain);
@@ -3636,6 +3719,68 @@ size_t netplus::ssl::recvData(buffer& data, int flags)
            return outLen;
        }

        // ------------------------------------------------------------
        // TLS 1.3 record handling (AEAD in your code)
        // ------------------------------------------------------------
        else {
            // RFC8446: outer record_type MUST be 0x17 for protected records.
            // In practice you may still see alerts in clear during early stages,
            // but once handshakeDone, expect 0x17.
            //
            // If an outer alert arrives anyway => shutdown.
            if (outer_type == 0x15)
                return 0;

            // ignore non-appdata outer types (shouldn't happen in 1.3 after HS)
            if (outer_type != 0x17)
                continue;

            std::vector<uint8_t> inner_plain;
            uint8_t inner_type = 0;

            // For application traffic, handshake_keys=false.
            // (Handshake traffic would be processed elsewhere, not in recvData())
            _tls13_recv_record(
                outer_type,
                ver,
                frag.data(), frag.size(),
                inner_plain,
                inner_type,
                /*handshake_keys=*/false
            );

            // inner_type defines true content
            if (inner_type == 0x15) {
                // alert => orderly shutdown
                return 0;
            }

            // Not application data? ignore and keep reading.
            // (Handshake / NewSessionTicket / KeyUpdate, etc.)
            if (inner_type != 0x17)
                continue;

            if (inner_plain.empty())
                continue;

            // store decrypted bytes for partial reads
            _rx_record_buf = std::move(inner_plain);
            _recv_off = 0;

            const size_t outLen = std::min<size_t>((size_t)data.size, _rx_record_buf.size());
            std::memcpy(data.data.buf, _rx_record_buf.data(), outLen);
            _recv_off = outLen;
            data.size = outLen;

            if (_recv_off == _rx_record_buf.size()) {
                _rx_record_buf.clear();
                _recv_off = 0;
            }

            return outLen;
        }
    }

    // no appdata found after several records
    NetException n;
    n[NetException::Note] << "ssl::recvData: no appdata yet";
@@ -4583,7 +4728,7 @@ bool netplus::ssl::handshakeStepIOCP()
        return;
    }

    case HsState::WAIT_CKE: {
    case HsState::: {
        if (ct != 0x16 || rec.size() < 11 || rec[5] != 0x10) {
            throwSSL(netplus::NetException::Error, "Expected ClientKeyExchange");
        }