feat(tls): SAN/hostname verification + driver enforcement (rv2 9 phase E/F3c)
- wo_x509_check_host: match a hostname against the cert subjectAltName dNSNames (RFC 6125) — case-insensitive, single left-most wildcard that covers exactly one label; no SAN => refused; no legacy CN fallback. Completes the phase-E deferred hostname check (walks the [3] extensions) - wo_tls_client_set_host + driver enforcement: with a host set, a leaf whose SAN does not match is refused at the Certificate step (MITM defense); unset skips the check (offline testing only, documented unsafe) - KAT: exact/case-insensitive/mismatch, no-SAN refused, wildcard one-label (not zero, not sub-label) via a wildcard-SAN cert; driver refuses the RFC 8448 leaf (no SAN) once a host is set. test_crypto 95, test_tls 91, ASan/UBSan clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 319ce8bfcf608030f958b36ab2c8f62fd76e1740)
This commit is contained in:
parent
d7e7eff6b5
commit
d7a6888931
7 changed files with 171 additions and 3 deletions
|
|
@ -1485,6 +1485,9 @@ typedef struct {
|
|||
const uint8_t *ec_x, *ec_y; /* 32 bytes each when EC P-256 */
|
||||
/* validity as YYYYMMDDHHMMSSZ-comparable 14-byte strings */
|
||||
char not_before[15], not_after[15];
|
||||
/* the bytes of tbsCertificate after subjectPublicKeyInfo — optional
|
||||
* uniqueIDs then the [3] extensions; walked lazily by the SAN check. */
|
||||
const uint8_t *ext_area; size_t ext_area_len;
|
||||
} x509_cert;
|
||||
|
||||
enum { WO_X509_SIG_RSA_PKCS1_SHA256 = 1, WO_X509_SIG_RSA_PSS_SHA256, WO_X509_SIG_ECDSA_P256_SHA256, WO_X509_SIG_UNKNOWN = 0 };
|
||||
|
|
@ -1596,12 +1599,92 @@ static int x509_parse(const uint8_t *der_buf, size_t len, x509_cert *c) {
|
|||
} else {
|
||||
return -1;
|
||||
}
|
||||
/* extensions [3] (incl. SAN) are left for phase F, where the target
|
||||
* hostname is known and can be matched. Chain signature, SPKI and
|
||||
* validity are settled here. */
|
||||
/* Remaining tbs bytes (optional uniqueIDs + [3] extensions). The chain
|
||||
* signature, SPKI and validity are settled above; the SAN/hostname check
|
||||
* walks this area on demand (wo_x509_check_host). */
|
||||
c->ext_area = tbs.p; c->ext_area_len = (size_t)(tbs.end - tbs.p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const uint8_t OID_SAN[] = { 0x55, 0x1d, 0x11 }; /* 2.5.29.17 */
|
||||
|
||||
/* Case-insensitive match of a presented dNSName pattern against a hostname,
|
||||
* with a single left-most "*" wildcard (RFC 6125 §6.4.3): "*.example.com"
|
||||
* matches one label, never a bare "example.com" or a dotted sub-label. */
|
||||
static int host_match(const char *pat, size_t patlen, const char *host,
|
||||
size_t hostlen) {
|
||||
if (patlen == 0 || hostlen == 0) return 0;
|
||||
if (pat[0] == '*') {
|
||||
/* pattern is "*"+rest; rest must start with '.'. Match the suffix and
|
||||
* require the wildcard to cover exactly one (non-empty, dot-free) label. */
|
||||
if (patlen < 2 || pat[1] != '.') return 0;
|
||||
const char *rest = pat + 1; size_t restlen = patlen - 1;
|
||||
if (hostlen <= restlen) return 0;
|
||||
size_t hlead = hostlen - restlen; /* the part '*' must cover */
|
||||
for (size_t i = 0; i < hlead; i++)
|
||||
if (host[i] == '.') return 0; /* no dot in the wildcard */
|
||||
/* suffix compare, case-insensitive */
|
||||
for (size_t i = 0; i < restlen; i++) {
|
||||
char a = rest[i], b = host[hlead + i];
|
||||
if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
|
||||
if (b >= 'A' && b <= 'Z') b = (char)(b + 32);
|
||||
if (a != b) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (patlen != hostlen) return 0;
|
||||
for (size_t i = 0; i < patlen; i++) {
|
||||
char a = pat[i], b = host[i];
|
||||
if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
|
||||
if (b >= 'A' && b <= 'Z') b = (char)(b + 32);
|
||||
if (a != b) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Verify `hostname` against the certificate's subjectAltName dNSName entries
|
||||
* (RFC 6125). Returns 1 if any SAN dNSName matches, 0 otherwise (including no
|
||||
* SAN present — a cert without SAN is not accepted for a hostname). The legacy
|
||||
* CN fallback is deliberately not implemented. */
|
||||
int wo_x509_check_host(const uint8_t *cert_der, size_t cert_len,
|
||||
const char *hostname, size_t hostlen) {
|
||||
x509_cert c;
|
||||
if (x509_parse(cert_der, cert_len, &c) != 0) return 0;
|
||||
der r = { c.ext_area, c.ext_area + c.ext_area_len };
|
||||
/* skip optional issuerUniqueID [1] (0x81) / subjectUniqueID [2] (0x82) */
|
||||
while (r.p < r.end && (*r.p == 0x81 || *r.p == 0x82))
|
||||
if (der_skip(&r) < 0) return 0;
|
||||
der exp;
|
||||
if (der_into(&r, 0xA3, &exp) < 0) return 0; /* [3] EXPLICIT */
|
||||
der exts;
|
||||
if (der_into(&exp, 0x30, &exts) < 0) return 0; /* SEQUENCE OF Extension */
|
||||
while (exts.p < exts.end) {
|
||||
der ext;
|
||||
if (der_into(&exts, 0x30, &ext) < 0) return 0;
|
||||
const uint8_t *oid; size_t oidlen;
|
||||
if (der_tlv(&ext, &oid, &oidlen) != 0x06) return 0;
|
||||
/* optional critical BOOLEAN */
|
||||
if (ext.p < ext.end && *ext.p == 0x01)
|
||||
if (der_skip(&ext) < 0) return 0;
|
||||
const uint8_t *val; size_t vallen;
|
||||
if (der_tlv(&ext, &val, &vallen) != 0x04) return 0; /* OCTET STRING */
|
||||
if (!oid_eq(oid, oidlen, OID_SAN, sizeof OID_SAN)) continue;
|
||||
/* val = SEQUENCE OF GeneralName; dNSName is [2] IMPLICIT IA5String. */
|
||||
der names = { val, val + vallen }, seq;
|
||||
if (der_into(&names, 0x30, &seq) < 0) return 0;
|
||||
while (seq.p < seq.end) {
|
||||
const uint8_t *gn; size_t gnlen;
|
||||
int tag = der_tlv(&seq, &gn, &gnlen);
|
||||
if (tag < 0) return 0;
|
||||
if (tag == 0x82 && /* dNSName */
|
||||
host_match((const char *)gn, gnlen, hostname, hostlen))
|
||||
return 1;
|
||||
}
|
||||
return 0; /* SAN present, no match */
|
||||
}
|
||||
return 0; /* no SAN extension */
|
||||
}
|
||||
|
||||
/* Verify `c`'s signature over its tbsCertificate using an issuer public key
|
||||
* already parsed into `issuer`. 1 valid, 0 otherwise. */
|
||||
static int x509_verify_sig(const x509_cert *c, const x509_cert *issuer) {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ int wo_x509_parse_spki(const uint8_t *cert_der, size_t cert_len, int *key_alg,
|
|||
const uint8_t **ec_x, const uint8_t **ec_y);
|
||||
int wo_x509_check_validity(const uint8_t *cert_der, size_t cert_len,
|
||||
const char now14[14]);
|
||||
/* Match hostname against the cert's subjectAltName dNSNames (RFC 6125, single
|
||||
* left-most wildcard). 1 match, 0 otherwise (no SAN => 0; no CN fallback). */
|
||||
int wo_x509_check_host(const uint8_t *cert_der, size_t cert_len,
|
||||
const char *hostname, size_t hostlen);
|
||||
|
||||
int wo_builtin_crypto(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
|
||||
|
||||
|
|
|
|||
|
|
@ -443,6 +443,10 @@ int wo_tls_client_start_with(wo_tls_client *c, const uint8_t *ch_msg,
|
|||
return 0;
|
||||
}
|
||||
|
||||
void wo_tls_client_set_host(wo_tls_client *c, const char *host, size_t hostlen) {
|
||||
c->host = host; c->hostlen = hostlen;
|
||||
}
|
||||
|
||||
size_t wo_tls_client_take_output(wo_tls_client *c, uint8_t *out, size_t outcap) {
|
||||
size_t n = c->outn < outcap ? c->outn : 0; /* all-or-nothing */
|
||||
if (n) { memcpy(out, c->out, n); c->outn = 0; }
|
||||
|
|
@ -492,6 +496,10 @@ static int on_flight_msg(wo_tls_client *c, const uint8_t *msg, size_t mlen) {
|
|||
p += 3;
|
||||
if (p + clen > mlen || clen > sizeof c->leaf) return -1;
|
||||
memcpy(c->leaf, msg + p, clen); c->leaflen = clen;
|
||||
/* hostname check (when a host was set): a leaf whose SAN does not match
|
||||
* the target host is a refused connection, not a warning. */
|
||||
if (c->host && !wo_x509_check_host(c->leaf, c->leaflen, c->host, c->hostlen))
|
||||
return -1;
|
||||
return tr_add(c, msg, mlen) == 0 ? 0 : -1;
|
||||
}
|
||||
if (type == 0x0f) { /* CertificateVerify */
|
||||
|
|
|
|||
|
|
@ -145,9 +145,17 @@ typedef struct {
|
|||
uint8_t leaf[WO_TLS_LEAF_MAX]; size_t leaflen;
|
||||
uint16_t cv_scheme;
|
||||
uint8_t out[1024]; size_t outn; /* bytes for the caller to send */
|
||||
const char *host; size_t hostlen; /* if set, leaf SAN is enforced */
|
||||
int st; /* internal FSM state */
|
||||
} wo_tls_client;
|
||||
|
||||
/* Enforce the leaf certificate's SAN against `host` during the handshake — a
|
||||
* connection whose certificate does not match is refused. MUST be called
|
||||
* (after start) for a real connection; if left unset the driver skips the
|
||||
* hostname check (offline testing only, and MITM-unsafe on the wire). The
|
||||
* string must outlive the handshake (not copied). */
|
||||
void wo_tls_client_set_host(wo_tls_client *c, const char *host, size_t hostlen);
|
||||
|
||||
/* Start a handshake from a caller-built ClientHello handshake message and a
|
||||
* fixed X25519 private key (production passes fresh randomness; the KAT injects
|
||||
* the RFC's). Frames the ClientHello into a plaintext record in c->out for the
|
||||
|
|
|
|||
|
|
@ -420,5 +420,25 @@ int main(void) {
|
|||
"20250101000000") == 1);
|
||||
}
|
||||
|
||||
/* SAN / hostname verification (rv2 9 phase E completion, RFC 6125). */
|
||||
{
|
||||
#define HOST(cert, h) wo_x509_check_host(cert, sizeof cert, h, strlen(h))
|
||||
/* leaf SANs: kat_rsa_leaf=leaf.example.com, kat_ec_leaf=leaf.example.org */
|
||||
T_CHECK(HOST(kat_rsa_leaf, "leaf.example.com") == 1);
|
||||
T_CHECK(HOST(kat_rsa_leaf, "LEAF.Example.CoM") == 1); /* case-insensitive */
|
||||
T_CHECK(HOST(kat_rsa_leaf, "other.example.com") == 0);
|
||||
T_CHECK(HOST(kat_rsa_leaf, "leaf.example.org") == 0);
|
||||
T_CHECK(HOST(kat_ec_leaf, "leaf.example.org") == 1);
|
||||
/* a CA cert here has no SAN -> refused for any hostname */
|
||||
T_CHECK(HOST(kat_rsa_ca, "wo-rsa-ca") == 0);
|
||||
/* wildcard *.example.com matches one label, not zero or a sub-label */
|
||||
T_CHECK(HOST(kat_wild_leaf, "api.example.com") == 1);
|
||||
T_CHECK(HOST(kat_wild_leaf, "API.example.com") == 1);
|
||||
T_CHECK(HOST(kat_wild_leaf, "example.com") == 0); /* no label */
|
||||
T_CHECK(HOST(kat_wild_leaf, "a.b.example.com") == 0); /* extra label */
|
||||
T_CHECK(HOST(kat_wild_leaf, "api.example.org") == 0);
|
||||
#undef HOST
|
||||
}
|
||||
|
||||
return t_report("test_crypto");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,5 +295,19 @@ int main(void) {
|
|||
T_CHECK(wo_tls_client_push_record(&c, rfl, sizeof drv_rec_flight) == WO_TLS_FAILED);
|
||||
}
|
||||
|
||||
/* Hostname enforcement: with a host set, the RFC 8448 leaf (which carries
|
||||
* no SAN) is refused at the Certificate step. */
|
||||
{
|
||||
static wo_tls_client c;
|
||||
uint8_t priv[32], ch[256];
|
||||
memcpy(priv, drv_client_priv, 32); memcpy(ch, drv_ch_msg, sizeof drv_ch_msg);
|
||||
wo_tls_client_start_with(&c, ch, sizeof drv_ch_msg, priv);
|
||||
wo_tls_client_set_host(&c, "api.anthropic.com", 17);
|
||||
uint8_t rsh[128]; memcpy(rsh, drv_rec_sh, sizeof drv_rec_sh);
|
||||
wo_tls_client_push_record(&c, rsh, sizeof drv_rec_sh);
|
||||
uint8_t rfl[1024]; memcpy(rfl, drv_rec_flight, sizeof drv_rec_flight);
|
||||
T_CHECK(wo_tls_client_push_record(&c, rfl, sizeof drv_rec_flight) == WO_TLS_FAILED);
|
||||
}
|
||||
|
||||
return t_report("test_tls");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,3 +191,34 @@ static const uint8_t kat_ec_leaf[] = {
|
|||
0xc1,0x1c,
|
||||
};
|
||||
|
||||
|
||||
/* wildcard-SAN leaf (*.example.com) for the hostname-match KAT */
|
||||
static const uint8_t kat_wild_leaf[] = {
|
||||
0x30,0x82,0x01,0x3a,0x30,0x81,0xe2,0xa0,0x03,0x02,0x01,0x02,
|
||||
0x02,0x14,0x7e,0x22,0xf7,0xef,0x7a,0x6b,0x37,0xb4,0x70,0xeb,
|
||||
0x2b,0xb6,0x91,0xae,0xa1,0xe5,0x34,0x8c,0x4b,0x3c,0x30,0x0a,
|
||||
0x06,0x08,0x2a,0x86,0x48,0xce,0x3d,0x04,0x03,0x02,0x30,0x0f,
|
||||
0x31,0x0d,0x30,0x0b,0x06,0x03,0x55,0x04,0x03,0x0c,0x04,0x77,
|
||||
0x69,0x6c,0x64,0x30,0x1e,0x17,0x0d,0x32,0x30,0x30,0x31,0x30,
|
||||
0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5a,0x17,0x0d,0x33,0x30,
|
||||
0x30,0x31,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5a,0x30,
|
||||
0x0f,0x31,0x0d,0x30,0x0b,0x06,0x03,0x55,0x04,0x03,0x0c,0x04,
|
||||
0x77,0x69,0x6c,0x64,0x30,0x59,0x30,0x13,0x06,0x07,0x2a,0x86,
|
||||
0x48,0xce,0x3d,0x02,0x01,0x06,0x08,0x2a,0x86,0x48,0xce,0x3d,
|
||||
0x03,0x01,0x07,0x03,0x42,0x00,0x04,0xf5,0x4c,0xa6,0x77,0x22,
|
||||
0x89,0x33,0xab,0x6c,0x84,0xd0,0x0a,0x2c,0x16,0x68,0x1e,0x3f,
|
||||
0xb4,0x44,0xe9,0x69,0x71,0x2a,0x1b,0x79,0xd3,0xa9,0x40,0xcb,
|
||||
0xc3,0x4f,0xd8,0x29,0x7c,0x85,0xd3,0xf9,0x9e,0x9c,0x42,0x8c,
|
||||
0x99,0x2e,0xee,0x93,0x26,0xfe,0x9e,0xd0,0x9b,0x75,0x19,0x7a,
|
||||
0x1a,0xc9,0x2a,0x12,0x8d,0x9d,0x19,0xb9,0x43,0x31,0x0c,0xa3,
|
||||
0x1c,0x30,0x1a,0x30,0x18,0x06,0x03,0x55,0x1d,0x11,0x04,0x11,
|
||||
0x30,0x0f,0x82,0x0d,0x2a,0x2e,0x65,0x78,0x61,0x6d,0x70,0x6c,
|
||||
0x65,0x2e,0x63,0x6f,0x6d,0x30,0x0a,0x06,0x08,0x2a,0x86,0x48,
|
||||
0xce,0x3d,0x04,0x03,0x02,0x03,0x47,0x00,0x30,0x44,0x02,0x20,
|
||||
0x44,0x88,0xdd,0xd7,0x3b,0x8b,0xe8,0xaf,0xd5,0xc0,0xf0,0xc3,
|
||||
0x86,0xc2,0xf8,0xfa,0x6c,0x23,0x0c,0xdd,0x17,0xcc,0x13,0xb4,
|
||||
0xe7,0xa3,0x01,0x92,0xce,0xe3,0x9f,0xf6,0x02,0x20,0x11,0x8d,
|
||||
0x50,0xab,0xde,0x45,0xb0,0xf8,0x34,0x6f,0xbd,0xa6,0x28,0xd3,
|
||||
0x4f,0x23,0x67,0x58,0xb0,0x41,0x6e,0x31,0xcf,0x59,0xad,0x5b,
|
||||
0xef,0x49,0x24,0x3e,0x92,0xa3,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue