commit 68eb46b7fb3289bfc552e26f3567321acfb8d6d1
parent 0cd54e2ee4f7fc860defffcbc7cd93652051bb12
Author: Robin Bron <robin.bron@yourhosting.nl>
Date: Sun, 13 Sep 2026 22:27:29 +0200
Add hdr and asc key formats
Diffstat:
22 files changed, 933 insertions(+), 28 deletions(-)
diff --git a/encoding.md b/encoding.md
@@ -3,16 +3,61 @@
Keys can (or will) be encoded in multiple formats. This file shows a list of
encodings supported by this program.
-The first byte of a key-file must ALWAYS start with the version identifier
-(byte sequence prefix), allowing a different prefix to identify a different type
-of key encoding.
+Each key-file must start with a distinct prefix (byte sequence), allowing a
+different prefix to identify a different type of key encoding.
+
+A decoded key is either full (public + private half) or public-only. A
+`NULL` private half means public-only; commands requiring the private half
+(`sign`) refuse such keys while `verify` accepts them. A lone private half
+additionally yields its public half via scalar multiplication.
## 0x00
Plain, binary, ed25519.
Not text-safe. Starts with a 0x00 byte, followed by 32 bytes of the raw public
-key, then followed by 64 bytes of the raw private key.
+key. Full keys additionally carry 64 bytes of the raw private key after that,
+so valid sizes are 33 (public-only) or 97 (full) bytes.
Intended as the first format supported and to be deprecated later. Meant to be
simple to code, not to be safe or flexible.
+
+## asc
+
+Ascii-armored raw key bytes, no prefix. A full key emits both a PRIVATE
+block (64 private bytes) and a PUBLIC block (32 public bytes):
+
+```
+-----BEGIN SUPERCOP PRIVATE KEY-----
+<base64, 64 columns>
+-----END SUPERCOP PRIVATE KEY-----
+-----BEGIN SUPERCOP PUBLIC KEY-----
+<base64, 64 columns>
+-----END SUPERCOP PUBLIC KEY-----
+```
+
+Public-only keys emit just the PUBLIC block. Labels are namespaced so
+openssl never mistakes the raw payload for DER/PKCS#8. When both blocks
+are present, the private block is authoritative and the public block must
+agree with the derived public half; a corrupt private block fails closed.
+
+## hdr
+
+General-purpose text-protocol header encoding. Byte-identical to what
+`printkey` outputs:
+
+```
+public-key: <64 hex>
+private-key: <128 hex | (no private key)>
+```
+
+Parsing rules, meant to make integration in other programs easy:
+
+- Key lines may appear on any line, in any order, surrounded by any other
+ content; unknown labels are ignored.
+- Labels are case-insensitive, with 0 or more spaces/tabs around the `:`.
+- A parenthesized value (e.g. `(no private key)`) marks the field absent; a
+ missing `private-key:` line means the same. First valid occurrence wins,
+ malformed hex runs are skipped.
+- A lone `private-key:` line derives its public half; a file with neither
+ key line is invalid.
diff --git a/man/supercop.1 b/man/supercop.1
@@ -25,11 +25,11 @@ Show version number and exit (same as \fBsupercop version\fR)
.SH COMMANDS
.TP
-.B generate\fR [\fB-k\fR \fIkeyfile\fR]
+.B generate\fR [\fIoptions\fR]
Generate a new key, writing to keyfile or stdout
.TP
-.B printkey\fR \fB-k\fR \fIkeyfile\fR
-Print contents of key file
+.B printkey\fR [\fIoptions\fR]
+Print contents of key file, optionally converting formats
.TP
.B sign\fR \fB-k\fR \fIkeyfile\fR [\fB-m\fR \fImessage\fR | \fB-M\fR \fImessage-file\fR]
Sign a message
@@ -56,6 +56,15 @@ Message file to sign or verify (defaults to stdin)
.TP
.B \-s, \-\-signature\fR \fIsignature_string\fR
Signature to verify the message against
+.TP
+.B \-f, \-\-format\fR \fIname\fR
+Key format for generate output or printkey conversion: 0x00, asc, hdr
+.TP
+.B \-o, \-\-out\fR \fIpath\fR
+Write printkey output to file instead of stdout
+.TP
+.B \-\-public\-only
+Output only the public half of the key (printkey)
.SH RETURN VALUE
Here are the possible return values:
diff --git a/src/cli/command/generate.c b/src/cli/command/generate.c
@@ -1,5 +1,6 @@
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
#include "cli/command.h"
#include "cli/common.h"
@@ -8,16 +9,20 @@
static int cmd_generate(int argc, const char **argv) {
const char *keyFile = NULL;
+ const char *format = "0x00";
FILE *fout = stdout;
+ struct Format *fmt;
+ struct KeyPair out;
int result;
static const char *const usages[] = {
- "supercop generate [-k keyfile]",
+ "supercop generate [options]",
NULL,
};
struct argparse_option options[] = {
OPT_HELP(),
OPT_STRING('k', "key-file", &keyFile, "Key file to write (defaults to stdout)", NULL, 0, 0),
+ OPT_STRING('f', "format", &format, "Key format to write (0x00, asc, hdr)", NULL, 0, 0),
OPT_END(),
};
@@ -45,14 +50,18 @@ static int cmd_generate(int argc, const char **argv) {
}
ed25519_create_keypair(public_key, private_key, seed);
- struct KeyPair kp;
- kp.public_key = public_key;
- kp.private_key = private_key;
+ out.public_key = public_key;
+ out.private_key = private_key;
- // Encode in the last-registered format
- // TODO: allow format selection
+ // Encode in the requested format (default keeps historical behavior)
+ fmt = supercop_find_format(format);
+ if (!fmt) {
+ fprintf(stderr, "Unknown format: %s\n", format);
+ if (keyFile) fclose(fout);
+ return 1;
+ }
size_t encoded_length;
- char *encoded = supercop_formats->encode(&kp, &encoded_length);
+ char *encoded = fmt->encode(&out, &encoded_length);
if (!encoded) {
fprintf(stderr, "Error while encoding key\n");
if (keyFile) fclose(fout);
@@ -85,9 +94,10 @@ void __attribute__((constructor)) cmd_generate_setup(void) {
"supercop generate - Generate new key\n"
"\n"
"Usage:\n"
- " supercop generate [-k keyfile]\n"
+ " supercop generate [options]\n"
"\n"
"Options:\n"
- " -k, --key-file <path> Key file to write (defaults to stdout)\n";
+ " -k, --key-file <path> Key file to write (defaults to stdout)\n"
+ " -f, --format <name> Key format to write: 0x00, asc, hdr (default 0x00)\n";
commands = cmd;
}
diff --git a/src/cli/command/printkey.c b/src/cli/command/printkey.c
@@ -7,16 +7,27 @@
static int cmd_printkey(int argc, const char **argv) {
const char *keyFile = NULL;
+ const char *format = "hdr";
+ const char *outFile = NULL;
+ int pubOnly = 0;
struct KeyPair *kp;
- int i, result;
+ struct Format *fmt;
+ struct KeyPair out;
+ FILE *fout = stdout;
+ size_t encoded_length;
+ char *encoded;
+ int result;
static const char *const usages[] = {
- "supercop printkey -k keyfile",
+ "supercop printkey [options]",
NULL,
};
struct argparse_option options[] = {
OPT_HELP(),
OPT_STRING('k', "key-file", &keyFile, "Select key file to use for the operation", NULL, 0, 0),
+ OPT_STRING('f', "format", &format, "Output format (0x00, asc, hdr, defaults to hdr)", NULL, 0, 0),
+ OPT_STRING('o', "out", &outFile, "Write output to file instead of stdout", NULL, 0, 0),
+ OPT_BOOLEAN(0, "public-only", &pubOnly, "Output only the public half of the key", NULL, 0, 0),
OPT_END(),
};
@@ -38,17 +49,38 @@ static int cmd_printkey(int argc, const char **argv) {
return 1;
}
- fprintf(stdout, "public-key: ");
- for(i=0;i<32;i++) fprintf(stdout, "%02x", kp->public_key[i]);
- if (kp->private_key) {
- fprintf(stdout, "\nprivate-key: ");
- for(i=0;i<64;i++) fprintf(stdout, "%02x", kp->private_key[i]);
- } else {
- fprintf(stdout, "\nprivate-key: (no private key in file)");
+ fmt = supercop_find_format(format);
+ if (!fmt) {
+ fprintf(stderr, "Unknown format: %s\n", format);
+ keypair_free(kp);
+ return 1;
+ }
+
+ if (outFile) {
+ fout = fopen(outFile, "w+");
+ if (!fout) {
+ fprintf(stderr, "Could not open output file\n");
+ keypair_free(kp);
+ return 1;
+ }
+ }
+
+ // Shallow copy: encoders never take ownership, priv stripped on request
+ out = *kp;
+ if (pubOnly) out.private_key = NULL;
+
+ encoded = fmt->encode(&out, &encoded_length);
+ if (!encoded) {
+ fprintf(stderr, "Error while encoding key\n");
+ if (outFile && fout) fclose(fout);
+ keypair_free(kp);
+ return 1;
}
- fprintf(stdout, "\n");
+ fwrite(encoded, 1, encoded_length, fout);
+ if (encoded) free(encoded);
result = 0;
+ if (outFile && fout) fclose(fout);
keypair_free(kp);
return result;
}
@@ -69,9 +101,12 @@ void __attribute__((constructor)) cmd_printkey_setup(void) {
"supercop printkey - Print contents of key file\n"
"\n"
"Usage:\n"
- " supercop printkey -k keyfile\n"
+ " supercop printkey [options]\n"
"\n"
"Options:\n"
- " -k, --key-file <path> Select key file to use for the operation\n";
+ " -k, --key-file <path> Select key file to use for the operation\n"
+ " -f, --format <name> Output format: 0x00, asc, hdr (default hdr)\n"
+ " -o, --out <path> Write output to file instead of stdout\n"
+ " --public-only Output only the public half of the key\n";
commands = cmd;
}
diff --git a/src/cli/common.c b/src/cli/common.c
@@ -16,6 +16,16 @@ long fremaining(FILE *fd) {
return end - current;
}
+struct Format *supercop_find_format(const char *name) {
+ struct Format *fmt = supercop_formats;
+ if (!name) return NULL;
+ while (fmt) {
+ if (fmt->name && strcmp(fmt->name, name) == 0) return fmt;
+ fmt = fmt->next;
+ }
+ return NULL;
+}
+
struct KeyPair *readKeyFile(const char *filename) {
struct Format *fmt = supercop_formats;
FILE *fd = fopen(filename, "r");
diff --git a/src/cli/common.h b/src/cli/common.h
@@ -20,6 +20,9 @@ extern struct Format *supercop_formats;
long fremaining(FILE *fd);
struct KeyPair *readKeyFile(const char *filename);
+// Find a registered key format by name (0x00, asc, hdr); NULL if unknown
+struct Format *supercop_find_format(const char *name);
+
// Message source: -m string wins, then -M file, then stdin
FILE *supercop_open_message(const char *message, const char *messageFile);
diff --git a/src/fmt/asc/armor.c b/src/fmt/asc/armor.c
@@ -0,0 +1,44 @@
+#include <stdio.h>
+#include <string.h>
+
+#include "armor.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int asc_find_block(const unsigned char *buf, size_t len, const char *label,
+ const unsigned char **payload, size_t *payloadlen) {
+ char begin[64], end[64];
+ size_t blen, elen, i, pend;
+ int n;
+
+ if (!buf || !label) return 0;
+ n = snprintf(begin, sizeof(begin), "-----BEGIN %s-----", label);
+ if (n < 0 || (size_t)n >= sizeof(begin)) return 0;
+ n = snprintf(end, sizeof(end), "-----END %s-----", label);
+ if (n < 0 || (size_t)n >= sizeof(end)) return 0;
+ blen = strlen(begin);
+ elen = strlen(end);
+ if (len < blen) return 0;
+
+ for (i = 0; i + blen <= len; i++) {
+ size_t pstart;
+ if (memcmp(buf + i, begin, blen) != 0) continue;
+ pstart = i + blen;
+ for (pend = pstart; pend + elen <= len; pend++) {
+ if (memcmp(buf + pend, end, elen) == 0) {
+ if (payload) *payload = buf + pstart;
+ if (payloadlen) *payloadlen = pend - pstart;
+ return 1;
+ }
+ }
+ // Begin marker without an end: no valid block
+ return 0;
+ }
+ return 0;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/asc/armor.h b/src/fmt/asc/armor.h
@@ -0,0 +1,36 @@
+// Shared ascii-armor block locating for the asc format.
+//
+// A block looks like:
+//
+// -----BEGIN <label>-----
+// <base64 payload, 64-col wrapped>
+// -----END <label>-----
+//
+// All scanning is bounded by the buffer length, never assuming
+// NUL-termination.
+
+#ifndef __SUPERCOP_FMT_ASC_ARMOR_H__
+#define __SUPERCOP_FMT_ASC_ARMOR_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Namespaced labels: bare "PRIVATE KEY"/"PUBLIC KEY" would invite openssl
+// to misparse our raw payload as DER/PKCS#8.
+#define ASC_LABEL_PRIVATE "SUPERCOP PRIVATE KEY"
+#define ASC_LABEL_PUBLIC "SUPERCOP PUBLIC KEY"
+
+// Locate the first block with the given label. On success sets
+// *payload/*payloadlen to the bytes between the markers and returns 1,
+// otherwise returns 0. payload/payloadlen may be NULL to just test presence.
+int asc_find_block(const unsigned char *buf, size_t len, const char *label,
+ const unsigned char **payload, size_t *payloadlen);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __SUPERCOP_FMT_ASC_ARMOR_H__
diff --git a/src/fmt/asc/b64.c b/src/fmt/asc/b64.c
@@ -0,0 +1,113 @@
+#include <stdlib.h>
+
+#include "b64.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static const char asc_b64_alphabet[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+char *asc_b64_encode(const unsigned char *in, size_t len, size_t *outlen) {
+ size_t rawlen, full_lines, i, o, raw;
+ char *out;
+ unsigned int n;
+
+ if (!in || !outlen || len == 0) return NULL;
+ // 4 chars per 3 bytes, plus '\n' per 64-char line, plus trailing '\n'
+ rawlen = ((len + 2) / 3) * 4;
+ full_lines = (rawlen + 63) / 64;
+ out = malloc(rawlen + full_lines + 1);
+ if (!out) return NULL;
+
+ o = 0;
+ raw = 0;
+ for (i = 0; i < len; i += 3) {
+ size_t rem = len - i;
+ n = (unsigned int)in[i] << 16;
+ if (rem > 1) n |= (unsigned int)in[i + 1] << 8;
+ if (rem > 2) n |= in[i + 2];
+ out[o++] = asc_b64_alphabet[(n >> 18) & 0x3F];
+ out[o++] = asc_b64_alphabet[(n >> 12) & 0x3F];
+ out[o++] = (rem > 1) ? asc_b64_alphabet[(n >> 6) & 0x3F] : '=';
+ out[o++] = (rem > 2) ? asc_b64_alphabet[n & 0x3F] : '=';
+ raw += 4;
+ if (raw % 64 == 0) out[o++] = '\n';
+ }
+ if (out[o - 1] != '\n') out[o++] = '\n';
+ out[o] = '\0';
+ *outlen = o;
+ return out;
+}
+
+static int asc_b64_value(unsigned char 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;
+}
+
+static int asc_b64_is_ws(unsigned char c) {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
+}
+
+unsigned char *asc_b64_decode(const unsigned char *in, size_t len, size_t *outlen) {
+ size_t cleanlen = 0, i, o;
+ unsigned char *clean, *out;
+ unsigned int n;
+ int a, b, c, d;
+
+ if (!in || !outlen || len == 0) return NULL;
+ clean = malloc(len);
+ if (!clean) return NULL;
+ for (i = 0; i < len; i++) {
+ if (asc_b64_is_ws(in[i])) continue;
+ if (in[i] == '=' || asc_b64_value(in[i]) >= 0) {
+ clean[cleanlen++] = in[i];
+ } else {
+ free(clean);
+ return NULL;
+ }
+ }
+ // Input must arrive in complete 4-char quanta
+ if (cleanlen == 0 || cleanlen % 4 != 0) {
+ free(clean);
+ return NULL;
+ }
+ out = malloc((cleanlen / 4) * 3);
+ if (!out) {
+ free(clean);
+ return NULL;
+ }
+ o = 0;
+ for (i = 0; i < cleanlen; i += 4) {
+ a = asc_b64_value(clean[i]);
+ b = asc_b64_value(clean[i + 1]);
+ c = (clean[i + 2] == '=') ? 0 : asc_b64_value(clean[i + 2]);
+ d = (clean[i + 3] == '=') ? 0 : asc_b64_value(clean[i + 3]);
+ // Padding only valid as the last quantum's tail
+ if (a < 0 || b < 0 || c < 0 || d < 0 ||
+ clean[i] == '=' || clean[i + 1] == '=' ||
+ (clean[i + 2] == '=' && clean[i + 3] != '=') ||
+ ((clean[i + 2] == '=' || clean[i + 3] == '=') && i + 4 != cleanlen)) {
+ free(clean);
+ free(out);
+ return NULL;
+ }
+ n = ((unsigned int)a << 18) | ((unsigned int)b << 12) |
+ ((unsigned int)c << 6) | (unsigned int)d;
+ out[o++] = (n >> 16) & 0xFF;
+ if (clean[i + 2] != '=') out[o++] = (n >> 8) & 0xFF;
+ if (clean[i + 3] != '=') out[o++] = n & 0xFF;
+ }
+ free(clean);
+ *outlen = o;
+ return out;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/asc/b64.h b/src/fmt/asc/b64.h
@@ -0,0 +1,25 @@
+// Minimal internal base64 (RFC 4648 alphabet), just enough for the asc armor.
+// No external dependency; operates on length-bounded buffers.
+
+#ifndef __SUPERCOP_FMT_ASC_B64_H__
+#define __SUPERCOP_FMT_ASC_B64_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Encodes in, wrapping lines at 64 columns with '\n', trailing '\n' included.
+// Returns malloc'd buffer with *outlen set, or NULL on failure/empty input.
+char *asc_b64_encode(const unsigned char *in, size_t len, size_t *outlen);
+
+// Decodes in, skipping whitespace (space/tab/CR/LF). Expects correct '='
+// padding. Returns malloc'd buffer with *outlen set, or NULL on failure.
+unsigned char *asc_b64_decode(const unsigned char *in, size_t len, size_t *outlen);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __SUPERCOP_FMT_ASC_B64_H__
diff --git a/src/fmt/asc/common.h b/src/fmt/asc/common.h
@@ -0,0 +1,11 @@
+#include "../common.h"
+#include "../../keypair/keypair.h"
+
+#ifndef __SUPERCOP_FMT_ASC_COMMON_H__
+#define __SUPERCOP_FMT_ASC_COMMON_H__
+
+char fmt_asc_detect(unsigned char *cipherdata, size_t len);
+char * fmt_asc_encode(struct KeyPair *keypair, size_t *len);
+struct KeyPair * fmt_asc_decode(unsigned char *cipherdata, size_t len);
+
+#endif // __SUPERCOP_FMT_ASC_COMMON_H__
diff --git a/src/fmt/asc/decode.c b/src/fmt/asc/decode.c
@@ -0,0 +1,94 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+#include "armor.h"
+#include "b64.h"
+#include "orlp/ed25519.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Decode one armor block into malloc'd raw bytes; NULL unless the block
+// exists, is valid base64, and decodes to exactly wantlen bytes.
+static unsigned char *asc_block_bytes(unsigned char *cipherdata, size_t len,
+ const char *label, size_t wantlen) {
+ const unsigned char *payload;
+ size_t payloadlen;
+ unsigned char *raw;
+ size_t rawlen;
+ if (!asc_find_block(cipherdata, len, label, &payload, &payloadlen)) return NULL;
+ raw = asc_b64_decode(payload, payloadlen, &rawlen);
+ if (!raw || rawlen != wantlen) {
+ if (raw) free(raw);
+ return NULL;
+ }
+ return raw;
+}
+
+static struct KeyPair *asc_pair_alloc(void) {
+ struct KeyPair *kp = calloc(1, sizeof(struct KeyPair));
+ if (!kp) return NULL;
+ kp->public_key = calloc(1, 32);
+ if (!kp->public_key) {
+ free(kp);
+ return NULL;
+ }
+ return kp;
+}
+
+struct KeyPair * fmt_asc_decode(unsigned char *cipherdata, size_t len) {
+ unsigned char *privraw;
+ unsigned char *pubraw;
+ struct KeyPair *kp;
+
+ // A PRIVATE block is authoritative for full keys: present-but-corrupt
+ // fails closed instead of silently downgrading to pub-only.
+ if (asc_find_block(cipherdata, len, ASC_LABEL_PRIVATE, NULL, NULL)) {
+ privraw = asc_block_bytes(cipherdata, len, ASC_LABEL_PRIVATE, 64);
+ if (!privraw) return NULL;
+ kp = asc_pair_alloc();
+ if (!kp) {
+ free(privraw);
+ return NULL;
+ }
+ kp->private_key = calloc(1, 64);
+ if (!kp->private_key) {
+ free(privraw);
+ free(kp->public_key);
+ free(kp);
+ return NULL;
+ }
+ memcpy(kp->private_key, privraw, 64);
+ if (privraw) free(privraw);
+ ed25519_derive_pubkey(kp->public_key, kp->private_key);
+ // An accompanying PUBLIC block must agree when present
+ pubraw = asc_block_bytes(cipherdata, len, ASC_LABEL_PUBLIC, 32);
+ if (pubraw) {
+ int ok = memcmp(pubraw, kp->public_key, 32) == 0;
+ free(pubraw);
+ if (!ok) {
+ keypair_free(kp);
+ return NULL;
+ }
+ }
+ return kp;
+ }
+
+ // Lone PUBLIC block (32 public bytes) decodes as a pub-only pair
+ pubraw = asc_block_bytes(cipherdata, len, ASC_LABEL_PUBLIC, 32);
+ if (!pubraw) return NULL;
+ kp = asc_pair_alloc();
+ if (!kp) {
+ free(pubraw);
+ return NULL;
+ }
+ memcpy(kp->public_key, pubraw, 32);
+ if (pubraw) free(pubraw);
+ return kp;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/asc/detect.c b/src/fmt/asc/detect.c
@@ -0,0 +1,22 @@
+#include <stddef.h>
+#include <string.h>
+
+#include "common.h"
+#include "armor.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+char fmt_asc_detect(unsigned char *cipherdata, size_t len) {
+ // Stream-friendly: either armor block may start on any line.
+ // Claiming is cheap; decode validates and returns NULL on garbage,
+ // letting readKeyFile move on to the next format.
+ if (asc_find_block(cipherdata, len, ASC_LABEL_PRIVATE, NULL, NULL)) return 1;
+ if (asc_find_block(cipherdata, len, ASC_LABEL_PUBLIC, NULL, NULL)) return 1;
+ return 0;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/asc/encode.c b/src/fmt/asc/encode.c
@@ -0,0 +1,74 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+#include "armor.h"
+#include "b64.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Wrap raw bytes in an ascii-armor block; returns malloc'd NUL-terminated text.
+static char *asc_wrap(const char *label, const unsigned char *raw, size_t rawlen,
+ size_t *outlen) {
+ char *b64, *out;
+ size_t b64len, need;
+ int n;
+
+ b64 = asc_b64_encode(raw, rawlen, &b64len);
+ if (!b64) return NULL;
+ // "-----BEGIN %s-----\n" + b64 + "-----END %s-----\n" + NUL
+ need = strlen("-----BEGIN -----\n-----END -----\n") + 2 * strlen(label) + b64len + 1;
+ out = malloc(need);
+ if (!out) {
+ free(b64);
+ return NULL;
+ }
+ n = snprintf(out, need, "-----BEGIN %s-----\n%s-----END %s-----\n",
+ label, b64, label);
+ free(b64);
+ if (n < 0 || (size_t)n >= need) {
+ free(out);
+ return NULL;
+ }
+ *outlen = (size_t)n;
+ return out;
+}
+
+char * fmt_asc_encode(struct KeyPair *kp, size_t *len) {
+ char *priv = NULL, *pub = NULL, *out;
+ size_t privlen = 0, publen = 0;
+
+ if (!kp || !kp->public_key || !len) return NULL;
+ // Armor carries just the key bytes, no prefix. A full pair emits both a
+ // PRIVATE block (64 private bytes) and a PUBLIC block (32 public bytes);
+ // a pub-only pair emits just the PUBLIC block.
+ if (kp->private_key) {
+ priv = asc_wrap(ASC_LABEL_PRIVATE, kp->private_key, 64, &privlen);
+ if (!priv) return NULL;
+ }
+ pub = asc_wrap(ASC_LABEL_PUBLIC, kp->public_key, 32, &publen);
+ if (!pub) {
+ if (priv) free(priv);
+ return NULL;
+ }
+ out = malloc(privlen + publen + 1);
+ if (!out) {
+ if (priv) free(priv);
+ if (pub) free(pub);
+ return NULL;
+ }
+ memcpy(out, priv, privlen);
+ memcpy(out + privlen, pub, publen);
+ out[privlen + publen] = '\0';
+ free(priv);
+ free(pub);
+ *len = privlen + publen;
+ return out;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/asc/register.c b/src/fmt/asc/register.c
@@ -0,0 +1,30 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Format *supercop_formats;
+
+void __attribute__ ((constructor)) fmt_asc_register() {
+ struct Format *fmt = calloc(1, sizeof(struct Format));
+ if (!fmt) return;
+ fmt->next = supercop_formats;
+ fmt->name = calloc(1, 4);
+ if (!fmt->name) {
+ free(fmt);
+ return;
+ }
+ fmt->detect = fmt_asc_detect;
+ fmt->encode = fmt_asc_encode;
+ fmt->decode = fmt_asc_decode;
+ supercop_formats = fmt;
+ strcpy(fmt->name, "asc");
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/hdr/common.h b/src/fmt/hdr/common.h
@@ -0,0 +1,11 @@
+#include "../common.h"
+#include "../../keypair/keypair.h"
+
+#ifndef __SUPERCOP_FMT_HDR_COMMON_H__
+#define __SUPERCOP_FMT_HDR_COMMON_H__
+
+char fmt_hdr_detect(unsigned char *cipherdata, size_t len);
+char * fmt_hdr_encode(struct KeyPair *keypair, size_t *len);
+struct KeyPair * fmt_hdr_decode(unsigned char *cipherdata, size_t len);
+
+#endif // __SUPERCOP_FMT_HDR_COMMON_H__
diff --git a/src/fmt/hdr/decode.c b/src/fmt/hdr/decode.c
@@ -0,0 +1,102 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+#include "scan.h"
+#include "orlp/ed25519.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static int hdr_hexval(unsigned char c) {
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+}
+
+// Parse exactly hexlen hex chars into out; returns 1 on success.
+static int hdr_parse_hex(const unsigned char *val, size_t vallen,
+ unsigned char *out, size_t hexlen) {
+ size_t i;
+ int hi, lo;
+ if (vallen != hexlen * 2) return 0;
+ for (i = 0; i < hexlen; i++) {
+ hi = hdr_hexval(val[i * 2]);
+ lo = hdr_hexval(val[i * 2 + 1]);
+ if (hi < 0 || lo < 0) return 0;
+ out[i] = (unsigned char)((hi << 4) | lo);
+ }
+ return 1;
+}
+
+struct KeyPair * fmt_hdr_decode(unsigned char *cipherdata, size_t len) {
+ const unsigned char *pos = cipherdata;
+ const unsigned char *end = cipherdata + len;
+ const unsigned char *line;
+ size_t linelen;
+ int which;
+ const unsigned char *val;
+ size_t vallen;
+ unsigned char pub[32];
+ unsigned char priv[64];
+ int seen_pub = 0, have_pub = 0;
+ int seen_priv = 0, have_priv = 0;
+ struct KeyPair *kp;
+
+ // First valid occurrence of each label wins; malformed hex runs are
+ // ignored so a later valid block in the stream can still win.
+ while (hdr_next_line(&pos, end, &line, &linelen)) {
+ if (!hdr_match_label(line, linelen, &which, &val, &vallen)) continue;
+ if (which == 1 && seen_pub) continue;
+ if (which == 2 && seen_priv) continue;
+ if (vallen == 0) continue;
+ // Parenthesized value marks the field absent (e.g. "(no private key)")
+ if (val[0] == '(') {
+ if (which == 1) seen_pub = 1;
+ else seen_priv = 1;
+ continue;
+ }
+ if (which == 1) {
+ if (!hdr_parse_hex(val, vallen, pub, 32)) continue;
+ seen_pub = 1;
+ have_pub = 1;
+ } else {
+ if (!hdr_parse_hex(val, vallen, priv, 64)) continue;
+ seen_priv = 1;
+ have_priv = 1;
+ }
+ }
+
+ if (!have_pub && !have_priv) return NULL;
+
+ // A lone private key still yields its public half via scalar mult
+ if (!have_pub) {
+ ed25519_derive_pubkey(pub, priv);
+ have_pub = 1;
+ }
+
+ kp = calloc(1, sizeof(struct KeyPair));
+ if (!kp) return NULL;
+ kp->public_key = calloc(1, 32);
+ if (!kp->public_key) {
+ free(kp);
+ return NULL;
+ }
+ memcpy(kp->public_key, pub, 32);
+ if (have_priv) {
+ kp->private_key = calloc(1, 64);
+ if (!kp->private_key) {
+ free(kp->public_key);
+ free(kp);
+ return NULL;
+ }
+ memcpy(kp->private_key, priv, 64);
+ }
+ return kp;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/hdr/detect.c b/src/fmt/hdr/detect.c
@@ -0,0 +1,38 @@
+#include <stddef.h>
+
+#include "common.h"
+#include "scan.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static int hdr_is_hex(unsigned char c) {
+ return (c >= '0' && c <= '9') ||
+ (c >= 'a' && c <= 'f') ||
+ (c >= 'A' && c <= 'F');
+}
+
+char fmt_hdr_detect(unsigned char *cipherdata, size_t len) {
+ const unsigned char *pos = cipherdata;
+ const unsigned char *end = cipherdata + len;
+ const unsigned char *line;
+ size_t linelen;
+ int which;
+ const unsigned char *val;
+ size_t vallen;
+
+ // Claim the buffer when any key-labeled line with a non-empty value
+ // appears anywhere in it; surrounding stream content is irrelevant.
+ while (hdr_next_line(&pos, end, &line, &linelen)) {
+ if (!hdr_match_label(line, linelen, &which, &val, &vallen)) continue;
+ if (vallen == 0) continue;
+ // A hex run or a parenthesized absent-marker both count as values
+ if (val[0] == '(' || hdr_is_hex(val[0])) return 1;
+ }
+ return 0;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/hdr/encode.c b/src/fmt/hdr/encode.c
@@ -0,0 +1,52 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static void hdr_hex_encode(char *dst, const unsigned char *src, size_t n) {
+ static const char digits[] = "0123456789abcdef";
+ size_t i;
+ for (i = 0; i < n; i++) {
+ dst[i * 2] = digits[(src[i] >> 4) & 0xF];
+ dst[i * 2 + 1] = digits[src[i] & 0xF];
+ }
+ dst[n * 2] = '\0';
+}
+
+char * fmt_hdr_encode(struct KeyPair *kp, size_t *len) {
+ char pubhex[65];
+ char privhex[129];
+ const char *privstr;
+ char *out;
+ size_t need;
+ int n;
+
+ if (!kp || !kp->public_key || !len) return NULL;
+ hdr_hex_encode(pubhex, kp->public_key, 32);
+ if (kp->private_key) {
+ hdr_hex_encode(privhex, kp->private_key, 64);
+ privstr = privhex;
+ } else {
+ privstr = "(no private key)";
+ }
+
+ need = strlen("public-key: \nprivate-key: \n") + 64 + strlen(privstr) + 1;
+ out = malloc(need);
+ if (!out) return NULL;
+ n = snprintf(out, need, "public-key: %s\nprivate-key: %s\n", pubhex, privstr);
+ if (n < 0 || (size_t)n >= need) {
+ free(out);
+ return NULL;
+ }
+ *len = (size_t)n;
+ return out;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/hdr/register.c b/src/fmt/hdr/register.c
@@ -0,0 +1,30 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern struct Format *supercop_formats;
+
+void __attribute__ ((constructor)) fmt_hdr_register() {
+ struct Format *fmt = calloc(1, sizeof(struct Format));
+ if (!fmt) return;
+ fmt->next = supercop_formats;
+ fmt->name = calloc(1, 4);
+ if (!fmt->name) {
+ free(fmt);
+ return;
+ }
+ fmt->detect = fmt_hdr_detect;
+ fmt->encode = fmt_hdr_encode;
+ fmt->decode = fmt_hdr_decode;
+ supercop_formats = fmt;
+ strcpy(fmt->name, "hdr");
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/hdr/scan.c b/src/fmt/hdr/scan.c
@@ -0,0 +1,77 @@
+#include <stddef.h>
+
+#include "scan.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static int hdr_is_ws(unsigned char c) {
+ return c == ' ' || c == '\t' || c == '\r';
+}
+
+static int hdr_lower(unsigned char c) {
+ if (c >= 'A' && c <= 'Z') return c + ('a' - 'A');
+ return c;
+}
+
+// Case-insensitive literal match, bounded by remaining length.
+static int hdr_match_word(const unsigned char *p, size_t left, const char *word) {
+ size_t i = 0;
+ while (word[i]) {
+ if (i >= left) return 0;
+ if (hdr_lower(p[i]) != (unsigned char)word[i]) return 0;
+ i++;
+ }
+ return 1;
+}
+
+int hdr_match_label(const unsigned char *line, size_t linelen, int *which,
+ const unsigned char **val, size_t *vallen) {
+ size_t i = 0;
+ int w = 0;
+ size_t wlen = 0;
+
+ while (i < linelen && hdr_is_ws(line[i])) i++;
+
+ if (hdr_match_word(line + i, linelen - i, "public-key")) {
+ w = 1;
+ wlen = 10;
+ } else if (hdr_match_word(line + i, linelen - i, "private-key")) {
+ w = 2;
+ wlen = 11;
+ } else {
+ return 0;
+ }
+ i += wlen;
+
+ while (i < linelen && hdr_is_ws(line[i])) i++;
+ if (i >= linelen || line[i] != ':') return 0;
+ i++;
+
+ while (i < linelen && hdr_is_ws(line[i])) i++;
+
+ *which = w;
+ *val = line + i;
+ *vallen = linelen - i;
+ while (*vallen > 0 && hdr_is_ws((*val)[*vallen - 1])) (*vallen)--;
+ return 1;
+}
+
+int hdr_next_line(const unsigned char **pos, const unsigned char *end,
+ const unsigned char **line, size_t *linelen) {
+ const unsigned char *nl;
+ if (*pos >= end) return 0;
+ *line = *pos;
+ nl = *pos;
+ while (nl < end && *nl != '\n') nl++;
+ *linelen = (size_t)(nl - *pos);
+ // Strip a trailing carriage return so CRLF streams parse cleanly
+ if (*linelen > 0 && (*pos)[*linelen - 1] == '\r') (*linelen)--;
+ *pos = (nl < end) ? nl + 1 : end;
+ return 1;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/fmt/hdr/scan.h b/src/fmt/hdr/scan.h
@@ -0,0 +1,34 @@
+// Internal line scanner shared by the hdr detect/decode implementations.
+//
+// A key line looks like:
+//
+// [spaces/tabs] label [spaces/tabs] : [spaces/tabs] value [spaces/tabs]
+//
+// where label is public-key or private-key (case-insensitive). Matching is
+// done against the length-bounded buffer, never assuming NUL-termination.
+
+#ifndef __SUPERCOP_FMT_HDR_SCAN_H__
+#define __SUPERCOP_FMT_HDR_SCAN_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// which: 1 = public-key, 2 = private-key
+// Returns 1 and fills which/val/vallen when line holds a key label,
+// 0 otherwise. val/vallen may point at an empty value; callers decide.
+int hdr_match_label(const unsigned char *line, size_t linelen, int *which,
+ const unsigned char **val, size_t *vallen);
+
+// Advance helpers for walking a length-bounded buffer line by line.
+// Returns 1 while a line (without its '\n') is available.
+int hdr_next_line(const unsigned char **pos, const unsigned char *end,
+ const unsigned char **line, size_t *linelen);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __SUPERCOP_FMT_HDR_SCAN_H__