/* pkcs8_ed25519.c -- see pkcs8_ed25519.h. */ #define _GNU_SOURCE /* memmem() */ #include "pkcs8_ed25519.h" #include #include /* * `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); }