The header scan (Captain Bob, 2026-11-10): 788 source files -- 351 with the
SL-1.0 header, 12 with the old name (docs/working/experiments, kept by
ruling), 31 under someone else's licence (tools/kconfig, Linux's, GPL-2.0:
kept), and 394 with none. Ruled: the SPDX form; capsules (.4th, .f18) and
proofs (.thy) left for a ruling of their own (mkcapsule's 64x16 block format,
content-addressed capsules).
253 files now begin
SPDX-License-Identifier: LicenseRef-Starship-1.0
Copyright (c) 2023-2026 Robert A. James
as a /* */ block (C, headers, assembly, linker scripts) or # lines (scripts,
Makefiles, Kconfig; below a shebang or a coding line); tools/mkcapsule.c,
which had the copyright line, gets the SPDX line only.
Not touched: docs/working/ (archive and old campaign material, as the
ruling kept the experiments); v4/system/post_cases.c (generated by
v4/tools/mkpost.py, "do not edit" -- the generator has the header).
Comments only: the kernel builds on amd64, aarch64, riscv64; make -C v4
test-32: 39 programs, 0 failures; every changed script parses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2
176 lines
6.4 KiB
C
176 lines
6.4 KiB
C
/* SPDX-License-Identifier: LicenseRef-Starship-1.0
|
|
* Copyright (c) 2023-2026 Robert A. James
|
|
*/
|
|
/* pkcs8_ed25519.c -- see pkcs8_ed25519.h. */
|
|
#define _GNU_SOURCE /* memmem() */
|
|
#include "pkcs8_ed25519.h"
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
/*
|
|
* `openssl genpkey` writes PEM (base64 text between BEGIN/END markers)
|
|
* by default, not raw DER -- accept either transparently rather than
|
|
* requiring callers to pre-convert with an external tool (consistent
|
|
* with this file's whole point: no new host binary dependency). A small,
|
|
* self-contained base64 decoder, since none existed anywhere in this
|
|
* repo to reuse (checked 2026-08-26).
|
|
*
|
|
* Returns a malloc'd DER buffer (caller frees) and sets *der_len, or
|
|
* NULL if this doesn't look like a well-formed PEM block. Decodes only
|
|
* the FIRST "-----BEGIN ... -----" / "-----END ... -----" pair found;
|
|
* good enough for a single private key, not a general PEM bundle parser.
|
|
*/
|
|
static int b64_val(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; /* not a base64 char (newline, '=', etc.) */
|
|
}
|
|
|
|
static uint8_t *pem_to_der(const uint8_t *pem, size_t pem_len, size_t *der_len_out) {
|
|
const char *begin_marker = "-----BEGIN";
|
|
const char *end_marker = "-----END";
|
|
|
|
const uint8_t *begin = memmem(pem, pem_len, begin_marker, strlen(begin_marker));
|
|
if (!begin) return NULL;
|
|
const uint8_t *line_end = memchr(begin, '\n', (size_t)(pem + pem_len - begin));
|
|
if (!line_end) return NULL;
|
|
const uint8_t *body_start = line_end + 1;
|
|
|
|
const uint8_t *end = memmem(body_start, (size_t)(pem + pem_len - body_start),
|
|
end_marker, strlen(end_marker));
|
|
if (!end) return NULL;
|
|
|
|
/* Decode base64 chars between body_start and end, skipping anything
|
|
* that isn't a valid base64 character (newlines, stray whitespace). */
|
|
size_t max_out = (size_t)(end - body_start); /* over-allocate, safe upper bound */
|
|
uint8_t *out = malloc(max_out ? max_out : 1);
|
|
if (!out) return NULL;
|
|
|
|
size_t out_len = 0;
|
|
int acc = 0, nbits = 0;
|
|
for (const uint8_t *p = body_start; p < end; p++) {
|
|
int v = b64_val(*p);
|
|
if (v < 0) continue; /* skip newlines and '=' padding */
|
|
acc = (acc << 6) | v;
|
|
nbits += 6;
|
|
if (nbits >= 8) {
|
|
nbits -= 8;
|
|
out[out_len++] = (uint8_t)((acc >> nbits) & 0xFF);
|
|
}
|
|
}
|
|
|
|
*der_len_out = out_len;
|
|
return out;
|
|
}
|
|
|
|
typedef struct {
|
|
const uint8_t *p;
|
|
size_t len;
|
|
} der_span_t;
|
|
|
|
/* Same short-form/long-form DER TLV walker as x509_ed25519.c's der_next()
|
|
* -- see that file's comment for the full rationale. Bounds-checked
|
|
* against limit at every step; refuses malformed input, never faults. */
|
|
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;
|
|
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;
|
|
}
|
|
|
|
static int pkcs8_extract_ed25519_seed_der(const uint8_t *der, size_t der_len,
|
|
uint8_t seed_out[32]) {
|
|
if (!der || !seed_out) return -1;
|
|
|
|
const uint8_t *cur = der;
|
|
const uint8_t *end = der + der_len;
|
|
uint8_t tag;
|
|
der_span_t outer;
|
|
|
|
/* OneAsymmetricKey ::= SEQUENCE { version INTEGER, privateKeyAlgorithm
|
|
* AlgorithmIdentifier, privateKey OCTET STRING, ... } */
|
|
if (der_next(&cur, end, &tag, &outer) != 0 || tag != 0x30) return -1;
|
|
|
|
const uint8_t *cur2 = outer.p;
|
|
const uint8_t *limit2 = outer.p + outer.len;
|
|
|
|
/* version INTEGER -- skip, don't care about the value */
|
|
der_span_t version;
|
|
if (der_next(&cur2, limit2, &tag, &version) != 0 || tag != 0x02) return -1;
|
|
|
|
/* privateKeyAlgorithm ::= AlgorithmIdentifier ::= SEQUENCE { OID, params OPTIONAL } */
|
|
der_span_t algid;
|
|
if (der_next(&cur2, limit2, &tag, &algid) != 0 || tag != 0x30) return -1;
|
|
|
|
const uint8_t *cur3 = algid.p;
|
|
const uint8_t *limit3 = algid.p + algid.len;
|
|
der_span_t oid;
|
|
if (der_next(&cur3, limit3, &tag, &oid) != 0 || tag != 0x06) return -1;
|
|
|
|
static const uint8_t ED25519_OID[3] = { 0x2B, 0x65, 0x70 }; /* 1.3.101.112 */
|
|
if (oid.len != sizeof(ED25519_OID) ||
|
|
memcmp(oid.p, ED25519_OID, sizeof(ED25519_OID)) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
/* privateKey ::= OCTET STRING, whose content is itself a DER-encoded
|
|
* OCTET STRING (RFC 8410's CurvePrivateKey) wrapping the raw 32-byte
|
|
* seed -- double-wrapped, confirmed empirically against a real
|
|
* openssl-generated key (2026-08-26), not assumed from the RFC text
|
|
* alone: `04 22 04 20 <32 bytes>` (outer OCTET STRING len=34,
|
|
* containing inner OCTET STRING len=32). */
|
|
der_span_t outer_octet;
|
|
if (der_next(&cur2, limit2, &tag, &outer_octet) != 0 || tag != 0x04) return -1;
|
|
|
|
const uint8_t *cur4 = outer_octet.p;
|
|
const uint8_t *limit4 = outer_octet.p + outer_octet.len;
|
|
der_span_t inner_octet;
|
|
if (der_next(&cur4, limit4, &tag, &inner_octet) != 0 || tag != 0x04) return -1;
|
|
if (inner_octet.len != 32) return -1;
|
|
|
|
memcpy(seed_out, inner_octet.p, 32);
|
|
return 0;
|
|
}
|
|
|
|
int pkcs8_extract_ed25519_seed(const uint8_t *data, size_t data_len,
|
|
uint8_t seed_out[32]) {
|
|
if (!data || !seed_out) return -1;
|
|
|
|
/* `openssl genpkey`'s default output is PEM, not raw DER -- accept
|
|
* either. A PEM block always starts with "-----BEGIN" somewhere near
|
|
* the top; anything else is assumed to already be raw DER. */
|
|
if (memmem(data, data_len, "-----BEGIN", 10) != NULL) {
|
|
size_t der_len = 0;
|
|
uint8_t *der = pem_to_der(data, data_len, &der_len);
|
|
if (!der) return -1;
|
|
int rc = pkcs8_extract_ed25519_seed_der(der, der_len, seed_out);
|
|
free(der);
|
|
return rc;
|
|
}
|
|
|
|
return pkcs8_extract_ed25519_seed_der(data, data_len, seed_out);
|
|
}
|