Files
SUB-OS/net/tls.c
T
subhobhai943andClaude Opus 4.8 8a55ac345b feat(crypto,net): TLS 1.3 client -- SUB-OS can now browse HTTPS sites
The browser could only reach plain HTTP, which is almost none of the real
web. This adds a from-scratch TLS 1.3 client so it can fetch https:// pages,
including real sites on the open internet.

Crypto primitives (all integer-only, freestanding, verified on the host
against their RFC test vectors before going in the kernel):
  - crypto/x25519.c            X25519 ECDH (RFC 7748), compact TweetNaCl field
                               arithmetic.
  - crypto/chacha20poly1305.c  ChaCha20 + Poly1305 AEAD (RFC 8439).
  - crypto/hkdf.c              HMAC-SHA256, HKDF (RFC 2104/5869) and the TLS 1.3
                               HKDF-Expand-Label / Derive-Secret helpers, on top
                               of the existing SHA-256.
A host harness checked every one against its published vectors (X25519 s6.1,
ChaCha20 s2.4.2, AEAD s2.8.2 incl. tamper rejection, HKDF TC1) -- all pass.

TLS 1.3 client (net/tls.c), single suite TLS_CHACHA20_POLY1305_SHA256 over
X25519: ClientHello (with SNI and ALPN http/1.1) -> ServerHello -> the RFC 8446
key schedule over a running transcript hash -> decrypt the server flight ->
send Finished -> switch to application keys. A small record layer handles the
plaintext-then-AEAD split and the middlebox ChangeCipherSpec. All large buffers
live in the heap-allocated connection, never on the 16 KB worker stack.

http_client routes https:// through TLS instead of raw TCP, so the async
worker, webget, netcheck and the GUI Web app all get HTTPS for free; the
redirect follower now follows http->https upgrades. The Web app's default page
is now https://example.com/.

Verified end to end on x86_64: the handshake interoperates with openssl
s_server (TLS 1.3, ChaCha20) -- GET returned HTTP 200 with the decrypted body
-- and, over QEMU's NAT, `webget https://example.com/` and the GUI Web app both
loaded the real Example Domain page served by Cloudflare over TLS 1.3 (HTTP
200, 828 bytes). The server only accepts our Finished if the transcript hash
and key schedule match byte-for-byte, so a 200 is proof the whole stack is
correct. Builds clean on x86_64, aarch64 and armv8i.

SECURITY CAVEAT, stated plainly: the server certificate is NOT verified. The
channel is encrypted and interoperates, but a man-in-the-middle could
impersonate the peer. Real certificate-chain validation (X.509 parsing,
signature checks, a CA bundle, hostname matching) is a large separate piece and
is not built here. This is fine for a hobby OS over a trusted local NAT; it is
not fit to protect real secrets. Also: TLS 1.3 only, one cipher suite, and
still no JavaScript/media -- so JS-heavy or app-style sites (YouTube) remain
out of reach; this makes static/text HTTPS pages readable.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
2026-08-27 18:08:51 +05:30

573 lines
21 KiB
C

/*
* TLS 1.3 client for SUB-OS (RFC 8446), single suite:
* TLS_CHACHA20_POLY1305_SHA256 over X25519. See net/tls.h for the scope and
* the honest security caveat (the server certificate is not verified).
*
* Structure:
* - a small record layer (read/write, plaintext then AEAD-protected),
* - the handshake: ClientHello -> ServerHello -> key schedule -> decrypt the
* server flight -> send Finished -> switch to application keys,
* - the TLS 1.3 key schedule driven by crypto/hkdf.c over a running
* transcript hash.
*
* All large buffers live in the heap-allocated connection object, never on the
* stack, because this runs on the 16 KB worker-thread stack.
*/
#include <net/tls.h>
#include <net/tcp.h>
#include <net/net.h>
#include <crypto/crypto.h>
#include <mm/kmalloc.h>
#include <lib/string.h>
#include <lib/printf.h>
#include <kernel/printk.h>
#define TLS_PLAINTEXT_MAX 16384
#define TLS_REC_MAX (TLS_PLAINTEXT_MAX + 256)
// Content types.
#define CT_CHANGE_CIPHER_SPEC 20
#define CT_ALERT 21
#define CT_HANDSHAKE 22
#define CT_APPLICATION_DATA 23
// Handshake message types.
#define HS_CLIENT_HELLO 1
#define HS_SERVER_HELLO 2
#define HS_NEW_SESSION_TICKET 4
#define HS_ENCRYPTED_EXT 8
#define HS_CERTIFICATE 11
#define HS_CERT_REQUEST 13
#define HS_CERT_VERIFY 15
#define HS_FINISHED 20
#define TLS_AEAD_TAG 16
struct tls_conn {
tcp_conn_t* tcp;
sha256_ctx_t transcript;
uint8_t priv[32]; // our X25519 private scalar
uint8_t handshake_secret[32];
uint8_t c_hs_secret[32]; // client handshake traffic secret
uint8_t c_hs_key[32], c_hs_iv[12];
uint8_t s_hs_key[32], s_hs_iv[12];
uint8_t c_ap_key[32], c_ap_iv[12];
uint8_t s_ap_key[32], s_ap_iv[12];
uint64_t send_seq;
uint64_t recv_seq;
bool app_keys; // switched from handshake to application keys
bool established;
// Decrypted application-data left over from a record, awaiting the reader.
uint8_t rbuf[TLS_PLAINTEXT_MAX];
int rbuf_len, rbuf_off;
// Decrypted handshake bytes of the current record, consumed by hs_getbytes.
uint8_t hs_cur[TLS_PLAINTEXT_MAX];
int hs_cur_len, hs_cur_off;
// Scratch for record assembly / reads (kept off the stack).
uint8_t rec[TLS_REC_MAX];
uint8_t pt[TLS_PLAINTEXT_MAX + 1];
};
static char g_tls_err[96] = "";
static void tls_err(const char* m) {
strncpy(g_tls_err, m, sizeof(g_tls_err) - 1);
g_tls_err[sizeof(g_tls_err) - 1] = '\0';
}
const char* tls_last_error(void) { return g_tls_err; }
// ===========================================================================
// TCP framing helpers
// ===========================================================================
static bool read_exact(tcp_conn_t* c, uint8_t* buf, int n, uint32_t timeout_ms) {
int got = 0;
while (got < n) {
int r = tcp_recv(c, buf + got, (uint16_t)(n - got), timeout_ms);
if (r <= 0) return false;
got += r;
}
return true;
}
static bool send_all(tcp_conn_t* c, const uint8_t* buf, int n) {
return tcp_send(c, buf, (uint16_t)n) == n;
}
// ===========================================================================
// Transcript hash
// ===========================================================================
static void transcript_add(tls_conn_t* t, const uint8_t* data, int len) {
sha256_update(&t->transcript, data, len);
}
static void transcript_hash(tls_conn_t* t, uint8_t out[32]) {
sha256_ctx_t copy = t->transcript; // snapshot; hashing is non-destructive
sha256_final(out, &copy);
}
// ===========================================================================
// Key schedule (RFC 8446 section 7.1)
// ===========================================================================
static void derive_traffic_keys(const uint8_t secret[32],
uint8_t key[32], uint8_t iv[12]) {
tls13_expand_label(secret, "key", NULL, 0, key, 32);
tls13_expand_label(secret, "iv", NULL, 0, iv, 12);
}
// After ClientHello and ServerHello are in the transcript, derive the
// handshake traffic keys from the X25519 shared secret.
static void key_schedule_handshake(tls_conn_t* t, const uint8_t shared[32]) {
uint8_t empty_hash[32];
sha256((const uint8_t*)"", 0, empty_hash);
uint8_t psk[32] = { 0 };
uint8_t early_secret[32];
hkdf_extract(NULL, 0, psk, 32, early_secret);
uint8_t derived[32];
tls13_derive_secret(early_secret, "derived", empty_hash, derived);
hkdf_extract(derived, 32, shared, 32, t->handshake_secret);
uint8_t th[32];
transcript_hash(t, th); // CH || SH
uint8_t s_hs_secret[32];
tls13_derive_secret(t->handshake_secret, "c hs traffic", th, t->c_hs_secret);
tls13_derive_secret(t->handshake_secret, "s hs traffic", th, s_hs_secret);
derive_traffic_keys(t->c_hs_secret, t->c_hs_key, t->c_hs_iv);
derive_traffic_keys(s_hs_secret, t->s_hs_key, t->s_hs_iv);
t->send_seq = t->recv_seq = 0;
}
// After the server Finished is in the transcript, derive the application keys.
static void key_schedule_application(tls_conn_t* t) {
uint8_t empty_hash[32];
sha256((const uint8_t*)"", 0, empty_hash);
uint8_t derived[32];
tls13_derive_secret(t->handshake_secret, "derived", empty_hash, derived);
uint8_t psk[32] = { 0 };
uint8_t master[32];
hkdf_extract(derived, 32, psk, 32, master);
uint8_t th[32];
transcript_hash(t, th); // CH .. server Finished
uint8_t c_ap_secret[32], s_ap_secret[32];
tls13_derive_secret(master, "c ap traffic", th, c_ap_secret);
tls13_derive_secret(master, "s ap traffic", th, s_ap_secret);
derive_traffic_keys(c_ap_secret, t->c_ap_key, t->c_ap_iv);
derive_traffic_keys(s_ap_secret, t->s_ap_key, t->s_ap_iv);
}
// ===========================================================================
// Record layer
// ===========================================================================
static void build_nonce(uint8_t nonce[12], const uint8_t iv[12], uint64_t seq) {
memcpy(nonce, iv, 12);
for (int i = 0; i < 8; i++) nonce[11 - i] ^= (uint8_t)(seq >> (8 * i));
}
// Encrypt `data` (of inner content type `inner_type`) as one record and send it.
static bool write_record(tls_conn_t* t, uint8_t inner_type,
const uint8_t* data, int len) {
const uint8_t* key = t->app_keys ? t->c_ap_key : t->c_hs_key;
const uint8_t* iv = t->app_keys ? t->c_ap_iv : t->c_hs_iv;
int ptlen = len + 1; // data || inner_type
int reclen = ptlen + TLS_AEAD_TAG;
uint8_t* out = t->rec;
out[0] = CT_APPLICATION_DATA;
out[1] = 0x03; out[2] = 0x03;
out[3] = (uint8_t)(reclen >> 8);
out[4] = (uint8_t)(reclen & 0xff);
memcpy(t->pt, data, len);
t->pt[len] = inner_type;
uint8_t nonce[12];
build_nonce(nonce, iv, t->send_seq);
uint8_t tag[16];
chacha20poly1305_encrypt(out + 5, tag, key, nonce, out, 5, t->pt, ptlen);
memcpy(out + 5 + ptlen, tag, TLS_AEAD_TAG);
t->send_seq++;
return send_all(t->tcp, out, 5 + reclen);
}
// Read one record. On an encrypted record, decrypt it and return the inner
// content type in *inner_type and the plaintext length; ChangeCipherSpec
// records are skipped transparently. Cleartext handshake (ServerHello) is
// returned as-is. Returns -1 on error.
static int read_record(tls_conn_t* t, uint8_t* inner_type,
uint8_t* out, int outcap, uint32_t timeout_ms) {
for (;;) {
uint8_t hdr[5];
if (!read_exact(t->tcp, hdr, 5, timeout_ms)) { tls_err("record header read failed"); return -1; }
int type = hdr[0];
int len = (hdr[3] << 8) | hdr[4];
if (len < 0 || len > TLS_REC_MAX) { tls_err("record too large"); return -1; }
if (!read_exact(t->tcp, t->rec, len, timeout_ms)) { tls_err("record body read failed"); return -1; }
if (type == CT_CHANGE_CIPHER_SPEC) continue; // middlebox compat: ignore
if (type == CT_HANDSHAKE) {
// Cleartext handshake (only ServerHello before keys are set).
if (len > outcap) { tls_err("handshake record too large"); return -1; }
memcpy(out, t->rec, len);
*inner_type = CT_HANDSHAKE;
return len;
}
if (type == CT_ALERT && !t->app_keys && t->recv_seq == 0 && t->send_seq == 0) {
tls_err("server sent a cleartext alert");
return -1;
}
if (type == CT_APPLICATION_DATA) {
const uint8_t* key = t->app_keys ? t->s_ap_key : t->s_hs_key;
const uint8_t* iv = t->app_keys ? t->s_ap_iv : t->s_hs_iv;
if (len < TLS_AEAD_TAG) { tls_err("short encrypted record"); return -1; }
int ct_len = len - TLS_AEAD_TAG;
uint8_t nonce[12];
build_nonce(nonce, iv, t->recv_seq);
if (!chacha20poly1305_decrypt(t->pt, key, nonce, hdr, 5,
t->rec, ct_len, t->rec + ct_len)) {
tls_err("record decrypt/auth failed");
return -1;
}
t->recv_seq++;
// Strip zero padding; the last non-zero byte is the inner type.
int n = ct_len;
while (n > 0 && t->pt[n - 1] == 0) n--;
if (n == 0) { tls_err("empty inner plaintext"); return -1; }
*inner_type = t->pt[n - 1];
int body = n - 1;
if (body > outcap) { tls_err("inner plaintext too large"); return -1; }
memcpy(out, t->pt, body);
return body;
}
tls_err("unexpected record type");
return -1;
}
}
// ===========================================================================
// Handshake byte stream (server flight), feeding the transcript as it goes
// ===========================================================================
static bool hs_getbytes(tls_conn_t* t, uint8_t* dst, int n, uint32_t timeout_ms) {
int got = 0;
while (got < n) {
if (t->hs_cur_off >= t->hs_cur_len) {
uint8_t inner_type;
int r = read_record(t, &inner_type, t->hs_cur, sizeof(t->hs_cur), timeout_ms);
if (r < 0) return false;
if (inner_type == CT_ALERT) { tls_err("alert during handshake"); return false; }
if (inner_type != CT_HANDSHAKE) { tls_err("non-handshake in flight"); return false; }
transcript_add(t, t->hs_cur, r); // every handshake byte is hashed
t->hs_cur_len = r;
t->hs_cur_off = 0;
}
int take = t->hs_cur_len - t->hs_cur_off;
if (take > n - got) take = n - got;
memcpy(dst + got, t->hs_cur + t->hs_cur_off, take);
t->hs_cur_off += take;
got += take;
}
return true;
}
// ===========================================================================
// ClientHello
// ===========================================================================
// A tiny append-and-backpatch buffer.
typedef struct { uint8_t* p; int len; int cap; } buf_t;
static void bput(buf_t* b, uint8_t v) { if (b->len < b->cap) b->p[b->len] = v; b->len++; }
static void bput16(buf_t* b, uint16_t v) { bput(b, v >> 8); bput(b, v & 0xff); }
static void bputn(buf_t* b, const uint8_t* d, int n) { for (int i = 0; i < n; i++) bput(b, d[i]); }
static bool send_client_hello(tls_conn_t* t, const char* host, const uint8_t pub[32]) {
uint8_t body[1024];
buf_t b = { body, 0, sizeof(body) };
bput16(&b, 0x0303); // legacy_version
uint8_t random[32];
prng_get_bytes(random, 32);
bputn(&b, random, 32);
uint8_t sid[32];
prng_get_bytes(sid, 32);
bput(&b, 32); bputn(&b, sid, 32); // legacy_session_id (compat)
bput16(&b, 2); bput16(&b, 0x1303); // cipher_suites: chacha20-poly1305
bput(&b, 1); bput(&b, 0); // compression: null
// Extensions.
int ext_len_at = b.len;
bput16(&b, 0); // placeholder for extensions length
int ext_start = b.len;
// supported_versions (43): TLS 1.3
bput16(&b, 43); bput16(&b, 3); bput(&b, 2); bput16(&b, 0x0304);
// supported_groups (10): x25519
bput16(&b, 10); bput16(&b, 4); bput16(&b, 2); bput16(&b, 0x001d);
// signature_algorithms (13): a common set (unused -- we do not verify)
static const uint16_t sigs[] = { 0x0403, 0x0804, 0x0401, 0x0805, 0x0806, 0x0807 };
int nsig = (int)(sizeof(sigs) / sizeof(sigs[0]));
bput16(&b, 13); bput16(&b, 2 + nsig * 2); bput16(&b, nsig * 2);
for (int i = 0; i < nsig; i++) bput16(&b, sigs[i]);
// key_share (51): x25519 public key
bput16(&b, 51); bput16(&b, 38); bput16(&b, 36);
bput16(&b, 0x001d); bput16(&b, 32); bputn(&b, pub, 32);
// server_name (0): SNI
int hlen = (int)strlen(host);
bput16(&b, 0); bput16(&b, hlen + 5); bput16(&b, hlen + 3);
bput(&b, 0); bput16(&b, hlen); bputn(&b, (const uint8_t*)host, hlen);
// ALPN (16): http/1.1
bput16(&b, 16); bput16(&b, 11); bput16(&b, 9); bput(&b, 8);
bputn(&b, (const uint8_t*)"http/1.1", 8);
int ext_len = b.len - ext_start;
body[ext_len_at] = (uint8_t)(ext_len >> 8);
body[ext_len_at + 1] = (uint8_t)(ext_len & 0xff);
if (b.len > (int)sizeof(body)) { tls_err("ClientHello overflow"); return false; }
// Wrap in a handshake header, then a plaintext record.
uint8_t hs[4 + sizeof(body)];
hs[0] = HS_CLIENT_HELLO;
hs[1] = (uint8_t)(b.len >> 16);
hs[2] = (uint8_t)(b.len >> 8);
hs[3] = (uint8_t)(b.len);
memcpy(hs + 4, body, b.len);
int hs_len = 4 + b.len;
transcript_add(t, hs, hs_len);
uint8_t rec[5 + 4 + sizeof(body)];
rec[0] = CT_HANDSHAKE; rec[1] = 0x03; rec[2] = 0x01; // legacy 0x0301 for CH
rec[3] = (uint8_t)(hs_len >> 8);
rec[4] = (uint8_t)(hs_len & 0xff);
memcpy(rec + 5, hs, hs_len);
return send_all(t->tcp, rec, 5 + hs_len);
}
// ===========================================================================
// ServerHello
// ===========================================================================
static bool parse_server_hello(tls_conn_t* t, const uint8_t* msg, int len,
uint8_t server_pub[32]) {
// msg is the handshake message: type(1) len(3) body.
if (len < 4 || msg[0] != HS_SERVER_HELLO) { tls_err("expected ServerHello"); return false; }
const uint8_t* p = msg + 4;
const uint8_t* end = msg + len;
if (end - p < 2 + 32 + 1) { tls_err("ServerHello truncated"); return false; }
p += 2; // legacy_version
p += 32; // random
int sid_len = *p++;
if (end - p < sid_len + 3) { tls_err("ServerHello truncated"); return false; }
p += sid_len; // session id echo
uint16_t cipher = ((uint16_t)p[0] << 8) | p[1]; p += 2;
if (cipher != 0x1303) { tls_err("server chose an unsupported cipher"); return false; }
p += 1; // compression
if (end - p < 2) { tls_err("ServerHello no extensions"); return false; }
int ext_total = (p[0] << 8) | p[1]; p += 2;
const uint8_t* ext_end = p + ext_total;
if (ext_end > end) { tls_err("ServerHello ext overflow"); return false; }
bool got_key = false;
while (p + 4 <= ext_end) {
uint16_t etype = ((uint16_t)p[0] << 8) | p[1];
uint16_t elen = ((uint16_t)p[2] << 8) | p[3];
p += 4;
if (p + elen > ext_end) { tls_err("ServerHello ext len"); return false; }
if (etype == 51) { // key_share
if (elen < 4) { tls_err("bad key_share"); return false; }
uint16_t group = ((uint16_t)p[0] << 8) | p[1];
uint16_t klen = ((uint16_t)p[2] << 8) | p[3];
if (group != 0x001d || klen != 32 || elen < 4 + 32) {
tls_err("server key_share is not X25519");
return false;
}
memcpy(server_pub, p + 4, 32);
got_key = true;
}
p += elen;
}
if (!got_key) { tls_err("server sent no X25519 key_share (HelloRetry?)"); return false; }
return true;
}
// ===========================================================================
// Server flight: EncryptedExtensions, Certificate*, CertVerify, Finished
// ===========================================================================
static bool read_server_flight(tls_conn_t* t, uint32_t timeout_ms) {
for (;;) {
uint8_t hdr[4];
if (!hs_getbytes(t, hdr, 4, timeout_ms)) return false;
int htype = hdr[0];
int hlen = (hdr[1] << 16) | (hdr[2] << 8) | hdr[3];
// Consume (and discard) the message body; every byte was already fed to
// the transcript by hs_getbytes.
uint8_t skip[512];
int left = hlen;
while (left > 0) {
int chunk = left < (int)sizeof(skip) ? left : (int)sizeof(skip);
if (!hs_getbytes(t, skip, chunk, timeout_ms)) return false;
left -= chunk;
}
if (htype == HS_FINISHED) return true; // end of the server flight
if (htype == HS_CERT_REQUEST) {
// We do not present a client certificate; most public servers do not
// request one. Proceeding without it is the documented limitation.
}
// EE / Certificate / CertificateVerify: accepted without verification.
}
}
// ===========================================================================
// Client Finished
// ===========================================================================
static bool send_client_finished(tls_conn_t* t) {
uint8_t finished_key[32];
tls13_expand_label(t->c_hs_secret, "finished", NULL, 0, finished_key, 32);
uint8_t th[32];
transcript_hash(t, th); // CH .. server Finished
uint8_t verify[32];
hmac_sha256(finished_key, 32, th, 32, verify);
uint8_t msg[4 + 32];
msg[0] = HS_FINISHED; msg[1] = 0; msg[2] = 0; msg[3] = 32;
memcpy(msg + 4, verify, 32);
// Sent under handshake keys.
return write_record(t, CT_HANDSHAKE, msg, sizeof(msg));
}
// ===========================================================================
// Public API
// ===========================================================================
tls_conn_t* tls_connect(const char* host, uint32_t ip, uint16_t port,
uint32_t timeout_ms) {
tls_err("");
if (!host || ip == 0 || port == 0) { tls_err("bad arguments"); return NULL; }
tls_conn_t* t = (tls_conn_t*)kzalloc(sizeof(tls_conn_t));
if (!t) { tls_err("out of memory"); return NULL; }
sha256_init(&t->transcript);
t->tcp = tcp_connect(ip, port, timeout_ms);
if (!t->tcp) { tls_err("TCP connect failed"); kfree(t); return NULL; }
uint8_t pub[32];
prng_get_bytes(t->priv, 32);
x25519_base(pub, t->priv);
if (!send_client_hello(t, host, pub)) { tls_err("ClientHello send failed"); goto fail; }
// ServerHello (cleartext handshake record).
uint8_t inner_type;
int shlen = read_record(t, &inner_type, t->hs_cur, sizeof(t->hs_cur), timeout_ms);
if (shlen < 0) goto fail;
if (inner_type != CT_HANDSHAKE) { tls_err("expected ServerHello record"); goto fail; }
transcript_add(t, t->hs_cur, shlen);
uint8_t server_pub[32];
if (!parse_server_hello(t, t->hs_cur, shlen, server_pub)) goto fail;
uint8_t shared[32];
x25519(shared, t->priv, server_pub);
key_schedule_handshake(t, shared);
// Server flight is now encrypted under the server handshake key.
if (!read_server_flight(t, timeout_ms)) goto fail;
// Application keys are derived over CH..server Finished.
key_schedule_application(t);
if (!send_client_finished(t)) { tls_err("Finished send failed"); goto fail; }
// From here on both directions use the application keys.
t->app_keys = true;
t->send_seq = t->recv_seq = 0;
t->established = true;
return t;
fail:
if (t->tcp) tcp_close(t->tcp);
kfree(t);
return NULL;
}
int tls_send(tls_conn_t* t, const void* data, int len) {
if (!t || !t->established) return -1;
const uint8_t* p = (const uint8_t*)data;
int sent = 0;
while (sent < len) {
int chunk = len - sent;
if (chunk > TLS_PLAINTEXT_MAX) chunk = TLS_PLAINTEXT_MAX;
if (!write_record(t, CT_APPLICATION_DATA, p + sent, chunk)) return sent ? sent : -1;
sent += chunk;
}
return sent;
}
int tls_recv(tls_conn_t* t, void* buf, int len, uint32_t timeout_ms) {
if (!t || !t->established) return -1;
// Serve any leftover decrypted application data first.
if (t->rbuf_off >= t->rbuf_len) {
for (;;) {
uint8_t inner_type;
int r = read_record(t, &inner_type, t->rbuf, sizeof(t->rbuf), timeout_ms);
if (r < 0) return -1;
if (inner_type == CT_APPLICATION_DATA) { t->rbuf_len = r; t->rbuf_off = 0; break; }
if (inner_type == CT_HANDSHAKE) continue; // NewSessionTicket etc.: ignore
if (inner_type == CT_ALERT) return 0; // close_notify or fatal: end of stream
// anything else: ignore and keep reading
}
}
int avail = t->rbuf_len - t->rbuf_off;
int n = avail < len ? avail : len;
memcpy(buf, t->rbuf + t->rbuf_off, n);
t->rbuf_off += n;
return n;
}
void tls_close(tls_conn_t* t) {
if (!t) return;
if (t->tcp) tcp_close(t->tcp);
kfree(t);
}