From 5f0ac2d4344064e9ec7d9a77693945c921d9e861 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 8 Sep 2026 18:26:16 +0200 Subject: [PATCH] feat(tls): certificate chain validation (rv2 9 phase F3c-net security core) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wo_tls_verify_chain: leaf-first DER chain — each cert signed by the next, the top trusted (equal to, or signed by, a trust anchor), the leaf SAN matching host, every cert temporally valid. Any failure rejects; no partial trust. Pure over the phase-D/E verifiers, so offline-testable - KAT with the phase-E RSA + EC chains: leaf trusted via its issuing CA anchor; wrong-anchor / wrong-host / expired / broken-link / no-anchor all rejected; two-cert chain with a byte-equal root anchor; NULL host skips the SAN check. test_tls 100 pass, ASan/UBSan clean - remaining F3c-net (live-gated): CA-bundle PEM loader, random ephemeral, the net.connect_tls builtin driving the sans-io driver over a real fd Co-Authored-By: Claude Opus 4.8 (cherry picked from commit 9d40055108d301be32df0e1d4140c23446baa7c6) --- runtime/src/tls.c | 38 ++++++++++++++++++++++++++++++ runtime/src/tls.h | 12 ++++++++++ runtime/test/test_tls.c | 51 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/runtime/src/tls.c b/runtime/src/tls.c index 99acd2a..0b53bd3 100644 --- a/runtime/src/tls.c +++ b/runtime/src/tls.c @@ -610,3 +610,41 @@ int wo_tls_client_decrypt(wo_tls_client *c, const uint8_t *rec, size_t reclen, c->rd_seq++; return n; } + +/* ---- certificate chain validation (phase F3c-net security core) ---------- + * Verify a server certificate chain (leaf-first DER). Each cert must be signed + * by the next; the chain top must be trusted (equal to, or signed by, a trust + * anchor); the leaf SAN must match the host; and every cert must be inside its + * validity window. Any failure is a rejection — no partial trust. Pure over + * the public phase-D/E verifiers, so it is offline-testable; the CA-bundle load + * and socket glue that feed it are the live-gated remainder of F3c-net. */ +int wo_tls_verify_chain(const uint8_t *const *certs, const size_t *cert_lens, + size_t n_certs, const uint8_t *const *anchors, + const size_t *anchor_lens, size_t n_anchors, + const char *host, size_t hostlen, const char now14[14]) { + if (n_certs == 0 || n_anchors == 0) return 0; + + /* leaf hostname (SAN) must match. */ + if (host && !wo_x509_check_host(certs[0], cert_lens[0], host, hostlen)) + return 0; + + /* every cert must be temporally valid. */ + for (size_t i = 0; i < n_certs; i++) + if (!wo_x509_check_validity(certs[i], cert_lens[i], now14)) return 0; + + /* each cert is signed by the next one the server sent. */ + for (size_t i = 0; i + 1 < n_certs; i++) + if (!wo_x509_verify_one(certs[i], cert_lens[i], certs[i + 1], cert_lens[i + 1])) + return 0; + + /* the chain top must chain to a trust anchor: either it is one verbatim, or + * an anchor signed it. */ + const uint8_t *top = certs[n_certs - 1]; size_t toplen = cert_lens[n_certs - 1]; + for (size_t a = 0; a < n_anchors; a++) { + if (toplen == anchor_lens[a] && memcmp(top, anchors[a], toplen) == 0) + return 1; /* server sent the root */ + if (wo_x509_verify_one(top, toplen, anchors[a], anchor_lens[a])) + return 1; /* anchor signed the top */ + } + return 0; /* untrusted */ +} diff --git a/runtime/src/tls.h b/runtime/src/tls.h index 2e0255d..ad94f9b 100644 --- a/runtime/src/tls.h +++ b/runtime/src/tls.h @@ -109,6 +109,18 @@ int wo_tls_build_client_hello(const char *hostname, size_t hostlen, const uint8_t session_id[32], uint8_t *out, size_t outcap, size_t *outlen); +/* Verify a server certificate chain (leaf-first DER): each cert signed by the + * next, the chain top trusted (equal to, or signed by, one of the anchors), the + * leaf SAN matching host (pass NULL to skip), and every cert within its + * validity window at now14 ("YYYYMMDDHHMMSS"). 1 fully valid & trusted, 0 + * otherwise (no partial trust). The offline-testable security core of the + * F3c-net remainder; the CA-bundle load + socket glue that feed it are still to + * come. */ +int wo_tls_verify_chain(const uint8_t *const *certs, const size_t *cert_lens, + size_t n_certs, const uint8_t *const *anchors, + const size_t *anchor_lens, size_t n_anchors, + const char *host, size_t hostlen, const char now14[14]); + /* ---- sans-io client handshake driver (phase F3c) ------------------------- * A pure state machine: no sockets. The caller frames TLS records (read the * 5-byte header, then that many bytes) and feeds whole records in; the driver diff --git a/runtime/test/test_tls.c b/runtime/test/test_tls.c index d8fbe00..db2b088 100644 --- a/runtime/test/test_tls.c +++ b/runtime/test/test_tls.c @@ -10,6 +10,7 @@ #include "tls_record_vectors.h" #include "tls_hs_vectors.h" #include "tls_driver_vectors.h" +#include "x509_vectors.h" /* phase-E RSA + EC chains, for chain validation */ /* 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" @@ -309,5 +310,55 @@ int main(void) { T_CHECK(wo_tls_client_push_record(&c, rfl, sizeof drv_rec_flight) == WO_TLS_FAILED); } + /* Certificate chain validation (phase F3c-net security core) with the + * phase-E RSA + EC chains (kat_rsa_ca signs kat_rsa_leaf, SAN + * leaf.example.com; kat_ec_ca signs kat_ec_leaf, SAN leaf.example.org). */ + { + const char *NOW = "20250101000000"; /* inside 2020..2030 */ + const uint8_t *leaf1[] = { kat_rsa_leaf }; + size_t leaf1n[] = { sizeof kat_rsa_leaf }; + const uint8_t *rsa_anchor[] = { kat_rsa_ca }; + size_t rsa_anchor_n[] = { sizeof kat_rsa_ca }; + const uint8_t *ec_anchor[] = { kat_ec_ca }; + size_t ec_anchor_n[] = { sizeof kat_ec_ca }; + + /* leaf trusted via its issuing CA anchor + host + validity */ + T_CHECK(wo_tls_verify_chain(leaf1, leaf1n, 1, rsa_anchor, rsa_anchor_n, 1, + "leaf.example.com", 16, NOW) == 1); + /* wrong anchor (EC CA did not sign the RSA leaf) -> untrusted */ + T_CHECK(wo_tls_verify_chain(leaf1, leaf1n, 1, ec_anchor, ec_anchor_n, 1, + "leaf.example.com", 16, NOW) == 0); + /* wrong host -> reject */ + T_CHECK(wo_tls_verify_chain(leaf1, leaf1n, 1, rsa_anchor, rsa_anchor_n, 1, + "evil.example.com", 16, NOW) == 0); + /* expired (before validity) -> reject */ + T_CHECK(wo_tls_verify_chain(leaf1, leaf1n, 1, rsa_anchor, rsa_anchor_n, 1, + "leaf.example.com", 16, "20190101000000") == 0); + /* NULL host skips the SAN check (still trusted) */ + T_CHECK(wo_tls_verify_chain(leaf1, leaf1n, 1, rsa_anchor, rsa_anchor_n, 1, + NULL, 0, NOW) == 1); + + /* two-cert chain [leaf, ca] with the CA also supplied as the anchor: + * links leaf->ca and the top (ca) equals the anchor verbatim. */ + const uint8_t *chain2[] = { kat_rsa_leaf, kat_rsa_ca }; + size_t chain2n[] = { sizeof kat_rsa_leaf, sizeof kat_rsa_ca }; + T_CHECK(wo_tls_verify_chain(chain2, chain2n, 2, rsa_anchor, rsa_anchor_n, 1, + "leaf.example.com", 16, NOW) == 1); + /* a broken link (leaf not signed by an unrelated top) -> reject */ + const uint8_t *badchain[] = { kat_rsa_leaf, kat_ec_ca }; + size_t badchainn[] = { sizeof kat_rsa_leaf, sizeof kat_ec_ca }; + T_CHECK(wo_tls_verify_chain(badchain, badchainn, 2, rsa_anchor, rsa_anchor_n, 1, + "leaf.example.com", 16, NOW) == 0); + + /* EC chain trusts via its EC CA */ + const uint8_t *ecleaf[] = { kat_ec_leaf }; + size_t ecleafn[] = { sizeof kat_ec_leaf }; + T_CHECK(wo_tls_verify_chain(ecleaf, ecleafn, 1, ec_anchor, ec_anchor_n, 1, + "leaf.example.org", 16, NOW) == 1); + /* no anchors -> never trusted */ + T_CHECK(wo_tls_verify_chain(leaf1, leaf1n, 1, NULL, NULL, 0, + "leaf.example.com", 16, NOW) == 0); + } + return t_report("test_tls"); }