feat(tls): TLS 1.3 handshake message layer (rv2 9 phase F3a)
- bounded wire reader/writer (malformation -> reject, overflow -> fail; no over-read on attacker-controlled bytes) - wo_tls_parse_server_hello: extracts negotiated suite + server x25519 key share; rejects HelloRetryRequest, unsupported suite/group, non-1.3 selected_version, and any truncation - wo_tls_build_client_hello: ClientHello offering TLS 1.3 / x25519 / RSA-PSS+RSA-PKCS1+ECDSA-P256, SNI, 32-byte legacy session id - KAT: ServerHello parser vs RFC 8448 recorded message (suite 0x1301 + server pubkey byte-exact), malformed rejected; ClientHello builder structural + SNI/keyshare present + too-small refused, and validated byte-for-byte spec-valid by an independent python parser. test_tls 71 pass, ASan/UBSan clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 541c71bca1655f370b145621d272d2e8bdb6c7ce)
This commit is contained in:
parent
25dda4cb2f
commit
c375110aac
3 changed files with 237 additions and 0 deletions
|
|
@ -167,3 +167,167 @@ void wo_tls_finished_verify(const uint8_t base_key[32],
|
|||
wo_hkdf_sha256_expand_label(base_key, "finished", 8, NULL, 0, finished_key, 32);
|
||||
wo_hmac_sha256(finished_key, 32, transcript_hash, 32, out);
|
||||
}
|
||||
|
||||
/* ---- handshake messages (phase F3, RFC 8446 §4) --------------------------
|
||||
* Serialization uses a bounds-checked writer; parsing a bounds-checked
|
||||
* reader. Every malformation in a parsed message is a rejection — these bytes
|
||||
* are attacker-controlled. TLS wire lengths are fixed-size big-endian. */
|
||||
|
||||
/* handshake types + extension/wire constants we use. */
|
||||
enum { HS_CLIENT_HELLO = 1, HS_SERVER_HELLO = 2 };
|
||||
enum {
|
||||
EXT_SERVER_NAME = 0x0000, EXT_SUPPORTED_GROUPS = 0x000a,
|
||||
EXT_SIGNATURE_ALGORITHMS = 0x000d, EXT_SUPPORTED_VERSIONS = 0x002b,
|
||||
EXT_KEY_SHARE = 0x0033,
|
||||
};
|
||||
enum { GROUP_X25519 = 0x001d };
|
||||
enum { CS_AES_128_GCM = 0x1301, CS_CHACHA20_POLY1305 = 0x1303 };
|
||||
|
||||
/* HelloRetryRequest is a ServerHello carrying this fixed random (§4.1.3). We
|
||||
* do not implement HRR; detect it and reject. */
|
||||
static const uint8_t HRR_RANDOM[32] = {
|
||||
0xCF,0x21,0xAD,0x74,0xE5,0x9A,0x61,0x11,0xBE,0x1D,0x8C,0x02,0x1E,0x65,0xB8,0x91,
|
||||
0xC2,0xA2,0x11,0x16,0x7A,0xBB,0x8C,0x5E,0x07,0x9E,0x09,0xE2,0xC8,0xA8,0x33,0x9C
|
||||
};
|
||||
|
||||
/* ---- bounded reader (all malformation -> ok=0) ---- */
|
||||
typedef struct { const uint8_t *p; size_t n, i; int ok; } rbuf;
|
||||
static uint8_t r8(rbuf *r) { if (r->i >= r->n) { r->ok = 0; return 0; } return r->p[r->i++]; }
|
||||
static uint16_t r16(rbuf *r) { uint16_t v = (uint16_t)r8(r) << 8; return v | r8(r); }
|
||||
static uint32_t r24(rbuf *r) {
|
||||
uint32_t v = (uint32_t)r8(r) << 16; v |= (uint32_t)r8(r) << 8; return v | r8(r);
|
||||
}
|
||||
static const uint8_t *rbytes(rbuf *r, size_t k) {
|
||||
if (!r->ok || r->i + k > r->n) { r->ok = 0; return NULL; }
|
||||
const uint8_t *p = r->p + r->i; r->i += k; return p;
|
||||
}
|
||||
|
||||
/* ---- bounded writer (overflow -> ok=0) ---- */
|
||||
typedef struct { uint8_t *p; size_t cap, n; int ok; } wbuf;
|
||||
static void wbytes(wbuf *w, const uint8_t *b, size_t k) {
|
||||
if (!w->ok || w->n + k > w->cap) { w->ok = 0; return; }
|
||||
memcpy(w->p + w->n, b, k); w->n += k;
|
||||
}
|
||||
static void w8(wbuf *w, uint8_t v) { wbytes(w, &v, 1); }
|
||||
static void w16(wbuf *w, uint16_t v) { uint8_t b[2] = { (uint8_t)(v >> 8), (uint8_t)v }; wbytes(w, b, 2); }
|
||||
/* Reserve a 16-bit length placeholder; returns its offset for backpatch. */
|
||||
static size_t w16_stub(wbuf *w) { size_t at = w->n; w16(w, 0); return at; }
|
||||
static void w16_fill(wbuf *w, size_t at) {
|
||||
if (!w->ok) return;
|
||||
size_t len = w->n - at - 2;
|
||||
w->p[at] = (uint8_t)(len >> 8); w->p[at + 1] = (uint8_t)len;
|
||||
}
|
||||
|
||||
/* Parse a ServerHello handshake message. Extracts the negotiated suite (as a
|
||||
* WO_TLS_* enum) and the server's X25519 key-share. 0 ok, -1 on any
|
||||
* malformation, an unsupported suite/group, or a HelloRetryRequest. */
|
||||
int wo_tls_parse_server_hello(const uint8_t *msg, size_t len, int *suite,
|
||||
uint8_t server_pub[32]) {
|
||||
rbuf r = { msg, len, 0, 1 };
|
||||
if (r8(&r) != HS_SERVER_HELLO) return -1;
|
||||
uint32_t body = r24(&r);
|
||||
if (!r.ok || body != len - 4) return -1;
|
||||
if (r16(&r) != 0x0303) return -1; /* legacy_version */
|
||||
const uint8_t *random = rbytes(&r, 32);
|
||||
if (!random || memcmp(random, HRR_RANDOM, 32) == 0) return -1; /* no HRR */
|
||||
uint8_t sidlen = r8(&r);
|
||||
if (sidlen > 32 || !rbytes(&r, sidlen)) return -1;
|
||||
uint16_t cs = r16(&r);
|
||||
if (cs == CS_AES_128_GCM) *suite = WO_TLS_AES_128_GCM_SHA256;
|
||||
else if (cs == CS_CHACHA20_POLY1305) *suite = WO_TLS_CHACHA20_POLY1305_SHA256;
|
||||
else return -1;
|
||||
if (r8(&r) != 0) return -1; /* legacy_compression */
|
||||
|
||||
uint16_t extlen = r16(&r);
|
||||
const uint8_t *ext = rbytes(&r, extlen);
|
||||
if (!ext) return -1;
|
||||
rbuf e = { ext, extlen, 0, 1 };
|
||||
int have_ks = 0, have_ver = 0;
|
||||
while (e.ok && e.i < e.n) {
|
||||
uint16_t type = r16(&e), elen = r16(&e);
|
||||
const uint8_t *ed = rbytes(&e, elen);
|
||||
if (!ed) return -1;
|
||||
rbuf d = { ed, elen, 0, 1 };
|
||||
if (type == EXT_KEY_SHARE) {
|
||||
if (r16(&d) != GROUP_X25519) return -1; /* group */
|
||||
if (r16(&d) != 32) return -1; /* key_exchange length */
|
||||
const uint8_t *k = rbytes(&d, 32);
|
||||
if (!k) return -1;
|
||||
memcpy(server_pub, k, 32);
|
||||
have_ks = 1;
|
||||
} else if (type == EXT_SUPPORTED_VERSIONS) {
|
||||
if (r16(&d) != 0x0304) return -1; /* selected_version 1.3 */
|
||||
have_ver = 1;
|
||||
}
|
||||
}
|
||||
if (!e.ok || !have_ks || !have_ver) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Build a ClientHello handshake message (offering TLS 1.3, x25519, and
|
||||
* RSA-PSS/RSA-PKCS1/ECDSA-P256 signatures) for `hostname`. random32 and
|
||||
* session_id are caller-supplied (fresh randomness / a 32-byte legacy id for
|
||||
* middlebox compatibility). Writes the message into out; *outlen gets its
|
||||
* length. Returns 0 ok, -1 if out is too small. */
|
||||
int wo_tls_build_client_hello(const char *hostname, size_t hostlen,
|
||||
const uint8_t client_pub[32],
|
||||
const uint8_t random32[32],
|
||||
const uint8_t session_id[32], uint8_t *out,
|
||||
size_t outcap, size_t *outlen) {
|
||||
wbuf w = { out, outcap, 0, 1 };
|
||||
w8(&w, HS_CLIENT_HELLO);
|
||||
/* 3-byte handshake length placeholder, backpatched at the end */
|
||||
size_t hlen_at = w.n; w8(&w, 0); w8(&w, 0); w8(&w, 0);
|
||||
|
||||
w16(&w, 0x0303); /* legacy_version */
|
||||
wbytes(&w, random32, 32);
|
||||
w8(&w, 32); wbytes(&w, session_id, 32); /* legacy_session_id */
|
||||
/* cipher_suites: AES-128-GCM, ChaCha20-Poly1305 */
|
||||
w16(&w, 4); w16(&w, CS_AES_128_GCM); w16(&w, CS_CHACHA20_POLY1305);
|
||||
w8(&w, 1); w8(&w, 0); /* compression: null */
|
||||
|
||||
size_t exts_at = w16_stub(&w); /* extensions length */
|
||||
|
||||
/* server_name (SNI) */
|
||||
w16(&w, EXT_SERVER_NAME);
|
||||
size_t sni_at = w16_stub(&w);
|
||||
w16(&w, (uint16_t)(hostlen + 3)); /* server_name_list len */
|
||||
w8(&w, 0); /* name_type host_name */
|
||||
w16(&w, (uint16_t)hostlen);
|
||||
wbytes(&w, (const uint8_t *)hostname, hostlen);
|
||||
w16_fill(&w, sni_at);
|
||||
|
||||
/* supported_groups: x25519 */
|
||||
w16(&w, EXT_SUPPORTED_GROUPS);
|
||||
w16(&w, 4); w16(&w, 2); w16(&w, GROUP_X25519);
|
||||
|
||||
/* signature_algorithms */
|
||||
w16(&w, EXT_SIGNATURE_ALGORITHMS);
|
||||
w16(&w, 8); w16(&w, 6);
|
||||
w16(&w, 0x0804); /* rsa_pss_rsae_sha256 */
|
||||
w16(&w, 0x0401); /* rsa_pkcs1_sha256 */
|
||||
w16(&w, 0x0403); /* ecdsa_secp256r1_sha256 */
|
||||
|
||||
/* supported_versions: TLS 1.3 */
|
||||
w16(&w, EXT_SUPPORTED_VERSIONS);
|
||||
w16(&w, 3); w8(&w, 2); w16(&w, 0x0304);
|
||||
|
||||
/* key_share: x25519 */
|
||||
w16(&w, EXT_KEY_SHARE);
|
||||
w16(&w, 38); w16(&w, 36); /* ext len, client_shares len */
|
||||
w16(&w, GROUP_X25519); w16(&w, 32);
|
||||
wbytes(&w, client_pub, 32);
|
||||
|
||||
w16_fill(&w, exts_at);
|
||||
|
||||
/* backpatch the 3-byte handshake length */
|
||||
if (w.ok) {
|
||||
size_t blen = w.n - hlen_at - 3;
|
||||
w.p[hlen_at] = (uint8_t)(blen >> 16);
|
||||
w.p[hlen_at + 1] = (uint8_t)(blen >> 8);
|
||||
w.p[hlen_at + 2] = (uint8_t)blen;
|
||||
}
|
||||
if (!w.ok) return -1;
|
||||
*outlen = w.n;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,4 +80,23 @@ void wo_tls_traffic_keys(const uint8_t traffic_secret[32], size_t key_len,
|
|||
void wo_tls_finished_verify(const uint8_t base_key[32],
|
||||
const uint8_t transcript_hash[32], uint8_t out[32]);
|
||||
|
||||
/* ---- handshake messages (phase F3, RFC 8446 §4) -------------------------- */
|
||||
|
||||
/* Parse a ServerHello handshake message (bytes start at the handshake type
|
||||
* 0x02). Fills *suite (a WO_TLS_* enum) and the server's 32-byte X25519 key
|
||||
* share. Returns 0, or -1 on any malformation, an unsupported suite/group, or
|
||||
* a HelloRetryRequest. */
|
||||
int wo_tls_parse_server_hello(const uint8_t *msg, size_t len, int *suite,
|
||||
uint8_t server_pub[32]);
|
||||
|
||||
/* Build a ClientHello handshake message offering TLS 1.3 / x25519 /
|
||||
* RSA-PSS+RSA-PKCS1+ECDSA-P256, for `hostname` (SNI). random32 and the 32-byte
|
||||
* legacy session_id are caller-supplied. Writes into out (cap outcap); *outlen
|
||||
* gets the length. Returns 0, or -1 if the buffer is too small. */
|
||||
int wo_tls_build_client_hello(const char *hostname, size_t hostlen,
|
||||
const uint8_t client_pub[32],
|
||||
const uint8_t random32[32],
|
||||
const uint8_t session_id[32], uint8_t *out,
|
||||
size_t outcap, size_t *outlen);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -8,6 +8,18 @@
|
|||
#include "t.h"
|
||||
#include "tls_record_vectors.h"
|
||||
|
||||
/* RFC 8448 §3 recorded ServerHello handshake message (90 octets). */
|
||||
#define SH_MSG "\x02\x00\x00\x56\x03\x03\xa6\xaf\x06\xa4\x12\x18\x60\xdc\x5e\x6e\x60\x24\x9c\xd3\x4c\x95\x93\x0c\x8a\xc5\xcb\x14\x34\xda\xc1\x55\x77\x2e\xd3\xe2\x69\x28\x00\x13\x01\x00\x00\x2e\x00\x33\x00\x24\x00\x1d\x00\x20\xc9\x82\x88\x76\x11\x20\x95\xfe\x66\x76\x2b\xdb\xf7\xc6\x72\xe1\x56\xd6\xcc\x25\x3b\x83\x3d\xf1\xdd\x69\xb1\xb0\x4e\x75\x1f\x0f\x00\x2b\x00\x02\x03\x04"
|
||||
#define SH_MSG_LEN 90
|
||||
|
||||
/* naive subsequence search (test-only). */
|
||||
static int contains(const uint8_t *hay, size_t hn, const uint8_t *needle, size_t nn) {
|
||||
if (nn > hn) return 0;
|
||||
for (size_t i = 0; i + nn <= hn; i++)
|
||||
if (memcmp(hay + i, needle, nn) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* hex string -> bytes; returns the byte count. */
|
||||
static size_t unhex(const char *h, uint8_t *out) {
|
||||
size_t n = 0;
|
||||
|
|
@ -133,5 +145,47 @@ int main(void) {
|
|||
T_CHECK(memcmp(vv, s_hs_iv, 12) == 0);
|
||||
}
|
||||
|
||||
/* ServerHello parser (phase F3) against the RFC 8448 recorded message. */
|
||||
{
|
||||
uint8_t sh[SH_MSG_LEN]; memcpy(sh, SH_MSG, SH_MSG_LEN);
|
||||
int suite = 0; uint8_t spub[32];
|
||||
T_CHECK(wo_tls_parse_server_hello(sh, SH_MSG_LEN, &suite, spub) == 0);
|
||||
T_CHECK(suite == WO_TLS_AES_128_GCM_SHA256); /* 0x1301 */
|
||||
uint8_t want_spub[32];
|
||||
unhex("c9828876112095fe66762bdbf7c672e156d6cc253b833df1dd69b1b04e751f0f", want_spub);
|
||||
T_CHECK(memcmp(spub, want_spub, 32) == 0);
|
||||
|
||||
/* Malformed inputs are rejected, never over-read. */
|
||||
T_CHECK(wo_tls_parse_server_hello(sh, 10, &suite, spub) == -1); /* truncated */
|
||||
uint8_t bad[SH_MSG_LEN]; memcpy(bad, SH_MSG, SH_MSG_LEN);
|
||||
bad[0] = 0x01; /* wrong handshake type */
|
||||
T_CHECK(wo_tls_parse_server_hello(bad, SH_MSG_LEN, &suite, spub) == -1);
|
||||
memcpy(bad, SH_MSG, SH_MSG_LEN);
|
||||
bad[39] = 0x02; /* cipher suite 0x1302 unsupported */
|
||||
T_CHECK(wo_tls_parse_server_hello(bad, SH_MSG_LEN, &suite, spub) == -1);
|
||||
}
|
||||
|
||||
/* ClientHello builder (phase F3): structural checks + SNI/keyshare present. */
|
||||
{
|
||||
uint8_t cpub[32], rnd[32], sid[32];
|
||||
for (int j = 0; j < 32; j++) { cpub[j] = (uint8_t)j; rnd[j] = (uint8_t)(j + 1); sid[j] = (uint8_t)(j + 2); }
|
||||
const char *host = "api.anthropic.com";
|
||||
uint8_t ch[512]; size_t chl = 0;
|
||||
T_CHECK(wo_tls_build_client_hello(host, strlen(host), cpub, rnd, sid,
|
||||
ch, sizeof ch, &chl) == 0);
|
||||
T_CHECK(ch[0] == 1); /* client_hello */
|
||||
size_t declared = ((size_t)ch[1] << 16) | ((size_t)ch[2] << 8) | ch[3];
|
||||
T_CHECK(declared == chl - 4); /* length field consistent */
|
||||
T_CHECK(contains(ch, chl, (const uint8_t *)host, strlen(host))); /* SNI */
|
||||
T_CHECK(contains(ch, chl, cpub, 32)); /* x25519 key share */
|
||||
/* Our ServerHello parser must not accept a ClientHello. */
|
||||
int suite; uint8_t spub[32];
|
||||
T_CHECK(wo_tls_parse_server_hello(ch, chl, &suite, spub) == -1);
|
||||
/* Too-small buffer refuses cleanly. */
|
||||
uint8_t tiny[32]; size_t tl;
|
||||
T_CHECK(wo_tls_build_client_hello(host, strlen(host), cpub, rnd, sid,
|
||||
tiny, sizeof tiny, &tl) == -1);
|
||||
}
|
||||
|
||||
return t_report("test_tls");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue