kit

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

commit 54d233934315c9eb5df71573041f9e4c6488f000
parent b3ebd86493250b7a890baa07b4f2ad2106a9470f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon,  1 Jun 2026 14:30:36 -0700

dist: move CAS + signed-package subsystem into libcfree

Move driver/dist/ into the library behind two public headers:
  - include/cfree/cas.h   (+ src/api/cas.c): content-addressed store
  - include/cfree/package.h (+ src/api/package.c): signed packages

The dist_* model/codec/crypto now lives in src/dist/ (gated by new
CFREE_CAS_ENABLED / CFREE_PKG_ENABLED subsystem flags), and the vendored
monocypher/lz4 trees move to a top-level vendor/. The cfree cas/pkg tools
become thin CLIs over the public API via a CfreeCasHost vtable
(driver/lib/dist_host.c); operational errors flow through ctx->diag while
arg-parsing and trusted-keys path/pin policy stay in the driver.

Design + plan: doc/DISTRIBUTE.md, doc/plan/DIST_LIBRARY.md.

Diffstat:
MMakefile | 45+++++++++++++++++++++++++++++++++------------
Mdoc/DESIGN.md | 11+++++++----
Mdoc/DISTRIBUTE.md | 50++++++++++++++++++++++++++++++--------------------
Adoc/plan/DIST_LIBRARY.md | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/cmd/cas.c | 419++++++++++++++++++++++---------------------------------------------------------
Mdriver/cmd/pkg.c | 2115++++++++++++-------------------------------------------------------------------
Ddriver/dist/blake2b.h | 23-----------------------
Ddriver/dist/ed25519.c | 24------------------------
Ddriver/dist/lz4.c | 39---------------------------------------
Adriver/lib/dist_host.c | 32++++++++++++++++++++++++++++++++
Adriver/lib/dist_host.h | 21+++++++++++++++++++++
Ainclude/cfree/cas.h | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/cfree/config.h | 6++++++
Ainclude/cfree/package.h | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/api/cas.c | 392+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/api/package.c | 1777+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/core/config_assert.c | 6++++++
Rdriver/dist/b64.c -> src/dist/b64.c | 0
Rdriver/dist/b64.h -> src/dist/b64.h | 0
Rdriver/dist/blake2b.c -> src/dist/blake2b.c | 0
Asrc/dist/blake2b.h | 23+++++++++++++++++++++++
Rdriver/dist/blob.c -> src/dist/blob.c | 0
Rdriver/dist/blob.h -> src/dist/blob.h | 0
Rdriver/dist/cas.c -> src/dist/cas.c | 0
Rdriver/dist/cas.h -> src/dist/cas.h | 0
Rdriver/dist/cfpkg.c -> src/dist/cfpkg.c | 0
Rdriver/dist/cfpkg.h -> src/dist/cfpkg.h | 0
Rdriver/dist/deflate.c -> src/dist/deflate.c | 0
Rdriver/dist/deflate.h -> src/dist/deflate.h | 0
Rdriver/dist/dist.c -> src/dist/dist.c | 0
Rdriver/dist/dist.h -> src/dist/dist.h | 0
Asrc/dist/ed25519.c | 24++++++++++++++++++++++++
Rdriver/dist/ed25519.h -> src/dist/ed25519.h | 0
Asrc/dist/lz4.c | 39+++++++++++++++++++++++++++++++++++++++
Rdriver/dist/lz4.h -> src/dist/lz4.h | 0
Rdriver/dist/manifest.c -> src/dist/manifest.c | 0
Rdriver/dist/manifest.h -> src/dist/manifest.h | 0
Rdriver/dist/minisig.c -> src/dist/minisig.c | 0
Rdriver/dist/minisig.h -> src/dist/minisig.h | 0
Rdriver/dist/tar.c -> src/dist/tar.c | 0
Rdriver/dist/tar.h -> src/dist/tar.h | 0
Rdriver/dist/tree.c -> src/dist/tree.c | 0
Rdriver/dist/tree.h -> src/dist/tree.h | 0
Rdriver/dist/trust.c -> src/dist/trust.c | 0
Rdriver/dist/trust.h -> src/dist/trust.h | 0
Rdriver/dist/vendor/lz4/LICENSE -> vendor/lz4/LICENSE | 0
Rdriver/dist/vendor/lz4/lz4.c -> vendor/lz4/lz4.c | 0
Rdriver/dist/vendor/lz4/lz4.h -> vendor/lz4/lz4.h | 0
Rdriver/dist/vendor/monocypher/LICENCE.md -> vendor/monocypher/LICENCE.md | 0
Rdriver/dist/vendor/monocypher/monocypher-ed25519.c -> vendor/monocypher/monocypher-ed25519.c | 0
Rdriver/dist/vendor/monocypher/monocypher-ed25519.h -> vendor/monocypher/monocypher-ed25519.h | 0
Rdriver/dist/vendor/monocypher/monocypher.c -> vendor/monocypher/monocypher.c | 0
Rdriver/dist/vendor/monocypher/monocypher.h -> vendor/monocypher/monocypher.h | 0
53 files changed, 3355 insertions(+), 2235 deletions(-)

diff --git a/Makefile b/Makefile @@ -95,7 +95,8 @@ include mk/config.mk # directories, and ABI implementations are added below from their own groups. LIB_SRCS_ABI_CORE = src/abi/abi.c src/abi/registry.c LIB_SRCS_API_CORE = $(filter-out src/api/archive.c src/api/disasm.c \ - src/api/link.c src/api/stubs.c,$(wildcard src/api/*.c)) + src/api/link.c src/api/cas.c src/api/package.c \ + src/api/stubs.c,$(wildcard src/api/*.c)) LIB_SRCS_ARCH_CORE = $(filter-out src/arch/%_stubs.c,$(wildcard src/arch/*.c)) LIB_SRCS_ASM_CORE = $(wildcard src/asm/*.c) LIB_SRCS_CG_CORE = $(wildcard src/cg/*.c) @@ -174,6 +175,19 @@ LIB_SRCS_WASM_CORE := $(shell find src/wasm -name '*.c' 2>/dev/null) LIB_SRCS_API_AR = src/api/archive.c LIB_SRCS_API_DISASM = src/api/disasm.c LIB_SRCS_API_LINK = src/api/link.c +# Distribution subsystem (content store + signed packages). The cas layer +# needs blake2b (-> monocypher); the pkg layer adds the crypto/compression/ +# container shims and the second monocypher TU. lz4's vendored .c is #included +# by src/dist/lz4.c (amalgamation), so it is NOT compiled standalone. +LIB_SRCS_API_CAS = src/api/cas.c +LIB_SRCS_API_PKG = src/api/package.c +LIB_SRCS_DIST_CAS = src/dist/dist.c src/dist/blake2b.c src/dist/blob.c \ + src/dist/tree.c src/dist/cas.c +LIB_SRCS_VENDOR_CAS = vendor/monocypher/monocypher.c +LIB_SRCS_DIST_PKG = src/dist/b64.c src/dist/ed25519.c src/dist/minisig.c \ + src/dist/tar.c src/dist/deflate.c src/dist/lz4.c \ + src/dist/cfpkg.c src/dist/manifest.c src/dist/trust.c +LIB_SRCS_VENDOR_PKG = vendor/monocypher/monocypher-ed25519.c LIB_SRCS_DEBUG := $(shell find src/debug -name '*.c' 2>/dev/null) LIB_SRCS_DBG := $(shell find src/dbg -name '*.c' 2>/dev/null) LIB_SRCS_EMU := $(shell find src/emu -name '*.c' 2>/dev/null) \ @@ -217,6 +231,12 @@ endif ifeq ($(CFREE_EMU_ENABLED),1) LIB_SRCS += $(LIB_SRCS_EMU) endif +ifeq ($(CFREE_CAS_ENABLED),1) +LIB_SRCS += $(LIB_SRCS_API_CAS) $(LIB_SRCS_DIST_CAS) $(LIB_SRCS_VENDOR_CAS) +endif +ifeq ($(CFREE_PKG_ENABLED),1) +LIB_SRCS += $(LIB_SRCS_API_PKG) $(LIB_SRCS_DIST_PKG) $(LIB_SRCS_VENDOR_PKG) +endif ifeq ($(CFREE_ARCH_AA64_ENABLED),1) LIB_SRCS += $(LIB_SRCS_ARCH_AA64) endif @@ -299,7 +319,8 @@ LIB_ASMS = ifeq ($(CFREE_JIT_ENABLED),1) LIB_ASMS += $(LIB_SRCS_JIT_ASM) endif -LIB_OBJS = $(patsubst src/%.c,$(BUILD_DIR)/lib/%.o,$(LIB_SRCS)) \ +LIB_OBJS = $(patsubst src/%.c,$(BUILD_DIR)/lib/%.o,$(filter src/%.c,$(LIB_SRCS))) \ + $(patsubst vendor/%.c,$(BUILD_DIR)/vendor/%.o,$(filter vendor/%.c,$(LIB_SRCS))) \ $(LANG_OBJS) \ $(patsubst src/%.S,$(BUILD_DIR)/lib/%.o,$(LIB_ASMS)) LIB_DEPS = $(LIB_OBJS:.o=.d) @@ -358,23 +379,17 @@ endif ifeq ($(CFREE_TOOL_STRINGS_ENABLED),1) DRIVER_TOOL_SRCS += driver/cmd/strings.c endif +# The cas/pkg tools link libcfree's public cas/package APIs (gated by +# CFREE_CAS_ENABLED / CFREE_PKG_ENABLED, asserted by config_assert.c); the +# dist implementation and vendored primitives live in the library now. ifneq ($(filter 1,$(CFREE_TOOL_CAS_ENABLED) $(CFREE_TOOL_PKG_ENABLED)),) -DRIVER_TOOL_SRCS += driver/dist/dist.c driver/dist/blake2b.c \ - driver/dist/blob.c driver/dist/tree.c driver/dist/cas.c +DRIVER_TOOL_SRCS += driver/lib/dist_host.c endif ifeq ($(CFREE_TOOL_CAS_ENABLED),1) DRIVER_TOOL_SRCS += driver/cmd/cas.c endif ifeq ($(CFREE_TOOL_PKG_ENABLED),1) DRIVER_TOOL_SRCS += driver/cmd/pkg.c -DRIVER_TOOL_SRCS += driver/dist/dist.c driver/dist/b64.c \ - driver/dist/blake2b.c driver/dist/ed25519.c \ - driver/dist/vendor/monocypher/monocypher.c \ - driver/dist/vendor/monocypher/monocypher-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 DRIVER_SRCS += $(sort $(DRIVER_TOOL_SRCS)) ifneq ($(filter 1,$(CFREE_TOOL_CC_ENABLED) $(CFREE_TOOL_CHECK_ENABLED) $(CFREE_TOOL_CPP_ENABLED) $(CFREE_TOOL_AS_ENABLED) $(CFREE_TOOL_DBG_ENABLED) $(CFREE_TOOL_RUN_ENABLED)),) @@ -471,6 +486,12 @@ $(BUILD_DIR)/lib/%.o: src/%.S Makefile $(BUILD_CONFIG) @mkdir -p $(dir $@) $(CC) $(LIB_CFLAGS) $(DEPFLAGS) -c $< -o $@ +# Vendored third-party sources (monocypher) compiled into libcfree. Same +# freestanding flags as the rest of the library; symbols stay hidden. +$(BUILD_DIR)/vendor/%.o: vendor/%.c Makefile $(BUILD_CONFIG) + @mkdir -p $(dir $@) + $(CC) $(LIB_CFLAGS) $(DEPFLAGS) -c $< -o $@ + $(BUILD_DIR)/driver/%.o: driver/%.c Makefile $(BUILD_CONFIG) @mkdir -p $(dir $@) $(CC) $(DRIVER_CFLAGS) $(DEPFLAGS) -c $< -o $@ diff --git a/doc/DESIGN.md b/doc/DESIGN.md @@ -79,8 +79,10 @@ driver/ CLI policy + host I/O. Includes ONLY <cfree/*.h>. - **`driver/`** implements the multi-call binary. `driver/main.c` holds the central tool table; each tool (`cc`, `as`, `ld`, `ar`, `run`, `dbg`, `emu`, `cas`, `pkg`, …) translates command-line flags into public API calls and - supplies the host vtables. `driver/dist/` carries the content-addressed store - and `.cfpkg` packaging (tar/deflate/lz4, BLAKE2b/SHA-256, ed25519/minisign). + supplies the host vtables. The content-addressed store and `.cfpkg` packaging + (tar/deflate/lz4, BLAKE2b, ed25519/minisign) are a libcfree subsystem + (`src/dist/`, behind `<cfree/cas.h>` / `<cfree/package.h>`); `cas`/`pkg` are + thin CLIs over it. - **`lang/`** holds the frontends. `lang/c` preprocesses (`lang/cpp`), parses, type-checks, manages C declarations, and drives the public CG API; `lang/toy` and `lang/wasm` are smaller frontends exercising the same boundary. Each @@ -94,7 +96,8 @@ driver/ CLI policy + host I/O. Includes ONLY <cfree/*.h>. place where public types meet internal subsystems. - **`src/`** subsystems do the work: `core` (arenas, vectors, buffers, symbol interning, diagnostics, hashing), `abi`, `arch`, `asm`, `cg`, `opt`, `obj`, - `link`, `jit`, `dbg`, `emu`, `interp`, `debug` (DWARF), `wasm`, and `os`. + `link`, `jit`, `dbg`, `emu`, `interp`, `debug` (DWARF), `wasm`, `os`, and + `dist` (content-addressed store + signed `.cfpkg` packaging). **The layering invariant:** `driver/` and `lang/` include only `<cfree/*.h>` — never a `src/` header. Anything a frontend or tool needs is promoted into the @@ -214,7 +217,7 @@ unless an API states otherwise. | [DBG.md](DBG.md) | The debugger: breakpoints, single-step, displaced execution, register/memory access. | | [CBACKEND.md](CBACKEND.md) | The portable C-source backend (`src/arch/c_target/`). | | [WASM.md](WASM.md) | The WebAssembly backend, object form, and host-import binding. | -| [DISTRIBUTE.md](DISTRIBUTE.md) | Signed `.cfpkg` packaging and the content-addressed store (`driver/dist/`, `cas`/`pkg` tools). | +| [DISTRIBUTE.md](DISTRIBUTE.md) | Signed `.cfpkg` packaging and the content-addressed store (`src/dist/`, `<cfree/cas.h>` / `<cfree/package.h>`, `cas`/`pkg` tools). | | [DRIVER.md](DRIVER.md) | The multi-call binary, tool registry, and command-line policy. | | [RUNTIME.md](RUNTIME.md) | The freestanding headers and compiler-rt/libc-style support in `rt/`. | | [BUILD.md](BUILD.md) | The build system and `CFREE_*_ENABLED` component gating. | diff --git a/doc/DISTRIBUTE.md b/doc/DISTRIBUTE.md @@ -3,11 +3,14 @@ cfree ships signed, content-addressed software packages with zero host library dependencies. Everything the package pipeline needs — BLAKE2b and Ed25519 (via vendored monocypher), the minisign file format, DEFLATE/gzip, -LZ4, base64, and ustar tar — is vendored under `driver/dist/`, so a stock +LZ4, base64, and ustar tar — is vendored under `src/dist/`, so a stock cfree binary can create, sign, verify, inspect, and unpack packages without -linking OpenSSL, zlib, libsodium, or libarchive. Two driver tools surface the -subsystem: `cfree cas` (the shared content store) and `cfree pkg` (signed -packages). See [DRIVER.md](DRIVER.md) for how these slot into the multitool. +linking OpenSSL, zlib, libsodium, or libarchive. The subsystem is a libcfree +component, gated by `CFREE_CAS_ENABLED` / `CFREE_PKG_ENABLED` and exposed +through two public headers — `<cfree/cas.h>` (the content store) and +`<cfree/package.h>` (signed packages). Two thin driver tools surface it on the +command line: `cfree cas` and `cfree pkg`. See [DRIVER.md](DRIVER.md) for how +these slot into the multitool. ## Why this shape @@ -15,7 +18,7 @@ Three design decisions drive the whole subsystem: - **No host crypto/compression.** A self-hosting toolchain that depended on the host's OpenSSL or zlib would not be freestanding. The primitives are - small, audited, and checked into the tree (`driver/dist/vendor/`), wrapped + small, audited, and checked into the tree (`vendor/`), wrapped behind narrow `dist_*` shims so the rest of the code never touches a vendor API directly. - **Content identity before trust.** The store layer is self-verifying by @@ -30,25 +33,32 @@ Three design decisions drive the whole subsystem: ## Layering ``` - cfree pkg / cfree cas driver/cmd/{pkg,cas}.c + cfree pkg / cfree cas driver/cmd/{pkg,cas}.c (thin CLIs) | - package model (signed) driver/dist/{manifest,cfpkg}.c + public API include/cfree/{cas,package}.h + src/api/{cas,package}.c | - content model (self-verifying) driver/dist/{tree,blob,cas}.c + package model (signed) src/dist/{manifest,cfpkg}.c | - vendored primitives driver/dist/{blake2b,ed25519,minisig, + content model (self-verifying) src/dist/{tree,blob,cas}.c + | + vendored primitives src/dist/{blake2b,ed25519,minisig, deflate,lz4,b64,tar}.c - driver/dist/vendor/{monocypher,lz4} + vendor/{monocypher,lz4} ``` The content model knows nothing about signatures or package names. The package -model adds a signed claim over content ids. The CLI tools wire these to the -host filesystem and CSPRNG through `DriverEnv`/`CfreeFileIO`; the `dist_*` -layers themselves source no entropy and do no I/O beyond writer callbacks. +model adds a signed claim over content ids. `src/api/{cas,package}.c` compose +the internal `dist_*` model into the public `cfree_cas_*` / `cfree_pkg_*` API; +the CLI tools wire that to the host filesystem and CSPRNG through a +`CfreeCasHost` vtable (`driver/lib/dist_host.c`). The library sources no entropy +and does no I/O beyond the host vtable and writer callbacks; operational errors +return a `CfreeStatus` and emit detail through `ctx->diag`, while argument +parsing and trusted-keys path/pin policy stay in the driver. ## Vendored primitives -| Shim (`driver/dist/`) | Backed by | Used for | +| Shim (`src/dist/`) | Backed by | Used for | |---|---|---| | `blake2b.c` | monocypher | content ids, region/merkle roots, minisign checksums | | `ed25519.c` | monocypher | minisign signature scheme | @@ -83,7 +93,7 @@ blob-id = BLAKE2b-256(raw file bytes) ``` Blobs also carry a **chunk merkle root** (`dist_blob_root`, in -`driver/dist/blob.c`) computed over fixed-size chunks (default 64 KiB, +`src/dist/blob.c`) computed over fixed-size chunks (default 64 KiB, `DIST_BLOB_CHUNK_SIZE_DEFAULT`). Leaves are domain-separated hashes of `("cfree blob leaf v1" || u64le chunk-index || u64le raw-size || bytes)`; interior nodes hash `("cfree blob node v1" || left || right)`, pairing @@ -99,7 +109,7 @@ can accept chunks as they arrive without holding the whole file. ### Trees A tree is a deterministic manifest for one output directory -(`driver/dist/tree.c`). It is strict, byte-stable INI-style text beginning with +(`src/dist/tree.c`). It is strict, byte-stable INI-style text beginning with `cfree-tree 1`, one `[file]` section per regular file, sorted bytewise by path. Each entry records path, mode (`-` regular or `x` executable; directories are implicit), size, `blob` id, and `root`. Unknown keys/sections, duplicate @@ -113,7 +123,7 @@ tree-id = BLAKE2b-256(canonical tree manifest bytes) ### CAS layout -`cfree cas` maintains a shared on-disk store (`driver/dist/cas.c`): +`cfree cas` maintains a shared on-disk store (`src/dist/cas.c`): ``` <cas>/ @@ -135,7 +145,7 @@ writing). ## Package model A package is a signed claim over one or more output trees. The signed object is -the **package manifest** (`driver/dist/manifest.c`), strict byte-stable text +the **package manifest** (`src/dist/manifest.c`), strict byte-stable text beginning with `cfree-package 3`: top-level name/version/description plus `[output]` sections (each naming a numeric id, a human-readable name, a tree id, an optional target triple, and an optional default flag), `[artifact]` @@ -169,7 +179,7 @@ pkgid does not match. This binds the signature to the exact manifest content, not merely to a name. Trust anchors live in a trusted-keys file (`$CFREE_TRUSTED_KEYS`, else -`$HOME/.config/cfree/trusted_keys`; `driver/dist/trust.c`), one +`$HOME/.config/cfree/trusted_keys`; `src/dist/trust.c`), one `keyid pubkey label` line each. A `.pub` bundled inside a package is never trusted on its own. The verifier picks a key by the signature's key id: @@ -210,7 +220,7 @@ blob by `blob-id` and `blob-root`, and checks that artifact overlays resolve. ### Native `.cfpkg` -A signed pack (`driver/dist/cfpkg.c`, `pkg_create_cfpkg`) with a fixed +A signed pack (`src/dist/cfpkg.c`, `pkg_create_cfpkg`) with a fixed trust-neutral header (`cfpkg3\0`, 96 bytes) that only locates the early signed metadata: manifest, manifest signature, encoding descriptor, descriptor signature, and bundled pubkey. It supports three shapes from one format: diff --git a/doc/plan/DIST_LIBRARY.md b/doc/plan/DIST_LIBRARY.md @@ -0,0 +1,290 @@ +# Distribution as a library subsystem + +> **Status: implemented.** The migration below has landed (one commit): the +> dist subsystem moved to `src/dist/` (+ top-level `vendor/`), exposed through +> `<cfree/cas.h>` / `<cfree/package.h>` (`src/api/{cas,package}.c`), gated by +> `CFREE_CAS_ENABLED` / `CFREE_PKG_ENABLED`; `cfree cas` / `cfree pkg` are thin +> CLIs over the public API via a `CfreeCasHost` vtable (`driver/lib/dist_host.c`), +> with operational errors flowing through `ctx->diag`. Verified green: +> `test-driver-cas` (41) + `test-driver-pkg` (182) under ASan/UBSan. +> +> **Deferred:** the v2 deletion + `3`-suffix rename (Stage 3, below) were *not* +> done — the dead v2 code was carried over unchanged. On inspection the deletion +> is more surgical than the line-ranges below imply: the v2 *extern* surface +> (`DistManifest`/`DistArtifact`/`DistDependency`, `dist_manifest_*`, +> `dist_cfpkg2_*`, the v2 `DistCfpkg*` structs + `dist_cfpkg_*` v2 codecs) is +> safely unreferenced, but the v2 and v3 manifest parsers **share** the static +> helpers `set_err` / `trim_lead` / `trim_trail` / `copy_field` / `kind_valid` +> in `src/dist/manifest.c` (only `parse_u64`, the v2 `finalize`, and +> `dist_manifest_path_valid` are v2-only). The cleanup must keep the shared +> helpers — verify the same in `src/dist/cfpkg.c` — and recompile + rerun the +> cas/pkg suites after. + +Signed, content-addressed distribution (`cfree cas` / `cfree pkg`) is today the +only major capability that lives **entirely inside `driver/`** — its model, +its vendored crypto/compression, and its create/verify/unpack pipelines all sit +under `driver/dist/` and `driver/cmd/{cas,pkg}.c`. Every other capability is a +libcfree subsystem behind `include/cfree/`, with the CLI tool a thin +arg-parser on top. This doc captures the plan to bring distribution into the +same shape: move the implementation into the library, expose it through two +public headers, and reduce `cas.c`/`pkg.c` to flag-parsing + host wiring. The +design it realizes is in [../DISTRIBUTE.md](../DISTRIBUTE.md); the precedent it +follows is the `ar` subsystem (`src/api/archive.c` + `include/cfree/archive.h`, +gated by `CFREE_AR_ENABLED` distinct from `CFREE_TOOL_AR_ENABLED`). + +## Goal + +`libcfree.a` gains a content-store API and a signed-package API, gated by their +own subsystem flags so a minimal embedding pays nothing for them. The `cfree +cas` and `cfree pkg` tools become thin CLIs that translate flags into public +calls and supply host vtables — exactly like `ar`, `ld`, `objdump`. An embedder +can create, sign, verify, inspect, and unpack packages, and drive a CAS, without +the driver and without linking host crypto/compression. + +## Why this is the right shape (not CLI-only logic) + +Two layers are stacked under `driver/dist/`, and they have very different +readiness: + +- **The `dist_*` byte model** (`driver/dist/*.c`, ~6.4k lines plus ~6.7k + vendored) is **already written to the public boundary's contract.** It + includes only `<cfree/core.h>` plus its own headers — no `driver.h`/`env.h`. + It sources no entropy and does no I/O except through `CfreeWriter` callbacks + and a small host vtable (`DistCasHost` = `CfreeFileIO` + `mkdir_p` + + `mark_executable`). This obeys the "host supplies all side effects" principle + verbatim. Moving it is near-mechanical. + +- **The `pkg_*` / `cas_*` orchestration** (`driver/cmd/pkg.c` 2123 lines, + `cas.c` 491 lines) holds the valuable pipelines — `pkg_create_targz`, + `pkg_create_cfpkg`, `pkg_verify_portable`, `pkg_verify_native`, blob + reconstruction, trust/key resolution — but is entangled with the CLI. The + glue to unwind, by call count in `pkg.c`: + - `driver_errf` ×88 — stderr error reporting → structured error returns / the + `CfreeContext` diag sink (the `dist_*` parsers already take + `char* err, size_t errcap`). + - `driver_mkdir_p`, `driver_mark_executable_output`, + `driver_walk_regular_files` — host filesystem ops beyond `CfreeFileIO`. + - `driver_random_bytes` ×2 — host CSPRNG, only for keygen. + - `driver_getenv` ×2 — trust-file path defaulting + (`$CFREE_TRUSTED_KEYS` / `$HOME`); env-var *policy* that **stays in the + driver**. + - `driver_streq` / `driver_printf` / `driver_has_suffix` — arg parsing and + stdout formatting; **stay in the driver**. + +The layering invariant forces the move: `driver/` may include only +`<cfree/*.h>`, and `src/api` may not include `driver/` headers — so a public +boundary is impossible while the code sits in `driver/dist/`. Relocating to +`src/` is a precondition, not a cleanup. + +## Target tree layout + +``` +vendor/ # top-level: pristine third-party trees + monocypher/ # (moved from driver/dist/vendor/monocypher) + lz4/ # (moved from driver/dist/vendor/lz4) +include/cfree/cas.h # content model: blob/tree hashing + CAS store +include/cfree/package.h # package model: manifest, sign/verify, create/unpack +src/api/cas.c # public handles <-> internal (archive.c precedent) +src/api/package.c +src/dist/ # moved dist_* subsystem (private headers) + dist.{c,h} blob.{c,h} tree.{c,h} cas.{c,h} + manifest.{c,h} cfpkg.{c,h} trust.{c,h} + blake2b.{c,h} ed25519.{c,h} minisig.{c,h} b64.{c,h} + deflate.{c,h} lz4.{c,h} tar.{c,h} # cfree-maintained shims/extracts +``` + +Vendor split, confirmed by inspection: only **monocypher** and **lz4** are +pristine third-party trees pulled in by `#include` — they move to a repo-root +`vendor/`. `deflate.c` is a cfree-maintained *extract* of miniz (already +modified, not pristine), and `b64.c` / `tar.c` are self-contained — these stay +in `src/dist/`. The shim includes that currently read +`"vendor/monocypher/..."` (e.g. `blake2b.h`, `ed25519.c`) get rewritten to the +new top-level path. + +## Config gating + +Add subsystem flags to `include/cfree/config.h`, separate from the tool flags +(mirroring `CFREE_AR_ENABLED` vs `CFREE_TOOL_AR_ENABLED`): + +```c +#define CFREE_CAS_ENABLED 1 /* content store: src/dist/{blob,tree,cas} + cfree/cas.h */ +#define CFREE_PKG_ENABLED 1 /* signed packages: adds manifest/cfpkg/minisig/crypto + cfree/package.h */ +``` + +`CFREE_PKG_ENABLED` implies `CFREE_CAS_ENABLED` (packages are built over the +content model). `CFREE_TOOL_CAS_ENABLED` / `CFREE_TOOL_PKG_ENABLED` stay and +assert their subsystem flag. Off → the units (and the vendored crypto) drop +entirely, so a minimal embedding carries no Ed25519/BLAKE2b/DEFLATE/LZ4. The +Makefile's `LIB_SRCS_*` gains a dist regime that pulls `src/dist/*.c` plus the +enabled `vendor/` trees. + +## Public API surface + +Two headers, mirroring DISTRIBUTE.md's content-model vs signed-package split. +Model structs are exposed as POD (renamed to the `Cfree*` convention); the +vendored primitives and the cfpkg wire codecs stay internal. + +### `include/cfree/cas.h` — content model (self-verifying, no trust) + +- POD types: `CfreeTree`, `CfreeTreeEntry`, `CfreeBlobInfo`. +- Pure hashing (no I/O): `cfree_blob_id`, `cfree_blob_root`, `cfree_blob_info`, + `cfree_tree_id`, `cfree_tree_emit`, `cfree_tree_parse`, `cfree_tree_find`. +- A `CfreeCas` handle over `CfreeContext` + a host vtable: + `cfree_cas_open`, `cfree_cas_put_blob` / `get_blob`, + `cfree_cas_put_tree` / `get_tree`, `cfree_cas_add_tree_from_dir`, + `cfree_cas_verify_tree`, `cfree_cas_materialize`. + +### `include/cfree/package.h` — package model (signed) + +- POD model: `CfreePackageManifest` with its outputs / artifacts / deps; a + public `CfreePackageEncoding` descriptor (region layout, chunk-index summary, + external-fetch templates) so `inspect --encoding` and external-fetch planning + are real library features. +- Keys / trust: `CfreeMinisigKeypair`; `cfree_pkg_keygen` (entropy injected via + the host vtable, never read by the library); pubkey/seckey emit + parse; + `cfree_pkg_sign` / `cfree_pkg_verify_signature`. Trust resolution takes + **explicit** trusted-keys bytes — the library reads no env vars and no + `$HOME`; the driver supplies the resolved path/bytes. +- Pipelines as opts-struct calls: `cfree_pkg_create` (format `cfpkg`|`tar.gz`, + native-shape `fat`|`metadata`|`thin`, compression, source = `--root` dir or + `cas + tree`, external dir), `cfree_pkg_verify`, `cfree_pkg_unpack`, + `cfree_pkg_inspect`. + +### Kept internal (`src/dist/` private headers) + +All vendored code; the `dist_blake2b` / `dist_ed25519` / `dist_minisig` / +`dist_b64` / `dist_gz` / `dist_lz4` / `dist_tar` shims; and the cfpkg wire +codecs (header / descriptor / index encode-decode). Rationale: raw crypto and +on-wire binary layout are implementation detail — exposing them invites misuse +and an API-stability burden. The logical model and pipelines are the contract. + +## New host capabilities + +The library reaches the host through `CfreeContext.file_io` (read/write, +already present) plus one new vtable for the operations `CfreeFileIO` doesn't +cover — every one of which the driver already implements: + +```c +typedef struct CfreeDistHost { + int (*mkdir_p)(void* user, const char* path); + int (*mark_executable)(void* user, const char* path); + int (*walk_regular_files)(void* user, const char* root, /* callback */ ...); + int (*fill_random)(void* user, uint8_t* out, size_t n); /* keygen only */ + void* user; +} CfreeDistHost; +``` + +`DistCasHost` already models `mkdir_p` + `mark_executable`; this generalizes it +and adds the directory walk (`driver_walk_regular_files`) and CSPRNG +(`driver_random_bytes`). Naming/placement TBD during Stage 2 (could fold the +CAS-only subset into `CfreeCas` and keep `fill_random` package-side). + +## Error reporting (decided) + +Public dist calls **return `CfreeStatus`** and **emit human-readable detail +through `ctx->diag`** — not through an err-buffer at the boundary. This is the +established convention, not a new pattern: + +- `CfreeContext` carries `CfreeDiagSink* diag` directly (`core.h`), so the sink + is reachable without a `CfreeCompiler` — exactly as the pure-byte subsystems + (object, archive, dwarf) get it. +- It mirrors the linker: `src/link/link_layout.c` emits operational errors such + as "linker script: undefined symbol …" through the sink and returns a status. + Package/CAS errors are the same shape — operational, no source position. +- `CfreeStatus` already carries the right categories: `CFREE_MALFORMED` (bad + manifest/tree/signature), `CFREE_NOT_FOUND` (missing blob/tree/key), + `CFREE_IO`, `CFREE_INVALID` (unsafe path), `CFREE_UNSUPPORTED` (encrypted + seckey / scrypt). The status is the machine-readable category; the diag + message is the actionable detail (`"blob root mismatch for: <path>"`). + +Mechanics: + +- **No source location.** Emit with a zero `CfreeSrcLoc` (file_id 0), as the + linker does for non-source errors; the host stderr sink already tolerates it. +- **The internal `dist_*` parsers keep their `(char* err, size_t errcap)` + buffer** unchanged. The `src/api` wrapper catches that string and forwards it + to `ctx->diag`, so the byte model barely changes and its detailed parse + messages survive intact. +- **A small internal `api_diagf(ctx, kind, fmt, …)` helper** over + `ctx->diag->emit` (no-op when `diag` is NULL) packs varargs for the api layer. +- **The 88 `driver_errf` sites split by ownership.** Operational/pipeline errors + (create / verify / unpack / resolve) move into `src/api/package.c` as diag + emits; pure arg-parse errors (`"unknown option"`, `"-o BASE is required"`) + stay in `pkg.c` as `driver_errf`, because argument parsing is driver policy. +- **Embedder control.** The sink bumps its `errors` counter and prints. For the + CLI that is exactly today's `driver_errf` behavior. An embedder doing + speculative verification supplies its own (or no) sink and reads only the + `CfreeStatus`, so a failed verify stays quiet. + +## Versioning: latest-only (decided) + +We support only the current on-disk format and drop all back-compat code. This +is verified to be **pure deletion with zero behavioral change**: every v2 symbol +(`dist_manifest_*`, the non-`3` `dist_cfpkg_*`, `dist_cfpkg2_*`, and the +`DistManifest` / `DistArtifact` / `DistDependency` / `DistCfpkgHeader` / +`DistCfpkgDescriptor` / `DistCfpkgIndexRecord` structs) is referenced *only* in +its own definition files — never by `pkg.c`, `cas.c`, or any test. The driver +already emits and reads v3 exclusively. + +Dropping it pays off twice: + +1. Deletes the dead v2 structs / functions / constants from `manifest.c` and + `cfpkg.c`. +2. Lets the survivors **shed the `3` suffix** as they go public: + `DistPackageManifest` → `CfreePackageManifest`, `DistCfpkg3Header` → + `CfreePackageHeader`, internal `dist_cfpkg3_*` → `dist_cfpkg_*`. The versioned + naming only existed to coexist with v2. + +**Precision:** drop the v2 *parse paths and C identifiers*, but keep the on-disk +wire magic at `cfpkg3\0` / `cfree-package 3` / `cfree-encoding 3`. "Latest +version" means v3 on disk; renumbering the wire format would itself break +anything already produced. We stop *accepting* v2 input; we do not renumber. + +## Staged plan (each stage builds green) + +1. **Vendor move.** `driver/dist/vendor/{monocypher,lz4}` → top-level + `vendor/{monocypher,lz4}`; rewrite the shim `#include` paths; update the + Makefile. Pure relocation, no API change — lands first to isolate path + churn. +2. **Lift-and-shift the content layer.** Move `dist.{c,h}` `blob` `tree` `cas` + to `src/dist/`; add `src/api/cas.c` + `include/cfree/cas.h` wrapping + blob/tree/CAS; add `CFREE_CAS_ENABLED`; repoint `driver/cmd/cas.c` at the + public header + a host vtable. Smallest behavioral slice; proves the + boundary end to end. +3. **Drop v2 first, then extract the package pipelines.** Delete the dead v2 + code (see *Versioning* above) and shed the `3` suffix — a self-contained, + zero-behavior-change cleanup that shrinks the surface before it moves. Then + move `manifest` `cfpkg` `minisig` `trust` `b64` `deflate` `lz4` `tar` + + crypto shims to `src/dist/`; lift the `pkg_create_*` / `pkg_verify_*` / + unpack / key-resolution logic out of `driver/cmd/pkg.c` into + `src/api/package.c` behind `cfree_pkg_*`, converting operational `driver_errf` + → `api_diagf` (see *Error reporting* above) and `driver_*` fs/random → + `CfreeDistHost`. `pkg.c` shrinks to arg parsing + host wiring + trust-path/env + policy. This is the bulk of the work and the main risk. +4. **Tests.** Keep `test/cas/run.sh` + `test/pkg/run.sh` as end-to-end CLI + tests; optionally add unit tests that call the new public API directly (now + possible — coverage was CLI-only before). +5. **Docs.** Update `../DISTRIBUTE.md` paths (the layering diagram's + `driver/dist/*` rows become `src/dist/*` + the two public headers), the + `../DESIGN.md` layering box (the `driver/dist/` callout moves), and the + `CLAUDE.md` code map (add `vendor/`, `src/dist/`, `cfree/cas.h`, + `cfree/package.h`). + +## Risks / watch items + +- **Error reporting** is decided (see above): `CfreeStatus` + `ctx->diag`, no + boundary err-buffers. Remaining care is mechanical — route the ~70 operational + `driver_errf` sites to `api_diagf` while leaving arg-parse errors in `pkg.c`, + and confirm the CLI's stderr output is unchanged by the existing + `test/pkg/run.sh` corpus. +- **Trust policy must not leak into the library.** `$CFREE_TRUSTED_KEYS` / + `$HOME` defaulting and `--tofu` write-back are *driver* policy; the library + takes resolved bytes/paths and returns "would-pin this key id" decisions for + the driver to act on. Keep `getenv` driver-side. +- **Binary-format stability.** Once the manifest/tree/cfpkg model is public, the + determinism invariants in DISTRIBUTE.md become a public contract. With v2 gone + there is only one format to preserve — keep the wire magic at v3 (do not + renumber) and lock the bytes with the existing corpus before refactoring. +- **Subsystem flag matrix.** Verify `CFREE_PKG_ENABLED && !CFREE_CAS_ENABLED` + is a build-time error, and that both-off drops the vendored crypto so a + no-dist embedding stays clean (assert as the other subsystems do). diff --git a/driver/cmd/cas.c b/driver/cmd/cas.c @@ -1,17 +1,15 @@ +#include <cfree/cas.h> #include <cfree/core.h> #include <stddef.h> #include <stdint.h> -#include <stdio.h> #include <string.h> -#include "dist/blob.h" -#include "dist/cas.h" -#include "dist/dist.h" -#include "dist/tree.h" +#include "dist_host.h" #include "driver.h" #include "env.h" #define CAS_TOOL "cas" +#define CAS_HEX_MAX (2u * CFREE_CAS_HASH_LEN + 1u) void driver_help_cas(void) { driver_printf( @@ -26,141 +24,64 @@ void driver_help_cas(void) { " cfree cas materialize --cas DIR TREE_ID -C DIR\n"); } -typedef struct CasAddTree { - DriverEnv* env; - DistCas* cas; - DistTree tree; -} CasAddTree; - -static int cas_mkdir_p(void* user, const char* path) { - return driver_mkdir_p((DriverEnv*)user, path); -} - -static int cas_mark_executable(void* user, const char* path) { - (void)user; - return driver_mark_executable_output(path); -} - -static void cas_init(DistCas* cas, DriverEnv* env, const char* root) { - cas->host.file_io = &env->file_io; - cas->host.mkdir_p = cas_mkdir_p; - cas->host.mark_executable = cas_mark_executable; - cas->host.user = env; - cas->root = root; -} - -static void cas_hex(char out[2 * DIST_BLAKE2B_LEN + 1], - const uint8_t id[DIST_BLAKE2B_LEN]) { - dist_hex_encode(out, id, DIST_BLAKE2B_LEN); -} - -static int cas_parse_id(const char* s, uint8_t out[DIST_BLAKE2B_LEN]) { - size_t n = driver_strlen(s); - if (n != 2u * DIST_BLAKE2B_LEN) return DIST_ERR; - return dist_hex_decode(out, s, DIST_BLAKE2B_LEN); -} - -static int cas_write_stdout(DriverEnv* env, const uint8_t* data, size_t len) { - CfreeWriter* w = driver_stdout_writer(env); - if (!w) return DIST_ERR; - if (len && cfree_writer_write(w, data, len) != CFREE_OK) { - cfree_writer_close(w); - return DIST_ERR; - } - if (cfree_writer_status(w) != CFREE_OK) { - cfree_writer_close(w); - return DIST_ERR; - } - cfree_writer_close(w); - return DIST_OK; +static int cas_parse_id(const char* s, uint8_t out[CFREE_CAS_HASH_LEN]) { + if (driver_strlen(s) != 2u * CFREE_CAS_HASH_LEN) return 1; + return cfree_hex_decode(out, s, CFREE_CAS_HASH_LEN) == CFREE_OK ? 0 : 1; } -static int cas_add_tree_entry(CasAddTree* a, const char* tree_path, - uint8_t mode, const char* source_path) { - CfreeFileData fd; - DistBlobInfo bi; - DistTreeEntry* e; - fd.data = NULL; - fd.size = 0; - fd.token = NULL; - if (a->tree.n_entries >= a->tree.cap_entries) { - driver_errf(CAS_TOOL, "too many tree entries"); - return 1; - } - if (!dist_tree_path_valid(tree_path)) { - driver_errf(CAS_TOOL, "unsafe tree path: %s", tree_path); - return 1; - } - if (!dist_tree_mode_name(mode)) { - driver_errf(CAS_TOOL, "bad tree mode for: %s", tree_path); - return 1; - } - if (a->env->file_io.read_all(a->env->file_io.user, source_path, &fd) != - CFREE_OK) { - driver_errf(CAS_TOOL, "failed to read: %s", source_path); +static int cas_open(DriverEnv* env, const char* dir, CfreeCas** out) { + CfreeContext ctx = driver_env_to_context(env); + CfreeCasHost host = driver_cas_host(env); + if (cfree_cas_open(&ctx, &host, dir, out) != CFREE_OK) { + driver_errf(CAS_TOOL, "failed to open store: %s", dir); return 1; } - if (dist_blob_info(&bi, fd.data, fd.size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != - DIST_OK) { - driver_errf(CAS_TOOL, "failed to hash blob: %s", source_path); - a->env->file_io.release(a->env->file_io.user, &fd); - return 1; - } - if (dist_cas_put_blob(a->cas, bi.id, fd.data, fd.size) != DIST_OK) { - driver_errf(CAS_TOOL, "failed to store blob: %s", source_path); - a->env->file_io.release(a->env->file_io.user, &fd); - return 1; - } - e = &a->tree.entries[a->tree.n_entries++]; - memset(e, 0, sizeof *e); - snprintf(e->path, sizeof e->path, "%s", tree_path); - e->mode = mode; - e->size = bi.size; - memcpy(e->blob, bi.id, DIST_BLAKE2B_LEN); - memcpy(e->root, bi.root, DIST_BLAKE2B_LEN); - a->env->file_io.release(a->env->file_io.user, &fd); return 0; } -static int cas_walk_add_file(void* user, const char* source_path, - const char* tree_path, int executable) { - CasAddTree* a = (CasAddTree*)user; - uint8_t mode = - executable ? DIST_TREE_MODE_EXEC : DIST_TREE_MODE_FILE; - return cas_add_tree_entry(a, tree_path, mode, source_path); -} - -static int cas_emit_store_tree(CasAddTree* a, - uint8_t out_id[DIST_BLAKE2B_LEN]) { - CfreeWriter* w = NULL; - const uint8_t* bytes; - size_t len; - char err[128]; - if (dist_tree_sort_validate(&a->tree, err, sizeof err) != DIST_OK) { - driver_errf(CAS_TOOL, "%s", err); - return DIST_ERR; +static int cas_cmd_add_blob(DriverEnv* env, int argc, char** argv) { + const char* cas_dir = NULL; + const char* file = NULL; + DriverLoad load; + CfreeSlice in; + CfreeCas* cas; + CfreeBlobInfo bi; + char hex[CAS_HEX_MAX]; + int i; + int rc = 1; + load.loaded = 0; + for (i = 2; i < argc; ++i) { + if (driver_streq(argv[i], "--cas") && i + 1 < argc) { + cas_dir = argv[++i]; + } else if (!file) { + file = argv[i]; + } else { + driver_errf(CAS_TOOL, "unexpected argument: %s", argv[i]); + return 2; + } } - if (cfree_writer_mem(a->env->heap, &w) != CFREE_OK) { - driver_errf(CAS_TOOL, "failed to allocate tree writer"); - return DIST_ERR; + if (!cas_dir || !file) { + driver_errf(CAS_TOOL, "usage: cfree cas add-blob --cas DIR FILE"); + return 2; } - if (dist_tree_emit(&a->tree, w) != DIST_OK || - cfree_writer_status(w) != CFREE_OK) { - cfree_writer_close(w); - driver_errf(CAS_TOOL, "failed to emit tree manifest"); - return DIST_ERR; + if (cas_open(env, cas_dir, &cas) != 0) return 1; + if (driver_load_bytes(&env->file_io, CAS_TOOL, file, &load, &in) != 0) { + cfree_cas_close(cas); + return 1; } - bytes = cfree_writer_mem_bytes(w, &len); - dist_tree_id(out_id, bytes, len); - if (dist_cas_put_tree(a->cas, out_id, bytes, len) != DIST_OK) { - cfree_writer_close(w); - driver_errf(CAS_TOOL, "failed to store tree manifest"); - return DIST_ERR; + if (cfree_cas_add_blob(cas, in.data, in.len, &bi) == CFREE_OK) { + cfree_hex_encode(hex, bi.id, CFREE_CAS_HASH_LEN); + driver_printf("%s\n", hex); + rc = 0; } - cfree_writer_close(w); - return DIST_OK; + driver_release_bytes(&env->file_io, &load); + cfree_cas_close(cas); + return rc; } +/* Map-file parsing stays in the driver: it is the CLI's input format. Each + * line is "tree/path mode source/path"; the source bytes are read here and + * handed to the library tree builder. */ static int cas_read_token(const uint8_t* line, size_t len, size_t* pos, const uint8_t** start, size_t* tok_len) { size_t i = *pos; @@ -173,15 +94,16 @@ static int cas_read_token(const uint8_t* line, size_t len, size_t* pos, return 1; } -static int cas_parse_map_line(CasAddTree* a, const uint8_t* line, size_t len, - unsigned line_no) { +static int cas_map_add_line(DriverEnv* env, CfreeCasTreeBuilder* b, + const uint8_t* line, size_t len, unsigned line_no) { const uint8_t *path_b, *mode_b, *src_b; size_t path_l, mode_l, src_l; size_t pos = 0; - char path[DIST_PATH_MAX + 1]; - char mode_s[2]; + char path[CFREE_CAS_HASH_LEN * 4]; char* src; - uint8_t mode; + CfreeFileData fd; + CfreeTreeMode mode; + int rc = 1; while (len && line[len - 1u] == '\r') --len; while (pos < len && (line[pos] == ' ' || line[pos] == '\t')) ++pos; if (pos == len || line[pos] == '#') return 0; @@ -192,87 +114,57 @@ static int cas_parse_map_line(CasAddTree* a, const uint8_t* line, size_t len, return 1; } while (pos < len && (line[pos] == ' ' || line[pos] == '\t')) ++pos; - if (pos != len || path_l > DIST_PATH_MAX || mode_l != 1u || src_l == 0) { + if (pos != len || path_l >= sizeof path || mode_l != 1u || src_l == 0) { driver_errf(CAS_TOOL, "bad map line %u", line_no); return 1; } memcpy(path, path_b, path_l); path[path_l] = '\0'; - mode_s[0] = (char)mode_b[0]; - mode_s[1] = '\0'; - if (dist_tree_mode_parse(mode_s, &mode) != DIST_OK) { + if (mode_b[0] == '-') { + mode = CFREE_TREE_MODE_FILE; + } else if (mode_b[0] == 'x') { + mode = CFREE_TREE_MODE_EXEC; + } else { driver_errf(CAS_TOOL, "bad mode on map line %u", line_no); return 1; } - src = (char*)driver_alloc(a->env, src_l + 1u); + src = (char*)driver_alloc(env, src_l + 1u); if (!src) { driver_errf(CAS_TOOL, "out of memory"); return 1; } memcpy(src, src_b, src_l); src[src_l] = '\0'; - /* v1 map files intentionally split on ASCII whitespace, so paths with - * spaces are not representable yet. */ - if (cas_add_tree_entry(a, path, mode, src) != 0) { - driver_free(a->env, src, src_l + 1u); + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (env->file_io.read_all(env->file_io.user, src, &fd) != CFREE_OK) { + driver_errf(CAS_TOOL, "failed to read: %s", src); + driver_free(env, src, src_l + 1u); return 1; } - driver_free(a->env, src, src_l + 1u); - return 0; + /* v1 map files intentionally split on ASCII whitespace, so paths with + * spaces are not representable yet. */ + if (cfree_cas_tree_builder_add(b, path, mode, fd.data, fd.size) == CFREE_OK) + rc = 0; + if (env->file_io.release) env->file_io.release(env->file_io.user, &fd); + driver_free(env, src, src_l + 1u); + return rc; } -static int cas_add_tree_map(CasAddTree* a, const uint8_t* data, size_t len) { +static int cas_add_tree_map(DriverEnv* env, CfreeCasTreeBuilder* b, + const uint8_t* data, size_t len) { size_t start = 0; unsigned line_no = 1; while (start <= len) { size_t end = start; while (end < len && data[end] != '\n') ++end; - if (cas_parse_map_line(a, data + start, end - start, line_no) != 0) - return DIST_ERR; + if (cas_map_add_line(env, b, data + start, end - start, line_no) != 0) + return 1; if (end == len) break; start = end + 1u; ++line_no; } - return DIST_OK; -} - -static int cas_cmd_add_blob(DriverEnv* env, int argc, char** argv) { - const char* cas_dir = NULL; - const char* file = NULL; - DriverLoad load; - CfreeSlice in; - DistBlobInfo bi; - DistCas cas; - char hex[2 * DIST_BLAKE2B_LEN + 1]; - int i; - load.loaded = 0; - for (i = 2; i < argc; ++i) { - if (driver_streq(argv[i], "--cas") && i + 1 < argc) { - cas_dir = argv[++i]; - } else if (!file) { - file = argv[i]; - } else { - driver_errf(CAS_TOOL, "unexpected argument: %s", argv[i]); - return 2; - } - } - if (!cas_dir || !file) { - driver_errf(CAS_TOOL, "usage: cfree cas add-blob --cas DIR FILE"); - return 2; - } - cas_init(&cas, env, cas_dir); - if (driver_load_bytes(&env->file_io, CAS_TOOL, file, &load, &in) != 0) - return 1; - if (dist_blob_info(&bi, in.data, in.len, DIST_BLOB_CHUNK_SIZE_DEFAULT) != - DIST_OK || - dist_cas_put_blob(&cas, bi.id, in.data, in.len) != DIST_OK) { - driver_release_bytes(&env->file_io, &load); - driver_errf(CAS_TOOL, "failed to store blob: %s", file); - return 1; - } - cas_hex(hex, bi.id); - driver_printf("%s\n", hex); - driver_release_bytes(&env->file_io, &load); return 0; } @@ -280,11 +172,9 @@ static int cas_cmd_add_tree(DriverEnv* env, int argc, char** argv) { const char* cas_dir = NULL; const char* root = NULL; const char* map = NULL; - DistTreeEntry* entries; - DistCas cas; - CasAddTree add; - uint8_t tree_id[DIST_BLAKE2B_LEN]; - char hex[2 * DIST_BLAKE2B_LEN + 1]; + CfreeCas* cas; + uint8_t tree_id[CFREE_CAS_HASH_LEN]; + char hex[CAS_HEX_MAX]; int i; int rc = 1; for (i = 2; i < argc; ++i) { @@ -304,96 +194,36 @@ static int cas_cmd_add_tree(DriverEnv* env, int argc, char** argv) { "usage: cfree cas add-tree --cas DIR (--root DIR | --map FILE)"); return 2; } - entries = - (DistTreeEntry*)driver_alloc_zeroed(env, DIST_MAX_FILES * sizeof *entries); - if (!entries) { - driver_errf(CAS_TOOL, "out of memory"); - return 1; - } - cas_init(&cas, env, cas_dir); - add.env = env; - add.cas = &cas; - add.tree.entries = entries; - add.tree.n_entries = 0; - add.tree.cap_entries = DIST_MAX_FILES; + if (cas_open(env, cas_dir, &cas) != 0) return 1; if (root) { - if (driver_walk_regular_files(env, root, cas_walk_add_file, &add) != 0) { - driver_errf(CAS_TOOL, "failed to walk directory: %s", root); - goto out; - } + if (cfree_cas_add_tree_from_dir(cas, root, tree_id) == CFREE_OK) rc = 0; } else { + CfreeCasTreeBuilder* b; CfreeFileData fd; fd.data = NULL; fd.size = 0; fd.token = NULL; + if (cfree_cas_tree_builder_new(cas, &b) != CFREE_OK) { + cfree_cas_close(cas); + driver_errf(CAS_TOOL, "out of memory"); + return 1; + } if (env->file_io.read_all(env->file_io.user, map, &fd) != CFREE_OK) { driver_errf(CAS_TOOL, "failed to read map: %s", map); - goto out; - } - if (cas_add_tree_map(&add, fd.data, fd.size) != DIST_OK) { - env->file_io.release(env->file_io.user, &fd); - goto out; - } - env->file_io.release(env->file_io.user, &fd); - } - if (cas_emit_store_tree(&add, tree_id) != DIST_OK) goto out; - cas_hex(hex, tree_id); - driver_printf("%s\n", hex); - rc = 0; - -out: - driver_free(env, entries, DIST_MAX_FILES * sizeof *entries); - return rc; -} - -static int cas_load_parse_tree(DriverEnv* env, DistCas* cas, - const uint8_t id[DIST_BLAKE2B_LEN], - DistTree* tree, DistTreeEntry* entries, - CfreeFileData* raw) { - char err[128]; - tree->entries = entries; - tree->n_entries = 0; - tree->cap_entries = DIST_MAX_FILES; - raw->data = NULL; - raw->size = 0; - raw->token = NULL; - if (dist_cas_get_tree(cas, id, raw) != DIST_OK) { - driver_errf(CAS_TOOL, "failed to load tree"); - return DIST_ERR; - } - if (dist_tree_parse(raw->data, raw->size, tree, err, sizeof err) != DIST_OK) { - driver_errf(CAS_TOOL, "%s", err); - if (env->file_io.release) env->file_io.release(env->file_io.user, raw); - return DIST_ERR; - } - return DIST_OK; -} - -static int cas_verify_tree_blobs(DriverEnv* env, DistCas* cas, - const DistTree* tree) { - size_t i; - for (i = 0; i < tree->n_entries; ++i) { - const DistTreeEntry* e = &tree->entries[i]; - CfreeFileData fd; - DistBlobInfo bi; - fd.data = NULL; - fd.size = 0; - fd.token = NULL; - if (dist_cas_get_blob(cas, e->blob, &fd) != DIST_OK) { - driver_errf(CAS_TOOL, "missing or corrupt blob for: %s", e->path); - return DIST_ERR; - } - if (dist_blob_info(&bi, fd.data, fd.size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != - DIST_OK || - bi.size != e->size || - memcmp(bi.root, e->root, DIST_BLAKE2B_LEN) != 0) { + } else { + if (cas_add_tree_map(env, b, fd.data, fd.size) == 0 && + cfree_cas_tree_builder_finish(b, tree_id) == CFREE_OK) + rc = 0; if (env->file_io.release) env->file_io.release(env->file_io.user, &fd); - driver_errf(CAS_TOOL, "blob root mismatch for: %s", e->path); - return DIST_ERR; } - if (env->file_io.release) env->file_io.release(env->file_io.user, &fd); + cfree_cas_tree_builder_free(b); } - return DIST_OK; + if (rc == 0) { + cfree_hex_encode(hex, tree_id, CFREE_CAS_HASH_LEN); + driver_printf("%s\n", hex); + } + cfree_cas_close(cas); + return rc; } static int cas_cmd_tree_common(DriverEnv* env, int argc, char** argv, @@ -401,11 +231,8 @@ static int cas_cmd_tree_common(DriverEnv* env, int argc, char** argv, const char* cas_dir = NULL; const char* tree_s = NULL; const char* out_dir = NULL; - uint8_t tree_id[DIST_BLAKE2B_LEN]; - DistTreeEntry* entries; - DistTree tree; - CfreeFileData raw; - DistCas cas; + uint8_t tree_id[CFREE_CAS_HASH_LEN]; + CfreeCas* cas; int i; int rc = 1; for (i = 2; i < argc; ++i) { @@ -420,8 +247,7 @@ static int cas_cmd_tree_common(DriverEnv* env, int argc, char** argv, return 2; } } - if (!cas_dir || !tree_s || - (driver_streq(cmd, "materialize") && !out_dir) || + if (!cas_dir || !tree_s || (driver_streq(cmd, "materialize") && !out_dir) || (!driver_streq(cmd, "materialize") && out_dir)) { if (driver_streq(cmd, "materialize")) driver_errf(CAS_TOOL, @@ -430,39 +256,28 @@ static int cas_cmd_tree_common(DriverEnv* env, int argc, char** argv, driver_errf(CAS_TOOL, "usage: cfree cas %s --cas DIR TREE_ID", cmd); return 2; } - if (cas_parse_id(tree_s, tree_id) != DIST_OK) { + if (cas_parse_id(tree_s, tree_id) != 0) { driver_errf(CAS_TOOL, "bad tree id: %s", tree_s); return 2; } - entries = - (DistTreeEntry*)driver_alloc_zeroed(env, DIST_MAX_FILES * sizeof *entries); - if (!entries) { - driver_errf(CAS_TOOL, "out of memory"); - return 1; - } - cas_init(&cas, env, cas_dir); - if (cas_load_parse_tree(env, &cas, tree_id, &tree, entries, &raw) != DIST_OK) - goto out_entries; + if (cas_open(env, cas_dir, &cas) != 0) return 1; if (driver_streq(cmd, "inspect-tree")) { - if (cas_write_stdout(env, raw.data, raw.size) != DIST_OK) { - driver_errf(CAS_TOOL, "failed to write tree manifest"); - goto out_raw; + CfreeWriter* w = driver_stdout_writer(env); + if (w) { + if (cfree_cas_inspect_tree(cas, tree_id, w) == CFREE_OK) rc = 0; + cfree_writer_close(w); + } else { + driver_errf(CAS_TOOL, "failed to open stdout"); } } else if (driver_streq(cmd, "verify-tree")) { - if (cas_verify_tree_blobs(env, &cas, &tree) != DIST_OK) goto out_raw; - driver_printf("ok\n"); - } else { - if (dist_cas_materialize_tree(&cas, &tree, out_dir) != DIST_OK) { - driver_errf(CAS_TOOL, "failed to materialize tree"); - goto out_raw; + if (cfree_cas_verify_tree(cas, tree_id) == CFREE_OK) { + driver_printf("ok\n"); + rc = 0; } + } else { + if (cfree_cas_materialize_tree(cas, tree_id, out_dir) == CFREE_OK) rc = 0; } - rc = 0; - -out_raw: - if (env->file_io.release) env->file_io.release(env->file_io.user, &raw); -out_entries: - driver_free(env, entries, DIST_MAX_FILES * sizeof *entries); + cfree_cas_close(cas); return rc; } diff --git a/driver/cmd/pkg.c b/driver/cmd/pkg.c @@ -1,112 +1,53 @@ +#include <cfree/cas.h> #include <cfree/core.h> +#include <cfree/package.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> -#include <stdlib.h> #include <string.h> -#include "dist/blake2b.h" -#include "dist/blob.h" -#include "dist/cas.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/tar.h" -#include "dist/tree.h" -#include "dist/trust.h" +#include "dist_host.h" #include "driver.h" #include "env.h" #define PKG_TOOL "pkg" #define PKG_PATH_BUF 1024u -#define PKG_META_MANIFEST "cfree/package.manifest" -#define PKG_META_SIG "cfree/package.manifest.minisig" -#define PKG_META_PUB "cfree/package.pub" -#define PKG_DEFAULT_OUTPUT_ID 0u -#define PKG_MAX_TAR_ENTRIES (DIST_MAX_FILES + DIST_MAX_OUTPUTS + 8u) +#define PKG_TRUST_LINE_MAX 1024u +#define PKG_KEYID_HEX (2u * CFREE_PKG_KEYID_LEN + 1u) -typedef enum PkgFormat { PKG_FMT_AUTO, PKG_FMT_CFPKG, PKG_FMT_TARGZ } PkgFormat; - -typedef enum PkgNativeShape { - PKG_NATIVE_FAT, - PKG_NATIVE_METADATA, - PKG_NATIVE_THIN -} PkgNativeShape; - -typedef struct PkgBlob { - CfreeFileData fd; - int loaded; - uint8_t id[DIST_BLAKE2B_LEN]; - uint8_t root[DIST_BLAKE2B_LEN]; - uint64_t size; -} PkgBlob; - -typedef struct PkgSource { - DriverEnv* env; - DistTree tree; - DistTreeEntry entries[DIST_MAX_FILES]; - PkgBlob blobs[DIST_MAX_FILES]; - size_t n_blobs; - uint8_t tree_id[DIST_BLAKE2B_LEN]; - const uint8_t* tree_bytes; - size_t tree_size; - CfreeWriter* tree_mem; - CfreeFileData tree_fd; - int tree_loaded; -} PkgSource; - -typedef struct PkgVerified { - DistPackageManifest 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; - -typedef struct PkgLoadedTree { - DistTree tree; - DistTreeEntry entries[DIST_MAX_FILES]; - uint8_t id[DIST_BLAKE2B_LEN]; - const uint8_t* bytes; - size_t size; -} PkgLoadedTree; - -static int pkg_write_file(const CfreeContext* ctx, const char* path, - const uint8_t* data, size_t len) { - CfreeWriter* w = NULL; - int rc; - if (ctx->file_io->open_writer(ctx->file_io->user, path, &w) != CFREE_OK) { - driver_errf(PKG_TOOL, "failed to open output: %s", path); - return DIST_ERR; - } - rc = (len == 0 || cfree_writer_write(w, data, len) == CFREE_OK) ? DIST_OK - : DIST_ERR; - if (cfree_writer_status(w) != CFREE_OK) rc = DIST_ERR; - cfree_writer_close(w); - if (rc != DIST_OK) driver_errf(PKG_TOOL, "failed to write: %s", path); - return rc; +void driver_help_pkg(void) { + driver_printf( + "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" + " [--format cfpkg|tar.gz] [--compression none|lz4-block-v1]\n" + " [--native-shape fat|metadata|thin] [--external DIR]\n" + " (--cas DIR --tree TREE_ID | --root DIR) -o OUT\n" + " cfree pkg verify [-p PUBKEY | --tofu] [--format cfpkg|tar.gz]\n" + " [--external DIR] FILE\n" + " cfree pkg unpack [--verify] [-p PUBKEY | --tofu] [--format cfpkg|tar.gz]\n" + " [--external DIR] FILE -C DIR\n" + " cfree pkg inspect [--manifest | --encoding] FILE\n" + " cfree pkg trust {path | list | add PUBKEY [label] | remove KEYID}\n"); } -static CfreeWriter* pkg_mem(const CfreeContext* ctx) { - CfreeWriter* w = NULL; - if (cfree_writer_mem(ctx->heap, &w) != CFREE_OK) return NULL; - return w; +/* ---------------------------------------------------------------------- */ +/* small driver-side helpers */ +/* ---------------------------------------------------------------------- */ + +static int pkg_read(const CfreeContext* ctx, const char* path, + CfreeFileData* out) { + out->data = NULL; + out->size = 0; + out->token = NULL; + return ctx->file_io->read_all(ctx->file_io->user, path, out) == CFREE_OK; } -static int pkg_trust_path(char* buf, size_t cap) { - const char* env = driver_getenv("CFREE_TRUSTED_KEYS"); - const char* home; - if (env) { - snprintf(buf, cap, "%s", env); - return DIST_OK; - } - home = driver_getenv("HOME"); - if (!home) return DIST_ERR; - snprintf(buf, cap, "%s/.config/cfree/trusted_keys", home); - return DIST_OK; +static void pkg_release(const CfreeContext* ctx, CfreeFileData* fd) { + if ((fd->token || fd->data) && ctx->file_io->release) + ctx->file_io->release(ctx->file_io->user, fd); } static void pkg_parent_dir(const char* path, char* buf, size_t cap) { @@ -125,445 +66,72 @@ static void pkg_parent_dir(const char* path, char* buf, size_t cap) { buf[n] = '\0'; } -static int pkg_join_path(char* out, size_t cap, const char* dir, - const char* rel) { - size_t dl, rl; - int slash; - if (!out || !cap || !dir || !rel) return DIST_ERR; - dl = strlen(dir); - rl = strlen(rel); - slash = dl > 0 && dir[dl - 1u] != '/'; - if (dl + (slash ? 1u : 0u) + rl + 1u > cap) return DIST_ERR; - memcpy(out, dir, dl); - if (slash) out[dl++] = '/'; - memcpy(out + dl, rel, rl); - out[dl + rl] = '\0'; - return DIST_OK; -} - -static int pkg_read_file(const CfreeContext* ctx, const char* path, - CfreeFileData* out) { - return ctx->file_io->read_all(ctx->file_io->user, path, out) == CFREE_OK - ? DIST_OK - : DIST_ERR; -} - -static const DistTarEntry* pkg_find_name(const DistTarEntry* e, size_t n, - const char* name) { - size_t i; - for (i = 0; i < n; ++i) - if (driver_streq(e[i].name, name)) return &e[i]; - return NULL; +static int pkg_trust_path(char* buf, size_t cap) { + const char* env = driver_getenv("CFREE_TRUSTED_KEYS"); + const char* home; + if (env) { + snprintf(buf, cap, "%s", env); + return 0; + } + home = driver_getenv("HOME"); + if (!home) return 1; + snprintf(buf, cap, "%s/.config/cfree/trusted_keys", home); + return 0; } -static PkgFormat pkg_parse_format(const char* s) { - if (driver_streq(s, "cfpkg") || driver_streq(s, "native")) return PKG_FMT_CFPKG; +static CfreePkgFormat pkg_parse_format(const char* s) { + if (driver_streq(s, "cfpkg") || driver_streq(s, "native")) + return CFREE_PKG_FORMAT_CFPKG; if (driver_streq(s, "tar.gz") || driver_streq(s, "portable")) - return PKG_FMT_TARGZ; - return PKG_FMT_AUTO; + return CFREE_PKG_FORMAT_TARGZ; + return CFREE_PKG_FORMAT_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 CfreePkgFormat pkg_infer_format(const char* path) { + if (driver_has_suffix(path, ".tar.gz")) return CFREE_PKG_FORMAT_TARGZ; + if (driver_has_suffix(path, ".cfpkg")) return CFREE_PKG_FORMAT_CFPKG; + return CFREE_PKG_FORMAT_AUTO; } -static int pkg_parse_native_shape(const char* s, PkgNativeShape* out) { +static int pkg_parse_native_shape(const char* s, CfreePkgShape* out) { if (driver_streq(s, "fat")) { - *out = PKG_NATIVE_FAT; - return DIST_OK; + *out = CFREE_PKG_SHAPE_FAT; + return 0; } if (driver_streq(s, "metadata") || driver_streq(s, "metadata-rich")) { - *out = PKG_NATIVE_METADATA; - return DIST_OK; + *out = CFREE_PKG_SHAPE_METADATA; + return 0; } if (driver_streq(s, "thin")) { - *out = PKG_NATIVE_THIN; - return DIST_OK; - } - return DIST_ERR; -} - -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 cfree_writer_tell(w) == target ? DIST_OK : DIST_ERR; -} - -static void pkg_hash(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, - size_t len) { - dist_blake2b(out, data, len); -} - -static int pkg_parse_id(const char* s, uint8_t out[DIST_BLAKE2B_LEN]) { - if (!s || driver_strlen(s) != 2u * DIST_BLAKE2B_LEN) return DIST_ERR; - return dist_hex_decode(out, s, DIST_BLAKE2B_LEN); -} - -static int pkg_cas_rel_path(char* out, size_t cap, const char* kind, - const uint8_t id[DIST_BLAKE2B_LEN]) { - char hex[2 * DIST_BLAKE2B_LEN + 1]; - int n; - dist_hex_encode(hex, id, DIST_BLAKE2B_LEN); - n = snprintf(out, cap, "cfree/cas/%s/%c%c/%s", kind, hex[0], hex[1], hex); - return n > 0 && (size_t)n < cap ? DIST_OK : DIST_ERR; -} - -static int pkg_external_id_path(char* out, size_t cap, const char* kind, - const uint8_t id[DIST_BLAKE2B_LEN]) { - if (driver_streq(kind, "tree")) return dist_cas_tree_relpath(out, cap, id); - if (driver_streq(kind, "index")) return dist_cas_index_relpath(out, cap, id); - if (driver_streq(kind, "blob")) return dist_cas_blob_relpath(out, cap, id); - return DIST_ERR; -} - -static int pkg_external_chunk_path(char* out, size_t cap, - const uint8_t blob[DIST_BLAKE2B_LEN], - uint64_t chunk_index) { - return dist_cas_chunk_relpath(out, cap, blob, chunk_index); -} - -static int pkg_locator_safe(const char* path) { - size_t start = 0, i; - if (!path || !path[0] || path[0] == '/') return 0; - for (i = 0;; ++i) { - char c = path[i]; - if (c == '\\' || c == ':' || c == '\n' || c == '\r') return 0; - if (c == '/' || c == '\0') { - size_t n = i - start; - if (n == 0) return 0; - if (n == 1 && path[start] == '.') return 0; - if (n == 2 && path[start] == '.' && path[start + 1] == '.') return 0; - if (c == '\0') return 1; - start = i + 1u; - } - } -} - -static int pkg_external_path(char* out, size_t cap, const char* root, - const char* rel) { - if (!pkg_locator_safe(rel)) return DIST_ERR; - return pkg_join_path(out, cap, root, rel); -} - -static int pkg_write_external_file(DriverEnv* env, const CfreeContext* ctx, - const char* root, const char* rel, - const uint8_t* data, size_t len) { - char full[PKG_PATH_BUF], parent[PKG_PATH_BUF]; - if (!root || pkg_external_path(full, sizeof full, root, rel) != DIST_OK) - return DIST_ERR; - pkg_parent_dir(full, parent, sizeof parent); - if (parent[0] && driver_mkdir_p(env, parent) != 0) return DIST_ERR; - return pkg_write_file(ctx, full, data, len); -} - -static int pkg_read_external_file(const CfreeContext* ctx, const char* root, - const char* rel, CfreeFileData* out) { - char full[PKG_PATH_BUF]; - if (!root || pkg_external_path(full, sizeof full, root, rel) != DIST_OK) - return DIST_ERR; - return pkg_read_file(ctx, full, out); -} - -static int pkg_blob_cmp(const void* ap, const void* bp) { - const PkgBlob* a = (const PkgBlob*)ap; - const PkgBlob* b = (const PkgBlob*)bp; - return memcmp(a->id, b->id, DIST_BLAKE2B_LEN); -} - -static PkgBlob* pkg_source_find_blob(PkgSource* src, - const uint8_t id[DIST_BLAKE2B_LEN]) { - size_t i; - for (i = 0; i < src->n_blobs; ++i) - if (memcmp(src->blobs[i].id, id, DIST_BLAKE2B_LEN) == 0) - return &src->blobs[i]; - return NULL; -} - -static void pkg_source_init(PkgSource* src, DriverEnv* env) { - memset(src, 0, sizeof *src); - src->env = env; - src->tree.entries = src->entries; - src->tree.cap_entries = DIST_MAX_FILES; -} - -static void pkg_source_release(PkgSource* src) { - size_t i; - if (!src || !src->env) return; - for (i = 0; i < src->n_blobs; ++i) { - if (src->blobs[i].loaded && src->env->file_io.release) - src->env->file_io.release(src->env->file_io.user, &src->blobs[i].fd); - } - if (src->tree_loaded && src->env->file_io.release) - src->env->file_io.release(src->env->file_io.user, &src->tree_fd); - if (src->tree_mem) cfree_writer_close(src->tree_mem); - memset(src, 0, sizeof *src); -} - -static int pkg_source_store_blob(PkgSource* src, CfreeFileData* fd, - const DistBlobInfo* bi, int take_fd) { - PkgBlob* existing = pkg_source_find_blob(src, bi->id); - if (existing) { - if (existing->size != bi->size || - memcmp(existing->root, bi->root, DIST_BLAKE2B_LEN) != 0) - return DIST_ERR; - return DIST_OK; - } - if (src->n_blobs >= DIST_MAX_FILES) return DIST_ERR; - existing = &src->blobs[src->n_blobs++]; - memset(existing, 0, sizeof *existing); - memcpy(existing->id, bi->id, DIST_BLAKE2B_LEN); - memcpy(existing->root, bi->root, DIST_BLAKE2B_LEN); - existing->size = bi->size; - if (take_fd) { - existing->fd = *fd; - existing->loaded = 1; - fd->data = NULL; - fd->size = 0; - fd->token = NULL; - } - return DIST_OK; -} - -static int pkg_source_add_entry(PkgSource* src, const char* tree_path, - uint8_t mode, CfreeFileData* fd, - int take_fd) { - DistBlobInfo bi; - DistTreeEntry* e; - if (src->tree.n_entries >= src->tree.cap_entries) { - driver_errf(PKG_TOOL, "create: too many tree entries"); - return DIST_ERR; - } - if (!dist_tree_path_valid(tree_path)) { - driver_errf(PKG_TOOL, "create: unsafe tree path: %s", tree_path); - return DIST_ERR; - } - if (!dist_tree_mode_name(mode)) { - driver_errf(PKG_TOOL, "create: bad tree mode: %s", tree_path); - return DIST_ERR; - } - if (dist_blob_info(&bi, fd->data, fd->size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != - DIST_OK) { - driver_errf(PKG_TOOL, "create: failed to hash blob: %s", tree_path); - return DIST_ERR; - } - if (pkg_source_store_blob(src, fd, &bi, take_fd) != DIST_OK) { - driver_errf(PKG_TOOL, "create: failed to store blob metadata: %s", - tree_path); - return DIST_ERR; - } - e = &src->tree.entries[src->tree.n_entries++]; - memset(e, 0, sizeof *e); - snprintf(e->path, sizeof e->path, "%s", tree_path); - e->mode = mode; - e->size = bi.size; - memcpy(e->blob, bi.id, DIST_BLAKE2B_LEN); - memcpy(e->root, bi.root, DIST_BLAKE2B_LEN); - return DIST_OK; -} - -static int pkg_source_walk_file(void* user, const char* source_path, - const char* tree_path, int executable) { - PkgSource* src = (PkgSource*)user; - CfreeFileData fd; - int rc; - fd.data = NULL; - fd.size = 0; - fd.token = NULL; - if (src->env->file_io.read_all(src->env->file_io.user, source_path, &fd) != - CFREE_OK) { - driver_errf(PKG_TOOL, "create: cannot read file: %s", source_path); - return 1; - } - rc = pkg_source_add_entry(src, tree_path, - executable ? DIST_TREE_MODE_EXEC - : DIST_TREE_MODE_FILE, - &fd, 1); - if (fd.data && src->env->file_io.release) - src->env->file_io.release(src->env->file_io.user, &fd); - return rc == DIST_OK ? 0 : 1; -} - -static int pkg_source_finish_tree(PkgSource* src) { - char err[128]; - if (dist_tree_sort_validate(&src->tree, err, sizeof err) != DIST_OK) { - driver_errf(PKG_TOOL, "create: %s", err); - return DIST_ERR; - } - if (cfree_writer_mem(src->env->heap, &src->tree_mem) != CFREE_OK) - return DIST_ERR; - if (dist_tree_emit(&src->tree, src->tree_mem) != DIST_OK || - cfree_writer_status(src->tree_mem) != CFREE_OK) { - driver_errf(PKG_TOOL, "create: failed to emit tree manifest"); - return DIST_ERR; - } - src->tree_bytes = cfree_writer_mem_bytes(src->tree_mem, &src->tree_size); - dist_tree_id(src->tree_id, src->tree_bytes, src->tree_size); - qsort(src->blobs, src->n_blobs, sizeof src->blobs[0], pkg_blob_cmp); - return DIST_OK; -} - -static int pkg_source_from_root(PkgSource* src, const char* root) { - if (driver_walk_regular_files(src->env, root, pkg_source_walk_file, src) != - 0) { - driver_errf(PKG_TOOL, "create: failed to walk directory: %s", root); - return DIST_ERR; - } - return pkg_source_finish_tree(src); -} - -static void pkg_cas_init_get(DistCas* cas, DriverEnv* env, const char* root) { - memset(cas, 0, sizeof *cas); - cas->host.file_io = &env->file_io; - cas->host.user = env; - cas->root = root; -} - -static int pkg_source_load_blob_from_cas(PkgSource* src, DistCas* cas, - const DistTreeEntry* e) { - PkgBlob* existing = pkg_source_find_blob(src, e->blob); - CfreeFileData fd; - DistBlobInfo bi; - if (existing) { - if (existing->size != e->size || - memcmp(existing->root, e->root, DIST_BLAKE2B_LEN) != 0) { - driver_errf(PKG_TOOL, "create: duplicate blob metadata mismatch: %s", - e->path); - return DIST_ERR; - } - return DIST_OK; - } - fd.data = NULL; - fd.size = 0; - fd.token = NULL; - if (dist_cas_get_blob(cas, e->blob, &fd) != DIST_OK) { - driver_errf(PKG_TOOL, "create: missing or corrupt blob for: %s", e->path); - return DIST_ERR; - } - if (dist_blob_info(&bi, fd.data, fd.size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != - DIST_OK || - bi.size != e->size || memcmp(bi.root, e->root, DIST_BLAKE2B_LEN) != 0) { - if (src->env->file_io.release) - src->env->file_io.release(src->env->file_io.user, &fd); - driver_errf(PKG_TOOL, "create: blob root mismatch for: %s", e->path); - return DIST_ERR; - } - if (pkg_source_store_blob(src, &fd, &bi, 1) != DIST_OK) { - if (fd.data && src->env->file_io.release) - src->env->file_io.release(src->env->file_io.user, &fd); - return DIST_ERR; + *out = CFREE_PKG_SHAPE_THIN; + return 0; } - return DIST_OK; + return 1; } -static int pkg_source_from_cas(PkgSource* src, const char* cas_dir, - const char* tree_s) { - DistCas cas; - char err[128]; - size_t i; - if (pkg_parse_id(tree_s, src->tree_id) != DIST_OK) { - driver_errf(PKG_TOOL, "create: bad tree id: %s", tree_s); - return DIST_ERR; - } - pkg_cas_init_get(&cas, src->env, cas_dir); - src->tree_fd.data = NULL; - src->tree_fd.size = 0; - src->tree_fd.token = NULL; - if (dist_cas_get_tree(&cas, src->tree_id, &src->tree_fd) != DIST_OK) { - driver_errf(PKG_TOOL, "create: missing or corrupt tree: %s", tree_s); - return DIST_ERR; - } - src->tree_loaded = 1; - src->tree_bytes = src->tree_fd.data; - src->tree_size = src->tree_fd.size; - if (dist_tree_parse(src->tree_bytes, src->tree_size, &src->tree, err, - sizeof err) != DIST_OK) { - driver_errf(PKG_TOOL, "create: tree: %s", err); - return DIST_ERR; - } - for (i = 0; i < src->tree.n_entries; ++i) { - if (pkg_source_load_blob_from_cas(src, &cas, &src->tree.entries[i]) != - DIST_OK) - return DIST_ERR; +static int pkg_parse_compression(const char* s, CfreePkgCompression* out) { + if (driver_streq(s, "none")) { + *out = CFREE_PKG_COMPRESSION_NONE; + return 0; } - qsort(src->blobs, src->n_blobs, sizeof src->blobs[0], pkg_blob_cmp); - return DIST_OK; -} - -static int pkg_manifest_from_source(const char* name, const char* version, - const char* desc, const PkgSource* src, - DistPackageManifest* m) { - size_t i; - memset(m, 0, sizeof *m); - snprintf(m->name, sizeof m->name, "%s", name); - snprintf(m->version, sizeof m->version, "%s", version); - if (desc) snprintf(m->description, sizeof m->description, "%s", desc); - m->n_outputs = 1; - m->outputs[0].id = PKG_DEFAULT_OUTPUT_ID; - snprintf(m->outputs[0].name, sizeof m->outputs[0].name, "%s", "default"); - memcpy(m->outputs[0].tree, src->tree_id, DIST_BLAKE2B_LEN); - m->outputs[0].is_default = 1; - for (i = 0; i < src->tree.n_entries; ++i) { - DistPackageArtifact* a; - if (m->n_artifacts >= DIST_MAX_ARTIFACTS) return DIST_ERR; - a = &m->artifacts[m->n_artifacts++]; - a->output_id = PKG_DEFAULT_OUTPUT_ID; - snprintf(a->path, sizeof a->path, "%s", src->tree.entries[i].path); - snprintf(a->kind, sizeof a->kind, "%s", "data"); - a->entry = 1; + if (driver_streq(s, "lz4-block-v1") || driver_streq(s, "lz4")) { + *out = CFREE_PKG_COMPRESSION_LZ4_BLOCK_V1; + return 0; } - return dist_package_manifest_validate(m, NULL, 0); -} - -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); + return 1; } -void driver_help_pkg(void) { - driver_printf( - "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" - " [--format cfpkg|tar.gz] [--compression none|lz4-block-v1]\n" - " [--native-shape fat|metadata|thin] [--external DIR]\n" - " (--cas DIR --tree TREE_ID | --root DIR) -o OUT\n" - " cfree pkg verify [-p PUBKEY | --tofu] [--format cfpkg|tar.gz]\n" - " [--external DIR] FILE\n" - " cfree pkg unpack [--verify] [-p PUBKEY | --tofu] [--format cfpkg|tar.gz]\n" - " [--external DIR] FILE -C DIR\n" - " cfree pkg inspect [--manifest | --encoding] FILE\n" - " cfree pkg trust {path | 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], keyid[DIST_KEYID_LEN]; - DistKeypair kp; - CfreeWriter* w; - char path[PKG_PATH_BUF]; - const uint8_t* bytes; - size_t len; - int i; + CfreeWriter *pubw = NULL, *seckw = NULL; + uint8_t keyid[CFREE_PKG_KEYID_LEN]; + char path[PKG_PATH_BUF], hex[PKG_KEYID_HEX]; + int i, rc = 1; (void)env; for (i = 0; i < argc; ++i) { if (driver_streq(argv[i], "-o") && i + 1 < argc) @@ -577,381 +145,81 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc, driver_errf(PKG_TOOL, "keygen: -o BASE is required"); return 2; } - 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"); + snprintf(path, sizeof path, "%s.pub", base); + if (ctx->file_io->open_writer(ctx->file_io->user, path, &pubw) != CFREE_OK) { + driver_errf(PKG_TOOL, "keygen: failed to open output: %s", path); return 1; } - dist_minisig_keygen(&kp, seed, keyid); - w = pkg_mem(ctx); - if (!w || dist_minisig_emit_pubkey(w, &kp) != DIST_OK) return 1; - bytes = cfree_writer_mem_bytes(w, &len); - snprintf(path, sizeof path, "%s.pub", base); - if (pkg_write_file(ctx, path, bytes, len) != DIST_OK) return 1; - cfree_writer_close(w); - w = pkg_mem(ctx); - if (!w || dist_minisig_emit_seckey(w, &kp) != DIST_OK) return 1; - bytes = cfree_writer_mem_bytes(w, &len); snprintf(path, sizeof path, "%s.key", base); - if (pkg_write_file(ctx, path, bytes, len) != DIST_OK) return 1; - cfree_writer_close(w); - { - char hex[2 * DIST_KEYID_LEN + 1]; - dist_hex_encode(hex, kp.keyid, DIST_KEYID_LEN); - driver_printf("wrote %s.pub and %s.key (key id %s)\n", base, base, hex); + if (ctx->file_io->open_writer(ctx->file_io->user, path, &seckw) != CFREE_OK) { + driver_errf(PKG_TOOL, "keygen: failed to open output: %s", path); + cfree_writer_close(pubw); + return 1; } - return 0; -} - -static int pkg_create_targz(const CfreeContext* ctx, const char* out, - const PkgSource* src, 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; - char path[PKG_PATH_BUF]; - 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; - if (pkg_cas_rel_path(path, sizeof path, "tree", src->tree_id) != DIST_OK || - dist_tar_append(tar, path, src->tree_bytes, src->tree_size) != DIST_OK) - goto done; - for (i = 0; i < src->n_blobs; ++i) { - if (pkg_cas_rel_path(path, sizeof path, "blob", src->blobs[i].id) != - DIST_OK || - dist_tar_append(tar, path, src->blobs[i].fd.data, - (size_t)src->blobs[i].size) != DIST_OK) - goto done; + if (cfree_pkg_keygen(ctx, driver_dist_random, NULL, pubw, seckw, keyid) == + CFREE_OK) + rc = 0; + cfree_writer_close(seckw); + cfree_writer_close(pubw); + if (rc == 0) { + cfree_hex_encode(hex, keyid, CFREE_PKG_KEYID_LEN); + driver_printf("wrote %s.pub and %s.key (key id %s)\n", base, base, hex); } - 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(DriverEnv* env, const CfreeContext* ctx, - const PkgSource* src, - uint32_t compression, - const char* external_dir, - int embed_content, - CfreeWriter** index_out, - CfreeWriter** content_out) { - CfreeWriter* index = pkg_mem(ctx); - CfreeWriter* content = pkg_mem(ctx); - size_t bi; - if (!index || !content) return DIST_ERR; - for (bi = 0; bi < src->n_blobs; ++bi) { - const PkgBlob* blob = &src->blobs[bi]; - size_t off = 0, ci = 0; - if (blob->size == 0) continue; - while (off < blob->size) { - uint8_t recbuf[DIST_CFPKG3_INDEX_RECORD_SIZE]; - DistCfpkg3IndexRecord r; - const uint8_t* raw = blob->fd.data + off; - size_t raw_len = (size_t)blob->size - off; - if (raw_len > DIST_CFPKG3_CHUNK_SIZE_DEFAULT) - raw_len = DIST_CFPKG3_CHUNK_SIZE_DEFAULT; - memset(&r, 0, sizeof r); - memcpy(r.blob_id, blob->id, DIST_BLAKE2B_LEN); - r.chunk_index = (uint64_t)ci; - r.content_offset = embed_content ? cfree_writer_tell(content) : 0; - r.raw_size = raw_len; - r.compression = compression; - pkg_hash(r.raw_hash, raw, raw_len); - dist_blob_leaf_hash(r.leaf_hash, 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 (embed_content && - cfree_writer_write(content, raw, raw_len) != CFREE_OK) - return DIST_ERR; - if (!embed_content) { - char rel[PKG_PATH_BUF]; - if (pkg_external_chunk_path(rel, sizeof rel, blob->id, - r.chunk_index) != DIST_OK || - pkg_write_external_file(env, ctx, external_dir, rel, raw, - raw_len) != DIST_OK) - return DIST_ERR; - } - } else { - uint8_t tmp[DIST_CFPKG3_CHUNK_SIZE_DEFAULT + 1024u]; - 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 compression failed"); - return DIST_ERR; - } - r.stored_size = stored_len; - pkg_hash(r.stored_hash, tmp, stored_len); - if (embed_content && - cfree_writer_write(content, tmp, stored_len) != CFREE_OK) - return DIST_ERR; - if (!embed_content) { - char rel[PKG_PATH_BUF]; - if (pkg_external_chunk_path(rel, sizeof rel, blob->id, - r.chunk_index) != DIST_OK || - pkg_write_external_file(env, ctx, external_dir, rel, tmp, - stored_len) != DIST_OK) - return DIST_ERR; - } - } - dist_cfpkg3_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, - const PkgSource* src, 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, PkgNativeShape shape, - const char* external_dir) { - 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 tree_root[DIST_BLAKE2B_LEN], index_root[DIST_BLAKE2B_LEN], - content_root[DIST_BLAKE2B_LEN]; - DistCfpkg3Header h; - uint64_t tree_offset = 0, index_offset = 0, content_offset = 0; - int embed_tree = shape != PKG_NATIVE_THIN; - int embed_index = shape != PKG_NATIVE_THIN; - int embed_content = shape == PKG_NATIVE_FAT; - int stable = 0, iter, rc = DIST_ERR; - char tree_url[PKG_PATH_BUF], index_url[PKG_PATH_BUF]; - - tree_url[0] = '\0'; - index_url[0] = '\0'; - if (shape != PKG_NATIVE_FAT && !external_dir) { - driver_errf(PKG_TOOL, - "create: --external DIR is required for non-fat native packages"); - goto done; - } - if (pkg_external_id_path(tree_url, sizeof tree_url, "tree", src->tree_id) != - DIST_OK) - goto done; - - if (pkg_build_native_regions(env, ctx, src, compression, external_dir, - embed_content, &index, &content) != - DIST_OK) - goto done; - index_b = cfree_writer_mem_bytes(index, &index_l); - content_b = cfree_writer_mem_bytes(content, &content_l); - dist_cfpkg3_region_root(tree_root, "tree", - embed_tree ? src->tree_bytes : NULL, - embed_tree ? src->tree_size : 0); - dist_cfpkg3_region_root(index_root, "index", index_b, index_l); - dist_cfpkg3_region_root(content_root, "content", - embed_content ? content_b : NULL, - embed_content ? content_l : 0); - if (!embed_tree && - pkg_write_external_file(env, ctx, external_dir, tree_url, src->tree_bytes, - src->tree_size) != DIST_OK) - goto done; - if (!embed_index) { - if (pkg_external_id_path(index_url, sizeof index_url, "index", - index_root) != DIST_OK || - pkg_write_external_file(env, ctx, external_dir, index_url, index_b, - index_l) != DIST_OK) - goto done; - } - - memset(&h, 0, sizeof h); - for (iter = 0; iter < 8; ++iter) { - DistCfpkg3Descriptor 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_CFPKG3_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; - tree_offset = embed_tree ? pkg_align_up(h.pubkey_offset + h.pubkey_size, - DIST_CFPKG3_ALIGNMENT) - : 0; - index_offset = - embed_index - ? pkg_align_up((embed_tree ? tree_offset + src->tree_size - : h.pubkey_offset + h.pubkey_size), - DIST_CFPKG3_ALIGNMENT) - : 0; - content_offset = - embed_content - ? pkg_align_up((embed_index ? index_offset + index_l - : (embed_tree ? tree_offset + - src->tree_size - : h.pubkey_offset + - h.pubkey_size)), - DIST_CFPKG3_ALIGNMENT) - : 0; - - memset(&d, 0, sizeof d); - memcpy(d.package_id, pkgid, DIST_BLAKE2B_LEN); - d.chunk_size = DIST_CFPKG3_CHUNK_SIZE_DEFAULT; - d.alignment = DIST_CFPKG3_ALIGNMENT; - d.tree_offset = tree_offset; - d.tree_size = embed_tree ? src->tree_size : 0; - memcpy(d.tree_root, tree_root, DIST_BLAKE2B_LEN); - d.index_offset = index_offset; - d.index_size = embed_index ? index_l : 0; - d.index_bytes = index_l; - memcpy(d.index_root, index_root, DIST_BLAKE2B_LEN); - if (!embed_index) snprintf(d.index_url, sizeof d.index_url, "%s", index_url); - d.content_offset = content_offset; - d.content_size = embed_content ? content_l : 0; - memcpy(d.content_root, content_root, DIST_BLAKE2B_LEN); - d.n_trees = 1; - memcpy(d.trees[0].tree, src->tree_id, DIST_BLAKE2B_LEN); - if (embed_tree) { - d.trees[0].offset = 0; - d.trees[0].size = src->tree_size; - d.trees[0].embedded = 1; - } else { - snprintf(d.trees[0].url, sizeof d.trees[0].url, "%s", tree_url); - } - memcpy(d.trees[0].blake2b, src->tree_id, DIST_BLAKE2B_LEN); - d.n_chunk_sources = 1; - if (embed_content) { - d.chunk_sources[0].kind = DIST_CFPKG3_CHUNK_SOURCE_EMBEDDED; - } else { - d.chunk_sources[0].kind = DIST_CFPKG3_CHUNK_SOURCE_URL_TEMPLATE; - snprintf(d.chunk_sources[0].tmpl, sizeof d.chunk_sources[0].tmpl, - "%s", "chunk/{blob-prefix}/{blob}/{chunk}"); - } - if (dist_cfpkg3_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_cfpkg3_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 || - (embed_tree && - (pkg_write_pad(pkg, tree_offset) != DIST_OK || - cfree_writer_write(pkg, src->tree_bytes, src->tree_size) != - CFREE_OK)) || - (embed_index && - (pkg_write_pad(pkg, index_offset) != DIST_OK || - cfree_writer_write(pkg, index_b, index_l) != CFREE_OK)) || - (embed_content && - (pkg_write_pad(pkg, 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; -} +/* ---------------------------------------------------------------------- */ +/* create */ +/* ---------------------------------------------------------------------- */ static int pkg_create(DriverEnv* env, const CfreeContext* ctx, int argc, char** argv) { - const char *name = NULL, *version = NULL, *desc = NULL, *out = NULL, - *seckey = NULL, *cas_dir = NULL, *tree_id = NULL, *root = NULL, - *external_dir = NULL; + CfreePkgCreateOptions opts; + CfreePkgCreateResult result; + CfreeCasHost host; + CfreeFileData skfd; + uint8_t sk[CFREE_PKG_SK_LEN], keyid[CFREE_PKG_KEYID_LEN]; + const char* seckey = NULL; + char pkgid_hex[2 * CFREE_CAS_HASH_LEN + 1]; int i, rc = 1, sk_loaded = 0; - PkgFormat fmt = PKG_FMT_AUTO; - PkgNativeShape native_shape = PKG_NATIVE_FAT; - uint32_t compression = DIST_CFPKG_COMP_NONE; - DistKeypair kp; - CfreeFileData skfd = {0}; - DistPackageManifest m; - PkgSource src; - 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]; - pkg_source_init(&src, env); + memset(&opts, 0, sizeof opts); + opts.format = CFREE_PKG_FORMAT_AUTO; + opts.native_shape = CFREE_PKG_SHAPE_FAT; + opts.compression = CFREE_PKG_COMPRESSION_NONE; for (i = 0; i < argc; ++i) { const char* a = argv[i]; if (driver_streq(a, "--name") && i + 1 < argc) - name = argv[++i]; + opts.name = argv[++i]; else if (driver_streq(a, "--version") && i + 1 < argc) - version = argv[++i]; + opts.version = argv[++i]; else if (driver_streq(a, "--desc") && i + 1 < argc) - desc = argv[++i]; + opts.description = argv[++i]; else if (driver_streq(a, "-o") && i + 1 < argc) - out = argv[++i]; + opts.out_path = argv[++i]; else if (driver_streq(a, "-s") && i + 1 < argc) seckey = argv[++i]; else if (driver_streq(a, "--cas") && i + 1 < argc) - cas_dir = argv[++i]; + opts.cas_dir = argv[++i]; else if (driver_streq(a, "--tree") && i + 1 < argc) - tree_id = argv[++i]; + opts.tree_id = argv[++i]; else if (driver_streq(a, "--root") && i + 1 < argc) - root = argv[++i]; + opts.root_dir = argv[++i]; else if (driver_streq(a, "--external") && i + 1 < argc) - external_dir = argv[++i]; + opts.external_dir = argv[++i]; else if (driver_streq(a, "--native-shape") && i + 1 < argc) { - if (pkg_parse_native_shape(argv[++i], &native_shape) != DIST_OK) { + if (pkg_parse_native_shape(argv[++i], &opts.native_shape) != 0) { driver_errf(PKG_TOOL, "create: unknown native shape"); return 2; } - } - else if (driver_streq(a, "--format") && i + 1 < argc) { - fmt = pkg_parse_format(argv[++i]); - if (fmt == PKG_FMT_AUTO) { + } else if (driver_streq(a, "--format") && i + 1 < argc) { + opts.format = pkg_parse_format(argv[++i]); + if (opts.format == CFREE_PKG_FORMAT_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) { + if (pkg_parse_compression(argv[++i], &opts.compression) != 0) { driver_errf(PKG_TOOL, "create: unknown compression"); return 2; } @@ -959,917 +227,108 @@ static int pkg_create(DriverEnv* env, const CfreeContext* ctx, int argc, driver_errf(PKG_TOOL, "create: unknown option: %s", a); return 2; } else { - driver_errf(PKG_TOOL, "create: positional file inputs were removed; use --root DIR"); + driver_errf(PKG_TOOL, + "create: positional file inputs were removed; use --root DIR"); return 2; } } - if (!name || !version || !out || !seckey) { + if (!opts.name || !opts.version || !opts.out_path || !seckey) { driver_errf(PKG_TOOL, "create: --name, --version, -s SECKEY and -o OUT are required"); return 2; } - if ((root != NULL) == (cas_dir != NULL || tree_id != NULL) || (cas_dir && !tree_id) || - (tree_id && !cas_dir)) { - driver_errf(PKG_TOOL, - "create: pass exactly one of --root DIR or --cas DIR --tree TREE_ID"); + if ((opts.root_dir != NULL) == (opts.cas_dir != NULL || opts.tree_id != NULL) || + (opts.cas_dir && !opts.tree_id) || (opts.tree_id && !opts.cas_dir)) { + driver_errf( + PKG_TOOL, + "create: pass exactly one of --root DIR or --cas DIR --tree TREE_ID"); return 2; } - if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(out); - if (fmt == PKG_FMT_AUTO) { + if (opts.format == CFREE_PKG_FORMAT_AUTO) + opts.format = pkg_infer_format(opts.out_path); + if (opts.format == CFREE_PKG_FORMAT_AUTO) { driver_errf(PKG_TOOL, "create: cannot infer format; pass --format"); return 2; } - if (fmt != PKG_FMT_CFPKG && native_shape != PKG_NATIVE_FAT) { + if (opts.format != CFREE_PKG_FORMAT_CFPKG && + opts.native_shape != CFREE_PKG_SHAPE_FAT) { driver_errf(PKG_TOOL, "create: --native-shape only applies to cfpkg"); return 2; } - if (fmt != PKG_FMT_CFPKG && external_dir) { + if (opts.format != CFREE_PKG_FORMAT_CFPKG && opts.external_dir) { driver_errf(PKG_TOOL, "create: --external only applies to cfpkg"); return 2; } - if (pkg_read_file(ctx, seckey, &skfd) != DIST_OK) { + if (!pkg_read(ctx, seckey, &skfd)) { driver_errf(PKG_TOOL, "create: cannot read secret key: %s", seckey); - goto done; + return 1; } sk_loaded = 1; { - int kr = dist_minisig_parse_seckey(skfd.data, skfd.size, kp.sk, kp.keyid); - if (kr == DIST_ENCRYPTED) + CfreeStatus kr = cfree_minisig_parse_seckey(skfd.data, skfd.size, sk, keyid); + if (kr == CFREE_UNSUPPORTED) driver_errf(PKG_TOOL, "create: encrypted secret keys need scrypt"); - if (kr != DIST_OK) goto done; - } - memcpy(kp.pk, kp.sk + DIST_ED25519_SEED_LEN, DIST_ED25519_PK_LEN); - - if (root) { - if (pkg_source_from_root(&src, root) != DIST_OK) goto done; - } else { - if (pkg_source_from_cas(&src, cas_dir, tree_id) != DIST_OK) goto done; - } - if (pkg_manifest_from_source(name, version, desc, &src, &m) != DIST_OK) { - driver_errf(PKG_TOOL, "create: failed to build package manifest"); - goto done; - } - - manw = pkg_mem(ctx); - sigw = pkg_mem(ctx); - pubw = pkg_mem(ctx); - if (!manw || !sigw || !pubw) goto done; - if (dist_package_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; - 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, &src, 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, &src, man_b, man_l, sig_b, sig_l, - pub_b, pub_l, pkgid, compression, native_shape, - external_dir) == DIST_OK - ? 0 - : 1; - if (rc == 0) - driver_printf("wrote %s (%llu file(s), id %s)\n", out, - (unsigned long long)src.tree.n_entries, pkgid_hex); - -done: - if (pubw) cfree_writer_close(pubw); - if (sigw) cfree_writer_close(sigw); - if (manw) cfree_writer_close(manw); - pkg_source_release(&src); - if (sk_loaded) ctx->file_io->release(ctx->file_io->user, &skfd); - return rc; -} - -static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx, - const uint8_t keyid[DIST_KEYID_LEN], - 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; - if (pkg_read_file(ctx, pubkey_opt, &fd) != DIST_OK) { - driver_errf(PKG_TOOL, "cannot read public key: %s", pubkey_opt); - return DIST_ERR; - } - ok = dist_minisig_parse_pubkey(fd.data, fd.size, pk, kid_chk); - ctx->file_io->release(ctx->file_io->user, &fd); - if (ok != DIST_OK || memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) { - driver_errf(PKG_TOOL, "public key id does not match signature"); - return DIST_ERR; - } - return DIST_OK; - } - if (pkg_trust_path(tpath, sizeof tpath) != DIST_OK) { - driver_errf(PKG_TOOL, - "no trusted-keys path (set CFREE_TRUSTED_KEYS or HOME)"); - return DIST_ERR; - } - have_store = (pkg_read_file(ctx, tpath, &store) == DIST_OK); - if (have_store && - dist_trust_lookup(store.data, store.size, keyid, pk) == DIST_OK) { - 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)", hex); - return DIST_ERR; - } - 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], parent[PKG_PATH_BUF], hex[17]; - CfreeFileData old = {0}; - CfreeWriter* w = NULL; - int had_old = (pkg_read_file(ctx, tpath, &old) == DIST_OK); - int rc = DIST_ERR; - dist_hex_encode(hex, keyid, DIST_KEYID_LEN); - 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) { - int ok = 1; - if (had_old && old.size) - ok = cfree_writer_write(w, old.data, old.size) == CFREE_OK; - if (ok) ok = cfree_writer_write(w, line, strlen(line)) == CFREE_OK; - if (ok && cfree_writer_status(w) == CFREE_OK) rc = DIST_OK; - cfree_writer_close(w); - } - if (had_old) ctx->file_io->release(ctx->file_io->user, &old); - if (rc == DIST_OK) - driver_printf("pkg: tofu-pinned key id %s to %s\n", hex, tpath); - return rc; - } -} - -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; - memset(out, 0, sizeof *out); - 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, out->keyid, pub, pub_len, pubkey_opt, tofu, - out->pk) != DIST_OK) - return DIST_ERR; - 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; - } - 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_package_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 DistPackageOutput* pkg_default_output( - const DistPackageManifest* m) { - size_t i; - for (i = 0; i < m->n_outputs; ++i) - if (m->outputs[i].is_default) return &m->outputs[i]; - return m->n_outputs ? &m->outputs[0] : NULL; -} - -static int pkg_parse_tree_object(PkgLoadedTree* out, - const uint8_t id[DIST_BLAKE2B_LEN], - const uint8_t* data, size_t len, - const char* label) { - uint8_t got[DIST_BLAKE2B_LEN]; - char err[128]; - memset(out, 0, sizeof *out); - dist_tree_id(got, data, len); - if (memcmp(got, id, DIST_BLAKE2B_LEN) != 0) { - driver_errf(PKG_TOOL, "tree id mismatch: %s", label); - return DIST_ERR; - } - out->tree.entries = out->entries; - out->tree.cap_entries = DIST_MAX_FILES; - if (dist_tree_parse(data, len, &out->tree, err, sizeof err) != DIST_OK) { - driver_errf(PKG_TOOL, "tree: %s", err); - return DIST_ERR; - } - memcpy(out->id, id, DIST_BLAKE2B_LEN); - out->bytes = data; - out->size = len; - return DIST_OK; -} - -static int pkg_verify_artifact_overlays(const DistPackageManifest* m, - const DistPackageOutput* out, - const DistTree* tree) { - size_t i; - for (i = 0; i < m->n_artifacts; ++i) { - const DistPackageArtifact* a = &m->artifacts[i]; - if (a->output_id != out->id) continue; - if (!dist_tree_find(tree, a->path)) { - driver_errf(PKG_TOOL, "artifact path not in output tree: %s", a->path); - return DIST_ERR; - } - } - return DIST_OK; -} - -static int pkg_write_output_file(DriverEnv* env, const CfreeContext* ctx, - const char* out_dir, - const DistTreeEntry* e, - const uint8_t* data, size_t len) { - char full[PKG_PATH_BUF], parent[PKG_PATH_BUF]; - if (pkg_join_path(full, sizeof full, out_dir, e->path) != DIST_OK) { - driver_errf(PKG_TOOL, "output path too long: %s", e->path); - return DIST_ERR; - } - pkg_parent_dir(full, parent, sizeof parent); - if (parent[0] && driver_mkdir_p(env, parent) != 0) return DIST_ERR; - if (pkg_write_file(ctx, full, data, len) != DIST_OK) return DIST_ERR; - if (e->mode == DIST_TREE_MODE_EXEC && - driver_mark_executable_output(full) != 0) - return DIST_ERR; - driver_printf(" extracted %s\n", full); - return DIST_OK; -} - -static const DistTarEntry* pkg_portable_find_cas(const DistTarEntry* entries, - size_t ne, const char* kind, - const uint8_t id[DIST_BLAKE2B_LEN]) { - char path[PKG_PATH_BUF]; - if (pkg_cas_rel_path(path, sizeof path, kind, id) != DIST_OK) return NULL; - return pkg_find_name(entries, ne, path); -} - -static int pkg_verify_blob_bytes(const DistTreeEntry* e, const uint8_t* data, - size_t len) { - DistBlobInfo bi; - if (dist_blob_info(&bi, data, len, DIST_BLOB_CHUNK_SIZE_DEFAULT) != DIST_OK) - return DIST_ERR; - return bi.size == e->size && - memcmp(bi.id, e->blob, DIST_BLAKE2B_LEN) == 0 && - memcmp(bi.root, e->root, DIST_BLAKE2B_LEN) == 0 - ? DIST_OK - : DIST_ERR; -} - -static int pkg_verify_portable_tree(DriverEnv* env, const CfreeContext* ctx, - const PkgVerified* v, - const DistPackageOutput* out, - const DistTarEntry* entries, size_t ne, - const char* out_dir) { - const DistTarEntry* te = pkg_portable_find_cas(entries, ne, "tree", out->tree); - PkgLoadedTree tree; - size_t i; - if (!te) { - driver_errf(PKG_TOOL, "portable package missing tree object"); - return DIST_ERR; - } - if (pkg_parse_tree_object(&tree, out->tree, te->data, te->size, - out->name) != DIST_OK) - return DIST_ERR; - if (pkg_verify_artifact_overlays(&v->manifest, out, &tree.tree) != DIST_OK) - return DIST_ERR; - for (i = 0; i < tree.tree.n_entries; ++i) { - const DistTreeEntry* e = &tree.tree.entries[i]; - const DistTarEntry* be = pkg_portable_find_cas(entries, ne, "blob", e->blob); - if (!be) { - driver_errf(PKG_TOOL, "portable package missing blob: %s", e->path); - return DIST_ERR; - } - if (pkg_verify_blob_bytes(e, be->data, be->size) != DIST_OK) { - driver_errf(PKG_TOOL, "blob hash mismatch: %s", e->path); - return DIST_ERR; - } - if (out_dir && - pkg_write_output_file(env, ctx, out_dir, e, be->data, be->size) != - DIST_OK) - return DIST_ERR; - } - return DIST_OK; -} - -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; - } - 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; - } - bytes = cfree_writer_mem_bytes(inflated, &len); - if (dist_tar_iter(bytes, len, entries, PKG_MAX_TAR_ENTRIES, ne) != DIST_OK) { - driver_errf(PKG_TOOL, "malformed portable tar"); - return DIST_ERR; + else if (kr != CFREE_OK) + driver_errf(PKG_TOOL, "create: malformed secret key: %s", seckey); + if (kr != CFREE_OK) goto done; + } + opts.sk = sk; + opts.keyid = keyid; + + host = driver_cas_host(env); + if (cfree_pkg_create(ctx, &host, &opts, &result) == CFREE_OK) { + cfree_hex_encode(pkgid_hex, result.package_id, CFREE_CAS_HASH_LEN); + driver_printf("wrote %s (%llu file(s), id %s)\n", opts.out_path, + (unsigned long long)result.n_files, pkgid_hex); + rc = 0; } - *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[PKG_MAX_TAR_ENTRIES]; - size_t ne = 0, oi; - const DistTarEntry *man, *sig, *pub; - const DistPackageOutput* def; - 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; - def = pkg_default_output(&v.manifest); - if (!def) goto done; - for (oi = 0; oi < v.manifest.n_outputs; ++oi) { - const DistPackageOutput* out = &v.manifest.outputs[oi]; - if (pkg_verify_portable_tree(env, ctx, &v, out, entries, ne, - out == def ? out_dir : NULL) != DIST_OK) - 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 (inflated) cfree_writer_close(inflated); - if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd); + if (sk_loaded) pkg_release(ctx, &skfd); return rc; } -static int pkg_bounds3(const DistCfpkg3Header* 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}}; - 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_range_ok(uint64_t off, uint64_t size, size_t len) { - return off <= len && size <= len - off; -} - -static const DistCfpkg3TreeObject* pkg_descriptor_find_tree( - const DistCfpkg3Descriptor* d, const uint8_t id[DIST_BLAKE2B_LEN]) { - size_t i; - for (i = 0; i < d->n_trees; ++i) - if (memcmp(d->trees[i].tree, id, DIST_BLAKE2B_LEN) == 0) - return &d->trees[i]; - return NULL; -} - -static int pkg_descriptor_has_embedded_chunks(const DistCfpkg3Descriptor* d) { - size_t i; - for (i = 0; i < d->n_chunk_sources; ++i) - if (d->chunk_sources[i].kind == DIST_CFPKG3_CHUNK_SOURCE_EMBEDDED) - return 1; - return 0; -} - -static const char* pkg_descriptor_chunk_template( - const DistCfpkg3Descriptor* d) { - size_t i; - for (i = 0; i < d->n_chunk_sources; ++i) - if (d->chunk_sources[i].kind == DIST_CFPKG3_CHUNK_SOURCE_URL_TEMPLATE) - return d->chunk_sources[i].tmpl; - return NULL; -} - -static int pkg_render_chunk_template(char* out, size_t cap, const char* tmpl, - const uint8_t blob[DIST_BLAKE2B_LEN], - uint64_t chunk_index) { - char blob_hex[2 * DIST_BLAKE2B_LEN + 1]; - char blob_prefix[3]; - char chunk_dec[24]; - size_t oi = 0, i; - dist_hex_encode(blob_hex, blob, DIST_BLAKE2B_LEN); - blob_prefix[0] = blob_hex[0]; - blob_prefix[1] = blob_hex[1]; - blob_prefix[2] = '\0'; - snprintf(chunk_dec, sizeof chunk_dec, "%llu", - (unsigned long long)chunk_index); - for (i = 0; tmpl[i];) { - const char* repl = NULL; - size_t repl_len = 0; - if (strncmp(tmpl + i, "{blob}", 6) == 0) { - repl = blob_hex; - repl_len = strlen(blob_hex); - i += 6; - } else if (strncmp(tmpl + i, "{blob-prefix}", 13) == 0) { - repl = blob_prefix; - repl_len = strlen(blob_prefix); - i += 13; - } else if (strncmp(tmpl + i, "{chunk}", 7) == 0) { - repl = chunk_dec; - repl_len = strlen(chunk_dec); - i += 7; - } else { - if (oi + 1u >= cap) return DIST_ERR; - out[oi++] = tmpl[i++]; - continue; - } - if (oi + repl_len >= cap) return DIST_ERR; - memcpy(out + oi, repl, repl_len); - oi += repl_len; - } - if (oi >= cap) return DIST_ERR; - out[oi] = '\0'; - return pkg_locator_safe(out) ? DIST_OK : DIST_ERR; -} +/* ---------------------------------------------------------------------- */ +/* verify / unpack */ +/* ---------------------------------------------------------------------- */ -static int pkg_verify_native_index_sorted(const uint8_t* index_b, - size_t index_l, - const DistCfpkg3Descriptor* d) { - DistCfpkg3IndexRecord prev; - size_t off; - int have_prev = 0; - int embedded_chunks = pkg_descriptor_has_embedded_chunks(d); - if (index_l != d->index_bytes || - index_l % DIST_CFPKG3_INDEX_RECORD_SIZE != 0) - return DIST_ERR; - memset(&prev, 0, sizeof prev); - for (off = 0; off < index_l; off += DIST_CFPKG3_INDEX_RECORD_SIZE) { - DistCfpkg3IndexRecord r; - int cmp; - if (dist_cfpkg3_decode_index_record(index_b + off, - DIST_CFPKG3_INDEX_RECORD_SIZE, - &r) != DIST_OK) - return DIST_ERR; - if (r.raw_size == 0 || r.raw_size > d->chunk_size || - !dist_cfpkg_compression_name(r.compression)) - return DIST_ERR; - if (embedded_chunks) { - if (r.content_offset > d->content_size || - r.stored_size > d->content_size - r.content_offset) - return DIST_ERR; - } else if (r.content_offset != 0) { - return DIST_ERR; - } - if (!have_prev) { - if (r.chunk_index != 0) return DIST_ERR; - } else { - cmp = memcmp(prev.blob_id, r.blob_id, DIST_BLAKE2B_LEN); - if (cmp > 0) return DIST_ERR; - if (cmp == 0) { - if (r.chunk_index <= prev.chunk_index) return DIST_ERR; - } else if (r.chunk_index != 0) { - return DIST_ERR; - } - } - prev = r; - have_prev = 1; - } - return DIST_OK; -} - -static int pkg_native_load_tree(const CfreeContext* ctx, const uint8_t* data, - size_t len, const DistCfpkg3Descriptor* d, - const DistPackageOutput* out, - const char* external_dir, - PkgLoadedTree* tree) { - const DistCfpkg3TreeObject* obj = pkg_descriptor_find_tree(d, out->tree); - const uint8_t* bytes; - uint8_t h[DIST_BLAKE2B_LEN]; - (void)len; - if (!obj || !obj->embedded) { - CfreeFileData fd; - char rel[PKG_PATH_BUF]; - int rc; - if (!obj || !external_dir) { - driver_errf(PKG_TOOL, "external tree object is missing"); - return DIST_ERR; - } - if (obj->url[0]) - snprintf(rel, sizeof rel, "%s", obj->url); - else if (pkg_external_id_path(rel, sizeof rel, "tree", out->tree) != - DIST_OK) - return DIST_ERR; - fd.data = NULL; - fd.size = 0; - fd.token = NULL; - if (pkg_read_external_file(ctx, external_dir, rel, &fd) != DIST_OK) { - driver_errf(PKG_TOOL, "missing external tree object: %s", rel); - return DIST_ERR; - } - pkg_hash(h, fd.data, fd.size); - if (memcmp(h, obj->blake2b, DIST_BLAKE2B_LEN) != 0 || - memcmp(h, out->tree, DIST_BLAKE2B_LEN) != 0) { - ctx->file_io->release(ctx->file_io->user, &fd); - driver_errf(PKG_TOOL, "tree object hash mismatch"); - return DIST_ERR; - } - rc = pkg_parse_tree_object(tree, out->tree, fd.data, fd.size, out->name); - ctx->file_io->release(ctx->file_io->user, &fd); - tree->bytes = NULL; - tree->size = 0; - return rc; - } - if (obj->offset > d->tree_size || obj->size > d->tree_size - obj->offset) - return DIST_ERR; - bytes = data + d->tree_offset + obj->offset; - pkg_hash(h, bytes, (size_t)obj->size); - if (memcmp(h, obj->blake2b, DIST_BLAKE2B_LEN) != 0 || - memcmp(h, out->tree, DIST_BLAKE2B_LEN) != 0) { - driver_errf(PKG_TOOL, "tree object hash mismatch"); - return DIST_ERR; - } - return pkg_parse_tree_object(tree, out->tree, bytes, (size_t)obj->size, - out->name); -} - -static int pkg_native_load_index(const CfreeContext* ctx, const uint8_t* data, - const DistCfpkg3Descriptor* d, - const char* external_dir, CfreeFileData* fd, - const uint8_t** index_b, - size_t* index_l) { - uint8_t root[DIST_BLAKE2B_LEN]; - fd->data = NULL; - fd->size = 0; - fd->token = NULL; - if (d->index_size != 0) { - if (d->index_size != d->index_bytes) return DIST_ERR; - *index_b = data + d->index_offset; - *index_l = (size_t)d->index_size; - } else { - char rel[PKG_PATH_BUF]; - if (!external_dir) { - driver_errf(PKG_TOOL, "external index is missing"); - return DIST_ERR; - } - if (d->index_url[0]) - snprintf(rel, sizeof rel, "%s", d->index_url); - else if (pkg_external_id_path(rel, sizeof rel, "index", d->index_root) != - DIST_OK) - return DIST_ERR; - if (pkg_read_external_file(ctx, external_dir, rel, fd) != DIST_OK) { - driver_errf(PKG_TOOL, "missing external index: %s", rel); - return DIST_ERR; - } - *index_b = fd->data; - *index_l = fd->size; - } - if (*index_l != d->index_bytes) return DIST_ERR; - dist_cfpkg3_region_root(root, "index", *index_b, *index_l); - return memcmp(root, d->index_root, DIST_BLAKE2B_LEN) == 0 ? DIST_OK - : DIST_ERR; -} - -static int pkg_decode_native_chunk(CfreeWriter* raww, const uint8_t* stored, - size_t stored_len, - const DistCfpkg3Descriptor* d, - const DistCfpkg3IndexRecord* r) { - uint8_t sh[DIST_BLAKE2B_LEN], rh[DIST_BLAKE2B_LEN], - leaf[DIST_BLAKE2B_LEN]; - if (r->raw_size == 0 || r->raw_size > d->chunk_size || - r->stored_size != stored_len) - return DIST_ERR; - 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) { - if (r->raw_size != r->stored_size) return DIST_ERR; - pkg_hash(rh, stored, (size_t)r->stored_size); - dist_blob_leaf_hash(leaf, r->chunk_index, stored, (size_t)r->stored_size); - if (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_CFPKG3_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_blob_leaf_hash(leaf, 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; - } - return DIST_OK; -} - -static int pkg_native_load_stored_chunk(const CfreeContext* ctx, - const uint8_t* data, - const DistCfpkg3Descriptor* d, - const DistCfpkg3IndexRecord* r, - const char* external_dir, - const char* chunk_template, - CfreeFileData* fd, - const uint8_t** stored, - size_t* stored_len) { - fd->data = NULL; - fd->size = 0; - fd->token = NULL; - if (pkg_descriptor_has_embedded_chunks(d)) { - if (r->content_offset > d->content_size || - r->stored_size > d->content_size - r->content_offset) - return DIST_ERR; - *stored = data + d->content_offset + r->content_offset; - *stored_len = (size_t)r->stored_size; - return DIST_OK; - } - { - char rel[PKG_PATH_BUF]; - if (!external_dir) return DIST_ERR; - if (chunk_template) { - if (pkg_render_chunk_template(rel, sizeof rel, chunk_template, r->blob_id, - r->chunk_index) != DIST_OK) - return DIST_ERR; - } else if (dist_cas_chunk_relpath(rel, sizeof rel, r->blob_id, - r->chunk_index) != DIST_OK) { - return DIST_ERR; - } - if (pkg_read_external_file(ctx, external_dir, rel, fd) != DIST_OK) { - driver_errf(PKG_TOOL, "missing external chunk: %s", rel); - return DIST_ERR; - } - *stored = fd->data; - *stored_len = fd->size; - return DIST_OK; - } -} - -static int pkg_native_reconstruct_blob(const CfreeContext* ctx, - const uint8_t* data, - const uint8_t* index_b, size_t index_l, - const DistCfpkg3Descriptor* d, - const DistTreeEntry* e, - const char* external_dir, - const char* chunk_template, - CfreeWriter** raww_out) { - CfreeWriter* raww = pkg_mem(ctx); - uint64_t want_chunk = 0; - size_t off; - int saw = 0; - if (!raww) return DIST_ERR; - if (index_l % DIST_CFPKG3_INDEX_RECORD_SIZE != 0) goto fail; - for (off = 0; off < index_l; off += DIST_CFPKG3_INDEX_RECORD_SIZE) { - DistCfpkg3IndexRecord r; - CfreeFileData chunk_fd; - const uint8_t* stored; - size_t stored_len; - int cmp; - if (dist_cfpkg3_decode_index_record(index_b + off, - DIST_CFPKG3_INDEX_RECORD_SIZE, - &r) != DIST_OK) - goto fail; - cmp = memcmp(r.blob_id, e->blob, DIST_BLAKE2B_LEN); - if (cmp < 0) continue; - if (cmp > 0 && saw) break; - if (cmp > 0) continue; - saw = 1; - if (r.chunk_index != want_chunk++) goto fail; - if (pkg_native_load_stored_chunk(ctx, data, d, &r, external_dir, - chunk_template, &chunk_fd, &stored, - &stored_len) != DIST_OK) - goto fail; - if (pkg_decode_native_chunk(raww, stored, stored_len, d, &r) != DIST_OK) { - if (chunk_fd.data && ctx->file_io->release) - ctx->file_io->release(ctx->file_io->user, &chunk_fd); - goto fail; - } - if (chunk_fd.data && ctx->file_io->release) - ctx->file_io->release(ctx->file_io->user, &chunk_fd); - } - *raww_out = raww; - return DIST_OK; -fail: - cfree_writer_close(raww); - return DIST_ERR; -} - -static int pkg_verify_native_tree(DriverEnv* env, const CfreeContext* ctx, - const uint8_t* data, size_t len, - const uint8_t* index_b, size_t index_l, - const DistCfpkg3Descriptor* d, - const PkgVerified* v, - const DistPackageOutput* out, - const char* external_dir, - const char* chunk_template, - const char* out_dir) { - PkgLoadedTree tree; - size_t i; - if (pkg_native_load_tree(ctx, data, len, d, out, external_dir, &tree) != - DIST_OK) - return DIST_ERR; - if (pkg_verify_artifact_overlays(&v->manifest, out, &tree.tree) != DIST_OK) - return DIST_ERR; - for (i = 0; i < tree.tree.n_entries; ++i) { - const DistTreeEntry* e = &tree.tree.entries[i]; - CfreeWriter* raww = NULL; - const uint8_t* rawb; - size_t rawl; - if (pkg_native_reconstruct_blob(ctx, data, index_b, index_l, d, e, - external_dir, chunk_template, - &raww) != DIST_OK) { - driver_errf(PKG_TOOL, "native chunk verification failed: %s", e->path); - return DIST_ERR; - } - rawb = cfree_writer_mem_bytes(raww, &rawl); - if (pkg_verify_blob_bytes(e, rawb, rawl) != DIST_OK) { - cfree_writer_close(raww); - driver_errf(PKG_TOOL, "blob hash mismatch: %s", e->path); - return DIST_ERR; - } - if (out_dir && - pkg_write_output_file(env, ctx, out_dir, e, rawb, rawl) != DIST_OK) { - cfree_writer_close(raww); - return DIST_ERR; - } - cfree_writer_close(raww); - } - return DIST_OK; -} - -static int pkg_verify_native(DriverEnv* env, const CfreeContext* ctx, - const char* file, const char* pubkey, int tofu, - const char* external_dir, const char* out_dir, - int quiet) { - CfreeFileData fd = {0}, index_fd = {0}; - DistCfpkg3Header h; - DistCfpkg3Descriptor d; - PkgVerified v; - char err[128]; - uint8_t desc_keyid[DIST_KEYID_LEN], tree_root[DIST_BLAKE2B_LEN], - index_root[DIST_BLAKE2B_LEN], content_root[DIST_BLAKE2B_LEN]; - char desc_trusted[DIST_TRUSTED_COMMENT_MAX]; - const DistPackageOutput* def; - const uint8_t* index_b = NULL; - size_t index_l = 0; - const char* chunk_template = NULL; - size_t oi; - 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 (dist_cfpkg3_read_header(fd.data, fd.size, &h) != DIST_OK || - pkg_bounds3(&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_cfpkg3_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 (memcmp(d.package_id, v.package_id, DIST_BLAKE2B_LEN) != 0 || - d.chunk_size != DIST_CFPKG3_CHUNK_SIZE_DEFAULT || - d.alignment != DIST_CFPKG3_ALIGNMENT || - !pkg_range_ok(d.tree_offset, d.tree_size, fd.size) || - !pkg_range_ok(d.index_offset, d.index_size, fd.size) || - !pkg_range_ok(d.content_offset, d.content_size, fd.size)) { - driver_errf(PKG_TOOL, "encoding descriptor does not match package layout"); - goto done; - } - dist_cfpkg3_region_root(tree_root, "tree", fd.data + d.tree_offset, - (size_t)d.tree_size); - dist_cfpkg3_region_root(content_root, "content", fd.data + d.content_offset, - (size_t)d.content_size); - if (pkg_native_load_index(ctx, fd.data, &d, external_dir, &index_fd, - &index_b, &index_l) != DIST_OK) { - driver_errf(PKG_TOOL, "native package index verification failed"); - goto done; - } - dist_cfpkg3_region_root(index_root, "index", index_b, index_l); - if (!pkg_descriptor_has_embedded_chunks(&d)) - chunk_template = pkg_descriptor_chunk_template(&d); - if (d.index_bytes && !pkg_descriptor_has_embedded_chunks(&d) && - !external_dir) { - driver_errf(PKG_TOOL, "external native chunks are missing"); - goto done; - } - if (memcmp(tree_root, d.tree_root, DIST_BLAKE2B_LEN) != 0 || - 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 (pkg_verify_native_index_sorted(index_b, index_l, &d) != DIST_OK) { - driver_errf(PKG_TOOL, "native chunk index is malformed"); - goto done; - } - def = pkg_default_output(&v.manifest); - if (!def) goto done; - for (oi = 0; oi < v.manifest.n_outputs; ++oi) { - const DistPackageOutput* out = &v.manifest.outputs[oi]; - if (pkg_verify_native_tree(env, ctx, fd.data, fd.size, index_b, index_l, - &d, &v, out, external_dir, chunk_template, - out == def ? out_dir : NULL) != DIST_OK) - 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); +static void pkg_pin_tofu(DriverEnv* env, const CfreeContext* ctx, + const char* tpath, const CfreePkgVerifyResult* r) { + char line[PKG_TRUST_LINE_MAX], parent[PKG_PATH_BUF], hex[PKG_KEYID_HEX]; + CfreeFileData old; + CfreeWriter* w = NULL; + int had_old, ok = 1, wrote = 0; + if (!tpath[0]) return; + cfree_hex_encode(hex, r->keyid, CFREE_PKG_KEYID_LEN); + if (cfree_trust_format_entry(line, sizeof line, r->keyid, r->tofu_pk, + "tofu-pinned") != CFREE_OK) + return; + had_old = pkg_read(ctx, tpath, &old); + 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) { + if (had_old && old.size) + ok = cfree_writer_write(w, old.data, old.size) == CFREE_OK; + if (ok) ok = cfree_writer_write(w, line, strlen(line)) == CFREE_OK; + if (ok && cfree_writer_status(w) == CFREE_OK) wrote = 1; + cfree_writer_close(w); } - if (out_dir) - driver_printf("unpacked %s %s to %s\n", v.manifest.name, - v.manifest.version, out_dir); - rc = DIST_OK; -done: - if (index_fd.token || index_fd.data) - ctx->file_io->release(ctx->file_io->user, &index_fd); - if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd); - return rc; + if (had_old) pkg_release(ctx, &old); + if (wrote) driver_printf("pkg: tofu-pinned key id %s to %s\n", hex, tpath); } static int pkg_verify_or_unpack(DriverEnv* env, const CfreeContext* ctx, int argc, char** argv, int unpack) { const char *file = NULL, *pubkey = NULL, *dir = ".", *external_dir = NULL; - int tofu = 0, explicit_verify = 0, i; - PkgFormat fmt = PKG_FMT_AUTO; + int tofu = 0, explicit_verify = 0, i, rc = 1; + CfreePkgFormat fmt = CFREE_PKG_FORMAT_AUTO; + CfreePkgVerifyOptions opts; + CfreePkgVerifyResult result; + CfreeCasHost host; + CfreeFileData pkgfd, pubfd, trustfd; + char tpath[PKG_PATH_BUF]; + int pub_loaded = 0, trust_loaded = 0, have_tpath; for (i = 0; i < argc; ++i) { if (driver_streq(argv[i], "-p") && i + 1 < argc) pubkey = argv[++i]; @@ -1881,7 +340,7 @@ static int pkg_verify_or_unpack(DriverEnv* env, const CfreeContext* ctx, external_dir = argv[++i]; else if (driver_streq(argv[i], "--format") && i + 1 < argc) { fmt = pkg_parse_format(argv[++i]); - if (fmt == PKG_FMT_AUTO) { + if (fmt == CFREE_PKG_FORMAT_AUTO) { driver_errf(PKG_TOOL, "%s: unknown format", unpack ? "unpack" : "verify"); return 2; } @@ -1899,29 +358,74 @@ static int pkg_verify_or_unpack(DriverEnv* env, const CfreeContext* ctx, driver_errf(PKG_TOOL, "%s: FILE is required", unpack ? "unpack" : "verify"); return 2; } - 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 && !explicit_verify) == DIST_OK - ? 0 - : 1; - return pkg_verify_native(env, ctx, file, pubkey, tofu, external_dir, - unpack ? dir : NULL, - unpack && !explicit_verify) == DIST_OK - ? 0 - : 1; + if (fmt == CFREE_PKG_FORMAT_AUTO) fmt = pkg_infer_format(file); + + if (!pkg_read(ctx, file, &pkgfd)) { + driver_errf(PKG_TOOL, "cannot read package: %s", file); + return 1; + } + memset(&opts, 0, sizeof opts); + opts.pkg_data = pkgfd.data; + opts.pkg_len = pkgfd.size; + opts.format = fmt; + opts.external_dir = external_dir; + opts.unpack_dir = unpack ? dir : NULL; + opts.tofu = tofu; + if (pubkey) { + if (!pkg_read(ctx, pubkey, &pubfd)) { + driver_errf(PKG_TOOL, "cannot read public key: %s", pubkey); + pkg_release(ctx, &pkgfd); + return 1; + } + pub_loaded = 1; + opts.pubkey_bytes = pubfd.data; + opts.pubkey_len = pubfd.size; + } + have_tpath = (pkg_trust_path(tpath, sizeof tpath) == 0); + if (!have_tpath) tpath[0] = '\0'; + if (have_tpath && !pubkey && pkg_read(ctx, tpath, &trustfd)) { + trust_loaded = 1; + opts.trusted_keys = trustfd.data; + opts.trusted_keys_len = trustfd.size; + } + + host = driver_cas_host(env); + if (cfree_pkg_verify(ctx, &host, &opts, &result) == CFREE_OK) { + int quiet = unpack && !explicit_verify; + if (result.tofu_pin) pkg_pin_tofu(env, ctx, tpath, &result); + if (!quiet) { + char idhex[PKG_KEYID_HEX]; + cfree_hex_encode(idhex, result.keyid, CFREE_PKG_KEYID_LEN); + driver_printf("ok: %s %s signer %s [%s]\n", result.name, result.version, + idhex, result.trusted); + } + if (opts.unpack_dir) + driver_printf("unpacked %s %s to %s\n", result.name, result.version, + opts.unpack_dir); + rc = 0; + } + + if (trust_loaded) pkg_release(ctx, &trustfd); + if (pub_loaded) pkg_release(ctx, &pubfd); + pkg_release(ctx, &pkgfd); + return rc; } -static int pkg_inspect(const CfreeContext* ctx, int argc, char** argv) { +/* ---------------------------------------------------------------------- */ +/* inspect */ +/* ---------------------------------------------------------------------- */ + +static int pkg_inspect(DriverEnv* env, const CfreeContext* ctx, int argc, + char** argv) { const char* file = NULL; - PkgFormat fmt = PKG_FMT_AUTO; - int show_encoding = 0; - int i, rc = 1; + CfreePkgFormat fmt = CFREE_PKG_FORMAT_AUTO; + int show_encoding = 0, i, rc = 1; + CfreeFileData fd; + CfreeWriter* out; 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) { + if (fmt == CFREE_PKG_FORMAT_AUTO) { driver_errf(PKG_TOOL, "inspect: unknown format"); return 2; } @@ -1940,51 +444,36 @@ static int pkg_inspect(const CfreeContext* ctx, int argc, char** argv) { driver_errf(PKG_TOOL, "inspect: FILE is required"); return 2; } - if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(file); - if (fmt == PKG_FMT_TARGZ) { - if (show_encoding) { - driver_errf(PKG_TOOL, "inspect: portable packages have no encoding descriptor"); - return 2; - } - CfreeFileData fd = {0}; - CfreeWriter* inflated = NULL; - DistTarEntry entries[PKG_MAX_TAR_ENTRIES]; - 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}; - DistCfpkg3Header h; - if (pkg_read_file(ctx, file, &fd) == DIST_OK && - dist_cfpkg3_read_header(fd.data, fd.size, &h) == DIST_OK && - pkg_bounds3(&h, fd.size) == DIST_OK) { - if (show_encoding) - driver_printf("%.*s", (int)h.descriptor_size, - (const char*)(fd.data + h.descriptor_offset)); - else - driver_printf("%.*s", (int)h.manifest_size, - (const char*)(fd.data + h.manifest_offset)); + if (fmt == CFREE_PKG_FORMAT_AUTO) fmt = pkg_infer_format(file); + if (fmt == CFREE_PKG_FORMAT_TARGZ && show_encoding) { + driver_errf(PKG_TOOL, + "inspect: portable packages have no encoding descriptor"); + return 2; + } + if (!pkg_read(ctx, file, &fd)) { + driver_errf(PKG_TOOL, "cannot read package: %s", file); + return 1; + } + out = driver_stdout_writer(env); + if (out) { + if (cfree_pkg_inspect(ctx, fd.data, fd.size, fmt, show_encoding, out) == + CFREE_OK) 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; + cfree_writer_close(out); } + pkg_release(ctx, &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) { + if (pkg_trust_path(tpath, sizeof tpath) != 0) { driver_errf(PKG_TOOL, "no trusted-keys path (set CFREE_TRUSTED_KEYS or HOME)"); return 1; @@ -1998,44 +487,46 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, return 0; } if (driver_streq(sub, "list")) { - CfreeFileData fd = {0}; - if (pkg_read_file(ctx, tpath, &fd) != DIST_OK) { + CfreeFileData fd; + if (!pkg_read(ctx, tpath, &fd)) { driver_printf("(no trusted keys at %s)\n", tpath); return 0; } driver_printf("%.*s", (int)fd.size, (const char*)fd.data); - ctx->file_io->release(ctx->file_io->user, &fd); + pkg_release(ctx, &fd); return 0; } if (driver_streq(sub, "add")) { const char* pubkey = (argc > 1) ? argv[1] : NULL; const char* label = (argc > 2) ? argv[2] : ""; - CfreeFileData kf = {0}, old = {0}; - uint8_t pk[DIST_ED25519_PK_LEN], keyid[DIST_KEYID_LEN], - dummy[DIST_ED25519_PK_LEN]; - char line[DIST_TRUST_LINE_MAX], parent[PKG_PATH_BUF]; + CfreeFileData kf, old; + uint8_t pk[CFREE_PKG_PK_LEN], keyid[CFREE_PKG_KEYID_LEN], + dummy[CFREE_PKG_PK_LEN]; + char line[PKG_TRUST_LINE_MAX], parent[PKG_PATH_BUF]; int had_old, ok = 1, rc = 1; CfreeWriter* w = NULL; if (!pubkey) { driver_errf(PKG_TOOL, "trust add: PUBKEY is required"); return 2; } - if (pkg_read_file(ctx, pubkey, &kf) != DIST_OK) { + if (!pkg_read(ctx, pubkey, &kf)) { driver_errf(PKG_TOOL, "cannot read public key: %s", pubkey); return 1; } - ok = dist_minisig_parse_pubkey(kf.data, kf.size, pk, keyid) == DIST_OK; - ctx->file_io->release(ctx->file_io->user, &kf); + ok = cfree_minisig_parse_pubkey(kf.data, kf.size, pk, keyid) == CFREE_OK; + pkg_release(ctx, &kf); if (!ok) return 1; - had_old = (pkg_read_file(ctx, tpath, &old) == DIST_OK); + had_old = pkg_read(ctx, tpath, &old); if (had_old && - dist_trust_lookup(old.data, old.size, keyid, dummy) == DIST_OK) { - ctx->file_io->release(ctx->file_io->user, &old); + cfree_trust_lookup(old.data, old.size, keyid, dummy) == CFREE_OK) { + pkg_release(ctx, &old); return 0; } - if (dist_trust_format_entry(line, sizeof line, keyid, pk, label) != - DIST_OK) + if (cfree_trust_format_entry(line, sizeof line, keyid, pk, label) != + CFREE_OK) { + if (had_old) pkg_release(ctx, &old); 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) { @@ -2045,34 +536,36 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc, if (ok && cfree_writer_status(w) == CFREE_OK) rc = 0; cfree_writer_close(w); } - if (had_old) ctx->file_io->release(ctx->file_io->user, &old); + if (had_old) pkg_release(ctx, &old); return rc; } if (driver_streq(sub, "remove")) { const char* idhex = (argc > 1) ? argv[1] : NULL; - CfreeFileData old = {0}; - uint8_t want[DIST_KEYID_LEN]; + CfreeFileData old; + uint8_t want[CFREE_PKG_KEYID_LEN]; CfreeWriter* w = NULL; size_t pos = 0; int rc = 1; - if (!idhex || strlen(idhex) != 2 * DIST_KEYID_LEN || - dist_hex_decode(want, idhex, DIST_KEYID_LEN) != DIST_OK) + if (!idhex || strlen(idhex) != 2 * CFREE_PKG_KEYID_LEN || + cfree_hex_decode(want, idhex, CFREE_PKG_KEYID_LEN) != CFREE_OK) return 2; - 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) + if (!pkg_read(ctx, tpath, &old)) return 0; + if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) != CFREE_OK) { + pkg_release(ctx, &old); return 1; + } rc = 0; while (pos < old.size) { size_t start = pos, end = pos; - uint8_t got[DIST_KEYID_LEN]; - char idbuf[2 * DIST_KEYID_LEN + 1]; + uint8_t got[CFREE_PKG_KEYID_LEN]; + char idbuf[2 * CFREE_PKG_KEYID_LEN + 1]; int keep = 1; while (end < old.size && old.data[end] != '\n') ++end; - 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) + if (end - start >= 2 * CFREE_PKG_KEYID_LEN) { + memcpy(idbuf, old.data + start, 2 * CFREE_PKG_KEYID_LEN); + idbuf[2 * CFREE_PKG_KEYID_LEN] = '\0'; + if (cfree_hex_decode(got, idbuf, CFREE_PKG_KEYID_LEN) == CFREE_OK && + memcmp(got, want, CFREE_PKG_KEYID_LEN) == 0) keep = 0; } if (keep) { @@ -2083,7 +576,7 @@ 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); + pkg_release(ctx, &old); return rc; } driver_errf(PKG_TOOL, "trust: unknown subcommand: %s", sub); @@ -2111,7 +604,7 @@ int driver_pkg(int argc, char** argv) { 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); + rc = pkg_inspect(&env, &ctx, argc - 2, argv + 2); else if (driver_streq(sub, "trust")) rc = pkg_trust(&env, &ctx, argc - 2, argv + 2); else { diff --git a/driver/dist/blake2b.h b/driver/dist/blake2b.h @@ -1,23 +0,0 @@ -#ifndef CFREE_DIST_BLAKE2B_H -#define CFREE_DIST_BLAKE2B_H - -#include <stddef.h> -#include <stdint.h> - -#include "dist.h" -#include "vendor/monocypher/monocypher.h" - -/* v2 package/content hash. Minisign signatures use their own 64-byte BLAKE2b - * prehash path in minisig.c for stock minisign compatibility. */ -typedef struct DistBlake2b { - crypto_blake2b_ctx ctx; -} 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); - -#endif diff --git a/driver/dist/ed25519.c b/driver/dist/ed25519.c @@ -1,24 +0,0 @@ -#include "ed25519.h" - -#include <string.h> - -#include "vendor/monocypher/monocypher-ed25519.h" - -void dist_ed25519_keypair(uint8_t pk[DIST_ED25519_PK_LEN], - uint8_t sk[DIST_ED25519_SK_LEN], - const uint8_t seed[DIST_ED25519_SEED_LEN]) { - uint8_t seed_copy[DIST_ED25519_SEED_LEN]; - memcpy(seed_copy, seed, sizeof seed_copy); - crypto_ed25519_key_pair(sk, pk, seed_copy); -} - -void dist_ed25519_sign(uint8_t sig[DIST_ED25519_SIG_LEN], const uint8_t* msg, - size_t msglen, const uint8_t sk[DIST_ED25519_SK_LEN]) { - crypto_ed25519_sign(sig, sk, msg, msglen); -} - -int dist_ed25519_verify(const uint8_t sig[DIST_ED25519_SIG_LEN], - const uint8_t* msg, size_t msglen, - const uint8_t pk[DIST_ED25519_PK_LEN]) { - return crypto_ed25519_check(sig, pk, msg, msglen) == 0 ? 1 : 0; -} diff --git a/driver/dist/lz4.c b/driver/dist/lz4.c @@ -1,39 +0,0 @@ -#include "lz4.h" - -#include <limits.h> - -#define LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION 1 -#define LZ4LIB_VISIBILITY -#include "vendor/lz4/lz4.c" - -size_t dist_lz4_compress_bound(size_t raw_len) { - int bound; - if (raw_len > (size_t)LZ4_MAX_INPUT_SIZE) return 0; - bound = LZ4_compressBound((int)raw_len); - if (bound <= 0) return 0; - return (size_t)bound; -} - -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 n; - if (!dst || !dst_len || (!src && src_len != 0)) return DIST_ERR; - if (src_len > (size_t)LZ4_MAX_INPUT_SIZE || dst_cap > (size_t)INT_MAX) - return DIST_ERR; - n = LZ4_compress_default((const char*)src, (char*)dst, (int)src_len, - (int)dst_cap); - if (n <= 0) return DIST_ERR; - *dst_len = (size_t)n; - return DIST_OK; -} - -int dist_lz4_decompress_block(uint8_t* dst, size_t dst_len, - const uint8_t* src, size_t src_len) { - int n; - if (!dst || (!src && src_len != 0)) return DIST_ERR; - if (dst_len > (size_t)INT_MAX || src_len > (size_t)INT_MAX) return DIST_ERR; - n = LZ4_decompress_safe((const char*)src, (char*)dst, (int)src_len, - (int)dst_len); - if (n != (int)dst_len) return DIST_ERR; - return DIST_OK; -} diff --git a/driver/lib/dist_host.c b/driver/lib/dist_host.c @@ -0,0 +1,32 @@ +#include "dist_host.h" + +static int dh_mkdir_p(void* user, const char* path) { + return driver_mkdir_p((DriverEnv*)user, path); +} + +static int dh_mark_executable(void* user, const char* path) { + (void)user; + return driver_mark_executable_output(path); +} + +static int dh_walk(void* user, const char* root, CfreeCasWalkFn cb, + void* cb_user) { + /* CfreeCasWalkFn and DriverWalkFileFn have identical signatures. */ + return driver_walk_regular_files((DriverEnv*)user, root, (DriverWalkFileFn)cb, + cb_user); +} + +CfreeCasHost driver_cas_host(DriverEnv* env) { + CfreeCasHost h; + h.file_io = &env->file_io; + h.mkdir_p = dh_mkdir_p; + h.mark_executable = dh_mark_executable; + h.walk_regular_files = dh_walk; + h.user = env; + return h; +} + +int driver_dist_random(void* user, uint8_t* out, size_t n) { + (void)user; + return driver_random_bytes(out, n); +} diff --git a/driver/lib/dist_host.h b/driver/lib/dist_host.h @@ -0,0 +1,21 @@ +#ifndef CFREE_DRIVER_DIST_HOST_H +#define CFREE_DRIVER_DIST_HOST_H + +#include <cfree/cas.h> +#include <stdint.h> + +#include "env.h" + +/* Adapters that turn a DriverEnv into the host vtables libcfree's cas/package + * APIs expect. The returned struct borrows `env`; it must not outlive + * driver_env_fini. */ + +/* CfreeCasHost backed by the DriverEnv's POSIX file I/O, mkdir -p, chmod, and + * recursive directory walk. */ +CfreeCasHost driver_cas_host(DriverEnv* env); + +/* Host CSPRNG adapter, shaped for cfree_pkg_keygen's fill_random parameter. + * Ignores `user`; reads from the host CSPRNG. */ +int driver_dist_random(void* user, uint8_t* out, size_t n); + +#endif diff --git a/include/cfree/cas.h b/include/cfree/cas.h @@ -0,0 +1,112 @@ +#ifndef CFREE_CAS_H +#define CFREE_CAS_H + +#include <cfree/core.h> +#include <stddef.h> +#include <stdint.h> + +/* + * Content-addressed store (CAS): blobs (raw file bytes) and tree manifests + * (one directory listing) keyed by their BLAKE2b-256 hash. The content model + * is self-verifying and carries no signatures; the signed package layer is in + * <cfree/package.h>. + * + * The library sources no entropy and performs no I/O except through the host + * vtable below and CfreeContext.file_io. Operational failures return a + * CfreeStatus and emit a human-readable message through ctx->diag (with no + * source location); pass a NULL diag sink to stay quiet and inspect the status. + */ + +#define CFREE_CAS_HASH_LEN 32u + +typedef enum CfreeTreeMode { + CFREE_TREE_MODE_FILE = 0, + CFREE_TREE_MODE_EXEC = 1, +} CfreeTreeMode; + +/* Identity of one blob: the CAS key (hash of the whole bytes), the chunk + * merkle root (authenticates a streamed chunk sequence), the byte size, and + * the chunk count. */ +typedef struct CfreeBlobInfo { + uint8_t id[CFREE_CAS_HASH_LEN]; + uint8_t root[CFREE_CAS_HASH_LEN]; + uint64_t size; + uint64_t chunks; +} CfreeBlobInfo; + +/* Invoked once per regular file during a directory walk. tree_path is the + * slash-separated path relative to the walk root; executable is nonzero for + * files that should carry the exec mode. Return nonzero to abort the walk. */ +typedef int (*CfreeCasWalkFn)(void* user, const char* source_path, + const char* tree_path, int executable); + +/* Host services beyond CfreeContext. file_io is always required; mkdir_p and + * mark_executable are required by materialize; walk_regular_files is required + * only by cfree_cas_add_tree_from_dir. Each callback returns 0 on success and + * nonzero on failure. */ +typedef struct CfreeCasHost { + const CfreeFileIO* file_io; + int (*mkdir_p)(void* user, const char* path); + int (*mark_executable)(void* user, const char* path); + int (*walk_regular_files)(void* user, const char* root, CfreeCasWalkFn cb, + void* cb_user); + void* user; +} CfreeCasHost; + +typedef struct CfreeCas CfreeCas; +typedef struct CfreeCasTreeBuilder CfreeCasTreeBuilder; + +/* Hash raw bytes into a blob id + chunk merkle root (no I/O, no store). */ +CFREE_API void cfree_blob_info(CfreeBlobInfo* out, const uint8_t* data, + size_t len); + +/* Lowercase-hex helpers for content ids. cfree_hex_encode writes 2*n+1 bytes + * including the trailing NUL; cfree_hex_decode reads exactly 2*n hex chars and + * returns CFREE_MALFORMED on a non-hex character. */ +CFREE_API void cfree_hex_encode(char* out, const uint8_t* in, size_t n); +CFREE_API CfreeStatus cfree_hex_decode(uint8_t* out, const char* in, size_t n); + +/* Open a CAS rooted at root_path. host and root_path are borrowed for the + * lifetime of the handle. */ +CFREE_API CfreeStatus cfree_cas_open(const CfreeContext* ctx, + const CfreeCasHost* host, + const char* root_path, CfreeCas** out); +CFREE_API void cfree_cas_close(CfreeCas* cas); + +/* Store raw bytes as a blob; fills out with its id/root/size/chunks. */ +CFREE_API CfreeStatus cfree_cas_add_blob(CfreeCas* cas, const uint8_t* data, + size_t len, CfreeBlobInfo* out); + +/* Incrementally build a tree. Each add hashes its bytes and stores the blob; + * finish sorts and validates the entries, stores the canonical tree manifest, + * and yields its id. Free the builder when done (finish does not free it). */ +CFREE_API CfreeStatus cfree_cas_tree_builder_new(CfreeCas* cas, + CfreeCasTreeBuilder** out); +CFREE_API CfreeStatus cfree_cas_tree_builder_add(CfreeCasTreeBuilder* b, + const char* tree_path, + CfreeTreeMode mode, + const uint8_t* data, + size_t len); +CFREE_API CfreeStatus cfree_cas_tree_builder_finish( + CfreeCasTreeBuilder* b, uint8_t out_tree_id[CFREE_CAS_HASH_LEN]); +CFREE_API void cfree_cas_tree_builder_free(CfreeCasTreeBuilder* b); + +/* Walk a directory (requires host->walk_regular_files), hashing and storing + * every regular file, then store the resulting tree and yield its id. */ +CFREE_API CfreeStatus cfree_cas_add_tree_from_dir( + CfreeCas* cas, const char* root, uint8_t out_tree_id[CFREE_CAS_HASH_LEN]); + +/* Write a stored tree manifest's canonical bytes to out. */ +CFREE_API CfreeStatus cfree_cas_inspect_tree( + CfreeCas* cas, const uint8_t tree_id[CFREE_CAS_HASH_LEN], CfreeWriter* out); + +/* Verify every blob referenced by a stored tree (presence + size + root). */ +CFREE_API CfreeStatus cfree_cas_verify_tree( + CfreeCas* cas, const uint8_t tree_id[CFREE_CAS_HASH_LEN]); + +/* Recreate a stored tree's files under dst, verifying each blob and applying + * modes (requires host->mkdir_p and host->mark_executable). */ +CFREE_API CfreeStatus cfree_cas_materialize_tree( + CfreeCas* cas, const uint8_t tree_id[CFREE_CAS_HASH_LEN], const char* dst); + +#endif diff --git a/include/cfree/config.h b/include/cfree/config.h @@ -72,6 +72,12 @@ #define CFREE_DBG_ENABLED 1 #define CFREE_EMU_ENABLED 1 +/* Code distribution: the content-addressed store (CAS) and signed `.cfpkg` + * packaging. CAS is the self-verifying content model; PKG layers signed + * manifests + the vendored crypto/compression on top, so it requires CAS. */ +#define CFREE_CAS_ENABLED 1 +#define CFREE_PKG_ENABLED 1 + /* Threaded-bytecode interpreter for the optimizer IR. Runs cfree IR * directly (host-identity) or over the emu address space; requires the * optimizer pipeline (it consumes the O1 PReg-path Func). */ diff --git a/include/cfree/package.h b/include/cfree/package.h @@ -0,0 +1,142 @@ +#ifndef CFREE_PACKAGE_H +#define CFREE_PACKAGE_H + +#include <cfree/cas.h> +#include <cfree/core.h> +#include <stddef.h> +#include <stdint.h> + +/* + * Signed, content-addressed distribution packages. A package is a minisign + * signature over a canonical manifest that claims one or more CAS trees, + * carried as either a portable .tar.gz or a native .cfpkg. See + * doc/DISTRIBUTE.md. + * + * Trust *policy* stays with the caller: where the trusted-keys file lives and + * whether to pin a key are decided outside this layer. The verify entry point + * is handed the trusted-keys bytes (and any -p / --tofu inputs) and reports a + * pin decision back through its result. Operational failures return a + * CfreeStatus and emit detail through ctx->diag. + */ + +#define CFREE_PKG_KEYID_LEN 8u +#define CFREE_PKG_PK_LEN 32u +#define CFREE_PKG_SK_LEN 64u +#define CFREE_PKG_NAME_MAX 128u +#define CFREE_PKG_VERSION_MAX 64u +#define CFREE_PKG_TRUSTED_COMMENT_MAX 512u + +typedef enum CfreePkgFormat { + CFREE_PKG_FORMAT_AUTO = 0, + CFREE_PKG_FORMAT_CFPKG = 1, + CFREE_PKG_FORMAT_TARGZ = 2, +} CfreePkgFormat; + +typedef enum CfreePkgShape { + CFREE_PKG_SHAPE_FAT = 0, + CFREE_PKG_SHAPE_METADATA = 1, + CFREE_PKG_SHAPE_THIN = 2, +} CfreePkgShape; + +typedef enum CfreePkgCompression { + CFREE_PKG_COMPRESSION_NONE = 0, + CFREE_PKG_COMPRESSION_LZ4_BLOCK_V1 = 1, +} CfreePkgCompression; + +/* CSPRNG callback for keygen: fill out with n random bytes, return 0 on + * success. This layer sources no entropy itself. */ +typedef int (*CfreePkgRandomFn)(void* user, uint8_t* out, size_t n); + +/* Generate a passwordless minisign keypair, emit the public key to pub_out and + * the secret key to sec_out, and return the 8-byte key id. */ +CFREE_API CfreeStatus cfree_pkg_keygen(const CfreeContext* ctx, + CfreePkgRandomFn rng, void* rng_user, + CfreeWriter* pub_out, CfreeWriter* sec_out, + uint8_t out_keyid[CFREE_PKG_KEYID_LEN]); + +/* Parse minisign key files (passwordless only). parse_seckey returns + * CFREE_UNSUPPORTED for scrypt-encrypted secret keys. */ +CFREE_API CfreeStatus cfree_minisig_parse_pubkey( + const uint8_t* data, size_t len, uint8_t pk_out[CFREE_PKG_PK_LEN], + uint8_t keyid_out[CFREE_PKG_KEYID_LEN]); +CFREE_API CfreeStatus cfree_minisig_parse_seckey( + const uint8_t* data, size_t len, uint8_t sk_out[CFREE_PKG_SK_LEN], + uint8_t keyid_out[CFREE_PKG_KEYID_LEN]); + +/* Trusted-keys store helpers (the store is plain text managed by the caller). + * lookup finds keyid's public key; format_entry renders one NUL-terminated, + * newline-included store line. */ +CFREE_API CfreeStatus cfree_trust_lookup( + const uint8_t* file, size_t len, const uint8_t keyid[CFREE_PKG_KEYID_LEN], + uint8_t pk_out[CFREE_PKG_PK_LEN]); +CFREE_API CfreeStatus cfree_trust_format_entry( + char* out, size_t cap, const uint8_t keyid[CFREE_PKG_KEYID_LEN], + const uint8_t pk[CFREE_PKG_PK_LEN], const char* label); + +typedef struct CfreePkgCreateOptions { + const char* name; + const char* version; + const char* description; /* NULL or "" if absent */ + CfreePkgFormat format; /* CFPKG or TARGZ (not AUTO) */ + CfreePkgShape native_shape; /* cfpkg only */ + CfreePkgCompression compression; + const char* external_dir; /* non-fat cfpkg; else NULL */ + const char* root_dir; /* package a directory, or NULL */ + const char* cas_dir; /* package an existing CAS tree, or NULL */ + const char* tree_id; /* hex tree id when cas_dir is set */ + const uint8_t* sk; /* signing secret key (64 bytes) */ + const uint8_t* keyid; /* signing key id (8 bytes) */ + const char* out_path; /* output package path */ +} CfreePkgCreateOptions; + +typedef struct CfreePkgCreateResult { + uint64_t n_files; + uint8_t package_id[CFREE_CAS_HASH_LEN]; +} CfreePkgCreateResult; + +/* Build (and sign) a package from a directory (root_dir) or an existing CAS + * tree (cas_dir + tree_id), writing it to opts->out_path. */ +CFREE_API CfreeStatus cfree_pkg_create(const CfreeContext* ctx, + const CfreeCasHost* host, + const CfreePkgCreateOptions* opts, + CfreePkgCreateResult* result); + +typedef struct CfreePkgVerifyOptions { + const uint8_t* pkg_data; + size_t pkg_len; + CfreePkgFormat format; /* CFPKG or TARGZ (not AUTO) */ + const char* external_dir; /* or NULL */ + const char* unpack_dir; /* NULL = verify only; else materialize here */ + const uint8_t* pubkey_bytes; /* -p key file content, or NULL */ + size_t pubkey_len; + int tofu; /* trust-on-first-use */ + const uint8_t* trusted_keys; /* trusted-keys file content, or NULL */ + size_t trusted_keys_len; +} CfreePkgVerifyOptions; + +typedef struct CfreePkgVerifyResult { + char name[CFREE_PKG_NAME_MAX]; + char version[CFREE_PKG_VERSION_MAX]; + char trusted[CFREE_PKG_TRUSTED_COMMENT_MAX]; + uint8_t keyid[CFREE_PKG_KEYID_LEN]; /* signer */ + int tofu_pin; /* 1 -> caller should persist tofu_pk */ + uint8_t tofu_pk[CFREE_PKG_PK_LEN]; +} CfreePkgVerifyResult; + +/* Verify a package's signature and content, and (when unpack_dir is set) + * materialize its default output there. Key resolution uses the supplied + * pubkey/trusted-keys/tofu inputs; on a trust-on-first-use acceptance, result + * reports the key the caller should pin. */ +CFREE_API CfreeStatus cfree_pkg_verify(const CfreeContext* ctx, + const CfreeCasHost* host, + const CfreePkgVerifyOptions* opts, + CfreePkgVerifyResult* result); + +/* Write the manifest (show_encoding == 0) or the native encoding descriptor + * (show_encoding == 1) text of a package to out. */ +CFREE_API CfreeStatus cfree_pkg_inspect(const CfreeContext* ctx, + const uint8_t* pkg_data, size_t pkg_len, + CfreePkgFormat format, int show_encoding, + CfreeWriter* out); + +#endif diff --git a/src/api/cas.c b/src/api/cas.c @@ -0,0 +1,392 @@ +/* Public content-addressed store API: a thin composition over the internal + * dist content model (src/dist/{blob,tree,cas}.c). See <cfree/cas.h>. */ + +#include <cfree/cas.h> + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +#include "dist/blob.h" +#include "dist/cas.h" +#include "dist/dist.h" +#include "dist/tree.h" + +/* Emit a human-readable operational error through the context diag sink (no + * source location), mirroring how other subsystems report. No-op when the + * caller supplied no sink. */ +static void cas_diagf(const CfreeContext* ctx, const char* fmt, ...) { + va_list ap; + CfreeSrcLoc loc; + if (!ctx || !ctx->diag || !ctx->diag->emit) return; + loc.file_id = 0; + loc.line = 0; + loc.col = 0; + va_start(ap, fmt); + ctx->diag->emit(ctx->diag, CFREE_DIAG_ERROR, loc, fmt, ap); + va_end(ap); +} + +struct CfreeCas { + /* Own a copy of the context so the handle outlives the caller's (possibly + * stack-local) CfreeContext; ctx points at the stored copy. The pointed-to + * heap/file_io/diag must still outlive the handle. */ + CfreeContext ctx_storage; + const CfreeContext* ctx; + CfreeCasHost host; + DistCas dist; +}; + +struct CfreeCasTreeBuilder { + CfreeCas* cas; + DistTree tree; + DistTreeEntry* entries; +}; + +void cfree_blob_info(CfreeBlobInfo* out, const uint8_t* data, size_t len) { + DistBlobInfo bi; + if (!out) return; + memset(out, 0, sizeof *out); + if (dist_blob_info(&bi, data, len, DIST_BLOB_CHUNK_SIZE_DEFAULT) != DIST_OK) + return; + memcpy(out->id, bi.id, DIST_BLAKE2B_LEN); + memcpy(out->root, bi.root, DIST_BLAKE2B_LEN); + out->size = bi.size; + out->chunks = bi.chunks; +} + +void cfree_hex_encode(char* out, const uint8_t* in, size_t n) { + dist_hex_encode(out, in, n); +} + +CfreeStatus cfree_hex_decode(uint8_t* out, const char* in, size_t n) { + return dist_hex_decode(out, in, n) == DIST_OK ? CFREE_OK : CFREE_MALFORMED; +} + +CfreeStatus cfree_cas_open(const CfreeContext* ctx, const CfreeCasHost* host, + const char* root_path, CfreeCas** out) { + CfreeCas* cas; + if (!ctx || !ctx->heap || !host || !host->file_io || !root_path || !out) + return CFREE_INVALID; + *out = NULL; + cas = (CfreeCas*)ctx->heap->alloc(ctx->heap, sizeof *cas, _Alignof(CfreeCas)); + if (!cas) return CFREE_NOMEM; + cas->ctx_storage = *ctx; + cas->ctx = &cas->ctx_storage; + cas->host = *host; + cas->dist.host.file_io = host->file_io; + cas->dist.host.mkdir_p = host->mkdir_p; + cas->dist.host.mark_executable = host->mark_executable; + cas->dist.host.user = host->user; + cas->dist.root = root_path; + *out = cas; + return CFREE_OK; +} + +void cfree_cas_close(CfreeCas* cas) { + if (!cas) return; + cas->ctx->heap->free(cas->ctx->heap, cas, sizeof *cas); +} + +CfreeStatus cfree_cas_add_blob(CfreeCas* cas, const uint8_t* data, size_t len, + CfreeBlobInfo* out) { + DistBlobInfo bi; + if (!cas || !out) return CFREE_INVALID; + if (dist_blob_info(&bi, data, len, DIST_BLOB_CHUNK_SIZE_DEFAULT) != DIST_OK) { + cas_diagf(cas->ctx, "failed to hash blob"); + return CFREE_ERR; + } + if (dist_cas_put_blob(&cas->dist, bi.id, data, len) != DIST_OK) { + cas_diagf(cas->ctx, "failed to store blob"); + return CFREE_IO; + } + memcpy(out->id, bi.id, DIST_BLAKE2B_LEN); + memcpy(out->root, bi.root, DIST_BLAKE2B_LEN); + out->size = bi.size; + out->chunks = bi.chunks; + return CFREE_OK; +} + +CfreeStatus cfree_cas_tree_builder_new(CfreeCas* cas, + CfreeCasTreeBuilder** out) { + CfreeCasTreeBuilder* b; + CfreeHeap* h; + if (!cas || !out) return CFREE_INVALID; + *out = NULL; + h = cas->ctx->heap; + b = (CfreeCasTreeBuilder*)h->alloc(h, sizeof *b, _Alignof(CfreeCasTreeBuilder)); + if (!b) return CFREE_NOMEM; + b->cas = cas; + b->entries = (DistTreeEntry*)h->alloc(h, DIST_MAX_FILES * sizeof *b->entries, + _Alignof(DistTreeEntry)); + if (!b->entries) { + h->free(h, b, sizeof *b); + return CFREE_NOMEM; + } + b->tree.entries = b->entries; + b->tree.n_entries = 0; + b->tree.cap_entries = DIST_MAX_FILES; + *out = b; + return CFREE_OK; +} + +CfreeStatus cfree_cas_tree_builder_add(CfreeCasTreeBuilder* b, + const char* tree_path, CfreeTreeMode mode, + const uint8_t* data, size_t len) { + CfreeCas* cas; + DistBlobInfo bi; + DistTreeEntry* e; + if (!b || !tree_path) return CFREE_INVALID; + cas = b->cas; + if (b->tree.n_entries >= b->tree.cap_entries) { + cas_diagf(cas->ctx, "too many tree entries"); + return CFREE_ERR; + } + if (!dist_tree_path_valid(tree_path)) { + cas_diagf(cas->ctx, "unsafe tree path: %s", tree_path); + return CFREE_INVALID; + } + if (!dist_tree_mode_name((uint8_t)mode)) { + cas_diagf(cas->ctx, "bad tree mode for: %s", tree_path); + return CFREE_INVALID; + } + if (dist_blob_info(&bi, data, len, DIST_BLOB_CHUNK_SIZE_DEFAULT) != DIST_OK) { + cas_diagf(cas->ctx, "failed to hash blob: %s", tree_path); + return CFREE_ERR; + } + if (dist_cas_put_blob(&cas->dist, bi.id, data, len) != DIST_OK) { + cas_diagf(cas->ctx, "failed to store blob: %s", tree_path); + return CFREE_IO; + } + e = &b->tree.entries[b->tree.n_entries++]; + memset(e, 0, sizeof *e); + snprintf(e->path, sizeof e->path, "%s", tree_path); + e->mode = (uint8_t)mode; + e->size = bi.size; + memcpy(e->blob, bi.id, DIST_BLAKE2B_LEN); + memcpy(e->root, bi.root, DIST_BLAKE2B_LEN); + return CFREE_OK; +} + +CfreeStatus cfree_cas_tree_builder_finish(CfreeCasTreeBuilder* b, + uint8_t out_tree_id[CFREE_CAS_HASH_LEN]) { + CfreeCas* cas; + CfreeWriter* w = NULL; + const uint8_t* bytes; + size_t len; + char err[128]; + if (!b || !out_tree_id) return CFREE_INVALID; + cas = b->cas; + if (dist_tree_sort_validate(&b->tree, err, sizeof err) != DIST_OK) { + cas_diagf(cas->ctx, "%s", err); + return CFREE_MALFORMED; + } + if (cfree_writer_mem(cas->ctx->heap, &w) != CFREE_OK) { + cas_diagf(cas->ctx, "failed to allocate tree writer"); + return CFREE_NOMEM; + } + if (dist_tree_emit(&b->tree, w) != DIST_OK || + cfree_writer_status(w) != CFREE_OK) { + cfree_writer_close(w); + cas_diagf(cas->ctx, "failed to emit tree manifest"); + return CFREE_ERR; + } + bytes = cfree_writer_mem_bytes(w, &len); + dist_tree_id(out_tree_id, bytes, len); + if (dist_cas_put_tree(&cas->dist, out_tree_id, bytes, len) != DIST_OK) { + cfree_writer_close(w); + cas_diagf(cas->ctx, "failed to store tree manifest"); + return CFREE_IO; + } + cfree_writer_close(w); + return CFREE_OK; +} + +void cfree_cas_tree_builder_free(CfreeCasTreeBuilder* b) { + CfreeHeap* h; + if (!b) return; + h = b->cas->ctx->heap; + h->free(h, b->entries, DIST_MAX_FILES * sizeof *b->entries); + h->free(h, b, sizeof *b); +} + +typedef struct CasDirWalk { + CfreeCasTreeBuilder* b; + CfreeStatus status; +} CasDirWalk; + +static int cas_dir_walk_file(void* user, const char* source_path, + const char* tree_path, int executable) { + CasDirWalk* w = (CasDirWalk*)user; + CfreeCas* cas = w->b->cas; + const CfreeFileIO* io = cas->host.file_io; + CfreeFileData fd; + CfreeStatus st; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (io->read_all(io->user, source_path, &fd) != CFREE_OK) { + cas_diagf(cas->ctx, "failed to read: %s", source_path); + w->status = CFREE_IO; + return 1; + } + st = cfree_cas_tree_builder_add( + w->b, tree_path, + executable ? CFREE_TREE_MODE_EXEC : CFREE_TREE_MODE_FILE, fd.data, + fd.size); + if (io->release) io->release(io->user, &fd); + if (st != CFREE_OK) { + w->status = st; + return 1; + } + return 0; +} + +CfreeStatus cfree_cas_add_tree_from_dir(CfreeCas* cas, const char* root, + uint8_t out_tree_id[CFREE_CAS_HASH_LEN]) { + CfreeCasTreeBuilder* b; + CasDirWalk w; + CfreeStatus st; + if (!cas || !root || !out_tree_id) return CFREE_INVALID; + if (!cas->host.walk_regular_files) return CFREE_UNSUPPORTED; + st = cfree_cas_tree_builder_new(cas, &b); + if (st != CFREE_OK) return st; + w.b = b; + w.status = CFREE_OK; + if (cas->host.walk_regular_files(cas->host.user, root, cas_dir_walk_file, + &w) != 0) { + if (w.status == CFREE_OK) { + cas_diagf(cas->ctx, "failed to walk directory: %s", root); + w.status = CFREE_IO; + } + cfree_cas_tree_builder_free(b); + return w.status; + } + st = cfree_cas_tree_builder_finish(b, out_tree_id); + cfree_cas_tree_builder_free(b); + return st; +} + +/* Load and parse a stored tree into a heap-allocated entries buffer. On + * success, *raw holds the borrowed manifest bytes (release via file_io) and + * *entries the allocation to free. */ +static CfreeStatus cas_load_tree(CfreeCas* cas, + const uint8_t tree_id[CFREE_CAS_HASH_LEN], + DistTree* tree, DistTreeEntry** entries, + CfreeFileData* raw) { + CfreeHeap* h = cas->ctx->heap; + char err[128]; + *entries = (DistTreeEntry*)h->alloc(h, DIST_MAX_FILES * sizeof **entries, + _Alignof(DistTreeEntry)); + if (!*entries) return CFREE_NOMEM; + tree->entries = *entries; + tree->n_entries = 0; + tree->cap_entries = DIST_MAX_FILES; + raw->data = NULL; + raw->size = 0; + raw->token = NULL; + if (dist_cas_get_tree(&cas->dist, tree_id, raw) != DIST_OK) { + cas_diagf(cas->ctx, "failed to load tree"); + h->free(h, *entries, DIST_MAX_FILES * sizeof **entries); + *entries = NULL; + return CFREE_NOT_FOUND; + } + if (dist_tree_parse(raw->data, raw->size, tree, err, sizeof err) != DIST_OK) { + cas_diagf(cas->ctx, "%s", err); + if (cas->host.file_io->release) + cas->host.file_io->release(cas->host.file_io->user, raw); + h->free(h, *entries, DIST_MAX_FILES * sizeof **entries); + *entries = NULL; + return CFREE_MALFORMED; + } + return CFREE_OK; +} + +static void cas_free_tree(CfreeCas* cas, DistTreeEntry* entries, + CfreeFileData* raw) { + CfreeHeap* h = cas->ctx->heap; + if (cas->host.file_io->release) + cas->host.file_io->release(cas->host.file_io->user, raw); + if (entries) h->free(h, entries, DIST_MAX_FILES * sizeof *entries); +} + +CfreeStatus cfree_cas_inspect_tree(CfreeCas* cas, + const uint8_t tree_id[CFREE_CAS_HASH_LEN], + CfreeWriter* out) { + CfreeFileData raw; + if (!cas || !tree_id || !out) return CFREE_INVALID; + raw.data = NULL; + raw.size = 0; + raw.token = NULL; + if (dist_cas_get_tree(&cas->dist, tree_id, &raw) != DIST_OK) { + cas_diagf(cas->ctx, "failed to load tree"); + return CFREE_NOT_FOUND; + } + if (raw.size && cfree_writer_write(out, raw.data, raw.size) != CFREE_OK) { + if (cas->host.file_io->release) + cas->host.file_io->release(cas->host.file_io->user, &raw); + cas_diagf(cas->ctx, "failed to write tree manifest"); + return CFREE_IO; + } + if (cas->host.file_io->release) + cas->host.file_io->release(cas->host.file_io->user, &raw); + return cfree_writer_status(out) == CFREE_OK ? CFREE_OK : CFREE_IO; +} + +CfreeStatus cfree_cas_verify_tree(CfreeCas* cas, + const uint8_t tree_id[CFREE_CAS_HASH_LEN]) { + DistTree tree; + DistTreeEntry* entries; + CfreeFileData raw; + CfreeStatus st; + size_t i; + if (!cas || !tree_id) return CFREE_INVALID; + st = cas_load_tree(cas, tree_id, &tree, &entries, &raw); + if (st != CFREE_OK) return st; + st = CFREE_OK; + for (i = 0; i < tree.n_entries; ++i) { + const DistTreeEntry* e = &tree.entries[i]; + CfreeFileData fd; + DistBlobInfo bi; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (dist_cas_get_blob(&cas->dist, e->blob, &fd) != DIST_OK) { + cas_diagf(cas->ctx, "missing or corrupt blob for: %s", e->path); + st = CFREE_NOT_FOUND; + break; + } + if (dist_blob_info(&bi, fd.data, fd.size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != + DIST_OK || + bi.size != e->size || + memcmp(bi.root, e->root, DIST_BLAKE2B_LEN) != 0) { + if (cas->host.file_io->release) + cas->host.file_io->release(cas->host.file_io->user, &fd); + cas_diagf(cas->ctx, "blob root mismatch for: %s", e->path); + st = CFREE_INVALID; + break; + } + if (cas->host.file_io->release) + cas->host.file_io->release(cas->host.file_io->user, &fd); + } + cas_free_tree(cas, entries, &raw); + return st; +} + +CfreeStatus cfree_cas_materialize_tree(CfreeCas* cas, + const uint8_t tree_id[CFREE_CAS_HASH_LEN], + const char* dst) { + DistTree tree; + DistTreeEntry* entries; + CfreeFileData raw; + CfreeStatus st; + if (!cas || !tree_id || !dst) return CFREE_INVALID; + st = cas_load_tree(cas, tree_id, &tree, &entries, &raw); + if (st != CFREE_OK) return st; + if (dist_cas_materialize_tree(&cas->dist, &tree, dst) != DIST_OK) { + cas_diagf(cas->ctx, "failed to materialize tree"); + st = CFREE_ERR; + } + cas_free_tree(cas, entries, &raw); + return st; +} diff --git a/src/api/package.c b/src/api/package.c @@ -0,0 +1,1777 @@ +/* Public signed-package API: the create/verify/unpack/inspect pipelines for + * portable .tar.gz and native .cfpkg packages, composed over the internal + * dist model (src/dist/). See <cfree/package.h> and doc/DISTRIBUTE.md. + * + * The pipelines were lifted from the cfree pkg tool; the driver keeps only + * argument parsing, stdout formatting, host-vtable wiring, and trusted-keys + * path/pin policy. Operational errors emit through ctx->diag (pkg_diagf); + * arg/CLI errors stay in the driver. */ + +#include <cfree/cas.h> +#include <cfree/package.h> + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "dist/blake2b.h" +#include "dist/blob.h" +#include "dist/cas.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/tar.h" +#include "dist/tree.h" +#include "dist/trust.h" + +#define PKG_PATH_BUF 1024u +#define PKG_META_MANIFEST "cfree/package.manifest" +#define PKG_META_SIG "cfree/package.manifest.minisig" +#define PKG_META_PUB "cfree/package.pub" +#define PKG_DEFAULT_OUTPUT_ID 0u +#define PKG_MAX_TAR_ENTRIES (DIST_MAX_FILES + DIST_MAX_OUTPUTS + 8u) + +_Static_assert(CFREE_PKG_KEYID_LEN == DIST_KEYID_LEN, "keyid len"); +_Static_assert(CFREE_PKG_PK_LEN == DIST_ED25519_PK_LEN, "pk len"); +_Static_assert(CFREE_PKG_SK_LEN == DIST_ED25519_SK_LEN, "sk len"); +_Static_assert(CFREE_PKG_NAME_MAX == DIST_NAME_MAX, "name max"); +_Static_assert(CFREE_PKG_VERSION_MAX == DIST_VERSION_MAX, "version max"); +_Static_assert(CFREE_PKG_TRUSTED_COMMENT_MAX == DIST_TRUSTED_COMMENT_MAX, + "trusted comment max"); +_Static_assert(CFREE_CAS_HASH_LEN == DIST_BLAKE2B_LEN, "hash len"); +_Static_assert((int)CFREE_PKG_COMPRESSION_NONE == DIST_CFPKG_COMP_NONE, + "comp none"); +_Static_assert((int)CFREE_PKG_COMPRESSION_LZ4_BLOCK_V1 == + DIST_CFPKG_COMP_LZ4_BLOCK_V1, + "comp lz4"); + +typedef enum PkgNativeShape { + PKG_NATIVE_FAT, + PKG_NATIVE_METADATA, + PKG_NATIVE_THIN +} PkgNativeShape; + +typedef struct PkgBlob { + CfreeFileData fd; + int loaded; + uint8_t id[DIST_BLAKE2B_LEN]; + uint8_t root[DIST_BLAKE2B_LEN]; + uint64_t size; +} PkgBlob; + +typedef struct PkgSource { + const CfreeContext* ctx; + const CfreeCasHost* host; + DistTree tree; + DistTreeEntry entries[DIST_MAX_FILES]; + PkgBlob blobs[DIST_MAX_FILES]; + size_t n_blobs; + uint8_t tree_id[DIST_BLAKE2B_LEN]; + const uint8_t* tree_bytes; + size_t tree_size; + CfreeWriter* tree_mem; + CfreeFileData tree_fd; + int tree_loaded; +} PkgSource; + +typedef struct PkgVerified { + DistPackageManifest 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]; + int tofu_pin; +} PkgVerified; + +typedef struct PkgLoadedTree { + DistTree tree; + DistTreeEntry entries[DIST_MAX_FILES]; + uint8_t id[DIST_BLAKE2B_LEN]; + const uint8_t* bytes; + size_t size; +} PkgLoadedTree; + +static void pkg_diagf(const CfreeContext* ctx, const char* fmt, ...) { + va_list ap; + CfreeSrcLoc loc; + if (!ctx || !ctx->diag || !ctx->diag->emit) return; + loc.file_id = 0; + loc.line = 0; + loc.col = 0; + va_start(ap, fmt); + ctx->diag->emit(ctx->diag, CFREE_DIAG_ERROR, loc, fmt, ap); + va_end(ap); +} + +/* ---------------------------------------------------------------------- */ +/* shared helpers */ +/* ---------------------------------------------------------------------- */ + +static int pkg_write_file(const CfreeContext* ctx, const char* path, + const uint8_t* data, size_t len) { + CfreeWriter* w = NULL; + int rc; + if (ctx->file_io->open_writer(ctx->file_io->user, path, &w) != CFREE_OK) { + pkg_diagf(ctx, "failed to open output: %s", path); + return DIST_ERR; + } + rc = (len == 0 || cfree_writer_write(w, data, len) == CFREE_OK) ? DIST_OK + : DIST_ERR; + if (cfree_writer_status(w) != CFREE_OK) rc = DIST_ERR; + cfree_writer_close(w); + if (rc != DIST_OK) pkg_diagf(ctx, "failed to write: %s", path); + return rc; +} + +static CfreeWriter* pkg_mem(const CfreeContext* ctx) { + CfreeWriter* w = NULL; + if (cfree_writer_mem(ctx->heap, &w) != CFREE_OK) return NULL; + return w; +} + +static void pkg_parent_dir(const char* path, char* buf, size_t cap) { + const char* slash = NULL; + const char* p; + size_t n; + for (p = path; *p; ++p) + if (*p == '/') slash = p; + if (!slash) { + buf[0] = '\0'; + return; + } + n = (size_t)(slash - path); + if (n >= cap) n = cap - 1u; + memcpy(buf, path, n); + buf[n] = '\0'; +} + +static int pkg_join_path(char* out, size_t cap, const char* dir, + const char* rel) { + size_t dl, rl; + int slash; + if (!out || !cap || !dir || !rel) return DIST_ERR; + dl = strlen(dir); + rl = strlen(rel); + slash = dl > 0 && dir[dl - 1u] != '/'; + if (dl + (slash ? 1u : 0u) + rl + 1u > cap) return DIST_ERR; + memcpy(out, dir, dl); + if (slash) out[dl++] = '/'; + memcpy(out + dl, rel, rl); + out[dl + rl] = '\0'; + return DIST_OK; +} + +static int pkg_read_file(const CfreeContext* ctx, const char* path, + CfreeFileData* out) { + return ctx->file_io->read_all(ctx->file_io->user, path, out) == CFREE_OK + ? DIST_OK + : DIST_ERR; +} + +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 (strcmp(e[i].name, name) == 0) return &e[i]; + return NULL; +} + +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 cfree_writer_tell(w) == target ? DIST_OK : DIST_ERR; +} + +static void pkg_hash(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data, + size_t len) { + dist_blake2b(out, data, len); +} + +static int pkg_parse_id(const char* s, uint8_t out[DIST_BLAKE2B_LEN]) { + if (!s || strlen(s) != 2u * DIST_BLAKE2B_LEN) return DIST_ERR; + return dist_hex_decode(out, s, DIST_BLAKE2B_LEN); +} + +static int pkg_cas_rel_path(char* out, size_t cap, const char* kind, + const uint8_t id[DIST_BLAKE2B_LEN]) { + char hex[2 * DIST_BLAKE2B_LEN + 1]; + int n; + dist_hex_encode(hex, id, DIST_BLAKE2B_LEN); + n = snprintf(out, cap, "cfree/cas/%s/%c%c/%s", kind, hex[0], hex[1], hex); + return n > 0 && (size_t)n < cap ? DIST_OK : DIST_ERR; +} + +static int pkg_external_id_path(char* out, size_t cap, const char* kind, + const uint8_t id[DIST_BLAKE2B_LEN]) { + if (strcmp(kind, "tree") == 0) return dist_cas_tree_relpath(out, cap, id); + if (strcmp(kind, "index") == 0) return dist_cas_index_relpath(out, cap, id); + if (strcmp(kind, "blob") == 0) return dist_cas_blob_relpath(out, cap, id); + return DIST_ERR; +} + +static int pkg_external_chunk_path(char* out, size_t cap, + const uint8_t blob[DIST_BLAKE2B_LEN], + uint64_t chunk_index) { + return dist_cas_chunk_relpath(out, cap, blob, chunk_index); +} + +static int pkg_locator_safe(const char* path) { + size_t start = 0, i; + if (!path || !path[0] || path[0] == '/') return 0; + for (i = 0;; ++i) { + char c = path[i]; + if (c == '\\' || c == ':' || c == '\n' || c == '\r') return 0; + if (c == '/' || c == '\0') { + size_t n = i - start; + if (n == 0) return 0; + if (n == 1 && path[start] == '.') return 0; + if (n == 2 && path[start] == '.' && path[start + 1] == '.') return 0; + if (c == '\0') return 1; + start = i + 1u; + } + } +} + +static int pkg_external_path(char* out, size_t cap, const char* root, + const char* rel) { + if (!pkg_locator_safe(rel)) return DIST_ERR; + return pkg_join_path(out, cap, root, rel); +} + +static int pkg_write_external_file(const CfreeCasHost* host, + const CfreeContext* ctx, const char* root, + const char* rel, const uint8_t* data, + size_t len) { + char full[PKG_PATH_BUF], parent[PKG_PATH_BUF]; + if (!root || pkg_external_path(full, sizeof full, root, rel) != DIST_OK) + return DIST_ERR; + pkg_parent_dir(full, parent, sizeof parent); + if (parent[0] && host->mkdir_p(host->user, parent) != 0) return DIST_ERR; + return pkg_write_file(ctx, full, data, len); +} + +static int pkg_read_external_file(const CfreeContext* ctx, const char* root, + const char* rel, CfreeFileData* out) { + char full[PKG_PATH_BUF]; + if (!root || pkg_external_path(full, sizeof full, root, rel) != DIST_OK) + return DIST_ERR; + return pkg_read_file(ctx, full, out); +} + +static int pkg_blob_cmp(const void* ap, const void* bp) { + const PkgBlob* a = (const PkgBlob*)ap; + const PkgBlob* b = (const PkgBlob*)bp; + return memcmp(a->id, b->id, DIST_BLAKE2B_LEN); +} + +/* ---------------------------------------------------------------------- */ +/* source assembly (root walk / cas load) */ +/* ---------------------------------------------------------------------- */ + +static PkgBlob* pkg_source_find_blob(PkgSource* src, + const uint8_t id[DIST_BLAKE2B_LEN]) { + size_t i; + for (i = 0; i < src->n_blobs; ++i) + if (memcmp(src->blobs[i].id, id, DIST_BLAKE2B_LEN) == 0) + return &src->blobs[i]; + return NULL; +} + +static void pkg_source_init(PkgSource* src, const CfreeContext* ctx, + const CfreeCasHost* host) { + memset(src, 0, sizeof *src); + src->ctx = ctx; + src->host = host; + src->tree.entries = src->entries; + src->tree.cap_entries = DIST_MAX_FILES; +} + +static void pkg_source_release(PkgSource* src) { + const CfreeFileIO* io; + size_t i; + if (!src || !src->host) return; + io = src->host->file_io; + for (i = 0; i < src->n_blobs; ++i) { + if (src->blobs[i].loaded && io->release) + io->release(io->user, &src->blobs[i].fd); + } + if (src->tree_loaded && io->release) io->release(io->user, &src->tree_fd); + if (src->tree_mem) cfree_writer_close(src->tree_mem); + memset(src, 0, sizeof *src); +} + +static int pkg_source_store_blob(PkgSource* src, CfreeFileData* fd, + const DistBlobInfo* bi, int take_fd) { + PkgBlob* existing = pkg_source_find_blob(src, bi->id); + if (existing) { + if (existing->size != bi->size || + memcmp(existing->root, bi->root, DIST_BLAKE2B_LEN) != 0) + return DIST_ERR; + return DIST_OK; + } + if (src->n_blobs >= DIST_MAX_FILES) return DIST_ERR; + existing = &src->blobs[src->n_blobs++]; + memset(existing, 0, sizeof *existing); + memcpy(existing->id, bi->id, DIST_BLAKE2B_LEN); + memcpy(existing->root, bi->root, DIST_BLAKE2B_LEN); + existing->size = bi->size; + if (take_fd) { + existing->fd = *fd; + existing->loaded = 1; + fd->data = NULL; + fd->size = 0; + fd->token = NULL; + } + return DIST_OK; +} + +static int pkg_source_add_entry(PkgSource* src, const char* tree_path, + uint8_t mode, CfreeFileData* fd, int take_fd) { + DistBlobInfo bi; + DistTreeEntry* e; + if (src->tree.n_entries >= src->tree.cap_entries) { + pkg_diagf(src->ctx, "create: too many tree entries"); + return DIST_ERR; + } + if (!dist_tree_path_valid(tree_path)) { + pkg_diagf(src->ctx, "create: unsafe tree path: %s", tree_path); + return DIST_ERR; + } + if (!dist_tree_mode_name(mode)) { + pkg_diagf(src->ctx, "create: bad tree mode: %s", tree_path); + return DIST_ERR; + } + if (dist_blob_info(&bi, fd->data, fd->size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != + DIST_OK) { + pkg_diagf(src->ctx, "create: failed to hash blob: %s", tree_path); + return DIST_ERR; + } + if (pkg_source_store_blob(src, fd, &bi, take_fd) != DIST_OK) { + pkg_diagf(src->ctx, "create: failed to store blob metadata: %s", tree_path); + return DIST_ERR; + } + e = &src->tree.entries[src->tree.n_entries++]; + memset(e, 0, sizeof *e); + snprintf(e->path, sizeof e->path, "%s", tree_path); + e->mode = mode; + e->size = bi.size; + memcpy(e->blob, bi.id, DIST_BLAKE2B_LEN); + memcpy(e->root, bi.root, DIST_BLAKE2B_LEN); + return DIST_OK; +} + +static int pkg_source_walk_file(void* user, const char* source_path, + const char* tree_path, int executable) { + PkgSource* src = (PkgSource*)user; + const CfreeFileIO* io = src->host->file_io; + CfreeFileData fd; + int rc; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (io->read_all(io->user, source_path, &fd) != CFREE_OK) { + pkg_diagf(src->ctx, "create: cannot read file: %s", source_path); + return 1; + } + rc = pkg_source_add_entry( + src, tree_path, + executable ? DIST_TREE_MODE_EXEC : DIST_TREE_MODE_FILE, &fd, 1); + if (fd.data && io->release) io->release(io->user, &fd); + return rc == DIST_OK ? 0 : 1; +} + +static int pkg_source_finish_tree(PkgSource* src) { + char err[128]; + if (dist_tree_sort_validate(&src->tree, err, sizeof err) != DIST_OK) { + pkg_diagf(src->ctx, "create: %s", err); + return DIST_ERR; + } + if (cfree_writer_mem(src->ctx->heap, &src->tree_mem) != CFREE_OK) + return DIST_ERR; + if (dist_tree_emit(&src->tree, src->tree_mem) != DIST_OK || + cfree_writer_status(src->tree_mem) != CFREE_OK) { + pkg_diagf(src->ctx, "create: failed to emit tree manifest"); + return DIST_ERR; + } + src->tree_bytes = cfree_writer_mem_bytes(src->tree_mem, &src->tree_size); + dist_tree_id(src->tree_id, src->tree_bytes, src->tree_size); + qsort(src->blobs, src->n_blobs, sizeof src->blobs[0], pkg_blob_cmp); + return DIST_OK; +} + +static int pkg_source_from_root(PkgSource* src, const char* root) { + if (src->host->walk_regular_files(src->host->user, root, pkg_source_walk_file, + src) != 0) { + pkg_diagf(src->ctx, "create: failed to walk directory: %s", root); + return DIST_ERR; + } + return pkg_source_finish_tree(src); +} + +static void pkg_cas_init_get(DistCas* cas, const CfreeCasHost* host, + const char* root) { + memset(cas, 0, sizeof *cas); + cas->host.file_io = host->file_io; + cas->host.user = host->user; + cas->root = root; +} + +static int pkg_source_load_blob_from_cas(PkgSource* src, DistCas* cas, + const DistTreeEntry* e) { + const CfreeFileIO* io = src->host->file_io; + PkgBlob* existing = pkg_source_find_blob(src, e->blob); + CfreeFileData fd; + DistBlobInfo bi; + if (existing) { + if (existing->size != e->size || + memcmp(existing->root, e->root, DIST_BLAKE2B_LEN) != 0) { + pkg_diagf(src->ctx, "create: duplicate blob metadata mismatch: %s", + e->path); + return DIST_ERR; + } + return DIST_OK; + } + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (dist_cas_get_blob(cas, e->blob, &fd) != DIST_OK) { + pkg_diagf(src->ctx, "create: missing or corrupt blob for: %s", e->path); + return DIST_ERR; + } + if (dist_blob_info(&bi, fd.data, fd.size, DIST_BLOB_CHUNK_SIZE_DEFAULT) != + DIST_OK || + bi.size != e->size || memcmp(bi.root, e->root, DIST_BLAKE2B_LEN) != 0) { + if (io->release) io->release(io->user, &fd); + pkg_diagf(src->ctx, "create: blob root mismatch for: %s", e->path); + return DIST_ERR; + } + if (pkg_source_store_blob(src, &fd, &bi, 1) != DIST_OK) { + if (fd.data && io->release) io->release(io->user, &fd); + return DIST_ERR; + } + return DIST_OK; +} + +static int pkg_source_from_cas(PkgSource* src, const char* cas_dir, + const char* tree_s) { + DistCas cas; + char err[128]; + size_t i; + if (pkg_parse_id(tree_s, src->tree_id) != DIST_OK) { + pkg_diagf(src->ctx, "create: bad tree id: %s", tree_s); + return DIST_ERR; + } + pkg_cas_init_get(&cas, src->host, cas_dir); + src->tree_fd.data = NULL; + src->tree_fd.size = 0; + src->tree_fd.token = NULL; + if (dist_cas_get_tree(&cas, src->tree_id, &src->tree_fd) != DIST_OK) { + pkg_diagf(src->ctx, "create: missing or corrupt tree: %s", tree_s); + return DIST_ERR; + } + src->tree_loaded = 1; + src->tree_bytes = src->tree_fd.data; + src->tree_size = src->tree_fd.size; + if (dist_tree_parse(src->tree_bytes, src->tree_size, &src->tree, err, + sizeof err) != DIST_OK) { + pkg_diagf(src->ctx, "create: tree: %s", err); + return DIST_ERR; + } + for (i = 0; i < src->tree.n_entries; ++i) { + if (pkg_source_load_blob_from_cas(src, &cas, &src->tree.entries[i]) != + DIST_OK) + return DIST_ERR; + } + qsort(src->blobs, src->n_blobs, sizeof src->blobs[0], pkg_blob_cmp); + return DIST_OK; +} + +static int pkg_manifest_from_source(const char* name, const char* version, + const char* desc, const PkgSource* src, + DistPackageManifest* m) { + size_t i; + memset(m, 0, sizeof *m); + snprintf(m->name, sizeof m->name, "%s", name); + snprintf(m->version, sizeof m->version, "%s", version); + if (desc) snprintf(m->description, sizeof m->description, "%s", desc); + m->n_outputs = 1; + m->outputs[0].id = PKG_DEFAULT_OUTPUT_ID; + snprintf(m->outputs[0].name, sizeof m->outputs[0].name, "%s", "default"); + memcpy(m->outputs[0].tree, src->tree_id, DIST_BLAKE2B_LEN); + m->outputs[0].is_default = 1; + for (i = 0; i < src->tree.n_entries; ++i) { + DistPackageArtifact* a; + if (m->n_artifacts >= DIST_MAX_ARTIFACTS) return DIST_ERR; + a = &m->artifacts[m->n_artifacts++]; + a->output_id = PKG_DEFAULT_OUTPUT_ID; + snprintf(a->path, sizeof a->path, "%s", src->tree.entries[i].path); + snprintf(a->kind, sizeof a->kind, "%s", "data"); + a->entry = 1; + } + return dist_package_manifest_validate(m, NULL, 0); +} + +static int pkg_sign(CfreeWriter* out, const CfreeContext* ctx, + 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)(ctx->now > 0 ? ctx->now : 0), pkgid_hex); + return dist_minisig_sign(out, data, len, kp->sk, kp->keyid, what, tcomment); +} + +/* ---------------------------------------------------------------------- */ +/* create */ +/* ---------------------------------------------------------------------- */ + +static int pkg_create_targz(const CfreeContext* ctx, const char* out, + const PkgSource* src, 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; + char path[PKG_PATH_BUF]; + 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; + if (pkg_cas_rel_path(path, sizeof path, "tree", src->tree_id) != DIST_OK || + dist_tar_append(tar, path, src->tree_bytes, src->tree_size) != DIST_OK) + goto done; + for (i = 0; i < src->n_blobs; ++i) { + if (pkg_cas_rel_path(path, sizeof path, "blob", src->blobs[i].id) != + DIST_OK || + dist_tar_append(tar, path, src->blobs[i].fd.data, + (size_t)src->blobs[i].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 CfreeCasHost* host, + const CfreeContext* ctx, + const PkgSource* src, uint32_t compression, + const char* external_dir, int embed_content, + CfreeWriter** index_out, + CfreeWriter** content_out) { + CfreeWriter* index = pkg_mem(ctx); + CfreeWriter* content = pkg_mem(ctx); + size_t bi; + if (!index || !content) return DIST_ERR; + for (bi = 0; bi < src->n_blobs; ++bi) { + const PkgBlob* blob = &src->blobs[bi]; + size_t off = 0, ci = 0; + if (blob->size == 0) continue; + while (off < blob->size) { + uint8_t recbuf[DIST_CFPKG3_INDEX_RECORD_SIZE]; + DistCfpkg3IndexRecord r; + const uint8_t* raw = blob->fd.data + off; + size_t raw_len = (size_t)blob->size - off; + if (raw_len > DIST_CFPKG3_CHUNK_SIZE_DEFAULT) + raw_len = DIST_CFPKG3_CHUNK_SIZE_DEFAULT; + memset(&r, 0, sizeof r); + memcpy(r.blob_id, blob->id, DIST_BLAKE2B_LEN); + r.chunk_index = (uint64_t)ci; + r.content_offset = embed_content ? cfree_writer_tell(content) : 0; + r.raw_size = raw_len; + r.compression = compression; + pkg_hash(r.raw_hash, raw, raw_len); + dist_blob_leaf_hash(r.leaf_hash, 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 (embed_content && + cfree_writer_write(content, raw, raw_len) != CFREE_OK) + return DIST_ERR; + if (!embed_content) { + char rel[PKG_PATH_BUF]; + if (pkg_external_chunk_path(rel, sizeof rel, blob->id, + r.chunk_index) != DIST_OK || + pkg_write_external_file(host, ctx, external_dir, rel, raw, + raw_len) != DIST_OK) + return DIST_ERR; + } + } else { + uint8_t tmp[DIST_CFPKG3_CHUNK_SIZE_DEFAULT + 1024u]; + size_t stored_len = 0; + if (dist_lz4_compress_block(tmp, sizeof tmp, &stored_len, raw, + raw_len) != DIST_OK) { + pkg_diagf(ctx, "create: lz4-block-v1 compression failed"); + return DIST_ERR; + } + r.stored_size = stored_len; + pkg_hash(r.stored_hash, tmp, stored_len); + if (embed_content && + cfree_writer_write(content, tmp, stored_len) != CFREE_OK) + return DIST_ERR; + if (!embed_content) { + char rel[PKG_PATH_BUF]; + if (pkg_external_chunk_path(rel, sizeof rel, blob->id, + r.chunk_index) != DIST_OK || + pkg_write_external_file(host, ctx, external_dir, rel, tmp, + stored_len) != DIST_OK) + return DIST_ERR; + } + } + dist_cfpkg3_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(const CfreeCasHost* host, const CfreeContext* ctx, + const char* out, const DistKeypair* kp, + const PkgSource* src, 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, PkgNativeShape shape, + const char* external_dir) { + 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 tree_root[DIST_BLAKE2B_LEN], index_root[DIST_BLAKE2B_LEN], + content_root[DIST_BLAKE2B_LEN]; + DistCfpkg3Header h; + uint64_t tree_offset = 0, index_offset = 0, content_offset = 0; + int embed_tree = shape != PKG_NATIVE_THIN; + int embed_index = shape != PKG_NATIVE_THIN; + int embed_content = shape == PKG_NATIVE_FAT; + int stable = 0, iter, rc = DIST_ERR; + char tree_url[PKG_PATH_BUF], index_url[PKG_PATH_BUF]; + + tree_url[0] = '\0'; + index_url[0] = '\0'; + if (shape != PKG_NATIVE_FAT && !external_dir) { + pkg_diagf(ctx, + "create: --external DIR is required for non-fat native packages"); + goto done; + } + if (pkg_external_id_path(tree_url, sizeof tree_url, "tree", src->tree_id) != + DIST_OK) + goto done; + + if (pkg_build_native_regions(host, ctx, src, compression, external_dir, + embed_content, &index, &content) != DIST_OK) + goto done; + index_b = cfree_writer_mem_bytes(index, &index_l); + content_b = cfree_writer_mem_bytes(content, &content_l); + dist_cfpkg3_region_root(tree_root, "tree", embed_tree ? src->tree_bytes : NULL, + embed_tree ? src->tree_size : 0); + dist_cfpkg3_region_root(index_root, "index", index_b, index_l); + dist_cfpkg3_region_root(content_root, "content", + embed_content ? content_b : NULL, + embed_content ? content_l : 0); + if (!embed_tree && + pkg_write_external_file(host, ctx, external_dir, tree_url, src->tree_bytes, + src->tree_size) != DIST_OK) + goto done; + if (!embed_index) { + if (pkg_external_id_path(index_url, sizeof index_url, "index", index_root) != + DIST_OK || + pkg_write_external_file(host, ctx, external_dir, index_url, index_b, + index_l) != DIST_OK) + goto done; + } + + memset(&h, 0, sizeof h); + for (iter = 0; iter < 8; ++iter) { + DistCfpkg3Descriptor 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_CFPKG3_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; + tree_offset = embed_tree ? pkg_align_up(h.pubkey_offset + h.pubkey_size, + DIST_CFPKG3_ALIGNMENT) + : 0; + index_offset = + embed_index + ? pkg_align_up((embed_tree ? tree_offset + src->tree_size + : h.pubkey_offset + h.pubkey_size), + DIST_CFPKG3_ALIGNMENT) + : 0; + content_offset = + embed_content + ? pkg_align_up( + (embed_index + ? index_offset + index_l + : (embed_tree ? tree_offset + src->tree_size + : h.pubkey_offset + h.pubkey_size)), + DIST_CFPKG3_ALIGNMENT) + : 0; + + memset(&d, 0, sizeof d); + memcpy(d.package_id, pkgid, DIST_BLAKE2B_LEN); + d.chunk_size = DIST_CFPKG3_CHUNK_SIZE_DEFAULT; + d.alignment = DIST_CFPKG3_ALIGNMENT; + d.tree_offset = tree_offset; + d.tree_size = embed_tree ? src->tree_size : 0; + memcpy(d.tree_root, tree_root, DIST_BLAKE2B_LEN); + d.index_offset = index_offset; + d.index_size = embed_index ? index_l : 0; + d.index_bytes = index_l; + memcpy(d.index_root, index_root, DIST_BLAKE2B_LEN); + if (!embed_index) snprintf(d.index_url, sizeof d.index_url, "%s", index_url); + d.content_offset = content_offset; + d.content_size = embed_content ? content_l : 0; + memcpy(d.content_root, content_root, DIST_BLAKE2B_LEN); + d.n_trees = 1; + memcpy(d.trees[0].tree, src->tree_id, DIST_BLAKE2B_LEN); + if (embed_tree) { + d.trees[0].offset = 0; + d.trees[0].size = src->tree_size; + d.trees[0].embedded = 1; + } else { + snprintf(d.trees[0].url, sizeof d.trees[0].url, "%s", tree_url); + } + memcpy(d.trees[0].blake2b, src->tree_id, DIST_BLAKE2B_LEN); + d.n_chunk_sources = 1; + if (embed_content) { + d.chunk_sources[0].kind = DIST_CFPKG3_CHUNK_SOURCE_EMBEDDED; + } else { + d.chunk_sources[0].kind = DIST_CFPKG3_CHUNK_SOURCE_URL_TEMPLATE; + snprintf(d.chunk_sources[0].tmpl, sizeof d.chunk_sources[0].tmpl, "%s", + "chunk/{blob-prefix}/{blob}/{chunk}"); + } + if (dist_cfpkg3_descriptor_emit(descw, &d) != DIST_OK) goto done; + desc_b = cfree_writer_mem_bytes(descw, &desc_l); + if (pkg_sign(descsigw, ctx, 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_cfpkg3_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 || + (embed_tree && + (pkg_write_pad(pkg, tree_offset) != DIST_OK || + cfree_writer_write(pkg, src->tree_bytes, src->tree_size) != CFREE_OK)) || + (embed_index && + (pkg_write_pad(pkg, index_offset) != DIST_OK || + cfree_writer_write(pkg, index_b, index_l) != CFREE_OK)) || + (embed_content && + (pkg_write_pad(pkg, 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; +} + +CfreeStatus cfree_pkg_create(const CfreeContext* ctx, const CfreeCasHost* host, + const CfreePkgCreateOptions* opts, + CfreePkgCreateResult* result) { + DistKeypair kp; + DistPackageManifest m; + PkgSource src; + 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]; + int rc = DIST_ERR; + if (!ctx || !host || !opts || !result || !opts->sk || !opts->keyid || + !opts->out_path) + return CFREE_INVALID; + + memset(&kp, 0, sizeof kp); + memcpy(kp.sk, opts->sk, DIST_ED25519_SK_LEN); + memcpy(kp.keyid, opts->keyid, DIST_KEYID_LEN); + memcpy(kp.pk, kp.sk + DIST_ED25519_SEED_LEN, DIST_ED25519_PK_LEN); + + pkg_source_init(&src, ctx, host); + if (opts->root_dir) { + if (pkg_source_from_root(&src, opts->root_dir) != DIST_OK) goto done; + } else { + if (pkg_source_from_cas(&src, opts->cas_dir, opts->tree_id) != DIST_OK) + goto done; + } + if (pkg_manifest_from_source(opts->name, opts->version, opts->description, + &src, &m) != DIST_OK) { + pkg_diagf(ctx, "create: failed to build package manifest"); + goto done; + } + + manw = pkg_mem(ctx); + sigw = pkg_mem(ctx); + pubw = pkg_mem(ctx); + if (!manw || !sigw || !pubw) goto done; + if (dist_package_manifest_emit(&m, manw) != DIST_OK) goto done; + man_b = cfree_writer_mem_bytes(manw, &man_l); + pkg_hash(pkgid, man_b, man_l); + if (pkg_sign(sigw, ctx, man_b, man_l, &kp, pkgid, "signature from cfree pkg") != + DIST_OK) + goto done; + 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 (opts->format == CFREE_PKG_FORMAT_TARGZ) + rc = pkg_create_targz(ctx, opts->out_path, &src, man_b, man_l, sig_b, sig_l, + pub_b, pub_l); + else + rc = pkg_create_cfpkg(host, ctx, opts->out_path, &kp, &src, man_b, man_l, + sig_b, sig_l, pub_b, pub_l, pkgid, + (uint32_t)opts->compression, + (PkgNativeShape)opts->native_shape, + opts->external_dir); + if (rc == DIST_OK) { + result->n_files = src.tree.n_entries; + memcpy(result->package_id, pkgid, DIST_BLAKE2B_LEN); + } + +done: + if (pubw) cfree_writer_close(pubw); + if (sigw) cfree_writer_close(sigw); + if (manw) cfree_writer_close(manw); + pkg_source_release(&src); + return rc == DIST_OK ? CFREE_OK : CFREE_ERR; +} + +/* ---------------------------------------------------------------------- */ +/* key resolution + manifest verification */ +/* ---------------------------------------------------------------------- */ + +static int pkg_resolve_key(const CfreeContext* ctx, + const uint8_t keyid[DIST_KEYID_LEN], + const uint8_t* bundled_pub, size_t bundled_pub_size, + const CfreePkgVerifyOptions* opts, + uint8_t pk[DIST_ED25519_PK_LEN], int* tofu_pin) { + uint8_t kid_chk[DIST_KEYID_LEN]; + *tofu_pin = 0; + if (opts->pubkey_bytes) { + if (dist_minisig_parse_pubkey(opts->pubkey_bytes, opts->pubkey_len, pk, + kid_chk) != DIST_OK || + memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) { + pkg_diagf(ctx, "public key id does not match signature"); + return DIST_ERR; + } + return DIST_OK; + } + if (opts->trusted_keys && + dist_trust_lookup(opts->trusted_keys, opts->trusted_keys_len, keyid, pk) == + DIST_OK) + return DIST_OK; + if (!opts->tofu) { + char hex[2 * DIST_KEYID_LEN + 1]; + dist_hex_encode(hex, keyid, DIST_KEYID_LEN); + pkg_diagf(ctx, "untrusted signer (key id %s)", hex); + return DIST_ERR; + } + 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) { + pkg_diagf(ctx, "--tofu: bundled public key is missing or mismatched"); + return DIST_ERR; + } + *tofu_pin = 1; + return DIST_OK; +} + +static int pkg_verify_manifest(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 CfreePkgVerifyOptions* opts, + PkgVerified* out) { + char err[128], pkgid_hex[2 * DIST_BLAKE2B_LEN + 1]; + const char* pidp; + memset(out, 0, sizeof *out); + if (dist_minisig_sig_keyid(sig, sig_len, out->keyid) != DIST_OK) { + pkg_diagf(ctx, "malformed signature"); + return DIST_ERR; + } + if (pkg_resolve_key(ctx, out->keyid, pub, pub_len, opts, out->pk, + &out->tofu_pin) != DIST_OK) + return DIST_ERR; + if (dist_minisig_verify(sig, sig_len, man, man_len, out->pk, out->trusted, + sizeof out->trusted) != DIST_OK) { + pkg_diagf(ctx, "signature verification FAILED"); + return DIST_ERR; + } + 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) { + pkg_diagf(ctx, "trusted comment does not match package id"); + return DIST_ERR; + } + if (dist_package_manifest_parse(man, man_len, &out->manifest, err, + sizeof err) != DIST_OK) { + pkg_diagf(ctx, "manifest: %s", err); + return DIST_ERR; + } + return DIST_OK; +} + +static const DistPackageOutput* pkg_default_output(const DistPackageManifest* m) { + size_t i; + for (i = 0; i < m->n_outputs; ++i) + if (m->outputs[i].is_default) return &m->outputs[i]; + return m->n_outputs ? &m->outputs[0] : NULL; +} + +static int pkg_parse_tree_object(const CfreeContext* ctx, PkgLoadedTree* out, + const uint8_t id[DIST_BLAKE2B_LEN], + const uint8_t* data, size_t len, + const char* label) { + uint8_t got[DIST_BLAKE2B_LEN]; + char err[128]; + memset(out, 0, sizeof *out); + dist_tree_id(got, data, len); + if (memcmp(got, id, DIST_BLAKE2B_LEN) != 0) { + pkg_diagf(ctx, "tree id mismatch: %s", label); + return DIST_ERR; + } + out->tree.entries = out->entries; + out->tree.cap_entries = DIST_MAX_FILES; + if (dist_tree_parse(data, len, &out->tree, err, sizeof err) != DIST_OK) { + pkg_diagf(ctx, "tree: %s", err); + return DIST_ERR; + } + memcpy(out->id, id, DIST_BLAKE2B_LEN); + out->bytes = data; + out->size = len; + return DIST_OK; +} + +static int pkg_verify_artifact_overlays(const CfreeContext* ctx, + const DistPackageManifest* m, + const DistPackageOutput* out, + const DistTree* tree) { + size_t i; + for (i = 0; i < m->n_artifacts; ++i) { + const DistPackageArtifact* a = &m->artifacts[i]; + if (a->output_id != out->id) continue; + if (!dist_tree_find(tree, a->path)) { + pkg_diagf(ctx, "artifact path not in output tree: %s", a->path); + return DIST_ERR; + } + } + return DIST_OK; +} + +static int pkg_write_output_file(const CfreeCasHost* host, + const CfreeContext* ctx, const char* out_dir, + const DistTreeEntry* e, const uint8_t* data, + size_t len) { + char full[PKG_PATH_BUF], parent[PKG_PATH_BUF]; + if (pkg_join_path(full, sizeof full, out_dir, e->path) != DIST_OK) { + pkg_diagf(ctx, "output path too long: %s", e->path); + return DIST_ERR; + } + pkg_parent_dir(full, parent, sizeof parent); + if (parent[0] && host->mkdir_p(host->user, parent) != 0) return DIST_ERR; + if (pkg_write_file(ctx, full, data, len) != DIST_OK) return DIST_ERR; + if (e->mode == DIST_TREE_MODE_EXEC && + host->mark_executable(host->user, full) != 0) + return DIST_ERR; + return DIST_OK; +} + +/* ---------------------------------------------------------------------- */ +/* portable (.tar.gz) verify / unpack */ +/* ---------------------------------------------------------------------- */ + +static const DistTarEntry* pkg_portable_find_cas( + const DistTarEntry* entries, size_t ne, const char* kind, + const uint8_t id[DIST_BLAKE2B_LEN]) { + char path[PKG_PATH_BUF]; + if (pkg_cas_rel_path(path, sizeof path, kind, id) != DIST_OK) return NULL; + return pkg_find_name(entries, ne, path); +} + +static int pkg_verify_blob_bytes(const DistTreeEntry* e, const uint8_t* data, + size_t len) { + DistBlobInfo bi; + if (dist_blob_info(&bi, data, len, DIST_BLOB_CHUNK_SIZE_DEFAULT) != DIST_OK) + return DIST_ERR; + return bi.size == e->size && memcmp(bi.id, e->blob, DIST_BLAKE2B_LEN) == 0 && + memcmp(bi.root, e->root, DIST_BLAKE2B_LEN) == 0 + ? DIST_OK + : DIST_ERR; +} + +static int pkg_verify_portable_tree(const CfreeCasHost* host, + const CfreeContext* ctx, + const PkgVerified* v, + const DistPackageOutput* out, + const DistTarEntry* entries, size_t ne, + const char* out_dir) { + const DistTarEntry* te = pkg_portable_find_cas(entries, ne, "tree", out->tree); + PkgLoadedTree tree; + size_t i; + if (!te) { + pkg_diagf(ctx, "portable package missing tree object"); + return DIST_ERR; + } + if (pkg_parse_tree_object(ctx, &tree, out->tree, te->data, te->size, + out->name) != DIST_OK) + return DIST_ERR; + if (pkg_verify_artifact_overlays(ctx, &v->manifest, out, &tree.tree) != + DIST_OK) + return DIST_ERR; + for (i = 0; i < tree.tree.n_entries; ++i) { + const DistTreeEntry* e = &tree.tree.entries[i]; + const DistTarEntry* be = + pkg_portable_find_cas(entries, ne, "blob", e->blob); + if (!be) { + pkg_diagf(ctx, "portable package missing blob: %s", e->path); + return DIST_ERR; + } + if (pkg_verify_blob_bytes(e, be->data, be->size) != DIST_OK) { + pkg_diagf(ctx, "blob hash mismatch: %s", e->path); + return DIST_ERR; + } + if (out_dir && + pkg_write_output_file(host, ctx, out_dir, e, be->data, be->size) != + DIST_OK) + return DIST_ERR; + } + return DIST_OK; +} + +static int pkg_load_portable(const CfreeContext* ctx, const uint8_t* data, + size_t len, CfreeWriter** inflated_out, + DistTarEntry* entries, size_t* ne) { + CfreeWriter* inflated = NULL; + const uint8_t* bytes; + size_t ilen; + inflated = pkg_mem(ctx); + if (!inflated || dist_gz_decompress(inflated, data, len) != DIST_OK) { + pkg_diagf(ctx, "malformed portable package"); + if (inflated) cfree_writer_close(inflated); + return DIST_ERR; + } + bytes = cfree_writer_mem_bytes(inflated, &ilen); + if (dist_tar_iter(bytes, ilen, entries, PKG_MAX_TAR_ENTRIES, ne) != DIST_OK) { + pkg_diagf(ctx, "malformed portable tar"); + cfree_writer_close(inflated); + return DIST_ERR; + } + *inflated_out = inflated; + return DIST_OK; +} + +static int pkg_verify_portable(const CfreeContext* ctx, + const CfreeCasHost* host, + const CfreePkgVerifyOptions* opts, + PkgVerified* v) { + CfreeWriter* inflated = NULL; + DistTarEntry entries[PKG_MAX_TAR_ENTRIES]; + size_t ne = 0, oi; + const DistTarEntry *man, *sig, *pub; + const DistPackageOutput* def; + int rc = DIST_ERR; + if (pkg_load_portable(ctx, opts->pkg_data, opts->pkg_len, &inflated, entries, + &ne) != DIST_OK) + return DIST_ERR; + 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) { + pkg_diagf(ctx, "package missing manifest or signature"); + goto done; + } + if (pkg_verify_manifest(ctx, man->data, man->size, sig->data, sig->size, + pub ? pub->data : NULL, pub ? pub->size : 0, opts, + v) != DIST_OK) + goto done; + def = pkg_default_output(&v->manifest); + if (!def) goto done; + for (oi = 0; oi < v->manifest.n_outputs; ++oi) { + const DistPackageOutput* out = &v->manifest.outputs[oi]; + if (pkg_verify_portable_tree(host, ctx, v, out, entries, ne, + out == def ? opts->unpack_dir : NULL) != + DIST_OK) + goto done; + } + rc = DIST_OK; +done: + if (inflated) cfree_writer_close(inflated); + return rc; +} + +/* ---------------------------------------------------------------------- */ +/* native (.cfpkg) verify / unpack */ +/* ---------------------------------------------------------------------- */ + +static int pkg_bounds3(const DistCfpkg3Header* 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}}; + 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_range_ok(uint64_t off, uint64_t size, size_t len) { + return off <= len && size <= len - off; +} + +static const DistCfpkg3TreeObject* pkg_descriptor_find_tree( + const DistCfpkg3Descriptor* d, const uint8_t id[DIST_BLAKE2B_LEN]) { + size_t i; + for (i = 0; i < d->n_trees; ++i) + if (memcmp(d->trees[i].tree, id, DIST_BLAKE2B_LEN) == 0) return &d->trees[i]; + return NULL; +} + +static int pkg_descriptor_has_embedded_chunks(const DistCfpkg3Descriptor* d) { + size_t i; + for (i = 0; i < d->n_chunk_sources; ++i) + if (d->chunk_sources[i].kind == DIST_CFPKG3_CHUNK_SOURCE_EMBEDDED) return 1; + return 0; +} + +static const char* pkg_descriptor_chunk_template( + const DistCfpkg3Descriptor* d) { + size_t i; + for (i = 0; i < d->n_chunk_sources; ++i) + if (d->chunk_sources[i].kind == DIST_CFPKG3_CHUNK_SOURCE_URL_TEMPLATE) + return d->chunk_sources[i].tmpl; + return NULL; +} + +static int pkg_render_chunk_template(char* out, size_t cap, const char* tmpl, + const uint8_t blob[DIST_BLAKE2B_LEN], + uint64_t chunk_index) { + char blob_hex[2 * DIST_BLAKE2B_LEN + 1]; + char blob_prefix[3]; + char chunk_dec[24]; + size_t oi = 0, i; + dist_hex_encode(blob_hex, blob, DIST_BLAKE2B_LEN); + blob_prefix[0] = blob_hex[0]; + blob_prefix[1] = blob_hex[1]; + blob_prefix[2] = '\0'; + snprintf(chunk_dec, sizeof chunk_dec, "%llu", + (unsigned long long)chunk_index); + for (i = 0; tmpl[i];) { + const char* repl = NULL; + size_t repl_len = 0; + if (strncmp(tmpl + i, "{blob}", 6) == 0) { + repl = blob_hex; + repl_len = strlen(blob_hex); + i += 6; + } else if (strncmp(tmpl + i, "{blob-prefix}", 13) == 0) { + repl = blob_prefix; + repl_len = strlen(blob_prefix); + i += 13; + } else if (strncmp(tmpl + i, "{chunk}", 7) == 0) { + repl = chunk_dec; + repl_len = strlen(chunk_dec); + i += 7; + } else { + if (oi + 1u >= cap) return DIST_ERR; + out[oi++] = tmpl[i++]; + continue; + } + if (oi + repl_len >= cap) return DIST_ERR; + memcpy(out + oi, repl, repl_len); + oi += repl_len; + } + if (oi >= cap) return DIST_ERR; + out[oi] = '\0'; + return pkg_locator_safe(out) ? DIST_OK : DIST_ERR; +} + +static int pkg_verify_native_index_sorted(const uint8_t* index_b, + size_t index_l, + const DistCfpkg3Descriptor* d) { + DistCfpkg3IndexRecord prev; + size_t off; + int have_prev = 0; + int embedded_chunks = pkg_descriptor_has_embedded_chunks(d); + if (index_l != d->index_bytes || + index_l % DIST_CFPKG3_INDEX_RECORD_SIZE != 0) + return DIST_ERR; + memset(&prev, 0, sizeof prev); + for (off = 0; off < index_l; off += DIST_CFPKG3_INDEX_RECORD_SIZE) { + DistCfpkg3IndexRecord r; + int cmp; + if (dist_cfpkg3_decode_index_record(index_b + off, + DIST_CFPKG3_INDEX_RECORD_SIZE, + &r) != DIST_OK) + return DIST_ERR; + if (r.raw_size == 0 || r.raw_size > d->chunk_size || + !dist_cfpkg_compression_name(r.compression)) + return DIST_ERR; + if (embedded_chunks) { + if (r.content_offset > d->content_size || + r.stored_size > d->content_size - r.content_offset) + return DIST_ERR; + } else if (r.content_offset != 0) { + return DIST_ERR; + } + if (!have_prev) { + if (r.chunk_index != 0) return DIST_ERR; + } else { + cmp = memcmp(prev.blob_id, r.blob_id, DIST_BLAKE2B_LEN); + if (cmp > 0) return DIST_ERR; + if (cmp == 0) { + if (r.chunk_index <= prev.chunk_index) return DIST_ERR; + } else if (r.chunk_index != 0) { + return DIST_ERR; + } + } + prev = r; + have_prev = 1; + } + return DIST_OK; +} + +static int pkg_native_load_tree(const CfreeContext* ctx, const uint8_t* data, + size_t len, const DistCfpkg3Descriptor* d, + const DistPackageOutput* out, + const char* external_dir, PkgLoadedTree* tree) { + const DistCfpkg3TreeObject* obj = pkg_descriptor_find_tree(d, out->tree); + const uint8_t* bytes; + uint8_t h[DIST_BLAKE2B_LEN]; + (void)len; + if (!obj || !obj->embedded) { + CfreeFileData fd; + char rel[PKG_PATH_BUF]; + int rc; + if (!obj || !external_dir) { + pkg_diagf(ctx, "external tree object is missing"); + return DIST_ERR; + } + if (obj->url[0]) + snprintf(rel, sizeof rel, "%s", obj->url); + else if (pkg_external_id_path(rel, sizeof rel, "tree", out->tree) != DIST_OK) + return DIST_ERR; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (pkg_read_external_file(ctx, external_dir, rel, &fd) != DIST_OK) { + pkg_diagf(ctx, "missing external tree object: %s", rel); + return DIST_ERR; + } + pkg_hash(h, fd.data, fd.size); + if (memcmp(h, obj->blake2b, DIST_BLAKE2B_LEN) != 0 || + memcmp(h, out->tree, DIST_BLAKE2B_LEN) != 0) { + ctx->file_io->release(ctx->file_io->user, &fd); + pkg_diagf(ctx, "tree object hash mismatch"); + return DIST_ERR; + } + rc = pkg_parse_tree_object(ctx, tree, out->tree, fd.data, fd.size, + out->name); + ctx->file_io->release(ctx->file_io->user, &fd); + tree->bytes = NULL; + tree->size = 0; + return rc; + } + if (obj->offset > d->tree_size || obj->size > d->tree_size - obj->offset) + return DIST_ERR; + bytes = data + d->tree_offset + obj->offset; + pkg_hash(h, bytes, (size_t)obj->size); + if (memcmp(h, obj->blake2b, DIST_BLAKE2B_LEN) != 0 || + memcmp(h, out->tree, DIST_BLAKE2B_LEN) != 0) { + pkg_diagf(ctx, "tree object hash mismatch"); + return DIST_ERR; + } + return pkg_parse_tree_object(ctx, tree, out->tree, bytes, (size_t)obj->size, + out->name); +} + +static int pkg_native_load_index(const CfreeContext* ctx, const uint8_t* data, + const DistCfpkg3Descriptor* d, + const char* external_dir, CfreeFileData* fd, + const uint8_t** index_b, size_t* index_l) { + uint8_t root[DIST_BLAKE2B_LEN]; + fd->data = NULL; + fd->size = 0; + fd->token = NULL; + if (d->index_size != 0) { + if (d->index_size != d->index_bytes) return DIST_ERR; + *index_b = data + d->index_offset; + *index_l = (size_t)d->index_size; + } else { + char rel[PKG_PATH_BUF]; + if (!external_dir) { + pkg_diagf(ctx, "external index is missing"); + return DIST_ERR; + } + if (d->index_url[0]) + snprintf(rel, sizeof rel, "%s", d->index_url); + else if (pkg_external_id_path(rel, sizeof rel, "index", d->index_root) != + DIST_OK) + return DIST_ERR; + if (pkg_read_external_file(ctx, external_dir, rel, fd) != DIST_OK) { + pkg_diagf(ctx, "missing external index: %s", rel); + return DIST_ERR; + } + *index_b = fd->data; + *index_l = fd->size; + } + if (*index_l != d->index_bytes) return DIST_ERR; + dist_cfpkg3_region_root(root, "index", *index_b, *index_l); + return memcmp(root, d->index_root, DIST_BLAKE2B_LEN) == 0 ? DIST_OK + : DIST_ERR; +} + +static int pkg_decode_native_chunk(CfreeWriter* raww, const uint8_t* stored, + size_t stored_len, + const DistCfpkg3Descriptor* d, + const DistCfpkg3IndexRecord* r) { + uint8_t sh[DIST_BLAKE2B_LEN], rh[DIST_BLAKE2B_LEN], leaf[DIST_BLAKE2B_LEN]; + if (r->raw_size == 0 || r->raw_size > d->chunk_size || + r->stored_size != stored_len) + return DIST_ERR; + 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) { + if (r->raw_size != r->stored_size) return DIST_ERR; + pkg_hash(rh, stored, (size_t)r->stored_size); + dist_blob_leaf_hash(leaf, r->chunk_index, stored, (size_t)r->stored_size); + if (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_CFPKG3_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_blob_leaf_hash(leaf, 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; + } + return DIST_OK; +} + +static int pkg_native_load_stored_chunk(const CfreeContext* ctx, + const uint8_t* data, + const DistCfpkg3Descriptor* d, + const DistCfpkg3IndexRecord* r, + const char* external_dir, + const char* chunk_template, + CfreeFileData* fd, const uint8_t** stored, + size_t* stored_len) { + fd->data = NULL; + fd->size = 0; + fd->token = NULL; + if (pkg_descriptor_has_embedded_chunks(d)) { + if (r->content_offset > d->content_size || + r->stored_size > d->content_size - r->content_offset) + return DIST_ERR; + *stored = data + d->content_offset + r->content_offset; + *stored_len = (size_t)r->stored_size; + return DIST_OK; + } + { + char rel[PKG_PATH_BUF]; + if (!external_dir) return DIST_ERR; + if (chunk_template) { + if (pkg_render_chunk_template(rel, sizeof rel, chunk_template, r->blob_id, + r->chunk_index) != DIST_OK) + return DIST_ERR; + } else if (dist_cas_chunk_relpath(rel, sizeof rel, r->blob_id, + r->chunk_index) != DIST_OK) { + return DIST_ERR; + } + if (pkg_read_external_file(ctx, external_dir, rel, fd) != DIST_OK) { + pkg_diagf(ctx, "missing external chunk: %s", rel); + return DIST_ERR; + } + *stored = fd->data; + *stored_len = fd->size; + return DIST_OK; + } +} + +static int pkg_native_reconstruct_blob(const CfreeContext* ctx, + const uint8_t* data, + const uint8_t* index_b, size_t index_l, + const DistCfpkg3Descriptor* d, + const DistTreeEntry* e, + const char* external_dir, + const char* chunk_template, + CfreeWriter** raww_out) { + CfreeWriter* raww = pkg_mem(ctx); + uint64_t want_chunk = 0; + size_t off; + int saw = 0; + if (!raww) return DIST_ERR; + if (index_l % DIST_CFPKG3_INDEX_RECORD_SIZE != 0) goto fail; + for (off = 0; off < index_l; off += DIST_CFPKG3_INDEX_RECORD_SIZE) { + DistCfpkg3IndexRecord r; + CfreeFileData chunk_fd; + const uint8_t* stored; + size_t stored_len; + int cmp; + if (dist_cfpkg3_decode_index_record(index_b + off, + DIST_CFPKG3_INDEX_RECORD_SIZE, + &r) != DIST_OK) + goto fail; + cmp = memcmp(r.blob_id, e->blob, DIST_BLAKE2B_LEN); + if (cmp < 0) continue; + if (cmp > 0 && saw) break; + if (cmp > 0) continue; + saw = 1; + if (r.chunk_index != want_chunk++) goto fail; + if (pkg_native_load_stored_chunk(ctx, data, d, &r, external_dir, + chunk_template, &chunk_fd, &stored, + &stored_len) != DIST_OK) + goto fail; + if (pkg_decode_native_chunk(raww, stored, stored_len, d, &r) != DIST_OK) { + if (chunk_fd.data && ctx->file_io->release) + ctx->file_io->release(ctx->file_io->user, &chunk_fd); + goto fail; + } + if (chunk_fd.data && ctx->file_io->release) + ctx->file_io->release(ctx->file_io->user, &chunk_fd); + } + *raww_out = raww; + return DIST_OK; +fail: + cfree_writer_close(raww); + return DIST_ERR; +} + +static int pkg_verify_native_tree(const CfreeCasHost* host, + const CfreeContext* ctx, const uint8_t* data, + size_t len, const uint8_t* index_b, + size_t index_l, const DistCfpkg3Descriptor* d, + const PkgVerified* v, + const DistPackageOutput* out, + const char* external_dir, + const char* chunk_template, + const char* out_dir) { + PkgLoadedTree tree; + size_t i; + if (pkg_native_load_tree(ctx, data, len, d, out, external_dir, &tree) != + DIST_OK) + return DIST_ERR; + if (pkg_verify_artifact_overlays(ctx, &v->manifest, out, &tree.tree) != + DIST_OK) + return DIST_ERR; + for (i = 0; i < tree.tree.n_entries; ++i) { + const DistTreeEntry* e = &tree.tree.entries[i]; + CfreeWriter* raww = NULL; + const uint8_t* rawb; + size_t rawl; + if (pkg_native_reconstruct_blob(ctx, data, index_b, index_l, d, e, + external_dir, chunk_template, + &raww) != DIST_OK) { + pkg_diagf(ctx, "native chunk verification failed: %s", e->path); + return DIST_ERR; + } + rawb = cfree_writer_mem_bytes(raww, &rawl); + if (pkg_verify_blob_bytes(e, rawb, rawl) != DIST_OK) { + cfree_writer_close(raww); + pkg_diagf(ctx, "blob hash mismatch: %s", e->path); + return DIST_ERR; + } + if (out_dir && + pkg_write_output_file(host, ctx, out_dir, e, rawb, rawl) != DIST_OK) { + cfree_writer_close(raww); + return DIST_ERR; + } + cfree_writer_close(raww); + } + return DIST_OK; +} + +static int pkg_verify_native(const CfreeContext* ctx, const CfreeCasHost* host, + const CfreePkgVerifyOptions* opts, + PkgVerified* v) { + const uint8_t* data = opts->pkg_data; + size_t len = opts->pkg_len; + const char* external_dir = opts->external_dir; + CfreeFileData index_fd = {0}; + DistCfpkg3Header h; + DistCfpkg3Descriptor d; + char err[128]; + uint8_t desc_keyid[DIST_KEYID_LEN], tree_root[DIST_BLAKE2B_LEN], + index_root[DIST_BLAKE2B_LEN], content_root[DIST_BLAKE2B_LEN]; + char desc_trusted[DIST_TRUSTED_COMMENT_MAX]; + const DistPackageOutput* def; + const uint8_t* index_b = NULL; + size_t index_l = 0; + const char* chunk_template = NULL; + size_t oi; + int rc = DIST_ERR; + if (dist_cfpkg3_read_header(data, len, &h) != DIST_OK || + pkg_bounds3(&h, len) != DIST_OK) { + pkg_diagf(ctx, "malformed native package"); + return DIST_ERR; + } + if (pkg_verify_manifest(ctx, data + h.manifest_offset, + (size_t)h.manifest_size, data + h.signature_offset, + (size_t)h.signature_size, data + h.pubkey_offset, + (size_t)h.pubkey_size, opts, v) != DIST_OK) + return DIST_ERR; + if (dist_minisig_sig_keyid(data + h.descriptor_signature_offset, + (size_t)h.descriptor_signature_size, + desc_keyid) != DIST_OK || + memcmp(desc_keyid, v->keyid, DIST_KEYID_LEN) != 0) { + pkg_diagf(ctx, "encoding descriptor signer mismatch"); + return DIST_ERR; + } + if (dist_minisig_verify(data + h.descriptor_signature_offset, + (size_t)h.descriptor_signature_size, + data + h.descriptor_offset, (size_t)h.descriptor_size, + v->pk, desc_trusted, sizeof desc_trusted) != DIST_OK) { + pkg_diagf(ctx, "encoding descriptor signature FAILED"); + return DIST_ERR; + } + if (dist_cfpkg3_descriptor_parse(data + h.descriptor_offset, + (size_t)h.descriptor_size, &d, err, + sizeof err) != DIST_OK) { + pkg_diagf(ctx, "encoding descriptor: %s", err); + return DIST_ERR; + } + if (memcmp(d.package_id, v->package_id, DIST_BLAKE2B_LEN) != 0 || + d.chunk_size != DIST_CFPKG3_CHUNK_SIZE_DEFAULT || + d.alignment != DIST_CFPKG3_ALIGNMENT || + !pkg_range_ok(d.tree_offset, d.tree_size, len) || + !pkg_range_ok(d.index_offset, d.index_size, len) || + !pkg_range_ok(d.content_offset, d.content_size, len)) { + pkg_diagf(ctx, "encoding descriptor does not match package layout"); + return DIST_ERR; + } + dist_cfpkg3_region_root(tree_root, "tree", data + d.tree_offset, + (size_t)d.tree_size); + dist_cfpkg3_region_root(content_root, "content", data + d.content_offset, + (size_t)d.content_size); + if (pkg_native_load_index(ctx, data, &d, external_dir, &index_fd, &index_b, + &index_l) != DIST_OK) { + pkg_diagf(ctx, "native package index verification failed"); + goto done; + } + dist_cfpkg3_region_root(index_root, "index", index_b, index_l); + if (!pkg_descriptor_has_embedded_chunks(&d)) + chunk_template = pkg_descriptor_chunk_template(&d); + if (d.index_bytes && !pkg_descriptor_has_embedded_chunks(&d) && + !external_dir) { + pkg_diagf(ctx, "external native chunks are missing"); + goto done; + } + if (memcmp(tree_root, d.tree_root, DIST_BLAKE2B_LEN) != 0 || + memcmp(index_root, d.index_root, DIST_BLAKE2B_LEN) != 0 || + memcmp(content_root, d.content_root, DIST_BLAKE2B_LEN) != 0) { + pkg_diagf(ctx, "native package region hash mismatch"); + goto done; + } + if (pkg_verify_native_index_sorted(index_b, index_l, &d) != DIST_OK) { + pkg_diagf(ctx, "native chunk index is malformed"); + goto done; + } + def = pkg_default_output(&v->manifest); + if (!def) goto done; + for (oi = 0; oi < v->manifest.n_outputs; ++oi) { + const DistPackageOutput* out = &v->manifest.outputs[oi]; + if (pkg_verify_native_tree(host, ctx, data, len, index_b, index_l, &d, v, + out, external_dir, chunk_template, + out == def ? opts->unpack_dir : NULL) != DIST_OK) + goto done; + } + rc = DIST_OK; +done: + if (index_fd.token || index_fd.data) + ctx->file_io->release(ctx->file_io->user, &index_fd); + return rc; +} + +/* ---------------------------------------------------------------------- */ +/* public verbs + primitives */ +/* ---------------------------------------------------------------------- */ + +CfreeStatus cfree_pkg_verify(const CfreeContext* ctx, const CfreeCasHost* host, + const CfreePkgVerifyOptions* opts, + CfreePkgVerifyResult* result) { + PkgVerified v; + int rc; + if (!ctx || !host || !opts || !result || !opts->pkg_data) return CFREE_INVALID; + if (opts->format == CFREE_PKG_FORMAT_TARGZ) + rc = pkg_verify_portable(ctx, host, opts, &v); + else + rc = pkg_verify_native(ctx, host, opts, &v); + if (rc != DIST_OK) return CFREE_ERR; + memset(result, 0, sizeof *result); + snprintf(result->name, sizeof result->name, "%s", v.manifest.name); + snprintf(result->version, sizeof result->version, "%s", v.manifest.version); + snprintf(result->trusted, sizeof result->trusted, "%s", v.trusted); + memcpy(result->keyid, v.keyid, DIST_KEYID_LEN); + result->tofu_pin = v.tofu_pin; + memcpy(result->tofu_pk, v.pk, DIST_ED25519_PK_LEN); + return CFREE_OK; +} + +CfreeStatus cfree_pkg_inspect(const CfreeContext* ctx, const uint8_t* pkg_data, + size_t pkg_len, CfreePkgFormat format, + int show_encoding, CfreeWriter* out) { + if (!ctx || !pkg_data || !out) return CFREE_INVALID; + if (format == CFREE_PKG_FORMAT_TARGZ) { + CfreeWriter* inflated = NULL; + DistTarEntry entries[PKG_MAX_TAR_ENTRIES]; + size_t ne = 0; + const DistTarEntry* man; + CfreeStatus st = CFREE_ERR; + if (pkg_load_portable(ctx, pkg_data, pkg_len, &inflated, entries, &ne) == + DIST_OK && + (man = pkg_find_name(entries, ne, PKG_META_MANIFEST)) != NULL) { + if (man->size && cfree_writer_write(out, man->data, man->size) != CFREE_OK) + st = CFREE_IO; + else + st = cfree_writer_status(out) == CFREE_OK ? CFREE_OK : CFREE_IO; + } + if (inflated) cfree_writer_close(inflated); + return st; + } else { + DistCfpkg3Header h; + const uint8_t* region; + size_t region_len; + if (dist_cfpkg3_read_header(pkg_data, pkg_len, &h) != DIST_OK || + pkg_bounds3(&h, pkg_len) != DIST_OK) { + pkg_diagf(ctx, "malformed native package"); + return CFREE_MALFORMED; + } + if (show_encoding) { + region = pkg_data + h.descriptor_offset; + region_len = (size_t)h.descriptor_size; + } else { + region = pkg_data + h.manifest_offset; + region_len = (size_t)h.manifest_size; + } + if (region_len && cfree_writer_write(out, region, region_len) != CFREE_OK) + return CFREE_IO; + return cfree_writer_status(out) == CFREE_OK ? CFREE_OK : CFREE_IO; + } +} + +CfreeStatus cfree_pkg_keygen(const CfreeContext* ctx, CfreePkgRandomFn rng, + void* rng_user, CfreeWriter* pub_out, + CfreeWriter* sec_out, + uint8_t out_keyid[CFREE_PKG_KEYID_LEN]) { + uint8_t seed[DIST_ED25519_SEED_LEN], keyid[DIST_KEYID_LEN]; + DistKeypair kp; + if (!rng || !pub_out || !sec_out) return CFREE_INVALID; + if (rng(rng_user, seed, sizeof seed) != 0 || + rng(rng_user, keyid, sizeof keyid) != 0) { + pkg_diagf(ctx, "keygen: failed to read system randomness"); + return CFREE_ERR; + } + dist_minisig_keygen(&kp, seed, keyid); + if (dist_minisig_emit_pubkey(pub_out, &kp) != DIST_OK || + cfree_writer_status(pub_out) != CFREE_OK) + return CFREE_IO; + if (dist_minisig_emit_seckey(sec_out, &kp) != DIST_OK || + cfree_writer_status(sec_out) != CFREE_OK) + return CFREE_IO; + if (out_keyid) memcpy(out_keyid, kp.keyid, DIST_KEYID_LEN); + return CFREE_OK; +} + +CfreeStatus cfree_minisig_parse_pubkey(const uint8_t* data, size_t len, + uint8_t pk_out[CFREE_PKG_PK_LEN], + uint8_t keyid_out[CFREE_PKG_KEYID_LEN]) { + return dist_minisig_parse_pubkey(data, len, pk_out, keyid_out) == DIST_OK + ? CFREE_OK + : CFREE_MALFORMED; +} + +CfreeStatus cfree_minisig_parse_seckey(const uint8_t* data, size_t len, + uint8_t sk_out[CFREE_PKG_SK_LEN], + uint8_t keyid_out[CFREE_PKG_KEYID_LEN]) { + int rc = dist_minisig_parse_seckey(data, len, sk_out, keyid_out); + if (rc == DIST_OK) return CFREE_OK; + if (rc == DIST_ENCRYPTED) return CFREE_UNSUPPORTED; + return CFREE_MALFORMED; +} + +CfreeStatus cfree_trust_lookup(const uint8_t* file, size_t len, + const uint8_t keyid[CFREE_PKG_KEYID_LEN], + uint8_t pk_out[CFREE_PKG_PK_LEN]) { + return dist_trust_lookup(file, len, keyid, pk_out) == DIST_OK ? CFREE_OK + : CFREE_NOT_FOUND; +} + +CfreeStatus cfree_trust_format_entry(char* out, size_t cap, + const uint8_t keyid[CFREE_PKG_KEYID_LEN], + const uint8_t pk[CFREE_PKG_PK_LEN], + const char* label) { + return dist_trust_format_entry(out, cap, keyid, pk, label) == DIST_OK + ? CFREE_OK + : CFREE_ERR; +} diff --git a/src/core/config_assert.c b/src/core/config_assert.c @@ -82,6 +82,8 @@ _Static_assert(!CFREE_EMU_ENABLED || "support"); _Static_assert(!CFREE_INTERP_ENABLED || CFREE_OPT_ENABLED, "CFREE_INTERP_ENABLED requires CFREE_OPT_ENABLED"); +_Static_assert(!CFREE_PKG_ENABLED || CFREE_CAS_ENABLED, + "CFREE_PKG_ENABLED requires CFREE_CAS_ENABLED"); _Static_assert(!CFREE_TOOL_CC_ENABLED || (CFREE_LANG_C_ENABLED && CFREE_LINK_ENABLED && @@ -116,3 +118,7 @@ _Static_assert(!CFREE_TOOL_SIZE_ENABLED || CFREE_AR_ENABLED, "CFREE_TOOL_SIZE_ENABLED requires ar support"); _Static_assert(!CFREE_TOOL_ADDR2LINE_ENABLED || CFREE_DWARF_ENABLED, "CFREE_TOOL_ADDR2LINE_ENABLED requires DWARF support"); +_Static_assert(!CFREE_TOOL_CAS_ENABLED || CFREE_CAS_ENABLED, + "CFREE_TOOL_CAS_ENABLED requires CAS support"); +_Static_assert(!CFREE_TOOL_PKG_ENABLED || CFREE_PKG_ENABLED, + "CFREE_TOOL_PKG_ENABLED requires PKG support"); diff --git a/driver/dist/b64.c b/src/dist/b64.c diff --git a/driver/dist/b64.h b/src/dist/b64.h diff --git a/driver/dist/blake2b.c b/src/dist/blake2b.c diff --git a/src/dist/blake2b.h b/src/dist/blake2b.h @@ -0,0 +1,23 @@ +#ifndef CFREE_DIST_BLAKE2B_H +#define CFREE_DIST_BLAKE2B_H + +#include <stddef.h> +#include <stdint.h> + +#include "dist.h" +#include "../../vendor/monocypher/monocypher.h" + +/* v2 package/content hash. Minisign signatures use their own 64-byte BLAKE2b + * prehash path in minisig.c for stock minisign compatibility. */ +typedef struct DistBlake2b { + crypto_blake2b_ctx ctx; +} 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); + +#endif diff --git a/driver/dist/blob.c b/src/dist/blob.c diff --git a/driver/dist/blob.h b/src/dist/blob.h diff --git a/driver/dist/cas.c b/src/dist/cas.c diff --git a/driver/dist/cas.h b/src/dist/cas.h diff --git a/driver/dist/cfpkg.c b/src/dist/cfpkg.c diff --git a/driver/dist/cfpkg.h b/src/dist/cfpkg.h diff --git a/driver/dist/deflate.c b/src/dist/deflate.c diff --git a/driver/dist/deflate.h b/src/dist/deflate.h diff --git a/driver/dist/dist.c b/src/dist/dist.c diff --git a/driver/dist/dist.h b/src/dist/dist.h diff --git a/src/dist/ed25519.c b/src/dist/ed25519.c @@ -0,0 +1,24 @@ +#include "ed25519.h" + +#include <string.h> + +#include "../../vendor/monocypher/monocypher-ed25519.h" + +void dist_ed25519_keypair(uint8_t pk[DIST_ED25519_PK_LEN], + uint8_t sk[DIST_ED25519_SK_LEN], + const uint8_t seed[DIST_ED25519_SEED_LEN]) { + uint8_t seed_copy[DIST_ED25519_SEED_LEN]; + memcpy(seed_copy, seed, sizeof seed_copy); + crypto_ed25519_key_pair(sk, pk, seed_copy); +} + +void dist_ed25519_sign(uint8_t sig[DIST_ED25519_SIG_LEN], const uint8_t* msg, + size_t msglen, const uint8_t sk[DIST_ED25519_SK_LEN]) { + crypto_ed25519_sign(sig, sk, msg, msglen); +} + +int dist_ed25519_verify(const uint8_t sig[DIST_ED25519_SIG_LEN], + const uint8_t* msg, size_t msglen, + const uint8_t pk[DIST_ED25519_PK_LEN]) { + return crypto_ed25519_check(sig, pk, msg, msglen) == 0 ? 1 : 0; +} diff --git a/driver/dist/ed25519.h b/src/dist/ed25519.h diff --git a/src/dist/lz4.c b/src/dist/lz4.c @@ -0,0 +1,39 @@ +#include "lz4.h" + +#include <limits.h> + +#define LZ4_STATIC_LINKING_ONLY_DISABLE_MEMORY_ALLOCATION 1 +#define LZ4LIB_VISIBILITY +#include "../../vendor/lz4/lz4.c" + +size_t dist_lz4_compress_bound(size_t raw_len) { + int bound; + if (raw_len > (size_t)LZ4_MAX_INPUT_SIZE) return 0; + bound = LZ4_compressBound((int)raw_len); + if (bound <= 0) return 0; + return (size_t)bound; +} + +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 n; + if (!dst || !dst_len || (!src && src_len != 0)) return DIST_ERR; + if (src_len > (size_t)LZ4_MAX_INPUT_SIZE || dst_cap > (size_t)INT_MAX) + return DIST_ERR; + n = LZ4_compress_default((const char*)src, (char*)dst, (int)src_len, + (int)dst_cap); + if (n <= 0) return DIST_ERR; + *dst_len = (size_t)n; + return DIST_OK; +} + +int dist_lz4_decompress_block(uint8_t* dst, size_t dst_len, + const uint8_t* src, size_t src_len) { + int n; + if (!dst || (!src && src_len != 0)) return DIST_ERR; + if (dst_len > (size_t)INT_MAX || src_len > (size_t)INT_MAX) return DIST_ERR; + n = LZ4_decompress_safe((const char*)src, (char*)dst, (int)src_len, + (int)dst_len); + if (n != (int)dst_len) return DIST_ERR; + return DIST_OK; +} diff --git a/driver/dist/lz4.h b/src/dist/lz4.h diff --git a/driver/dist/manifest.c b/src/dist/manifest.c diff --git a/driver/dist/manifest.h b/src/dist/manifest.h diff --git a/driver/dist/minisig.c b/src/dist/minisig.c diff --git a/driver/dist/minisig.h b/src/dist/minisig.h diff --git a/driver/dist/tar.c b/src/dist/tar.c diff --git a/driver/dist/tar.h b/src/dist/tar.h diff --git a/driver/dist/tree.c b/src/dist/tree.c diff --git a/driver/dist/tree.h b/src/dist/tree.h diff --git a/driver/dist/trust.c b/src/dist/trust.c diff --git a/driver/dist/trust.h b/src/dist/trust.h diff --git a/driver/dist/vendor/lz4/LICENSE b/vendor/lz4/LICENSE diff --git a/driver/dist/vendor/lz4/lz4.c b/vendor/lz4/lz4.c diff --git a/driver/dist/vendor/lz4/lz4.h b/vendor/lz4/lz4.h diff --git a/driver/dist/vendor/monocypher/LICENCE.md b/vendor/monocypher/LICENCE.md diff --git a/driver/dist/vendor/monocypher/monocypher-ed25519.c b/vendor/monocypher/monocypher-ed25519.c diff --git a/driver/dist/vendor/monocypher/monocypher-ed25519.h b/vendor/monocypher/monocypher-ed25519.h diff --git a/driver/dist/vendor/monocypher/monocypher.c b/vendor/monocypher/monocypher.c diff --git a/driver/dist/vendor/monocypher/monocypher.h b/vendor/monocypher/monocypher.h