kit

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

commit 56f95449e7d0bcdd0570ee1898933546cfa4e54c
parent 5704791f29736c06d932c385e746f32c481cbb44
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 28 May 2026 17:47:57 -0700

Implement v2 package distribution

Diffstat:
MMakefile | 3++-
Mdoc/DISTRIBUTE.md | 550++++++++++++++++++++++++++++++++-----------------------------------------------
Mdriver/dist/blake2b.c | 30++++++++++++++++++++++++------
Mdriver/dist/blake2b.h | 14++++++++++++--
Adriver/dist/cfpkg.c | 419+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/dist/cfpkg.h | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/dist/dist.h | 2+-
Mdriver/dist/ed25519.c | 18++++++++----------
Adriver/dist/lz4.c | 28++++++++++++++++++++++++++++
Adriver/dist/lz4.h | 17+++++++++++++++++
Mdriver/dist/manifest.c | 137++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mdriver/dist/manifest.h | 27++++++++++-----------------
Ddriver/dist/sha256.c | 30------------------------------
Ddriver/dist/sha256.h | 18------------------
Mdriver/pkg.c | 1278++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
15 files changed, 1680 insertions(+), 982 deletions(-)

diff --git a/Makefile b/Makefile @@ -349,9 +349,10 @@ 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_TOOL_SRCS += driver/dist/dist.c driver/dist/b64.c \ driver/dist/blake2b.c driver/dist/ed25519.c \ driver/dist/tar.c driver/dist/deflate.c \ + driver/dist/lz4.c driver/dist/cfpkg.c \ driver/dist/manifest.c driver/dist/minisig.c \ driver/dist/trust.c endif diff --git a/doc/DISTRIBUTE.md b/doc/DISTRIBUTE.md @@ -1,387 +1,281 @@ # 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. +cfree distribution v2 produces signed, self-describing code packages with one +canonical logical format and two physical representations: + +- `.tar.gz` for portable archive tooling, +- `.cfpkg` for native chunked verification and future streaming. + +There is no network transport or dependency resolution in cfree. Existing tools +move package bytes; cfree creates, verifies, inspects, and unpacks them. + +## Trust and identity + +The signed object is always the logical manifest's literal byte stream. The +package id is: + +``` +package-id = BLAKE2b-512(logical manifest literal bytes) +``` + +The detached minisign signature covers the manifest. Its trusted comment also +contains `pkgid=<hex package-id>`, and verification rejects the package if that +signed value does not match the recomputed manifest hash. + +Trust anchors are public keys in the trusted-keys file: + +- `$CFREE_TRUSTED_KEYS`, if set, +- otherwise `$HOME/.config/cfree/trusted_keys`. + +Each line is: + +``` +<keyid-hex> <pubkey-base64> <label> +``` + +A bundled `.pub` is never trust by itself. It is only a TOFU candidate. With +`--tofu`, cfree pins the bundled public key after confirming its key id matches +the signature's key id. Without TOFU or `-p PUBKEY`, unknown signers fail. ## 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`). +The driver-side distribution subsystem uses BLAKE2b throughout. SHA-256 is not +part of the v2 package format. -| Primitive | Purpose | Notes | +| Primitive | Purpose | Status | |---|---|---| -| **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 | +| tar | portable archive container | real | +| gzip/DEFLATE | portable compression | gzip/stored blocks real; compressed DEFLATE stubbed | +| BLAKE2b | package id, whole-file hashes, Merkle hashing, minisign prehash | API real; math stubbed | +| Ed25519 | minisign signature scheme | API real; math stubbed | +| base64 | minisign key/signature text | real | +| LZ4 block | native chunk compression | API real; implementation stubbed | -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 stubs are deterministic and insecure. They let the package pipeline run +end-to-end now; replacing the stub bodies with real vendored primitives should +not require format or caller changes. -## The package: three canonical files, one bundle +## Logical manifest -A package is canonically **three files**: +The logical manifest is INI-style, byte-stable, and strict. Comments and blank +lines are signed bytes. Unknown keys, unknown sections, and unknown versions are +errors. -| 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 | +Example: -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. +```ini +cfree-package 2 +name = hello +version = 0.3.1 +description = minimal greeting program +hash = blake2b-merkle-v1 -Because moving three loose files by hand is clunky and transport is out -of scope, the **distribution unit is a single-file bundle**: +[artifact] +id = 0 +path = bin/hello +kind = exe +size = 16384 +blake2b = <whole-file-blake2b-512> +root = <artifact-merkle-root> +entry = true -``` -<name>-<version>.cfpkg = uncompressed tar of { manifest, manifest.minisig, tar.gz, .pub } +[dependency] +name = libfoo +version = >=1.2.0 +blake2b = <dependency-package-id> +key = <expected-signer-keyid> ``` -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: +Top-level fields: | 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 | +| `name` | yes | package name | +| `version` | yes | version string | | `description` | no | one-line free text | +| `hash` | yes | currently `blake2b-merkle-v1` | -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): +`[artifact]` fields: | 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 | +| `id` | yes | numeric artifact id, unique in the manifest | +| `path` | yes | unpacked artifact path | +| `kind` | yes | `exe`, `dso`, `obj`, `wasm`, `lib`, `data`, or `source` | +| `size` | yes | uncompressed byte length | +| `blake2b` | yes | BLAKE2b-512 of the whole artifact bytes | +| `root` | yes | artifact Merkle root | +| `target` | no | cfree target triple | | `entry` | no | `true` if runnable under jit/emu/wasm | -`[dependency]` block (repeatable; **reserved** — see Scope): +`[dependency]` fields are validated but not resolved: | 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 | +| `version` | yes | version constraint | +| `blake2b` | no | dependency package id | +| `key` | no | expected signer key id | -v1 tooling validates the `[dependency]` schema but performs no -resolution. +## Merkle tree -### Target triples +Artifacts are split into fixed 64 KiB raw chunks. The final chunk may be +shorter. The tree is deterministic and deliberately simple: -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. +``` +leaf = BLAKE2b("cfpkg2 leaf v1" || artifact-id || chunk-index || + raw-size || raw-bytes) +node = BLAKE2b("cfpkg2 node v1" || left-hash || right-hash) +root = BLAKE2b("cfpkg2 root v1" || "artifact" || top-hash) +``` -### Example +At each level, adjacent hashes are paired left-to-right. If a level has an odd +final hash, that hash is promoted unchanged to the next level. Empty artifacts +use a separate domain: -```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 +``` +root = BLAKE2b("cfpkg2 root v1" || "artifact-empty" || artifact-id || 0) +``` -[artifact] -path = bin/hello-x64 -target = x86_64-linux -kind = exe -sha256 = 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae -size = 16384 -entry = true +This avoids virtual padding, duplicated leaves, and power-of-two tree rules. +Proof verification only needs the leaf index, leaf count, sibling hashes, and +the expected root. -[artifact] -path = bin/hello-arm -target = aarch64-apple-darwin -kind = exe -sha256 = fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 -size = 16512 +## Portable `.tar.gz` + +The portable representation is a gzip-compressed tar: -[dependency] -name = libfoo -version = >=1.2.0 -sha256 = a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00 ``` +hello-0.3.1.tar.gz + cfree/package.manifest + cfree/package.manifest.minisig + cfree/package.pub + bin/hello + share/data.txt +``` + +Verification: + +1. Decompress and parse the tar container. +2. Read `cfree/package.manifest` and `cfree/package.manifest.minisig`. +3. Anchor and verify the manifest signature. +4. Parse the logical manifest. +5. Verify every artifact member's whole-file BLAKE2b and Merkle root. -## Identity and content addressing +This format is not optimized for seeking. It is meant to interoperate with +ordinary archive tooling. -`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: +## Native `.cfpkg` + +The native representation is a chunked binary container: ``` -signer signs manifest → sha256(manifest) is the package id -manifest pins archive → archive unpacks to per-file hash-checked tree +fixed header +logical manifest bytes +manifest minisign bytes +encoding descriptor bytes +encoding descriptor minisign bytes +bundled pubkey bytes +binary chunk index +aligned content region ``` -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 +The fixed header is trust-neutral. It locates the early byte ranges only: ``` -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 <───────┘ +magic = "cfpkg2\0" +version = 2 +header-size +manifest-offset / manifest-size +signature-offset / signature-size +descriptor-offset / descriptor-size +descriptor-signature-offset / descriptor-signature-size +pubkey-offset / pubkey-size +index-offset / index-size +content-offset / content-size +alignment +chunk-size ``` -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**. +Trust starts at the verified logical manifest. Layout trust starts at the +verified encoding descriptor, which is signed by the same trusted key as the +manifest: -## CLI +```ini +cfree-encoding 2 +package-id = <BLAKE2b-512 of logical manifest> +format = cfpkg +hash = blake2b-merkle-v1 +index-offset = 1024 +index-size = 240 +index-root = <authenticated index region root> +content-offset = 1264 +content-size = 16384 +content-root = <authenticated content region root> +chunk-size = 65536 +alignment = 16 +``` + +Each binary index record is fixed-size and little-endian: + +``` +artifact-id u64 +chunk-index u64 +content-offset u64 # relative to the content region +stored-size u64 +raw-size u64 +compression u32 # 0 = none, 1 = lz4-block-v1 +reserved u32 +stored-hash BLAKE2b-512 +raw-hash BLAKE2b-512 +leaf-hash BLAKE2b-512 +``` + +Native verification: -Distribution slots into the existing binutils-style multitool -(`cfree ar`, `cfree nm`, `cfree objdump`, `cfree ld`, …) as a single -`pkg` tool with subcommands: +1. Read the fixed header. +2. Verify the logical manifest signature. +3. Verify the encoding descriptor signature with the same key. +4. Confirm descriptor `package-id`, offsets, sizes, roots, chunk size, and + alignment match the container. +5. Verify stored chunk hashes before decoding. +6. Decode each chunk (`none` is implemented; `lz4-block-v1` is recognized but + stubbed until LZ4 is vendored). +7. Verify raw chunk hashes, leaf hashes, artifact roots, and whole-file + BLAKE2b hashes. + +## CLI ``` -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 keygen -o BASE +cfree pkg create --name N --version V [--desc D] -s SECKEY \ + [--format cfpkg|tar.gz] [--compression none|lz4-block-v1] \ + -o OUT FILE... +cfree pkg verify [-p PUBKEY | --tofu] [--format cfpkg|tar.gz] FILE +cfree pkg unpack FILE -C DIR +cfree pkg inspect FILE 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. +`create` infers the physical representation from `-o`: `.cfpkg` is native, +`.tar.gz` is portable. `--format` overrides inference. ## 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. +Implemented: + +- v2 logical manifest parser/emitter, +- portable `.tar.gz` create/verify/unpack/inspect, +- native `.cfpkg` create/verify/unpack/inspect, +- minisign-compatible key/signature file layout, +- trusted-keys store and opt-in TOFU, +- BLAKE2b streaming API and package Merkle helpers, +- raw LZ4 block API stubs, +- driver-side SHA-256 removal for distribution. + +Still stubbed/insecure: + +- `driver/dist/blake2b.c`, +- `driver/dist/ed25519.c`, +- compressed DEFLATE support in `driver/dist/deflate.c`, +- raw LZ4 block compression/decompression in `driver/dist/lz4.c`. diff --git a/driver/dist/blake2b.c b/driver/dist/blake2b.c @@ -1,7 +1,7 @@ #include "blake2b.h" -/* STUB digest. See blake2b.h. Same construction as the sha256 stub with a - * different domain constant, producing a 64-byte output. */ +/* STUB digest. See blake2b.h. A counter-driven FNV/xorshift sponge: enough + * avalanche to distinguish inputs in tests, but cryptographically worthless. */ #define DIST_B2_FNV_OFFSET 0x100000001b3cbf29ULL #define DIST_B2_FNV_PRIME 0x00000100000001b3ULL @@ -16,14 +16,32 @@ static uint64_t b2_absorb(const uint8_t* data, size_t len, uint64_t h) { 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); +void dist_blake2b_init(DistBlake2b* s, size_t out_len) { + s->h = DIST_B2_FNV_OFFSET ^ (uint64_t)out_len; + s->len = 0; + s->out_len = out_len; +} + +void dist_blake2b_update(DistBlake2b* s, const uint8_t* data, size_t len) { + s->h = b2_absorb(data, len, s->h); + s->len += (uint64_t)len; +} + +void dist_blake2b_final(DistBlake2b* s, uint8_t* out) { + uint64_t base = s->h ^ (s->len * 0x9e3779b97f4a7c15ULL); size_t i; - for (i = 0; i < DIST_BLAKE2B_LEN; ++i) { + for (i = 0; i < s->out_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); } } + +void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, + size_t len) { + DistBlake2b s; + dist_blake2b_init(&s, DIST_BLAKE2B_LEN); + dist_blake2b_update(&s, data, len); + dist_blake2b_final(&s, out); +} diff --git a/driver/dist/blake2b.h b/driver/dist/blake2b.h @@ -6,11 +6,21 @@ #include "dist.h" -/* minisign prehash. Real minisign hashes the signed file with BLAKE2b-512 and - * signs the 64-byte digest. +/* minisign prehash and v2 package/content hash. 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. */ +typedef struct DistBlake2b { + uint64_t h; + uint64_t len; + size_t out_len; +} DistBlake2b; + +void dist_blake2b_init(DistBlake2b* s, size_t out_len); +void dist_blake2b_update(DistBlake2b* s, const uint8_t* data, size_t len); +void dist_blake2b_final(DistBlake2b* s, uint8_t* out); + void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, size_t len); diff --git a/driver/dist/cfpkg.c b/driver/dist/cfpkg.c @@ -0,0 +1,419 @@ +#include "cfpkg.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "blake2b.h" + +#define DESC_LINE_MAX 1024u + +static void put_u32le(uint8_t* p, uint32_t v) { + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static void put_u64le(uint8_t* p, uint64_t v) { + unsigned i; + for (i = 0; i < 8u; ++i) p[i] = (uint8_t)(v >> (8u * i)); +} + +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); +} + +static uint64_t get_u64le(const uint8_t* p) { + uint64_t v = 0; + unsigned i; + for (i = 0; i < 8u; ++i) v |= ((uint64_t)p[i]) << (8u * i); + return v; +} + +static void hash_u64(DistBlake2b* h, uint64_t v) { + uint8_t b[8]; + put_u64le(b, v); + dist_blake2b_update(h, b, sizeof b); +} + +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[DESC_LINE_MAX]; + snprintf(line, sizeof line, "%s = %s\n", key, val); + return emit(out, line); +} + +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); +} + +static int emit_hex(CfreeWriter* out, const char* key, const uint8_t* h) { + char hex[2 * DIST_BLAKE2B_LEN + 1]; + dist_hex_encode(hex, h, DIST_BLAKE2B_LEN); + return emit_kv(out, key, hex); +} + +void dist_cfpkg2_leaf_hash(uint8_t out[DIST_BLAKE2B_LEN], + uint64_t artifact_id, uint64_t chunk_index, + const uint8_t* raw, size_t raw_len) { + DistBlake2b h; + static const uint8_t dom[] = "cfpkg2 leaf v1"; + dist_blake2b_init(&h, DIST_BLAKE2B_LEN); + dist_blake2b_update(&h, dom, sizeof dom - 1); + hash_u64(&h, artifact_id); + hash_u64(&h, chunk_index); + hash_u64(&h, (uint64_t)raw_len); + dist_blake2b_update(&h, raw, raw_len); + dist_blake2b_final(&h, out); +} + +void dist_cfpkg2_node_hash(uint8_t out[DIST_BLAKE2B_LEN], + const uint8_t left[DIST_BLAKE2B_LEN], + const uint8_t right[DIST_BLAKE2B_LEN]) { + DistBlake2b h; + static const uint8_t dom[] = "cfpkg2 node v1"; + dist_blake2b_init(&h, DIST_BLAKE2B_LEN); + dist_blake2b_update(&h, dom, sizeof dom - 1); + dist_blake2b_update(&h, left, DIST_BLAKE2B_LEN); + dist_blake2b_update(&h, right, DIST_BLAKE2B_LEN); + dist_blake2b_final(&h, out); +} + +void dist_cfpkg2_root_hash(uint8_t out[DIST_BLAKE2B_LEN], const char* kind, + uint64_t artifact_id, + const uint8_t top[DIST_BLAKE2B_LEN]) { + DistBlake2b h; + static const uint8_t dom[] = "cfpkg2 root v1"; + (void)artifact_id; + dist_blake2b_init(&h, DIST_BLAKE2B_LEN); + dist_blake2b_update(&h, dom, sizeof dom - 1); + dist_blake2b_update(&h, (const uint8_t*)kind, strlen(kind)); + dist_blake2b_update(&h, top, DIST_BLAKE2B_LEN); + dist_blake2b_final(&h, out); +} + +void dist_cfpkg2_empty_artifact_root(uint8_t out[DIST_BLAKE2B_LEN], + uint64_t artifact_id) { + DistBlake2b h; + static const uint8_t dom[] = "cfpkg2 root v1"; + static const uint8_t kind[] = "artifact-empty"; + dist_blake2b_init(&h, DIST_BLAKE2B_LEN); + dist_blake2b_update(&h, dom, sizeof dom - 1); + dist_blake2b_update(&h, kind, sizeof kind - 1); + hash_u64(&h, artifact_id); + hash_u64(&h, 0); + dist_blake2b_final(&h, out); +} + +int dist_cfpkg2_artifact_root(uint8_t out[DIST_BLAKE2B_LEN], + uint64_t artifact_id, const uint8_t* raw, + size_t raw_len, size_t chunk_size) { + uint8_t level[DIST_MAX_FILES][DIST_BLAKE2B_LEN]; + size_t leaves, i; + if (chunk_size == 0) return DIST_ERR; + if (raw_len == 0) { + dist_cfpkg2_empty_artifact_root(out, artifact_id); + return DIST_OK; + } + leaves = (raw_len + chunk_size - 1u) / chunk_size; + if (leaves > DIST_MAX_FILES) return DIST_ERR; + for (i = 0; i < leaves; ++i) { + size_t off = i * chunk_size; + size_t n = raw_len - off; + if (n > chunk_size) n = chunk_size; + dist_cfpkg2_leaf_hash(level[i], artifact_id, (uint64_t)i, raw + off, n); + } + while (leaves > 1u) { + size_t outn = 0; + for (i = 0; i < leaves; i += 2u) { + if (i + 1u < leaves) + dist_cfpkg2_node_hash(level[outn], level[i], level[i + 1u]); + else + memcpy(level[outn], level[i], DIST_BLAKE2B_LEN); + ++outn; + } + leaves = outn; + } + dist_cfpkg2_root_hash(out, "artifact", artifact_id, level[0]); + return DIST_OK; +} + +int dist_cfpkg2_verify_proof(const uint8_t leaf[DIST_BLAKE2B_LEN], + uint64_t leaf_index, uint64_t leaf_count, + const uint8_t* proof, size_t proof_len, + const uint8_t root[DIST_BLAKE2B_LEN]) { + uint8_t cur[DIST_BLAKE2B_LEN], tmp[DIST_BLAKE2B_LEN]; + uint64_t idx = leaf_index, count = leaf_count; + size_t off = 0; + if (leaf_count == 0 || leaf_index >= leaf_count) return DIST_ERR; + memcpy(cur, leaf, DIST_BLAKE2B_LEN); + while (count > 1u) { + int has_pair = (idx + 1u < count) || (idx & 1u); + if (has_pair) { + if (off + DIST_BLAKE2B_LEN > proof_len) return DIST_ERR; + if (idx & 1u) + dist_cfpkg2_node_hash(tmp, proof + off, cur); + else + dist_cfpkg2_node_hash(tmp, cur, proof + off); + memcpy(cur, tmp, DIST_BLAKE2B_LEN); + off += DIST_BLAKE2B_LEN; + } + idx >>= 1; + count = (count + 1u) >> 1; + } + dist_cfpkg2_root_hash(tmp, "artifact", 0, cur); + return off == proof_len && memcmp(tmp, root, DIST_BLAKE2B_LEN) == 0 + ? DIST_OK + : DIST_ERR; +} + +int dist_cfpkg_write_header(CfreeWriter* out, const DistCfpkgHeader* h) { + uint8_t b[DIST_CFPKG_HEADER_SIZE]; + size_t off = 16u; + memset(b, 0, sizeof b); + memcpy(b, DIST_CFPKG_MAGIC, 7u); + put_u32le(b + 8u, DIST_CFPKG_VERSION); + put_u32le(b + 12u, DIST_CFPKG_HEADER_SIZE); +#define PUT(v) \ + do { \ + put_u64le(b + off, (v)); \ + off += 8u; \ + } while (0) + PUT(h->manifest_offset); + PUT(h->manifest_size); + PUT(h->signature_offset); + PUT(h->signature_size); + PUT(h->descriptor_offset); + PUT(h->descriptor_size); + PUT(h->descriptor_signature_offset); + PUT(h->descriptor_signature_size); + PUT(h->pubkey_offset); + PUT(h->pubkey_size); + PUT(h->index_offset); + PUT(h->index_size); + PUT(h->content_offset); + PUT(h->content_size); + PUT(h->alignment); + PUT(h->chunk_size); +#undef PUT + return cfree_writer_write(out, b, sizeof b) == CFREE_OK ? DIST_OK : DIST_ERR; +} + +int dist_cfpkg_read_header(const uint8_t* data, size_t len, + DistCfpkgHeader* h) { + size_t off = 16u; + if (len < DIST_CFPKG_HEADER_SIZE) return DIST_ERR; + if (memcmp(data, DIST_CFPKG_MAGIC, 7u) != 0) return DIST_ERR; + if (get_u32le(data + 8u) != DIST_CFPKG_VERSION || + get_u32le(data + 12u) != DIST_CFPKG_HEADER_SIZE) + return DIST_ERR; +#define GET(dst) \ + do { \ + (dst) = get_u64le(data + off); \ + off += 8u; \ + } while (0) + GET(h->manifest_offset); + GET(h->manifest_size); + GET(h->signature_offset); + GET(h->signature_size); + GET(h->descriptor_offset); + GET(h->descriptor_size); + GET(h->descriptor_signature_offset); + GET(h->descriptor_signature_size); + GET(h->pubkey_offset); + GET(h->pubkey_size); + GET(h->index_offset); + GET(h->index_size); + GET(h->content_offset); + GET(h->content_size); + GET(h->alignment); + GET(h->chunk_size); +#undef GET + return DIST_OK; +} + +void dist_cfpkg_encode_index_record(uint8_t out[DIST_CFPKG_INDEX_RECORD_SIZE], + const DistCfpkgIndexRecord* r) { + memset(out, 0, DIST_CFPKG_INDEX_RECORD_SIZE); + put_u64le(out + 0, r->artifact_id); + put_u64le(out + 8, r->chunk_index); + put_u64le(out + 16, r->content_offset); + put_u64le(out + 24, r->stored_size); + put_u64le(out + 32, r->raw_size); + put_u32le(out + 40, r->compression); + memcpy(out + 48, r->stored_hash, DIST_BLAKE2B_LEN); + memcpy(out + 112, r->raw_hash, DIST_BLAKE2B_LEN); + memcpy(out + 176, r->leaf_hash, DIST_BLAKE2B_LEN); +} + +int dist_cfpkg_decode_index_record(const uint8_t* data, size_t len, + DistCfpkgIndexRecord* r) { + if (len < DIST_CFPKG_INDEX_RECORD_SIZE) return DIST_ERR; + r->artifact_id = get_u64le(data + 0); + r->chunk_index = get_u64le(data + 8); + r->content_offset = get_u64le(data + 16); + r->stored_size = get_u64le(data + 24); + r->raw_size = get_u64le(data + 32); + r->compression = get_u32le(data + 40); + memcpy(r->stored_hash, data + 48, DIST_BLAKE2B_LEN); + memcpy(r->raw_hash, data + 112, DIST_BLAKE2B_LEN); + memcpy(r->leaf_hash, data + 176, DIST_BLAKE2B_LEN); + return DIST_OK; +} + +int dist_cfpkg_descriptor_emit(CfreeWriter* out, + const DistCfpkgDescriptor* d) { + if (emit(out, "cfree-encoding 2\n") != DIST_OK) return DIST_ERR; + if (emit_hex(out, "package-id", d->package_id) != DIST_OK) return DIST_ERR; + if (emit_kv(out, "format", "cfpkg") != DIST_OK) return DIST_ERR; + if (emit_kv(out, "hash", "blake2b-merkle-v1") != DIST_OK) return DIST_ERR; + if (emit_u64(out, "index-offset", d->index_offset) != DIST_OK) return DIST_ERR; + if (emit_u64(out, "index-size", d->index_size) != DIST_OK) return DIST_ERR; + if (emit_hex(out, "index-root", d->index_root) != DIST_OK) return DIST_ERR; + if (emit_u64(out, "content-offset", d->content_offset) != DIST_OK) + return DIST_ERR; + if (emit_u64(out, "content-size", d->content_size) != DIST_OK) + return DIST_ERR; + if (emit_hex(out, "content-root", d->content_root) != DIST_OK) + return DIST_ERR; + if (emit_u64(out, "chunk-size", d->chunk_size) != DIST_OK) return DIST_ERR; + return emit_u64(out, "alignment", d->alignment); +} + +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 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; +} + +int dist_cfpkg_descriptor_parse(const uint8_t* data, size_t len, + DistCfpkgDescriptor* d, char* err, + size_t errcap) { + size_t pos = 0; + int first = 1; + unsigned seen = 0; + memset(d, 0, sizeof *d); + while (pos < len) { + char buf[DESC_LINE_MAX], *t, *eq, *key, *val; + size_t end = pos, n; + 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, "cfree-encoding 2") != 0) + return set_err(err, errcap, "bad encoding descriptor magic/version"); + continue; + } + t = trim_lead(buf); + if (*t == '\0' || *t == '#') 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 (strcmp(key, "package-id") == 0) { + if (strlen(val) != 2 * DIST_BLAKE2B_LEN || + dist_hex_decode(d->package_id, val, DIST_BLAKE2B_LEN) != DIST_OK) + return set_err(err, errcap, "bad package-id"); + seen |= 1u << 0; + } else if (strcmp(key, "format") == 0) { + if (strcmp(val, "cfpkg") != 0) return set_err(err, errcap, "bad format"); + seen |= 1u << 1; + } else if (strcmp(key, "hash") == 0) { + if (strcmp(val, "blake2b-merkle-v1") != 0) + return set_err(err, errcap, "bad hash algorithm"); + seen |= 1u << 2; + } else if (strcmp(key, "index-offset") == 0) { + if (parse_u64(val, &d->index_offset) != DIST_OK) + return set_err(err, errcap, "bad index-offset"); + seen |= 1u << 3; + } else if (strcmp(key, "index-size") == 0) { + if (parse_u64(val, &d->index_size) != DIST_OK) + return set_err(err, errcap, "bad index-size"); + seen |= 1u << 4; + } else if (strcmp(key, "index-root") == 0) { + if (strlen(val) != 2 * DIST_BLAKE2B_LEN || + dist_hex_decode(d->index_root, val, DIST_BLAKE2B_LEN) != DIST_OK) + return set_err(err, errcap, "bad index-root"); + seen |= 1u << 5; + } else if (strcmp(key, "content-offset") == 0) { + if (parse_u64(val, &d->content_offset) != DIST_OK) + return set_err(err, errcap, "bad content-offset"); + seen |= 1u << 6; + } else if (strcmp(key, "content-size") == 0) { + if (parse_u64(val, &d->content_size) != DIST_OK) + return set_err(err, errcap, "bad content-size"); + seen |= 1u << 7; + } else if (strcmp(key, "content-root") == 0) { + if (strlen(val) != 2 * DIST_BLAKE2B_LEN || + dist_hex_decode(d->content_root, val, DIST_BLAKE2B_LEN) != DIST_OK) + return set_err(err, errcap, "bad content-root"); + seen |= 1u << 8; + } else if (strcmp(key, "chunk-size") == 0) { + if (parse_u64(val, &d->chunk_size) != DIST_OK) + return set_err(err, errcap, "bad chunk-size"); + seen |= 1u << 9; + } else if (strcmp(key, "alignment") == 0) { + if (parse_u64(val, &d->alignment) != DIST_OK) + return set_err(err, errcap, "bad alignment"); + seen |= 1u << 10; + } else { + return set_err(err, errcap, "unknown encoding descriptor key"); + } + } + return seen == 0x7ffu ? DIST_OK + : set_err(err, errcap, + "missing required encoding descriptor field"); +} + +const char* dist_cfpkg_compression_name(uint32_t c) { + if (c == DIST_CFPKG_COMP_NONE) return "none"; + if (c == DIST_CFPKG_COMP_LZ4_BLOCK_V1) return "lz4-block-v1"; + return NULL; +} + +int dist_cfpkg_compression_parse(const char* s, uint32_t* out) { + if (strcmp(s, "none") == 0) { + *out = DIST_CFPKG_COMP_NONE; + return DIST_OK; + } + if (strcmp(s, "lz4-block-v1") == 0) { + *out = DIST_CFPKG_COMP_LZ4_BLOCK_V1; + return DIST_OK; + } + return DIST_ERR; +} diff --git a/driver/dist/cfpkg.h b/driver/dist/cfpkg.h @@ -0,0 +1,91 @@ +#ifndef CFREE_DIST_CFPKG_H +#define CFREE_DIST_CFPKG_H + +#include <cfree/core.h> +#include <stddef.h> +#include <stdint.h> + +#include "dist.h" + +#define DIST_CFPKG_MAGIC "cfpkg2\0" +#define DIST_CFPKG_VERSION 2u +#define DIST_CFPKG_HEADER_SIZE 160u +#define DIST_CFPKG_ALIGNMENT 16u +#define DIST_CFPKG_CHUNK_SIZE_DEFAULT 65536u +#define DIST_CFPKG_INDEX_RECORD_SIZE 240u + +typedef enum DistCfpkgCompression { + DIST_CFPKG_COMP_NONE = 0, + DIST_CFPKG_COMP_LZ4_BLOCK_V1 = 1, +} DistCfpkgCompression; + +typedef struct DistCfpkgHeader { + uint64_t manifest_offset, manifest_size; + uint64_t signature_offset, signature_size; + uint64_t descriptor_offset, descriptor_size; + uint64_t descriptor_signature_offset, descriptor_signature_size; + uint64_t pubkey_offset, pubkey_size; + uint64_t index_offset, index_size; + uint64_t content_offset, content_size; + uint64_t alignment, chunk_size; +} DistCfpkgHeader; + +typedef struct DistCfpkgIndexRecord { + uint64_t artifact_id; + uint64_t chunk_index; + uint64_t content_offset; /* relative to the content region */ + uint64_t stored_size; + uint64_t raw_size; + uint32_t compression; + uint8_t stored_hash[DIST_BLAKE2B_LEN]; + uint8_t raw_hash[DIST_BLAKE2B_LEN]; + uint8_t leaf_hash[DIST_BLAKE2B_LEN]; +} DistCfpkgIndexRecord; + +typedef struct DistCfpkgDescriptor { + uint8_t package_id[DIST_BLAKE2B_LEN]; + uint64_t index_offset, index_size; + uint8_t index_root[DIST_BLAKE2B_LEN]; + uint64_t content_offset, content_size; + uint8_t content_root[DIST_BLAKE2B_LEN]; + uint64_t chunk_size, alignment; +} DistCfpkgDescriptor; + +void dist_cfpkg2_leaf_hash(uint8_t out[DIST_BLAKE2B_LEN], + uint64_t artifact_id, uint64_t chunk_index, + const uint8_t* raw, size_t raw_len); +void dist_cfpkg2_node_hash(uint8_t out[DIST_BLAKE2B_LEN], + const uint8_t left[DIST_BLAKE2B_LEN], + const uint8_t right[DIST_BLAKE2B_LEN]); +void dist_cfpkg2_root_hash(uint8_t out[DIST_BLAKE2B_LEN], const char* kind, + uint64_t artifact_id, + const uint8_t top[DIST_BLAKE2B_LEN]); +void dist_cfpkg2_empty_artifact_root(uint8_t out[DIST_BLAKE2B_LEN], + uint64_t artifact_id); +int dist_cfpkg2_artifact_root(uint8_t out[DIST_BLAKE2B_LEN], + uint64_t artifact_id, const uint8_t* raw, + size_t raw_len, size_t chunk_size); +int dist_cfpkg2_verify_proof(const uint8_t leaf[DIST_BLAKE2B_LEN], + uint64_t leaf_index, uint64_t leaf_count, + const uint8_t* proof, size_t proof_len, + const uint8_t root[DIST_BLAKE2B_LEN]); + +int dist_cfpkg_write_header(CfreeWriter* out, const DistCfpkgHeader* h); +int dist_cfpkg_read_header(const uint8_t* data, size_t len, + DistCfpkgHeader* h); + +void dist_cfpkg_encode_index_record(uint8_t out[DIST_CFPKG_INDEX_RECORD_SIZE], + const DistCfpkgIndexRecord* r); +int dist_cfpkg_decode_index_record(const uint8_t* data, size_t len, + DistCfpkgIndexRecord* r); + +int dist_cfpkg_descriptor_emit(CfreeWriter* out, + const DistCfpkgDescriptor* d); +int dist_cfpkg_descriptor_parse(const uint8_t* data, size_t len, + DistCfpkgDescriptor* d, char* err, + size_t errcap); + +const char* dist_cfpkg_compression_name(uint32_t c); +int dist_cfpkg_compression_parse(const char* s, uint32_t* out); + +#endif diff --git a/driver/dist/dist.h b/driver/dist/dist.h @@ -14,8 +14,8 @@ * Every stub is marked as such at its definition. */ /* Primitive output sizes. */ -#define DIST_SHA256_LEN 32u #define DIST_BLAKE2B_LEN 64u +#define DIST_HASH_LEN DIST_BLAKE2B_LEN #define DIST_ED25519_PK_LEN 32u #define DIST_ED25519_SK_LEN 64u #define DIST_ED25519_SIG_LEN 64u diff --git a/driver/dist/ed25519.c b/driver/dist/ed25519.c @@ -2,7 +2,7 @@ #include <string.h> -#include "sha256.h" +#include "blake2b.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 @@ -21,15 +21,13 @@ void dist_ed25519_keypair(uint8_t pk[DIST_ED25519_PK_LEN], 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] */ + DistBlake2b h; + static const uint8_t dom[] = "cfree stub ed25519"; + dist_blake2b_init(&h, DIST_ED25519_SIG_LEN); + dist_blake2b_update(&h, dom, sizeof dom - 1); + dist_blake2b_update(&h, seed, DIST_ED25519_SEED_LEN); + dist_blake2b_update(&h, msg, msglen); + dist_blake2b_final(&h, sig); } void dist_ed25519_sign(uint8_t sig[DIST_ED25519_SIG_LEN], const uint8_t* msg, diff --git a/driver/dist/lz4.c b/driver/dist/lz4.c @@ -0,0 +1,28 @@ +#include "lz4.h" + +/* STUB: raw LZ4 block support is an external vendored primitive. The API is + * real so the package format and callers will not change when the + * implementation lands. */ + +size_t dist_lz4_compress_bound(size_t raw_len) { + return raw_len + raw_len / 255u + 16u; +} + +int dist_lz4_compress_block(uint8_t* dst, size_t dst_cap, size_t* dst_len, + const uint8_t* src, size_t src_len) { + (void)dst; + (void)dst_cap; + (void)dst_len; + (void)src; + (void)src_len; + return DIST_ERR; +} + +int dist_lz4_decompress_block(uint8_t* dst, size_t dst_len, + const uint8_t* src, size_t src_len) { + (void)dst; + (void)dst_len; + (void)src; + (void)src_len; + return DIST_ERR; +} diff --git a/driver/dist/lz4.h b/driver/dist/lz4.h @@ -0,0 +1,17 @@ +#ifndef CFREE_DIST_LZ4_H +#define CFREE_DIST_LZ4_H + +#include <stddef.h> +#include <stdint.h> + +#include "dist.h" + +size_t dist_lz4_compress_bound(size_t raw_len); + +int dist_lz4_compress_block(uint8_t* dst, size_t dst_cap, size_t* dst_len, + const uint8_t* src, size_t src_len); + +int dist_lz4_decompress_block(uint8_t* dst, size_t dst_len, + const uint8_t* src, size_t src_len); + +#endif diff --git a/driver/dist/manifest.c b/driver/dist/manifest.c @@ -6,21 +6,18 @@ #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_HASH 0x04u +#define F_ID 0x08u +#define F_PATH 0x10u +#define F_KIND 0x20u +#define F_BLAKE2B 0x40u +#define F_ROOT 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; } @@ -33,7 +30,7 @@ static int emit_kv(CfreeWriter* out, const char* key, const char* val) { static int emit_hex(CfreeWriter* out, const char* key, const uint8_t* h, size_t n) { - char hex[2 * DIST_SHA256_LEN + 1]; + char hex[2 * DIST_BLAKE2B_LEN + 1]; dist_hex_encode(hex, h, n); return emit_kv(out, key, hex); } @@ -50,27 +47,23 @@ int dist_manifest_emit(const DistManifest* m, CfreeWriter* out) { 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) + emit_kv(out, "description", m->description) != DIST_OK) return DIST_ERR; + if (emit_kv(out, "hash", DIST_MANIFEST_HASH) != 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_u64(out, "id", a->id) != 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 (emit_hex(out, "blake2b", a->blake2b, DIST_BLAKE2B_LEN) != DIST_OK) + return DIST_ERR; + if (emit_hex(out, "root", a->root, DIST_BLAKE2B_LEN) != DIST_OK) + return DIST_ERR; if (a->entry && emit_kv(out, "entry", "true") != DIST_OK) return DIST_ERR; } @@ -79,20 +72,16 @@ int dist_manifest_emit(const DistManifest* m, CfreeWriter* out) { 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) { + if (d->has_blake2b && + emit_hex(out, "blake2b", d->blake2b, DIST_BLAKE2B_LEN) != DIST_OK) return DIST_ERR; - } if (d->has_keyid && - emit_hex(out, "key", d->keyid, DIST_KEYID_LEN) != DIST_OK) { + 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; @@ -101,9 +90,8 @@ static char* trim_lead(char* 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 - 1] == '\n')) s[--n] = '\0'; - } } static int set_err(char* err, size_t cap, const char* msg) { @@ -135,22 +123,18 @@ static int parse_u64(const char* s, uint64_t* out) { 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)) { + if ((seen & (F_NAME | F_VERSION | F_HASH)) != + (F_NAME | F_VERSION | F_HASH)) 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)) { + if ((seen & (F_ID | F_PATH | F_KIND | F_BLAKE2B | F_ROOT | F_SIZE)) != + (F_ID | F_PATH | F_KIND | F_BLAKE2B | F_ROOT | F_SIZE)) return set_err(err, errcap, "missing required [artifact] field"); - } - } else { /* SEC_DEP */ - if ((seen & (F_NAME | F_VERSION)) != (F_NAME | F_VERSION)) { + } else { + if ((seen & (F_NAME | F_VERSION)) != (F_NAME | F_VERSION)) return set_err(err, errcap, "missing required [dependency] field"); - } } return DIST_OK; } @@ -178,19 +162,17 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m, 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) { + 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 == '\0' || *t == '#') continue; if (*t == '[') { if (finalize(sec, seen, err, errcap) != DIST_OK) return DIST_ERR; @@ -230,24 +212,19 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m, } 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 if (strcmp(key, "hash") == 0) { + if (strcmp(val, DIST_MANIFEST_HASH) != 0) + return set_err(err, errcap, "unsupported hash algorithm"); + seen |= F_HASH; } else { return set_err(err, errcap, "unknown top-level key"); } } else if (sec == SEC_ART) { - if (strcmp(key, "path") == 0) { + if (strcmp(key, "id") == 0) { + if (parse_u64(val, &art->id) != DIST_OK) + return set_err(err, errcap, "bad artifact id"); + seen |= F_ID; + } else if (strcmp(key, "path") == 0) { if (copy_field(art->path, sizeof art->path, val, err, errcap)) return DIST_ERR; seen |= F_PATH; @@ -255,20 +232,24 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m, 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 (!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, "blake2b") == 0) { + if (strlen(val) != 2 * DIST_BLAKE2B_LEN || + dist_hex_decode(art->blake2b, val, DIST_BLAKE2B_LEN) != DIST_OK) + return set_err(err, errcap, "bad artifact blake2b"); + seen |= F_BLAKE2B; + } else if (strcmp(key, "root") == 0) { + if (strlen(val) != 2 * DIST_BLAKE2B_LEN || + dist_hex_decode(art->root, val, DIST_BLAKE2B_LEN) != DIST_OK) + return set_err(err, errcap, "bad artifact root"); + seen |= F_ROOT; } else if (strcmp(key, "entry") == 0) { art->entry = (strcmp(val, "true") == 0); if (!art->entry && strcmp(val, "false") != 0) @@ -276,7 +257,7 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m, } else { return set_err(err, errcap, "unknown [artifact] key"); } - } else { /* SEC_DEP */ + } else { if (strcmp(key, "name") == 0) { if (copy_field(dep->name, sizeof dep->name, val, err, errcap)) return DIST_ERR; @@ -285,11 +266,11 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m, 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, "blake2b") == 0) { + if (strlen(val) != 2 * DIST_BLAKE2B_LEN || + dist_hex_decode(dep->blake2b, val, DIST_BLAKE2B_LEN) != DIST_OK) + return set_err(err, errcap, "bad dependency blake2b"); + dep->has_blake2b = 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) @@ -301,5 +282,17 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m, } } - return finalize(sec, seen, err, errcap); + if (finalize(sec, seen, err, errcap) != DIST_OK) return DIST_ERR; + { + size_t i, j; + for (i = 0; i < m->n_artifacts; ++i) { + for (j = i + 1u; j < m->n_artifacts; ++j) { + if (m->artifacts[i].id == m->artifacts[j].id) + return set_err(err, errcap, "duplicate artifact id"); + if (strcmp(m->artifacts[i].path, m->artifacts[j].path) == 0) + return set_err(err, errcap, "duplicate artifact path"); + } + } + } + return DIST_OK; } diff --git a/driver/dist/manifest.h b/driver/dist/manifest.h @@ -7,19 +7,20 @@ #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. */ +/* The signed logical package object for distribution v2. The physical + * encodings (.tar.gz and .cfpkg) both carry these literal bytes and the same + * detached minisign signature. */ -#define DIST_MANIFEST_MAGIC "cfree-manifest 1" +#define DIST_MANIFEST_MAGIC "cfree-package 2" +#define DIST_MANIFEST_HASH "blake2b-merkle-v1" typedef struct DistArtifact { + uint64_t id; char path[DIST_PATH_MAX + 1]; char target[DIST_TRIPLE_MAX]; /* "" = target-independent */ char kind[DIST_KIND_MAX]; - uint8_t sha256[DIST_SHA256_LEN]; + uint8_t blake2b[DIST_BLAKE2B_LEN]; + uint8_t root[DIST_BLAKE2B_LEN]; uint64_t size; int entry; } DistArtifact; @@ -27,8 +28,8 @@ typedef struct 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 blake2b[DIST_BLAKE2B_LEN]; + int has_blake2b; uint8_t keyid[DIST_KEYID_LEN]; int has_keyid; } DistDependency; @@ -37,22 +38,14 @@ 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); diff --git a/driver/dist/sha256.c b/driver/dist/sha256.c @@ -1,30 +0,0 @@ -#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 @@ -1,18 +0,0 @@ -#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/pkg.c b/driver/pkg.c @@ -4,35 +4,41 @@ #include <stdio.h> #include <string.h> +#include "dist/blake2b.h" +#include "dist/cfpkg.h" #include "dist/deflate.h" #include "dist/dist.h" +#include "dist/lz4.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 --- */ +#define PKG_META_MANIFEST "cfree/package.manifest" +#define PKG_META_SIG "cfree/package.manifest.minisig" +#define PKG_META_PUB "cfree/package.pub" + +typedef enum PkgFormat { PKG_FMT_AUTO, PKG_FMT_CFPKG, PKG_FMT_TARGZ } PkgFormat; + +typedef struct PkgInputFile { + const char* src; + const char* path; + CfreeFileData fd; + int loaded; +} PkgInputFile; + +typedef struct PkgVerified { + DistManifest manifest; + uint8_t package_id[DIST_BLAKE2B_LEN]; + uint8_t keyid[DIST_KEYID_LEN]; + uint8_t pk[DIST_ED25519_PK_LEN]; + char trusted[DIST_TRUSTED_COMMENT_MAX]; +} PkgVerified; static int pkg_write_file(const CfreeContext* ctx, const char* path, const uint8_t* data, size_t len) { @@ -50,15 +56,12 @@ static int pkg_write_file(const CfreeContext* ctx, const char* 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; @@ -72,14 +75,12 @@ static int pkg_trust_path(char* buf, size_t cap) { 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) { + for (p = path; *p; ++p) if (*p == '/') slash = p; - } if (!slash) { buf[0] = '\0'; return; @@ -90,8 +91,6 @@ static void pkg_parent_dir(const char* path, char* buf, size_t cap) { 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 @@ -99,60 +98,124 @@ static int pkg_read_file(const CfreeContext* ctx, const char* path, : 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) { +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_has_suffix(e[i].name, suffix)) return &e[i]; - } + for (i = 0; i < n; ++i) + if (driver_streq(e[i].name, name)) return &e[i]; return NULL; } -static const DistTarEntry* pkg_find_name(const DistTarEntry* e, size_t n, - const char* name) { +static PkgFormat pkg_parse_format(const char* s) { + if (driver_streq(s, "cfpkg") || driver_streq(s, "native")) return PKG_FMT_CFPKG; + if (driver_streq(s, "tar.gz") || driver_streq(s, "portable")) + return PKG_FMT_TARGZ; + return PKG_FMT_AUTO; +} + +static PkgFormat pkg_infer_format(const char* path) { + if (driver_has_suffix(path, ".tar.gz")) return PKG_FMT_TARGZ; + if (driver_has_suffix(path, ".cfpkg")) return PKG_FMT_CFPKG; + return PKG_FMT_AUTO; +} + +static uint64_t pkg_align_up(uint64_t v, uint64_t a) { + return a ? ((v + a - 1u) / a) * a : v; +} + +static int pkg_write_pad(CfreeWriter* w, uint64_t target) { + static const uint8_t z[64] = {0}; + while (cfree_writer_tell(w) < target) { + uint64_t left = target - cfree_writer_tell(w); + size_t n = left < sizeof z ? (size_t)left : sizeof z; + if (cfree_writer_write(w, z, n) != CFREE_OK) return DIST_ERR; + } + return DIST_OK; +} + +static void pkg_hash(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, + size_t len) { + dist_blake2b(out, data, len); +} + +static void pkg_region_root(uint8_t out[DIST_BLAKE2B_LEN], const char* kind, + const uint8_t* data, size_t len) { + uint8_t h[DIST_BLAKE2B_LEN]; + pkg_hash(h, data, len); + dist_cfpkg2_root_hash(out, kind, 0, h); +} + +static int pkg_load_inputs(const CfreeContext* ctx, const char** files, + size_t n_files, PkgInputFile* in, DistManifest* m) { size_t i; - for (i = 0; i < n; ++i) { - if (driver_streq(e[i].name, name)) return &e[i]; + memset(m, 0, sizeof *m); + for (i = 0; i < n_files; ++i) { + DistArtifact* a; + in[i].src = files[i]; + in[i].path = driver_basename(files[i]); + if (pkg_read_file(ctx, files[i], &in[i].fd) != DIST_OK) { + driver_errf(PKG_TOOL, "create: cannot read file: %s", files[i]); + return DIST_ERR; + } + in[i].loaded = 1; + if (m->n_artifacts >= DIST_MAX_ARTIFACTS) { + driver_errf(PKG_TOOL, "create: too many artifacts"); + return DIST_ERR; + } + a = &m->artifacts[m->n_artifacts++]; + a->id = (uint64_t)i; + snprintf(a->path, sizeof a->path, "%s", in[i].path); + snprintf(a->kind, sizeof a->kind, "%s", "data"); + a->size = in[i].fd.size; + pkg_hash(a->blake2b, in[i].fd.data, in[i].fd.size); + if (dist_cfpkg2_artifact_root(a->root, a->id, in[i].fd.data, + in[i].fd.size, + DIST_CFPKG_CHUNK_SIZE_DEFAULT) != DIST_OK) { + driver_errf(PKG_TOOL, "create: artifact too large for current chunk cap"); + return DIST_ERR; + } } - return NULL; + return DIST_OK; } -/* ---------------------------------------------------------------- help --- */ +static void pkg_release_inputs(const CfreeContext* ctx, PkgInputFile* in, + size_t n) { + size_t i; + for (i = 0; i < n; ++i) + if (in[i].loaded) ctx->file_io->release(ctx->file_io->user, &in[i].fd); +} + +static int pkg_sign(CfreeWriter* out, DriverEnv* env, const uint8_t* data, + size_t len, const DistKeypair* kp, + const uint8_t pkgid[DIST_BLAKE2B_LEN], + const char* what) { + char tcomment[DIST_TRUSTED_COMMENT_MAX]; + char pkgid_hex[2 * DIST_BLAKE2B_LEN + 1]; + dist_hex_encode(pkgid_hex, pkgid, DIST_BLAKE2B_LEN); + snprintf(tcomment, sizeof tcomment, "created=%lld pkgid=%s", + (long long)(env->now > 0 ? env->now : 0), pkgid_hex); + return dist_minisig_sign(out, data, len, kp->sk, kp->keyid, what, tcomment); +} void driver_help_pkg(void) { driver_printf( - "cfree pkg — basic signed code distribution\n" + "cfree pkg - 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"); + " [--format cfpkg|tar.gz] [--compression none|lz4-block-v1]\n" + " -o OUT FILE...\n" + " cfree pkg verify [-p PUBKEY | --tofu] [--format cfpkg|tar.gz] FILE\n" + " cfree pkg unpack FILE -C DIR\n" + " cfree pkg inspect FILE\n" + " cfree pkg trust {list | add PUBKEY [label] | remove KEYID}\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]; + uint8_t seed[DIST_ED25519_SEED_LEN], keyid[DIST_KEYID_LEN]; DistKeypair kp; CfreeWriter* w; char path[PKG_PATH_BUF]; @@ -160,11 +223,10 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc, size_t len; int i; (void)env; - for (i = 0; i < argc; ++i) { - if (driver_streq(argv[i], "-o") && i + 1 < argc) { + if (driver_streq(argv[i], "-o") && i + 1 < argc) base = argv[++i]; - } else { + else { driver_errf(PKG_TOOL, "keygen: unexpected argument: %s", argv[i]); return 2; } @@ -173,29 +235,24 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc, 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); @@ -204,46 +261,243 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc, return 0; } -/* -------------------------------------------------------------- create --- */ +static int pkg_create_targz(const CfreeContext* ctx, const char* out, + PkgInputFile* in, size_t n_files, + const uint8_t* man, size_t man_len, + const uint8_t* sig, size_t sig_len, + const uint8_t* pub, size_t pub_len) { + CfreeWriter *tar = NULL, *gz = NULL; + const uint8_t *tb, *gb; + size_t tl, gl, i; + int rc = DIST_ERR; + tar = pkg_mem(ctx); + gz = pkg_mem(ctx); + if (!tar || !gz) goto done; + if (dist_tar_append(tar, PKG_META_MANIFEST, man, man_len) != DIST_OK || + dist_tar_append(tar, PKG_META_SIG, sig, sig_len) != DIST_OK || + dist_tar_append(tar, PKG_META_PUB, pub, pub_len) != DIST_OK) + goto done; + for (i = 0; i < n_files; ++i) + if (dist_tar_append(tar, in[i].path, in[i].fd.data, in[i].fd.size) != + DIST_OK) + goto done; + if (dist_tar_finish(tar) != DIST_OK) goto done; + tb = cfree_writer_mem_bytes(tar, &tl); + if (dist_gz_compress(gz, tb, tl) != DIST_OK) goto done; + gb = cfree_writer_mem_bytes(gz, &gl); + rc = pkg_write_file(ctx, out, gb, gl); +done: + if (gz) cfree_writer_close(gz); + if (tar) cfree_writer_close(tar); + return rc; +} + +static int pkg_build_native_regions(const CfreeContext* ctx, PkgInputFile* in, + size_t n_files, uint32_t compression, + CfreeWriter** index_out, + CfreeWriter** content_out) { + CfreeWriter* index = pkg_mem(ctx); + CfreeWriter* content = pkg_mem(ctx); + size_t i; + if (!index || !content) return DIST_ERR; + for (i = 0; i < n_files; ++i) { + size_t off = 0, ci = 0; + if (in[i].fd.size == 0) continue; + while (off < in[i].fd.size) { + uint8_t recbuf[DIST_CFPKG_INDEX_RECORD_SIZE]; + DistCfpkgIndexRecord r; + const uint8_t* raw = in[i].fd.data + off; + size_t raw_len = in[i].fd.size - off; + if (raw_len > DIST_CFPKG_CHUNK_SIZE_DEFAULT) + raw_len = DIST_CFPKG_CHUNK_SIZE_DEFAULT; + memset(&r, 0, sizeof r); + r.artifact_id = (uint64_t)i; + r.chunk_index = (uint64_t)ci; + r.content_offset = cfree_writer_tell(content); + r.raw_size = raw_len; + r.compression = compression; + pkg_hash(r.raw_hash, raw, raw_len); + dist_cfpkg2_leaf_hash(r.leaf_hash, r.artifact_id, r.chunk_index, raw, + raw_len); + if (compression == DIST_CFPKG_COMP_NONE) { + r.stored_size = raw_len; + pkg_hash(r.stored_hash, raw, raw_len); + if (cfree_writer_write(content, raw, raw_len) != CFREE_OK) + return DIST_ERR; + } else { + uint8_t tmp[DIST_CFPKG_CHUNK_SIZE_DEFAULT + 512u]; + size_t stored_len = 0; + if (dist_lz4_compress_block(tmp, sizeof tmp, &stored_len, raw, + raw_len) != DIST_OK) { + driver_errf(PKG_TOOL, "create: lz4-block-v1 is stubbed"); + return DIST_ERR; + } + r.stored_size = stored_len; + pkg_hash(r.stored_hash, tmp, stored_len); + if (cfree_writer_write(content, tmp, stored_len) != CFREE_OK) + return DIST_ERR; + } + dist_cfpkg_encode_index_record(recbuf, &r); + if (cfree_writer_write(index, recbuf, sizeof recbuf) != CFREE_OK) + return DIST_ERR; + off += raw_len; + ++ci; + } + } + *index_out = index; + *content_out = content; + return DIST_OK; +} + +static int pkg_create_cfpkg(DriverEnv* env, const CfreeContext* ctx, + const char* out, const DistKeypair* kp, + PkgInputFile* in, size_t n_files, + const uint8_t* man, size_t man_len, + const uint8_t* sig, size_t sig_len, + const uint8_t* pub, size_t pub_len, + const uint8_t pkgid[DIST_BLAKE2B_LEN], + uint32_t compression) { + CfreeWriter *index = NULL, *content = NULL, *descw = NULL, *descsigw = NULL, + *pkg = NULL; + const uint8_t *index_b, *content_b, *desc_b = NULL, *descsig_b = NULL; + size_t index_l, content_l, desc_l = 0, descsig_l = 0; + uint8_t index_root[DIST_BLAKE2B_LEN], content_root[DIST_BLAKE2B_LEN]; + DistCfpkgHeader h; + int stable = 0, iter, rc = DIST_ERR; + + if (pkg_build_native_regions(ctx, in, n_files, compression, &index, &content) != + DIST_OK) + goto done; + index_b = cfree_writer_mem_bytes(index, &index_l); + content_b = cfree_writer_mem_bytes(content, &content_l); + pkg_region_root(index_root, "index", index_b, index_l); + pkg_region_root(content_root, "content", content_b, content_l); + + memset(&h, 0, sizeof h); + for (iter = 0; iter < 8; ++iter) { + DistCfpkgDescriptor d; + uint64_t old_desc_l = desc_l, old_descsig_l = descsig_l; + if (descw) cfree_writer_close(descw); + if (descsigw) cfree_writer_close(descsigw); + descw = pkg_mem(ctx); + descsigw = pkg_mem(ctx); + if (!descw || !descsigw) goto done; + h.manifest_offset = DIST_CFPKG_HEADER_SIZE; + h.manifest_size = man_len; + h.signature_offset = h.manifest_offset + h.manifest_size; + h.signature_size = sig_len; + h.descriptor_offset = h.signature_offset + h.signature_size; + h.descriptor_size = old_desc_l; + h.descriptor_signature_offset = h.descriptor_offset + h.descriptor_size; + h.descriptor_signature_size = old_descsig_l; + h.pubkey_offset = + h.descriptor_signature_offset + h.descriptor_signature_size; + h.pubkey_size = pub_len; + h.index_offset = + pkg_align_up(h.pubkey_offset + h.pubkey_size, DIST_CFPKG_ALIGNMENT); + h.index_size = index_l; + h.content_offset = + pkg_align_up(h.index_offset + h.index_size, DIST_CFPKG_ALIGNMENT); + h.content_size = content_l; + h.alignment = DIST_CFPKG_ALIGNMENT; + h.chunk_size = DIST_CFPKG_CHUNK_SIZE_DEFAULT; + + memset(&d, 0, sizeof d); + memcpy(d.package_id, pkgid, DIST_BLAKE2B_LEN); + d.index_offset = h.index_offset; + d.index_size = h.index_size; + memcpy(d.index_root, index_root, DIST_BLAKE2B_LEN); + d.content_offset = h.content_offset; + d.content_size = h.content_size; + memcpy(d.content_root, content_root, DIST_BLAKE2B_LEN); + d.chunk_size = h.chunk_size; + d.alignment = h.alignment; + if (dist_cfpkg_descriptor_emit(descw, &d) != DIST_OK) goto done; + desc_b = cfree_writer_mem_bytes(descw, &desc_l); + if (pkg_sign(descsigw, env, desc_b, desc_l, kp, pkgid, + "cfree cfpkg encoding descriptor") != DIST_OK) + goto done; + descsig_b = cfree_writer_mem_bytes(descsigw, &descsig_l); + if (desc_l == old_desc_l && descsig_l == old_descsig_l) { + stable = 1; + break; + } + } + if (!stable) goto done; + + pkg = pkg_mem(ctx); + if (!pkg) goto done; + if (dist_cfpkg_write_header(pkg, &h) != DIST_OK || + cfree_writer_write(pkg, man, man_len) != CFREE_OK || + cfree_writer_write(pkg, sig, sig_len) != CFREE_OK || + cfree_writer_write(pkg, desc_b, desc_l) != CFREE_OK || + cfree_writer_write(pkg, descsig_b, descsig_l) != CFREE_OK || + cfree_writer_write(pkg, pub, pub_len) != CFREE_OK || + pkg_write_pad(pkg, h.index_offset) != DIST_OK || + cfree_writer_write(pkg, index_b, index_l) != CFREE_OK || + pkg_write_pad(pkg, h.content_offset) != DIST_OK || + cfree_writer_write(pkg, content_b, content_l) != CFREE_OK) + goto done; + { + const uint8_t* bytes; + size_t len; + bytes = cfree_writer_mem_bytes(pkg, &len); + rc = pkg_write_file(ctx, out, bytes, len); + } + +done: + if (pkg) cfree_writer_close(pkg); + if (descsigw) cfree_writer_close(descsigw); + if (descw) cfree_writer_close(descw); + if (content) cfree_writer_close(content); + if (index) cfree_writer_close(index); + return rc; +} 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 *name = NULL, *version = NULL, *desc = NULL, *out = NULL, + *seckey = NULL; const char* files[DIST_MAX_FILES]; + PkgInputFile inputs[DIST_MAX_FILES]; size_t n_files = 0; - int i, rc = 1; - - DistManifest m; + int i, rc = 1, sk_loaded = 0; + PkgFormat fmt = PKG_FMT_AUTO; + uint32_t compression = DIST_CFPKG_COMP_NONE; 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]; + DistManifest m; + CfreeWriter *manw = NULL, *sigw = NULL, *pubw = NULL; + const uint8_t *man_b, *sig_b, *pub_b; + size_t man_l, sig_l, pub_l; + uint8_t pkgid[DIST_BLAKE2B_LEN]; + char pkgid_hex[2 * DIST_BLAKE2B_LEN + 1]; + memset(inputs, 0, sizeof inputs); for (i = 0; i < argc; ++i) { const char* a = argv[i]; - if (driver_streq(a, "--name") && i + 1 < argc) { + if (driver_streq(a, "--name") && i + 1 < argc) name = argv[++i]; - } else if (driver_streq(a, "--version") && i + 1 < argc) { + else if (driver_streq(a, "--version") && i + 1 < argc) version = argv[++i]; - } else if (driver_streq(a, "--desc") && i + 1 < argc) { + else if (driver_streq(a, "--desc") && i + 1 < argc) desc = argv[++i]; - } else if (driver_streq(a, "-o") && i + 1 < argc) { + else if (driver_streq(a, "-o") && i + 1 < argc) out = argv[++i]; - } else if (driver_streq(a, "-s") && i + 1 < argc) { + else if (driver_streq(a, "-s") && i + 1 < argc) seckey = argv[++i]; + else if (driver_streq(a, "--format") && i + 1 < argc) { + fmt = pkg_parse_format(argv[++i]); + if (fmt == PKG_FMT_AUTO) { + driver_errf(PKG_TOOL, "create: unknown format"); + return 2; + } + } + else if (driver_streq(a, "--compression") && i + 1 < argc) { + if (dist_cfpkg_compression_parse(argv[++i], &compression) != DIST_OK) { + driver_errf(PKG_TOOL, "create: unknown compression"); + return 2; + } } else if (a[0] == '-' && a[1] != '\0') { driver_errf(PKG_TOOL, "create: unknown option: %s", a); return 2; @@ -260,157 +514,78 @@ static int pkg_create(DriverEnv* env, const CfreeContext* ctx, int argc, "create: --name, --version, -s SECKEY and -o OUT are required"); return 2; } + if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(out); + if (fmt == PKG_FMT_AUTO) { + driver_errf(PKG_TOOL, "create: cannot infer format; pass --format"); + 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; + goto done; } 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; - } + if (kr == DIST_ENCRYPTED) + driver_errf(PKG_TOOL, "create: encrypted secret keys need scrypt"); + if (kr != DIST_OK) 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); + if (pkg_load_inputs(ctx, files, n_files, inputs, &m) != DIST_OK) goto done; 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"); + if (!manw || !sigw || !pubw) goto done; + if (dist_manifest_emit(&m, manw) != DIST_OK) goto done; + man_b = cfree_writer_mem_bytes(manw, &man_l); + pkg_hash(pkgid, man_b, man_l); + dist_hex_encode(pkgid_hex, pkgid, DIST_BLAKE2B_LEN); + if (pkg_sign(sigw, env, man_b, man_l, &kp, pkgid, + "signature from cfree pkg") != DIST_OK) 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; + sig_b = cfree_writer_mem_bytes(sigw, &sig_l); + if (dist_minisig_emit_pubkey(pubw, &kp) != DIST_OK) goto done; + pub_b = cfree_writer_mem_bytes(pubw, &pub_l); + + if (fmt == PKG_FMT_TARGZ) + rc = pkg_create_targz(ctx, out, inputs, n_files, man_b, man_l, sig_b, sig_l, + pub_b, pub_l) == DIST_OK + ? 0 + : 1; + else + rc = pkg_create_cfpkg(env, ctx, out, &kp, inputs, n_files, man_b, man_l, + sig_b, sig_l, pub_b, pub_l, pkgid, + compression) == DIST_OK + ? 0 + : 1; + if (rc == 0) + driver_printf("wrote %s (%llu artifact(s), id %s)\n", out, + (unsigned long long)n_files, pkgid_hex); 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); + pkg_release_inputs(ctx, inputs, n_files); 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 uint8_t* bundled_pub, size_t bundled_pub_size, 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; @@ -420,17 +595,12 @@ static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx, } 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) { + if (ok != DIST_OK || 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)"); @@ -439,52 +609,33 @@ static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx, 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); + 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); + driver_errf(PKG_TOOL, "untrusted signer (key id %s)", 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"); + if (!bundled_pub || bundled_pub_size == 0 || + dist_minisig_parse_pubkey(bundled_pub, bundled_pub_size, pk, kid_chk) != + DIST_OK || + memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) { + driver_errf(PKG_TOOL, "--tofu: bundled public key is missing or mismatched"); return DIST_ERR; } { - char line[DIST_TRUST_LINE_MAX]; - char parent[PKG_PATH_BUF]; - char label[PKG_NAME_BUF]; + char line[DIST_TRUST_LINE_MAX], parent[PKG_PATH_BUF], hex[17]; 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); + if (dist_trust_format_entry(line, sizeof line, keyid, pk, "tofu-pinned") != + DIST_OK) 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) { @@ -498,273 +649,450 @@ static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx, 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]; +static int pkg_verify_manifest(DriverEnv* env, const CfreeContext* ctx, + const uint8_t* man, size_t man_len, + const uint8_t* sig, size_t sig_len, + const uint8_t* pub, size_t pub_len, + const char* pubkey_opt, int tofu, + PkgVerified* out) { + char err[128], pkgid_hex[2 * DIST_BLAKE2B_LEN + 1]; 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) { + if (dist_minisig_sig_keyid(sig, sig_len, out->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) + if (pkg_resolve_key(env, ctx, out->keyid, pub, pub_len, pubkey_opt, tofu, + out->pk) != DIST_OK) return DIST_ERR; - - if (dist_minisig_verify(sig->data, sig->size, man->data, man->size, pk, - trusted, sizeof trusted) != DIST_OK) { + if (dist_minisig_verify(sig, sig_len, man, man_len, out->pk, out->trusted, + sizeof out->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) { + pkg_hash(out->package_id, man, man_len); + dist_hex_encode(pkgid_hex, out->package_id, DIST_BLAKE2B_LEN); + pidp = strstr(out->trusted, "pkgid="); + if (!pidp || strncmp(pidp + 6, pkgid_hex, 2 * DIST_BLAKE2B_LEN) != 0) { driver_errf(PKG_TOOL, "trusted comment does not match package id"); return DIST_ERR; } + if (dist_manifest_parse(man, man_len, &out->manifest, err, sizeof err) != + DIST_OK) { + driver_errf(PKG_TOOL, "manifest: %s", err); + return DIST_ERR; + } + return DIST_OK; +} + +static const DistArtifact* pkg_find_artifact(const DistManifest* m, + const char* path) { + size_t i; + for (i = 0; i < m->n_artifacts; ++i) + if (driver_streq(m->artifacts[i].path, path)) return &m->artifacts[i]; + return NULL; +} - /* 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); +static int pkg_verify_artifact_bytes(const DistArtifact* a, const uint8_t* data, + size_t len) { + uint8_t h[DIST_BLAKE2B_LEN], root[DIST_BLAKE2B_LEN]; + if (len != a->size) return DIST_ERR; + pkg_hash(h, data, len); + if (memcmp(h, a->blake2b, DIST_BLAKE2B_LEN) != 0) return DIST_ERR; + if (dist_cfpkg2_artifact_root(root, a->id, data, len, + DIST_CFPKG_CHUNK_SIZE_DEFAULT) != DIST_OK) + return DIST_ERR; + return memcmp(root, a->root, DIST_BLAKE2B_LEN) == 0 ? DIST_OK : DIST_ERR; +} + +static int pkg_load_portable(const CfreeContext* ctx, const char* file, + CfreeFileData* fd, CfreeWriter** inflated_out, + DistTarEntry* entries, size_t* ne) { + CfreeWriter* inflated = NULL; + const uint8_t* bytes; + size_t len; + if (pkg_read_file(ctx, file, fd) != DIST_OK) { + driver_errf(PKG_TOOL, "cannot read package: %s", file); return DIST_ERR; } - if (archive->size != m->archive_size) { - driver_errf(PKG_TOOL, "archive size mismatch"); + inflated = pkg_mem(ctx); + if (!inflated || dist_gz_decompress(inflated, fd->data, fd->size) != DIST_OK) { + driver_errf(PKG_TOOL, "malformed portable package"); 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"); + bytes = cfree_writer_mem_bytes(inflated, &len); + if (dist_tar_iter(bytes, len, entries, DIST_MAX_FILES, ne) != DIST_OK) { + driver_errf(PKG_TOOL, "malformed portable tar"); return DIST_ERR; } + *inflated_out = inflated; + return DIST_OK; +} +static int pkg_verify_portable(DriverEnv* env, const CfreeContext* ctx, + const char* file, const char* pubkey, int tofu, + const char* out_dir, int quiet) { + CfreeFileData fd = {0}; + CfreeWriter* inflated = NULL; + DistTarEntry entries[DIST_MAX_FILES]; + size_t ne = 0, i, seen = 0; + const DistTarEntry *man, *sig, *pub; + PkgVerified v; + int rc = DIST_ERR; + if (pkg_load_portable(ctx, file, &fd, &inflated, entries, &ne) != DIST_OK) + goto done; + man = pkg_find_name(entries, ne, PKG_META_MANIFEST); + sig = pkg_find_name(entries, ne, PKG_META_SIG); + pub = pkg_find_name(entries, ne, PKG_META_PUB); + if (!man || !sig) { + driver_errf(PKG_TOOL, "package missing manifest or signature"); + goto done; + } + if (pkg_verify_manifest(env, ctx, man->data, man->size, sig->data, sig->size, + pub ? pub->data : NULL, pub ? pub->size : 0, pubkey, + tofu, &v) != DIST_OK) + goto done; + for (i = 0; i < ne; ++i) { + const DistArtifact* a; + if (driver_streq(entries[i].name, PKG_META_MANIFEST) || + driver_streq(entries[i].name, PKG_META_SIG) || + driver_streq(entries[i].name, PKG_META_PUB)) + continue; + a = pkg_find_artifact(&v.manifest, entries[i].name); + if (!a) { + driver_errf(PKG_TOOL, "portable member not in manifest: %s", + entries[i].name); + goto done; + } + if (pkg_verify_artifact_bytes(a, entries[i].data, entries[i].size) != + DIST_OK) { + driver_errf(PKG_TOOL, "artifact hash mismatch: %s", entries[i].name); + goto done; + } + ++seen; + if (out_dir) { + char full[PKG_PATH_BUF], parent[PKG_PATH_BUF]; + snprintf(full, sizeof full, "%s/%s", out_dir, entries[i].name); + pkg_parent_dir(full, parent, sizeof parent); + if (parent[0]) driver_mkdir_p(env, parent); + if (pkg_write_file(ctx, full, entries[i].data, entries[i].size) != + DIST_OK) + goto done; + driver_printf(" extracted %s\n", full); + } + } + if (seen != v.manifest.n_artifacts) { + driver_errf(PKG_TOOL, "portable package is missing artifacts"); + goto done; + } 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); + dist_hex_encode(idhex, v.keyid, DIST_KEYID_LEN); + driver_printf("ok: %s %s signer %s [%s]\n", v.manifest.name, + v.manifest.version, idhex, v.trusted); } - *archive_out = archive; - return DIST_OK; + if (out_dir) + driver_printf("unpacked %s %s to %s\n", v.manifest.name, + v.manifest.version, out_dir); + rc = DIST_OK; +done: + if (inflated) cfree_writer_close(inflated); + if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd); + return rc; } -/* ------------------------------------------------------ verify / unpack --- */ +static int pkg_bounds(const DistCfpkgHeader* h, size_t len) { + uint64_t ranges[][2] = {{h->manifest_offset, h->manifest_size}, + {h->signature_offset, h->signature_size}, + {h->descriptor_offset, h->descriptor_size}, + {h->descriptor_signature_offset, + h->descriptor_signature_size}, + {h->pubkey_offset, h->pubkey_size}, + {h->index_offset, h->index_size}, + {h->content_offset, h->content_size}}; + size_t i; + for (i = 0; i < sizeof ranges / sizeof ranges[0]; ++i) + if (ranges[i][0] > len || ranges[i][1] > len - ranges[i][0]) + return DIST_ERR; + return DIST_OK; +} -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; +static int pkg_verify_native_content(DriverEnv* env, const CfreeContext* ctx, + const uint8_t* data, + const DistCfpkgHeader* h, + const PkgVerified* v, + const char* out_dir) { + size_t ai; + for (ai = 0; ai < v->manifest.n_artifacts; ++ai) { + const DistArtifact* a = &v->manifest.artifacts[ai]; + CfreeWriter* raww = pkg_mem(ctx); + const uint8_t* rawb; + size_t rawl; + uint64_t want_chunk = 0; + size_t off; + if (!raww) return DIST_ERR; + for (off = 0; off < h->index_size; off += DIST_CFPKG_INDEX_RECORD_SIZE) { + DistCfpkgIndexRecord r; + const uint8_t* stored; + uint8_t sh[DIST_BLAKE2B_LEN], rh[DIST_BLAKE2B_LEN], + leaf[DIST_BLAKE2B_LEN]; + if (dist_cfpkg_decode_index_record(data + h->index_offset + off, + DIST_CFPKG_INDEX_RECORD_SIZE, + &r) != DIST_OK) + return DIST_ERR; + if (r.artifact_id != a->id) continue; + if (r.chunk_index != want_chunk++) { + driver_errf(PKG_TOOL, "native index chunks out of order"); + return DIST_ERR; + } + if (r.content_offset > h->content_size || + r.stored_size > h->content_size - r.content_offset) + return DIST_ERR; + stored = data + h->content_offset + r.content_offset; + pkg_hash(sh, stored, (size_t)r.stored_size); + if (memcmp(sh, r.stored_hash, DIST_BLAKE2B_LEN) != 0) return DIST_ERR; + if (r.compression == DIST_CFPKG_COMP_NONE) { + pkg_hash(rh, stored, (size_t)r.stored_size); + dist_cfpkg2_leaf_hash(leaf, r.artifact_id, r.chunk_index, stored, + (size_t)r.stored_size); + if (r.raw_size != r.stored_size || + memcmp(rh, r.raw_hash, DIST_BLAKE2B_LEN) != 0 || + memcmp(leaf, r.leaf_hash, DIST_BLAKE2B_LEN) != 0 || + cfree_writer_write(raww, stored, (size_t)r.stored_size) != CFREE_OK) + return DIST_ERR; + } else if (r.compression == DIST_CFPKG_COMP_LZ4_BLOCK_V1) { + uint8_t tmp[DIST_CFPKG_CHUNK_SIZE_DEFAULT]; + if (r.raw_size > sizeof tmp || + dist_lz4_decompress_block(tmp, (size_t)r.raw_size, stored, + (size_t)r.stored_size) != DIST_OK) + return DIST_ERR; + pkg_hash(rh, tmp, (size_t)r.raw_size); + dist_cfpkg2_leaf_hash(leaf, r.artifact_id, r.chunk_index, tmp, + (size_t)r.raw_size); + if (memcmp(rh, r.raw_hash, DIST_BLAKE2B_LEN) != 0 || + memcmp(leaf, r.leaf_hash, DIST_BLAKE2B_LEN) != 0 || + cfree_writer_write(raww, tmp, (size_t)r.raw_size) != CFREE_OK) + return DIST_ERR; + } else { + return DIST_ERR; + } + } + rawb = cfree_writer_mem_bytes(raww, &rawl); + if (pkg_verify_artifact_bytes(a, rawb, rawl) != DIST_OK) { + cfree_writer_close(raww); + driver_errf(PKG_TOOL, "artifact hash mismatch: %s", a->path); + return DIST_ERR; + } + if (out_dir) { + char full[PKG_PATH_BUF], parent[PKG_PATH_BUF]; + snprintf(full, sizeof full, "%s/%s", out_dir, a->path); + pkg_parent_dir(full, parent, sizeof parent); + if (parent[0]) driver_mkdir_p(env, parent); + if (pkg_write_file(ctx, full, rawb, rawl) != DIST_OK) { + cfree_writer_close(raww); + return DIST_ERR; + } + driver_printf(" extracted %s\n", full); + } + cfree_writer_close(raww); } 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; +static int pkg_verify_native(DriverEnv* env, const CfreeContext* ctx, + const char* file, const char* pubkey, int tofu, + const char* out_dir, int quiet) { 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; - } + DistCfpkgHeader h; + DistCfpkgDescriptor d; + PkgVerified v; + char err[128]; + uint8_t desc_keyid[DIST_KEYID_LEN], index_root[DIST_BLAKE2B_LEN], + content_root[DIST_BLAKE2B_LEN]; + char desc_trusted[DIST_TRUSTED_COMMENT_MAX]; + int rc = DIST_ERR; + if (pkg_read_file(ctx, file, &fd) != DIST_OK) { + driver_errf(PKG_TOOL, "cannot read package: %s", file); + return DIST_ERR; } - if (!file) { - driver_errf(PKG_TOOL, "verify: FILE.cfpkg is required"); - return 2; + if (dist_cfpkg_read_header(fd.data, fd.size, &h) != DIST_OK || + pkg_bounds(&h, fd.size) != DIST_OK) { + driver_errf(PKG_TOOL, "malformed native package"); + goto done; + } + if (pkg_verify_manifest(env, ctx, fd.data + h.manifest_offset, + (size_t)h.manifest_size, + fd.data + h.signature_offset, (size_t)h.signature_size, + fd.data + h.pubkey_offset, (size_t)h.pubkey_size, + pubkey, tofu, &v) != DIST_OK) + goto done; + if (dist_minisig_sig_keyid(fd.data + h.descriptor_signature_offset, + (size_t)h.descriptor_signature_size, + desc_keyid) != DIST_OK || + memcmp(desc_keyid, v.keyid, DIST_KEYID_LEN) != 0) { + driver_errf(PKG_TOOL, "encoding descriptor signer mismatch"); + goto done; + } + if (dist_minisig_verify(fd.data + h.descriptor_signature_offset, + (size_t)h.descriptor_signature_size, + fd.data + h.descriptor_offset, + (size_t)h.descriptor_size, v.pk, desc_trusted, + sizeof desc_trusted) != DIST_OK) { + driver_errf(PKG_TOOL, "encoding descriptor signature FAILED"); + goto done; + } + if (dist_cfpkg_descriptor_parse(fd.data + h.descriptor_offset, + (size_t)h.descriptor_size, &d, err, + sizeof err) != DIST_OK) { + driver_errf(PKG_TOOL, "encoding descriptor: %s", err); + goto done; } - if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK) + if (memcmp(d.package_id, v.package_id, DIST_BLAKE2B_LEN) != 0 || + d.index_offset != h.index_offset || d.index_size != h.index_size || + d.content_offset != h.content_offset || d.content_size != h.content_size || + d.chunk_size != DIST_CFPKG_CHUNK_SIZE_DEFAULT || + d.alignment != DIST_CFPKG_ALIGNMENT) { + driver_errf(PKG_TOOL, "encoding descriptor does not match package layout"); goto done; - if (pkg_verify_bundle(env, ctx, entries, ne, &m, &archive, pubkey, tofu, 0) == + } + pkg_region_root(index_root, "index", fd.data + h.index_offset, + (size_t)h.index_size); + pkg_region_root(content_root, "content", fd.data + h.content_offset, + (size_t)h.content_size); + if (memcmp(index_root, d.index_root, DIST_BLAKE2B_LEN) != 0 || + memcmp(content_root, d.content_root, DIST_BLAKE2B_LEN) != 0) { + driver_errf(PKG_TOOL, "native package region hash mismatch"); + goto done; + } + if (h.index_size % DIST_CFPKG_INDEX_RECORD_SIZE != 0) goto done; + if (pkg_verify_native_content(env, ctx, fd.data, &h, &v, out_dir) != DIST_OK) - rc = 0; + goto done; + if (!quiet) { + char idhex[2 * DIST_KEYID_LEN + 1]; + dist_hex_encode(idhex, v.keyid, DIST_KEYID_LEN); + driver_printf("ok: %s %s signer %s [%s]\n", v.manifest.name, + v.manifest.version, idhex, v.trusted); + } + if (out_dir) + driver_printf("unpacked %s %s to %s\n", v.manifest.name, + v.manifest.version, out_dir); + rc = DIST_OK; 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; - +static int pkg_verify_or_unpack(DriverEnv* env, const CfreeContext* ctx, + int argc, char** argv, int unpack) { + const char *file = NULL, *pubkey = NULL, *dir = "."; + int tofu = 0, i; + PkgFormat fmt = PKG_FMT_AUTO; 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) { + if (driver_streq(argv[i], "-p") && i + 1 < argc) pubkey = argv[++i]; - } else if (driver_streq(argv[i], "--tofu")) { + else if (driver_streq(argv[i], "--tofu")) tofu = 1; - } else if (argv[i][0] != '-') { + else if (driver_streq(argv[i], "--format") && i + 1 < argc) { + fmt = pkg_parse_format(argv[++i]); + if (fmt == PKG_FMT_AUTO) { + driver_errf(PKG_TOOL, "%s: unknown format", unpack ? "unpack" : "verify"); + return 2; + } + } + else if (unpack && driver_streq(argv[i], "-C") && i + 1 < argc) + dir = argv[++i]; + else if (argv[i][0] != '-') file = argv[i]; - } else { - driver_errf(PKG_TOOL, "unpack: unknown option: %s", argv[i]); + else { + driver_errf(PKG_TOOL, "%s: unknown option: %s", + unpack ? "unpack" : "verify", argv[i]); return 2; } } if (!file) { - driver_errf(PKG_TOOL, "unpack: FILE.cfpkg is required"); + driver_errf(PKG_TOOL, "%s: FILE is required", unpack ? "unpack" : "verify"); 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; - } + if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(file); + if (fmt == PKG_FMT_TARGZ) + return pkg_verify_portable(env, ctx, file, pubkey, tofu, + unpack ? dir : NULL, unpack) == DIST_OK + ? 0 + : 1; + return pkg_verify_native(env, ctx, file, pubkey, tofu, unpack ? dir : NULL, + unpack) == DIST_OK + ? 0 + : 1; +} - 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; +static int pkg_inspect(const CfreeContext* ctx, int argc, char** argv) { + const char* file = NULL; + PkgFormat fmt = PKG_FMT_AUTO; + int i, rc = 1; + for (i = 0; i < argc; ++i) { + if (driver_streq(argv[i], "--format") && i + 1 < argc) { + fmt = pkg_parse_format(argv[++i]); + if (fmt == PKG_FMT_AUTO) { + driver_errf(PKG_TOOL, "inspect: unknown format"); + return 2; } } - 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; + else if (argv[i][0] != '-') + file = argv[i]; + else { + driver_errf(PKG_TOOL, "inspect: unknown option: %s", argv[i]); + return 2; } - 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"); + driver_errf(PKG_TOOL, "inspect: FILE 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; + if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(file); + if (fmt == PKG_FMT_TARGZ) { + CfreeFileData fd = {0}; + CfreeWriter* inflated = NULL; + DistTarEntry entries[DIST_MAX_FILES]; + size_t ne = 0; + const DistTarEntry* man; + if (pkg_load_portable(ctx, file, &fd, &inflated, entries, &ne) == DIST_OK && + (man = pkg_find_name(entries, ne, PKG_META_MANIFEST)) != NULL) { + driver_printf("%.*s", (int)man->size, (const char*)man->data); + rc = 0; + } + if (inflated) cfree_writer_close(inflated); + if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd); + return rc; + } else { + CfreeFileData fd = {0}; + DistCfpkgHeader h; + if (pkg_read_file(ctx, file, &fd) == DIST_OK && + dist_cfpkg_read_header(fd.data, fd.size, &h) == DIST_OK && + pkg_bounds(&h, fd.size) == DIST_OK) { + driver_printf("%.*s", (int)h.manifest_size, + (const char*)(fd.data + h.manifest_offset)); + rc = 0; + } else { + driver_errf(PKG_TOOL, "malformed native package"); + } + if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd); + return rc; } - 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) { @@ -775,7 +1103,6 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, 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] : ""; @@ -785,7 +1112,6 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, 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; @@ -796,29 +1122,19 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, } 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; - } + if (!ok) 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"); + DIST_OK) 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; @@ -826,38 +1142,21 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, 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; - + int rc = 1; 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"); + dist_hex_decode(want, idhex, DIST_KEYID_LEN) != DIST_OK) 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); + if (pkg_read_file(ctx, tpath, &old) != DIST_OK) return 0; + if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) != CFREE_OK) return 1; - } rc = 0; while (pos < old.size) { size_t start = pos, end = pos; @@ -865,15 +1164,12 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, 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) { + memcmp(got, want, DIST_KEYID_LEN) == 0) keep = 0; - removed = 1; - } } if (keep) { size_t n = (end < old.size ? end + 1 : end) - start; @@ -884,52 +1180,40 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, 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")) { + if (driver_streq(sub, "keygen")) rc = pkg_keygen(&env, &ctx, argc - 2, argv + 2); - } else if (driver_streq(sub, "create")) { + 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")) { + else if (driver_streq(sub, "verify")) + rc = pkg_verify_or_unpack(&env, &ctx, argc - 2, argv + 2, 0); + else if (driver_streq(sub, "unpack")) + rc = pkg_verify_or_unpack(&env, &ctx, argc - 2, argv + 2, 1); + else if (driver_streq(sub, "inspect")) rc = pkg_inspect(&ctx, argc - 2, argv + 2); - } else if (driver_streq(sub, "trust")) { + else if (driver_streq(sub, "trust")) rc = pkg_trust(&env, &ctx, argc - 2, argv + 2); - } else { + else { driver_errf(PKG_TOOL, "unknown subcommand: %s", sub); rc = 2; } - driver_env_fini(&env); return rc; }