feat(tls): certificate chain validation (rv2 9 phase F3c-net security core)

- 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 <noreply@anthropic.com>
(cherry picked from commit 9d40055108d301be32df0e1d4140c23446baa7c6)
This commit is contained in:
shoney.arickathil 2026-09-08 18:26:16 +02:00
parent ad088163cc
commit 5f0ac2d434
3 changed files with 101 additions and 0 deletions

View file

@ -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 */
}

View file

@ -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

View file

@ -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");
}