commit 5704791f29736c06d932c385e746f32c481cbb44
parent 9e004ebf1826ac067f5b6ab8c2790eb16ad52c6c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 14:20:12 -0700
pkg: basic signed code distribution (cfree pkg)
Add a `cfree pkg` tool for producing and consuming signed,
self-describing `.cfpkg` packages, plus the supporting subsystem under
driver/dist/. Design in doc/DISTRIBUTE.md.
- Package = signed INI manifest + detached minisig + gzip payload +
signer pubkey, bagged into one uncompressed-tar .cfpkg. The signature
covers only the manifest; the payload is reached by hash through it.
sha256(manifest) is the package id, cross-checked via the signed
trusted comment.
- Subcommands: keygen, create, verify, unpack, inspect, trust
{list,add,remove}. Trust is anchored by a trusted-keys file
($CFREE_TRUSTED_KEYS or ~/.config/cfree/trusted_keys); --tofu pins the
bundled key on first use. Manifest has no timestamp (reproducible);
signing time lives in the trusted comment.
- Key/signature files use minisign's exact byte layout (passwordless
secret keys, kdf_alg={0,0}); interchangeable with stock minisign once
the real crypto lands. Encrypted (scrypt) keys are detected/rejected.
- Real now: tar, base64, gzip wrapper + CRC32 + stored DEFLATE blocks,
manifest/minisig/trust glue, and key-generation entropy via a new
driver_random_bytes env shim (/dev/urandom on POSIX, rand_s on
Windows). STUBBED (insecure, marked at each definition): dist_sha256,
dist_blake2b, dist_ed25519, and DEFLATE compression. doc lists the
exact interfaces to make real.
Gated by CFREE_TOOL_PKG_ENABLED (config.h, mk/config.mk, Makefile,
driver.h, main.c).
Diffstat:
30 files changed, 2866 insertions(+), 19 deletions(-)
diff --git a/Makefile b/Makefile
@@ -347,6 +347,14 @@ endif
ifeq ($(CFREE_TOOL_STRINGS_ENABLED),1)
DRIVER_TOOL_SRCS += driver/strings.c
endif
+ifeq ($(CFREE_TOOL_PKG_ENABLED),1)
+DRIVER_TOOL_SRCS += driver/pkg.c
+DRIVER_TOOL_SRCS += driver/dist/dist.c driver/dist/b64.c driver/dist/sha256.c \
+ driver/dist/blake2b.c driver/dist/ed25519.c \
+ driver/dist/tar.c driver/dist/deflate.c \
+ driver/dist/manifest.c driver/dist/minisig.c \
+ driver/dist/trust.c
+endif
DRIVER_SRCS += $(sort $(DRIVER_TOOL_SRCS))
ifneq ($(filter 1,$(CFREE_TOOL_CC_ENABLED) $(CFREE_TOOL_CHECK_ENABLED) $(CFREE_TOOL_CPP_ENABLED) $(CFREE_TOOL_AS_ENABLED) $(CFREE_TOOL_DBG_ENABLED) $(CFREE_TOOL_RUN_ENABLED)),)
DRIVER_SRCS += driver/cflags.c
diff --git a/doc/DISTRIBUTE.md b/doc/DISTRIBUTE.md
@@ -0,0 +1,387 @@
+# Code distribution: packaging, signing, and verification
+
+This document specifies how cfree produces and consumes signed,
+self-describing code packages. The goal is *basic* code distribution: a
+producer can bundle a build into a single verifiable artifact, and a
+consumer can authenticate it against a trusted key and unpack it — with
+no network code in cfree.
+
+## Scope
+
+In scope:
+
+- A **package format** built from a manifest, a detached signature, and
+ a compressed payload, optionally bagged into a single `.cfpkg` file.
+- **Signing and verification** using the minisign signature scheme
+ (Ed25519 over a BLAKE2b prehash).
+- A **trust model**: a trusted-keys file, with opt-in trust-on-first-use
+ (TOFU).
+- **Content addressing**: every payload byte is reached by SHA-256
+ through the signed manifest.
+
+Out of scope (deliberately):
+
+- **Transport / fetch.** cfree does not speak HTTP, TLS, or any network
+ protocol. Moving bytes from producer to consumer is the job of
+ existing tools (`scp`, `curl`, `git`, a CDN, email). cfree only
+ *produces* and *consumes* the artifact. This keeps cfree out of the
+ ongoing security-maintenance burden of a TLS stack.
+- **Dependency resolution.** The manifest reserves a `[dependency]`
+ schema (see below) so manifests are forward-compatible, but v1 tooling
+ does not resolve, fetch, or version-solve dependencies.
+
+## Vendored primitives
+
+Distribution adds five small freestanding primitives. All are pure
+computation — no syscalls — and fit cfree's freestanding posture. SHA-256
+already exists (`src/core/sha256.c`).
+
+| Primitive | Purpose | Notes |
+|---|---|---|
+| **tar** | container read/write | header format only; trivial |
+| **DEFLATE** | gzip payload compression | inflate + deflate; the bulk of the work |
+| **Ed25519** | signature scheme | pulls in SHA-512 internally |
+| **BLAKE2b** | minisign prehash | hashes the signed file before Ed25519 |
+| **base64** | key/signature text encoding | minisign keys and sigs are base64 |
+| SHA-256 | content addressing | already vendored |
+
+Encoding conventions: **hashes are lowercase hex** in the manifest
+(diff-friendly); **base64 is reserved strictly for key material and
+signatures** (minisign's native encoding).
+
+## The package: three canonical files, one bundle
+
+A package is canonically **three files**:
+
+| File | Role | Trust |
+|---|---|---|
+| `<name>-<version>.manifest` | signed root: metadata + `archive-sha256` + per-file hashes | **the** signed object |
+| `<name>-<version>.manifest.minisig` | detached minisign signature over the manifest's literal bytes | the proof |
+| `<name>-<version>.tar.gz` | payload blob; its SHA-256 equals `archive-sha256` in the manifest | trusted *transitively* via the manifest |
+
+The signature **only ever covers the manifest** (~1 KB of text).
+Everything else is reached by hash *through* the manifest, so the large
+payload is never signed directly — the signed text vouches for it.
+
+Because moving three loose files by hand is clunky and transport is out
+of scope, the **distribution unit is a single-file bundle**:
+
+```
+<name>-<version>.cfpkg = uncompressed tar of { manifest, manifest.minisig, tar.gz, .pub }
+```
+
+The bag also carries a fourth member — the signer's **public key**
+(`<name>-<version>.pub`). It is *untrusted* (anyone can put a key in a
+bag); it exists only so a verifier running TOFU can pin it without a
+separate out-of-band fetch. It is never consulted unless the verifier
+opts into TOFU, and even then only after its key id matches the
+signature's.
+
+The outer tar is **uncompressed, unsigned, and trust-neutral**: it is
+pure transport glue. Nothing inside it is trusted until the signature
+inside it checks out. There is no chicken-and-egg, because the signature
+is a *sibling* of the manifest inside the bag, not nested under what it
+signs.
+
+The `.cfpkg` is the implemented distribution unit; emitting the three
+loose files alongside it is a planned producer flag. **Consumers accept
+the `.cfpkg`.**
+
+## Manifest format
+
+The manifest is INI-style, line-oriented text. It is designed around one
+overriding constraint: **it is the signed object**, so it must be
+
+1. **byte-stable** — we sign the literal bytes on disk; the parser only
+ *reads* and never reserializes. This sidesteps JSON-style
+ canonicalization entirely (key ordering, whitespace, number
+ formatting).
+2. **trivially parseable freestanding** — sectioned `key = value` lines,
+ no recursive value grammar, no escaping rules.
+3. **hash-pinning** — it carries the SHA-256 of the payload archive and
+ of each file, so the signature transitively covers all of it.
+
+### Grammar
+
+- First line is the exact magic and format version: `cfree-manifest 1`.
+ A parser that does not recognize the version **rejects** the file.
+- Top-level `key = value` lines precede any section.
+- `[artifact]` and `[dependency]` introduce repeatable blocks.
+- `value` is the rest of the line, trimmed of surrounding whitespace, so
+ free-text values (e.g. `description`) may contain spaces. There is no
+ escaping and no multi-line values.
+- Lines beginning with `#` are comments. **Comments and blank lines are
+ part of the signed bytes** — they are not normalized away. Tooling
+ that rewrites a manifest produces a fresh byte stream that must be
+ re-signed.
+- **Unknown keys are an error** (strict parse). Because the signed
+ surface is the literal bytes, there is no safe "ignore unknown" — an
+ unrecognized key means a manifest from a newer producer, which the
+ consumer must not silently accept.
+
+### Fields
+
+Top-level:
+
+| Key | Required | Meaning |
+|---|---|---|
+| `name` | yes | package name (human handle) |
+| `version` | yes | version string; ordering is a semver subset |
+| `archive` | yes | payload filename |
+| `archive-sha256` | yes | hex SHA-256 of the payload `.tar.gz` bytes |
+| `archive-size` | yes | payload size in bytes |
+| `description` | no | one-line free text |
+
+There is **deliberately no timestamp** in the manifest: two builds of
+the same inputs produce byte-identical manifests, hence the same package
+id, so the artifact stays reproducible. Signing *time* is a property of
+the signing act, not the artifact, so it lives in the signature's
+trusted comment (`created=<unix-seconds>`) — signed, tamper-evident, and
+outside the reproducible surface.
+
+`[artifact]` block (repeatable, one per distributable file):
+
+| Key | Required | Meaning |
+|---|---|---|
+| `path` | yes | path within the unpacked payload |
+| `sha256` | yes | hex SHA-256 of the **uncompressed** file contents |
+| `size` | yes | uncompressed file size in bytes |
+| `kind` | yes | `exe` \| `dso` \| `obj` \| `wasm` \| `lib` \| `data` \| `source` |
+| `target` | no | cfree triple (see below); omit ⇒ target-independent |
+| `entry` | no | `true` if runnable under jit/emu/wasm |
+
+`[dependency]` block (repeatable; **reserved** — see Scope):
+
+| Key | Required | Meaning |
+|---|---|---|
+| `name` | yes | dependency package name |
+| `version` | yes | version constraint, e.g. `>=1.2.0` |
+| `sha256` | no | pin to the dependency's manifest hash (its package id) |
+| `key` | no | hex id of the expected signer key |
+
+v1 tooling validates the `[dependency]` schema but performs no
+resolution.
+
+### Target triples
+
+The `target` field uses cfree's **own** canonical triple strings,
+parsed and formatted by `driver_target_from_triple` /
+`driver_target_to_triple` (`driver/target.c`) — not a parallel naming
+scheme. Examples: `x86_64-linux`, `aarch64-apple-darwin`,
+`riscv64-elf`, `wasm32-wasi`. A package is naturally multi-target: it
+carries one `[artifact]` per build.
+
+### Example
+
+```ini
+cfree-manifest 1
+name = hello
+version = 0.3.1
+description = minimal greeting program
+archive = hello-0.3.1.tar.gz
+archive-sha256 = 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
+archive-size = 20480
+
+[artifact]
+path = bin/hello-x64
+target = x86_64-linux
+kind = exe
+sha256 = 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae
+size = 16384
+entry = true
+
+[artifact]
+path = bin/hello-arm
+target = aarch64-apple-darwin
+kind = exe
+sha256 = fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9
+size = 16512
+
+[dependency]
+name = libfoo
+version = >=1.2.0
+sha256 = a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00
+```
+
+## Identity and content addressing
+
+`sha256(manifest)` is the package's **immutable cryptographic id**.
+`name@version` is the human handle; the manifest hash is what a
+dependency pins to (`[dependency].sha256`). The full chain:
+
+```
+signer signs manifest → sha256(manifest) is the package id
+manifest pins archive → archive unpacks to per-file hash-checked tree
+```
+
+The package id is *also* recorded in the minisign signature's trusted
+comment as `pkgid=<hex>` (alongside `created=<unix-seconds>`), giving a
+second, signed assertion of the id. Verification recomputes
+`sha256(manifest)` and **rejects** the package if it does not match the
+`pkgid` in the (signed) trusted comment.
+
+## Trust model
+
+A signature is worthless without a way to anchor the signing key.
+cfree's anchor is a **trusted-keys file**.
+
+- **Trusted-keys file.** A text list of trusted public keys, one per
+ line (`<keyid-hex> <pubkey-base64> <label>`), consulted at
+ verification time. Path: `$CFREE_TRUSTED_KEYS` if set, else
+ `$HOME/.config/cfree/trusted_keys`. Verification succeeds only if the
+ signature validates against a key in this file (or an explicit `-p`
+ key).
+- **Key id.** Each key has a short id (minisign's key id), used in
+ `[dependency].key` hints and in trust-store lookups. **A key carried
+ in a bundle (the `.pub` member) or named in a manifest is never
+ trust** — it is at most a TOFU candidate / hint. Trust comes only from
+ the trusted-keys file.
+- **TOFU is opt-in.** By default, an unknown signer fails verification.
+ With `--tofu`, the consumer pins the bundle's bundled public key on
+ first use — but only after its key id matches the signature's —
+ recording it in the trusted-keys file. Subsequent verifications
+ require the pinned key to match. TOFU is never the default and is
+ always an explicit, recorded action.
+
+## Verification chain
+
+```
+pkg.manifest.minisig ──verifies (against trusted key)──> pkg.manifest (literal bytes)
+ │ pins
+ archive-sha256 ──> verify pkg.tar.gz BEFORE decompressing │ (defends against decompression bombs / tampered tar)
+ [artifact].sha256 ──> verify each file AFTER unpack <───────┘
+```
+
+Steps:
+
+0. Crack the outer (uncompressed, unsigned) `.cfpkg` tar to recover its
+ members. Nothing extracted here is trusted until step 2 passes.
+1. Read the signature's key id; locate that public key in the
+ trusted-keys file (or use an explicit `-p` key, or pin the bundled
+ `.pub` via opt-in `--tofu`). On miss without TOFU: **fail**.
+2. Verify the detached signature over the manifest's literal bytes. On
+ mismatch: **fail**.
+3. Parse the manifest (strict; unknown keys/version: **fail**), and
+ confirm the signed trusted comment's `pkgid` equals
+ `sha256(manifest)`. On mismatch: **fail**.
+4. Hash the payload `.tar.gz` and compare to `archive-sha256` **before
+ decompressing**. On mismatch: **fail**.
+5. (unpack only) Decompress and untar; hash each extracted file and
+ compare to its `[artifact].sha256`. On mismatch: **fail**.
+
+## CLI
+
+Distribution slots into the existing binutils-style multitool
+(`cfree ar`, `cfree nm`, `cfree objdump`, `cfree ld`, …) as a single
+`pkg` tool with subcommands:
+
+```
+cfree pkg keygen -o BASE # write BASE.pub / BASE.key
+cfree pkg create --name N --version V [--desc D] \
+ -s SECKEY -o OUT.cfpkg FILE... # archive + manifest + sign + bundle
+cfree pkg verify [-p PUBKEY | --tofu] FILE.cfpkg # full chain (trust store / TOFU / hashes)
+cfree pkg unpack FILE.cfpkg -C DIR # verify, then extract the payload
+cfree pkg inspect FILE.cfpkg # print the manifest without unpacking
+cfree pkg trust {list | add PUBKEY [label] | remove KEYID}
+```
+
+`create` currently takes an explicit file list (one `[artifact]` per
+file); directory walking and richer per-file `kind`/`target` mapping are
+follow-ups.
+
+## Implementation status
+
+The orchestration (manifest, bundling, the trust store, the full
+verification chain), the **file formats**, the tar/base64/gzip+CRC32
+container layer, and **key generation entropy** are real. What remains
+stubbed is the **crypto math under `driver/dist/`**: `dist_sha256` /
+`dist_blake2b` are deterministic non-cryptographic digests, and
+`dist_ed25519` derives signatures from the seed (the "public key" *is*
+the seed). These are *insecure* placeholders, each marked at its
+definition.
+
+What is already real and final:
+
+- **Key/signature file layout is byte-identical to minisign.** Public
+ keys are `base64("Ed" || keyid || pk)`; signatures are
+ `base64("ED" || keyid || sig)` over a BLAKE2b prehash plus a global
+ signature over the trusted comment; secret keys use minisign's
+ passwordless struct (`kdf_alg = {0,0}`, no scrypt) with the `"B2"`
+ BLAKE2b checksum. So **once real Ed25519/BLAKE2b are vendored, cfree
+ and minisign key/signature files are interchangeable** — you can point
+ `-s` / `-p` at a passwordless minisign key. Password-encrypted secret
+ keys (`kdf_alg = "Sc"`) are detected and rejected with a clear error.
+- **Key generation uses the host CSPRNG.** Entropy is the one place real
+ crypto must touch the host; it flows through `driver_random_bytes`
+ (env.h → `/dev/urandom` on POSIX, `rand_s` on Windows). The crypto
+ modules stay pure: the seed and key id are passed *in*. This is also
+ the model for a future password prompt (a TTY shim in env.h).
+
+Until the math is vendored, `cfree pkg` still provides **no security**
+and stock `minisign` cannot validate cfree's (stubbed) signatures.
+
+## Implementation checklist: interfaces to make real
+
+Replacing the stub *bodies* below — without changing a single signature
+or any caller — turns this into a fully real, minisign-interoperable
+implementation. Each is its own self-contained file under `driver/dist/`.
+
+**Crypto / compression (the stubs):**
+
+- [ ] **`driver/dist/sha256.c`** — `dist_sha256`
+ ```c
+ void dist_sha256(uint8_t out[32], const uint8_t* data, size_t len);
+ ```
+ Real SHA-256. (Or expose the existing `src/core/sha256.c` to the driver
+ and forward to it.) Used for content addressing + the package id.
+
+- [ ] **`driver/dist/blake2b.c`** — `dist_blake2b`
+ ```c
+ void dist_blake2b(uint8_t out[64], const uint8_t* data, size_t len);
+ ```
+ Real BLAKE2b-512. Used as the minisign signature prehash and (first 32
+ bytes) as the secret-key checksum.
+
+- [ ] **`driver/dist/ed25519.c`** — keypair / sign / verify
+ ```c
+ void dist_ed25519_keypair(uint8_t pk[32], uint8_t sk[64], const uint8_t seed[32]);
+ void dist_ed25519_sign(uint8_t sig[64], const uint8_t* msg, size_t msglen, const uint8_t sk[64]);
+ int dist_ed25519_verify(const uint8_t sig[64], const uint8_t* msg, size_t msglen, const uint8_t pk[32]);
+ ```
+ Real Ed25519. **Must use the standard 64-byte secret-key layout
+ `sk = seed[32] || pk[32]`** — `pkg.c` and the secret-key file recover
+ the public key from `sk[32:64]`. `_verify` returns 1 on a valid
+ signature, 0 otherwise. Entropy is *not* this module's job: the seed is
+ passed in (filled by `driver_random_bytes`).
+
+- [ ] **`driver/dist/deflate.c`** — real compression
+ ```c
+ int dist_gz_compress(CfreeWriter* out, const uint8_t* data, size_t len);
+ int dist_gz_decompress(CfreeWriter* out, const uint8_t* data, size_t len);
+ ```
+ The gzip framing, CRC32, and stored-block handling are already real;
+ add real LZ77 + Huffman to `_compress`, and teach `_decompress` to
+ inflate compressed (non-stored) blocks. `DIST_OK` / `DIST_ERR`.
+
+**Already real — do not reimplement** (listed so the boundary is clear):
+`driver/dist/{b64,tar,manifest,minisig,trust,dist}.c`, the gzip
+wrapper/CRC32/stored blocks in `deflate.c`, and `driver_random_bytes`
+(env layer). `minisig.c` is the only consumer of the crypto stubs; it
+needs no change when they go real.
+
+**Additional, only for password-encrypted secret keys (optional):**
+
+- [ ] a `scrypt` KDF primitive, and
+- [ ] a password-prompt TTY shim in `env.h` (model on `driver_read_line`),
+ then have `dist_minisig_emit_seckey` / `parse_seckey` honor
+ `kdf_alg = "Sc"` instead of the passwordless `{0,0}` form. Not required
+ for verification or for passwordless-key interop.
+
+## Open questions / future work
+
+- The crypto/compression checklist above is the gating work; the file
+ formats, trust model, and entropy source are already final.
+- Add scrypt + a password-prompt TTY shim (env.h) for encrypted secret
+ keys, matching minisign's `kdf_alg = "Sc"` form.
+- Emit the three loose files alongside the `.cfpkg`, and accept a loose
+ manifest as `verify`/`unpack` input.
diff --git a/driver/dist/b64.c b/driver/dist/b64.c
@@ -0,0 +1,69 @@
+#include "b64.h"
+
+#include "dist.h"
+
+static const char B64_ENC[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+size_t dist_b64_encode(char* out, const uint8_t* in, size_t n) {
+ size_t i = 0, o = 0;
+ while (i + 3 <= n) {
+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) |
+ (uint32_t)in[i + 2];
+ out[o++] = B64_ENC[(v >> 18) & 0x3f];
+ out[o++] = B64_ENC[(v >> 12) & 0x3f];
+ out[o++] = B64_ENC[(v >> 6) & 0x3f];
+ out[o++] = B64_ENC[v & 0x3f];
+ i += 3;
+ }
+ if (n - i == 1) {
+ uint32_t v = (uint32_t)in[i] << 16;
+ out[o++] = B64_ENC[(v >> 18) & 0x3f];
+ out[o++] = B64_ENC[(v >> 12) & 0x3f];
+ out[o++] = '=';
+ out[o++] = '=';
+ } else if (n - i == 2) {
+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
+ out[o++] = B64_ENC[(v >> 18) & 0x3f];
+ out[o++] = B64_ENC[(v >> 12) & 0x3f];
+ out[o++] = B64_ENC[(v >> 6) & 0x3f];
+ out[o++] = '=';
+ }
+ out[o] = '\0';
+ return o;
+}
+
+/* Map a base64 character to its 6-bit value, or 64 for '=' / -1 on error. */
+static int b64_val(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;
+ if (c == '=') return 64;
+ return -1;
+}
+
+int dist_b64_decode(uint8_t* out, size_t* outlen, const char* in,
+ size_t inlen) {
+ size_t i = 0, o = 0;
+ if (inlen % 4u != 0u) return DIST_ERR;
+ while (i < inlen) {
+ int a = b64_val(in[i]);
+ int b = b64_val(in[i + 1]);
+ int c = b64_val(in[i + 2]);
+ int d = b64_val(in[i + 3]);
+ if (a < 0 || b < 0 || c < 0 || d < 0) return DIST_ERR;
+ if (a == 64 || b == 64) return DIST_ERR; /* pad only in last two slots */
+ out[o++] = (uint8_t)((a << 2) | (b >> 4));
+ if (c != 64) {
+ out[o++] = (uint8_t)((b << 4) | (c >> 2));
+ if (d != 64) out[o++] = (uint8_t)((c << 6) | d);
+ } else if (d != 64) {
+ return DIST_ERR; /* '=' followed by non-'=' */
+ }
+ i += 4;
+ }
+ *outlen = o;
+ return DIST_OK;
+}
diff --git a/driver/dist/b64.h b/driver/dist/b64.h
@@ -0,0 +1,23 @@
+#ifndef CFREE_DIST_B64_H
+#define CFREE_DIST_B64_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/* Standard base64 (RFC 4648, `+/` alphabet, `=` padding). Real, not a stub:
+ * minisign keys and signatures are base64 and round-tripping must be exact. */
+
+/* Bytes needed to hold the base64 encoding of `n` raw bytes, plus a NUL. */
+#define DIST_B64_ENCODED_CAP(n) ((((n) + 2u) / 3u) * 4u + 1u)
+
+/* Encode `n` bytes into `out` (must hold DIST_B64_ENCODED_CAP(n)). Writes a
+ * NUL terminator. Returns the number of base64 characters (excluding NUL). */
+size_t dist_b64_encode(char* out, const uint8_t* in, size_t n);
+
+/* Decode base64 text `in` of `inlen` chars into `out`, storing the decoded
+ * length in *outlen. `out` must hold at least (inlen/4)*3 bytes. Embedded
+ * whitespace is rejected (callers pass a single trimmed line). Returns DIST_OK
+ * or DIST_ERR. */
+int dist_b64_decode(uint8_t* out, size_t* outlen, const char* in, size_t inlen);
+
+#endif
diff --git a/driver/dist/blake2b.c b/driver/dist/blake2b.c
@@ -0,0 +1,29 @@
+#include "blake2b.h"
+
+/* STUB digest. See blake2b.h. Same construction as the sha256 stub with a
+ * different domain constant, producing a 64-byte output. */
+
+#define DIST_B2_FNV_OFFSET 0x100000001b3cbf29ULL
+#define DIST_B2_FNV_PRIME 0x00000100000001b3ULL
+
+static uint64_t b2_absorb(const uint8_t* data, size_t len, uint64_t h) {
+ size_t i;
+ for (i = 0; i < len; ++i) {
+ h ^= data[i];
+ h *= DIST_B2_FNV_PRIME;
+ h ^= h >> 31;
+ }
+ return h;
+}
+
+void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data,
+ size_t len) {
+ uint64_t base = b2_absorb(data, len, DIST_B2_FNV_OFFSET);
+ size_t i;
+ for (i = 0; i < DIST_BLAKE2B_LEN; ++i) {
+ uint64_t h = base ^ (0xc2b2ae3d27d4eb4fULL * (uint64_t)(i + 1));
+ h *= DIST_B2_FNV_PRIME;
+ h ^= h >> 27;
+ out[i] = (uint8_t)(h >> 32);
+ }
+}
diff --git a/driver/dist/blake2b.h b/driver/dist/blake2b.h
@@ -0,0 +1,17 @@
+#ifndef CFREE_DIST_BLAKE2B_H
+#define CFREE_DIST_BLAKE2B_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* minisign prehash. Real minisign hashes the signed file with BLAKE2b-512 and
+ * signs the 64-byte digest.
+ *
+ * STUB: deterministic non-cryptographic 64-byte digest. Not BLAKE2b. Replace
+ * with the real implementation when vendoring crypto. */
+void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data,
+ size_t len);
+
+#endif
diff --git a/driver/dist/deflate.c b/driver/dist/deflate.c
@@ -0,0 +1,123 @@
+#include "deflate.h"
+
+#include <string.h>
+
+#define GZ_MAGIC0 0x1fu
+#define GZ_MAGIC1 0x8bu
+#define GZ_METHOD_DEFLATE 0x08u
+#define GZ_OS_UNKNOWN 0xffu
+#define GZ_HEADER_LEN 10u
+#define GZ_TRAILER_LEN 8u
+#define DEFLATE_STORED_MAX 0xffffu /* max stored-block payload */
+
+static uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t len) {
+ size_t i;
+ unsigned k;
+ crc = ~crc;
+ for (i = 0; i < len; ++i) {
+ crc ^= data[i];
+ for (k = 0; k < 8u; ++k) {
+ uint32_t mask = (uint32_t)0 - (crc & 1u);
+ crc = (crc >> 1) ^ (0xedb88320u & mask);
+ }
+ }
+ return ~crc;
+}
+
+static int gz_write(CfreeWriter* out, const void* data, size_t n) {
+ return cfree_writer_write(out, data, n) == CFREE_OK ? DIST_OK : DIST_ERR;
+}
+
+static void put_u16le(uint8_t* p, uint16_t v) {
+ p[0] = (uint8_t)(v & 0xffu);
+ p[1] = (uint8_t)((v >> 8) & 0xffu);
+}
+
+static void put_u32le(uint8_t* p, uint32_t v) {
+ p[0] = (uint8_t)(v & 0xffu);
+ p[1] = (uint8_t)((v >> 8) & 0xffu);
+ p[2] = (uint8_t)((v >> 16) & 0xffu);
+ p[3] = (uint8_t)((v >> 24) & 0xffu);
+}
+
+static uint16_t get_u16le(const uint8_t* p) {
+ return (uint16_t)(p[0] | (p[1] << 8));
+}
+
+static uint32_t get_u32le(const uint8_t* p) {
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) |
+ ((uint32_t)p[3] << 24);
+}
+
+int dist_gz_compress(CfreeWriter* out, const uint8_t* data, size_t len) {
+ uint8_t hdr[GZ_HEADER_LEN];
+ uint8_t blk[5];
+ uint8_t trailer[GZ_TRAILER_LEN];
+ size_t off = 0;
+
+ memset(hdr, 0, sizeof hdr);
+ hdr[0] = GZ_MAGIC0;
+ hdr[1] = GZ_MAGIC1;
+ hdr[2] = GZ_METHOD_DEFLATE;
+ /* hdr[3] flags = 0; hdr[4..7] mtime = 0 (reproducible); hdr[8] XFL = 0 */
+ hdr[9] = GZ_OS_UNKNOWN;
+ if (gz_write(out, hdr, sizeof hdr) != DIST_OK) return DIST_ERR;
+
+ /* One or more stored DEFLATE blocks. Each: header byte (BFINAL in bit 0,
+ * BTYPE=00 in bits 1-2), then LEN and ~LEN little-endian, then raw bytes. */
+ do {
+ size_t chunk = len - off;
+ int last;
+ if (chunk > DEFLATE_STORED_MAX) chunk = DEFLATE_STORED_MAX;
+ last = (off + chunk == len);
+ blk[0] = last ? 0x01u : 0x00u;
+ put_u16le(blk + 1, (uint16_t)chunk);
+ put_u16le(blk + 3, (uint16_t)(~(uint16_t)chunk));
+ if (gz_write(out, blk, sizeof blk) != DIST_OK) return DIST_ERR;
+ if (chunk && gz_write(out, data + off, chunk) != DIST_OK) return DIST_ERR;
+ off += chunk;
+ } while (off < len);
+
+ put_u32le(trailer, crc32_update(0, data, len));
+ put_u32le(trailer + 4, (uint32_t)len);
+ return gz_write(out, trailer, sizeof trailer);
+}
+
+int dist_gz_decompress(CfreeWriter* out, const uint8_t* data, size_t len) {
+ size_t off = GZ_HEADER_LEN;
+ uint32_t crc = 0;
+ uint32_t total = 0;
+ int done = 0;
+
+ if (len < GZ_HEADER_LEN + GZ_TRAILER_LEN) return DIST_ERR;
+ if (data[0] != GZ_MAGIC0 || data[1] != GZ_MAGIC1 ||
+ data[2] != GZ_METHOD_DEFLATE) {
+ return DIST_ERR;
+ }
+ if (data[3] != 0u) return DIST_ERR; /* stub emits no header flags */
+
+ while (!done) {
+ uint8_t bhdr;
+ uint16_t blen, bnlen;
+ if (off + 5u > len) return DIST_ERR;
+ bhdr = data[off];
+ if (((bhdr >> 1) & 0x3u) != 0u) return DIST_ERR; /* non-stored block */
+ done = (bhdr & 0x1u) != 0u;
+ blen = get_u16le(data + off + 1);
+ bnlen = get_u16le(data + off + 3);
+ if ((uint16_t)~blen != bnlen) return DIST_ERR;
+ off += 5u;
+ if (off + blen > len) return DIST_ERR;
+ if (blen) {
+ if (gz_write(out, data + off, blen) != DIST_OK) return DIST_ERR;
+ crc = crc32_update(crc, data + off, blen);
+ total += blen;
+ off += blen;
+ }
+ }
+
+ if (off + GZ_TRAILER_LEN > len) return DIST_ERR;
+ if (get_u32le(data + off) != crc) return DIST_ERR;
+ if (get_u32le(data + off + 4) != total) return DIST_ERR;
+ return DIST_OK;
+}
diff --git a/driver/dist/deflate.h b/driver/dist/deflate.h
@@ -0,0 +1,25 @@
+#ifndef CFREE_DIST_DEFLATE_H
+#define CFREE_DIST_DEFLATE_H
+
+#include <cfree/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* gzip wrap/unwrap for the payload archive.
+ *
+ * The gzip container, CRC32, and DEFLATE *stored* blocks are real: the output
+ * is a valid `.gz` that stock `gunzip` can read. What is STUBBED is actual
+ * compression — `dist_gz_compress` emits stored (uncompressed) blocks, and
+ * `dist_gz_decompress` only understands stored blocks (it errors on a
+ * compressed block). Real LZ77 + Huffman is the work to vendor later. */
+
+/* Wrap `data` in a gzip stream of stored blocks, writing to `out`. */
+int dist_gz_compress(CfreeWriter* out, const uint8_t* data, size_t len);
+
+/* Unwrap a gzip stream into `out`. Returns DIST_ERR on a malformed stream, a
+ * CRC/size mismatch, or a non-stored DEFLATE block. */
+int dist_gz_decompress(CfreeWriter* out, const uint8_t* data, size_t len);
+
+#endif
diff --git a/driver/dist/dist.c b/driver/dist/dist.c
@@ -0,0 +1,41 @@
+#include "dist.h"
+
+static char dist_hex_digit(unsigned v) {
+ return (char)(v < 10u ? '0' + v : 'a' + (v - 10u));
+}
+
+void dist_hex_encode(char* out, const uint8_t* in, size_t n) {
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ out[2 * i] = dist_hex_digit((unsigned)(in[i] >> 4));
+ out[2 * i + 1] = dist_hex_digit((unsigned)(in[i] & 0xfu));
+ }
+ out[2 * n] = '\0';
+}
+
+static int dist_hex_val(char c, unsigned* out) {
+ if (c >= '0' && c <= '9') {
+ *out = (unsigned)(c - '0');
+ return DIST_OK;
+ }
+ if (c >= 'a' && c <= 'f') {
+ *out = (unsigned)(c - 'a') + 10u;
+ return DIST_OK;
+ }
+ if (c >= 'A' && c <= 'F') {
+ *out = (unsigned)(c - 'A') + 10u;
+ return DIST_OK;
+ }
+ return DIST_ERR;
+}
+
+int dist_hex_decode(uint8_t* out, const char* in, size_t n) {
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ unsigned hi, lo;
+ if (dist_hex_val(in[2 * i], &hi) != DIST_OK) return DIST_ERR;
+ if (dist_hex_val(in[2 * i + 1], &lo) != DIST_OK) return DIST_ERR;
+ out[i] = (uint8_t)((hi << 4) | lo);
+ }
+ return DIST_OK;
+}
diff --git a/driver/dist/dist.h b/driver/dist/dist.h
@@ -0,0 +1,53 @@
+#ifndef CFREE_DIST_DIST_H
+#define CFREE_DIST_DIST_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/* Shared constants and small helpers for the code-distribution subsystem
+ * (`cfree pkg`). See doc/DISTRIBUTE.md for the design.
+ *
+ * The crypto/compression primitives under driver/dist/ are STUBS during
+ * bootstrap: deterministic, functional, and INSECURE. They exist so the
+ * end-to-end packaging pipeline (manifest -> sign -> bundle -> verify ->
+ * unpack) can be exercised before the real vendored implementations land.
+ * Every stub is marked as such at its definition. */
+
+/* Primitive output sizes. */
+#define DIST_SHA256_LEN 32u
+#define DIST_BLAKE2B_LEN 64u
+#define DIST_ED25519_PK_LEN 32u
+#define DIST_ED25519_SK_LEN 64u
+#define DIST_ED25519_SIG_LEN 64u
+#define DIST_ED25519_SEED_LEN 32u
+#define DIST_KEYID_LEN 8u
+
+/* Fixed parse/build capacities. The stub phase avoids dynamic arrays; these
+ * caps bound a single package's metadata and member count. */
+#define DIST_MAX_ARTIFACTS 64u
+#define DIST_MAX_DEPS 64u
+#define DIST_MAX_FILES 256u
+
+/* String field caps inside in-memory manifest structs. */
+#define DIST_NAME_MAX 128u
+#define DIST_VERSION_MAX 64u
+#define DIST_DESC_MAX 256u
+#define DIST_PATH_MAX 100u /* matches ustar name field */
+#define DIST_TRIPLE_MAX 64u
+#define DIST_KIND_MAX 16u
+#define DIST_PCONSTRAINT_MAX 64u
+
+/* Result convention: 0 = ok, non-zero = error. */
+#define DIST_OK 0
+#define DIST_ERR 1
+
+/* Lowercase-hex encode `n` bytes into `out`, which must hold 2*n+1 chars
+ * (NUL-terminated). */
+void dist_hex_encode(char* out, const uint8_t* in, size_t n);
+
+/* Decode exactly `n` bytes of lowercase/uppercase hex from `in` (which must
+ * have >= 2*n hex chars) into `out`. Returns DIST_OK on success, DIST_ERR on a
+ * non-hex character. */
+int dist_hex_decode(uint8_t* out, const char* in, size_t n);
+
+#endif
diff --git a/driver/dist/ed25519.c b/driver/dist/ed25519.c
@@ -0,0 +1,46 @@
+#include "ed25519.h"
+
+#include <string.h>
+
+#include "sha256.h"
+
+/* STUB scheme. See ed25519.h. pk == seed; signatures are derived purely from
+ * the seed and message via the stub hash, and verification recomputes them
+ * from pk (== seed). Insecure by construction; deterministic and
+ * round-tripping for pipeline testing. */
+
+void dist_ed25519_keypair(uint8_t pk[DIST_ED25519_PK_LEN],
+ uint8_t sk[DIST_ED25519_SK_LEN],
+ const uint8_t seed[DIST_ED25519_SEED_LEN]) {
+ memcpy(pk, seed, DIST_ED25519_SEED_LEN);
+ memcpy(sk, seed, DIST_ED25519_SEED_LEN);
+ memcpy(sk + DIST_ED25519_SEED_LEN, pk, DIST_ED25519_PK_LEN);
+}
+
+/* Derive a signature from a 32-byte seed and a message digest. */
+static void stub_sign_with_seed(uint8_t sig[DIST_ED25519_SIG_LEN],
+ const uint8_t seed[DIST_ED25519_SEED_LEN],
+ const uint8_t* msg, size_t msglen) {
+ uint8_t m[DIST_SHA256_LEN];
+ uint8_t buf[DIST_ED25519_SEED_LEN + DIST_SHA256_LEN];
+ dist_sha256(m, msg, msglen);
+ memcpy(buf, seed, DIST_ED25519_SEED_LEN);
+ memcpy(buf + DIST_ED25519_SEED_LEN, m, DIST_SHA256_LEN);
+ dist_sha256(sig, buf, sizeof buf); /* sig[0:32] */
+ memcpy(buf, sig, DIST_SHA256_LEN);
+ memcpy(buf + DIST_SHA256_LEN, seed, DIST_ED25519_SEED_LEN);
+ dist_sha256(sig + DIST_SHA256_LEN, buf, sizeof buf); /* sig[32:64] */
+}
+
+void dist_ed25519_sign(uint8_t sig[DIST_ED25519_SIG_LEN], const uint8_t* msg,
+ size_t msglen, const uint8_t sk[DIST_ED25519_SK_LEN]) {
+ stub_sign_with_seed(sig, sk, msg, msglen); /* seed = sk[0:32] */
+}
+
+int dist_ed25519_verify(const uint8_t sig[DIST_ED25519_SIG_LEN],
+ const uint8_t* msg, size_t msglen,
+ const uint8_t pk[DIST_ED25519_PK_LEN]) {
+ uint8_t expect[DIST_ED25519_SIG_LEN];
+ stub_sign_with_seed(expect, pk, msg, msglen); /* pk == seed in the stub */
+ return memcmp(expect, sig, DIST_ED25519_SIG_LEN) == 0 ? 1 : 0;
+}
diff --git a/driver/dist/ed25519.h b/driver/dist/ed25519.h
@@ -0,0 +1,31 @@
+#ifndef CFREE_DIST_ED25519_H
+#define CFREE_DIST_ED25519_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* Signature scheme used by minisign.
+ *
+ * STUB: this is NOT Ed25519 and provides NO security. The "public key" is the
+ * seed itself, so anyone holding the public key can forge signatures. It is a
+ * deterministic placeholder that verifies correctly end-to-end so the
+ * packaging/trust pipeline can be exercised. Replace with real Ed25519
+ * (and a real CSPRNG for keygen) before trusting any of this. */
+
+/* Derive a keypair from a 32-byte seed. */
+void dist_ed25519_keypair(uint8_t pk[DIST_ED25519_PK_LEN],
+ uint8_t sk[DIST_ED25519_SK_LEN],
+ const uint8_t seed[DIST_ED25519_SEED_LEN]);
+
+/* Sign `msg` with `sk`, writing a 64-byte signature. */
+void dist_ed25519_sign(uint8_t sig[DIST_ED25519_SIG_LEN], const uint8_t* msg,
+ size_t msglen, const uint8_t sk[DIST_ED25519_SK_LEN]);
+
+/* Verify `sig` over `msg` against `pk`. Returns 1 if valid, 0 otherwise. */
+int dist_ed25519_verify(const uint8_t sig[DIST_ED25519_SIG_LEN],
+ const uint8_t* msg, size_t msglen,
+ const uint8_t pk[DIST_ED25519_PK_LEN]);
+
+#endif
diff --git a/driver/dist/manifest.c b/driver/dist/manifest.c
@@ -0,0 +1,305 @@
+#include "manifest.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define DIST_LINE_MAX 1024u
+
+/* Required-field bits, reused per block. */
+#define F_NAME 0x01u
+#define F_VERSION 0x02u
+#define F_ARCHIVE 0x04u
+#define F_ARCH_SHA 0x08u
+#define F_ARCH_SIZE 0x10u
+#define F_PATH 0x20u
+#define F_KIND 0x40u
+#define F_SHA 0x80u
+#define F_SIZE 0x100u
+
+typedef enum { SEC_TOP, SEC_ART, SEC_DEP } Section;
+
+/* ---------------------------------------------------------------- emit --- */
+
+static int emit(CfreeWriter* out, const char* s) {
+ return cfree_writer_write(out, s, strlen(s)) == CFREE_OK ? DIST_OK : DIST_ERR;
+}
+
+static int emit_kv(CfreeWriter* out, const char* key, const char* val) {
+ char line[DIST_LINE_MAX];
+ snprintf(line, sizeof line, "%s = %s\n", key, val);
+ return emit(out, line);
+}
+
+static int emit_hex(CfreeWriter* out, const char* key, const uint8_t* h,
+ size_t n) {
+ char hex[2 * DIST_SHA256_LEN + 1];
+ dist_hex_encode(hex, h, n);
+ return emit_kv(out, key, hex);
+}
+
+static int emit_u64(CfreeWriter* out, const char* key, uint64_t v) {
+ char num[24];
+ snprintf(num, sizeof num, "%llu", (unsigned long long)v);
+ return emit_kv(out, key, num);
+}
+
+int dist_manifest_emit(const DistManifest* m, CfreeWriter* out) {
+ size_t i;
+ if (emit(out, DIST_MANIFEST_MAGIC "\n") != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "name", m->name) != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "version", m->version) != DIST_OK) return DIST_ERR;
+ if (m->description[0] &&
+ emit_kv(out, "description", m->description) != DIST_OK) {
+ return DIST_ERR;
+ }
+ if (emit_kv(out, "archive", m->archive) != DIST_OK) return DIST_ERR;
+ if (emit_hex(out, "archive-sha256", m->archive_sha256, DIST_SHA256_LEN) !=
+ DIST_OK) {
+ return DIST_ERR;
+ }
+ if (emit_u64(out, "archive-size", m->archive_size) != DIST_OK)
+ return DIST_ERR;
+
+ for (i = 0; i < m->n_artifacts; ++i) {
+ const DistArtifact* a = &m->artifacts[i];
+ if (emit(out, "\n[artifact]\n") != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "path", a->path) != DIST_OK) return DIST_ERR;
+ if (a->target[0] && emit_kv(out, "target", a->target) != DIST_OK)
+ return DIST_ERR;
+ if (emit_kv(out, "kind", a->kind) != DIST_OK) return DIST_ERR;
+ if (emit_hex(out, "sha256", a->sha256, DIST_SHA256_LEN) != DIST_OK)
+ return DIST_ERR;
+ if (emit_u64(out, "size", a->size) != DIST_OK) return DIST_ERR;
+ if (a->entry && emit_kv(out, "entry", "true") != DIST_OK) return DIST_ERR;
+ }
+
+ for (i = 0; i < m->n_deps; ++i) {
+ const DistDependency* d = &m->deps[i];
+ if (emit(out, "\n[dependency]\n") != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "name", d->name) != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "version", d->version) != DIST_OK) return DIST_ERR;
+ if (d->has_sha256 &&
+ emit_hex(out, "sha256", d->sha256, DIST_SHA256_LEN) != DIST_OK) {
+ return DIST_ERR;
+ }
+ if (d->has_keyid &&
+ emit_hex(out, "key", d->keyid, DIST_KEYID_LEN) != DIST_OK) {
+ return DIST_ERR;
+ }
+ }
+ return DIST_OK;
+}
+
+/* --------------------------------------------------------------- parse --- */
+
+static char* trim_lead(char* s) {
+ while (*s == ' ' || *s == '\t') ++s;
+ return s;
+}
+
+static void trim_trail(char* s) {
+ size_t n = strlen(s);
+ while (n && (s[n - 1] == ' ' || s[n - 1] == '\t' || s[n - 1] == '\r' ||
+ s[n - 1] == '\n')) {
+ s[--n] = '\0';
+ }
+}
+
+static int set_err(char* err, size_t cap, const char* msg) {
+ if (err && cap) snprintf(err, cap, "%s", msg);
+ return DIST_ERR;
+}
+
+static int copy_field(char* dst, size_t cap, const char* src, char* err,
+ size_t errcap) {
+ if (strlen(src) >= cap) return set_err(err, errcap, "field value too long");
+ snprintf(dst, cap, "%s", src);
+ return DIST_OK;
+}
+
+static int kind_valid(const char* k) {
+ return strcmp(k, "exe") == 0 || strcmp(k, "dso") == 0 ||
+ strcmp(k, "obj") == 0 || strcmp(k, "wasm") == 0 ||
+ strcmp(k, "lib") == 0 || strcmp(k, "data") == 0 ||
+ strcmp(k, "source") == 0;
+}
+
+static int parse_u64(const char* s, uint64_t* out) {
+ char* end = NULL;
+ unsigned long long v;
+ if (!*s) return DIST_ERR;
+ v = strtoull(s, &end, 10);
+ if (!end || *end != '\0') return DIST_ERR;
+ *out = (uint64_t)v;
+ return DIST_OK;
+}
+
+/* Validate the just-completed block against its required-field set. */
+static int finalize(Section sec, uint32_t seen, char* err, size_t errcap) {
+ if (sec == SEC_TOP) {
+ if ((seen & (F_NAME | F_VERSION | F_ARCHIVE | F_ARCH_SHA | F_ARCH_SIZE)) !=
+ (F_NAME | F_VERSION | F_ARCHIVE | F_ARCH_SHA | F_ARCH_SIZE)) {
+ return set_err(err, errcap, "missing required top-level field");
+ }
+ } else if (sec == SEC_ART) {
+ if ((seen & (F_PATH | F_KIND | F_SHA | F_SIZE)) !=
+ (F_PATH | F_KIND | F_SHA | F_SIZE)) {
+ return set_err(err, errcap, "missing required [artifact] field");
+ }
+ } else { /* SEC_DEP */
+ if ((seen & (F_NAME | F_VERSION)) != (F_NAME | F_VERSION)) {
+ return set_err(err, errcap, "missing required [dependency] field");
+ }
+ }
+ return DIST_OK;
+}
+
+int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
+ char* err, size_t errcap) {
+ size_t pos = 0;
+ int first = 1;
+ Section sec = SEC_TOP;
+ uint32_t seen = 0;
+ DistArtifact* art = NULL;
+ DistDependency* dep = NULL;
+
+ memset(m, 0, sizeof *m);
+
+ while (pos < len) {
+ char buf[DIST_LINE_MAX];
+ size_t end = pos;
+ size_t n;
+ char *t, *key, *val, *eq;
+
+ while (end < len && data[end] != '\n') ++end;
+ n = end - pos;
+ if (n >= sizeof buf) return set_err(err, errcap, "line too long");
+ memcpy(buf, data + pos, n);
+ buf[n] = '\0';
+ pos = (end < len) ? end + 1 : end;
+
+ trim_trail(buf);
+
+ if (first) {
+ first = 0;
+ if (strcmp(buf, DIST_MANIFEST_MAGIC) != 0) {
+ return set_err(err, errcap, "bad manifest magic/version");
+ }
+ continue;
+ }
+
+ t = trim_lead(buf);
+ if (*t == '\0' || *t == '#') continue; /* blank / comment */
+
+ if (*t == '[') {
+ if (finalize(sec, seen, err, errcap) != DIST_OK) return DIST_ERR;
+ seen = 0;
+ if (strcmp(t, "[artifact]") == 0) {
+ if (m->n_artifacts >= DIST_MAX_ARTIFACTS)
+ return set_err(err, errcap, "too many artifacts");
+ sec = SEC_ART;
+ art = &m->artifacts[m->n_artifacts++];
+ } else if (strcmp(t, "[dependency]") == 0) {
+ if (m->n_deps >= DIST_MAX_DEPS)
+ return set_err(err, errcap, "too many dependencies");
+ sec = SEC_DEP;
+ dep = &m->deps[m->n_deps++];
+ } else {
+ return set_err(err, errcap, "unknown section");
+ }
+ continue;
+ }
+
+ eq = strchr(t, '=');
+ if (!eq) return set_err(err, errcap, "expected key = value");
+ *eq = '\0';
+ key = t;
+ trim_trail(key);
+ val = trim_lead(eq + 1);
+
+ if (sec == SEC_TOP) {
+ if (strcmp(key, "name") == 0) {
+ if (copy_field(m->name, sizeof m->name, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_NAME;
+ } else if (strcmp(key, "version") == 0) {
+ if (copy_field(m->version, sizeof m->version, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_VERSION;
+ } else if (strcmp(key, "description") == 0) {
+ if (copy_field(m->description, sizeof m->description, val, err, errcap))
+ return DIST_ERR;
+ } else if (strcmp(key, "archive") == 0) {
+ if (copy_field(m->archive, sizeof m->archive, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_ARCHIVE;
+ } else if (strcmp(key, "archive-sha256") == 0) {
+ if (strlen(val) != 2 * DIST_SHA256_LEN ||
+ dist_hex_decode(m->archive_sha256, val, DIST_SHA256_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad archive-sha256");
+ seen |= F_ARCH_SHA;
+ } else if (strcmp(key, "archive-size") == 0) {
+ if (parse_u64(val, &m->archive_size) != DIST_OK)
+ return set_err(err, errcap, "bad archive-size");
+ seen |= F_ARCH_SIZE;
+ } else {
+ return set_err(err, errcap, "unknown top-level key");
+ }
+ } else if (sec == SEC_ART) {
+ if (strcmp(key, "path") == 0) {
+ if (copy_field(art->path, sizeof art->path, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_PATH;
+ } else if (strcmp(key, "target") == 0) {
+ if (copy_field(art->target, sizeof art->target, val, err, errcap))
+ return DIST_ERR;
+ } else if (strcmp(key, "kind") == 0) {
+ if (!kind_valid(val))
+ return set_err(err, errcap, "unknown artifact kind");
+ if (copy_field(art->kind, sizeof art->kind, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_KIND;
+ } else if (strcmp(key, "sha256") == 0) {
+ if (strlen(val) != 2 * DIST_SHA256_LEN ||
+ dist_hex_decode(art->sha256, val, DIST_SHA256_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad artifact sha256");
+ seen |= F_SHA;
+ } else if (strcmp(key, "size") == 0) {
+ if (parse_u64(val, &art->size) != DIST_OK)
+ return set_err(err, errcap, "bad artifact size");
+ seen |= F_SIZE;
+ } else if (strcmp(key, "entry") == 0) {
+ art->entry = (strcmp(val, "true") == 0);
+ if (!art->entry && strcmp(val, "false") != 0)
+ return set_err(err, errcap, "bad entry value");
+ } else {
+ return set_err(err, errcap, "unknown [artifact] key");
+ }
+ } else { /* SEC_DEP */
+ if (strcmp(key, "name") == 0) {
+ if (copy_field(dep->name, sizeof dep->name, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_NAME;
+ } else if (strcmp(key, "version") == 0) {
+ if (copy_field(dep->version, sizeof dep->version, val, err, errcap))
+ return DIST_ERR;
+ seen |= F_VERSION;
+ } else if (strcmp(key, "sha256") == 0) {
+ if (strlen(val) != 2 * DIST_SHA256_LEN ||
+ dist_hex_decode(dep->sha256, val, DIST_SHA256_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad dependency sha256");
+ dep->has_sha256 = 1;
+ } else if (strcmp(key, "key") == 0) {
+ if (strlen(val) != 2 * DIST_KEYID_LEN ||
+ dist_hex_decode(dep->keyid, val, DIST_KEYID_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad dependency key id");
+ dep->has_keyid = 1;
+ } else {
+ return set_err(err, errcap, "unknown [dependency] key");
+ }
+ }
+ }
+
+ return finalize(sec, seen, err, errcap);
+}
diff --git a/driver/dist/manifest.h b/driver/dist/manifest.h
@@ -0,0 +1,59 @@
+#ifndef CFREE_DIST_MANIFEST_H
+#define CFREE_DIST_MANIFEST_H
+
+#include <cfree/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* The signed root object. INI-style, line-oriented, byte-stable. See
+ * doc/DISTRIBUTE.md. Emit and parse are real (no stub): the parser reads but
+ * never reserializes, so the signed surface is always the literal bytes on
+ * disk. There is deliberately no `created`/timestamp field — the manifest
+ * stays reproducible; signing time lives in the signature's trusted comment. */
+
+#define DIST_MANIFEST_MAGIC "cfree-manifest 1"
+
+typedef struct DistArtifact {
+ char path[DIST_PATH_MAX + 1];
+ char target[DIST_TRIPLE_MAX]; /* "" = target-independent */
+ char kind[DIST_KIND_MAX];
+ uint8_t sha256[DIST_SHA256_LEN];
+ uint64_t size;
+ int entry;
+} DistArtifact;
+
+typedef struct DistDependency {
+ char name[DIST_NAME_MAX];
+ char version[DIST_PCONSTRAINT_MAX];
+ uint8_t sha256[DIST_SHA256_LEN];
+ int has_sha256;
+ uint8_t keyid[DIST_KEYID_LEN];
+ int has_keyid;
+} DistDependency;
+
+typedef struct DistManifest {
+ char name[DIST_NAME_MAX];
+ char version[DIST_VERSION_MAX];
+ char description[DIST_DESC_MAX]; /* "" = absent */
+ char archive[DIST_PATH_MAX + 1];
+ uint8_t archive_sha256[DIST_SHA256_LEN];
+ uint64_t archive_size;
+ DistArtifact artifacts[DIST_MAX_ARTIFACTS];
+ size_t n_artifacts;
+ DistDependency deps[DIST_MAX_DEPS];
+ size_t n_deps;
+} DistManifest;
+
+/* Serialize `m` as INI text to `out`. Returns DIST_OK / DIST_ERR. */
+int dist_manifest_emit(const DistManifest* m, CfreeWriter* out);
+
+/* Strictly parse `data`/`len` into `m`. On error returns DIST_ERR and writes
+ * a human-readable reason into `err` (capacity `errcap`). Rejects an unknown
+ * magic/version, unknown keys, malformed values, and missing required
+ * fields. */
+int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
+ char* err, size_t errcap);
+
+#endif
diff --git a/driver/dist/minisig.c b/driver/dist/minisig.c
@@ -0,0 +1,265 @@
+#include "minisig.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "b64.h"
+#include "blake2b.h"
+#include "ed25519.h"
+
+#define U_PREFIX "untrusted comment: "
+#define T_PREFIX "trusted comment: "
+
+/* minisign algorithm tags. The key's sig_alg is "Ed"; signatures we emit are
+ * prehashed, so their tag is "ED". */
+#define MS_SIGALG "Ed"
+#define MS_SIGALG_HASHED "ED"
+#define MS_KDFALG_SCRYPT "Sc"
+#define MS_CHKALG "B2"
+#define MS_ALG_LEN 2u
+#define MS_SALT_LEN 32u
+#define MS_LIMIT_LEN 8u
+#define MS_CHK_LEN 32u /* BLAKE2b-256 checksum (first 32 of the stub's 64) */
+
+/* Public-key payload: sig_alg || keyid || pk. */
+#define KEY_BLOB_LEN (MS_ALG_LEN + DIST_KEYID_LEN + DIST_ED25519_PK_LEN)
+/* Signature line-1 payload: sig_alg || keyid || sig. */
+#define SIG_LINE1_LEN (MS_ALG_LEN + DIST_KEYID_LEN + DIST_ED25519_SIG_LEN)
+/* Secret-key payload (passwordless layout). */
+#define SK_KDF_OFF MS_ALG_LEN
+#define SK_CHKALG_OFF (SK_KDF_OFF + MS_ALG_LEN)
+#define SK_SALT_OFF (SK_CHKALG_OFF + MS_ALG_LEN)
+#define SK_OPS_OFF (SK_SALT_OFF + MS_SALT_LEN)
+#define SK_MEM_OFF (SK_OPS_OFF + MS_LIMIT_LEN)
+#define SK_KEYID_OFF (SK_MEM_OFF + MS_LIMIT_LEN)
+#define SK_SK_OFF (SK_KEYID_OFF + DIST_KEYID_LEN)
+#define SK_CHK_OFF (SK_SK_OFF + DIST_ED25519_SK_LEN)
+#define SKEY_BLOB_LEN (SK_CHK_OFF + MS_CHK_LEN)
+#define SIG_LINE_MAX 1024u
+#define B64_SCRATCH 320u
+
+void dist_minisig_keygen(DistKeypair* out,
+ const uint8_t seed[DIST_ED25519_SEED_LEN],
+ const uint8_t keyid[DIST_KEYID_LEN]) {
+ dist_ed25519_keypair(out->pk, out->sk, seed);
+ memcpy(out->keyid, keyid, DIST_KEYID_LEN);
+}
+
+/* The minisign secret-key checksum: BLAKE2b-256 over sig_alg || keyid || sk. */
+static void seckey_checksum(uint8_t out[MS_CHK_LEN],
+ const uint8_t keyid[DIST_KEYID_LEN],
+ const uint8_t sk[DIST_ED25519_SK_LEN]) {
+ uint8_t buf[MS_ALG_LEN + DIST_KEYID_LEN + DIST_ED25519_SK_LEN];
+ uint8_t full[DIST_BLAKE2B_LEN];
+ memcpy(buf, MS_SIGALG, MS_ALG_LEN);
+ memcpy(buf + MS_ALG_LEN, keyid, DIST_KEYID_LEN);
+ memcpy(buf + MS_ALG_LEN + DIST_KEYID_LEN, sk, DIST_ED25519_SK_LEN);
+ dist_blake2b(full, buf, sizeof buf);
+ memcpy(out, full, MS_CHK_LEN);
+}
+
+static int emit_str(CfreeWriter* out, const char* s) {
+ return cfree_writer_write(out, s, strlen(s)) == CFREE_OK ? DIST_OK : DIST_ERR;
+}
+
+static int emit_b64_line(CfreeWriter* out, const uint8_t* blob, size_t n) {
+ char b64[B64_SCRATCH];
+ dist_b64_encode(b64, blob, n);
+ if (emit_str(out, b64) != DIST_OK) return DIST_ERR;
+ return emit_str(out, "\n");
+}
+
+int dist_minisig_emit_pubkey(CfreeWriter* out, const DistKeypair* kp) {
+ char line[SIG_LINE_MAX];
+ char hex[2 * DIST_KEYID_LEN + 1];
+ uint8_t blob[KEY_BLOB_LEN];
+ dist_hex_encode(hex, kp->keyid, DIST_KEYID_LEN);
+ snprintf(line, sizeof line, U_PREFIX "cfree public key %s\n", hex);
+ if (emit_str(out, line) != DIST_OK) return DIST_ERR;
+ memcpy(blob, MS_SIGALG, MS_ALG_LEN);
+ memcpy(blob + MS_ALG_LEN, kp->keyid, DIST_KEYID_LEN);
+ memcpy(blob + MS_ALG_LEN + DIST_KEYID_LEN, kp->pk, DIST_ED25519_PK_LEN);
+ return emit_b64_line(out, blob, sizeof blob);
+}
+
+int dist_minisig_emit_seckey(CfreeWriter* out, const DistKeypair* kp) {
+ char line[SIG_LINE_MAX];
+ char hex[2 * DIST_KEYID_LEN + 1];
+ uint8_t blob[SKEY_BLOB_LEN];
+
+ dist_hex_encode(hex, kp->keyid, DIST_KEYID_LEN);
+ snprintf(line, sizeof line, U_PREFIX "cfree secret key %s\n", hex);
+ if (emit_str(out, line) != DIST_OK) return DIST_ERR;
+
+ memset(blob, 0, sizeof blob);
+ memcpy(blob, MS_SIGALG, MS_ALG_LEN);
+ /* kdf_alg [SK_KDF_OFF] left zero: passwordless, no scrypt. salt/opslimit/
+ * memlimit are unused when kdf_alg is zero, so they stay zero. */
+ memcpy(blob + SK_CHKALG_OFF, MS_CHKALG, MS_ALG_LEN);
+ memcpy(blob + SK_KEYID_OFF, kp->keyid, DIST_KEYID_LEN);
+ memcpy(blob + SK_SK_OFF, kp->sk, DIST_ED25519_SK_LEN);
+ seckey_checksum(blob + SK_CHK_OFF, kp->keyid, kp->sk);
+ return emit_b64_line(out, blob, sizeof blob);
+}
+
+/* Copy the idx-th line (0-based) into `out`, trimmed of trailing CR/LF. */
+static int sig_line(const uint8_t* data, size_t len, size_t idx, char* out,
+ size_t cap) {
+ size_t pos = 0, cur = 0;
+ while (pos < len) {
+ size_t end = pos;
+ while (end < len && data[end] != '\n') ++end;
+ if (cur == idx) {
+ size_t n = end - pos;
+ if (n >= cap) return DIST_ERR;
+ memcpy(out, data + pos, n);
+ out[n] = '\0';
+ while (n && (out[n - 1] == '\r')) out[--n] = '\0';
+ return DIST_OK;
+ }
+ pos = (end < len) ? end + 1 : end;
+ ++cur;
+ }
+ return DIST_ERR;
+}
+
+/* Decode the base64 on line `idx` into `out`, requiring exactly `want` bytes.
+ */
+static int decode_line(const uint8_t* data, size_t len, size_t idx,
+ uint8_t* out, size_t want) {
+ char line[SIG_LINE_MAX];
+ size_t got = 0;
+ if (sig_line(data, len, idx, line, sizeof line) != DIST_OK) return DIST_ERR;
+ if (dist_b64_decode(out, &got, line, strlen(line)) != DIST_OK)
+ return DIST_ERR;
+ return got == want ? DIST_OK : DIST_ERR;
+}
+
+int dist_minisig_parse_pubkey(const uint8_t* data, size_t len,
+ uint8_t pk[DIST_ED25519_PK_LEN],
+ uint8_t keyid[DIST_KEYID_LEN]) {
+ uint8_t blob[KEY_BLOB_LEN];
+ if (decode_line(data, len, 1, blob, sizeof blob) != DIST_OK) return DIST_ERR;
+ if (memcmp(blob, MS_SIGALG, MS_ALG_LEN) != 0) return DIST_ERR;
+ memcpy(keyid, blob + MS_ALG_LEN, DIST_KEYID_LEN);
+ memcpy(pk, blob + MS_ALG_LEN + DIST_KEYID_LEN, DIST_ED25519_PK_LEN);
+ return DIST_OK;
+}
+
+int dist_minisig_parse_seckey(const uint8_t* data, size_t len,
+ uint8_t sk[DIST_ED25519_SK_LEN],
+ uint8_t keyid[DIST_KEYID_LEN]) {
+ uint8_t blob[SKEY_BLOB_LEN];
+ uint8_t chk[MS_CHK_LEN];
+ if (decode_line(data, len, 1, blob, sizeof blob) != DIST_OK) return DIST_ERR;
+ if (memcmp(blob, MS_SIGALG, MS_ALG_LEN) != 0) return DIST_ERR;
+ /* Encrypted secret keys (kdf_alg = "Sc") need scrypt, not yet vendored. */
+ if (memcmp(blob + SK_KDF_OFF, MS_KDFALG_SCRYPT, MS_ALG_LEN) == 0)
+ return DIST_ENCRYPTED;
+ /* Otherwise require the passwordless form (kdf_alg = {0,0}). */
+ if (blob[SK_KDF_OFF] != 0 || blob[SK_KDF_OFF + 1] != 0) return DIST_ERR;
+ memcpy(keyid, blob + SK_KEYID_OFF, DIST_KEYID_LEN);
+ memcpy(sk, blob + SK_SK_OFF, DIST_ED25519_SK_LEN);
+ /* Verify the BLAKE2b checksum over the (cleartext) key material. */
+ seckey_checksum(chk, keyid, sk);
+ if (memcmp(chk, blob + SK_CHK_OFF, MS_CHK_LEN) != 0) return DIST_ERR;
+ return DIST_OK;
+}
+
+int dist_minisig_sign(CfreeWriter* out, const uint8_t* msg, size_t msglen,
+ const uint8_t sk[DIST_ED25519_SK_LEN],
+ const uint8_t keyid[DIST_KEYID_LEN],
+ const char* untrusted_comment,
+ const char* trusted_comment) {
+ uint8_t prehash[DIST_BLAKE2B_LEN];
+ uint8_t sig[DIST_ED25519_SIG_LEN];
+ uint8_t gsig[DIST_ED25519_SIG_LEN];
+ uint8_t line1[SIG_LINE1_LEN];
+ uint8_t gmsg[DIST_ED25519_SIG_LEN + DIST_TRUSTED_COMMENT_MAX];
+ size_t tclen = strlen(trusted_comment);
+ char line[SIG_LINE_MAX];
+
+ if (tclen >= DIST_TRUSTED_COMMENT_MAX) return DIST_ERR;
+
+ /* Signature over the BLAKE2b prehash of the message. */
+ dist_blake2b(prehash, msg, msglen);
+ dist_ed25519_sign(sig, prehash, sizeof prehash, sk);
+
+ /* Global signature also covers the trusted comment. */
+ memcpy(gmsg, sig, DIST_ED25519_SIG_LEN);
+ memcpy(gmsg + DIST_ED25519_SIG_LEN, trusted_comment, tclen);
+ dist_ed25519_sign(gsig, gmsg, DIST_ED25519_SIG_LEN + tclen, sk);
+
+ snprintf(line, sizeof line, U_PREFIX "%s\n", untrusted_comment);
+ if (emit_str(out, line) != DIST_OK) return DIST_ERR;
+
+ memcpy(line1, MS_SIGALG_HASHED, MS_ALG_LEN); /* "ED": prehashed */
+ memcpy(line1 + MS_ALG_LEN, keyid, DIST_KEYID_LEN);
+ memcpy(line1 + MS_ALG_LEN + DIST_KEYID_LEN, sig, DIST_ED25519_SIG_LEN);
+ if (emit_b64_line(out, line1, sizeof line1) != DIST_OK) return DIST_ERR;
+
+ snprintf(line, sizeof line, T_PREFIX "%s\n", trusted_comment);
+ if (emit_str(out, line) != DIST_OK) return DIST_ERR;
+
+ return emit_b64_line(out, gsig, sizeof gsig);
+}
+
+int dist_minisig_sig_keyid(const uint8_t* sig, size_t siglen,
+ uint8_t out_keyid[DIST_KEYID_LEN]) {
+ uint8_t blob[SIG_LINE1_LEN];
+ if (decode_line(sig, siglen, 1, blob, sizeof blob) != DIST_OK)
+ return DIST_ERR;
+ /* Accept either the prehashed ("ED") or legacy ("Ed") tag. */
+ if (memcmp(blob, MS_SIGALG_HASHED, MS_ALG_LEN) != 0 &&
+ memcmp(blob, MS_SIGALG, MS_ALG_LEN) != 0)
+ return DIST_ERR;
+ memcpy(out_keyid, blob + MS_ALG_LEN, DIST_KEYID_LEN);
+ return DIST_OK;
+}
+
+int dist_minisig_verify(const uint8_t* sig, size_t siglen, const uint8_t* msg,
+ size_t msglen, const uint8_t pk[DIST_ED25519_PK_LEN],
+ char* out_trusted, size_t trusted_cap) {
+ uint8_t blob[SIG_LINE1_LEN];
+ uint8_t gsig[DIST_ED25519_SIG_LEN];
+ uint8_t prehash[DIST_BLAKE2B_LEN];
+ uint8_t gmsg[DIST_ED25519_SIG_LEN + DIST_TRUSTED_COMMENT_MAX];
+ char tcline[SIG_LINE_MAX];
+ const char* tc;
+ size_t tclen;
+
+ int prehashed;
+ const uint8_t* sigbytes = blob + MS_ALG_LEN + DIST_KEYID_LEN;
+
+ if (decode_line(sig, siglen, 1, blob, sizeof blob) != DIST_OK)
+ return DIST_ERR;
+ prehashed = (memcmp(blob, MS_SIGALG_HASHED, MS_ALG_LEN) == 0);
+ if (!prehashed && memcmp(blob, MS_SIGALG, MS_ALG_LEN) != 0) return DIST_ERR;
+
+ /* "ED" signs a BLAKE2b prehash of the message; "Ed" signs it raw. */
+ if (prehashed) {
+ dist_blake2b(prehash, msg, msglen);
+ if (!dist_ed25519_verify(sigbytes, prehash, sizeof prehash, pk))
+ return DIST_ERR;
+ } else if (!dist_ed25519_verify(sigbytes, msg, msglen, pk)) {
+ return DIST_ERR;
+ }
+
+ /* Recover and verify the trusted comment via the global signature. */
+ if (sig_line(sig, siglen, 2, tcline, sizeof tcline) != DIST_OK)
+ return DIST_ERR;
+ if (strncmp(tcline, T_PREFIX, strlen(T_PREFIX)) != 0) return DIST_ERR;
+ tc = tcline + strlen(T_PREFIX);
+ tclen = strlen(tc);
+ if (tclen >= DIST_TRUSTED_COMMENT_MAX) return DIST_ERR;
+ if (decode_line(sig, siglen, 3, gsig, sizeof gsig) != DIST_OK)
+ return DIST_ERR;
+
+ memcpy(gmsg, sigbytes, DIST_ED25519_SIG_LEN);
+ memcpy(gmsg + DIST_ED25519_SIG_LEN, tc, tclen);
+ if (!dist_ed25519_verify(gsig, gmsg, DIST_ED25519_SIG_LEN + tclen, pk))
+ return DIST_ERR;
+
+ if (out_trusted && trusted_cap) snprintf(out_trusted, trusted_cap, "%s", tc);
+ return DIST_OK;
+}
diff --git a/driver/dist/minisig.h b/driver/dist/minisig.h
@@ -0,0 +1,73 @@
+#ifndef CFREE_DIST_MINISIG_H
+#define CFREE_DIST_MINISIG_H
+
+#include <cfree/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* minisign key and signature files, using minisign's exact on-disk byte
+ * layout:
+ * - public key : base64( "Ed" || keyid[8] || pk[32] )
+ * - signature : base64( "ED" || keyid[8] || sig[64] ) over a BLAKE2b
+ * prehash, plus a global signature over the trusted comment
+ * - secret key : base64( "Ed" || kdf_alg[2] || "B2" || salt[32] ||
+ * opslimit[8] || memlimit[8] || keyid[8] || sk[64] ||
+ * chk[32] ), passwordless (kdf_alg = {0,0}, no scrypt).
+ *
+ * So once the real Ed25519/BLAKE2b primitives are vendored, these files are
+ * interchangeable with stock `minisign` — a passwordless minisign key/sig can
+ * be pointed at directly. Password-encrypted secret keys (kdf_alg = "Sc") are
+ * recognized and rejected with a clear error until scrypt is vendored. The
+ * crypto values themselves are STUBBED today (see the dist primitive headers).
+ * See doc/DISTRIBUTE.md. */
+
+/* parse_seckey return codes beyond DIST_OK/DIST_ERR. */
+#define DIST_ENCRYPTED 2 /* kdf_alg = "Sc": needs scrypt (not yet vendored) */
+
+#define DIST_TRUSTED_COMMENT_MAX 512u
+#define DIST_UNTRUSTED_COMMENT_MAX 512u
+
+typedef struct DistKeypair {
+ uint8_t pk[DIST_ED25519_PK_LEN];
+ uint8_t sk[DIST_ED25519_SK_LEN];
+ uint8_t keyid[DIST_KEYID_LEN];
+} DistKeypair;
+
+/* Build a keypair from a 32-byte seed and a (random) 8-byte key id. Both are
+ * supplied by the caller from the host CSPRNG; this layer sources no entropy.
+ * The key id is stored in the key files (minisign-style), not derived. */
+void dist_minisig_keygen(DistKeypair* out,
+ const uint8_t seed[DIST_ED25519_SEED_LEN],
+ const uint8_t keyid[DIST_KEYID_LEN]);
+
+int dist_minisig_emit_pubkey(CfreeWriter* out, const DistKeypair* kp);
+int dist_minisig_emit_seckey(CfreeWriter* out, const DistKeypair* kp);
+
+int dist_minisig_parse_pubkey(const uint8_t* data, size_t len,
+ uint8_t pk[DIST_ED25519_PK_LEN],
+ uint8_t keyid[DIST_KEYID_LEN]);
+int dist_minisig_parse_seckey(const uint8_t* data, size_t len,
+ uint8_t sk[DIST_ED25519_SK_LEN],
+ uint8_t keyid[DIST_KEYID_LEN]);
+
+/* Write a detached signature over `msg` to `out`. The trusted comment is
+ * signed (covered by the global signature); the untrusted comment is not. */
+int dist_minisig_sign(CfreeWriter* out, const uint8_t* msg, size_t msglen,
+ const uint8_t sk[DIST_ED25519_SK_LEN],
+ const uint8_t keyid[DIST_KEYID_LEN],
+ const char* untrusted_comment,
+ const char* trusted_comment);
+
+/* Extract the signing key id from a signature file (to pick a trusted key). */
+int dist_minisig_sig_keyid(const uint8_t* sig, size_t siglen,
+ uint8_t out_keyid[DIST_KEYID_LEN]);
+
+/* Verify a detached signature file over `msg` against `pk`. On success returns
+ * DIST_OK and copies the (signed) trusted comment into `out_trusted`. */
+int dist_minisig_verify(const uint8_t* sig, size_t siglen, const uint8_t* msg,
+ size_t msglen, const uint8_t pk[DIST_ED25519_PK_LEN],
+ char* out_trusted, size_t trusted_cap);
+
+#endif
diff --git a/driver/dist/sha256.c b/driver/dist/sha256.c
@@ -0,0 +1,30 @@
+#include "sha256.h"
+
+/* STUB digest. See sha256.h. A counter-driven FNV/xorshift sponge: enough
+ * avalanche to make distinct inputs produce distinct digests in practice,
+ * but cryptographically worthless. */
+
+#define DIST_STUB_FNV_OFFSET 0xcbf29ce484222325ULL
+#define DIST_STUB_FNV_PRIME 0x00000100000001b3ULL
+
+static uint64_t stub_absorb(const uint8_t* data, size_t len, uint64_t h) {
+ size_t i;
+ for (i = 0; i < len; ++i) {
+ h ^= data[i];
+ h *= DIST_STUB_FNV_PRIME;
+ h ^= h >> 33;
+ }
+ return h;
+}
+
+void dist_sha256(uint8_t out[DIST_SHA256_LEN], const uint8_t* data,
+ size_t len) {
+ uint64_t base = stub_absorb(data, len, DIST_STUB_FNV_OFFSET);
+ size_t i;
+ for (i = 0; i < DIST_SHA256_LEN; ++i) {
+ uint64_t h = base ^ (0x9e3779b97f4a7c15ULL * (uint64_t)(i + 1));
+ h *= DIST_STUB_FNV_PRIME;
+ h ^= h >> 29;
+ out[i] = (uint8_t)(h >> 24);
+ }
+}
diff --git a/driver/dist/sha256.h b/driver/dist/sha256.h
@@ -0,0 +1,18 @@
+#ifndef CFREE_DIST_SHA256_H
+#define CFREE_DIST_SHA256_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* Content-addressing hash for the manifest and payload.
+ *
+ * STUB: this is a deterministic non-cryptographic digest so the packaging
+ * pipeline can content-address and verify integrity end-to-end. It is NOT
+ * SHA-256 and provides no collision resistance. To be replaced by the real
+ * SHA-256 (already vendored at src/core/sha256.c, to be exposed to the
+ * driver) before this subsystem is trusted. */
+void dist_sha256(uint8_t out[DIST_SHA256_LEN], const uint8_t* data, size_t len);
+
+#endif
diff --git a/driver/dist/tar.c b/driver/dist/tar.c
@@ -0,0 +1,128 @@
+#include "tar.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define TAR_BLOCK 512u
+#define TAR_NAME_OFF 0u
+#define TAR_MODE_OFF 100u
+#define TAR_UID_OFF 108u
+#define TAR_GID_OFF 116u
+#define TAR_SIZE_OFF 124u
+#define TAR_MTIME_OFF 136u
+#define TAR_CHKSUM_OFF 148u
+#define TAR_TYPE_OFF 156u
+#define TAR_MAGIC_OFF 257u
+#define TAR_CHKSUM_LEN 8u
+#define TAR_DEFAULT_MODE 0644u
+
+/* Round `n` up to the next multiple of TAR_BLOCK. */
+static size_t tar_round(size_t n) {
+ return (n + (TAR_BLOCK - 1u)) & ~(size_t)(TAR_BLOCK - 1u);
+}
+
+static unsigned tar_checksum(const uint8_t* hdr) {
+ unsigned sum = 0;
+ size_t i;
+ for (i = 0; i < TAR_BLOCK; ++i) {
+ /* The checksum field itself is treated as spaces. */
+ if (i >= TAR_CHKSUM_OFF && i < TAR_CHKSUM_OFF + TAR_CHKSUM_LEN) {
+ sum += (unsigned)' ';
+ } else {
+ sum += hdr[i];
+ }
+ }
+ return sum;
+}
+
+static int tar_write(CfreeWriter* out, const void* data, size_t n) {
+ return cfree_writer_write(out, data, n) == CFREE_OK ? DIST_OK : DIST_ERR;
+}
+
+int dist_tar_append(CfreeWriter* out, const char* name, const uint8_t* data,
+ size_t size) {
+ uint8_t hdr[TAR_BLOCK];
+ size_t namelen = strlen(name);
+ unsigned sum;
+ static const uint8_t zero[TAR_BLOCK] = {0};
+ size_t pad;
+
+ if (namelen >= DIST_PATH_MAX) return DIST_ERR;
+
+ memset(hdr, 0, sizeof hdr);
+ memcpy(hdr + TAR_NAME_OFF, name, namelen);
+ snprintf((char*)hdr + TAR_MODE_OFF, 8, "%07o", TAR_DEFAULT_MODE);
+ snprintf((char*)hdr + TAR_UID_OFF, 8, "%07o", 0u);
+ snprintf((char*)hdr + TAR_GID_OFF, 8, "%07o", 0u);
+ snprintf((char*)hdr + TAR_SIZE_OFF, 12, "%011o", (unsigned)size);
+ snprintf((char*)hdr + TAR_MTIME_OFF, 12, "%011o", 0u);
+ hdr[TAR_TYPE_OFF] = '0';
+ memcpy(hdr + TAR_MAGIC_OFF, "ustar", 5); /* trailing NUL already zeroed */
+ hdr[TAR_MAGIC_OFF + 6] = '0';
+ hdr[TAR_MAGIC_OFF + 7] = '0';
+
+ sum = tar_checksum(hdr);
+ /* "%06o\0 " — six octal digits, NUL, then a space. */
+ snprintf((char*)hdr + TAR_CHKSUM_OFF, 7, "%06o", sum);
+ hdr[TAR_CHKSUM_OFF + 7] = ' ';
+
+ if (tar_write(out, hdr, TAR_BLOCK) != DIST_OK) return DIST_ERR;
+ if (size && tar_write(out, data, size) != DIST_OK) return DIST_ERR;
+ pad = tar_round(size) - size;
+ if (pad && tar_write(out, zero, pad) != DIST_OK) return DIST_ERR;
+ return DIST_OK;
+}
+
+int dist_tar_finish(CfreeWriter* out) {
+ static const uint8_t zero[TAR_BLOCK] = {0};
+ if (tar_write(out, zero, TAR_BLOCK) != DIST_OK) return DIST_ERR;
+ if (tar_write(out, zero, TAR_BLOCK) != DIST_OK) return DIST_ERR;
+ return DIST_OK;
+}
+
+/* Parse an octal field of `len` bytes (space/NUL terminated). */
+static size_t tar_parse_octal(const uint8_t* field, size_t len) {
+ size_t v = 0;
+ size_t i;
+ for (i = 0; i < len; ++i) {
+ uint8_t c = field[i];
+ if (c == ' ' || c == '\0') break;
+ if (c < '0' || c > '7') break;
+ v = v * 8u + (size_t)(c - '0');
+ }
+ return v;
+}
+
+static int tar_block_is_zero(const uint8_t* b) {
+ size_t i;
+ for (i = 0; i < TAR_BLOCK; ++i) {
+ if (b[i]) return 0;
+ }
+ return 1;
+}
+
+int dist_tar_iter(const uint8_t* data, size_t len, DistTarEntry* out,
+ size_t cap, size_t* count) {
+ size_t off = 0;
+ size_t n = 0;
+ while (off + TAR_BLOCK <= len) {
+ const uint8_t* hdr = data + off;
+ size_t size;
+ if (tar_block_is_zero(hdr)) break; /* end-of-archive marker */
+ if (tar_checksum(hdr) !=
+ tar_parse_octal(hdr + TAR_CHKSUM_OFF, TAR_CHKSUM_LEN)) {
+ return DIST_ERR;
+ }
+ if (n >= cap) return DIST_ERR;
+ size = tar_parse_octal(hdr + TAR_SIZE_OFF, 12);
+ if (off + TAR_BLOCK + size > len) return DIST_ERR; /* truncated */
+ memcpy(out[n].name, hdr + TAR_NAME_OFF, DIST_PATH_MAX);
+ out[n].name[DIST_PATH_MAX] = '\0';
+ out[n].data = data + off + TAR_BLOCK;
+ out[n].size = size;
+ ++n;
+ off += TAR_BLOCK + tar_round(size);
+ }
+ *count = n;
+ return DIST_OK;
+}
diff --git a/driver/dist/tar.h b/driver/dist/tar.h
@@ -0,0 +1,35 @@
+#ifndef CFREE_DIST_TAR_H
+#define CFREE_DIST_TAR_H
+
+#include <cfree/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* Minimal ustar reader/writer. Real, not a stub: this is plain container
+ * framing, not security-sensitive. Regular files only; names < 100 bytes
+ * (the ustar `name` field). Used both for the payload archive and for the
+ * outer `.cfpkg` bag. */
+
+/* Append one regular-file member. Returns DIST_OK / DIST_ERR (name too long
+ * or writer error). */
+int dist_tar_append(CfreeWriter* out, const char* name, const uint8_t* data,
+ size_t size);
+
+/* Write the end-of-archive marker (two zero blocks). */
+int dist_tar_finish(CfreeWriter* out);
+
+typedef struct DistTarEntry {
+ char name[DIST_PATH_MAX + 1];
+ const uint8_t* data; /* aliases into the input buffer */
+ size_t size;
+} DistTarEntry;
+
+/* Parse `data`/`len` into up to `cap` entries, storing the count in *count.
+ * Returns DIST_OK on a well-formed archive, DIST_ERR on truncation, a bad
+ * checksum, or more than `cap` members. */
+int dist_tar_iter(const uint8_t* data, size_t len, DistTarEntry* out,
+ size_t cap, size_t* count);
+
+#endif
diff --git a/driver/dist/trust.c b/driver/dist/trust.c
@@ -0,0 +1,67 @@
+#include "trust.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "b64.h"
+
+#define KEYID_HEX_LEN (2u * DIST_KEYID_LEN)
+
+int dist_trust_lookup(const uint8_t* file, size_t len,
+ const uint8_t keyid[DIST_KEYID_LEN],
+ uint8_t pk[DIST_ED25519_PK_LEN]) {
+ size_t pos = 0;
+ while (pos < len) {
+ char line[DIST_TRUST_LINE_MAX];
+ size_t start = pos, end = pos, n;
+ char *p, *sp;
+ uint8_t got_id[DIST_KEYID_LEN];
+
+ while (end < len && file[end] != '\n') ++end;
+ n = end - start;
+ pos = (end < len) ? end + 1 : end;
+ if (n == 0 || n >= sizeof line) continue;
+ memcpy(line, file + start, n);
+ line[n] = '\0';
+
+ p = line;
+ while (*p == ' ' || *p == '\t') ++p;
+ if (*p == '\0' || *p == '#') continue;
+
+ /* First token: key id (hex). */
+ sp = strchr(p, ' ');
+ if (!sp) continue;
+ *sp = '\0';
+ if (strlen(p) != KEYID_HEX_LEN) continue;
+ if (dist_hex_decode(got_id, p, DIST_KEYID_LEN) != DIST_OK) continue;
+ if (memcmp(got_id, keyid, DIST_KEYID_LEN) != 0) continue;
+
+ /* Second token: base64 public key. */
+ p = sp + 1;
+ while (*p == ' ' || *p == '\t') ++p;
+ sp = strchr(p, ' ');
+ if (sp) *sp = '\0';
+ {
+ uint8_t buf[DIST_ED25519_PK_LEN + 4];
+ size_t got = 0;
+ if (dist_b64_decode(buf, &got, p, strlen(p)) != DIST_OK) return DIST_ERR;
+ if (got != DIST_ED25519_PK_LEN) return DIST_ERR;
+ memcpy(pk, buf, DIST_ED25519_PK_LEN);
+ return DIST_OK;
+ }
+ }
+ return DIST_ERR;
+}
+
+int dist_trust_format_entry(char* out, size_t cap,
+ const uint8_t keyid[DIST_KEYID_LEN],
+ const uint8_t pk[DIST_ED25519_PK_LEN],
+ const char* label) {
+ char hex[KEYID_HEX_LEN + 1];
+ char b64[DIST_B64_ENCODED_CAP(DIST_ED25519_PK_LEN)];
+ int n;
+ dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
+ dist_b64_encode(b64, pk, DIST_ED25519_PK_LEN);
+ n = snprintf(out, cap, "%s %s %s\n", hex, b64, label ? label : "");
+ return (n > 0 && (size_t)n < cap) ? DIST_OK : DIST_ERR;
+}
diff --git a/driver/dist/trust.h b/driver/dist/trust.h
@@ -0,0 +1,29 @@
+#ifndef CFREE_DIST_TRUST_H
+#define CFREE_DIST_TRUST_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+/* The trusted-keys store: the only anchor of trust. Plain text, one key per
+ * line: "<keyid-hex> <pubkey-base64> <label...>". This module is pure
+ * byte-level logic; path resolution and file I/O live in the `pkg` tool. A
+ * key embedded in a manifest is never trust — only a key present here is. */
+
+#define DIST_TRUST_LINE_MAX 1024u
+
+/* Find `keyid` in the store bytes, writing its public key to `pk`. Returns
+ * DIST_OK if present, DIST_ERR if absent or on a malformed line for that id. */
+int dist_trust_lookup(const uint8_t* file, size_t len,
+ const uint8_t keyid[DIST_KEYID_LEN],
+ uint8_t pk[DIST_ED25519_PK_LEN]);
+
+/* Format a store line for `keyid`/`pk`/`label` (NUL-terminated, newline
+ * included). Returns DIST_OK / DIST_ERR (buffer too small). */
+int dist_trust_format_entry(char* out, size_t cap,
+ const uint8_t keyid[DIST_KEYID_LEN],
+ const uint8_t pk[DIST_ED25519_PK_LEN],
+ const char* label);
+
+#endif
diff --git a/driver/driver.h b/driver/driver.h
@@ -53,6 +53,7 @@ int driver_nm(int argc, char** argv);
int driver_size(int argc, char** argv);
int driver_addr2line(int argc, char** argv);
int driver_strings(int argc, char** argv);
+int driver_pkg(int argc, char** argv);
/* Per-tool help printers. Write a multi-section help text to stdout and
* return. The tool entry-points call these when invoked with no args, -h,
@@ -75,6 +76,7 @@ void driver_help_nm(void);
void driver_help_size(void);
void driver_help_addr2line(void);
void driver_help_strings(void);
+void driver_help_pkg(void);
/* Multi-call top-level help (`cfree`, `cfree -h`, `cfree --help`,
* `cfree help`). Lists each tool with a one-line summary and explains
diff --git a/driver/env.h b/driver/env.h
@@ -125,6 +125,12 @@ void driver_printf(const char* fmt, ...);
/* Monotonic host time in nanoseconds, or 0 if unavailable. */
uint64_t driver_now_ns(void);
+/* Fill `out` with `n` cryptographically-random bytes from the host CSPRNG.
+ * Returns 0 on success, non-zero on failure (in which case `out` must not be
+ * used). This is the single entropy source for key generation; the crypto
+ * primitives themselves never source randomness — it always flows in here. */
+int driver_random_bytes(uint8_t* out, size_t n);
+
/* Lookup a process environment variable; returns NULL if unset. The returned
* pointer aliases libc-owned storage and is valid until the next setenv/
* putenv from any caller. */
diff --git a/driver/env/posix.c b/driver/env/posix.c
@@ -20,7 +20,8 @@
#include "env_posix.h"
-/* ---------------- exec memory: single-mapping core + registry ---------------- */
+/* ---------------- exec memory: single-mapping core + registry ----------------
+ */
int cfree_to_posix_prot(int prot) {
int p = 0;
@@ -398,6 +399,29 @@ uint64_t driver_now_ns(void) {
return 0;
}
+int driver_random_bytes(uint8_t* out, size_t n) {
+ /* /dev/urandom is the most portable CSPRNG across darwin/linux/freebsd and
+ * avoids getentropy()'s per-platform header/feature-macro gymnastics. */
+ size_t off = 0;
+ int fd;
+ if (!out) return 1;
+ fd = open("/dev/urandom", O_RDONLY);
+ if (fd < 0) return 1;
+ while (off < n) {
+ ssize_t r = read(fd, out + off, n - off);
+ if (r > 0) {
+ off += (size_t)r;
+ } else if (r < 0 && errno == EINTR) {
+ continue;
+ } else {
+ close(fd);
+ return 1;
+ }
+ }
+ close(fd);
+ return 0;
+}
+
/* ---------------- load helpers ---------------- */
int driver_load_bytes(const CfreeFileIO* io, const char* tool, const char* path,
@@ -788,4 +812,3 @@ CfreeDbgHost driver_env_to_dbg_host(const DriverEnv* e) {
h.os = e->dbg_os;
return h;
}
-
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -36,22 +36,22 @@
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef _WIN32_WINNT
-#define _WIN32_WINNT 0x0601 /* Windows 7+: SRWLock, AddVectoredExceptionHandler */
+#define _WIN32_WINNT \
+ 0x0601 /* Windows 7+: SRWLock, AddVectoredExceptionHandler */
#endif
-#include <windows.h>
-#include <psapi.h>
-
#include <io.h>
#include <process.h>
+#include <psapi.h>
#include <stdint.h>
#include <stdio.h>
+#include <windows.h>
+#define _CRT_RAND_S /* enable rand_s (OS CSPRNG) */
+#include <setjmp.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
-#include <setjmp.h>
-
#include "env_internal.h"
/* Win32 dbg interrupt code: a synthetic signo handed up to on_fault. The
@@ -191,8 +191,8 @@ static CfreeStatus execmem_reserve_dual_win(size_t size,
/* PAGE_EXECUTE_READWRITE on the section object is the max protection any
* view can request; per-view protections are narrower (RW vs RX). */
- map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL,
- PAGE_EXECUTE_READWRITE, hi, lo, NULL);
+ map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE,
+ hi, lo, NULL);
if (!map) return CFREE_ERR;
w = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, size);
@@ -427,7 +427,8 @@ static CfreeStatus win_read_all(void* user, const char* path,
}
got = 0;
while (got < size) {
- DWORD chunk = (size - got) > 0x40000000u ? 0x40000000u : (DWORD)(size - got);
+ DWORD chunk =
+ (size - got) > 0x40000000u ? 0x40000000u : (DWORD)(size - got);
DWORD n = 0;
if (!ReadFile(h, (unsigned char*)buf + got, chunk, &n, NULL) || n == 0) {
env->heap->free(env->heap, buf, size);
@@ -597,6 +598,21 @@ uint64_t driver_now_ns(void) {
}
}
+int driver_random_bytes(uint8_t* out, size_t n) {
+ /* rand_s draws from the OS CSPRNG (RtlGenRandom) without linking bcrypt. */
+ size_t off = 0;
+ if (!out) return 1;
+ while (off < n) {
+ unsigned int v;
+ size_t chunk;
+ if (rand_s(&v) != 0) return 1;
+ chunk = n - off < sizeof v ? n - off : sizeof v;
+ memcpy(out + off, &v, chunk);
+ off += chunk;
+ }
+ return 0;
+}
+
/* ============================================================
* load helpers
* ============================================================ */
@@ -851,10 +867,9 @@ static void dlsym_init_once(void) {
if (EnumProcessModules(proc, g_dlsym_modules, sizeof(g_dlsym_modules),
&need)) {
DWORD have = need / (DWORD)sizeof(HMODULE);
- g_dlsym_count =
- have > (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
- ? (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
- : have;
+ g_dlsym_count = have > (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
+ ? (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
+ : have;
} else {
/* Fall back to the executable module and the CRT/kernel essentials. */
g_dlsym_modules[0] = GetModuleHandleW(NULL);
@@ -1292,7 +1307,8 @@ static void dbg_code_write_end_win(void* user, void* runtime_addr, size_t n) {
size_t span;
DWORD old;
(void)user;
- if (exec_dual_lookup_w(runtime_addr, n, &w) == 0) return; /* nothing to flip */
+ if (exec_dual_lookup_w(runtime_addr, n, &w) == 0)
+ return; /* nothing to flip */
pg = driver_host_page_size_win();
a = (uintptr_t)runtime_addr;
base = page_floor(a, pg);
@@ -1565,9 +1581,7 @@ void driver_env_init(DriverEnv* e) {
}
}
-void driver_env_fini(DriverEnv* e) {
- (void)e;
-}
+void driver_env_fini(DriverEnv* e) { (void)e; }
CfreeContext driver_env_to_context(const DriverEnv* e) {
CfreeContext c;
diff --git a/driver/main.c b/driver/main.c
@@ -88,6 +88,10 @@ static const DriverToolDesc driver_tools[] = {
{"strings", driver_strings, driver_help_strings,
"Print printable character sequences found in a file"},
#endif
+#if CFREE_TOOL_PKG_ENABLED
+ {"pkg", driver_pkg, driver_help_pkg,
+ "Bundle, sign, verify, and unpack distributable .cfpkg packages"},
+#endif
{NULL, NULL, NULL, NULL},
};
diff --git a/driver/pkg.c b/driver/pkg.c
@@ -0,0 +1,935 @@
+#include <cfree/core.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "dist/deflate.h"
+#include "dist/dist.h"
+#include "dist/manifest.h"
+#include "dist/minisig.h"
+#include "dist/sha256.h"
+#include "dist/tar.h"
+#include "dist/trust.h"
+#include "driver.h"
+#include "env.h"
+
+/* `cfree pkg` — basic code distribution: bundle a build into a signed,
+ * self-describing `.cfpkg`, and verify/unpack it against a trusted key.
+ *
+ * The crypto and compression under driver/dist/ are STUBS for now (see
+ * doc/DISTRIBUTE.md): the whole pipeline runs end-to-end, but the signatures
+ * and content hashes are NOT yet secure. This tool wires the real flow so the
+ * vendored primitives can drop in later without touching the orchestration. */
+
+#define PKG_TOOL "pkg"
+#define PKG_PATH_BUF 1024u
+#define PKG_NAME_BUF 256u
+
+/* A `.cfpkg` bags these four members, all named "<name>-<version>.*". */
+#define PKG_SUFFIX_ARCHIVE ".tar.gz"
+#define PKG_SUFFIX_MANIFEST ".manifest"
+#define PKG_SUFFIX_SIG ".manifest.minisig"
+#define PKG_SUFFIX_PUBKEY ".pub"
+
+/* -------------------------------------------------------------- helpers --- */
+
+static int pkg_write_file(const CfreeContext* ctx, const char* path,
+ const uint8_t* data, size_t len) {
+ CfreeWriter* w = NULL;
+ int rc;
+ if (ctx->file_io->open_writer(ctx->file_io->user, path, &w) != CFREE_OK) {
+ driver_errf(PKG_TOOL, "failed to open output: %s", path);
+ return DIST_ERR;
+ }
+ rc = (len == 0 || cfree_writer_write(w, data, len) == CFREE_OK) ? DIST_OK
+ : DIST_ERR;
+ if (cfree_writer_status(w) != CFREE_OK) rc = DIST_ERR;
+ cfree_writer_close(w);
+ if (rc != DIST_OK) driver_errf(PKG_TOOL, "failed to write: %s", path);
+ return rc;
+}
+
+/* Fresh in-memory writer; aborts callers on OOM via NULL return. */
+static CfreeWriter* pkg_mem(const CfreeContext* ctx) {
+ CfreeWriter* w = NULL;
+ if (cfree_writer_mem(ctx->heap, &w) != CFREE_OK) return NULL;
+ return w;
+}
+
+/* Default trusted-keys path: $CFREE_TRUSTED_KEYS, else
+ * $HOME/.config/cfree/trusted_keys. Returns DIST_ERR if neither is set. */
+static int pkg_trust_path(char* buf, size_t cap) {
+ const char* env = driver_getenv("CFREE_TRUSTED_KEYS");
+ const char* home;
+ if (env) {
+ snprintf(buf, cap, "%s", env);
+ return DIST_OK;
+ }
+ home = driver_getenv("HOME");
+ if (!home) return DIST_ERR;
+ snprintf(buf, cap, "%s/.config/cfree/trusted_keys", home);
+ return DIST_OK;
+}
+
+/* Copy the parent directory of `path` into `buf` ("" if no slash). */
+static void pkg_parent_dir(const char* path, char* buf, size_t cap) {
+ const char* slash = NULL;
+ const char* p;
+ size_t n;
+ for (p = path; *p; ++p) {
+ if (*p == '/') slash = p;
+ }
+ if (!slash) {
+ buf[0] = '\0';
+ return;
+ }
+ n = (size_t)(slash - path);
+ if (n >= cap) n = cap - 1;
+ memcpy(buf, path, n);
+ buf[n] = '\0';
+}
+
+/* Read an entire file. Returns DIST_OK and a borrowed view (release with
+ * ctx->file_io->release on the returned CfreeFileData). */
+static int pkg_read_file(const CfreeContext* ctx, const char* path,
+ CfreeFileData* out) {
+ return ctx->file_io->read_all(ctx->file_io->user, path, out) == CFREE_OK
+ ? DIST_OK
+ : DIST_ERR;
+}
+
+/* Locate a bag member whose name ends with `suffix`. */
+static const DistTarEntry* pkg_find_suffix(const DistTarEntry* e, size_t n,
+ const char* suffix) {
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ if (driver_has_suffix(e[i].name, suffix)) return &e[i];
+ }
+ return NULL;
+}
+
+static const DistTarEntry* pkg_find_name(const DistTarEntry* e, size_t n,
+ const char* name) {
+ size_t i;
+ for (i = 0; i < n; ++i) {
+ if (driver_streq(e[i].name, name)) return &e[i];
+ }
+ return NULL;
+}
+
+/* ---------------------------------------------------------------- help --- */
+
+void driver_help_pkg(void) {
+ driver_printf(
+ "cfree pkg — basic signed code distribution\n"
+ "\n"
+ "USAGE\n"
+ " cfree pkg keygen -o BASE\n"
+ " cfree pkg create --name N --version V [--desc D] -s SECKEY\n"
+ " -o OUT.cfpkg FILE...\n"
+ " cfree pkg verify [-p PUBKEY | --tofu] FILE.cfpkg\n"
+ " cfree pkg unpack FILE.cfpkg -C DIR\n"
+ " cfree pkg inspect FILE.cfpkg\n"
+ " cfree pkg trust {list | add PUBKEY [label] | remove KEYID}\n"
+ "\n"
+ "DESCRIPTION\n"
+ " A .cfpkg bundles a signed manifest, its detached signature, the\n"
+ " gzip payload archive, and the signer's public key. Verification\n"
+ " anchors the signature against the trusted-keys store (or an\n"
+ " explicit -p key); --tofu pins the bundled key on first use.\n"
+ "\n"
+ " NOTE: crypto and hashing are STUBBED in this build and provide no\n"
+ " security yet (see doc/DISTRIBUTE.md).\n"
+ "\n"
+ "EXIT CODES\n"
+ " 0 success 1 error 2 bad usage\n");
+}
+
+/* -------------------------------------------------------------- keygen --- */
+
+static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc,
+ char** argv) {
+ const char* base = NULL;
+ uint8_t seed[DIST_ED25519_SEED_LEN];
+ uint8_t keyid[DIST_KEYID_LEN];
+ DistKeypair kp;
+ CfreeWriter* w;
+ char path[PKG_PATH_BUF];
+ const uint8_t* bytes;
+ size_t len;
+ int i;
+ (void)env;
+
+ for (i = 0; i < argc; ++i) {
+ if (driver_streq(argv[i], "-o") && i + 1 < argc) {
+ base = argv[++i];
+ } else {
+ driver_errf(PKG_TOOL, "keygen: unexpected argument: %s", argv[i]);
+ return 2;
+ }
+ }
+ if (!base) {
+ driver_errf(PKG_TOOL, "keygen: -o BASE is required");
+ return 2;
+ }
+
+ /* Real entropy from the host CSPRNG: a random seed and a random key id. */
+ if (driver_random_bytes(seed, sizeof seed) != 0 ||
+ driver_random_bytes(keyid, sizeof keyid) != 0) {
+ driver_errf(PKG_TOOL, "keygen: failed to read system randomness");
+ return 1;
+ }
+ dist_minisig_keygen(&kp, seed, keyid);
+
+ w = pkg_mem(ctx);
+ if (!w || dist_minisig_emit_pubkey(w, &kp) != DIST_OK) return 1;
+ bytes = cfree_writer_mem_bytes(w, &len);
+ snprintf(path, sizeof path, "%s.pub", base);
+ if (pkg_write_file(ctx, path, bytes, len) != DIST_OK) return 1;
+ cfree_writer_close(w);
+
+ w = pkg_mem(ctx);
+ if (!w || dist_minisig_emit_seckey(w, &kp) != DIST_OK) return 1;
+ bytes = cfree_writer_mem_bytes(w, &len);
+ snprintf(path, sizeof path, "%s.key", base);
+ if (pkg_write_file(ctx, path, bytes, len) != DIST_OK) return 1;
+ cfree_writer_close(w);
+
+ {
+ char hex[2 * DIST_KEYID_LEN + 1];
+ dist_hex_encode(hex, kp.keyid, DIST_KEYID_LEN);
+ driver_printf("wrote %s.pub and %s.key (key id %s)\n", base, base, hex);
+ }
+ return 0;
+}
+
+/* -------------------------------------------------------------- create --- */
+
+static int pkg_create(DriverEnv* env, const CfreeContext* ctx, int argc,
+ char** argv) {
+ const char* name = NULL;
+ const char* version = NULL;
+ const char* desc = NULL;
+ const char* out = NULL;
+ const char* seckey = NULL;
+ const char* files[DIST_MAX_FILES];
+ size_t n_files = 0;
+ int i, rc = 1;
+
+ DistManifest m;
+ DistKeypair kp;
+ CfreeWriter *payload = NULL, *gz = NULL, *manw = NULL, *sigw = NULL,
+ *pubw = NULL, *bag = NULL;
+ CfreeFileData skfd = {0};
+ int sk_loaded = 0;
+ const uint8_t *gz_bytes, *man_bytes, *sig_bytes, *pub_bytes, *bag_bytes;
+ size_t gz_len, man_len, sig_len, pub_len, bag_len;
+ char innerbase[PKG_NAME_BUF];
+ char arc_name[PKG_NAME_BUF], man_name[PKG_NAME_BUF], sig_name[PKG_NAME_BUF],
+ pub_name[PKG_NAME_BUF];
+ char tcomment[DIST_TRUSTED_COMMENT_MAX];
+ char pkgid_hex[2 * DIST_SHA256_LEN + 1];
+ uint8_t pkgid[DIST_SHA256_LEN];
+
+ for (i = 0; i < argc; ++i) {
+ const char* a = argv[i];
+ if (driver_streq(a, "--name") && i + 1 < argc) {
+ name = argv[++i];
+ } else if (driver_streq(a, "--version") && i + 1 < argc) {
+ version = argv[++i];
+ } else if (driver_streq(a, "--desc") && i + 1 < argc) {
+ desc = argv[++i];
+ } else if (driver_streq(a, "-o") && i + 1 < argc) {
+ out = argv[++i];
+ } else if (driver_streq(a, "-s") && i + 1 < argc) {
+ seckey = argv[++i];
+ } else if (a[0] == '-' && a[1] != '\0') {
+ driver_errf(PKG_TOOL, "create: unknown option: %s", a);
+ return 2;
+ } else {
+ if (n_files >= DIST_MAX_FILES) {
+ driver_errf(PKG_TOOL, "create: too many files");
+ return 2;
+ }
+ files[n_files++] = a;
+ }
+ }
+ if (!name || !version || !out || !seckey) {
+ driver_errf(PKG_TOOL,
+ "create: --name, --version, -s SECKEY and -o OUT are required");
+ return 2;
+ }
+
+ /* Load the signing key. */
+ if (pkg_read_file(ctx, seckey, &skfd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: cannot read secret key: %s", seckey);
+ return 1;
+ }
+ sk_loaded = 1;
+ {
+ int kr = dist_minisig_parse_seckey(skfd.data, skfd.size, kp.sk, kp.keyid);
+ if (kr == DIST_ENCRYPTED) {
+ driver_errf(PKG_TOOL,
+ "create: encrypted secret keys are not supported yet "
+ "(needs scrypt); use a passwordless key");
+ goto done;
+ }
+ if (kr != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: malformed secret key");
+ goto done;
+ }
+ }
+ /* The Ed25519 secret key carries the public key in its tail half. */
+ memcpy(kp.pk, kp.sk + DIST_ED25519_SEED_LEN, DIST_ED25519_PK_LEN);
+
+ memset(&m, 0, sizeof m);
+ snprintf(m.name, sizeof m.name, "%s", name);
+ snprintf(m.version, sizeof m.version, "%s", version);
+ if (desc) snprintf(m.description, sizeof m.description, "%s", desc);
+
+ /* Build the payload tar: one member + one artifact per input file. */
+ payload = pkg_mem(ctx);
+ if (!payload) goto done;
+ for (i = 0; i < (int)n_files; ++i) {
+ CfreeFileData fd = {0};
+ const char* base = driver_basename(files[i]);
+ DistArtifact* a;
+ if (pkg_read_file(ctx, files[i], &fd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: cannot read file: %s", files[i]);
+ goto done;
+ }
+ if (m.n_artifacts >= DIST_MAX_ARTIFACTS) {
+ driver_errf(PKG_TOOL, "create: too many artifacts");
+ ctx->file_io->release(ctx->file_io->user, &fd);
+ goto done;
+ }
+ if (dist_tar_append(payload, base, fd.data, fd.size) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: cannot archive '%s' (name too long?)",
+ base);
+ ctx->file_io->release(ctx->file_io->user, &fd);
+ goto done;
+ }
+ a = &m.artifacts[m.n_artifacts++];
+ snprintf(a->path, sizeof a->path, "%s", base);
+ snprintf(a->kind, sizeof a->kind, "%s", "data");
+ a->size = fd.size;
+ dist_sha256(a->sha256, fd.data, fd.size);
+ ctx->file_io->release(ctx->file_io->user, &fd);
+ }
+ if (dist_tar_finish(payload) != DIST_OK) goto done;
+
+ /* gzip the payload, then hash the gzip bytes for the manifest. */
+ {
+ const uint8_t* pb;
+ size_t pl;
+ pb = cfree_writer_mem_bytes(payload, &pl);
+ gz = pkg_mem(ctx);
+ if (!gz || dist_gz_compress(gz, pb, pl) != DIST_OK) goto done;
+ }
+ gz_bytes = cfree_writer_mem_bytes(gz, &gz_len);
+
+ snprintf(innerbase, sizeof innerbase, "%s-%s", name, version);
+ snprintf(arc_name, sizeof arc_name, "%s%s", innerbase, PKG_SUFFIX_ARCHIVE);
+ snprintf(man_name, sizeof man_name, "%s%s", innerbase, PKG_SUFFIX_MANIFEST);
+ snprintf(sig_name, sizeof sig_name, "%s%s", innerbase, PKG_SUFFIX_SIG);
+ snprintf(pub_name, sizeof pub_name, "%s%s", innerbase, PKG_SUFFIX_PUBKEY);
+
+ snprintf(m.archive, sizeof m.archive, "%s", arc_name);
+ m.archive_size = gz_len;
+ dist_sha256(m.archive_sha256, gz_bytes, gz_len);
+
+ /* Serialize the manifest; it is the signed object. */
+ manw = pkg_mem(ctx);
+ if (!manw || dist_manifest_emit(&m, manw) != DIST_OK) goto done;
+ man_bytes = cfree_writer_mem_bytes(manw, &man_len);
+
+ /* Sign the manifest. Signing time + package id go in the trusted comment,
+ * keeping the manifest itself reproducible. */
+ dist_sha256(pkgid, man_bytes, man_len);
+ dist_hex_encode(pkgid_hex, pkgid, DIST_SHA256_LEN);
+ snprintf(tcomment, sizeof tcomment, "created=%lld pkgid=%s",
+ (long long)(env->now > 0 ? env->now : 0), pkgid_hex);
+
+ sigw = pkg_mem(ctx);
+ if (!sigw ||
+ dist_minisig_sign(sigw, man_bytes, man_len, kp.sk, kp.keyid,
+ "signature from cfree pkg", tcomment) != DIST_OK) {
+ goto done;
+ }
+ sig_bytes = cfree_writer_mem_bytes(sigw, &sig_len);
+
+ /* The signer's public key travels with the bag (untrusted; TOFU may pin). */
+ pubw = pkg_mem(ctx);
+ if (!pubw || dist_minisig_emit_pubkey(pubw, &kp) != DIST_OK) goto done;
+ pub_bytes = cfree_writer_mem_bytes(pubw, &pub_len);
+
+ /* Bag the four members into the (uncompressed, unsigned) .cfpkg. */
+ bag = pkg_mem(ctx);
+ if (!bag) goto done;
+ if (dist_tar_append(bag, arc_name, gz_bytes, gz_len) != DIST_OK ||
+ dist_tar_append(bag, man_name, man_bytes, man_len) != DIST_OK ||
+ dist_tar_append(bag, sig_name, sig_bytes, sig_len) != DIST_OK ||
+ dist_tar_append(bag, pub_name, pub_bytes, pub_len) != DIST_OK ||
+ dist_tar_finish(bag) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: failed to build bundle");
+ goto done;
+ }
+ bag_bytes = cfree_writer_mem_bytes(bag, &bag_len);
+
+ if (pkg_write_file(ctx, out, bag_bytes, bag_len) != DIST_OK) goto done;
+ driver_printf("wrote %s (%llu bytes, %llu artifact(s), id %s)\n", out,
+ (unsigned long long)bag_len, (unsigned long long)m.n_artifacts,
+ pkgid_hex);
+ rc = 0;
+
+done:
+ if (bag) cfree_writer_close(bag);
+ if (pubw) cfree_writer_close(pubw);
+ if (sigw) cfree_writer_close(sigw);
+ if (manw) cfree_writer_close(manw);
+ if (gz) cfree_writer_close(gz);
+ if (payload) cfree_writer_close(payload);
+ if (sk_loaded) ctx->file_io->release(ctx->file_io->user, &skfd);
+ return rc;
+}
+
+/* ----------------------------------------------------- verify internals --- */
+
+/* Resolve the public key for a signature's key id. Strategy:
+ * -p PUBKEY : use that key file, ignore the store.
+ * otherwise : look the key id up in the trusted-keys store.
+ * --tofu : on a store miss, pin the bag's bundled public key.
+ * Writes the resolved key to `pk`. Returns DIST_OK on success. */
+static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx,
+ const uint8_t keyid[DIST_KEYID_LEN],
+ const DistTarEntry* pubmember,
+ const char* pubkey_opt, int tofu,
+ uint8_t pk[DIST_ED25519_PK_LEN]) {
+ char tpath[PKG_PATH_BUF];
+ CfreeFileData store = {0};
+ int have_store;
+ uint8_t kid_chk[DIST_KEYID_LEN];
+
+ if (pubkey_opt) {
+ CfreeFileData fd = {0};
+ int ok;
+ if (pkg_read_file(ctx, pubkey_opt, &fd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "cannot read public key: %s", pubkey_opt);
+ return DIST_ERR;
+ }
+ ok = dist_minisig_parse_pubkey(fd.data, fd.size, pk, kid_chk);
+ ctx->file_io->release(ctx->file_io->user, &fd);
+ if (ok != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed public key: %s", pubkey_opt);
+ return DIST_ERR;
+ }
+ if (memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) {
+ driver_errf(PKG_TOOL, "public key id does not match signature");
+ return DIST_ERR;
+ }
+ return DIST_OK;
+ }
+
+ if (pkg_trust_path(tpath, sizeof tpath) != DIST_OK) {
+ driver_errf(PKG_TOOL,
+ "no trusted-keys path (set CFREE_TRUSTED_KEYS or HOME)");
+ return DIST_ERR;
+ }
+ have_store = (pkg_read_file(ctx, tpath, &store) == DIST_OK);
+ if (have_store &&
+ dist_trust_lookup(store.data, store.size, keyid, pk) == DIST_OK) {
+ if (have_store) ctx->file_io->release(ctx->file_io->user, &store);
+ return DIST_OK;
+ }
+ if (have_store) ctx->file_io->release(ctx->file_io->user, &store);
+
+ if (!tofu) {
+ char hex[2 * DIST_KEYID_LEN + 1];
+ dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
+ driver_errf(PKG_TOOL,
+ "untrusted signer (key id %s): add it with `cfree pkg trust "
+ "add` or pass --tofu",
+ hex);
+ return DIST_ERR;
+ }
+
+ /* TOFU: pin the bundled public key. */
+ if (!pubmember) {
+ driver_errf(PKG_TOOL, "--tofu: bundle carries no public key");
+ return DIST_ERR;
+ }
+ if (dist_minisig_parse_pubkey(pubmember->data, pubmember->size, pk,
+ kid_chk) != DIST_OK) {
+ driver_errf(PKG_TOOL, "--tofu: malformed bundled public key");
+ return DIST_ERR;
+ }
+ if (memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) {
+ driver_errf(PKG_TOOL, "--tofu: bundled key id does not match signature");
+ return DIST_ERR;
+ }
+ {
+ char line[DIST_TRUST_LINE_MAX];
+ char parent[PKG_PATH_BUF];
+ char label[PKG_NAME_BUF];
+ CfreeFileData old = {0};
+ CfreeWriter* w = NULL;
+ int had_old = (pkg_read_file(ctx, tpath, &old) == DIST_OK);
+ char hex[2 * DIST_KEYID_LEN + 1];
+ int rc = DIST_ERR;
+
+ dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
+ snprintf(label, sizeof label, "tofu-pinned");
+ if (dist_trust_format_entry(line, sizeof line, keyid, pk, label) !=
+ DIST_OK) {
+ if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
+ return DIST_ERR;
+ }
+ pkg_parent_dir(tpath, parent, sizeof parent);
+ if (parent[0]) driver_mkdir_p(env, parent);
+ if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) == CFREE_OK) {
+ int ok = 1;
+ if (had_old && old.size)
+ ok = cfree_writer_write(w, old.data, old.size) == CFREE_OK;
+ if (ok) ok = cfree_writer_write(w, line, strlen(line)) == CFREE_OK;
+ if (ok && cfree_writer_status(w) == CFREE_OK) rc = DIST_OK;
+ cfree_writer_close(w);
+ }
+ if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
+ if (rc == DIST_OK)
+ driver_printf("pkg: tofu-pinned key id %s to %s\n", hex, tpath);
+ else
+ driver_errf(PKG_TOOL, "failed to pin key to %s", tpath);
+ return rc;
+ }
+}
+
+/* Verify signature + hashes for a parsed bundle. On success fills `m` (parsed
+ * manifest) and `archive` (the payload member). Returns DIST_OK. */
+static int pkg_verify_bundle(DriverEnv* env, const CfreeContext* ctx,
+ const DistTarEntry* e, size_t ne, DistManifest* m,
+ const DistTarEntry** archive_out,
+ const char* pubkey_opt, int tofu, int quiet) {
+ const DistTarEntry *man, *sig, *pub, *archive;
+ char err[128];
+ uint8_t keyid[DIST_KEYID_LEN];
+ uint8_t pk[DIST_ED25519_PK_LEN];
+ char trusted[DIST_TRUSTED_COMMENT_MAX];
+ uint8_t arc_hash[DIST_SHA256_LEN];
+ char pkgid_hex[2 * DIST_SHA256_LEN + 1];
+ uint8_t pkgid[DIST_SHA256_LEN];
+ const char* pidp;
+
+ man = pkg_find_suffix(e, ne, PKG_SUFFIX_MANIFEST);
+ sig = pkg_find_suffix(e, ne, PKG_SUFFIX_SIG);
+ pub = pkg_find_suffix(e, ne, PKG_SUFFIX_PUBKEY);
+ if (!man || !sig) {
+ driver_errf(PKG_TOOL, "bundle missing manifest or signature");
+ return DIST_ERR;
+ }
+
+ if (dist_manifest_parse(man->data, man->size, m, err, sizeof err) !=
+ DIST_OK) {
+ driver_errf(PKG_TOOL, "manifest: %s", err);
+ return DIST_ERR;
+ }
+
+ if (dist_minisig_sig_keyid(sig->data, sig->size, keyid) != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed signature");
+ return DIST_ERR;
+ }
+ if (pkg_resolve_key(env, ctx, keyid, pub, pubkey_opt, tofu, pk) != DIST_OK)
+ return DIST_ERR;
+
+ if (dist_minisig_verify(sig->data, sig->size, man->data, man->size, pk,
+ trusted, sizeof trusted) != DIST_OK) {
+ driver_errf(PKG_TOOL, "signature verification FAILED");
+ return DIST_ERR;
+ }
+
+ /* The signed trusted comment must vouch for this manifest's id. */
+ dist_sha256(pkgid, man->data, man->size);
+ dist_hex_encode(pkgid_hex, pkgid, DIST_SHA256_LEN);
+ pidp = strstr(trusted, "pkgid=");
+ if (!pidp || strncmp(pidp + 6, pkgid_hex, 2 * DIST_SHA256_LEN) != 0) {
+ driver_errf(PKG_TOOL, "trusted comment does not match package id");
+ return DIST_ERR;
+ }
+
+ /* Archive integrity (checked before any decompression downstream). */
+ archive = pkg_find_name(e, ne, m->archive);
+ if (!archive) {
+ driver_errf(PKG_TOOL, "bundle missing archive: %s", m->archive);
+ return DIST_ERR;
+ }
+ if (archive->size != m->archive_size) {
+ driver_errf(PKG_TOOL, "archive size mismatch");
+ return DIST_ERR;
+ }
+ dist_sha256(arc_hash, archive->data, archive->size);
+ if (memcmp(arc_hash, m->archive_sha256, DIST_SHA256_LEN) != 0) {
+ driver_errf(PKG_TOOL, "archive sha256 mismatch");
+ return DIST_ERR;
+ }
+
+ if (!quiet) {
+ char idhex[2 * DIST_KEYID_LEN + 1];
+ dist_hex_encode(idhex, keyid, DIST_KEYID_LEN);
+ driver_printf("ok: %s %s signer %s [%s]\n", m->name, m->version, idhex,
+ trusted);
+ }
+ *archive_out = archive;
+ return DIST_OK;
+}
+
+/* ------------------------------------------------------ verify / unpack --- */
+
+static int pkg_load_bundle(const CfreeContext* ctx, const char* path,
+ CfreeFileData* fd, DistTarEntry* entries, size_t cap,
+ size_t* ne) {
+ if (pkg_read_file(ctx, path, fd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "cannot read bundle: %s", path);
+ return DIST_ERR;
+ }
+ if (dist_tar_iter(fd->data, fd->size, entries, cap, ne) != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed bundle: %s", path);
+ return DIST_ERR;
+ }
+ return DIST_OK;
+}
+
+static int pkg_verify(DriverEnv* env, const CfreeContext* ctx, int argc,
+ char** argv) {
+ const char* file = NULL;
+ const char* pubkey = NULL;
+ int tofu = 0, i, rc = 1;
+ CfreeFileData fd = {0};
+ DistTarEntry entries[DIST_MAX_FILES];
+ size_t ne = 0;
+ DistManifest m;
+ const DistTarEntry* archive;
+
+ for (i = 0; i < argc; ++i) {
+ if (driver_streq(argv[i], "-p") && i + 1 < argc) {
+ pubkey = argv[++i];
+ } else if (driver_streq(argv[i], "--tofu")) {
+ tofu = 1;
+ } else if (argv[i][0] != '-') {
+ file = argv[i];
+ } else {
+ driver_errf(PKG_TOOL, "verify: unknown option: %s", argv[i]);
+ return 2;
+ }
+ }
+ if (!file) {
+ driver_errf(PKG_TOOL, "verify: FILE.cfpkg is required");
+ return 2;
+ }
+ if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK)
+ goto done;
+ if (pkg_verify_bundle(env, ctx, entries, ne, &m, &archive, pubkey, tofu, 0) ==
+ DIST_OK)
+ rc = 0;
+done:
+ if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
+ return rc;
+}
+
+static int pkg_unpack(DriverEnv* env, const CfreeContext* ctx, int argc,
+ char** argv) {
+ const char* file = NULL;
+ const char* dir = ".";
+ const char* pubkey = NULL;
+ int tofu = 0, i, rc = 1;
+ CfreeFileData fd = {0};
+ DistTarEntry entries[DIST_MAX_FILES];
+ size_t ne = 0;
+ DistManifest m;
+ const DistTarEntry* archive;
+ CfreeWriter* inflated = NULL;
+ DistTarEntry payload[DIST_MAX_FILES];
+ size_t np = 0;
+ const uint8_t* pbytes;
+ size_t plen;
+
+ for (i = 0; i < argc; ++i) {
+ if (driver_streq(argv[i], "-C") && i + 1 < argc) {
+ dir = argv[++i];
+ } else if (driver_streq(argv[i], "-p") && i + 1 < argc) {
+ pubkey = argv[++i];
+ } else if (driver_streq(argv[i], "--tofu")) {
+ tofu = 1;
+ } else if (argv[i][0] != '-') {
+ file = argv[i];
+ } else {
+ driver_errf(PKG_TOOL, "unpack: unknown option: %s", argv[i]);
+ return 2;
+ }
+ }
+ if (!file) {
+ driver_errf(PKG_TOOL, "unpack: FILE.cfpkg is required");
+ return 2;
+ }
+ if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK)
+ goto done;
+ if (pkg_verify_bundle(env, ctx, entries, ne, &m, &archive, pubkey, tofu, 1) !=
+ DIST_OK)
+ goto done;
+
+ /* Verified: decompress the payload and extract its members. */
+ inflated = pkg_mem(ctx);
+ if (!inflated ||
+ dist_gz_decompress(inflated, archive->data, archive->size) != DIST_OK) {
+ driver_errf(PKG_TOOL, "failed to decompress payload");
+ goto done;
+ }
+ pbytes = cfree_writer_mem_bytes(inflated, &plen);
+ if (dist_tar_iter(pbytes, plen, payload, DIST_MAX_FILES, &np) != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed payload archive");
+ goto done;
+ }
+
+ for (i = 0; i < (int)np; ++i) {
+ const DistTarEntry* pe = &payload[i];
+ char full[PKG_PATH_BUF];
+ char parent[PKG_PATH_BUF];
+ uint8_t h[DIST_SHA256_LEN];
+ size_t k;
+ const DistArtifact* a = NULL;
+ for (k = 0; k < m.n_artifacts; ++k) {
+ if (driver_streq(m.artifacts[k].path, pe->name)) {
+ a = &m.artifacts[k];
+ break;
+ }
+ }
+ if (!a) {
+ driver_errf(PKG_TOOL, "payload member not in manifest: %s", pe->name);
+ goto done;
+ }
+ dist_sha256(h, pe->data, pe->size);
+ if (pe->size != a->size || memcmp(h, a->sha256, DIST_SHA256_LEN) != 0) {
+ driver_errf(PKG_TOOL, "file hash mismatch: %s", pe->name);
+ goto done;
+ }
+ snprintf(full, sizeof full, "%s/%s", dir, pe->name);
+ pkg_parent_dir(full, parent, sizeof parent);
+ if (parent[0]) driver_mkdir_p(env, parent);
+ if (pkg_write_file(ctx, full, pe->data, pe->size) != DIST_OK) goto done;
+ driver_printf(" extracted %s\n", full);
+ }
+ driver_printf("unpacked %s %s to %s\n", m.name, m.version, dir);
+ rc = 0;
+
+done:
+ if (inflated) cfree_writer_close(inflated);
+ if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
+ return rc;
+}
+
+static int pkg_inspect(const CfreeContext* ctx, int argc, char** argv) {
+ const char* file = (argc > 0) ? argv[0] : NULL;
+ CfreeFileData fd = {0};
+ DistTarEntry entries[DIST_MAX_FILES];
+ size_t ne = 0;
+ const DistTarEntry* man;
+ int rc = 1;
+
+ if (!file) {
+ driver_errf(PKG_TOOL, "inspect: FILE.cfpkg is required");
+ return 2;
+ }
+ if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK)
+ goto done;
+ man = pkg_find_suffix(entries, ne, PKG_SUFFIX_MANIFEST);
+ if (!man) {
+ driver_errf(PKG_TOOL, "bundle has no manifest");
+ goto done;
+ }
+ driver_printf("%.*s", (int)man->size, (const char*)man->data);
+ rc = 0;
+done:
+ if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
+ return rc;
+}
+
+/* --------------------------------------------------------------- trust --- */
+
+static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
+ char** argv) {
+ char tpath[PKG_PATH_BUF];
+ const char* sub = (argc > 0) ? argv[0] : "list";
+
+ if (pkg_trust_path(tpath, sizeof tpath) != DIST_OK) {
+ driver_errf(PKG_TOOL,
+ "no trusted-keys path (set CFREE_TRUSTED_KEYS or HOME)");
+ return 1;
+ }
+
+ if (driver_streq(sub, "list")) {
+ CfreeFileData fd = {0};
+ if (pkg_read_file(ctx, tpath, &fd) != DIST_OK) {
+ driver_printf("(no trusted keys at %s)\n", tpath);
+ return 0;
+ }
+ driver_printf("%.*s", (int)fd.size, (const char*)fd.data);
+ ctx->file_io->release(ctx->file_io->user, &fd);
+ return 0;
+ }
+
+ if (driver_streq(sub, "add")) {
+ const char* pubkey = (argc > 1) ? argv[1] : NULL;
+ const char* label = (argc > 2) ? argv[2] : "";
+ CfreeFileData kf = {0}, old = {0};
+ uint8_t pk[DIST_ED25519_PK_LEN], keyid[DIST_KEYID_LEN],
+ dummy[DIST_ED25519_PK_LEN];
+ char line[DIST_TRUST_LINE_MAX], parent[PKG_PATH_BUF];
+ int had_old, ok = 1, rc = 1;
+ CfreeWriter* w = NULL;
+
+ if (!pubkey) {
+ driver_errf(PKG_TOOL, "trust add: PUBKEY is required");
+ return 2;
+ }
+ if (pkg_read_file(ctx, pubkey, &kf) != DIST_OK) {
+ driver_errf(PKG_TOOL, "cannot read public key: %s", pubkey);
+ return 1;
+ }
+ ok = dist_minisig_parse_pubkey(kf.data, kf.size, pk, keyid) == DIST_OK;
+ ctx->file_io->release(ctx->file_io->user, &kf);
+ if (!ok) {
+ driver_errf(PKG_TOOL, "malformed public key: %s", pubkey);
+ return 1;
+ }
+ had_old = (pkg_read_file(ctx, tpath, &old) == DIST_OK);
+ if (had_old &&
+ dist_trust_lookup(old.data, old.size, keyid, dummy) == DIST_OK) {
+ char hex[2 * DIST_KEYID_LEN + 1];
+ dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
+ driver_printf("key id %s already trusted\n", hex);
+ ctx->file_io->release(ctx->file_io->user, &old);
+ return 0;
+ }
+ if (dist_trust_format_entry(line, sizeof line, keyid, pk, label) !=
+ DIST_OK) {
+ if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
+ driver_errf(PKG_TOOL, "label too long");
+ return 1;
+ }
+ pkg_parent_dir(tpath, parent, sizeof parent);
+ if (parent[0]) driver_mkdir_p(env, parent);
+ if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) == CFREE_OK) {
+ ok = 1;
+ if (had_old && old.size)
+ ok = cfree_writer_write(w, old.data, old.size) == CFREE_OK;
+ if (ok) ok = cfree_writer_write(w, line, strlen(line)) == CFREE_OK;
+ if (ok && cfree_writer_status(w) == CFREE_OK) rc = 0;
+ cfree_writer_close(w);
+ }
+ if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
+ if (rc == 0) {
+ char hex[2 * DIST_KEYID_LEN + 1];
+ dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
+ driver_printf("trusted key id %s (%s)\n", hex, tpath);
+ } else {
+ driver_errf(PKG_TOOL, "failed to write %s", tpath);
+ }
+ return rc;
+ }
+
+ if (driver_streq(sub, "remove")) {
+ const char* idhex = (argc > 1) ? argv[1] : NULL;
+ CfreeFileData old = {0};
+ uint8_t want[DIST_KEYID_LEN];
+ CfreeWriter* w = NULL;
+ size_t pos = 0;
+ int rc = 1, removed = 0;
+
+ if (!idhex || strlen(idhex) != 2 * DIST_KEYID_LEN ||
+ dist_hex_decode(want, idhex, DIST_KEYID_LEN) != DIST_OK) {
+ driver_errf(PKG_TOOL, "trust remove: a 16-hex-char KEYID is required");
+ return 2;
+ }
+ if (pkg_read_file(ctx, tpath, &old) != DIST_OK) {
+ driver_printf("(no trusted keys at %s)\n", tpath);
+ return 0;
+ }
+ if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) != CFREE_OK) {
+ driver_errf(PKG_TOOL, "cannot rewrite %s", tpath);
+ ctx->file_io->release(ctx->file_io->user, &old);
+ return 1;
+ }
+ rc = 0;
+ while (pos < old.size) {
+ size_t start = pos, end = pos;
+ uint8_t got[DIST_KEYID_LEN];
+ char idbuf[2 * DIST_KEYID_LEN + 1];
+ int keep = 1;
+ while (end < old.size && old.data[end] != '\n') ++end;
+ /* Drop the line if its first token is the target key id. */
+ if (end - start >= 2 * DIST_KEYID_LEN) {
+ memcpy(idbuf, old.data + start, 2 * DIST_KEYID_LEN);
+ idbuf[2 * DIST_KEYID_LEN] = '\0';
+ if (dist_hex_decode(got, idbuf, DIST_KEYID_LEN) == DIST_OK &&
+ memcmp(got, want, DIST_KEYID_LEN) == 0) {
+ keep = 0;
+ removed = 1;
+ }
+ }
+ if (keep) {
+ size_t n = (end < old.size ? end + 1 : end) - start;
+ if (cfree_writer_write(w, old.data + start, n) != CFREE_OK) rc = 1;
+ }
+ pos = (end < old.size) ? end + 1 : end;
+ }
+ if (cfree_writer_status(w) != CFREE_OK) rc = 1;
+ cfree_writer_close(w);
+ ctx->file_io->release(ctx->file_io->user, &old);
+ if (rc == 0)
+ driver_printf(removed ? "removed key id %s\n" : "key id %s not found\n",
+ idhex);
+ else
+ driver_errf(PKG_TOOL, "failed to rewrite %s", tpath);
+ return rc;
+ }
+
+ driver_errf(PKG_TOOL, "trust: unknown subcommand: %s", sub);
+ return 2;
+}
+
+/* --------------------------------------------------------------- entry --- */
+
+int driver_pkg(int argc, char** argv) {
+ DriverEnv env;
+ CfreeContext ctx;
+ const char* sub;
+ int rc;
+
+ if (driver_argv_wants_help(argc, argv, 1) || argc < 2) {
+ driver_help_pkg();
+ return argc < 2 ? 2 : 0;
+ }
+ sub = argv[1];
+
+ driver_env_init(&env);
+ ctx = driver_env_to_context(&env);
+
+ if (driver_streq(sub, "keygen")) {
+ rc = pkg_keygen(&env, &ctx, argc - 2, argv + 2);
+ } else if (driver_streq(sub, "create")) {
+ rc = pkg_create(&env, &ctx, argc - 2, argv + 2);
+ } else if (driver_streq(sub, "verify")) {
+ rc = pkg_verify(&env, &ctx, argc - 2, argv + 2);
+ } else if (driver_streq(sub, "unpack")) {
+ rc = pkg_unpack(&env, &ctx, argc - 2, argv + 2);
+ } else if (driver_streq(sub, "inspect")) {
+ rc = pkg_inspect(&ctx, argc - 2, argv + 2);
+ } else if (driver_streq(sub, "trust")) {
+ rc = pkg_trust(&env, &ctx, argc - 2, argv + 2);
+ } else {
+ driver_errf(PKG_TOOL, "unknown subcommand: %s", sub);
+ rc = 2;
+ }
+
+ driver_env_fini(&env);
+ return rc;
+}
diff --git a/include/cfree/config.h b/include/cfree/config.h
@@ -91,5 +91,6 @@
#define CFREE_TOOL_SIZE_ENABLED 1
#define CFREE_TOOL_ADDR2LINE_ENABLED 1
#define CFREE_TOOL_STRINGS_ENABLED 1
+#define CFREE_TOOL_PKG_ENABLED 1
#endif /* CFREE_CONFIG_H */
diff --git a/mk/config.mk b/mk/config.mk
@@ -49,3 +49,4 @@ CFREE_TOOL_NM_ENABLED := $(call cfg_flag,CFREE_TOOL_NM_ENABLED)
CFREE_TOOL_SIZE_ENABLED := $(call cfg_flag,CFREE_TOOL_SIZE_ENABLED)
CFREE_TOOL_ADDR2LINE_ENABLED := $(call cfg_flag,CFREE_TOOL_ADDR2LINE_ENABLED)
CFREE_TOOL_STRINGS_ENABLED := $(call cfg_flag,CFREE_TOOL_STRINGS_ENABLED)
+CFREE_TOOL_PKG_ENABLED := $(call cfg_flag,CFREE_TOOL_PKG_ENABLED)