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:
co-authored by
Claude Sonnet 5
parent
a8692681a8
commit
431bcb1f34
+48
@@ -696,6 +696,54 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
|
||||
- [ ] Implement magic-number-based content-type detection (Section U item 14) — a shared
|
||||
primitive, also usable for Milestone 4's foreign-drive check.
|
||||
|
||||
- [x] **Root CA + snakeoil intermediate generated 2026-08-26**, entirely offline, in a sibling
|
||||
directory outside this repo (`/home/rajames/CLionProjects/lithosananke-ca/`, not tracked by
|
||||
git here). Ed25519, OpenSSL 3.5.5. Root: self-signed, 20-year validity (2026–2046),
|
||||
`CN=LithosAnanke Root CA`. Intermediate: a real CA-signed cert (not self-signed despite the
|
||||
name), 10-year validity, `CA:TRUE, pathlen:0` (can sign capsules, can't mint further
|
||||
intermediates), chain verified (`openssl verify` returns `OK`). Both private keys `chmod
|
||||
600`. The root key never touches this repo or any kernel code, per the design's own
|
||||
requirement.
|
||||
|
||||
- [x] **Snakeoil intermediate embedded as a capsule, 2026-08-26.** Exported to DER
|
||||
(`capsules/pki/snakeoil-intermediate.der`, 418 bytes) and dropped under `capsules/` —
|
||||
confirmed the font-capsule precedent needed zero new infrastructure: `mkcapsule`'s
|
||||
`process_file()` embeds any non-`.4th` file verbatim already. Shows up as capsule
|
||||
`pki:snakeoil-intermediate.der` in the generated capsule table (38 capsules total, up from
|
||||
37) — retrieve via `capsule_find_by_name()` + `capsule_get_payload()`, never
|
||||
`capsule_exec_payload()` (it's a passive data blob, not executable capsule code).
|
||||
|
||||
- [x] **Minimal DER/X.509 parser written and independently verified, 2026-08-26.** New
|
||||
`include/starkernel/x509_ed25519.h` + `src/starkernel/crypto/x509_ed25519.c`:
|
||||
`x509_extract_ed25519_pubkey()`, a from-scratch, narrow DER walker (not a general ASN.1/
|
||||
X.509 parser, per this milestone's own design decision) — walks `Certificate →
|
||||
TBSCertificate → SubjectPublicKeyInfo`, handles the optional `[0] EXPLICIT Version` field
|
||||
(present on v3 certs), verifies the `AlgorithmIdentifier` OID is exactly `1.3.101.112`
|
||||
(RFC 8410 Ed25519) rather than assuming, and extracts the raw 32-byte key from the trailing
|
||||
`BIT STRING`. Handles both short-form and long-form DER lengths (a real cert with v3
|
||||
extensions routinely exceeds the 127-byte short-form limit). Every step bounds-checked
|
||||
against the buffer end — refuses malformed input, never faults.
|
||||
|
||||
**Verified against ground truth, not self-consistency:** run against the real embedded
|
||||
`snakeoil-intermediate.der`, the extracted 32-byte key matched `openssl pkey -pubin -text`'s
|
||||
own reported public key byte-for-byte. Refusal path verified too: truncated input, 10 random
|
||||
garbage bytes, an empty file, and a real RSA certificate (algorithm-mismatch case, not just
|
||||
structural malformation) all correctly return failure rather than misreading or crashing.
|
||||
Compiles clean on all three kernel architectures (no `__STARKERNEL__` guard needed — same
|
||||
freestanding-safe shape as the other crypto files).
|
||||
|
||||
**Still open:** the signing step in `mkcapsule` (needs a new `sig[64]` field on
|
||||
`CapsuleEntry` and a parallel emitted array in `capsule_generated.c`, since `CapsuleDesc`
|
||||
itself has no spare bytes — confirmed exactly 64 bytes, every field used), wiring
|
||||
`ed25519_verify()` into the three `capsule_validate()` call sites in `capsule_birth.c`
|
||||
(**decided: land as WARN-only first, prove correct on all three architectures against both a
|
||||
valid and a deliberately-corrupted capsule, then flip to hard-refuse in a separate step** —
|
||||
a bug here has a much larger blast radius than anything else in Phase 8, since a false
|
||||
refusal on Mama's own capsule means no `ok>` at all, on any architecture), and the
|
||||
signature-status column on `capsules/BLOCK_MAP.md` (confirmed the real, live manifest target
|
||||
— `capsules/MANIFEST_AUTO.md` is stale/dead, not regenerated since 2026-07-05, flag as docs
|
||||
drift rather than a real target).
|
||||
|
||||
### From FABRIC-2.md §X, Milestone 7 — Contributor capsules / trust tiers
|
||||
|
||||
- [ ] Create the `capsules/contrib/` directory (mechanically trivial, matches existing
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-08-27T00:36:05Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-08-27T00:54:08Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* x509_ed25519.h -- extract a raw Ed25519 public key from a DER-encoded
|
||||
* X.509 certificate's SubjectPublicKeyInfo (RFC 8410).
|
||||
*
|
||||
* Deliberately NOT a general ASN.1/X.509 parser (Milestone 6 decision,
|
||||
* FABRIC-2.md): walks exactly as far into the DER structure as needed
|
||||
* to reach SubjectPublicKeyInfo, verifies its AlgorithmIdentifier OID is
|
||||
* Ed25519 (1.3.101.112) and rejects anything else, then returns the raw
|
||||
* 32-byte key from the trailing BIT STRING. No signature verification,
|
||||
* no chain validation, no extension parsing -- this only answers "what
|
||||
* public key does this cert claim to hold," matching exactly what
|
||||
* ed25519_verify() needs as input.
|
||||
*
|
||||
* Freestanding C99, no libc beyond memcmp/memcpy (already provided by
|
||||
* src/starkernel/vm/host/shim.c in the kernel build).
|
||||
*/
|
||||
#ifndef STARKERNEL_X509_ED25519_H
|
||||
#define STARKERNEL_X509_ED25519_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Returns 0 on success (pubkey_out[32] filled), -1 on any malformed
|
||||
* encoding, unexpected structure, or non-Ed25519 algorithm. Never
|
||||
* faults on malformed input -- every DER length/tag is bounds-checked
|
||||
* against der_len before use. */
|
||||
int x509_extract_ed25519_pubkey(const uint8_t *der, size_t der_len,
|
||||
uint8_t pubkey_out[32]);
|
||||
|
||||
#endif /* STARKERNEL_X509_ED25519_H */
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user