From 74b153aa0ae4d0b4520cdf28ad6d161d0eac2aa2 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 8 Sep 2026 18:06:36 +0200 Subject: [PATCH] feat(tls): offline handshake verification (rv2 9 phase F3b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wo_tls_verify_cert_verify: verifies a server CertificateVerify (RFC 8446 §4.4.3) — builds the 64-space || context || 0x00 || transcript-hash content, parses the leaf SPKI (phase E) and dispatches to phase-D RSA-PSS / RSA-PKCS1 / ECDSA-P256; the scheme must match the leaf key type. ECDSA sig r/s pulled from its DER SEQ - reuses wo_tls_finished_verify (phase F2) for server + client Finished - KAT: the whole handshake crypto driven offline from the RFC 8448 §3 recorded messages — CertificateVerify (RSA-PSS) VALID, wrong-transcript / tampered-sig / mismatched-scheme rejected, server Finished byte-exact, and the client Finished we would send byte-exact. test_tls 78 pass, ASan/UBSan clean Co-Authored-By: Claude Opus 4.8 (cherry picked from commit afd9f23508c648322712df91329aa117c975ccab) --- runtime/src/tls.c | 70 ++++++++++++++++++++ runtime/src/tls.h | 10 +++ runtime/test/test_tls.c | 48 ++++++++++++++ runtime/test/tls_hs_vectors.h | 121 ++++++++++++++++++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 runtime/test/tls_hs_vectors.h diff --git a/runtime/src/tls.c b/runtime/src/tls.c index d84edd1..aaee9da 100644 --- a/runtime/src/tls.c +++ b/runtime/src/tls.c @@ -13,6 +13,7 @@ * 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 @@ -218,6 +219,75 @@ static void w16_fill(wbuf *w, size_t at) { w->p[at] = (uint8_t)(len >> 8); w->p[at + 1] = (uint8_t)len; } +/* signature_scheme wire values ClientHello offers. */ +enum { + SIG_RSA_PKCS1_SHA256 = 0x0401, + SIG_ECDSA_P256_SHA256 = 0x0403, + SIG_RSA_PSS_SHA256 = 0x0804, +}; + +/* Pull r/s (each padded to 32 bytes) out of a DER ECDSA-Sig-Value + * SEQ { INTEGER r, INTEGER s }. 0 ok, -1 malformed. */ +static int ecdsa_sig_rs(const uint8_t *sig, size_t len, uint8_t r32[32], + uint8_t s32[32]) { + rbuf r = { sig, len, 0, 1 }; + if (r8(&r) != 0x30) return -1; + size_t seqlen = r8(&r); + if (seqlen & 0x80) return -1; /* short-form only here */ + for (int part = 0; part < 2; part++) { + if (r8(&r) != 0x02) return -1; /* INTEGER */ + size_t il = r8(&r); + const uint8_t *iv = rbytes(&r, il); + if (!iv) return -1; + while (il > 0 && iv[0] == 0) { iv++; il--; } /* drop sign byte(s) */ + if (il > 32) return -1; + uint8_t *dst = part == 0 ? r32 : s32; + memset(dst, 0, 32); + memcpy(dst + (32 - il), iv, il); + } + return r.ok ? 0 : -1; +} + +/* Verify a server CertificateVerify (RFC 8446 §4.4.3). The signed content is + * 64*0x20 || "TLS 1.3, server CertificateVerify" || 0x00 || transcript_hash, + * and the signature is over that content under the leaf certificate's key. + * Only the three schemes ClientHello offered are accepted; the scheme must + * match the leaf key type. 1 valid, 0 otherwise. */ +int wo_tls_verify_cert_verify(const uint8_t *leaf_der, size_t leaf_len, + uint16_t sig_scheme, const uint8_t *sig, + size_t sig_len, const uint8_t transcript_hash[32]) { + int key_alg; + const uint8_t *n, *e, *x, *y; size_t nl, el; + if (wo_x509_parse_spki(leaf_der, leaf_len, &key_alg, &n, &nl, &e, &el, &x, &y) != 0) + return 0; + + /* content = 64 spaces || context-string || 0x00 || transcript_hash */ + static const char CTX[] = "TLS 1.3, server CertificateVerify"; + uint8_t content[64 + 33 + 1 + 32]; + memset(content, 0x20, 64); + memcpy(content + 64, CTX, 33); + content[64 + 33] = 0x00; + memcpy(content + 64 + 34, transcript_hash, 32); + uint8_t mhash[32]; + wo_sha256(content, sizeof content, mhash); + + switch (sig_scheme) { + case SIG_RSA_PSS_SHA256: + return key_alg == 1 && + wo_rsa_pss_sha256_verify(n, nl, e, el, sig, sig_len, mhash, 32); + case SIG_RSA_PKCS1_SHA256: + return key_alg == 1 && + wo_rsa_pkcs1_sha256_verify(n, nl, e, el, sig, sig_len, mhash); + case SIG_ECDSA_P256_SHA256: { + uint8_t r32[32], s32[32]; + if (key_alg != 2 || ecdsa_sig_rs(sig, sig_len, r32, s32) != 0) return 0; + return wo_ecdsa_p256_sha256_verify(x, y, r32, s32, mhash); + } + default: + return 0; + } +} + /* 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. */ diff --git a/runtime/src/tls.h b/runtime/src/tls.h index b0fefc0..15d0525 100644 --- a/runtime/src/tls.h +++ b/runtime/src/tls.h @@ -89,6 +89,16 @@ void wo_tls_finished_verify(const uint8_t base_key[32], int wo_tls_parse_server_hello(const uint8_t *msg, size_t len, int *suite, uint8_t server_pub[32]); +/* Verify a server CertificateVerify (RFC 8446 §4.4.3) against the leaf + * certificate DER, given the negotiated signature_scheme wire value, the + * signature, and the running transcript hash (ClientHello..Certificate). Only + * rsa_pss_rsae_sha256 (0x0804), rsa_pkcs1_sha256 (0x0401) and + * ecdsa_secp256r1_sha256 (0x0403) are accepted, and the scheme must match the + * leaf key type. Returns 1 valid, 0 otherwise. */ +int wo_tls_verify_cert_verify(const uint8_t *leaf_der, size_t leaf_len, + uint16_t sig_scheme, const uint8_t *sig, + size_t sig_len, const uint8_t transcript_hash[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 diff --git a/runtime/test/test_tls.c b/runtime/test/test_tls.c index 502da6f..301881c 100644 --- a/runtime/test/test_tls.c +++ b/runtime/test/test_tls.c @@ -5,8 +5,10 @@ #include #include "tls.h" +#include "crypto.h" #include "t.h" #include "tls_record_vectors.h" +#include "tls_hs_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" @@ -187,5 +189,51 @@ int main(void) { tiny, sizeof tiny, &tl) == -1); } + /* Full offline handshake verification (phase F3b) against RFC 8448 §3: + * CertificateVerify (RSA-PSS), server Finished, and the client Finished we + * would send — driven from the recorded handshake messages. */ + { + /* running transcripts over the recorded handshake messages */ + uint8_t buf[2048]; size_t n = 0; + #define ADD(a) do { memcpy(buf + n, a, sizeof a); n += sizeof a; } while (0) + uint8_t th_cert[32], th_cv[32], th_sf[32]; + n = 0; ADD(hs_ch); ADD(hs_sh); ADD(hs_ee); ADD(hs_cert); + wo_sha256(buf, n, th_cert); /* CH..Certificate */ + memcpy(buf + n, hs_cv, sizeof hs_cv); n += sizeof hs_cv; + wo_sha256(buf, n, th_cv); /* CH..CertificateVerify */ + memcpy(buf + n, hs_sfin, sizeof hs_sfin); n += sizeof hs_sfin; + wo_sha256(buf, n, th_sf); /* CH..server Finished */ + #undef ADD + + /* leaf cert out of the Certificate message; sig out of CertificateVerify */ + size_t p = 4; p += 1 + hs_cert[4]; /* skip ctx (len 0) */ + p += 3; /* cert_list length */ + size_t clen = ((size_t)hs_cert[p] << 16) | ((size_t)hs_cert[p+1] << 8) | hs_cert[p+2]; + p += 3; + const uint8_t *leaf = hs_cert + p; + uint16_t scheme = ((uint16_t)hs_cv[4] << 8) | hs_cv[5]; + size_t siglen = ((size_t)hs_cv[6] << 8) | hs_cv[7]; + const uint8_t *sig = hs_cv + 8; + T_CHECK(scheme == 0x0804); /* rsa_pss_rsae_sha256 */ + + T_CHECK(wo_tls_verify_cert_verify(leaf, clen, scheme, sig, siglen, th_cert) == 1); + /* wrong transcript hash and tampered signature both reject */ + uint8_t bad_th[32]; memcpy(bad_th, th_cert, 32); bad_th[0] ^= 1; + T_CHECK(wo_tls_verify_cert_verify(leaf, clen, scheme, sig, siglen, bad_th) == 0); + uint8_t bad_sig[256]; memcpy(bad_sig, sig, siglen); bad_sig[5] ^= 1; + T_CHECK(wo_tls_verify_cert_verify(leaf, clen, scheme, bad_sig, siglen, th_cert) == 0); + /* a scheme that mismatches the RSA leaf key is refused */ + T_CHECK(wo_tls_verify_cert_verify(leaf, clen, 0x0403, sig, siglen, th_cert) == 0); + + /* server Finished: recompute verify_data, compare to the recorded value + * (skip the 4-byte handshake header). */ + uint8_t vd[32]; + wo_tls_finished_verify(hs_s_traffic, th_cv, vd); + T_CHECK(memcmp(vd, hs_sfin + 4, 32) == 0); + /* client Finished we would send matches the recorded one. */ + wo_tls_finished_verify(hs_c_traffic, th_sf, vd); + T_CHECK(memcmp(vd, hs_cfin + 4, 32) == 0); + } + return t_report("test_tls"); } diff --git a/runtime/test/tls_hs_vectors.h b/runtime/test/tls_hs_vectors.h new file mode 100644 index 0000000..812cabe --- /dev/null +++ b/runtime/test/tls_hs_vectors.h @@ -0,0 +1,121 @@ +/* Generated from RFC 8448 §3 'Simple 1-RTT Handshake' by + * scratchpad gen (see commit msg). Handshake messages + secrets for the + * phase-F3b offline handshake-verification KAT. */ +static const unsigned char hs_ch[] = { + 0x01,0x00,0x00,0xc0,0x03,0x03,0xcb,0x34,0xec,0xb1,0xe7,0x81, + 0x63,0xba,0x1c,0x38,0xc6,0xda,0xcb,0x19,0x6a,0x6d,0xff,0xa2, + 0x1a,0x8d,0x99,0x12,0xec,0x18,0xa2,0xef,0x62,0x83,0x02,0x4d, + 0xec,0xe7,0x00,0x00,0x06,0x13,0x01,0x13,0x03,0x13,0x02,0x01, + 0x00,0x00,0x91,0x00,0x00,0x00,0x0b,0x00,0x09,0x00,0x00,0x06, + 0x73,0x65,0x72,0x76,0x65,0x72,0xff,0x01,0x00,0x01,0x00,0x00, + 0x0a,0x00,0x14,0x00,0x12,0x00,0x1d,0x00,0x17,0x00,0x18,0x00, + 0x19,0x01,0x00,0x01,0x01,0x01,0x02,0x01,0x03,0x01,0x04,0x00, + 0x23,0x00,0x00,0x00,0x33,0x00,0x26,0x00,0x24,0x00,0x1d,0x00, + 0x20,0x99,0x38,0x1d,0xe5,0x60,0xe4,0xbd,0x43,0xd2,0x3d,0x8e, + 0x43,0x5a,0x7d,0xba,0xfe,0xb3,0xc0,0x6e,0x51,0xc1,0x3c,0xae, + 0x4d,0x54,0x13,0x69,0x1e,0x52,0x9a,0xaf,0x2c,0x00,0x2b,0x00, + 0x03,0x02,0x03,0x04,0x00,0x0d,0x00,0x20,0x00,0x1e,0x04,0x03, + 0x05,0x03,0x06,0x03,0x02,0x03,0x08,0x04,0x08,0x05,0x08,0x06, + 0x04,0x01,0x05,0x01,0x06,0x01,0x02,0x01,0x04,0x02,0x05,0x02, + 0x06,0x02,0x02,0x02,0x00,0x2d,0x00,0x02,0x01,0x01,0x00,0x1c, + 0x00,0x02,0x40,0x01, +}; + +static const unsigned char hs_sh[] = { + 0x02,0x00,0x00,0x56,0x03,0x03,0xa6,0xaf,0x06,0xa4,0x12,0x18, + 0x60,0xdc,0x5e,0x6e,0x60,0x24,0x9c,0xd3,0x4c,0x95,0x93,0x0c, + 0x8a,0xc5,0xcb,0x14,0x34,0xda,0xc1,0x55,0x77,0x2e,0xd3,0xe2, + 0x69,0x28,0x00,0x13,0x01,0x00,0x00,0x2e,0x00,0x33,0x00,0x24, + 0x00,0x1d,0x00,0x20,0xc9,0x82,0x88,0x76,0x11,0x20,0x95,0xfe, + 0x66,0x76,0x2b,0xdb,0xf7,0xc6,0x72,0xe1,0x56,0xd6,0xcc,0x25, + 0x3b,0x83,0x3d,0xf1,0xdd,0x69,0xb1,0xb0,0x4e,0x75,0x1f,0x0f, + 0x00,0x2b,0x00,0x02,0x03,0x04, +}; + +static const unsigned char hs_ee[] = { + 0x08,0x00,0x00,0x24,0x00,0x22,0x00,0x0a,0x00,0x14,0x00,0x12, + 0x00,0x1d,0x00,0x17,0x00,0x18,0x00,0x19,0x01,0x00,0x01,0x01, + 0x01,0x02,0x01,0x03,0x01,0x04,0x00,0x1c,0x00,0x02,0x40,0x01, + 0x00,0x00,0x00,0x00, +}; + +static const unsigned char hs_cert[] = { + 0x0b,0x00,0x01,0xb9,0x00,0x00,0x01,0xb5,0x00,0x01,0xb0,0x30, + 0x82,0x01,0xac,0x30,0x82,0x01,0x15,0xa0,0x03,0x02,0x01,0x02, + 0x02,0x01,0x02,0x30,0x0d,0x06,0x09,0x2a,0x86,0x48,0x86,0xf7, + 0x0d,0x01,0x01,0x0b,0x05,0x00,0x30,0x0e,0x31,0x0c,0x30,0x0a, + 0x06,0x03,0x55,0x04,0x03,0x13,0x03,0x72,0x73,0x61,0x30,0x1e, + 0x17,0x0d,0x31,0x36,0x30,0x37,0x33,0x30,0x30,0x31,0x32,0x33, + 0x35,0x39,0x5a,0x17,0x0d,0x32,0x36,0x30,0x37,0x33,0x30,0x30, + 0x31,0x32,0x33,0x35,0x39,0x5a,0x30,0x0e,0x31,0x0c,0x30,0x0a, + 0x06,0x03,0x55,0x04,0x03,0x13,0x03,0x72,0x73,0x61,0x30,0x81, + 0x9f,0x30,0x0d,0x06,0x09,0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01, + 0x01,0x01,0x05,0x00,0x03,0x81,0x8d,0x00,0x30,0x81,0x89,0x02, + 0x81,0x81,0x00,0xb4,0xbb,0x49,0x8f,0x82,0x79,0x30,0x3d,0x98, + 0x08,0x36,0x39,0x9b,0x36,0xc6,0x98,0x8c,0x0c,0x68,0xde,0x55, + 0xe1,0xbd,0xb8,0x26,0xd3,0x90,0x1a,0x24,0x61,0xea,0xfd,0x2d, + 0xe4,0x9a,0x91,0xd0,0x15,0xab,0xbc,0x9a,0x95,0x13,0x7a,0xce, + 0x6c,0x1a,0xf1,0x9e,0xaa,0x6a,0xf9,0x8c,0x7c,0xed,0x43,0x12, + 0x09,0x98,0xe1,0x87,0xa8,0x0e,0xe0,0xcc,0xb0,0x52,0x4b,0x1b, + 0x01,0x8c,0x3e,0x0b,0x63,0x26,0x4d,0x44,0x9a,0x6d,0x38,0xe2, + 0x2a,0x5f,0xda,0x43,0x08,0x46,0x74,0x80,0x30,0x53,0x0e,0xf0, + 0x46,0x1c,0x8c,0xa9,0xd9,0xef,0xbf,0xae,0x8e,0xa6,0xd1,0xd0, + 0x3e,0x2b,0xd1,0x93,0xef,0xf0,0xab,0x9a,0x80,0x02,0xc4,0x74, + 0x28,0xa6,0xd3,0x5a,0x8d,0x88,0xd7,0x9f,0x7f,0x1e,0x3f,0x02, + 0x03,0x01,0x00,0x01,0xa3,0x1a,0x30,0x18,0x30,0x09,0x06,0x03, + 0x55,0x1d,0x13,0x04,0x02,0x30,0x00,0x30,0x0b,0x06,0x03,0x55, + 0x1d,0x0f,0x04,0x04,0x03,0x02,0x05,0xa0,0x30,0x0d,0x06,0x09, + 0x2a,0x86,0x48,0x86,0xf7,0x0d,0x01,0x01,0x0b,0x05,0x00,0x03, + 0x81,0x81,0x00,0x85,0xaa,0xd2,0xa0,0xe5,0xb9,0x27,0x6b,0x90, + 0x8c,0x65,0xf7,0x3a,0x72,0x67,0x17,0x06,0x18,0xa5,0x4c,0x5f, + 0x8a,0x7b,0x33,0x7d,0x2d,0xf7,0xa5,0x94,0x36,0x54,0x17,0xf2, + 0xea,0xe8,0xf8,0xa5,0x8c,0x8f,0x81,0x72,0xf9,0x31,0x9c,0xf3, + 0x6b,0x7f,0xd6,0xc5,0x5b,0x80,0xf2,0x1a,0x03,0x01,0x51,0x56, + 0x72,0x60,0x96,0xfd,0x33,0x5e,0x5e,0x67,0xf2,0xdb,0xf1,0x02, + 0x70,0x2e,0x60,0x8c,0xca,0xe6,0xbe,0xc1,0xfc,0x63,0xa4,0x2a, + 0x99,0xbe,0x5c,0x3e,0xb7,0x10,0x7c,0x3c,0x54,0xe9,0xb9,0xeb, + 0x2b,0xd5,0x20,0x3b,0x1c,0x3b,0x84,0xe0,0xa8,0xb2,0xf7,0x59, + 0x40,0x9b,0xa3,0xea,0xc9,0xd9,0x1d,0x40,0x2d,0xcc,0x0c,0xc8, + 0xf8,0x96,0x12,0x29,0xac,0x91,0x87,0xb4,0x2b,0x4d,0xe1,0x00, + 0x00, +}; + +static const unsigned char hs_cv[] = { + 0x0f,0x00,0x00,0x84,0x08,0x04,0x00,0x80,0x5a,0x74,0x7c,0x5d, + 0x88,0xfa,0x9b,0xd2,0xe5,0x5a,0xb0,0x85,0xa6,0x10,0x15,0xb7, + 0x21,0x1f,0x82,0x4c,0xd4,0x84,0x14,0x5a,0xb3,0xff,0x52,0xf1, + 0xfd,0xa8,0x47,0x7b,0x0b,0x7a,0xbc,0x90,0xdb,0x78,0xe2,0xd3, + 0x3a,0x5c,0x14,0x1a,0x07,0x86,0x53,0xfa,0x6b,0xef,0x78,0x0c, + 0x5e,0xa2,0x48,0xee,0xaa,0xa7,0x85,0xc4,0xf3,0x94,0xca,0xb6, + 0xd3,0x0b,0xbe,0x8d,0x48,0x59,0xee,0x51,0x1f,0x60,0x29,0x57, + 0xb1,0x54,0x11,0xac,0x02,0x76,0x71,0x45,0x9e,0x46,0x44,0x5c, + 0x9e,0xa5,0x8c,0x18,0x1e,0x81,0x8e,0x95,0xb8,0xc3,0xfb,0x0b, + 0xf3,0x27,0x84,0x09,0xd3,0xbe,0x15,0x2a,0x3d,0xa5,0x04,0x3e, + 0x06,0x3d,0xda,0x65,0xcd,0xf5,0xae,0xa2,0x0d,0x53,0xdf,0xac, + 0xd4,0x2f,0x74,0xf3, +}; + +static const unsigned char hs_sfin[] = { + 0x14,0x00,0x00,0x20,0x9b,0x9b,0x14,0x1d,0x90,0x63,0x37,0xfb, + 0xd2,0xcb,0xdc,0xe7,0x1d,0xf4,0xde,0xda,0x4a,0xb4,0x2c,0x30, + 0x95,0x72,0xcb,0x7f,0xff,0xee,0x54,0x54,0xb7,0x8f,0x07,0x18, +}; + +static const unsigned char hs_cfin[] = { + 0x14,0x00,0x00,0x20,0xa8,0xec,0x43,0x6d,0x67,0x76,0x34,0xae, + 0x52,0x5a,0xc1,0xfc,0xeb,0xe1,0x1a,0x03,0x9e,0xc1,0x76,0x94, + 0xfa,0xc6,0xe9,0x85,0x27,0xb6,0x42,0xf2,0xed,0xd5,0xce,0x61, +}; + +static const unsigned char hs_c_traffic[] = { + 0xb3,0xed,0xdb,0x12,0x6e,0x06,0x7f,0x35,0xa7,0x80,0xb3,0xab, + 0xf4,0x5e,0x2d,0x8f,0x3b,0x1a,0x95,0x07,0x38,0xf5,0x2e,0x96, + 0x00,0x74,0x6a,0x0e,0x27,0xa5,0x5a,0x21, +}; + +static const unsigned char hs_s_traffic[] = { + 0xb6,0x7b,0x7d,0x69,0x0c,0xc1,0x6c,0x4e,0x75,0xe5,0x42,0x13, + 0xcb,0x2d,0x37,0xb4,0xe9,0xc9,0x12,0xbc,0xde,0xd9,0x10,0x5d, + 0x42,0xbe,0xfd,0x59,0xd3,0x91,0xad,0x38, +}; +