diff --git a/runtime/src/tls.c b/runtime/src/tls.c index fc51ea5..9a040e1 100644 --- a/runtime/src/tls.c +++ b/runtime/src/tls.c @@ -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; +} diff --git a/runtime/src/tls.h b/runtime/src/tls.h index ad94f9b..b81e806 100644 --- a/runtime/src/tls.h +++ b/runtime/src/tls.h @@ -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 diff --git a/runtime/test/test_tls.c b/runtime/test/test_tls.c index a7bc599..8c9573e 100644 --- a/runtime/test/test_tls.c +++ b/runtime/test/test_tls.c @@ -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 +#include +#include #include #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"); }