kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
MMakefile | 8++++++++
Adoc/DISTRIBUTE.md | 387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/b64.c | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/b64.h | 23+++++++++++++++++++++++
Adriver/dist/blake2b.c | 29+++++++++++++++++++++++++++++
Adriver/dist/blake2b.h | 17+++++++++++++++++
Adriver/dist/deflate.c | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/deflate.h | 25+++++++++++++++++++++++++
Adriver/dist/dist.c | 41+++++++++++++++++++++++++++++++++++++++++
Adriver/dist/dist.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/ed25519.c | 46++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/ed25519.h | 31+++++++++++++++++++++++++++++++
Adriver/dist/manifest.c | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/manifest.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/minisig.c | 265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/minisig.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/sha256.c | 30++++++++++++++++++++++++++++++
Adriver/dist/sha256.h | 18++++++++++++++++++
Adriver/dist/tar.c | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/tar.h | 35+++++++++++++++++++++++++++++++++++
Adriver/dist/trust.c | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/trust.h | 29+++++++++++++++++++++++++++++
Mdriver/driver.h | 2++
Mdriver/env.h | 6++++++
Mdriver/env/posix.c | 27+++++++++++++++++++++++++--
Mdriver/env/windows.c | 48+++++++++++++++++++++++++++++++-----------------
Mdriver/main.c | 4++++
Adriver/pkg.c | 935+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/cfree/config.h | 1+
Mmk/config.mk | 1+
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)