Milestone 6: root CA generated, snakeoil intermediate embedded, DER parser

Root CA + snakeoil intermediate generated entirely offline
(/home/rajames/CLionProjects/lithosananke-ca/, outside this repo,
private keys chmod 600) per this milestone's own requirement: Ed25519,
root self-signed 20-year validity, intermediate real-CA-signed
(CA:TRUE, pathlen:0), chain verified via openssl.

Snakeoil intermediate embedded as a capsule (capsules/pki/
snakeoil-intermediate.der) -- confirmed the font-capsule precedent
needed zero new infrastructure, any non-.4th file under capsules/
embeds verbatim already.

New x509_ed25519.c: a from-scratch, narrow DER walker (not general
ASN.1/X.509, per this milestone's design decision) extracting the raw
Ed25519 pubkey from a cert's SubjectPublicKeyInfo -- handles the
optional v3 version field, verifies the AlgorithmIdentifier OID is
Ed25519 rather than assuming, handles both DER length forms. Verified
against ground truth: the extracted key from the real embedded cert
matches openssl's own reported pubkey byte-for-byte; refusal path
checked against truncated/garbage/empty/wrong-algorithm (real RSA cert)
input. Compiles clean on all three architectures.

Still open: mkcapsule signing step, wiring ed25519_verify() into
capsule_birth.c's three validate call sites (landing warn-only first,
per decision -- a bug here could stop every capsule from birthing,
including Mama's own, on all three arches), and the BLOCK_MAP.md
signature-status column. Documented in FABRIC-3.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
This commit is contained in:
Robert Allan James
2026-08-26 20:55:05 -04:00
co-authored by Claude Sonnet 5
parent a8692681a8
commit 431bcb1f34
5 changed files with 203 additions and 1 deletions
+124
View File
@@ -0,0 +1,124 @@
/* x509_ed25519.c -- see x509_ed25519.h. */
#include "starkernel/x509_ed25519.h"
#include <string.h>
typedef struct {
const uint8_t *p;
size_t len;
} der_span_t;
/*
* Parse a single DER TLV starting at *cursor (must be < limit). Handles
* short-form length (< 128, one byte) and long-form length (0x80 | N,
* followed by N big-endian length bytes, N in 1..4 -- a real X.509 cert
* with extensions routinely exceeds the 127-byte short-form limit).
* Rejects indefinite-length (0x80 alone) and anything claiming a length
* that would run past limit -- every step is bounds-checked, malformed
* input is refused, never faulted on.
*
* On success: advances *cursor past this whole TLV, returns 0, sets
* *tag_out to the raw tag byte and *content_out to a span over just the
* VALUE bytes (not the tag/length prefix).
*/
static int der_next(const uint8_t **cursor, const uint8_t *limit,
uint8_t *tag_out, der_span_t *content_out) {
const uint8_t *p = *cursor;
if (p >= limit) return -1;
uint8_t tag = *p++;
if (p >= limit) return -1;
uint8_t len_byte = *p++;
size_t len;
if (len_byte & 0x80u) {
uint8_t nbytes = (uint8_t)(len_byte & 0x7Fu);
if (nbytes == 0 || nbytes > 4) return -1; /* indefinite-length or unreasonably large */
if ((size_t)(limit - p) < nbytes) return -1;
len = 0;
for (uint8_t i = 0; i < nbytes; i++) len = (len << 8) | *p++;
} else {
len = len_byte;
}
if ((size_t)(limit - p) < len) return -1;
*tag_out = tag;
content_out->p = p;
content_out->len = len;
*cursor = p + len;
return 0;
}
int x509_extract_ed25519_pubkey(const uint8_t *der, size_t der_len,
uint8_t pubkey_out[32]) {
if (!der || !pubkey_out) return -1;
const uint8_t *cur = der;
const uint8_t *end = der + der_len;
uint8_t tag;
der_span_t cert;
/* Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue } */
if (der_next(&cur, end, &tag, &cert) != 0 || tag != 0x30) return -1;
/* tbsCertificate ::= SEQUENCE { ... } -- first child of Certificate */
const uint8_t *tbs_cur = cert.p;
const uint8_t *tbs_limit = cert.p + cert.len;
der_span_t tbs;
if (der_next(&tbs_cur, tbs_limit, &tag, &tbs) != 0 || tag != 0x30) return -1;
const uint8_t *cur2 = tbs.p;
const uint8_t *limit2 = tbs.p + tbs.len;
/* version [0] EXPLICIT Version DEFAULT v1 -- present (context-
* specific constructed tag 0xA0) for v2/v3 certs, absent for v1.
* Peek: if present, consume it; if not, leave cur2 where it is so
* the next field (serialNumber) is read correctly either way. */
{
const uint8_t *peek = cur2;
uint8_t ptag;
der_span_t pcontent;
if (der_next(&peek, limit2, &ptag, &pcontent) == 0 && ptag == 0xA0) {
cur2 = peek;
}
}
/* Skip serialNumber, signature (AlgorithmIdentifier), issuer,
* validity, subject -- exactly 5 fields, none of which this parser
* needs the content of, only their length to skip past them. */
for (int i = 0; i < 5; i++) {
der_span_t skip;
if (der_next(&cur2, limit2, &tag, &skip) != 0) return -1;
}
/* subjectPublicKeyInfo ::= SEQUENCE { algorithm, subjectPublicKey } */
der_span_t spki;
if (der_next(&cur2, limit2, &tag, &spki) != 0 || tag != 0x30) return -1;
const uint8_t *cur3 = spki.p;
const uint8_t *limit3 = spki.p + spki.len;
/* algorithm ::= AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL } */
der_span_t algid;
if (der_next(&cur3, limit3, &tag, &algid) != 0 || tag != 0x30) return -1;
const uint8_t *cur4 = algid.p;
const uint8_t *limit4 = algid.p + algid.len;
der_span_t oid;
if (der_next(&cur4, limit4, &tag, &oid) != 0 || tag != 0x06) return -1;
/* OID 1.3.101.112 (RFC 8410 Ed25519), DER-encoded: 43 (=40*1+3), 101, 112,
* each arc after the first pair fitting in one byte (both < 128). */
static const uint8_t ED25519_OID[3] = { 0x2B, 0x65, 0x70 };
if (oid.len != sizeof(ED25519_OID) ||
memcmp(oid.p, ED25519_OID, sizeof(ED25519_OID)) != 0) {
return -1; /* not an Ed25519 key -- refuse, don't guess */
}
/* subjectPublicKey ::= BIT STRING -- 1 unused-bits byte (must be 0
* for a byte-aligned 32-byte key) followed by the raw key itself. */
der_span_t bitstr;
if (der_next(&cur3, limit3, &tag, &bitstr) != 0 || tag != 0x03) return -1;
if (bitstr.len != 33 || bitstr.p[0] != 0x00) return -1;
memcpy(pubkey_out, bitstr.p + 1, 32);
return 0;
}