feat(tls): TLS 1.3 record layer (rv2 9 phase F1)
- new tls.c/tls.h on the crypto ladder: wo_tls_record_seal/open (RFC 8446 §5.2) — TLSInnerPlaintext (content||type, no padding), 5-byte header as AEAD additional-data, per-record nonce = iv XOR seq big-endian (§5.3) - suite dispatch: TLS_AES_128_GCM_SHA256 (mandatory) + TLS_CHACHA20_POLY1305_SHA256 (AES-NI-less fallback), over phase-A AEAD - open() strips trailing zero padding to recover the inner content type; rejects a length-field lie before the AEAD, and auth failure after - KAT vs python AEAD oracle (test/gen_tls_record.py): sealed record byte-for-byte both suites, open() recovers it, 5-seq round-trip, tamper + wrong-seq + bad-suite rejected. test_tls 51 pass, ASan/UBSan Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 5021a99f8359f78a642bb0cc2b28ab66f6b624e2)
This commit is contained in:
parent
bb64278aa9
commit
7e71c1a171
5 changed files with 311 additions and 0 deletions
111
runtime/src/tls.c
Normal file
111
runtime/src/tls.c
Normal file
|
|
@ -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 <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
50
runtime/src/tls.h
Normal file
50
runtime/src/tls.h
Normal file
|
|
@ -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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* 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
|
||||
45
runtime/test/gen_tls_record.py
Normal file
45
runtime/test/gen_tls_record.py
Normal file
|
|
@ -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))
|
||||
93
runtime/test/test_tls.c
Normal file
93
runtime/test/test_tls.c
Normal file
|
|
@ -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 <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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");
|
||||
}
|
||||
12
runtime/test/tls_record_vectors.h
Normal file
12
runtime/test/tls_record_vectors.h
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue