commit 56f95449e7d0bcdd0570ee1898933546cfa4e54c
parent 5704791f29736c06d932c385e746f32c481cbb44
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 17:47:57 -0700
Implement v2 package distribution
Diffstat:
15 files changed, 1680 insertions(+), 982 deletions(-)
diff --git a/Makefile b/Makefile
@@ -349,9 +349,10 @@ DRIVER_TOOL_SRCS += driver/strings.c
endif
ifeq ($(CFREE_TOOL_PKG_ENABLED),1)
DRIVER_TOOL_SRCS += driver/pkg.c
-DRIVER_TOOL_SRCS += driver/dist/dist.c driver/dist/b64.c driver/dist/sha256.c \
+DRIVER_TOOL_SRCS += driver/dist/dist.c driver/dist/b64.c \
driver/dist/blake2b.c driver/dist/ed25519.c \
driver/dist/tar.c driver/dist/deflate.c \
+ driver/dist/lz4.c driver/dist/cfpkg.c \
driver/dist/manifest.c driver/dist/minisig.c \
driver/dist/trust.c
endif
diff --git a/doc/DISTRIBUTE.md b/doc/DISTRIBUTE.md
@@ -1,387 +1,281 @@
# Code distribution: packaging, signing, and verification
-This document specifies how cfree produces and consumes signed,
-self-describing code packages. The goal is *basic* code distribution: a
-producer can bundle a build into a single verifiable artifact, and a
-consumer can authenticate it against a trusted key and unpack it — with
-no network code in cfree.
-
-## Scope
-
-In scope:
-
-- A **package format** built from a manifest, a detached signature, and
- a compressed payload, optionally bagged into a single `.cfpkg` file.
-- **Signing and verification** using the minisign signature scheme
- (Ed25519 over a BLAKE2b prehash).
-- A **trust model**: a trusted-keys file, with opt-in trust-on-first-use
- (TOFU).
-- **Content addressing**: every payload byte is reached by SHA-256
- through the signed manifest.
-
-Out of scope (deliberately):
-
-- **Transport / fetch.** cfree does not speak HTTP, TLS, or any network
- protocol. Moving bytes from producer to consumer is the job of
- existing tools (`scp`, `curl`, `git`, a CDN, email). cfree only
- *produces* and *consumes* the artifact. This keeps cfree out of the
- ongoing security-maintenance burden of a TLS stack.
-- **Dependency resolution.** The manifest reserves a `[dependency]`
- schema (see below) so manifests are forward-compatible, but v1 tooling
- does not resolve, fetch, or version-solve dependencies.
+cfree distribution v2 produces signed, self-describing code packages with one
+canonical logical format and two physical representations:
+
+- `.tar.gz` for portable archive tooling,
+- `.cfpkg` for native chunked verification and future streaming.
+
+There is no network transport or dependency resolution in cfree. Existing tools
+move package bytes; cfree creates, verifies, inspects, and unpacks them.
+
+## Trust and identity
+
+The signed object is always the logical manifest's literal byte stream. The
+package id is:
+
+```
+package-id = BLAKE2b-512(logical manifest literal bytes)
+```
+
+The detached minisign signature covers the manifest. Its trusted comment also
+contains `pkgid=<hex package-id>`, and verification rejects the package if that
+signed value does not match the recomputed manifest hash.
+
+Trust anchors are public keys in the trusted-keys file:
+
+- `$CFREE_TRUSTED_KEYS`, if set,
+- otherwise `$HOME/.config/cfree/trusted_keys`.
+
+Each line is:
+
+```
+<keyid-hex> <pubkey-base64> <label>
+```
+
+A bundled `.pub` is never trust by itself. It is only a TOFU candidate. With
+`--tofu`, cfree pins the bundled public key after confirming its key id matches
+the signature's key id. Without TOFU or `-p PUBKEY`, unknown signers fail.
## Vendored primitives
-Distribution adds five small freestanding primitives. All are pure
-computation — no syscalls — and fit cfree's freestanding posture. SHA-256
-already exists (`src/core/sha256.c`).
+The driver-side distribution subsystem uses BLAKE2b throughout. SHA-256 is not
+part of the v2 package format.
-| Primitive | Purpose | Notes |
+| Primitive | Purpose | Status |
|---|---|---|
-| **tar** | container read/write | header format only; trivial |
-| **DEFLATE** | gzip payload compression | inflate + deflate; the bulk of the work |
-| **Ed25519** | signature scheme | pulls in SHA-512 internally |
-| **BLAKE2b** | minisign prehash | hashes the signed file before Ed25519 |
-| **base64** | key/signature text encoding | minisign keys and sigs are base64 |
-| SHA-256 | content addressing | already vendored |
+| tar | portable archive container | real |
+| gzip/DEFLATE | portable compression | gzip/stored blocks real; compressed DEFLATE stubbed |
+| BLAKE2b | package id, whole-file hashes, Merkle hashing, minisign prehash | API real; math stubbed |
+| Ed25519 | minisign signature scheme | API real; math stubbed |
+| base64 | minisign key/signature text | real |
+| LZ4 block | native chunk compression | API real; implementation stubbed |
-Encoding conventions: **hashes are lowercase hex** in the manifest
-(diff-friendly); **base64 is reserved strictly for key material and
-signatures** (minisign's native encoding).
+The stubs are deterministic and insecure. They let the package pipeline run
+end-to-end now; replacing the stub bodies with real vendored primitives should
+not require format or caller changes.
-## The package: three canonical files, one bundle
+## Logical manifest
-A package is canonically **three files**:
+The logical manifest is INI-style, byte-stable, and strict. Comments and blank
+lines are signed bytes. Unknown keys, unknown sections, and unknown versions are
+errors.
-| File | Role | Trust |
-|---|---|---|
-| `<name>-<version>.manifest` | signed root: metadata + `archive-sha256` + per-file hashes | **the** signed object |
-| `<name>-<version>.manifest.minisig` | detached minisign signature over the manifest's literal bytes | the proof |
-| `<name>-<version>.tar.gz` | payload blob; its SHA-256 equals `archive-sha256` in the manifest | trusted *transitively* via the manifest |
+Example:
-The signature **only ever covers the manifest** (~1 KB of text).
-Everything else is reached by hash *through* the manifest, so the large
-payload is never signed directly — the signed text vouches for it.
+```ini
+cfree-package 2
+name = hello
+version = 0.3.1
+description = minimal greeting program
+hash = blake2b-merkle-v1
-Because moving three loose files by hand is clunky and transport is out
-of scope, the **distribution unit is a single-file bundle**:
+[artifact]
+id = 0
+path = bin/hello
+kind = exe
+size = 16384
+blake2b = <whole-file-blake2b-512>
+root = <artifact-merkle-root>
+entry = true
-```
-<name>-<version>.cfpkg = uncompressed tar of { manifest, manifest.minisig, tar.gz, .pub }
+[dependency]
+name = libfoo
+version = >=1.2.0
+blake2b = <dependency-package-id>
+key = <expected-signer-keyid>
```
-The bag also carries a fourth member — the signer's **public key**
-(`<name>-<version>.pub`). It is *untrusted* (anyone can put a key in a
-bag); it exists only so a verifier running TOFU can pin it without a
-separate out-of-band fetch. It is never consulted unless the verifier
-opts into TOFU, and even then only after its key id matches the
-signature's.
-
-The outer tar is **uncompressed, unsigned, and trust-neutral**: it is
-pure transport glue. Nothing inside it is trusted until the signature
-inside it checks out. There is no chicken-and-egg, because the signature
-is a *sibling* of the manifest inside the bag, not nested under what it
-signs.
-
-The `.cfpkg` is the implemented distribution unit; emitting the three
-loose files alongside it is a planned producer flag. **Consumers accept
-the `.cfpkg`.**
-
-## Manifest format
-
-The manifest is INI-style, line-oriented text. It is designed around one
-overriding constraint: **it is the signed object**, so it must be
-
-1. **byte-stable** — we sign the literal bytes on disk; the parser only
- *reads* and never reserializes. This sidesteps JSON-style
- canonicalization entirely (key ordering, whitespace, number
- formatting).
-2. **trivially parseable freestanding** — sectioned `key = value` lines,
- no recursive value grammar, no escaping rules.
-3. **hash-pinning** — it carries the SHA-256 of the payload archive and
- of each file, so the signature transitively covers all of it.
-
-### Grammar
-
-- First line is the exact magic and format version: `cfree-manifest 1`.
- A parser that does not recognize the version **rejects** the file.
-- Top-level `key = value` lines precede any section.
-- `[artifact]` and `[dependency]` introduce repeatable blocks.
-- `value` is the rest of the line, trimmed of surrounding whitespace, so
- free-text values (e.g. `description`) may contain spaces. There is no
- escaping and no multi-line values.
-- Lines beginning with `#` are comments. **Comments and blank lines are
- part of the signed bytes** — they are not normalized away. Tooling
- that rewrites a manifest produces a fresh byte stream that must be
- re-signed.
-- **Unknown keys are an error** (strict parse). Because the signed
- surface is the literal bytes, there is no safe "ignore unknown" — an
- unrecognized key means a manifest from a newer producer, which the
- consumer must not silently accept.
-
-### Fields
-
-Top-level:
+Top-level fields:
| Key | Required | Meaning |
|---|---|---|
-| `name` | yes | package name (human handle) |
-| `version` | yes | version string; ordering is a semver subset |
-| `archive` | yes | payload filename |
-| `archive-sha256` | yes | hex SHA-256 of the payload `.tar.gz` bytes |
-| `archive-size` | yes | payload size in bytes |
+| `name` | yes | package name |
+| `version` | yes | version string |
| `description` | no | one-line free text |
+| `hash` | yes | currently `blake2b-merkle-v1` |
-There is **deliberately no timestamp** in the manifest: two builds of
-the same inputs produce byte-identical manifests, hence the same package
-id, so the artifact stays reproducible. Signing *time* is a property of
-the signing act, not the artifact, so it lives in the signature's
-trusted comment (`created=<unix-seconds>`) — signed, tamper-evident, and
-outside the reproducible surface.
-
-`[artifact]` block (repeatable, one per distributable file):
+`[artifact]` fields:
| Key | Required | Meaning |
|---|---|---|
-| `path` | yes | path within the unpacked payload |
-| `sha256` | yes | hex SHA-256 of the **uncompressed** file contents |
-| `size` | yes | uncompressed file size in bytes |
-| `kind` | yes | `exe` \| `dso` \| `obj` \| `wasm` \| `lib` \| `data` \| `source` |
-| `target` | no | cfree triple (see below); omit ⇒ target-independent |
+| `id` | yes | numeric artifact id, unique in the manifest |
+| `path` | yes | unpacked artifact path |
+| `kind` | yes | `exe`, `dso`, `obj`, `wasm`, `lib`, `data`, or `source` |
+| `size` | yes | uncompressed byte length |
+| `blake2b` | yes | BLAKE2b-512 of the whole artifact bytes |
+| `root` | yes | artifact Merkle root |
+| `target` | no | cfree target triple |
| `entry` | no | `true` if runnable under jit/emu/wasm |
-`[dependency]` block (repeatable; **reserved** — see Scope):
+`[dependency]` fields are validated but not resolved:
| Key | Required | Meaning |
|---|---|---|
| `name` | yes | dependency package name |
-| `version` | yes | version constraint, e.g. `>=1.2.0` |
-| `sha256` | no | pin to the dependency's manifest hash (its package id) |
-| `key` | no | hex id of the expected signer key |
+| `version` | yes | version constraint |
+| `blake2b` | no | dependency package id |
+| `key` | no | expected signer key id |
-v1 tooling validates the `[dependency]` schema but performs no
-resolution.
+## Merkle tree
-### Target triples
+Artifacts are split into fixed 64 KiB raw chunks. The final chunk may be
+shorter. The tree is deterministic and deliberately simple:
-The `target` field uses cfree's **own** canonical triple strings,
-parsed and formatted by `driver_target_from_triple` /
-`driver_target_to_triple` (`driver/target.c`) — not a parallel naming
-scheme. Examples: `x86_64-linux`, `aarch64-apple-darwin`,
-`riscv64-elf`, `wasm32-wasi`. A package is naturally multi-target: it
-carries one `[artifact]` per build.
+```
+leaf = BLAKE2b("cfpkg2 leaf v1" || artifact-id || chunk-index ||
+ raw-size || raw-bytes)
+node = BLAKE2b("cfpkg2 node v1" || left-hash || right-hash)
+root = BLAKE2b("cfpkg2 root v1" || "artifact" || top-hash)
+```
-### Example
+At each level, adjacent hashes are paired left-to-right. If a level has an odd
+final hash, that hash is promoted unchanged to the next level. Empty artifacts
+use a separate domain:
-```ini
-cfree-manifest 1
-name = hello
-version = 0.3.1
-description = minimal greeting program
-archive = hello-0.3.1.tar.gz
-archive-sha256 = 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
-archive-size = 20480
+```
+root = BLAKE2b("cfpkg2 root v1" || "artifact-empty" || artifact-id || 0)
+```
-[artifact]
-path = bin/hello-x64
-target = x86_64-linux
-kind = exe
-sha256 = 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae
-size = 16384
-entry = true
+This avoids virtual padding, duplicated leaves, and power-of-two tree rules.
+Proof verification only needs the leaf index, leaf count, sibling hashes, and
+the expected root.
-[artifact]
-path = bin/hello-arm
-target = aarch64-apple-darwin
-kind = exe
-sha256 = fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9
-size = 16512
+## Portable `.tar.gz`
+
+The portable representation is a gzip-compressed tar:
-[dependency]
-name = libfoo
-version = >=1.2.0
-sha256 = a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00
```
+hello-0.3.1.tar.gz
+ cfree/package.manifest
+ cfree/package.manifest.minisig
+ cfree/package.pub
+ bin/hello
+ share/data.txt
+```
+
+Verification:
+
+1. Decompress and parse the tar container.
+2. Read `cfree/package.manifest` and `cfree/package.manifest.minisig`.
+3. Anchor and verify the manifest signature.
+4. Parse the logical manifest.
+5. Verify every artifact member's whole-file BLAKE2b and Merkle root.
-## Identity and content addressing
+This format is not optimized for seeking. It is meant to interoperate with
+ordinary archive tooling.
-`sha256(manifest)` is the package's **immutable cryptographic id**.
-`name@version` is the human handle; the manifest hash is what a
-dependency pins to (`[dependency].sha256`). The full chain:
+## Native `.cfpkg`
+
+The native representation is a chunked binary container:
```
-signer signs manifest → sha256(manifest) is the package id
-manifest pins archive → archive unpacks to per-file hash-checked tree
+fixed header
+logical manifest bytes
+manifest minisign bytes
+encoding descriptor bytes
+encoding descriptor minisign bytes
+bundled pubkey bytes
+binary chunk index
+aligned content region
```
-The package id is *also* recorded in the minisign signature's trusted
-comment as `pkgid=<hex>` (alongside `created=<unix-seconds>`), giving a
-second, signed assertion of the id. Verification recomputes
-`sha256(manifest)` and **rejects** the package if it does not match the
-`pkgid` in the (signed) trusted comment.
-
-## Trust model
-
-A signature is worthless without a way to anchor the signing key.
-cfree's anchor is a **trusted-keys file**.
-
-- **Trusted-keys file.** A text list of trusted public keys, one per
- line (`<keyid-hex> <pubkey-base64> <label>`), consulted at
- verification time. Path: `$CFREE_TRUSTED_KEYS` if set, else
- `$HOME/.config/cfree/trusted_keys`. Verification succeeds only if the
- signature validates against a key in this file (or an explicit `-p`
- key).
-- **Key id.** Each key has a short id (minisign's key id), used in
- `[dependency].key` hints and in trust-store lookups. **A key carried
- in a bundle (the `.pub` member) or named in a manifest is never
- trust** — it is at most a TOFU candidate / hint. Trust comes only from
- the trusted-keys file.
-- **TOFU is opt-in.** By default, an unknown signer fails verification.
- With `--tofu`, the consumer pins the bundle's bundled public key on
- first use — but only after its key id matches the signature's —
- recording it in the trusted-keys file. Subsequent verifications
- require the pinned key to match. TOFU is never the default and is
- always an explicit, recorded action.
-
-## Verification chain
+The fixed header is trust-neutral. It locates the early byte ranges only:
```
-pkg.manifest.minisig ──verifies (against trusted key)──> pkg.manifest (literal bytes)
- │ pins
- archive-sha256 ──> verify pkg.tar.gz BEFORE decompressing │ (defends against decompression bombs / tampered tar)
- [artifact].sha256 ──> verify each file AFTER unpack <───────┘
+magic = "cfpkg2\0"
+version = 2
+header-size
+manifest-offset / manifest-size
+signature-offset / signature-size
+descriptor-offset / descriptor-size
+descriptor-signature-offset / descriptor-signature-size
+pubkey-offset / pubkey-size
+index-offset / index-size
+content-offset / content-size
+alignment
+chunk-size
```
-Steps:
-
-0. Crack the outer (uncompressed, unsigned) `.cfpkg` tar to recover its
- members. Nothing extracted here is trusted until step 2 passes.
-1. Read the signature's key id; locate that public key in the
- trusted-keys file (or use an explicit `-p` key, or pin the bundled
- `.pub` via opt-in `--tofu`). On miss without TOFU: **fail**.
-2. Verify the detached signature over the manifest's literal bytes. On
- mismatch: **fail**.
-3. Parse the manifest (strict; unknown keys/version: **fail**), and
- confirm the signed trusted comment's `pkgid` equals
- `sha256(manifest)`. On mismatch: **fail**.
-4. Hash the payload `.tar.gz` and compare to `archive-sha256` **before
- decompressing**. On mismatch: **fail**.
-5. (unpack only) Decompress and untar; hash each extracted file and
- compare to its `[artifact].sha256`. On mismatch: **fail**.
+Trust starts at the verified logical manifest. Layout trust starts at the
+verified encoding descriptor, which is signed by the same trusted key as the
+manifest:
-## CLI
+```ini
+cfree-encoding 2
+package-id = <BLAKE2b-512 of logical manifest>
+format = cfpkg
+hash = blake2b-merkle-v1
+index-offset = 1024
+index-size = 240
+index-root = <authenticated index region root>
+content-offset = 1264
+content-size = 16384
+content-root = <authenticated content region root>
+chunk-size = 65536
+alignment = 16
+```
+
+Each binary index record is fixed-size and little-endian:
+
+```
+artifact-id u64
+chunk-index u64
+content-offset u64 # relative to the content region
+stored-size u64
+raw-size u64
+compression u32 # 0 = none, 1 = lz4-block-v1
+reserved u32
+stored-hash BLAKE2b-512
+raw-hash BLAKE2b-512
+leaf-hash BLAKE2b-512
+```
+
+Native verification:
-Distribution slots into the existing binutils-style multitool
-(`cfree ar`, `cfree nm`, `cfree objdump`, `cfree ld`, …) as a single
-`pkg` tool with subcommands:
+1. Read the fixed header.
+2. Verify the logical manifest signature.
+3. Verify the encoding descriptor signature with the same key.
+4. Confirm descriptor `package-id`, offsets, sizes, roots, chunk size, and
+ alignment match the container.
+5. Verify stored chunk hashes before decoding.
+6. Decode each chunk (`none` is implemented; `lz4-block-v1` is recognized but
+ stubbed until LZ4 is vendored).
+7. Verify raw chunk hashes, leaf hashes, artifact roots, and whole-file
+ BLAKE2b hashes.
+
+## CLI
```
-cfree pkg keygen -o BASE # write BASE.pub / BASE.key
-cfree pkg create --name N --version V [--desc D] \
- -s SECKEY -o OUT.cfpkg FILE... # archive + manifest + sign + bundle
-cfree pkg verify [-p PUBKEY | --tofu] FILE.cfpkg # full chain (trust store / TOFU / hashes)
-cfree pkg unpack FILE.cfpkg -C DIR # verify, then extract the payload
-cfree pkg inspect FILE.cfpkg # print the manifest without unpacking
+cfree pkg keygen -o BASE
+cfree pkg create --name N --version V [--desc D] -s SECKEY \
+ [--format cfpkg|tar.gz] [--compression none|lz4-block-v1] \
+ -o OUT FILE...
+cfree pkg verify [-p PUBKEY | --tofu] [--format cfpkg|tar.gz] FILE
+cfree pkg unpack FILE -C DIR
+cfree pkg inspect FILE
cfree pkg trust {list | add PUBKEY [label] | remove KEYID}
```
-`create` currently takes an explicit file list (one `[artifact]` per
-file); directory walking and richer per-file `kind`/`target` mapping are
-follow-ups.
+`create` infers the physical representation from `-o`: `.cfpkg` is native,
+`.tar.gz` is portable. `--format` overrides inference.
## Implementation status
-The orchestration (manifest, bundling, the trust store, the full
-verification chain), the **file formats**, the tar/base64/gzip+CRC32
-container layer, and **key generation entropy** are real. What remains
-stubbed is the **crypto math under `driver/dist/`**: `dist_sha256` /
-`dist_blake2b` are deterministic non-cryptographic digests, and
-`dist_ed25519` derives signatures from the seed (the "public key" *is*
-the seed). These are *insecure* placeholders, each marked at its
-definition.
-
-What is already real and final:
-
-- **Key/signature file layout is byte-identical to minisign.** Public
- keys are `base64("Ed" || keyid || pk)`; signatures are
- `base64("ED" || keyid || sig)` over a BLAKE2b prehash plus a global
- signature over the trusted comment; secret keys use minisign's
- passwordless struct (`kdf_alg = {0,0}`, no scrypt) with the `"B2"`
- BLAKE2b checksum. So **once real Ed25519/BLAKE2b are vendored, cfree
- and minisign key/signature files are interchangeable** — you can point
- `-s` / `-p` at a passwordless minisign key. Password-encrypted secret
- keys (`kdf_alg = "Sc"`) are detected and rejected with a clear error.
-- **Key generation uses the host CSPRNG.** Entropy is the one place real
- crypto must touch the host; it flows through `driver_random_bytes`
- (env.h → `/dev/urandom` on POSIX, `rand_s` on Windows). The crypto
- modules stay pure: the seed and key id are passed *in*. This is also
- the model for a future password prompt (a TTY shim in env.h).
-
-Until the math is vendored, `cfree pkg` still provides **no security**
-and stock `minisign` cannot validate cfree's (stubbed) signatures.
-
-## Implementation checklist: interfaces to make real
-
-Replacing the stub *bodies* below — without changing a single signature
-or any caller — turns this into a fully real, minisign-interoperable
-implementation. Each is its own self-contained file under `driver/dist/`.
-
-**Crypto / compression (the stubs):**
-
-- [ ] **`driver/dist/sha256.c`** — `dist_sha256`
- ```c
- void dist_sha256(uint8_t out[32], const uint8_t* data, size_t len);
- ```
- Real SHA-256. (Or expose the existing `src/core/sha256.c` to the driver
- and forward to it.) Used for content addressing + the package id.
-
-- [ ] **`driver/dist/blake2b.c`** — `dist_blake2b`
- ```c
- void dist_blake2b(uint8_t out[64], const uint8_t* data, size_t len);
- ```
- Real BLAKE2b-512. Used as the minisign signature prehash and (first 32
- bytes) as the secret-key checksum.
-
-- [ ] **`driver/dist/ed25519.c`** — keypair / sign / verify
- ```c
- void dist_ed25519_keypair(uint8_t pk[32], uint8_t sk[64], const uint8_t seed[32]);
- void dist_ed25519_sign(uint8_t sig[64], const uint8_t* msg, size_t msglen, const uint8_t sk[64]);
- int dist_ed25519_verify(const uint8_t sig[64], const uint8_t* msg, size_t msglen, const uint8_t pk[32]);
- ```
- Real Ed25519. **Must use the standard 64-byte secret-key layout
- `sk = seed[32] || pk[32]`** — `pkg.c` and the secret-key file recover
- the public key from `sk[32:64]`. `_verify` returns 1 on a valid
- signature, 0 otherwise. Entropy is *not* this module's job: the seed is
- passed in (filled by `driver_random_bytes`).
-
-- [ ] **`driver/dist/deflate.c`** — real compression
- ```c
- int dist_gz_compress(CfreeWriter* out, const uint8_t* data, size_t len);
- int dist_gz_decompress(CfreeWriter* out, const uint8_t* data, size_t len);
- ```
- The gzip framing, CRC32, and stored-block handling are already real;
- add real LZ77 + Huffman to `_compress`, and teach `_decompress` to
- inflate compressed (non-stored) blocks. `DIST_OK` / `DIST_ERR`.
-
-**Already real — do not reimplement** (listed so the boundary is clear):
-`driver/dist/{b64,tar,manifest,minisig,trust,dist}.c`, the gzip
-wrapper/CRC32/stored blocks in `deflate.c`, and `driver_random_bytes`
-(env layer). `minisig.c` is the only consumer of the crypto stubs; it
-needs no change when they go real.
-
-**Additional, only for password-encrypted secret keys (optional):**
-
-- [ ] a `scrypt` KDF primitive, and
-- [ ] a password-prompt TTY shim in `env.h` (model on `driver_read_line`),
- then have `dist_minisig_emit_seckey` / `parse_seckey` honor
- `kdf_alg = "Sc"` instead of the passwordless `{0,0}` form. Not required
- for verification or for passwordless-key interop.
-
-## Open questions / future work
-
-- The crypto/compression checklist above is the gating work; the file
- formats, trust model, and entropy source are already final.
-- Add scrypt + a password-prompt TTY shim (env.h) for encrypted secret
- keys, matching minisign's `kdf_alg = "Sc"` form.
-- Emit the three loose files alongside the `.cfpkg`, and accept a loose
- manifest as `verify`/`unpack` input.
+Implemented:
+
+- v2 logical manifest parser/emitter,
+- portable `.tar.gz` create/verify/unpack/inspect,
+- native `.cfpkg` create/verify/unpack/inspect,
+- minisign-compatible key/signature file layout,
+- trusted-keys store and opt-in TOFU,
+- BLAKE2b streaming API and package Merkle helpers,
+- raw LZ4 block API stubs,
+- driver-side SHA-256 removal for distribution.
+
+Still stubbed/insecure:
+
+- `driver/dist/blake2b.c`,
+- `driver/dist/ed25519.c`,
+- compressed DEFLATE support in `driver/dist/deflate.c`,
+- raw LZ4 block compression/decompression in `driver/dist/lz4.c`.
diff --git a/driver/dist/blake2b.c b/driver/dist/blake2b.c
@@ -1,7 +1,7 @@
#include "blake2b.h"
-/* STUB digest. See blake2b.h. Same construction as the sha256 stub with a
- * different domain constant, producing a 64-byte output. */
+/* STUB digest. See blake2b.h. A counter-driven FNV/xorshift sponge: enough
+ * avalanche to distinguish inputs in tests, but cryptographically worthless. */
#define DIST_B2_FNV_OFFSET 0x100000001b3cbf29ULL
#define DIST_B2_FNV_PRIME 0x00000100000001b3ULL
@@ -16,14 +16,32 @@ static uint64_t b2_absorb(const uint8_t* data, size_t len, uint64_t h) {
return h;
}
-void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data,
- size_t len) {
- uint64_t base = b2_absorb(data, len, DIST_B2_FNV_OFFSET);
+void dist_blake2b_init(DistBlake2b* s, size_t out_len) {
+ s->h = DIST_B2_FNV_OFFSET ^ (uint64_t)out_len;
+ s->len = 0;
+ s->out_len = out_len;
+}
+
+void dist_blake2b_update(DistBlake2b* s, const uint8_t* data, size_t len) {
+ s->h = b2_absorb(data, len, s->h);
+ s->len += (uint64_t)len;
+}
+
+void dist_blake2b_final(DistBlake2b* s, uint8_t* out) {
+ uint64_t base = s->h ^ (s->len * 0x9e3779b97f4a7c15ULL);
size_t i;
- for (i = 0; i < DIST_BLAKE2B_LEN; ++i) {
+ for (i = 0; i < s->out_len; ++i) {
uint64_t h = base ^ (0xc2b2ae3d27d4eb4fULL * (uint64_t)(i + 1));
h *= DIST_B2_FNV_PRIME;
h ^= h >> 27;
out[i] = (uint8_t)(h >> 32);
}
}
+
+void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data,
+ size_t len) {
+ DistBlake2b s;
+ dist_blake2b_init(&s, DIST_BLAKE2B_LEN);
+ dist_blake2b_update(&s, data, len);
+ dist_blake2b_final(&s, out);
+}
diff --git a/driver/dist/blake2b.h b/driver/dist/blake2b.h
@@ -6,11 +6,21 @@
#include "dist.h"
-/* minisign prehash. Real minisign hashes the signed file with BLAKE2b-512 and
- * signs the 64-byte digest.
+/* minisign prehash and v2 package/content hash. Real minisign hashes the
+ * signed file with BLAKE2b-512 and signs the 64-byte digest.
*
* STUB: deterministic non-cryptographic 64-byte digest. Not BLAKE2b. Replace
* with the real implementation when vendoring crypto. */
+typedef struct DistBlake2b {
+ uint64_t h;
+ uint64_t len;
+ size_t out_len;
+} DistBlake2b;
+
+void dist_blake2b_init(DistBlake2b* s, size_t out_len);
+void dist_blake2b_update(DistBlake2b* s, const uint8_t* data, size_t len);
+void dist_blake2b_final(DistBlake2b* s, uint8_t* out);
+
void dist_blake2b(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data,
size_t len);
diff --git a/driver/dist/cfpkg.c b/driver/dist/cfpkg.c
@@ -0,0 +1,419 @@
+#include "cfpkg.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "blake2b.h"
+
+#define DESC_LINE_MAX 1024u
+
+static void put_u32le(uint8_t* p, uint32_t v) {
+ p[0] = (uint8_t)v;
+ p[1] = (uint8_t)(v >> 8);
+ p[2] = (uint8_t)(v >> 16);
+ p[3] = (uint8_t)(v >> 24);
+}
+
+static void put_u64le(uint8_t* p, uint64_t v) {
+ unsigned i;
+ for (i = 0; i < 8u; ++i) p[i] = (uint8_t)(v >> (8u * i));
+}
+
+static uint32_t get_u32le(const uint8_t* p) {
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) |
+ ((uint32_t)p[3] << 24);
+}
+
+static uint64_t get_u64le(const uint8_t* p) {
+ uint64_t v = 0;
+ unsigned i;
+ for (i = 0; i < 8u; ++i) v |= ((uint64_t)p[i]) << (8u * i);
+ return v;
+}
+
+static void hash_u64(DistBlake2b* h, uint64_t v) {
+ uint8_t b[8];
+ put_u64le(b, v);
+ dist_blake2b_update(h, b, sizeof b);
+}
+
+static int emit(CfreeWriter* out, const char* s) {
+ return cfree_writer_write(out, s, strlen(s)) == CFREE_OK ? DIST_OK : DIST_ERR;
+}
+
+static int emit_kv(CfreeWriter* out, const char* key, const char* val) {
+ char line[DESC_LINE_MAX];
+ snprintf(line, sizeof line, "%s = %s\n", key, val);
+ return emit(out, line);
+}
+
+static int emit_u64(CfreeWriter* out, const char* key, uint64_t v) {
+ char num[24];
+ snprintf(num, sizeof num, "%llu", (unsigned long long)v);
+ return emit_kv(out, key, num);
+}
+
+static int emit_hex(CfreeWriter* out, const char* key, const uint8_t* h) {
+ char hex[2 * DIST_BLAKE2B_LEN + 1];
+ dist_hex_encode(hex, h, DIST_BLAKE2B_LEN);
+ return emit_kv(out, key, hex);
+}
+
+void dist_cfpkg2_leaf_hash(uint8_t out[DIST_BLAKE2B_LEN],
+ uint64_t artifact_id, uint64_t chunk_index,
+ const uint8_t* raw, size_t raw_len) {
+ DistBlake2b h;
+ static const uint8_t dom[] = "cfpkg2 leaf v1";
+ dist_blake2b_init(&h, DIST_BLAKE2B_LEN);
+ dist_blake2b_update(&h, dom, sizeof dom - 1);
+ hash_u64(&h, artifact_id);
+ hash_u64(&h, chunk_index);
+ hash_u64(&h, (uint64_t)raw_len);
+ dist_blake2b_update(&h, raw, raw_len);
+ dist_blake2b_final(&h, out);
+}
+
+void dist_cfpkg2_node_hash(uint8_t out[DIST_BLAKE2B_LEN],
+ const uint8_t left[DIST_BLAKE2B_LEN],
+ const uint8_t right[DIST_BLAKE2B_LEN]) {
+ DistBlake2b h;
+ static const uint8_t dom[] = "cfpkg2 node v1";
+ dist_blake2b_init(&h, DIST_BLAKE2B_LEN);
+ dist_blake2b_update(&h, dom, sizeof dom - 1);
+ dist_blake2b_update(&h, left, DIST_BLAKE2B_LEN);
+ dist_blake2b_update(&h, right, DIST_BLAKE2B_LEN);
+ dist_blake2b_final(&h, out);
+}
+
+void dist_cfpkg2_root_hash(uint8_t out[DIST_BLAKE2B_LEN], const char* kind,
+ uint64_t artifact_id,
+ const uint8_t top[DIST_BLAKE2B_LEN]) {
+ DistBlake2b h;
+ static const uint8_t dom[] = "cfpkg2 root v1";
+ (void)artifact_id;
+ dist_blake2b_init(&h, DIST_BLAKE2B_LEN);
+ dist_blake2b_update(&h, dom, sizeof dom - 1);
+ dist_blake2b_update(&h, (const uint8_t*)kind, strlen(kind));
+ dist_blake2b_update(&h, top, DIST_BLAKE2B_LEN);
+ dist_blake2b_final(&h, out);
+}
+
+void dist_cfpkg2_empty_artifact_root(uint8_t out[DIST_BLAKE2B_LEN],
+ uint64_t artifact_id) {
+ DistBlake2b h;
+ static const uint8_t dom[] = "cfpkg2 root v1";
+ static const uint8_t kind[] = "artifact-empty";
+ dist_blake2b_init(&h, DIST_BLAKE2B_LEN);
+ dist_blake2b_update(&h, dom, sizeof dom - 1);
+ dist_blake2b_update(&h, kind, sizeof kind - 1);
+ hash_u64(&h, artifact_id);
+ hash_u64(&h, 0);
+ dist_blake2b_final(&h, out);
+}
+
+int dist_cfpkg2_artifact_root(uint8_t out[DIST_BLAKE2B_LEN],
+ uint64_t artifact_id, const uint8_t* raw,
+ size_t raw_len, size_t chunk_size) {
+ uint8_t level[DIST_MAX_FILES][DIST_BLAKE2B_LEN];
+ size_t leaves, i;
+ if (chunk_size == 0) return DIST_ERR;
+ if (raw_len == 0) {
+ dist_cfpkg2_empty_artifact_root(out, artifact_id);
+ return DIST_OK;
+ }
+ leaves = (raw_len + chunk_size - 1u) / chunk_size;
+ if (leaves > DIST_MAX_FILES) return DIST_ERR;
+ for (i = 0; i < leaves; ++i) {
+ size_t off = i * chunk_size;
+ size_t n = raw_len - off;
+ if (n > chunk_size) n = chunk_size;
+ dist_cfpkg2_leaf_hash(level[i], artifact_id, (uint64_t)i, raw + off, n);
+ }
+ while (leaves > 1u) {
+ size_t outn = 0;
+ for (i = 0; i < leaves; i += 2u) {
+ if (i + 1u < leaves)
+ dist_cfpkg2_node_hash(level[outn], level[i], level[i + 1u]);
+ else
+ memcpy(level[outn], level[i], DIST_BLAKE2B_LEN);
+ ++outn;
+ }
+ leaves = outn;
+ }
+ dist_cfpkg2_root_hash(out, "artifact", artifact_id, level[0]);
+ return DIST_OK;
+}
+
+int dist_cfpkg2_verify_proof(const uint8_t leaf[DIST_BLAKE2B_LEN],
+ uint64_t leaf_index, uint64_t leaf_count,
+ const uint8_t* proof, size_t proof_len,
+ const uint8_t root[DIST_BLAKE2B_LEN]) {
+ uint8_t cur[DIST_BLAKE2B_LEN], tmp[DIST_BLAKE2B_LEN];
+ uint64_t idx = leaf_index, count = leaf_count;
+ size_t off = 0;
+ if (leaf_count == 0 || leaf_index >= leaf_count) return DIST_ERR;
+ memcpy(cur, leaf, DIST_BLAKE2B_LEN);
+ while (count > 1u) {
+ int has_pair = (idx + 1u < count) || (idx & 1u);
+ if (has_pair) {
+ if (off + DIST_BLAKE2B_LEN > proof_len) return DIST_ERR;
+ if (idx & 1u)
+ dist_cfpkg2_node_hash(tmp, proof + off, cur);
+ else
+ dist_cfpkg2_node_hash(tmp, cur, proof + off);
+ memcpy(cur, tmp, DIST_BLAKE2B_LEN);
+ off += DIST_BLAKE2B_LEN;
+ }
+ idx >>= 1;
+ count = (count + 1u) >> 1;
+ }
+ dist_cfpkg2_root_hash(tmp, "artifact", 0, cur);
+ return off == proof_len && memcmp(tmp, root, DIST_BLAKE2B_LEN) == 0
+ ? DIST_OK
+ : DIST_ERR;
+}
+
+int dist_cfpkg_write_header(CfreeWriter* out, const DistCfpkgHeader* h) {
+ uint8_t b[DIST_CFPKG_HEADER_SIZE];
+ size_t off = 16u;
+ memset(b, 0, sizeof b);
+ memcpy(b, DIST_CFPKG_MAGIC, 7u);
+ put_u32le(b + 8u, DIST_CFPKG_VERSION);
+ put_u32le(b + 12u, DIST_CFPKG_HEADER_SIZE);
+#define PUT(v) \
+ do { \
+ put_u64le(b + off, (v)); \
+ off += 8u; \
+ } while (0)
+ PUT(h->manifest_offset);
+ PUT(h->manifest_size);
+ PUT(h->signature_offset);
+ PUT(h->signature_size);
+ PUT(h->descriptor_offset);
+ PUT(h->descriptor_size);
+ PUT(h->descriptor_signature_offset);
+ PUT(h->descriptor_signature_size);
+ PUT(h->pubkey_offset);
+ PUT(h->pubkey_size);
+ PUT(h->index_offset);
+ PUT(h->index_size);
+ PUT(h->content_offset);
+ PUT(h->content_size);
+ PUT(h->alignment);
+ PUT(h->chunk_size);
+#undef PUT
+ return cfree_writer_write(out, b, sizeof b) == CFREE_OK ? DIST_OK : DIST_ERR;
+}
+
+int dist_cfpkg_read_header(const uint8_t* data, size_t len,
+ DistCfpkgHeader* h) {
+ size_t off = 16u;
+ if (len < DIST_CFPKG_HEADER_SIZE) return DIST_ERR;
+ if (memcmp(data, DIST_CFPKG_MAGIC, 7u) != 0) return DIST_ERR;
+ if (get_u32le(data + 8u) != DIST_CFPKG_VERSION ||
+ get_u32le(data + 12u) != DIST_CFPKG_HEADER_SIZE)
+ return DIST_ERR;
+#define GET(dst) \
+ do { \
+ (dst) = get_u64le(data + off); \
+ off += 8u; \
+ } while (0)
+ GET(h->manifest_offset);
+ GET(h->manifest_size);
+ GET(h->signature_offset);
+ GET(h->signature_size);
+ GET(h->descriptor_offset);
+ GET(h->descriptor_size);
+ GET(h->descriptor_signature_offset);
+ GET(h->descriptor_signature_size);
+ GET(h->pubkey_offset);
+ GET(h->pubkey_size);
+ GET(h->index_offset);
+ GET(h->index_size);
+ GET(h->content_offset);
+ GET(h->content_size);
+ GET(h->alignment);
+ GET(h->chunk_size);
+#undef GET
+ return DIST_OK;
+}
+
+void dist_cfpkg_encode_index_record(uint8_t out[DIST_CFPKG_INDEX_RECORD_SIZE],
+ const DistCfpkgIndexRecord* r) {
+ memset(out, 0, DIST_CFPKG_INDEX_RECORD_SIZE);
+ put_u64le(out + 0, r->artifact_id);
+ put_u64le(out + 8, r->chunk_index);
+ put_u64le(out + 16, r->content_offset);
+ put_u64le(out + 24, r->stored_size);
+ put_u64le(out + 32, r->raw_size);
+ put_u32le(out + 40, r->compression);
+ memcpy(out + 48, r->stored_hash, DIST_BLAKE2B_LEN);
+ memcpy(out + 112, r->raw_hash, DIST_BLAKE2B_LEN);
+ memcpy(out + 176, r->leaf_hash, DIST_BLAKE2B_LEN);
+}
+
+int dist_cfpkg_decode_index_record(const uint8_t* data, size_t len,
+ DistCfpkgIndexRecord* r) {
+ if (len < DIST_CFPKG_INDEX_RECORD_SIZE) return DIST_ERR;
+ r->artifact_id = get_u64le(data + 0);
+ r->chunk_index = get_u64le(data + 8);
+ r->content_offset = get_u64le(data + 16);
+ r->stored_size = get_u64le(data + 24);
+ r->raw_size = get_u64le(data + 32);
+ r->compression = get_u32le(data + 40);
+ memcpy(r->stored_hash, data + 48, DIST_BLAKE2B_LEN);
+ memcpy(r->raw_hash, data + 112, DIST_BLAKE2B_LEN);
+ memcpy(r->leaf_hash, data + 176, DIST_BLAKE2B_LEN);
+ return DIST_OK;
+}
+
+int dist_cfpkg_descriptor_emit(CfreeWriter* out,
+ const DistCfpkgDescriptor* d) {
+ if (emit(out, "cfree-encoding 2\n") != DIST_OK) return DIST_ERR;
+ if (emit_hex(out, "package-id", d->package_id) != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "format", "cfpkg") != DIST_OK) return DIST_ERR;
+ if (emit_kv(out, "hash", "blake2b-merkle-v1") != DIST_OK) return DIST_ERR;
+ if (emit_u64(out, "index-offset", d->index_offset) != DIST_OK) return DIST_ERR;
+ if (emit_u64(out, "index-size", d->index_size) != DIST_OK) return DIST_ERR;
+ if (emit_hex(out, "index-root", d->index_root) != DIST_OK) return DIST_ERR;
+ if (emit_u64(out, "content-offset", d->content_offset) != DIST_OK)
+ return DIST_ERR;
+ if (emit_u64(out, "content-size", d->content_size) != DIST_OK)
+ return DIST_ERR;
+ if (emit_hex(out, "content-root", d->content_root) != DIST_OK)
+ return DIST_ERR;
+ if (emit_u64(out, "chunk-size", d->chunk_size) != DIST_OK) return DIST_ERR;
+ return emit_u64(out, "alignment", d->alignment);
+}
+
+static char* trim_lead(char* s) {
+ while (*s == ' ' || *s == '\t') ++s;
+ return s;
+}
+
+static void trim_trail(char* s) {
+ size_t n = strlen(s);
+ while (n && (s[n - 1] == ' ' || s[n - 1] == '\t' || s[n - 1] == '\r' ||
+ s[n - 1] == '\n'))
+ s[--n] = '\0';
+}
+
+static int set_err(char* err, size_t cap, const char* msg) {
+ if (err && cap) snprintf(err, cap, "%s", msg);
+ return DIST_ERR;
+}
+
+static int parse_u64(const char* s, uint64_t* out) {
+ char* end = NULL;
+ unsigned long long v;
+ if (!*s) return DIST_ERR;
+ v = strtoull(s, &end, 10);
+ if (!end || *end != '\0') return DIST_ERR;
+ *out = (uint64_t)v;
+ return DIST_OK;
+}
+
+int dist_cfpkg_descriptor_parse(const uint8_t* data, size_t len,
+ DistCfpkgDescriptor* d, char* err,
+ size_t errcap) {
+ size_t pos = 0;
+ int first = 1;
+ unsigned seen = 0;
+ memset(d, 0, sizeof *d);
+ while (pos < len) {
+ char buf[DESC_LINE_MAX], *t, *eq, *key, *val;
+ size_t end = pos, n;
+ while (end < len && data[end] != '\n') ++end;
+ n = end - pos;
+ if (n >= sizeof buf) return set_err(err, errcap, "line too long");
+ memcpy(buf, data + pos, n);
+ buf[n] = '\0';
+ pos = (end < len) ? end + 1 : end;
+ trim_trail(buf);
+ if (first) {
+ first = 0;
+ if (strcmp(buf, "cfree-encoding 2") != 0)
+ return set_err(err, errcap, "bad encoding descriptor magic/version");
+ continue;
+ }
+ t = trim_lead(buf);
+ if (*t == '\0' || *t == '#') continue;
+ eq = strchr(t, '=');
+ if (!eq) return set_err(err, errcap, "expected key = value");
+ *eq = '\0';
+ key = t;
+ trim_trail(key);
+ val = trim_lead(eq + 1);
+ if (strcmp(key, "package-id") == 0) {
+ if (strlen(val) != 2 * DIST_BLAKE2B_LEN ||
+ dist_hex_decode(d->package_id, val, DIST_BLAKE2B_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad package-id");
+ seen |= 1u << 0;
+ } else if (strcmp(key, "format") == 0) {
+ if (strcmp(val, "cfpkg") != 0) return set_err(err, errcap, "bad format");
+ seen |= 1u << 1;
+ } else if (strcmp(key, "hash") == 0) {
+ if (strcmp(val, "blake2b-merkle-v1") != 0)
+ return set_err(err, errcap, "bad hash algorithm");
+ seen |= 1u << 2;
+ } else if (strcmp(key, "index-offset") == 0) {
+ if (parse_u64(val, &d->index_offset) != DIST_OK)
+ return set_err(err, errcap, "bad index-offset");
+ seen |= 1u << 3;
+ } else if (strcmp(key, "index-size") == 0) {
+ if (parse_u64(val, &d->index_size) != DIST_OK)
+ return set_err(err, errcap, "bad index-size");
+ seen |= 1u << 4;
+ } else if (strcmp(key, "index-root") == 0) {
+ if (strlen(val) != 2 * DIST_BLAKE2B_LEN ||
+ dist_hex_decode(d->index_root, val, DIST_BLAKE2B_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad index-root");
+ seen |= 1u << 5;
+ } else if (strcmp(key, "content-offset") == 0) {
+ if (parse_u64(val, &d->content_offset) != DIST_OK)
+ return set_err(err, errcap, "bad content-offset");
+ seen |= 1u << 6;
+ } else if (strcmp(key, "content-size") == 0) {
+ if (parse_u64(val, &d->content_size) != DIST_OK)
+ return set_err(err, errcap, "bad content-size");
+ seen |= 1u << 7;
+ } else if (strcmp(key, "content-root") == 0) {
+ if (strlen(val) != 2 * DIST_BLAKE2B_LEN ||
+ dist_hex_decode(d->content_root, val, DIST_BLAKE2B_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad content-root");
+ seen |= 1u << 8;
+ } else if (strcmp(key, "chunk-size") == 0) {
+ if (parse_u64(val, &d->chunk_size) != DIST_OK)
+ return set_err(err, errcap, "bad chunk-size");
+ seen |= 1u << 9;
+ } else if (strcmp(key, "alignment") == 0) {
+ if (parse_u64(val, &d->alignment) != DIST_OK)
+ return set_err(err, errcap, "bad alignment");
+ seen |= 1u << 10;
+ } else {
+ return set_err(err, errcap, "unknown encoding descriptor key");
+ }
+ }
+ return seen == 0x7ffu ? DIST_OK
+ : set_err(err, errcap,
+ "missing required encoding descriptor field");
+}
+
+const char* dist_cfpkg_compression_name(uint32_t c) {
+ if (c == DIST_CFPKG_COMP_NONE) return "none";
+ if (c == DIST_CFPKG_COMP_LZ4_BLOCK_V1) return "lz4-block-v1";
+ return NULL;
+}
+
+int dist_cfpkg_compression_parse(const char* s, uint32_t* out) {
+ if (strcmp(s, "none") == 0) {
+ *out = DIST_CFPKG_COMP_NONE;
+ return DIST_OK;
+ }
+ if (strcmp(s, "lz4-block-v1") == 0) {
+ *out = DIST_CFPKG_COMP_LZ4_BLOCK_V1;
+ return DIST_OK;
+ }
+ return DIST_ERR;
+}
diff --git a/driver/dist/cfpkg.h b/driver/dist/cfpkg.h
@@ -0,0 +1,91 @@
+#ifndef CFREE_DIST_CFPKG_H
+#define CFREE_DIST_CFPKG_H
+
+#include <cfree/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+#define DIST_CFPKG_MAGIC "cfpkg2\0"
+#define DIST_CFPKG_VERSION 2u
+#define DIST_CFPKG_HEADER_SIZE 160u
+#define DIST_CFPKG_ALIGNMENT 16u
+#define DIST_CFPKG_CHUNK_SIZE_DEFAULT 65536u
+#define DIST_CFPKG_INDEX_RECORD_SIZE 240u
+
+typedef enum DistCfpkgCompression {
+ DIST_CFPKG_COMP_NONE = 0,
+ DIST_CFPKG_COMP_LZ4_BLOCK_V1 = 1,
+} DistCfpkgCompression;
+
+typedef struct DistCfpkgHeader {
+ uint64_t manifest_offset, manifest_size;
+ uint64_t signature_offset, signature_size;
+ uint64_t descriptor_offset, descriptor_size;
+ uint64_t descriptor_signature_offset, descriptor_signature_size;
+ uint64_t pubkey_offset, pubkey_size;
+ uint64_t index_offset, index_size;
+ uint64_t content_offset, content_size;
+ uint64_t alignment, chunk_size;
+} DistCfpkgHeader;
+
+typedef struct DistCfpkgIndexRecord {
+ uint64_t artifact_id;
+ uint64_t chunk_index;
+ uint64_t content_offset; /* relative to the content region */
+ uint64_t stored_size;
+ uint64_t raw_size;
+ uint32_t compression;
+ uint8_t stored_hash[DIST_BLAKE2B_LEN];
+ uint8_t raw_hash[DIST_BLAKE2B_LEN];
+ uint8_t leaf_hash[DIST_BLAKE2B_LEN];
+} DistCfpkgIndexRecord;
+
+typedef struct DistCfpkgDescriptor {
+ uint8_t package_id[DIST_BLAKE2B_LEN];
+ uint64_t index_offset, index_size;
+ uint8_t index_root[DIST_BLAKE2B_LEN];
+ uint64_t content_offset, content_size;
+ uint8_t content_root[DIST_BLAKE2B_LEN];
+ uint64_t chunk_size, alignment;
+} DistCfpkgDescriptor;
+
+void dist_cfpkg2_leaf_hash(uint8_t out[DIST_BLAKE2B_LEN],
+ uint64_t artifact_id, uint64_t chunk_index,
+ const uint8_t* raw, size_t raw_len);
+void dist_cfpkg2_node_hash(uint8_t out[DIST_BLAKE2B_LEN],
+ const uint8_t left[DIST_BLAKE2B_LEN],
+ const uint8_t right[DIST_BLAKE2B_LEN]);
+void dist_cfpkg2_root_hash(uint8_t out[DIST_BLAKE2B_LEN], const char* kind,
+ uint64_t artifact_id,
+ const uint8_t top[DIST_BLAKE2B_LEN]);
+void dist_cfpkg2_empty_artifact_root(uint8_t out[DIST_BLAKE2B_LEN],
+ uint64_t artifact_id);
+int dist_cfpkg2_artifact_root(uint8_t out[DIST_BLAKE2B_LEN],
+ uint64_t artifact_id, const uint8_t* raw,
+ size_t raw_len, size_t chunk_size);
+int dist_cfpkg2_verify_proof(const uint8_t leaf[DIST_BLAKE2B_LEN],
+ uint64_t leaf_index, uint64_t leaf_count,
+ const uint8_t* proof, size_t proof_len,
+ const uint8_t root[DIST_BLAKE2B_LEN]);
+
+int dist_cfpkg_write_header(CfreeWriter* out, const DistCfpkgHeader* h);
+int dist_cfpkg_read_header(const uint8_t* data, size_t len,
+ DistCfpkgHeader* h);
+
+void dist_cfpkg_encode_index_record(uint8_t out[DIST_CFPKG_INDEX_RECORD_SIZE],
+ const DistCfpkgIndexRecord* r);
+int dist_cfpkg_decode_index_record(const uint8_t* data, size_t len,
+ DistCfpkgIndexRecord* r);
+
+int dist_cfpkg_descriptor_emit(CfreeWriter* out,
+ const DistCfpkgDescriptor* d);
+int dist_cfpkg_descriptor_parse(const uint8_t* data, size_t len,
+ DistCfpkgDescriptor* d, char* err,
+ size_t errcap);
+
+const char* dist_cfpkg_compression_name(uint32_t c);
+int dist_cfpkg_compression_parse(const char* s, uint32_t* out);
+
+#endif
diff --git a/driver/dist/dist.h b/driver/dist/dist.h
@@ -14,8 +14,8 @@
* Every stub is marked as such at its definition. */
/* Primitive output sizes. */
-#define DIST_SHA256_LEN 32u
#define DIST_BLAKE2B_LEN 64u
+#define DIST_HASH_LEN DIST_BLAKE2B_LEN
#define DIST_ED25519_PK_LEN 32u
#define DIST_ED25519_SK_LEN 64u
#define DIST_ED25519_SIG_LEN 64u
diff --git a/driver/dist/ed25519.c b/driver/dist/ed25519.c
@@ -2,7 +2,7 @@
#include <string.h>
-#include "sha256.h"
+#include "blake2b.h"
/* STUB scheme. See ed25519.h. pk == seed; signatures are derived purely from
* the seed and message via the stub hash, and verification recomputes them
@@ -21,15 +21,13 @@ void dist_ed25519_keypair(uint8_t pk[DIST_ED25519_PK_LEN],
static void stub_sign_with_seed(uint8_t sig[DIST_ED25519_SIG_LEN],
const uint8_t seed[DIST_ED25519_SEED_LEN],
const uint8_t* msg, size_t msglen) {
- uint8_t m[DIST_SHA256_LEN];
- uint8_t buf[DIST_ED25519_SEED_LEN + DIST_SHA256_LEN];
- dist_sha256(m, msg, msglen);
- memcpy(buf, seed, DIST_ED25519_SEED_LEN);
- memcpy(buf + DIST_ED25519_SEED_LEN, m, DIST_SHA256_LEN);
- dist_sha256(sig, buf, sizeof buf); /* sig[0:32] */
- memcpy(buf, sig, DIST_SHA256_LEN);
- memcpy(buf + DIST_SHA256_LEN, seed, DIST_ED25519_SEED_LEN);
- dist_sha256(sig + DIST_SHA256_LEN, buf, sizeof buf); /* sig[32:64] */
+ DistBlake2b h;
+ static const uint8_t dom[] = "cfree stub ed25519";
+ dist_blake2b_init(&h, DIST_ED25519_SIG_LEN);
+ dist_blake2b_update(&h, dom, sizeof dom - 1);
+ dist_blake2b_update(&h, seed, DIST_ED25519_SEED_LEN);
+ dist_blake2b_update(&h, msg, msglen);
+ dist_blake2b_final(&h, sig);
}
void dist_ed25519_sign(uint8_t sig[DIST_ED25519_SIG_LEN], const uint8_t* msg,
diff --git a/driver/dist/lz4.c b/driver/dist/lz4.c
@@ -0,0 +1,28 @@
+#include "lz4.h"
+
+/* STUB: raw LZ4 block support is an external vendored primitive. The API is
+ * real so the package format and callers will not change when the
+ * implementation lands. */
+
+size_t dist_lz4_compress_bound(size_t raw_len) {
+ return raw_len + raw_len / 255u + 16u;
+}
+
+int dist_lz4_compress_block(uint8_t* dst, size_t dst_cap, size_t* dst_len,
+ const uint8_t* src, size_t src_len) {
+ (void)dst;
+ (void)dst_cap;
+ (void)dst_len;
+ (void)src;
+ (void)src_len;
+ return DIST_ERR;
+}
+
+int dist_lz4_decompress_block(uint8_t* dst, size_t dst_len,
+ const uint8_t* src, size_t src_len) {
+ (void)dst;
+ (void)dst_len;
+ (void)src;
+ (void)src_len;
+ return DIST_ERR;
+}
diff --git a/driver/dist/lz4.h b/driver/dist/lz4.h
@@ -0,0 +1,17 @@
+#ifndef CFREE_DIST_LZ4_H
+#define CFREE_DIST_LZ4_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "dist.h"
+
+size_t dist_lz4_compress_bound(size_t raw_len);
+
+int dist_lz4_compress_block(uint8_t* dst, size_t dst_cap, size_t* dst_len,
+ const uint8_t* src, size_t src_len);
+
+int dist_lz4_decompress_block(uint8_t* dst, size_t dst_len,
+ const uint8_t* src, size_t src_len);
+
+#endif
diff --git a/driver/dist/manifest.c b/driver/dist/manifest.c
@@ -6,21 +6,18 @@
#define DIST_LINE_MAX 1024u
-/* Required-field bits, reused per block. */
#define F_NAME 0x01u
#define F_VERSION 0x02u
-#define F_ARCHIVE 0x04u
-#define F_ARCH_SHA 0x08u
-#define F_ARCH_SIZE 0x10u
-#define F_PATH 0x20u
-#define F_KIND 0x40u
-#define F_SHA 0x80u
+#define F_HASH 0x04u
+#define F_ID 0x08u
+#define F_PATH 0x10u
+#define F_KIND 0x20u
+#define F_BLAKE2B 0x40u
+#define F_ROOT 0x80u
#define F_SIZE 0x100u
typedef enum { SEC_TOP, SEC_ART, SEC_DEP } Section;
-/* ---------------------------------------------------------------- emit --- */
-
static int emit(CfreeWriter* out, const char* s) {
return cfree_writer_write(out, s, strlen(s)) == CFREE_OK ? DIST_OK : DIST_ERR;
}
@@ -33,7 +30,7 @@ static int emit_kv(CfreeWriter* out, const char* key, const char* val) {
static int emit_hex(CfreeWriter* out, const char* key, const uint8_t* h,
size_t n) {
- char hex[2 * DIST_SHA256_LEN + 1];
+ char hex[2 * DIST_BLAKE2B_LEN + 1];
dist_hex_encode(hex, h, n);
return emit_kv(out, key, hex);
}
@@ -50,27 +47,23 @@ int dist_manifest_emit(const DistManifest* m, CfreeWriter* out) {
if (emit_kv(out, "name", m->name) != DIST_OK) return DIST_ERR;
if (emit_kv(out, "version", m->version) != DIST_OK) return DIST_ERR;
if (m->description[0] &&
- emit_kv(out, "description", m->description) != DIST_OK) {
- return DIST_ERR;
- }
- if (emit_kv(out, "archive", m->archive) != DIST_OK) return DIST_ERR;
- if (emit_hex(out, "archive-sha256", m->archive_sha256, DIST_SHA256_LEN) !=
- DIST_OK) {
- return DIST_ERR;
- }
- if (emit_u64(out, "archive-size", m->archive_size) != DIST_OK)
+ emit_kv(out, "description", m->description) != DIST_OK)
return DIST_ERR;
+ if (emit_kv(out, "hash", DIST_MANIFEST_HASH) != DIST_OK) return DIST_ERR;
for (i = 0; i < m->n_artifacts; ++i) {
const DistArtifact* a = &m->artifacts[i];
if (emit(out, "\n[artifact]\n") != DIST_OK) return DIST_ERR;
+ if (emit_u64(out, "id", a->id) != DIST_OK) return DIST_ERR;
if (emit_kv(out, "path", a->path) != DIST_OK) return DIST_ERR;
if (a->target[0] && emit_kv(out, "target", a->target) != DIST_OK)
return DIST_ERR;
if (emit_kv(out, "kind", a->kind) != DIST_OK) return DIST_ERR;
- if (emit_hex(out, "sha256", a->sha256, DIST_SHA256_LEN) != DIST_OK)
- return DIST_ERR;
if (emit_u64(out, "size", a->size) != DIST_OK) return DIST_ERR;
+ if (emit_hex(out, "blake2b", a->blake2b, DIST_BLAKE2B_LEN) != DIST_OK)
+ return DIST_ERR;
+ if (emit_hex(out, "root", a->root, DIST_BLAKE2B_LEN) != DIST_OK)
+ return DIST_ERR;
if (a->entry && emit_kv(out, "entry", "true") != DIST_OK) return DIST_ERR;
}
@@ -79,20 +72,16 @@ int dist_manifest_emit(const DistManifest* m, CfreeWriter* out) {
if (emit(out, "\n[dependency]\n") != DIST_OK) return DIST_ERR;
if (emit_kv(out, "name", d->name) != DIST_OK) return DIST_ERR;
if (emit_kv(out, "version", d->version) != DIST_OK) return DIST_ERR;
- if (d->has_sha256 &&
- emit_hex(out, "sha256", d->sha256, DIST_SHA256_LEN) != DIST_OK) {
+ if (d->has_blake2b &&
+ emit_hex(out, "blake2b", d->blake2b, DIST_BLAKE2B_LEN) != DIST_OK)
return DIST_ERR;
- }
if (d->has_keyid &&
- emit_hex(out, "key", d->keyid, DIST_KEYID_LEN) != DIST_OK) {
+ emit_hex(out, "key", d->keyid, DIST_KEYID_LEN) != DIST_OK)
return DIST_ERR;
- }
}
return DIST_OK;
}
-/* --------------------------------------------------------------- parse --- */
-
static char* trim_lead(char* s) {
while (*s == ' ' || *s == '\t') ++s;
return s;
@@ -101,9 +90,8 @@ static char* trim_lead(char* s) {
static void trim_trail(char* s) {
size_t n = strlen(s);
while (n && (s[n - 1] == ' ' || s[n - 1] == '\t' || s[n - 1] == '\r' ||
- s[n - 1] == '\n')) {
+ s[n - 1] == '\n'))
s[--n] = '\0';
- }
}
static int set_err(char* err, size_t cap, const char* msg) {
@@ -135,22 +123,18 @@ static int parse_u64(const char* s, uint64_t* out) {
return DIST_OK;
}
-/* Validate the just-completed block against its required-field set. */
static int finalize(Section sec, uint32_t seen, char* err, size_t errcap) {
if (sec == SEC_TOP) {
- if ((seen & (F_NAME | F_VERSION | F_ARCHIVE | F_ARCH_SHA | F_ARCH_SIZE)) !=
- (F_NAME | F_VERSION | F_ARCHIVE | F_ARCH_SHA | F_ARCH_SIZE)) {
+ if ((seen & (F_NAME | F_VERSION | F_HASH)) !=
+ (F_NAME | F_VERSION | F_HASH))
return set_err(err, errcap, "missing required top-level field");
- }
} else if (sec == SEC_ART) {
- if ((seen & (F_PATH | F_KIND | F_SHA | F_SIZE)) !=
- (F_PATH | F_KIND | F_SHA | F_SIZE)) {
+ if ((seen & (F_ID | F_PATH | F_KIND | F_BLAKE2B | F_ROOT | F_SIZE)) !=
+ (F_ID | F_PATH | F_KIND | F_BLAKE2B | F_ROOT | F_SIZE))
return set_err(err, errcap, "missing required [artifact] field");
- }
- } else { /* SEC_DEP */
- if ((seen & (F_NAME | F_VERSION)) != (F_NAME | F_VERSION)) {
+ } else {
+ if ((seen & (F_NAME | F_VERSION)) != (F_NAME | F_VERSION))
return set_err(err, errcap, "missing required [dependency] field");
- }
}
return DIST_OK;
}
@@ -178,19 +162,17 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
memcpy(buf, data + pos, n);
buf[n] = '\0';
pos = (end < len) ? end + 1 : end;
-
trim_trail(buf);
if (first) {
first = 0;
- if (strcmp(buf, DIST_MANIFEST_MAGIC) != 0) {
+ if (strcmp(buf, DIST_MANIFEST_MAGIC) != 0)
return set_err(err, errcap, "bad manifest magic/version");
- }
continue;
}
t = trim_lead(buf);
- if (*t == '\0' || *t == '#') continue; /* blank / comment */
+ if (*t == '\0' || *t == '#') continue;
if (*t == '[') {
if (finalize(sec, seen, err, errcap) != DIST_OK) return DIST_ERR;
@@ -230,24 +212,19 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
} else if (strcmp(key, "description") == 0) {
if (copy_field(m->description, sizeof m->description, val, err, errcap))
return DIST_ERR;
- } else if (strcmp(key, "archive") == 0) {
- if (copy_field(m->archive, sizeof m->archive, val, err, errcap))
- return DIST_ERR;
- seen |= F_ARCHIVE;
- } else if (strcmp(key, "archive-sha256") == 0) {
- if (strlen(val) != 2 * DIST_SHA256_LEN ||
- dist_hex_decode(m->archive_sha256, val, DIST_SHA256_LEN) != DIST_OK)
- return set_err(err, errcap, "bad archive-sha256");
- seen |= F_ARCH_SHA;
- } else if (strcmp(key, "archive-size") == 0) {
- if (parse_u64(val, &m->archive_size) != DIST_OK)
- return set_err(err, errcap, "bad archive-size");
- seen |= F_ARCH_SIZE;
+ } else if (strcmp(key, "hash") == 0) {
+ if (strcmp(val, DIST_MANIFEST_HASH) != 0)
+ return set_err(err, errcap, "unsupported hash algorithm");
+ seen |= F_HASH;
} else {
return set_err(err, errcap, "unknown top-level key");
}
} else if (sec == SEC_ART) {
- if (strcmp(key, "path") == 0) {
+ if (strcmp(key, "id") == 0) {
+ if (parse_u64(val, &art->id) != DIST_OK)
+ return set_err(err, errcap, "bad artifact id");
+ seen |= F_ID;
+ } else if (strcmp(key, "path") == 0) {
if (copy_field(art->path, sizeof art->path, val, err, errcap))
return DIST_ERR;
seen |= F_PATH;
@@ -255,20 +232,24 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
if (copy_field(art->target, sizeof art->target, val, err, errcap))
return DIST_ERR;
} else if (strcmp(key, "kind") == 0) {
- if (!kind_valid(val))
- return set_err(err, errcap, "unknown artifact kind");
+ if (!kind_valid(val)) return set_err(err, errcap, "unknown artifact kind");
if (copy_field(art->kind, sizeof art->kind, val, err, errcap))
return DIST_ERR;
seen |= F_KIND;
- } else if (strcmp(key, "sha256") == 0) {
- if (strlen(val) != 2 * DIST_SHA256_LEN ||
- dist_hex_decode(art->sha256, val, DIST_SHA256_LEN) != DIST_OK)
- return set_err(err, errcap, "bad artifact sha256");
- seen |= F_SHA;
} else if (strcmp(key, "size") == 0) {
if (parse_u64(val, &art->size) != DIST_OK)
return set_err(err, errcap, "bad artifact size");
seen |= F_SIZE;
+ } else if (strcmp(key, "blake2b") == 0) {
+ if (strlen(val) != 2 * DIST_BLAKE2B_LEN ||
+ dist_hex_decode(art->blake2b, val, DIST_BLAKE2B_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad artifact blake2b");
+ seen |= F_BLAKE2B;
+ } else if (strcmp(key, "root") == 0) {
+ if (strlen(val) != 2 * DIST_BLAKE2B_LEN ||
+ dist_hex_decode(art->root, val, DIST_BLAKE2B_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad artifact root");
+ seen |= F_ROOT;
} else if (strcmp(key, "entry") == 0) {
art->entry = (strcmp(val, "true") == 0);
if (!art->entry && strcmp(val, "false") != 0)
@@ -276,7 +257,7 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
} else {
return set_err(err, errcap, "unknown [artifact] key");
}
- } else { /* SEC_DEP */
+ } else {
if (strcmp(key, "name") == 0) {
if (copy_field(dep->name, sizeof dep->name, val, err, errcap))
return DIST_ERR;
@@ -285,11 +266,11 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
if (copy_field(dep->version, sizeof dep->version, val, err, errcap))
return DIST_ERR;
seen |= F_VERSION;
- } else if (strcmp(key, "sha256") == 0) {
- if (strlen(val) != 2 * DIST_SHA256_LEN ||
- dist_hex_decode(dep->sha256, val, DIST_SHA256_LEN) != DIST_OK)
- return set_err(err, errcap, "bad dependency sha256");
- dep->has_sha256 = 1;
+ } else if (strcmp(key, "blake2b") == 0) {
+ if (strlen(val) != 2 * DIST_BLAKE2B_LEN ||
+ dist_hex_decode(dep->blake2b, val, DIST_BLAKE2B_LEN) != DIST_OK)
+ return set_err(err, errcap, "bad dependency blake2b");
+ dep->has_blake2b = 1;
} else if (strcmp(key, "key") == 0) {
if (strlen(val) != 2 * DIST_KEYID_LEN ||
dist_hex_decode(dep->keyid, val, DIST_KEYID_LEN) != DIST_OK)
@@ -301,5 +282,17 @@ int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
}
}
- return finalize(sec, seen, err, errcap);
+ if (finalize(sec, seen, err, errcap) != DIST_OK) return DIST_ERR;
+ {
+ size_t i, j;
+ for (i = 0; i < m->n_artifacts; ++i) {
+ for (j = i + 1u; j < m->n_artifacts; ++j) {
+ if (m->artifacts[i].id == m->artifacts[j].id)
+ return set_err(err, errcap, "duplicate artifact id");
+ if (strcmp(m->artifacts[i].path, m->artifacts[j].path) == 0)
+ return set_err(err, errcap, "duplicate artifact path");
+ }
+ }
+ }
+ return DIST_OK;
}
diff --git a/driver/dist/manifest.h b/driver/dist/manifest.h
@@ -7,19 +7,20 @@
#include "dist.h"
-/* The signed root object. INI-style, line-oriented, byte-stable. See
- * doc/DISTRIBUTE.md. Emit and parse are real (no stub): the parser reads but
- * never reserializes, so the signed surface is always the literal bytes on
- * disk. There is deliberately no `created`/timestamp field — the manifest
- * stays reproducible; signing time lives in the signature's trusted comment. */
+/* The signed logical package object for distribution v2. The physical
+ * encodings (.tar.gz and .cfpkg) both carry these literal bytes and the same
+ * detached minisign signature. */
-#define DIST_MANIFEST_MAGIC "cfree-manifest 1"
+#define DIST_MANIFEST_MAGIC "cfree-package 2"
+#define DIST_MANIFEST_HASH "blake2b-merkle-v1"
typedef struct DistArtifact {
+ uint64_t id;
char path[DIST_PATH_MAX + 1];
char target[DIST_TRIPLE_MAX]; /* "" = target-independent */
char kind[DIST_KIND_MAX];
- uint8_t sha256[DIST_SHA256_LEN];
+ uint8_t blake2b[DIST_BLAKE2B_LEN];
+ uint8_t root[DIST_BLAKE2B_LEN];
uint64_t size;
int entry;
} DistArtifact;
@@ -27,8 +28,8 @@ typedef struct DistArtifact {
typedef struct DistDependency {
char name[DIST_NAME_MAX];
char version[DIST_PCONSTRAINT_MAX];
- uint8_t sha256[DIST_SHA256_LEN];
- int has_sha256;
+ uint8_t blake2b[DIST_BLAKE2B_LEN];
+ int has_blake2b;
uint8_t keyid[DIST_KEYID_LEN];
int has_keyid;
} DistDependency;
@@ -37,22 +38,14 @@ typedef struct DistManifest {
char name[DIST_NAME_MAX];
char version[DIST_VERSION_MAX];
char description[DIST_DESC_MAX]; /* "" = absent */
- char archive[DIST_PATH_MAX + 1];
- uint8_t archive_sha256[DIST_SHA256_LEN];
- uint64_t archive_size;
DistArtifact artifacts[DIST_MAX_ARTIFACTS];
size_t n_artifacts;
DistDependency deps[DIST_MAX_DEPS];
size_t n_deps;
} DistManifest;
-/* Serialize `m` as INI text to `out`. Returns DIST_OK / DIST_ERR. */
int dist_manifest_emit(const DistManifest* m, CfreeWriter* out);
-/* Strictly parse `data`/`len` into `m`. On error returns DIST_ERR and writes
- * a human-readable reason into `err` (capacity `errcap`). Rejects an unknown
- * magic/version, unknown keys, malformed values, and missing required
- * fields. */
int dist_manifest_parse(const uint8_t* data, size_t len, DistManifest* m,
char* err, size_t errcap);
diff --git a/driver/dist/sha256.c b/driver/dist/sha256.c
@@ -1,30 +0,0 @@
-#include "sha256.h"
-
-/* STUB digest. See sha256.h. A counter-driven FNV/xorshift sponge: enough
- * avalanche to make distinct inputs produce distinct digests in practice,
- * but cryptographically worthless. */
-
-#define DIST_STUB_FNV_OFFSET 0xcbf29ce484222325ULL
-#define DIST_STUB_FNV_PRIME 0x00000100000001b3ULL
-
-static uint64_t stub_absorb(const uint8_t* data, size_t len, uint64_t h) {
- size_t i;
- for (i = 0; i < len; ++i) {
- h ^= data[i];
- h *= DIST_STUB_FNV_PRIME;
- h ^= h >> 33;
- }
- return h;
-}
-
-void dist_sha256(uint8_t out[DIST_SHA256_LEN], const uint8_t* data,
- size_t len) {
- uint64_t base = stub_absorb(data, len, DIST_STUB_FNV_OFFSET);
- size_t i;
- for (i = 0; i < DIST_SHA256_LEN; ++i) {
- uint64_t h = base ^ (0x9e3779b97f4a7c15ULL * (uint64_t)(i + 1));
- h *= DIST_STUB_FNV_PRIME;
- h ^= h >> 29;
- out[i] = (uint8_t)(h >> 24);
- }
-}
diff --git a/driver/dist/sha256.h b/driver/dist/sha256.h
@@ -1,18 +0,0 @@
-#ifndef CFREE_DIST_SHA256_H
-#define CFREE_DIST_SHA256_H
-
-#include <stddef.h>
-#include <stdint.h>
-
-#include "dist.h"
-
-/* Content-addressing hash for the manifest and payload.
- *
- * STUB: this is a deterministic non-cryptographic digest so the packaging
- * pipeline can content-address and verify integrity end-to-end. It is NOT
- * SHA-256 and provides no collision resistance. To be replaced by the real
- * SHA-256 (already vendored at src/core/sha256.c, to be exposed to the
- * driver) before this subsystem is trusted. */
-void dist_sha256(uint8_t out[DIST_SHA256_LEN], const uint8_t* data, size_t len);
-
-#endif
diff --git a/driver/pkg.c b/driver/pkg.c
@@ -4,35 +4,41 @@
#include <stdio.h>
#include <string.h>
+#include "dist/blake2b.h"
+#include "dist/cfpkg.h"
#include "dist/deflate.h"
#include "dist/dist.h"
+#include "dist/lz4.h"
#include "dist/manifest.h"
#include "dist/minisig.h"
-#include "dist/sha256.h"
#include "dist/tar.h"
#include "dist/trust.h"
#include "driver.h"
#include "env.h"
-/* `cfree pkg` — basic code distribution: bundle a build into a signed,
- * self-describing `.cfpkg`, and verify/unpack it against a trusted key.
- *
- * The crypto and compression under driver/dist/ are STUBS for now (see
- * doc/DISTRIBUTE.md): the whole pipeline runs end-to-end, but the signatures
- * and content hashes are NOT yet secure. This tool wires the real flow so the
- * vendored primitives can drop in later without touching the orchestration. */
-
#define PKG_TOOL "pkg"
#define PKG_PATH_BUF 1024u
#define PKG_NAME_BUF 256u
-
-/* A `.cfpkg` bags these four members, all named "<name>-<version>.*". */
-#define PKG_SUFFIX_ARCHIVE ".tar.gz"
-#define PKG_SUFFIX_MANIFEST ".manifest"
-#define PKG_SUFFIX_SIG ".manifest.minisig"
-#define PKG_SUFFIX_PUBKEY ".pub"
-
-/* -------------------------------------------------------------- helpers --- */
+#define PKG_META_MANIFEST "cfree/package.manifest"
+#define PKG_META_SIG "cfree/package.manifest.minisig"
+#define PKG_META_PUB "cfree/package.pub"
+
+typedef enum PkgFormat { PKG_FMT_AUTO, PKG_FMT_CFPKG, PKG_FMT_TARGZ } PkgFormat;
+
+typedef struct PkgInputFile {
+ const char* src;
+ const char* path;
+ CfreeFileData fd;
+ int loaded;
+} PkgInputFile;
+
+typedef struct PkgVerified {
+ DistManifest manifest;
+ uint8_t package_id[DIST_BLAKE2B_LEN];
+ uint8_t keyid[DIST_KEYID_LEN];
+ uint8_t pk[DIST_ED25519_PK_LEN];
+ char trusted[DIST_TRUSTED_COMMENT_MAX];
+} PkgVerified;
static int pkg_write_file(const CfreeContext* ctx, const char* path,
const uint8_t* data, size_t len) {
@@ -50,15 +56,12 @@ static int pkg_write_file(const CfreeContext* ctx, const char* path,
return rc;
}
-/* Fresh in-memory writer; aborts callers on OOM via NULL return. */
static CfreeWriter* pkg_mem(const CfreeContext* ctx) {
CfreeWriter* w = NULL;
if (cfree_writer_mem(ctx->heap, &w) != CFREE_OK) return NULL;
return w;
}
-/* Default trusted-keys path: $CFREE_TRUSTED_KEYS, else
- * $HOME/.config/cfree/trusted_keys. Returns DIST_ERR if neither is set. */
static int pkg_trust_path(char* buf, size_t cap) {
const char* env = driver_getenv("CFREE_TRUSTED_KEYS");
const char* home;
@@ -72,14 +75,12 @@ static int pkg_trust_path(char* buf, size_t cap) {
return DIST_OK;
}
-/* Copy the parent directory of `path` into `buf` ("" if no slash). */
static void pkg_parent_dir(const char* path, char* buf, size_t cap) {
const char* slash = NULL;
const char* p;
size_t n;
- for (p = path; *p; ++p) {
+ for (p = path; *p; ++p)
if (*p == '/') slash = p;
- }
if (!slash) {
buf[0] = '\0';
return;
@@ -90,8 +91,6 @@ static void pkg_parent_dir(const char* path, char* buf, size_t cap) {
buf[n] = '\0';
}
-/* Read an entire file. Returns DIST_OK and a borrowed view (release with
- * ctx->file_io->release on the returned CfreeFileData). */
static int pkg_read_file(const CfreeContext* ctx, const char* path,
CfreeFileData* out) {
return ctx->file_io->read_all(ctx->file_io->user, path, out) == CFREE_OK
@@ -99,60 +98,124 @@ static int pkg_read_file(const CfreeContext* ctx, const char* path,
: DIST_ERR;
}
-/* Locate a bag member whose name ends with `suffix`. */
-static const DistTarEntry* pkg_find_suffix(const DistTarEntry* e, size_t n,
- const char* suffix) {
+static const DistTarEntry* pkg_find_name(const DistTarEntry* e, size_t n,
+ const char* name) {
size_t i;
- for (i = 0; i < n; ++i) {
- if (driver_has_suffix(e[i].name, suffix)) return &e[i];
- }
+ for (i = 0; i < n; ++i)
+ if (driver_streq(e[i].name, name)) return &e[i];
return NULL;
}
-static const DistTarEntry* pkg_find_name(const DistTarEntry* e, size_t n,
- const char* name) {
+static PkgFormat pkg_parse_format(const char* s) {
+ if (driver_streq(s, "cfpkg") || driver_streq(s, "native")) return PKG_FMT_CFPKG;
+ if (driver_streq(s, "tar.gz") || driver_streq(s, "portable"))
+ return PKG_FMT_TARGZ;
+ return PKG_FMT_AUTO;
+}
+
+static PkgFormat pkg_infer_format(const char* path) {
+ if (driver_has_suffix(path, ".tar.gz")) return PKG_FMT_TARGZ;
+ if (driver_has_suffix(path, ".cfpkg")) return PKG_FMT_CFPKG;
+ return PKG_FMT_AUTO;
+}
+
+static uint64_t pkg_align_up(uint64_t v, uint64_t a) {
+ return a ? ((v + a - 1u) / a) * a : v;
+}
+
+static int pkg_write_pad(CfreeWriter* w, uint64_t target) {
+ static const uint8_t z[64] = {0};
+ while (cfree_writer_tell(w) < target) {
+ uint64_t left = target - cfree_writer_tell(w);
+ size_t n = left < sizeof z ? (size_t)left : sizeof z;
+ if (cfree_writer_write(w, z, n) != CFREE_OK) return DIST_ERR;
+ }
+ return DIST_OK;
+}
+
+static void pkg_hash(uint8_t out[DIST_BLAKE2B_LEN], const uint8_t* data,
+ size_t len) {
+ dist_blake2b(out, data, len);
+}
+
+static void pkg_region_root(uint8_t out[DIST_BLAKE2B_LEN], const char* kind,
+ const uint8_t* data, size_t len) {
+ uint8_t h[DIST_BLAKE2B_LEN];
+ pkg_hash(h, data, len);
+ dist_cfpkg2_root_hash(out, kind, 0, h);
+}
+
+static int pkg_load_inputs(const CfreeContext* ctx, const char** files,
+ size_t n_files, PkgInputFile* in, DistManifest* m) {
size_t i;
- for (i = 0; i < n; ++i) {
- if (driver_streq(e[i].name, name)) return &e[i];
+ memset(m, 0, sizeof *m);
+ for (i = 0; i < n_files; ++i) {
+ DistArtifact* a;
+ in[i].src = files[i];
+ in[i].path = driver_basename(files[i]);
+ if (pkg_read_file(ctx, files[i], &in[i].fd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: cannot read file: %s", files[i]);
+ return DIST_ERR;
+ }
+ in[i].loaded = 1;
+ if (m->n_artifacts >= DIST_MAX_ARTIFACTS) {
+ driver_errf(PKG_TOOL, "create: too many artifacts");
+ return DIST_ERR;
+ }
+ a = &m->artifacts[m->n_artifacts++];
+ a->id = (uint64_t)i;
+ snprintf(a->path, sizeof a->path, "%s", in[i].path);
+ snprintf(a->kind, sizeof a->kind, "%s", "data");
+ a->size = in[i].fd.size;
+ pkg_hash(a->blake2b, in[i].fd.data, in[i].fd.size);
+ if (dist_cfpkg2_artifact_root(a->root, a->id, in[i].fd.data,
+ in[i].fd.size,
+ DIST_CFPKG_CHUNK_SIZE_DEFAULT) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: artifact too large for current chunk cap");
+ return DIST_ERR;
+ }
}
- return NULL;
+ return DIST_OK;
}
-/* ---------------------------------------------------------------- help --- */
+static void pkg_release_inputs(const CfreeContext* ctx, PkgInputFile* in,
+ size_t n) {
+ size_t i;
+ for (i = 0; i < n; ++i)
+ if (in[i].loaded) ctx->file_io->release(ctx->file_io->user, &in[i].fd);
+}
+
+static int pkg_sign(CfreeWriter* out, DriverEnv* env, const uint8_t* data,
+ size_t len, const DistKeypair* kp,
+ const uint8_t pkgid[DIST_BLAKE2B_LEN],
+ const char* what) {
+ char tcomment[DIST_TRUSTED_COMMENT_MAX];
+ char pkgid_hex[2 * DIST_BLAKE2B_LEN + 1];
+ dist_hex_encode(pkgid_hex, pkgid, DIST_BLAKE2B_LEN);
+ snprintf(tcomment, sizeof tcomment, "created=%lld pkgid=%s",
+ (long long)(env->now > 0 ? env->now : 0), pkgid_hex);
+ return dist_minisig_sign(out, data, len, kp->sk, kp->keyid, what, tcomment);
+}
void driver_help_pkg(void) {
driver_printf(
- "cfree pkg — basic signed code distribution\n"
+ "cfree pkg - signed code distribution\n"
"\n"
"USAGE\n"
" cfree pkg keygen -o BASE\n"
" cfree pkg create --name N --version V [--desc D] -s SECKEY\n"
- " -o OUT.cfpkg FILE...\n"
- " cfree pkg verify [-p PUBKEY | --tofu] FILE.cfpkg\n"
- " cfree pkg unpack FILE.cfpkg -C DIR\n"
- " cfree pkg inspect FILE.cfpkg\n"
- " cfree pkg trust {list | add PUBKEY [label] | remove KEYID}\n"
- "\n"
- "DESCRIPTION\n"
- " A .cfpkg bundles a signed manifest, its detached signature, the\n"
- " gzip payload archive, and the signer's public key. Verification\n"
- " anchors the signature against the trusted-keys store (or an\n"
- " explicit -p key); --tofu pins the bundled key on first use.\n"
- "\n"
- " NOTE: crypto and hashing are STUBBED in this build and provide no\n"
- " security yet (see doc/DISTRIBUTE.md).\n"
- "\n"
- "EXIT CODES\n"
- " 0 success 1 error 2 bad usage\n");
+ " [--format cfpkg|tar.gz] [--compression none|lz4-block-v1]\n"
+ " -o OUT FILE...\n"
+ " cfree pkg verify [-p PUBKEY | --tofu] [--format cfpkg|tar.gz] FILE\n"
+ " cfree pkg unpack FILE -C DIR\n"
+ " cfree pkg inspect FILE\n"
+ " cfree pkg trust {list | add PUBKEY [label] | remove KEYID}\n");
}
-/* -------------------------------------------------------------- keygen --- */
-
static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc,
char** argv) {
const char* base = NULL;
- uint8_t seed[DIST_ED25519_SEED_LEN];
- uint8_t keyid[DIST_KEYID_LEN];
+ uint8_t seed[DIST_ED25519_SEED_LEN], keyid[DIST_KEYID_LEN];
DistKeypair kp;
CfreeWriter* w;
char path[PKG_PATH_BUF];
@@ -160,11 +223,10 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc,
size_t len;
int i;
(void)env;
-
for (i = 0; i < argc; ++i) {
- if (driver_streq(argv[i], "-o") && i + 1 < argc) {
+ if (driver_streq(argv[i], "-o") && i + 1 < argc)
base = argv[++i];
- } else {
+ else {
driver_errf(PKG_TOOL, "keygen: unexpected argument: %s", argv[i]);
return 2;
}
@@ -173,29 +235,24 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc,
driver_errf(PKG_TOOL, "keygen: -o BASE is required");
return 2;
}
-
- /* Real entropy from the host CSPRNG: a random seed and a random key id. */
if (driver_random_bytes(seed, sizeof seed) != 0 ||
driver_random_bytes(keyid, sizeof keyid) != 0) {
driver_errf(PKG_TOOL, "keygen: failed to read system randomness");
return 1;
}
dist_minisig_keygen(&kp, seed, keyid);
-
w = pkg_mem(ctx);
if (!w || dist_minisig_emit_pubkey(w, &kp) != DIST_OK) return 1;
bytes = cfree_writer_mem_bytes(w, &len);
snprintf(path, sizeof path, "%s.pub", base);
if (pkg_write_file(ctx, path, bytes, len) != DIST_OK) return 1;
cfree_writer_close(w);
-
w = pkg_mem(ctx);
if (!w || dist_minisig_emit_seckey(w, &kp) != DIST_OK) return 1;
bytes = cfree_writer_mem_bytes(w, &len);
snprintf(path, sizeof path, "%s.key", base);
if (pkg_write_file(ctx, path, bytes, len) != DIST_OK) return 1;
cfree_writer_close(w);
-
{
char hex[2 * DIST_KEYID_LEN + 1];
dist_hex_encode(hex, kp.keyid, DIST_KEYID_LEN);
@@ -204,46 +261,243 @@ static int pkg_keygen(DriverEnv* env, const CfreeContext* ctx, int argc,
return 0;
}
-/* -------------------------------------------------------------- create --- */
+static int pkg_create_targz(const CfreeContext* ctx, const char* out,
+ PkgInputFile* in, size_t n_files,
+ const uint8_t* man, size_t man_len,
+ const uint8_t* sig, size_t sig_len,
+ const uint8_t* pub, size_t pub_len) {
+ CfreeWriter *tar = NULL, *gz = NULL;
+ const uint8_t *tb, *gb;
+ size_t tl, gl, i;
+ int rc = DIST_ERR;
+ tar = pkg_mem(ctx);
+ gz = pkg_mem(ctx);
+ if (!tar || !gz) goto done;
+ if (dist_tar_append(tar, PKG_META_MANIFEST, man, man_len) != DIST_OK ||
+ dist_tar_append(tar, PKG_META_SIG, sig, sig_len) != DIST_OK ||
+ dist_tar_append(tar, PKG_META_PUB, pub, pub_len) != DIST_OK)
+ goto done;
+ for (i = 0; i < n_files; ++i)
+ if (dist_tar_append(tar, in[i].path, in[i].fd.data, in[i].fd.size) !=
+ DIST_OK)
+ goto done;
+ if (dist_tar_finish(tar) != DIST_OK) goto done;
+ tb = cfree_writer_mem_bytes(tar, &tl);
+ if (dist_gz_compress(gz, tb, tl) != DIST_OK) goto done;
+ gb = cfree_writer_mem_bytes(gz, &gl);
+ rc = pkg_write_file(ctx, out, gb, gl);
+done:
+ if (gz) cfree_writer_close(gz);
+ if (tar) cfree_writer_close(tar);
+ return rc;
+}
+
+static int pkg_build_native_regions(const CfreeContext* ctx, PkgInputFile* in,
+ size_t n_files, uint32_t compression,
+ CfreeWriter** index_out,
+ CfreeWriter** content_out) {
+ CfreeWriter* index = pkg_mem(ctx);
+ CfreeWriter* content = pkg_mem(ctx);
+ size_t i;
+ if (!index || !content) return DIST_ERR;
+ for (i = 0; i < n_files; ++i) {
+ size_t off = 0, ci = 0;
+ if (in[i].fd.size == 0) continue;
+ while (off < in[i].fd.size) {
+ uint8_t recbuf[DIST_CFPKG_INDEX_RECORD_SIZE];
+ DistCfpkgIndexRecord r;
+ const uint8_t* raw = in[i].fd.data + off;
+ size_t raw_len = in[i].fd.size - off;
+ if (raw_len > DIST_CFPKG_CHUNK_SIZE_DEFAULT)
+ raw_len = DIST_CFPKG_CHUNK_SIZE_DEFAULT;
+ memset(&r, 0, sizeof r);
+ r.artifact_id = (uint64_t)i;
+ r.chunk_index = (uint64_t)ci;
+ r.content_offset = cfree_writer_tell(content);
+ r.raw_size = raw_len;
+ r.compression = compression;
+ pkg_hash(r.raw_hash, raw, raw_len);
+ dist_cfpkg2_leaf_hash(r.leaf_hash, r.artifact_id, r.chunk_index, raw,
+ raw_len);
+ if (compression == DIST_CFPKG_COMP_NONE) {
+ r.stored_size = raw_len;
+ pkg_hash(r.stored_hash, raw, raw_len);
+ if (cfree_writer_write(content, raw, raw_len) != CFREE_OK)
+ return DIST_ERR;
+ } else {
+ uint8_t tmp[DIST_CFPKG_CHUNK_SIZE_DEFAULT + 512u];
+ size_t stored_len = 0;
+ if (dist_lz4_compress_block(tmp, sizeof tmp, &stored_len, raw,
+ raw_len) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: lz4-block-v1 is stubbed");
+ return DIST_ERR;
+ }
+ r.stored_size = stored_len;
+ pkg_hash(r.stored_hash, tmp, stored_len);
+ if (cfree_writer_write(content, tmp, stored_len) != CFREE_OK)
+ return DIST_ERR;
+ }
+ dist_cfpkg_encode_index_record(recbuf, &r);
+ if (cfree_writer_write(index, recbuf, sizeof recbuf) != CFREE_OK)
+ return DIST_ERR;
+ off += raw_len;
+ ++ci;
+ }
+ }
+ *index_out = index;
+ *content_out = content;
+ return DIST_OK;
+}
+
+static int pkg_create_cfpkg(DriverEnv* env, const CfreeContext* ctx,
+ const char* out, const DistKeypair* kp,
+ PkgInputFile* in, size_t n_files,
+ const uint8_t* man, size_t man_len,
+ const uint8_t* sig, size_t sig_len,
+ const uint8_t* pub, size_t pub_len,
+ const uint8_t pkgid[DIST_BLAKE2B_LEN],
+ uint32_t compression) {
+ CfreeWriter *index = NULL, *content = NULL, *descw = NULL, *descsigw = NULL,
+ *pkg = NULL;
+ const uint8_t *index_b, *content_b, *desc_b = NULL, *descsig_b = NULL;
+ size_t index_l, content_l, desc_l = 0, descsig_l = 0;
+ uint8_t index_root[DIST_BLAKE2B_LEN], content_root[DIST_BLAKE2B_LEN];
+ DistCfpkgHeader h;
+ int stable = 0, iter, rc = DIST_ERR;
+
+ if (pkg_build_native_regions(ctx, in, n_files, compression, &index, &content) !=
+ DIST_OK)
+ goto done;
+ index_b = cfree_writer_mem_bytes(index, &index_l);
+ content_b = cfree_writer_mem_bytes(content, &content_l);
+ pkg_region_root(index_root, "index", index_b, index_l);
+ pkg_region_root(content_root, "content", content_b, content_l);
+
+ memset(&h, 0, sizeof h);
+ for (iter = 0; iter < 8; ++iter) {
+ DistCfpkgDescriptor d;
+ uint64_t old_desc_l = desc_l, old_descsig_l = descsig_l;
+ if (descw) cfree_writer_close(descw);
+ if (descsigw) cfree_writer_close(descsigw);
+ descw = pkg_mem(ctx);
+ descsigw = pkg_mem(ctx);
+ if (!descw || !descsigw) goto done;
+ h.manifest_offset = DIST_CFPKG_HEADER_SIZE;
+ h.manifest_size = man_len;
+ h.signature_offset = h.manifest_offset + h.manifest_size;
+ h.signature_size = sig_len;
+ h.descriptor_offset = h.signature_offset + h.signature_size;
+ h.descriptor_size = old_desc_l;
+ h.descriptor_signature_offset = h.descriptor_offset + h.descriptor_size;
+ h.descriptor_signature_size = old_descsig_l;
+ h.pubkey_offset =
+ h.descriptor_signature_offset + h.descriptor_signature_size;
+ h.pubkey_size = pub_len;
+ h.index_offset =
+ pkg_align_up(h.pubkey_offset + h.pubkey_size, DIST_CFPKG_ALIGNMENT);
+ h.index_size = index_l;
+ h.content_offset =
+ pkg_align_up(h.index_offset + h.index_size, DIST_CFPKG_ALIGNMENT);
+ h.content_size = content_l;
+ h.alignment = DIST_CFPKG_ALIGNMENT;
+ h.chunk_size = DIST_CFPKG_CHUNK_SIZE_DEFAULT;
+
+ memset(&d, 0, sizeof d);
+ memcpy(d.package_id, pkgid, DIST_BLAKE2B_LEN);
+ d.index_offset = h.index_offset;
+ d.index_size = h.index_size;
+ memcpy(d.index_root, index_root, DIST_BLAKE2B_LEN);
+ d.content_offset = h.content_offset;
+ d.content_size = h.content_size;
+ memcpy(d.content_root, content_root, DIST_BLAKE2B_LEN);
+ d.chunk_size = h.chunk_size;
+ d.alignment = h.alignment;
+ if (dist_cfpkg_descriptor_emit(descw, &d) != DIST_OK) goto done;
+ desc_b = cfree_writer_mem_bytes(descw, &desc_l);
+ if (pkg_sign(descsigw, env, desc_b, desc_l, kp, pkgid,
+ "cfree cfpkg encoding descriptor") != DIST_OK)
+ goto done;
+ descsig_b = cfree_writer_mem_bytes(descsigw, &descsig_l);
+ if (desc_l == old_desc_l && descsig_l == old_descsig_l) {
+ stable = 1;
+ break;
+ }
+ }
+ if (!stable) goto done;
+
+ pkg = pkg_mem(ctx);
+ if (!pkg) goto done;
+ if (dist_cfpkg_write_header(pkg, &h) != DIST_OK ||
+ cfree_writer_write(pkg, man, man_len) != CFREE_OK ||
+ cfree_writer_write(pkg, sig, sig_len) != CFREE_OK ||
+ cfree_writer_write(pkg, desc_b, desc_l) != CFREE_OK ||
+ cfree_writer_write(pkg, descsig_b, descsig_l) != CFREE_OK ||
+ cfree_writer_write(pkg, pub, pub_len) != CFREE_OK ||
+ pkg_write_pad(pkg, h.index_offset) != DIST_OK ||
+ cfree_writer_write(pkg, index_b, index_l) != CFREE_OK ||
+ pkg_write_pad(pkg, h.content_offset) != DIST_OK ||
+ cfree_writer_write(pkg, content_b, content_l) != CFREE_OK)
+ goto done;
+ {
+ const uint8_t* bytes;
+ size_t len;
+ bytes = cfree_writer_mem_bytes(pkg, &len);
+ rc = pkg_write_file(ctx, out, bytes, len);
+ }
+
+done:
+ if (pkg) cfree_writer_close(pkg);
+ if (descsigw) cfree_writer_close(descsigw);
+ if (descw) cfree_writer_close(descw);
+ if (content) cfree_writer_close(content);
+ if (index) cfree_writer_close(index);
+ return rc;
+}
static int pkg_create(DriverEnv* env, const CfreeContext* ctx, int argc,
char** argv) {
- const char* name = NULL;
- const char* version = NULL;
- const char* desc = NULL;
- const char* out = NULL;
- const char* seckey = NULL;
+ const char *name = NULL, *version = NULL, *desc = NULL, *out = NULL,
+ *seckey = NULL;
const char* files[DIST_MAX_FILES];
+ PkgInputFile inputs[DIST_MAX_FILES];
size_t n_files = 0;
- int i, rc = 1;
-
- DistManifest m;
+ int i, rc = 1, sk_loaded = 0;
+ PkgFormat fmt = PKG_FMT_AUTO;
+ uint32_t compression = DIST_CFPKG_COMP_NONE;
DistKeypair kp;
- CfreeWriter *payload = NULL, *gz = NULL, *manw = NULL, *sigw = NULL,
- *pubw = NULL, *bag = NULL;
CfreeFileData skfd = {0};
- int sk_loaded = 0;
- const uint8_t *gz_bytes, *man_bytes, *sig_bytes, *pub_bytes, *bag_bytes;
- size_t gz_len, man_len, sig_len, pub_len, bag_len;
- char innerbase[PKG_NAME_BUF];
- char arc_name[PKG_NAME_BUF], man_name[PKG_NAME_BUF], sig_name[PKG_NAME_BUF],
- pub_name[PKG_NAME_BUF];
- char tcomment[DIST_TRUSTED_COMMENT_MAX];
- char pkgid_hex[2 * DIST_SHA256_LEN + 1];
- uint8_t pkgid[DIST_SHA256_LEN];
+ DistManifest m;
+ CfreeWriter *manw = NULL, *sigw = NULL, *pubw = NULL;
+ const uint8_t *man_b, *sig_b, *pub_b;
+ size_t man_l, sig_l, pub_l;
+ uint8_t pkgid[DIST_BLAKE2B_LEN];
+ char pkgid_hex[2 * DIST_BLAKE2B_LEN + 1];
+ memset(inputs, 0, sizeof inputs);
for (i = 0; i < argc; ++i) {
const char* a = argv[i];
- if (driver_streq(a, "--name") && i + 1 < argc) {
+ if (driver_streq(a, "--name") && i + 1 < argc)
name = argv[++i];
- } else if (driver_streq(a, "--version") && i + 1 < argc) {
+ else if (driver_streq(a, "--version") && i + 1 < argc)
version = argv[++i];
- } else if (driver_streq(a, "--desc") && i + 1 < argc) {
+ else if (driver_streq(a, "--desc") && i + 1 < argc)
desc = argv[++i];
- } else if (driver_streq(a, "-o") && i + 1 < argc) {
+ else if (driver_streq(a, "-o") && i + 1 < argc)
out = argv[++i];
- } else if (driver_streq(a, "-s") && i + 1 < argc) {
+ else if (driver_streq(a, "-s") && i + 1 < argc)
seckey = argv[++i];
+ else if (driver_streq(a, "--format") && i + 1 < argc) {
+ fmt = pkg_parse_format(argv[++i]);
+ if (fmt == PKG_FMT_AUTO) {
+ driver_errf(PKG_TOOL, "create: unknown format");
+ return 2;
+ }
+ }
+ else if (driver_streq(a, "--compression") && i + 1 < argc) {
+ if (dist_cfpkg_compression_parse(argv[++i], &compression) != DIST_OK) {
+ driver_errf(PKG_TOOL, "create: unknown compression");
+ return 2;
+ }
} else if (a[0] == '-' && a[1] != '\0') {
driver_errf(PKG_TOOL, "create: unknown option: %s", a);
return 2;
@@ -260,157 +514,78 @@ static int pkg_create(DriverEnv* env, const CfreeContext* ctx, int argc,
"create: --name, --version, -s SECKEY and -o OUT are required");
return 2;
}
+ if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(out);
+ if (fmt == PKG_FMT_AUTO) {
+ driver_errf(PKG_TOOL, "create: cannot infer format; pass --format");
+ return 2;
+ }
- /* Load the signing key. */
if (pkg_read_file(ctx, seckey, &skfd) != DIST_OK) {
driver_errf(PKG_TOOL, "create: cannot read secret key: %s", seckey);
- return 1;
+ goto done;
}
sk_loaded = 1;
{
int kr = dist_minisig_parse_seckey(skfd.data, skfd.size, kp.sk, kp.keyid);
- if (kr == DIST_ENCRYPTED) {
- driver_errf(PKG_TOOL,
- "create: encrypted secret keys are not supported yet "
- "(needs scrypt); use a passwordless key");
- goto done;
- }
- if (kr != DIST_OK) {
- driver_errf(PKG_TOOL, "create: malformed secret key");
- goto done;
- }
+ if (kr == DIST_ENCRYPTED)
+ driver_errf(PKG_TOOL, "create: encrypted secret keys need scrypt");
+ if (kr != DIST_OK) goto done;
}
- /* The Ed25519 secret key carries the public key in its tail half. */
memcpy(kp.pk, kp.sk + DIST_ED25519_SEED_LEN, DIST_ED25519_PK_LEN);
- memset(&m, 0, sizeof m);
+ if (pkg_load_inputs(ctx, files, n_files, inputs, &m) != DIST_OK) goto done;
snprintf(m.name, sizeof m.name, "%s", name);
snprintf(m.version, sizeof m.version, "%s", version);
if (desc) snprintf(m.description, sizeof m.description, "%s", desc);
- /* Build the payload tar: one member + one artifact per input file. */
- payload = pkg_mem(ctx);
- if (!payload) goto done;
- for (i = 0; i < (int)n_files; ++i) {
- CfreeFileData fd = {0};
- const char* base = driver_basename(files[i]);
- DistArtifact* a;
- if (pkg_read_file(ctx, files[i], &fd) != DIST_OK) {
- driver_errf(PKG_TOOL, "create: cannot read file: %s", files[i]);
- goto done;
- }
- if (m.n_artifacts >= DIST_MAX_ARTIFACTS) {
- driver_errf(PKG_TOOL, "create: too many artifacts");
- ctx->file_io->release(ctx->file_io->user, &fd);
- goto done;
- }
- if (dist_tar_append(payload, base, fd.data, fd.size) != DIST_OK) {
- driver_errf(PKG_TOOL, "create: cannot archive '%s' (name too long?)",
- base);
- ctx->file_io->release(ctx->file_io->user, &fd);
- goto done;
- }
- a = &m.artifacts[m.n_artifacts++];
- snprintf(a->path, sizeof a->path, "%s", base);
- snprintf(a->kind, sizeof a->kind, "%s", "data");
- a->size = fd.size;
- dist_sha256(a->sha256, fd.data, fd.size);
- ctx->file_io->release(ctx->file_io->user, &fd);
- }
- if (dist_tar_finish(payload) != DIST_OK) goto done;
-
- /* gzip the payload, then hash the gzip bytes for the manifest. */
- {
- const uint8_t* pb;
- size_t pl;
- pb = cfree_writer_mem_bytes(payload, &pl);
- gz = pkg_mem(ctx);
- if (!gz || dist_gz_compress(gz, pb, pl) != DIST_OK) goto done;
- }
- gz_bytes = cfree_writer_mem_bytes(gz, &gz_len);
-
- snprintf(innerbase, sizeof innerbase, "%s-%s", name, version);
- snprintf(arc_name, sizeof arc_name, "%s%s", innerbase, PKG_SUFFIX_ARCHIVE);
- snprintf(man_name, sizeof man_name, "%s%s", innerbase, PKG_SUFFIX_MANIFEST);
- snprintf(sig_name, sizeof sig_name, "%s%s", innerbase, PKG_SUFFIX_SIG);
- snprintf(pub_name, sizeof pub_name, "%s%s", innerbase, PKG_SUFFIX_PUBKEY);
-
- snprintf(m.archive, sizeof m.archive, "%s", arc_name);
- m.archive_size = gz_len;
- dist_sha256(m.archive_sha256, gz_bytes, gz_len);
-
- /* Serialize the manifest; it is the signed object. */
manw = pkg_mem(ctx);
- if (!manw || dist_manifest_emit(&m, manw) != DIST_OK) goto done;
- man_bytes = cfree_writer_mem_bytes(manw, &man_len);
-
- /* Sign the manifest. Signing time + package id go in the trusted comment,
- * keeping the manifest itself reproducible. */
- dist_sha256(pkgid, man_bytes, man_len);
- dist_hex_encode(pkgid_hex, pkgid, DIST_SHA256_LEN);
- snprintf(tcomment, sizeof tcomment, "created=%lld pkgid=%s",
- (long long)(env->now > 0 ? env->now : 0), pkgid_hex);
-
sigw = pkg_mem(ctx);
- if (!sigw ||
- dist_minisig_sign(sigw, man_bytes, man_len, kp.sk, kp.keyid,
- "signature from cfree pkg", tcomment) != DIST_OK) {
- goto done;
- }
- sig_bytes = cfree_writer_mem_bytes(sigw, &sig_len);
-
- /* The signer's public key travels with the bag (untrusted; TOFU may pin). */
pubw = pkg_mem(ctx);
- if (!pubw || dist_minisig_emit_pubkey(pubw, &kp) != DIST_OK) goto done;
- pub_bytes = cfree_writer_mem_bytes(pubw, &pub_len);
-
- /* Bag the four members into the (uncompressed, unsigned) .cfpkg. */
- bag = pkg_mem(ctx);
- if (!bag) goto done;
- if (dist_tar_append(bag, arc_name, gz_bytes, gz_len) != DIST_OK ||
- dist_tar_append(bag, man_name, man_bytes, man_len) != DIST_OK ||
- dist_tar_append(bag, sig_name, sig_bytes, sig_len) != DIST_OK ||
- dist_tar_append(bag, pub_name, pub_bytes, pub_len) != DIST_OK ||
- dist_tar_finish(bag) != DIST_OK) {
- driver_errf(PKG_TOOL, "create: failed to build bundle");
+ if (!manw || !sigw || !pubw) goto done;
+ if (dist_manifest_emit(&m, manw) != DIST_OK) goto done;
+ man_b = cfree_writer_mem_bytes(manw, &man_l);
+ pkg_hash(pkgid, man_b, man_l);
+ dist_hex_encode(pkgid_hex, pkgid, DIST_BLAKE2B_LEN);
+ if (pkg_sign(sigw, env, man_b, man_l, &kp, pkgid,
+ "signature from cfree pkg") != DIST_OK)
goto done;
- }
- bag_bytes = cfree_writer_mem_bytes(bag, &bag_len);
-
- if (pkg_write_file(ctx, out, bag_bytes, bag_len) != DIST_OK) goto done;
- driver_printf("wrote %s (%llu bytes, %llu artifact(s), id %s)\n", out,
- (unsigned long long)bag_len, (unsigned long long)m.n_artifacts,
- pkgid_hex);
- rc = 0;
+ sig_b = cfree_writer_mem_bytes(sigw, &sig_l);
+ if (dist_minisig_emit_pubkey(pubw, &kp) != DIST_OK) goto done;
+ pub_b = cfree_writer_mem_bytes(pubw, &pub_l);
+
+ if (fmt == PKG_FMT_TARGZ)
+ rc = pkg_create_targz(ctx, out, inputs, n_files, man_b, man_l, sig_b, sig_l,
+ pub_b, pub_l) == DIST_OK
+ ? 0
+ : 1;
+ else
+ rc = pkg_create_cfpkg(env, ctx, out, &kp, inputs, n_files, man_b, man_l,
+ sig_b, sig_l, pub_b, pub_l, pkgid,
+ compression) == DIST_OK
+ ? 0
+ : 1;
+ if (rc == 0)
+ driver_printf("wrote %s (%llu artifact(s), id %s)\n", out,
+ (unsigned long long)n_files, pkgid_hex);
done:
- if (bag) cfree_writer_close(bag);
if (pubw) cfree_writer_close(pubw);
if (sigw) cfree_writer_close(sigw);
if (manw) cfree_writer_close(manw);
- if (gz) cfree_writer_close(gz);
- if (payload) cfree_writer_close(payload);
+ pkg_release_inputs(ctx, inputs, n_files);
if (sk_loaded) ctx->file_io->release(ctx->file_io->user, &skfd);
return rc;
}
-/* ----------------------------------------------------- verify internals --- */
-
-/* Resolve the public key for a signature's key id. Strategy:
- * -p PUBKEY : use that key file, ignore the store.
- * otherwise : look the key id up in the trusted-keys store.
- * --tofu : on a store miss, pin the bag's bundled public key.
- * Writes the resolved key to `pk`. Returns DIST_OK on success. */
static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx,
const uint8_t keyid[DIST_KEYID_LEN],
- const DistTarEntry* pubmember,
+ const uint8_t* bundled_pub, size_t bundled_pub_size,
const char* pubkey_opt, int tofu,
uint8_t pk[DIST_ED25519_PK_LEN]) {
char tpath[PKG_PATH_BUF];
CfreeFileData store = {0};
int have_store;
uint8_t kid_chk[DIST_KEYID_LEN];
-
if (pubkey_opt) {
CfreeFileData fd = {0};
int ok;
@@ -420,17 +595,12 @@ static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx,
}
ok = dist_minisig_parse_pubkey(fd.data, fd.size, pk, kid_chk);
ctx->file_io->release(ctx->file_io->user, &fd);
- if (ok != DIST_OK) {
- driver_errf(PKG_TOOL, "malformed public key: %s", pubkey_opt);
- return DIST_ERR;
- }
- if (memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) {
+ if (ok != DIST_OK || memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) {
driver_errf(PKG_TOOL, "public key id does not match signature");
return DIST_ERR;
}
return DIST_OK;
}
-
if (pkg_trust_path(tpath, sizeof tpath) != DIST_OK) {
driver_errf(PKG_TOOL,
"no trusted-keys path (set CFREE_TRUSTED_KEYS or HOME)");
@@ -439,52 +609,33 @@ static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx,
have_store = (pkg_read_file(ctx, tpath, &store) == DIST_OK);
if (have_store &&
dist_trust_lookup(store.data, store.size, keyid, pk) == DIST_OK) {
- if (have_store) ctx->file_io->release(ctx->file_io->user, &store);
+ ctx->file_io->release(ctx->file_io->user, &store);
return DIST_OK;
}
if (have_store) ctx->file_io->release(ctx->file_io->user, &store);
-
if (!tofu) {
char hex[2 * DIST_KEYID_LEN + 1];
dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
- driver_errf(PKG_TOOL,
- "untrusted signer (key id %s): add it with `cfree pkg trust "
- "add` or pass --tofu",
- hex);
+ driver_errf(PKG_TOOL, "untrusted signer (key id %s)", hex);
return DIST_ERR;
}
-
- /* TOFU: pin the bundled public key. */
- if (!pubmember) {
- driver_errf(PKG_TOOL, "--tofu: bundle carries no public key");
- return DIST_ERR;
- }
- if (dist_minisig_parse_pubkey(pubmember->data, pubmember->size, pk,
- kid_chk) != DIST_OK) {
- driver_errf(PKG_TOOL, "--tofu: malformed bundled public key");
- return DIST_ERR;
- }
- if (memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) {
- driver_errf(PKG_TOOL, "--tofu: bundled key id does not match signature");
+ if (!bundled_pub || bundled_pub_size == 0 ||
+ dist_minisig_parse_pubkey(bundled_pub, bundled_pub_size, pk, kid_chk) !=
+ DIST_OK ||
+ memcmp(kid_chk, keyid, DIST_KEYID_LEN) != 0) {
+ driver_errf(PKG_TOOL, "--tofu: bundled public key is missing or mismatched");
return DIST_ERR;
}
{
- char line[DIST_TRUST_LINE_MAX];
- char parent[PKG_PATH_BUF];
- char label[PKG_NAME_BUF];
+ char line[DIST_TRUST_LINE_MAX], parent[PKG_PATH_BUF], hex[17];
CfreeFileData old = {0};
CfreeWriter* w = NULL;
int had_old = (pkg_read_file(ctx, tpath, &old) == DIST_OK);
- char hex[2 * DIST_KEYID_LEN + 1];
int rc = DIST_ERR;
-
dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
- snprintf(label, sizeof label, "tofu-pinned");
- if (dist_trust_format_entry(line, sizeof line, keyid, pk, label) !=
- DIST_OK) {
- if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
+ if (dist_trust_format_entry(line, sizeof line, keyid, pk, "tofu-pinned") !=
+ DIST_OK)
return DIST_ERR;
- }
pkg_parent_dir(tpath, parent, sizeof parent);
if (parent[0]) driver_mkdir_p(env, parent);
if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) == CFREE_OK) {
@@ -498,273 +649,450 @@ static int pkg_resolve_key(DriverEnv* env, const CfreeContext* ctx,
if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
if (rc == DIST_OK)
driver_printf("pkg: tofu-pinned key id %s to %s\n", hex, tpath);
- else
- driver_errf(PKG_TOOL, "failed to pin key to %s", tpath);
return rc;
}
}
-/* Verify signature + hashes for a parsed bundle. On success fills `m` (parsed
- * manifest) and `archive` (the payload member). Returns DIST_OK. */
-static int pkg_verify_bundle(DriverEnv* env, const CfreeContext* ctx,
- const DistTarEntry* e, size_t ne, DistManifest* m,
- const DistTarEntry** archive_out,
- const char* pubkey_opt, int tofu, int quiet) {
- const DistTarEntry *man, *sig, *pub, *archive;
- char err[128];
- uint8_t keyid[DIST_KEYID_LEN];
- uint8_t pk[DIST_ED25519_PK_LEN];
- char trusted[DIST_TRUSTED_COMMENT_MAX];
- uint8_t arc_hash[DIST_SHA256_LEN];
- char pkgid_hex[2 * DIST_SHA256_LEN + 1];
- uint8_t pkgid[DIST_SHA256_LEN];
+static int pkg_verify_manifest(DriverEnv* env, const CfreeContext* ctx,
+ const uint8_t* man, size_t man_len,
+ const uint8_t* sig, size_t sig_len,
+ const uint8_t* pub, size_t pub_len,
+ const char* pubkey_opt, int tofu,
+ PkgVerified* out) {
+ char err[128], pkgid_hex[2 * DIST_BLAKE2B_LEN + 1];
const char* pidp;
-
- man = pkg_find_suffix(e, ne, PKG_SUFFIX_MANIFEST);
- sig = pkg_find_suffix(e, ne, PKG_SUFFIX_SIG);
- pub = pkg_find_suffix(e, ne, PKG_SUFFIX_PUBKEY);
- if (!man || !sig) {
- driver_errf(PKG_TOOL, "bundle missing manifest or signature");
- return DIST_ERR;
- }
-
- if (dist_manifest_parse(man->data, man->size, m, err, sizeof err) !=
- DIST_OK) {
- driver_errf(PKG_TOOL, "manifest: %s", err);
- return DIST_ERR;
- }
-
- if (dist_minisig_sig_keyid(sig->data, sig->size, keyid) != DIST_OK) {
+ if (dist_minisig_sig_keyid(sig, sig_len, out->keyid) != DIST_OK) {
driver_errf(PKG_TOOL, "malformed signature");
return DIST_ERR;
}
- if (pkg_resolve_key(env, ctx, keyid, pub, pubkey_opt, tofu, pk) != DIST_OK)
+ if (pkg_resolve_key(env, ctx, out->keyid, pub, pub_len, pubkey_opt, tofu,
+ out->pk) != DIST_OK)
return DIST_ERR;
-
- if (dist_minisig_verify(sig->data, sig->size, man->data, man->size, pk,
- trusted, sizeof trusted) != DIST_OK) {
+ if (dist_minisig_verify(sig, sig_len, man, man_len, out->pk, out->trusted,
+ sizeof out->trusted) != DIST_OK) {
driver_errf(PKG_TOOL, "signature verification FAILED");
return DIST_ERR;
}
-
- /* The signed trusted comment must vouch for this manifest's id. */
- dist_sha256(pkgid, man->data, man->size);
- dist_hex_encode(pkgid_hex, pkgid, DIST_SHA256_LEN);
- pidp = strstr(trusted, "pkgid=");
- if (!pidp || strncmp(pidp + 6, pkgid_hex, 2 * DIST_SHA256_LEN) != 0) {
+ pkg_hash(out->package_id, man, man_len);
+ dist_hex_encode(pkgid_hex, out->package_id, DIST_BLAKE2B_LEN);
+ pidp = strstr(out->trusted, "pkgid=");
+ if (!pidp || strncmp(pidp + 6, pkgid_hex, 2 * DIST_BLAKE2B_LEN) != 0) {
driver_errf(PKG_TOOL, "trusted comment does not match package id");
return DIST_ERR;
}
+ if (dist_manifest_parse(man, man_len, &out->manifest, err, sizeof err) !=
+ DIST_OK) {
+ driver_errf(PKG_TOOL, "manifest: %s", err);
+ return DIST_ERR;
+ }
+ return DIST_OK;
+}
+
+static const DistArtifact* pkg_find_artifact(const DistManifest* m,
+ const char* path) {
+ size_t i;
+ for (i = 0; i < m->n_artifacts; ++i)
+ if (driver_streq(m->artifacts[i].path, path)) return &m->artifacts[i];
+ return NULL;
+}
- /* Archive integrity (checked before any decompression downstream). */
- archive = pkg_find_name(e, ne, m->archive);
- if (!archive) {
- driver_errf(PKG_TOOL, "bundle missing archive: %s", m->archive);
+static int pkg_verify_artifact_bytes(const DistArtifact* a, const uint8_t* data,
+ size_t len) {
+ uint8_t h[DIST_BLAKE2B_LEN], root[DIST_BLAKE2B_LEN];
+ if (len != a->size) return DIST_ERR;
+ pkg_hash(h, data, len);
+ if (memcmp(h, a->blake2b, DIST_BLAKE2B_LEN) != 0) return DIST_ERR;
+ if (dist_cfpkg2_artifact_root(root, a->id, data, len,
+ DIST_CFPKG_CHUNK_SIZE_DEFAULT) != DIST_OK)
+ return DIST_ERR;
+ return memcmp(root, a->root, DIST_BLAKE2B_LEN) == 0 ? DIST_OK : DIST_ERR;
+}
+
+static int pkg_load_portable(const CfreeContext* ctx, const char* file,
+ CfreeFileData* fd, CfreeWriter** inflated_out,
+ DistTarEntry* entries, size_t* ne) {
+ CfreeWriter* inflated = NULL;
+ const uint8_t* bytes;
+ size_t len;
+ if (pkg_read_file(ctx, file, fd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "cannot read package: %s", file);
return DIST_ERR;
}
- if (archive->size != m->archive_size) {
- driver_errf(PKG_TOOL, "archive size mismatch");
+ inflated = pkg_mem(ctx);
+ if (!inflated || dist_gz_decompress(inflated, fd->data, fd->size) != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed portable package");
return DIST_ERR;
}
- dist_sha256(arc_hash, archive->data, archive->size);
- if (memcmp(arc_hash, m->archive_sha256, DIST_SHA256_LEN) != 0) {
- driver_errf(PKG_TOOL, "archive sha256 mismatch");
+ bytes = cfree_writer_mem_bytes(inflated, &len);
+ if (dist_tar_iter(bytes, len, entries, DIST_MAX_FILES, ne) != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed portable tar");
return DIST_ERR;
}
+ *inflated_out = inflated;
+ return DIST_OK;
+}
+static int pkg_verify_portable(DriverEnv* env, const CfreeContext* ctx,
+ const char* file, const char* pubkey, int tofu,
+ const char* out_dir, int quiet) {
+ CfreeFileData fd = {0};
+ CfreeWriter* inflated = NULL;
+ DistTarEntry entries[DIST_MAX_FILES];
+ size_t ne = 0, i, seen = 0;
+ const DistTarEntry *man, *sig, *pub;
+ PkgVerified v;
+ int rc = DIST_ERR;
+ if (pkg_load_portable(ctx, file, &fd, &inflated, entries, &ne) != DIST_OK)
+ goto done;
+ man = pkg_find_name(entries, ne, PKG_META_MANIFEST);
+ sig = pkg_find_name(entries, ne, PKG_META_SIG);
+ pub = pkg_find_name(entries, ne, PKG_META_PUB);
+ if (!man || !sig) {
+ driver_errf(PKG_TOOL, "package missing manifest or signature");
+ goto done;
+ }
+ if (pkg_verify_manifest(env, ctx, man->data, man->size, sig->data, sig->size,
+ pub ? pub->data : NULL, pub ? pub->size : 0, pubkey,
+ tofu, &v) != DIST_OK)
+ goto done;
+ for (i = 0; i < ne; ++i) {
+ const DistArtifact* a;
+ if (driver_streq(entries[i].name, PKG_META_MANIFEST) ||
+ driver_streq(entries[i].name, PKG_META_SIG) ||
+ driver_streq(entries[i].name, PKG_META_PUB))
+ continue;
+ a = pkg_find_artifact(&v.manifest, entries[i].name);
+ if (!a) {
+ driver_errf(PKG_TOOL, "portable member not in manifest: %s",
+ entries[i].name);
+ goto done;
+ }
+ if (pkg_verify_artifact_bytes(a, entries[i].data, entries[i].size) !=
+ DIST_OK) {
+ driver_errf(PKG_TOOL, "artifact hash mismatch: %s", entries[i].name);
+ goto done;
+ }
+ ++seen;
+ if (out_dir) {
+ char full[PKG_PATH_BUF], parent[PKG_PATH_BUF];
+ snprintf(full, sizeof full, "%s/%s", out_dir, entries[i].name);
+ pkg_parent_dir(full, parent, sizeof parent);
+ if (parent[0]) driver_mkdir_p(env, parent);
+ if (pkg_write_file(ctx, full, entries[i].data, entries[i].size) !=
+ DIST_OK)
+ goto done;
+ driver_printf(" extracted %s\n", full);
+ }
+ }
+ if (seen != v.manifest.n_artifacts) {
+ driver_errf(PKG_TOOL, "portable package is missing artifacts");
+ goto done;
+ }
if (!quiet) {
char idhex[2 * DIST_KEYID_LEN + 1];
- dist_hex_encode(idhex, keyid, DIST_KEYID_LEN);
- driver_printf("ok: %s %s signer %s [%s]\n", m->name, m->version, idhex,
- trusted);
+ dist_hex_encode(idhex, v.keyid, DIST_KEYID_LEN);
+ driver_printf("ok: %s %s signer %s [%s]\n", v.manifest.name,
+ v.manifest.version, idhex, v.trusted);
}
- *archive_out = archive;
- return DIST_OK;
+ if (out_dir)
+ driver_printf("unpacked %s %s to %s\n", v.manifest.name,
+ v.manifest.version, out_dir);
+ rc = DIST_OK;
+done:
+ if (inflated) cfree_writer_close(inflated);
+ if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
+ return rc;
}
-/* ------------------------------------------------------ verify / unpack --- */
+static int pkg_bounds(const DistCfpkgHeader* h, size_t len) {
+ uint64_t ranges[][2] = {{h->manifest_offset, h->manifest_size},
+ {h->signature_offset, h->signature_size},
+ {h->descriptor_offset, h->descriptor_size},
+ {h->descriptor_signature_offset,
+ h->descriptor_signature_size},
+ {h->pubkey_offset, h->pubkey_size},
+ {h->index_offset, h->index_size},
+ {h->content_offset, h->content_size}};
+ size_t i;
+ for (i = 0; i < sizeof ranges / sizeof ranges[0]; ++i)
+ if (ranges[i][0] > len || ranges[i][1] > len - ranges[i][0])
+ return DIST_ERR;
+ return DIST_OK;
+}
-static int pkg_load_bundle(const CfreeContext* ctx, const char* path,
- CfreeFileData* fd, DistTarEntry* entries, size_t cap,
- size_t* ne) {
- if (pkg_read_file(ctx, path, fd) != DIST_OK) {
- driver_errf(PKG_TOOL, "cannot read bundle: %s", path);
- return DIST_ERR;
- }
- if (dist_tar_iter(fd->data, fd->size, entries, cap, ne) != DIST_OK) {
- driver_errf(PKG_TOOL, "malformed bundle: %s", path);
- return DIST_ERR;
+static int pkg_verify_native_content(DriverEnv* env, const CfreeContext* ctx,
+ const uint8_t* data,
+ const DistCfpkgHeader* h,
+ const PkgVerified* v,
+ const char* out_dir) {
+ size_t ai;
+ for (ai = 0; ai < v->manifest.n_artifacts; ++ai) {
+ const DistArtifact* a = &v->manifest.artifacts[ai];
+ CfreeWriter* raww = pkg_mem(ctx);
+ const uint8_t* rawb;
+ size_t rawl;
+ uint64_t want_chunk = 0;
+ size_t off;
+ if (!raww) return DIST_ERR;
+ for (off = 0; off < h->index_size; off += DIST_CFPKG_INDEX_RECORD_SIZE) {
+ DistCfpkgIndexRecord r;
+ const uint8_t* stored;
+ uint8_t sh[DIST_BLAKE2B_LEN], rh[DIST_BLAKE2B_LEN],
+ leaf[DIST_BLAKE2B_LEN];
+ if (dist_cfpkg_decode_index_record(data + h->index_offset + off,
+ DIST_CFPKG_INDEX_RECORD_SIZE,
+ &r) != DIST_OK)
+ return DIST_ERR;
+ if (r.artifact_id != a->id) continue;
+ if (r.chunk_index != want_chunk++) {
+ driver_errf(PKG_TOOL, "native index chunks out of order");
+ return DIST_ERR;
+ }
+ if (r.content_offset > h->content_size ||
+ r.stored_size > h->content_size - r.content_offset)
+ return DIST_ERR;
+ stored = data + h->content_offset + r.content_offset;
+ pkg_hash(sh, stored, (size_t)r.stored_size);
+ if (memcmp(sh, r.stored_hash, DIST_BLAKE2B_LEN) != 0) return DIST_ERR;
+ if (r.compression == DIST_CFPKG_COMP_NONE) {
+ pkg_hash(rh, stored, (size_t)r.stored_size);
+ dist_cfpkg2_leaf_hash(leaf, r.artifact_id, r.chunk_index, stored,
+ (size_t)r.stored_size);
+ if (r.raw_size != r.stored_size ||
+ memcmp(rh, r.raw_hash, DIST_BLAKE2B_LEN) != 0 ||
+ memcmp(leaf, r.leaf_hash, DIST_BLAKE2B_LEN) != 0 ||
+ cfree_writer_write(raww, stored, (size_t)r.stored_size) != CFREE_OK)
+ return DIST_ERR;
+ } else if (r.compression == DIST_CFPKG_COMP_LZ4_BLOCK_V1) {
+ uint8_t tmp[DIST_CFPKG_CHUNK_SIZE_DEFAULT];
+ if (r.raw_size > sizeof tmp ||
+ dist_lz4_decompress_block(tmp, (size_t)r.raw_size, stored,
+ (size_t)r.stored_size) != DIST_OK)
+ return DIST_ERR;
+ pkg_hash(rh, tmp, (size_t)r.raw_size);
+ dist_cfpkg2_leaf_hash(leaf, r.artifact_id, r.chunk_index, tmp,
+ (size_t)r.raw_size);
+ if (memcmp(rh, r.raw_hash, DIST_BLAKE2B_LEN) != 0 ||
+ memcmp(leaf, r.leaf_hash, DIST_BLAKE2B_LEN) != 0 ||
+ cfree_writer_write(raww, tmp, (size_t)r.raw_size) != CFREE_OK)
+ return DIST_ERR;
+ } else {
+ return DIST_ERR;
+ }
+ }
+ rawb = cfree_writer_mem_bytes(raww, &rawl);
+ if (pkg_verify_artifact_bytes(a, rawb, rawl) != DIST_OK) {
+ cfree_writer_close(raww);
+ driver_errf(PKG_TOOL, "artifact hash mismatch: %s", a->path);
+ return DIST_ERR;
+ }
+ if (out_dir) {
+ char full[PKG_PATH_BUF], parent[PKG_PATH_BUF];
+ snprintf(full, sizeof full, "%s/%s", out_dir, a->path);
+ pkg_parent_dir(full, parent, sizeof parent);
+ if (parent[0]) driver_mkdir_p(env, parent);
+ if (pkg_write_file(ctx, full, rawb, rawl) != DIST_OK) {
+ cfree_writer_close(raww);
+ return DIST_ERR;
+ }
+ driver_printf(" extracted %s\n", full);
+ }
+ cfree_writer_close(raww);
}
return DIST_OK;
}
-static int pkg_verify(DriverEnv* env, const CfreeContext* ctx, int argc,
- char** argv) {
- const char* file = NULL;
- const char* pubkey = NULL;
- int tofu = 0, i, rc = 1;
+static int pkg_verify_native(DriverEnv* env, const CfreeContext* ctx,
+ const char* file, const char* pubkey, int tofu,
+ const char* out_dir, int quiet) {
CfreeFileData fd = {0};
- DistTarEntry entries[DIST_MAX_FILES];
- size_t ne = 0;
- DistManifest m;
- const DistTarEntry* archive;
-
- for (i = 0; i < argc; ++i) {
- if (driver_streq(argv[i], "-p") && i + 1 < argc) {
- pubkey = argv[++i];
- } else if (driver_streq(argv[i], "--tofu")) {
- tofu = 1;
- } else if (argv[i][0] != '-') {
- file = argv[i];
- } else {
- driver_errf(PKG_TOOL, "verify: unknown option: %s", argv[i]);
- return 2;
- }
+ DistCfpkgHeader h;
+ DistCfpkgDescriptor d;
+ PkgVerified v;
+ char err[128];
+ uint8_t desc_keyid[DIST_KEYID_LEN], index_root[DIST_BLAKE2B_LEN],
+ content_root[DIST_BLAKE2B_LEN];
+ char desc_trusted[DIST_TRUSTED_COMMENT_MAX];
+ int rc = DIST_ERR;
+ if (pkg_read_file(ctx, file, &fd) != DIST_OK) {
+ driver_errf(PKG_TOOL, "cannot read package: %s", file);
+ return DIST_ERR;
}
- if (!file) {
- driver_errf(PKG_TOOL, "verify: FILE.cfpkg is required");
- return 2;
+ if (dist_cfpkg_read_header(fd.data, fd.size, &h) != DIST_OK ||
+ pkg_bounds(&h, fd.size) != DIST_OK) {
+ driver_errf(PKG_TOOL, "malformed native package");
+ goto done;
+ }
+ if (pkg_verify_manifest(env, ctx, fd.data + h.manifest_offset,
+ (size_t)h.manifest_size,
+ fd.data + h.signature_offset, (size_t)h.signature_size,
+ fd.data + h.pubkey_offset, (size_t)h.pubkey_size,
+ pubkey, tofu, &v) != DIST_OK)
+ goto done;
+ if (dist_minisig_sig_keyid(fd.data + h.descriptor_signature_offset,
+ (size_t)h.descriptor_signature_size,
+ desc_keyid) != DIST_OK ||
+ memcmp(desc_keyid, v.keyid, DIST_KEYID_LEN) != 0) {
+ driver_errf(PKG_TOOL, "encoding descriptor signer mismatch");
+ goto done;
+ }
+ if (dist_minisig_verify(fd.data + h.descriptor_signature_offset,
+ (size_t)h.descriptor_signature_size,
+ fd.data + h.descriptor_offset,
+ (size_t)h.descriptor_size, v.pk, desc_trusted,
+ sizeof desc_trusted) != DIST_OK) {
+ driver_errf(PKG_TOOL, "encoding descriptor signature FAILED");
+ goto done;
+ }
+ if (dist_cfpkg_descriptor_parse(fd.data + h.descriptor_offset,
+ (size_t)h.descriptor_size, &d, err,
+ sizeof err) != DIST_OK) {
+ driver_errf(PKG_TOOL, "encoding descriptor: %s", err);
+ goto done;
}
- if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK)
+ if (memcmp(d.package_id, v.package_id, DIST_BLAKE2B_LEN) != 0 ||
+ d.index_offset != h.index_offset || d.index_size != h.index_size ||
+ d.content_offset != h.content_offset || d.content_size != h.content_size ||
+ d.chunk_size != DIST_CFPKG_CHUNK_SIZE_DEFAULT ||
+ d.alignment != DIST_CFPKG_ALIGNMENT) {
+ driver_errf(PKG_TOOL, "encoding descriptor does not match package layout");
goto done;
- if (pkg_verify_bundle(env, ctx, entries, ne, &m, &archive, pubkey, tofu, 0) ==
+ }
+ pkg_region_root(index_root, "index", fd.data + h.index_offset,
+ (size_t)h.index_size);
+ pkg_region_root(content_root, "content", fd.data + h.content_offset,
+ (size_t)h.content_size);
+ if (memcmp(index_root, d.index_root, DIST_BLAKE2B_LEN) != 0 ||
+ memcmp(content_root, d.content_root, DIST_BLAKE2B_LEN) != 0) {
+ driver_errf(PKG_TOOL, "native package region hash mismatch");
+ goto done;
+ }
+ if (h.index_size % DIST_CFPKG_INDEX_RECORD_SIZE != 0) goto done;
+ if (pkg_verify_native_content(env, ctx, fd.data, &h, &v, out_dir) !=
DIST_OK)
- rc = 0;
+ goto done;
+ if (!quiet) {
+ char idhex[2 * DIST_KEYID_LEN + 1];
+ dist_hex_encode(idhex, v.keyid, DIST_KEYID_LEN);
+ driver_printf("ok: %s %s signer %s [%s]\n", v.manifest.name,
+ v.manifest.version, idhex, v.trusted);
+ }
+ if (out_dir)
+ driver_printf("unpacked %s %s to %s\n", v.manifest.name,
+ v.manifest.version, out_dir);
+ rc = DIST_OK;
done:
if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
return rc;
}
-static int pkg_unpack(DriverEnv* env, const CfreeContext* ctx, int argc,
- char** argv) {
- const char* file = NULL;
- const char* dir = ".";
- const char* pubkey = NULL;
- int tofu = 0, i, rc = 1;
- CfreeFileData fd = {0};
- DistTarEntry entries[DIST_MAX_FILES];
- size_t ne = 0;
- DistManifest m;
- const DistTarEntry* archive;
- CfreeWriter* inflated = NULL;
- DistTarEntry payload[DIST_MAX_FILES];
- size_t np = 0;
- const uint8_t* pbytes;
- size_t plen;
-
+static int pkg_verify_or_unpack(DriverEnv* env, const CfreeContext* ctx,
+ int argc, char** argv, int unpack) {
+ const char *file = NULL, *pubkey = NULL, *dir = ".";
+ int tofu = 0, i;
+ PkgFormat fmt = PKG_FMT_AUTO;
for (i = 0; i < argc; ++i) {
- if (driver_streq(argv[i], "-C") && i + 1 < argc) {
- dir = argv[++i];
- } else if (driver_streq(argv[i], "-p") && i + 1 < argc) {
+ if (driver_streq(argv[i], "-p") && i + 1 < argc)
pubkey = argv[++i];
- } else if (driver_streq(argv[i], "--tofu")) {
+ else if (driver_streq(argv[i], "--tofu"))
tofu = 1;
- } else if (argv[i][0] != '-') {
+ else if (driver_streq(argv[i], "--format") && i + 1 < argc) {
+ fmt = pkg_parse_format(argv[++i]);
+ if (fmt == PKG_FMT_AUTO) {
+ driver_errf(PKG_TOOL, "%s: unknown format", unpack ? "unpack" : "verify");
+ return 2;
+ }
+ }
+ else if (unpack && driver_streq(argv[i], "-C") && i + 1 < argc)
+ dir = argv[++i];
+ else if (argv[i][0] != '-')
file = argv[i];
- } else {
- driver_errf(PKG_TOOL, "unpack: unknown option: %s", argv[i]);
+ else {
+ driver_errf(PKG_TOOL, "%s: unknown option: %s",
+ unpack ? "unpack" : "verify", argv[i]);
return 2;
}
}
if (!file) {
- driver_errf(PKG_TOOL, "unpack: FILE.cfpkg is required");
+ driver_errf(PKG_TOOL, "%s: FILE is required", unpack ? "unpack" : "verify");
return 2;
}
- if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK)
- goto done;
- if (pkg_verify_bundle(env, ctx, entries, ne, &m, &archive, pubkey, tofu, 1) !=
- DIST_OK)
- goto done;
-
- /* Verified: decompress the payload and extract its members. */
- inflated = pkg_mem(ctx);
- if (!inflated ||
- dist_gz_decompress(inflated, archive->data, archive->size) != DIST_OK) {
- driver_errf(PKG_TOOL, "failed to decompress payload");
- goto done;
- }
- pbytes = cfree_writer_mem_bytes(inflated, &plen);
- if (dist_tar_iter(pbytes, plen, payload, DIST_MAX_FILES, &np) != DIST_OK) {
- driver_errf(PKG_TOOL, "malformed payload archive");
- goto done;
- }
+ if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(file);
+ if (fmt == PKG_FMT_TARGZ)
+ return pkg_verify_portable(env, ctx, file, pubkey, tofu,
+ unpack ? dir : NULL, unpack) == DIST_OK
+ ? 0
+ : 1;
+ return pkg_verify_native(env, ctx, file, pubkey, tofu, unpack ? dir : NULL,
+ unpack) == DIST_OK
+ ? 0
+ : 1;
+}
- for (i = 0; i < (int)np; ++i) {
- const DistTarEntry* pe = &payload[i];
- char full[PKG_PATH_BUF];
- char parent[PKG_PATH_BUF];
- uint8_t h[DIST_SHA256_LEN];
- size_t k;
- const DistArtifact* a = NULL;
- for (k = 0; k < m.n_artifacts; ++k) {
- if (driver_streq(m.artifacts[k].path, pe->name)) {
- a = &m.artifacts[k];
- break;
+static int pkg_inspect(const CfreeContext* ctx, int argc, char** argv) {
+ const char* file = NULL;
+ PkgFormat fmt = PKG_FMT_AUTO;
+ int i, rc = 1;
+ for (i = 0; i < argc; ++i) {
+ if (driver_streq(argv[i], "--format") && i + 1 < argc) {
+ fmt = pkg_parse_format(argv[++i]);
+ if (fmt == PKG_FMT_AUTO) {
+ driver_errf(PKG_TOOL, "inspect: unknown format");
+ return 2;
}
}
- if (!a) {
- driver_errf(PKG_TOOL, "payload member not in manifest: %s", pe->name);
- goto done;
- }
- dist_sha256(h, pe->data, pe->size);
- if (pe->size != a->size || memcmp(h, a->sha256, DIST_SHA256_LEN) != 0) {
- driver_errf(PKG_TOOL, "file hash mismatch: %s", pe->name);
- goto done;
+ else if (argv[i][0] != '-')
+ file = argv[i];
+ else {
+ driver_errf(PKG_TOOL, "inspect: unknown option: %s", argv[i]);
+ return 2;
}
- snprintf(full, sizeof full, "%s/%s", dir, pe->name);
- pkg_parent_dir(full, parent, sizeof parent);
- if (parent[0]) driver_mkdir_p(env, parent);
- if (pkg_write_file(ctx, full, pe->data, pe->size) != DIST_OK) goto done;
- driver_printf(" extracted %s\n", full);
}
- driver_printf("unpacked %s %s to %s\n", m.name, m.version, dir);
- rc = 0;
-
-done:
- if (inflated) cfree_writer_close(inflated);
- if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
- return rc;
-}
-
-static int pkg_inspect(const CfreeContext* ctx, int argc, char** argv) {
- const char* file = (argc > 0) ? argv[0] : NULL;
- CfreeFileData fd = {0};
- DistTarEntry entries[DIST_MAX_FILES];
- size_t ne = 0;
- const DistTarEntry* man;
- int rc = 1;
-
if (!file) {
- driver_errf(PKG_TOOL, "inspect: FILE.cfpkg is required");
+ driver_errf(PKG_TOOL, "inspect: FILE is required");
return 2;
}
- if (pkg_load_bundle(ctx, file, &fd, entries, DIST_MAX_FILES, &ne) != DIST_OK)
- goto done;
- man = pkg_find_suffix(entries, ne, PKG_SUFFIX_MANIFEST);
- if (!man) {
- driver_errf(PKG_TOOL, "bundle has no manifest");
- goto done;
+ if (fmt == PKG_FMT_AUTO) fmt = pkg_infer_format(file);
+ if (fmt == PKG_FMT_TARGZ) {
+ CfreeFileData fd = {0};
+ CfreeWriter* inflated = NULL;
+ DistTarEntry entries[DIST_MAX_FILES];
+ size_t ne = 0;
+ const DistTarEntry* man;
+ if (pkg_load_portable(ctx, file, &fd, &inflated, entries, &ne) == DIST_OK &&
+ (man = pkg_find_name(entries, ne, PKG_META_MANIFEST)) != NULL) {
+ driver_printf("%.*s", (int)man->size, (const char*)man->data);
+ rc = 0;
+ }
+ if (inflated) cfree_writer_close(inflated);
+ if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
+ return rc;
+ } else {
+ CfreeFileData fd = {0};
+ DistCfpkgHeader h;
+ if (pkg_read_file(ctx, file, &fd) == DIST_OK &&
+ dist_cfpkg_read_header(fd.data, fd.size, &h) == DIST_OK &&
+ pkg_bounds(&h, fd.size) == DIST_OK) {
+ driver_printf("%.*s", (int)h.manifest_size,
+ (const char*)(fd.data + h.manifest_offset));
+ rc = 0;
+ } else {
+ driver_errf(PKG_TOOL, "malformed native package");
+ }
+ if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
+ return rc;
}
- driver_printf("%.*s", (int)man->size, (const char*)man->data);
- rc = 0;
-done:
- if (fd.token || fd.data) ctx->file_io->release(ctx->file_io->user, &fd);
- return rc;
}
-/* --------------------------------------------------------------- trust --- */
-
static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
char** argv) {
char tpath[PKG_PATH_BUF];
const char* sub = (argc > 0) ? argv[0] : "list";
-
if (pkg_trust_path(tpath, sizeof tpath) != DIST_OK) {
driver_errf(PKG_TOOL,
"no trusted-keys path (set CFREE_TRUSTED_KEYS or HOME)");
return 1;
}
-
if (driver_streq(sub, "list")) {
CfreeFileData fd = {0};
if (pkg_read_file(ctx, tpath, &fd) != DIST_OK) {
@@ -775,7 +1103,6 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
ctx->file_io->release(ctx->file_io->user, &fd);
return 0;
}
-
if (driver_streq(sub, "add")) {
const char* pubkey = (argc > 1) ? argv[1] : NULL;
const char* label = (argc > 2) ? argv[2] : "";
@@ -785,7 +1112,6 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
char line[DIST_TRUST_LINE_MAX], parent[PKG_PATH_BUF];
int had_old, ok = 1, rc = 1;
CfreeWriter* w = NULL;
-
if (!pubkey) {
driver_errf(PKG_TOOL, "trust add: PUBKEY is required");
return 2;
@@ -796,29 +1122,19 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
}
ok = dist_minisig_parse_pubkey(kf.data, kf.size, pk, keyid) == DIST_OK;
ctx->file_io->release(ctx->file_io->user, &kf);
- if (!ok) {
- driver_errf(PKG_TOOL, "malformed public key: %s", pubkey);
- return 1;
- }
+ if (!ok) return 1;
had_old = (pkg_read_file(ctx, tpath, &old) == DIST_OK);
if (had_old &&
dist_trust_lookup(old.data, old.size, keyid, dummy) == DIST_OK) {
- char hex[2 * DIST_KEYID_LEN + 1];
- dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
- driver_printf("key id %s already trusted\n", hex);
ctx->file_io->release(ctx->file_io->user, &old);
return 0;
}
if (dist_trust_format_entry(line, sizeof line, keyid, pk, label) !=
- DIST_OK) {
- if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
- driver_errf(PKG_TOOL, "label too long");
+ DIST_OK)
return 1;
- }
pkg_parent_dir(tpath, parent, sizeof parent);
if (parent[0]) driver_mkdir_p(env, parent);
if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) == CFREE_OK) {
- ok = 1;
if (had_old && old.size)
ok = cfree_writer_write(w, old.data, old.size) == CFREE_OK;
if (ok) ok = cfree_writer_write(w, line, strlen(line)) == CFREE_OK;
@@ -826,38 +1142,21 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
cfree_writer_close(w);
}
if (had_old) ctx->file_io->release(ctx->file_io->user, &old);
- if (rc == 0) {
- char hex[2 * DIST_KEYID_LEN + 1];
- dist_hex_encode(hex, keyid, DIST_KEYID_LEN);
- driver_printf("trusted key id %s (%s)\n", hex, tpath);
- } else {
- driver_errf(PKG_TOOL, "failed to write %s", tpath);
- }
return rc;
}
-
if (driver_streq(sub, "remove")) {
const char* idhex = (argc > 1) ? argv[1] : NULL;
CfreeFileData old = {0};
uint8_t want[DIST_KEYID_LEN];
CfreeWriter* w = NULL;
size_t pos = 0;
- int rc = 1, removed = 0;
-
+ int rc = 1;
if (!idhex || strlen(idhex) != 2 * DIST_KEYID_LEN ||
- dist_hex_decode(want, idhex, DIST_KEYID_LEN) != DIST_OK) {
- driver_errf(PKG_TOOL, "trust remove: a 16-hex-char KEYID is required");
+ dist_hex_decode(want, idhex, DIST_KEYID_LEN) != DIST_OK)
return 2;
- }
- if (pkg_read_file(ctx, tpath, &old) != DIST_OK) {
- driver_printf("(no trusted keys at %s)\n", tpath);
- return 0;
- }
- if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) != CFREE_OK) {
- driver_errf(PKG_TOOL, "cannot rewrite %s", tpath);
- ctx->file_io->release(ctx->file_io->user, &old);
+ if (pkg_read_file(ctx, tpath, &old) != DIST_OK) return 0;
+ if (ctx->file_io->open_writer(ctx->file_io->user, tpath, &w) != CFREE_OK)
return 1;
- }
rc = 0;
while (pos < old.size) {
size_t start = pos, end = pos;
@@ -865,15 +1164,12 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
char idbuf[2 * DIST_KEYID_LEN + 1];
int keep = 1;
while (end < old.size && old.data[end] != '\n') ++end;
- /* Drop the line if its first token is the target key id. */
if (end - start >= 2 * DIST_KEYID_LEN) {
memcpy(idbuf, old.data + start, 2 * DIST_KEYID_LEN);
idbuf[2 * DIST_KEYID_LEN] = '\0';
if (dist_hex_decode(got, idbuf, DIST_KEYID_LEN) == DIST_OK &&
- memcmp(got, want, DIST_KEYID_LEN) == 0) {
+ memcmp(got, want, DIST_KEYID_LEN) == 0)
keep = 0;
- removed = 1;
- }
}
if (keep) {
size_t n = (end < old.size ? end + 1 : end) - start;
@@ -884,52 +1180,40 @@ static int pkg_trust(DriverEnv* env, const CfreeContext* ctx, int argc,
if (cfree_writer_status(w) != CFREE_OK) rc = 1;
cfree_writer_close(w);
ctx->file_io->release(ctx->file_io->user, &old);
- if (rc == 0)
- driver_printf(removed ? "removed key id %s\n" : "key id %s not found\n",
- idhex);
- else
- driver_errf(PKG_TOOL, "failed to rewrite %s", tpath);
return rc;
}
-
driver_errf(PKG_TOOL, "trust: unknown subcommand: %s", sub);
return 2;
}
-/* --------------------------------------------------------------- entry --- */
-
int driver_pkg(int argc, char** argv) {
DriverEnv env;
CfreeContext ctx;
const char* sub;
int rc;
-
if (driver_argv_wants_help(argc, argv, 1) || argc < 2) {
driver_help_pkg();
return argc < 2 ? 2 : 0;
}
sub = argv[1];
-
driver_env_init(&env);
ctx = driver_env_to_context(&env);
-
- if (driver_streq(sub, "keygen")) {
+ if (driver_streq(sub, "keygen"))
rc = pkg_keygen(&env, &ctx, argc - 2, argv + 2);
- } else if (driver_streq(sub, "create")) {
+ else if (driver_streq(sub, "create"))
rc = pkg_create(&env, &ctx, argc - 2, argv + 2);
- } else if (driver_streq(sub, "verify")) {
- rc = pkg_verify(&env, &ctx, argc - 2, argv + 2);
- } else if (driver_streq(sub, "unpack")) {
- rc = pkg_unpack(&env, &ctx, argc - 2, argv + 2);
- } else if (driver_streq(sub, "inspect")) {
+ else if (driver_streq(sub, "verify"))
+ rc = pkg_verify_or_unpack(&env, &ctx, argc - 2, argv + 2, 0);
+ else if (driver_streq(sub, "unpack"))
+ rc = pkg_verify_or_unpack(&env, &ctx, argc - 2, argv + 2, 1);
+ else if (driver_streq(sub, "inspect"))
rc = pkg_inspect(&ctx, argc - 2, argv + 2);
- } else if (driver_streq(sub, "trust")) {
+ else if (driver_streq(sub, "trust"))
rc = pkg_trust(&env, &ctx, argc - 2, argv + 2);
- } else {
+ else {
driver_errf(PKG_TOOL, "unknown subcommand: %s", sub);
rc = 2;
}
-
driver_env_fini(&env);
return rc;
}