diff --git a/runtime/src/tls.c b/runtime/src/tls.c new file mode 100644 index 0000000..08c7f5f --- /dev/null +++ b/runtime/src/tls.c @@ -0,0 +1,111 @@ +/* tls.c — hand-rolled TLS 1.3 (runtime-v2 9 phase F). + * + * Phase F1: the record layer (RFC 8446 §5.2). A TLS 1.3 record protects + * + * TLSInnerPlaintext = content || content_type(1) || zero padding + * + * with an AEAD whose additional data is the 5-byte record header and whose + * nonce is the static write IV XOR'd with the 64-bit record sequence number, + * big-endian, left-padded to 12 bytes (§5.3). The outer opaque_type is always + * application_data (23) once traffic is protected; the real type is the last + * non-zero byte of the decrypted inner plaintext. + * + * The AEAD itself is phase A (crypto.h): AES-128-GCM or ChaCha20-Poly1305, + * selected by the negotiated cipher suite. This file adds only the framing. */ + +#include +#include + +#include "tls.h" +#include "crypto.h" + +/* Build the per-record nonce: iv XOR (seq as a big-endian 64-bit value in the + * low 8 bytes). RFC 8446 §5.3. */ +static void record_nonce(const uint8_t iv[12], uint64_t seq, uint8_t nonce[12]) { + memcpy(nonce, iv, 12); + for (int i = 0; i < 8; i++) + nonce[4 + i] ^= (uint8_t)(seq >> (8 * (7 - i))); +} + +/* AEAD seal/open dispatch by suite. aad is the 5-byte header. seal writes + * ct||tag into out (inner_len + 16 bytes); open reads ct||tag from in. */ +static int aead_seal(int suite, const uint8_t *key, size_t keylen, + const uint8_t nonce[12], const uint8_t *aad, size_t aadlen, + const uint8_t *pt, size_t ptlen, uint8_t *out) { + if (suite == WO_TLS_AES_128_GCM_SHA256) + return wo_aes_gcm_seal(key, keylen, nonce, aad, aadlen, pt, ptlen, out); + if (suite == WO_TLS_CHACHA20_POLY1305_SHA256) + return wo_chacha20poly1305_seal(key, nonce, aad, aadlen, pt, ptlen, out); + return -1; +} +static int aead_open(int suite, const uint8_t *key, size_t keylen, + const uint8_t nonce[12], const uint8_t *aad, size_t aadlen, + const uint8_t *ct, size_t ctlen, const uint8_t tag[16], + uint8_t *out) { + if (suite == WO_TLS_AES_128_GCM_SHA256) + return wo_aes_gcm_open(key, keylen, nonce, aad, aadlen, ct, ctlen, tag, out); + if (suite == WO_TLS_CHACHA20_POLY1305_SHA256) + return wo_chacha20poly1305_open(key, nonce, aad, aadlen, ct, ctlen, tag, out); + return -1; +} + +int wo_tls_record_seal(int suite, const uint8_t *key, size_t keylen, + const uint8_t iv[12], uint64_t seq, uint8_t content_type, + const uint8_t *pt, size_t ptlen, uint8_t *out) { + if (suite != WO_TLS_AES_128_GCM_SHA256 && + suite != WO_TLS_CHACHA20_POLY1305_SHA256) + return -1; + + size_t inner_len = ptlen + 1; /* content || content_type */ + size_t payload_len = inner_len + 16; /* + AEAD tag */ + /* 5-byte header: application_data, legacy 0x0303, payload length. */ + out[0] = WO_TLS_CT_APPLICATION_DATA; + out[1] = 0x03; out[2] = 0x03; + out[3] = (uint8_t)(payload_len >> 8); + out[4] = (uint8_t)payload_len; + + /* Assemble the inner plaintext (no padding) in a scratch buffer. */ + uint8_t stackbuf[512]; + uint8_t *inner = inner_len <= sizeof stackbuf ? stackbuf + : (uint8_t *)malloc(inner_len); + if (!inner) return -1; + memcpy(inner, pt, ptlen); + inner[ptlen] = content_type; + + uint8_t nonce[12]; + record_nonce(iv, seq, nonce); + /* AEAD writes ct(inner_len) || tag(16) straight after the header. */ + int rc = aead_seal(suite, key, keylen, nonce, out, 5, inner, inner_len, + out + 5); + if (inner != stackbuf) free(inner); + if (rc != 0) return -1; + return (int)(5 + payload_len); +} + +int wo_tls_record_open(int suite, const uint8_t *key, size_t keylen, + const uint8_t iv[12], uint64_t seq, const uint8_t *rec, + size_t reclen, uint8_t *out, uint8_t *content_type) { + if (suite != WO_TLS_AES_128_GCM_SHA256 && + suite != WO_TLS_CHACHA20_POLY1305_SHA256) + return -1; + if (reclen < 5 + 16) return -1; /* header + at least a tag */ + size_t payload_len = ((size_t)rec[3] << 8) | rec[4]; + if (payload_len + 5 != reclen || payload_len < 16) return -1; + + size_t inner_len = payload_len - 16; + const uint8_t *ct = rec + 5; + const uint8_t *tag = rec + 5 + inner_len; + + uint8_t nonce[12]; + record_nonce(iv, seq, nonce); + /* additional data is the 5-byte header, verbatim. */ + if (aead_open(suite, key, keylen, nonce, rec, 5, ct, inner_len, tag, out) != 0) + return -1; + + /* Strip trailing zero padding; the last non-zero byte is the content type. */ + size_t n = inner_len; + while (n > 0 && out[n - 1] == 0) n--; + if (n == 0) return -1; /* all-zero: no content type */ + *content_type = out[n - 1]; + return (int)(n - 1); +} diff --git a/runtime/src/tls.h b/runtime/src/tls.h new file mode 100644 index 0000000..1f8e7f0 --- /dev/null +++ b/runtime/src/tls.h @@ -0,0 +1,50 @@ +/* tls.h — hand-rolled TLS 1.3 (runtime-v2 9 phase F). Sits on the crypto + * ladder (crypto.h): AEAD (A), HKDF (B), X25519 (C), signatures (D), X.509 + * (E). This header is phase F: the record layer first, the handshake FSM and + * net.connect_tls on top. Internal C; the VM enters through net builtins. */ +#ifndef WO_TLS_H +#define WO_TLS_H + +#include +#include + +/* The two SHA-256 TLS 1.3 cipher suites we implement. AES-128-GCM is + * mandatory-to-implement (RFC 8446 §9.1); ChaCha20-Poly1305 is the portable + * fallback when the CPU has no AES-NI. AES-256-GCM uses SHA-384 and is a later + * add (the HKDF is SHA-256 today). */ +enum { + WO_TLS_AES_128_GCM_SHA256 = 1, + WO_TLS_CHACHA20_POLY1305_SHA256 = 2, +}; + +/* TLS 1.3 record content types (RFC 8446 §5.1). */ +enum { + WO_TLS_CT_CHANGE_CIPHER_SPEC = 20, + WO_TLS_CT_ALERT = 21, + WO_TLS_CT_HANDSHAKE = 22, + WO_TLS_CT_APPLICATION_DATA = 23, +}; + +/* Overhead a sealed record adds over its plaintext: 5-byte header + 1-byte + * inner content-type + 16-byte AEAD tag. */ +#define WO_TLS_RECORD_OVERHEAD 22 + +/* Seal one TLS 1.3 record (RFC 8446 §5.2). Writes the full wire record — + * 5-byte header || encrypted (TLSInnerPlaintext) || 16-byte tag — into out, + * which must hold at least ptlen + WO_TLS_RECORD_OVERHEAD bytes. `seq` is the + * record sequence number; the per-record nonce is iv XOR seq (big-endian, §5.3). + * No padding. Returns the record length, or -1 on a bad suite. */ +int wo_tls_record_seal(int suite, const uint8_t *key, size_t keylen, + const uint8_t iv[12], uint64_t seq, uint8_t content_type, + const uint8_t *pt, size_t ptlen, uint8_t *out); + +/* Open one TLS 1.3 record. `rec` is the full wire record (header included), + * reclen its length. Writes the recovered content into out (must hold + * reclen bytes) and the recovered inner content-type into *content_type, + * after stripping trailing zero padding (§5.2/§5.4). Returns the content + * length, or -1 on a malformed record or AEAD authentication failure. */ +int wo_tls_record_open(int suite, const uint8_t *key, size_t keylen, + const uint8_t iv[12], uint64_t seq, const uint8_t *rec, + size_t reclen, uint8_t *out, uint8_t *content_type); + +#endif diff --git a/runtime/test/gen_tls_record.py b/runtime/test/gen_tls_record.py new file mode 100644 index 0000000..a077435 --- /dev/null +++ b/runtime/test/gen_tls_record.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""TLS 1.3 record-layer KAT vectors (RFC 8446 §5.2). For a fixed key/iv/seq/ +plaintext/content-type, compute the full wire record with python's AEAD as the +oracle, for both AES-128-GCM and ChaCha20-Poly1305. Emits C literals.""" +from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 + +def nonce(iv, seq): + n = bytearray(iv) + for i in range(8): + n[4 + i] ^= (seq >> (8 * (7 - i))) & 0xff + return bytes(n) + +def record(aead, key, iv, seq, ct, pt): + inner = pt + bytes([ct]) # no padding + payload_len = len(inner) + 16 + hdr = bytes([23, 3, 3, payload_len >> 8, payload_len & 0xff]) + enc = aead(key).encrypt(nonce(iv, seq), inner, hdr) + return hdr + enc + +def hexlit(b): + return '"' + "".join("\\x%02x" % x for x in b) + '"' + +# AES-128-GCM: 16-byte key, ChaCha: 32-byte key. Shared iv/seq/pt/type. +aes_key = bytes.fromhex("000102030405060708090a0b0c0d0e0f") +cha_key = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") +iv = bytes.fromhex("cafebabecafebabecafebabe") +seq = 0x0102030405060708 +pt = b"hello writeonce tls" +ct = 22 # handshake + +r_aes = record(AESGCM, aes_key, iv, seq, ct, pt) +r_cha = record(ChaCha20Poly1305, cha_key, iv, seq, ct, pt) + +print("/* Generated by scratchpad/gen_tls_record.py (python cryptography). */") +print("#define TLSREC_IV %s" % hexlit(iv)) +print("#define TLSREC_AESKEY %s" % hexlit(aes_key)) +print("#define TLSREC_CHAKEY %s" % hexlit(cha_key)) +print("#define TLSREC_SEQ 0x%016xULL" % seq) +print("#define TLSREC_CT %d" % ct) +print("#define TLSREC_PT %s" % hexlit(pt)) +print("#define TLSREC_PTLEN %d" % len(pt)) +print("#define TLSREC_AES %s" % hexlit(r_aes)) +print("#define TLSREC_AESLEN %d" % len(r_aes)) +print("#define TLSREC_CHA %s" % hexlit(r_cha)) +print("#define TLSREC_CHALEN %d" % len(r_cha)) diff --git a/runtime/test/test_tls.c b/runtime/test/test_tls.c new file mode 100644 index 0000000..e1b9e17 --- /dev/null +++ b/runtime/test/test_tls.c @@ -0,0 +1,93 @@ +/* test_tls.c — TLS 1.3 record layer (runtime-v2 9 phase F1). KAT against + * python's AEAD as oracle (tls_record_vectors.h), plus seal/open round-trip, + * a tamper-rejection, and the sequence-number nonce advancing. ASan/UBSan. */ +#include +#include + +#include "tls.h" +#include "t.h" +#include "tls_record_vectors.h" + +int main(void) { + uint8_t iv[12], aeskey[16], chakey[32], pt[TLSREC_PTLEN]; + memcpy(iv, TLSREC_IV, 12); + memcpy(aeskey, TLSREC_AESKEY, 16); + memcpy(chakey, TLSREC_CHAKEY, 32); + memcpy(pt, TLSREC_PT, TLSREC_PTLEN); + + /* AES-128-GCM: sealed record must equal python's byte-for-byte. */ + { + uint8_t out[TLSREC_PTLEN + WO_TLS_RECORD_OVERHEAD]; + int n = wo_tls_record_seal(WO_TLS_AES_128_GCM_SHA256, aeskey, 16, iv, + TLSREC_SEQ, TLSREC_CT, pt, TLSREC_PTLEN, out); + T_CHECK(n == TLSREC_AESLEN); + T_CHECK(memcmp(out, TLSREC_AES, TLSREC_AESLEN) == 0); + } + /* ChaCha20-Poly1305: same. */ + { + uint8_t out[TLSREC_PTLEN + WO_TLS_RECORD_OVERHEAD]; + int n = wo_tls_record_seal(WO_TLS_CHACHA20_POLY1305_SHA256, chakey, 32, + iv, TLSREC_SEQ, TLSREC_CT, pt, TLSREC_PTLEN, + out); + T_CHECK(n == TLSREC_CHALEN); + T_CHECK(memcmp(out, TLSREC_CHA, TLSREC_CHALEN) == 0); + } + + /* open() recovers the record python sealed: content, type, length. */ + { + uint8_t rec[TLSREC_AESLEN], out[TLSREC_AESLEN]; uint8_t ct = 0; + memcpy(rec, TLSREC_AES, TLSREC_AESLEN); + int n = wo_tls_record_open(WO_TLS_AES_128_GCM_SHA256, aeskey, 16, iv, + TLSREC_SEQ, rec, TLSREC_AESLEN, out, &ct); + T_CHECK(n == TLSREC_PTLEN); + T_CHECK(ct == TLSREC_CT); + T_CHECK(memcmp(out, pt, TLSREC_PTLEN) == 0); + } + + /* Round-trip both suites over several sequence numbers (nonce advances). */ + for (int suite = 1; suite <= 2; suite++) { + const uint8_t *k = suite == WO_TLS_AES_128_GCM_SHA256 ? aeskey : chakey; + size_t kl = suite == WO_TLS_AES_128_GCM_SHA256 ? 16 : 32; + for (uint64_t seq = 0; seq < 5; seq++) { + uint8_t msg[40], rec[40 + WO_TLS_RECORD_OVERHEAD]; + uint8_t got[sizeof rec]; uint8_t ct = 0; + for (size_t i = 0; i < sizeof msg; i++) msg[i] = (uint8_t)(i + seq); + int n = wo_tls_record_seal(suite, k, kl, iv, seq, + WO_TLS_CT_APPLICATION_DATA, msg, + sizeof msg, rec); + T_CHECK(n > 0); + int m = wo_tls_record_open(suite, k, kl, iv, seq, rec, (size_t)n, + got, &ct); + T_CHECK(m == (int)sizeof msg); + T_CHECK(ct == WO_TLS_CT_APPLICATION_DATA); + T_CHECK(memcmp(got, msg, sizeof msg) == 0); + } + } + + /* A tampered record fails to open; a wrong sequence number fails too. */ + { + uint8_t rec[TLSREC_AESLEN], out[TLSREC_AESLEN]; uint8_t ct = 0; + memcpy(rec, TLSREC_AES, TLSREC_AESLEN); + rec[10] ^= 0x01; + T_CHECK(wo_tls_record_open(WO_TLS_AES_128_GCM_SHA256, aeskey, 16, iv, + TLSREC_SEQ, rec, TLSREC_AESLEN, out, &ct) == -1); + memcpy(rec, TLSREC_AES, TLSREC_AESLEN); + T_CHECK(wo_tls_record_open(WO_TLS_AES_128_GCM_SHA256, aeskey, 16, iv, + TLSREC_SEQ + 1, rec, TLSREC_AESLEN, out, + &ct) == -1); + /* A length-field lie is rejected before the AEAD. */ + memcpy(rec, TLSREC_AES, TLSREC_AESLEN); + rec[4] ^= 0x01; + T_CHECK(wo_tls_record_open(WO_TLS_AES_128_GCM_SHA256, aeskey, 16, iv, + TLSREC_SEQ, rec, TLSREC_AESLEN, out, &ct) == -1); + } + + /* A bad suite id is rejected, not misdispatched. */ + { + uint8_t out[64]; + T_CHECK(wo_tls_record_seal(99, aeskey, 16, iv, 0, 23, pt, TLSREC_PTLEN, + out) == -1); + } + + return t_report("test_tls"); +} diff --git a/runtime/test/tls_record_vectors.h b/runtime/test/tls_record_vectors.h new file mode 100644 index 0000000..5364a36 --- /dev/null +++ b/runtime/test/tls_record_vectors.h @@ -0,0 +1,12 @@ +/* Generated by scratchpad/gen_tls_record.py (python cryptography). */ +#define TLSREC_IV "\xca\xfe\xba\xbe\xca\xfe\xba\xbe\xca\xfe\xba\xbe" +#define TLSREC_AESKEY "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" +#define TLSREC_CHAKEY "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +#define TLSREC_SEQ 0x0102030405060708ULL +#define TLSREC_CT 22 +#define TLSREC_PT "\x68\x65\x6c\x6c\x6f\x20\x77\x72\x69\x74\x65\x6f\x6e\x63\x65\x20\x74\x6c\x73" +#define TLSREC_PTLEN 19 +#define TLSREC_AES "\x17\x03\x03\x00\x24\x37\x16\x16\xb3\xfc\x57\x75\x92\xab\x49\xf1\x68\xcf\x12\x79\x81\x68\x15\x8e\xb3\x7d\x65\x87\x28\xeb\xa2\x58\x79\xac\x9c\x3a\x6f\x08\xa2\x3e\x3e" +#define TLSREC_AESLEN 41 +#define TLSREC_CHA "\x17\x03\x03\x00\x24\xbb\xd8\xe8\x3a\x0b\x08\xf5\x65\x6d\xcc\x12\x4b\x39\x3b\x77\xe2\x2a\x56\xc9\x53\xff\x6d\x6c\x1f\x99\x4a\x86\xf4\xab\x5d\x5d\xaf\x59\x2b\x99\xfb" +#define TLSREC_CHALEN 41