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:
parent
3a1ba15851
commit
040ec9c189
3 changed files with 112 additions and 0 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue