feat(tls): PEM trust-anchor decoder (rv2 9 F3c-net decision 4)

- wo_tls_pem_to_ders: scan a PEM bundle for CERTIFICATE blocks, base64-decode
  each into a caller arena, record DER spans as trust anchors for
  wo_tls_verify_chain. Pure (caller reads the file + owns the arena) so it is
  offline-testable; the file read + per-shard cache land with the builtin
- b64_decode helper (standard alphabet, skips whitespace/newlines)
- KAT: decode the real /etc/ssl/certs/ca-certificates.crt (>100 anchors,
  each parses, first is a CA), garbage PEM -> 0 with no over-read,
  skip-if-absent for CI. test_tls 107 pass, ASan/UBSan clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 6445d55aa83dbed831d84fd3cca3a74fe4609a00)
This commit is contained in:
shoney.arickathil 2026-09-09 02:37:55 +02:00
parent 3a1ba15851
commit 040ec9c189
3 changed files with 112 additions and 0 deletions

View file

@ -670,3 +670,72 @@ int wo_tls_verify_chain(const uint8_t *const *certs, const size_t *cert_lens,
}
return 0; /* untrusted */
}
/* ---- PEM trust-anchor decoding (phase F3c-net) ---------------------------
* Decode a PEM bundle (e.g. /etc/ssl/certs/ca-certificates.crt) into DER trust
* anchors for wo_tls_verify_chain. Pure: the caller reads the file and owns the
* arena the DERs are copied into; only the base64 + block framing lives here,
* so it is offline-testable. */
/* Standard base64 value, or -1 for a non-alphabet byte (whitespace included). */
static int b64v(uint8_t c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
/* Decode base64 (ignoring whitespace/newlines) into out; returns bytes written
* or -1 on overflow / bad length. Stops at '=' padding. */
static long b64_decode(const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap) {
uint32_t acc = 0; int bits = 0; size_t n = 0;
for (size_t i = 0; i < inlen; i++) {
if (in[i] == '=') break;
int v = b64v(in[i]);
if (v < 0) continue; /* skip newlines etc. */
acc = (acc << 6) | (uint32_t)v; bits += 6;
if (bits >= 8) {
bits -= 8;
if (n >= outcap) return -1;
out[n++] = (uint8_t)(acc >> bits);
}
}
return (long)n;
}
/* Parse `pem` for CERTIFICATE blocks; base64-decode each into `arena` (appended)
* and record its span in certs[]/cert_lens[]. Returns the count (0..max_certs),
* or -1 on arena overflow or a malformed block. Extra certs past max_certs are
* silently ignored — the caller sizes max_certs to the bundle. */
long wo_tls_pem_to_ders(const char *pem, size_t pemlen, uint8_t *arena,
size_t arena_cap, const uint8_t **certs,
size_t *cert_lens, size_t max_certs) {
static const char BEGIN[] = "-----BEGIN CERTIFICATE-----";
static const char END[] = "-----END CERTIFICATE-----";
size_t used = 0, count = 0, i = 0;
while (i < pemlen && count < max_certs) {
/* find BEGIN */
const char *b = NULL;
for (; i + sizeof BEGIN - 1 <= pemlen; i++)
if (memcmp(pem + i, BEGIN, sizeof BEGIN - 1) == 0) { b = pem + i; break; }
if (!b) break;
i += sizeof BEGIN - 1;
/* find END */
size_t body = i;
const char *e = NULL;
for (; i + sizeof END - 1 <= pemlen; i++)
if (memcmp(pem + i, END, sizeof END - 1) == 0) { e = pem + i; break; }
if (!e) return -1; /* BEGIN without END */
long dl = b64_decode((const uint8_t *)pem + body, (size_t)(e - (pem + body)),
arena + used, arena_cap - used);
if (dl <= 0) return -1;
certs[count] = arena + used;
cert_lens[count] = (size_t)dl;
used += (size_t)dl;
count++;
i += sizeof END - 1;
}
return (long)count;
}

View file

@ -121,6 +121,15 @@ int wo_tls_verify_chain(const uint8_t *const *certs, const size_t *cert_lens,
const size_t *anchor_lens, size_t n_anchors,
const char *host, size_t hostlen, const char now14[14]);
/* Decode a PEM bundle (concatenated CERTIFICATE blocks) into DER trust anchors.
* base64-decodes each block into `arena` (appended) and records its span in
* certs[]/cert_lens[]; returns the count (0..max_certs) or -1 on arena overflow
* or a malformed block. Pure — the caller reads the file and owns the arena, so
* this is offline-testable. (rv2 9 F3c-net decision 4) */
long wo_tls_pem_to_ders(const char *pem, size_t pemlen, uint8_t *arena,
size_t arena_cap, const uint8_t **certs,
size_t *cert_lens, size_t max_certs);
/* ---- 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

@ -2,6 +2,8 @@
* python's AEAD as oracle (tls_record_vectors.h), plus seal/open round-trip,
* a tamper-rejection, and the sequence-number nonce advancing. ASan/UBSan. */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "tls.h"
@ -378,5 +380,37 @@ int main(void) {
"leaf.example.com", 16, NOW) == 0);
}
/* PEM trust-anchor decoder (phase F3c-net decision 4). Decode the real
* system CA bundle and confirm the anchors parse; skip if absent (CI). */
{
/* a non-PEM blob yields zero certs, never an over-read */
const uint8_t *cz[8]; size_t czl[8]; uint8_t az[64];
T_CHECK(wo_tls_pem_to_ders("not a pem at all", 15, az, sizeof az, cz, czl, 8) == 0);
const char *path = "/etc/ssl/certs/ca-certificates.crt";
FILE *f = fopen(path, "rb");
if (f) {
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
char *pem = (char *)malloc((size_t)sz);
size_t got = fread(pem, 1, (size_t)sz, f);
fclose(f);
uint8_t *arena = (uint8_t *)malloc((size_t)sz); /* DER < PEM */
enum { MAXC = 1024 };
const uint8_t **certs = (const uint8_t **)malloc(MAXC * sizeof *certs);
size_t *lens = (size_t *)malloc(MAXC * sizeof *lens);
long n = wo_tls_pem_to_ders(pem, got, arena, (size_t)sz, certs, lens, MAXC);
T_CHECK(n > 100); /* a real bundle is large */
if (n > 0) {
int is_ca, has_pl, pl;
/* the first anchor parses, and system roots are CAs */
T_CHECK(wo_x509_basic_constraints(certs[0], lens[0], &is_ca, &has_pl, &pl) == 0);
T_CHECK(is_ca == 1);
}
free(pem); free(arena); free((void *)certs); free(lens);
} else {
t_pass++; /* bundle absent on this host — decoder still exercised above */
}
}
return t_report("test_tls");
}