Skip to content

dillingerstaffing/trust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

trust

A CLI that lets a developer sign their source code, build output, or component library — and anyone verify it — with nothing trusted except a short public key passed between humans.

You stamp a directory of files. You share a public key string through any human channel — voice, paper, DM, bio. A verifier runs one command with your key. The CLI hashes every file locally and checks the signature. If anything changed between your machine and theirs, it fails. No accounts, no services, no browser, no infrastructure.


Why This Exists

Nothing you receive over HTTP has integrity

When you run npm install @dillingerstaffing/strand-ui, npm checks a SHA-512 hash from package-lock.json. That hash came from npm's servers — an HTTP-accessible system. Under zero trust, those checksums are meaningless. The registry itself could be compromised. The checksums verify against hashes generated by a third party you don't trust.

When you run npx strand-ui add button, no checksums are checked at all — but that's not actually worse. Under zero trust, both methods have identical integrity: zero. Neither can prove the files came from the developer's machine. Both trust the network, the registry, the CDN, and the DNS infrastructure between the developer and you.

What copy-paste distribution does have is visibility. The code lands in your src/, not in node_modules/. It shows up in git diff. It goes through code review. You can read it, grep it, audit it. node_modules/ is gitignored, opaque, and practically un-auditable — thousands of packages you've never read and never will. Copy-paste puts the code where you can actually see it.

trust gives any distribution method provenance through the developer's own signature. But copy-paste + trust is the strictly superior combination: the code is both signed (you can verify it came from the developer) and visible (it's in your project, in your git history, in your code review).

The same problem applies to deployed websites. Between your ./dist folder and what a user's browser downloads, there are half a dozen systems — git hosts, CI/CD, CDNs, DNS — any of which can tamper with your files. trust closes both gaps with the same mechanism: the developer signs on their machine, the verifier checks on theirs, and nothing in between is trusted.

What this looks like in practice

Component library author (e.g., strand):

trust stamp ./packages/strand-ui/components

Signs every component file. Ships the manifest alongside the components. Publishes a 43-character public key in their README, their bio, and their docs site.

Developer who copies a component:

npx strand-ui add button
trust check ./src/components/strand --key ed25519:aB3x9Kf2Q...

Verifies that the files just copied into their project are byte-identical to what dillingerstaffing signed on their machine. If the registry was compromised, if the CDN injected code, if anything changed the files in transit — the check fails.

Developer who deploys a website:

npm run build
trust stamp ./dist
# deploy however you want

A verifier later runs:

trust check https://example.com --key ed25519:aB3x9Kf2Q...

Same mechanism. Same trust model. The only trusted input is the public key, carried by a human.


Install

cargo install trust-cli

Or build from source:

git clone https://github.com/dillingerstaffing/trust
cd trust
cargo build --release

The output is a single static binary: trust.


Quick Start

Developer: stamp your build

npm run build
trust stamp ./dist

Output:

Your public key: ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX
✓ Stamped 4 files → dist/trust-manifest.json
  Tip: run with --explain to see what just happened

Deploy ./dist however you already do. The manifest ships with the site.

Share your public key through a channel you control — GitHub bio, business card, podcast, tweet, in person. It never changes unless you rotate keys.

Developer: stamp your component library

trust stamp ./packages/strand-ui/components

Output:

✓ Stamped 31 files → packages/strand-ui/components/trust-manifest.json

The manifest ships with the package — via npm, via the copy-paste CLI, via git. It rides alongside the component files through whatever distribution channel you use.

Verifier: check a site

trust check https://example.com --key ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX

Output:

  index.html   ✓
  app.js       ✓
  style.css    ✓
  favicon.ico  ✓

✓ All 4 files match. Signed by ed25519:aB3x9Kf2Q...

The URL is untrusted. The manifest fetched from the site is untrusted. The only trusted input is the public key string you obtained from the developer through a human channel.

Verifier: check copied components

After running npx strand-ui add button:

trust check ./src/components/strand --key ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX

Output:

  Button/Button.tsx   ✓
  Button/Button.css   ✓
  Button/index.ts     ✓

✓ Verified 3 of 31 signed files. 28 not present (not installed).
  Signed by ed25519:aB3x9Kf2Q...

You installed 3 of 31 components. trust verified those 3 match the author's stamp. The other 28 aren't on disk — they weren't installed, so they're skipped. If you later run npx strand-ui add dialog, run trust check again to verify the new files too.


How It Works

Three concepts:

  1. Fingerprinting. Every file is hashed (SHA-256). Change one byte and the hash is completely different. The developer's CLI and the verifier's CLI both compute hashes independently.

  2. Signing. The developer's private key (never leaves their machine) signs the list of hashes. Anyone with the developer's public key can verify the signature. No one can forge it without the private key.

  3. Comparing. The verifier hashes files locally — whether fetched from a URL or read from disk after a copy-paste install — and checks that (a) the hashes match the signed manifest, and (b) the signature is valid for the provided public key.

The manifest (file list + hashes + signature) travels alongside the files it covers. For a website, it's at /trust-manifest.json. For a component library, it's in the package root. It is fetched or read from the same untrusted source as every other file. It is not trusted — it is verified. The trust chain is:

Public key (human channel) ── verifies ──▶ Signature
Signature ── verifies ──▶ Manifest (file list + hashes)
Manifest ── verifies ──▶ File contents

Only the public key requires a trusted channel.
Everything else is fetched from the untrusted source and verified.

If an attacker modifies any file, the hash won't match the manifest. If they modify the manifest, the signature won't match the public key. If they replace the manifest entirely with one signed by a different key, the public key check fails. The only attack that succeeds is compromising the developer's private key or the human channel carrying the public key.


Command Reference

trust init

Generate a new Ed25519 signing keypair.

$ trust init
Your public key: ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX

Share this key through a channel you control.
It never changes unless you run trust init again.

Private key saved. It never leaves this machine.

Behavior:

  • Generates an Ed25519 keypair (RFC 8032, pure mode)
  • Writes the private key to ~/.config/trust/private.key (32 bytes, raw)
  • Sets file permissions to 0600 (owner read/write only)
  • Creates ~/.config/trust/ directory if it doesn't exist (permissions 0700)
  • Prints the public key in the format ed25519:<base64url_no_padding>
  • If a key already exists, prints a warning and asks for confirmation before overwriting:
    A signing key already exists. Generating a new one will invalidate
    all previous stamps. Verifiers will need your new public key.
    Overwrite? [y/N]
    

Flags:

  • --force: Skip the overwrite confirmation.

Exit codes: 0 on success, 2 on error.


trust stamp <path>

Hash every file in the directory, sign the file list, and write trust-manifest.json into the directory.

$ trust stamp ./dist
✓ Stamped 4 files → dist/trust-manifest.json
$ trust stamp ./packages/strand-ui/components
✓ Stamped 31 files → packages/strand-ui/components/trust-manifest.json

Behavior:

  1. If no keypair exists at ~/.config/trust/private.key, auto-run trust init first (generate key, print public key, then continue stamping).
  2. Validate that <path> exists and is a directory.
  3. Walk the directory recursively. For each file:
    • Compute its path relative to <path>, using / as separator (regardless of OS).
    • Skip excluded files (see File Discovery below).
    • Read the file contents and compute SHA-256.
  4. Construct the signed payload (see Manifest Format below).
  5. Sign the payload with Ed25519.
  6. Write trust-manifest.json to <path>/trust-manifest.json.
  7. Print the number of files stamped and the output path.
  8. Print a one-line hint: Tip: run with --explain to see what just happened (suppressed by --quiet and after the first stamp in a session).
  9. If this is the first stamp (no prior key existed), also print the public key with sharing instructions.

Flags:

  • --explain: Show each step with plain-english annotations as it happens (see Progressive Disclosure).
  • --quiet / -q: Print only the final status line. For scripting.

Exit codes: 0 on success, 2 on error.

Errors:

  • <path> does not exist → Error: ./dist does not exist.
  • <path> is a file, not a directory → Error: ./dist is a file. trust stamp expects a directory.
  • Directory is empty (after exclusions) → Error: No files found in ./dist to stamp.
  • Private key file is unreadable → Error: Cannot read signing key at ~/.config/trust/private.key. Check permissions.

trust check <target> --key <public_key>

Verify that the files at a URL or in a local directory match a developer's signed stamp.

$ trust check https://example.com --key ed25519:aB3x9Kf2Q...

  index.html   ✓
  app.js       ✓
  style.css    ✓
  favicon.ico  ✓

✓ All 4 files match. Signed by ed25519:aB3x9Kf2Q...

Behavior when target is a URL:

  1. Parse and validate the public key from the --key argument.
  2. Normalize the URL: strip trailing /, ensure scheme is https:// (or http:// if explicitly provided — but print a warning for http).
  3. Fetch <url>/trust-manifest.json.
  4. Parse the manifest JSON. Validate required fields.
  5. Extract the signature from the manifest.
  6. Reconstruct the signed payload from the manifest's trust_version, timestamp, and files fields (see Signing Protocol).
  7. Verify the Ed25519 signature against the reconstructed payload and the provided public key. If it fails, stop and report.
  8. For each file listed in manifest.files:
    • Fetch <url>/<filepath> (follow redirects, decompress content-encoding).
    • Compute SHA-256 of the response body.
    • Compare to the hash in the manifest.
    • If a file returns HTTP 404 → treat as verification failure (file missing from deploy).
  9. Print per-file results.
  10. Print summary. All files must be present and match.

Behavior when target is a local directory:

$ trust check ./src/components/strand --key ed25519:aB3x9Kf2Q...

When the target is a path to a local directory (detected by checking if the path exists on disk):

  1. Read <path>/trust-manifest.json. If not found, exit 2 with error.
  2. Verify the signature against the provided public key (same as URL mode, steps 4-7).
  3. For each file listed in manifest.files:
    • If the file exists at <path>/<filepath>: read it, compute SHA-256, compare to manifest hash.
    • If the file does NOT exist at <path>/<filepath>: skip it (the file wasn't installed/copied). Count as "not present."
  4. Additionally, scan the directory for files that exist on disk but are NOT listed in the manifest. Report these as "unrecognized" with a warning.
  5. Print per-file results (present files only), then summary.

Local directory output (partial install):

$ trust check ./src/components/strand --key ed25519:aB3x9Kf2Q...

  Button/Button.tsx   ✓
  Button/Button.css   ✓
  Button/index.ts     ✓

✓ Verified 3 of 31 signed files. 28 not present (not installed).
  Signed by ed25519:aB3x9Kf2Q...

Local directory output (with unrecognized files):

$ trust check ./src/components/strand --key ed25519:aB3x9Kf2Q...

  Button/Button.tsx   ✓
  Button/Button.css   ✓
  Button/index.ts     ✓

  ⚠ 1 unrecognized file (not in manifest):
    Button/Button.local.tsx

✓ Verified 3 of 31 signed files. 28 not present (not installed).
  1 file not covered by manifest.
  Signed by ed25519:aB3x9Kf2Q...

Unrecognized files are files that exist on disk but are not in the manifest. They are reported as a warning but do not cause verification failure (exit 0). They may be local modifications the developer made intentionally. The warning ensures the user knows these files are not covered by the stamp.

Local directory output (with hash mismatch):

$ trust check ./src/components/strand --key ed25519:aB3x9Kf2Q...

  Button/Button.tsx   ✗ MISMATCH
  Button/Button.css   ✓
  Button/index.ts     ✓

✗ 1 of 3 verified files does not match the signed manifest.
  This file was modified after the author stamped it.

This exits 1. A hash mismatch is always a failure, whether checking a URL or a local directory.

Flags:

  • --key <public_key> (required): The developer's public key. Format: ed25519:<base64url_no_padding>.
  • --explain: Show each step with annotations (see Progressive Disclosure).
  • --quiet / -q: Print only the final pass/fail line.
  • --force: Skip TOFU key mismatch confirmation (accept the new key without prompting).

Exit codes:

  • 0: All present files verified successfully (signature valid, all hashes match).
  • 1: Verification failed (signature invalid, or any present file's hash doesn't match).
  • 2: Error (network failure, missing manifest, invalid arguments).

Output on signature failure:

✗ Signature invalid.
  The manifest was NOT signed by the provided key.
  This means either:
    • The manifest was tampered with after the developer signed it
    • The wrong public key was provided

  Expected key: ed25519:aB3x9Kf2Q...

Output when manifest not found:

✗ No trust-manifest.json found at https://example.com/trust-manifest.json
  This site may not be stamped, or the manifest was removed.

HTTP behavior (URL mode only):

  • Follow redirects (up to 10 hops).
  • Set header Cache-Control: no-cache on all requests.
  • Set header User-Agent: trust-cli/<version>.
  • Timeout per request: 30 seconds.
  • Decompress Content-Encoding: gzip, br, deflate transparently (hash the decompressed body).

trust inspect

Show the contents of the most recent stamp, with progressive detail.

$ trust inspect ./dist

Stamp for ./dist — 4 files

  index.html    sha256:e3b0c442...  (4,521 bytes)
  app.js        sha256:a1b2c3d4...  (128,934 bytes)
  style.css     sha256:d4e5f6a7...  (18,203 bytes)
  favicon.ico   sha256:789abcde...  (1,150 bytes)

Signed by: ed25519:aB3x9Kf2Q...
Stamped at: 2026-04-01T18:30:00Z

☞ Verify any file yourself: shasum -a 256 ./dist/app.js
☞ View raw manifest: trust inspect ./dist --raw
☞ Step through signature verification: trust inspect ./dist --verify

Behavior:

  1. Read <path>/trust-manifest.json.
  2. Display file list with hashes and sizes (sizes read from disk).
  3. Display public key and timestamp.
  4. Print the hints pointing to deeper inspection commands.

Flags:

  • --raw: Print the raw trust-manifest.json contents (pretty-printed JSON).
  • --verify: Step through the signature verification interactively, showing each cryptographic operation and its result (see Progressive Disclosure for exact output).
  • --quiet / -q: Print only the file list with hashes, no hints or decoration.

If <path> is omitted, default to the current directory.

Exit codes: 0 on success, 2 if manifest not found.


trust keys

Show the current signing keypair information.

$ trust keys
Public key: ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX
Key file:   ~/.config/trust/private.key

Flags:

  • --export: Print only the public key string with no decoration (for piping/scripting).

Exit codes: 0 if key exists, 2 if no key found.


trust learn

Interactive lessons that teach how trust works, using the developer's own files.

$ trust learn

Launches an interactive lesson sequence. Each lesson is hands-on — the user observes operations on real or simulated files. See the Progressive Disclosure section for the full lesson specifications.

Flags:

  • --lesson <number>: Start at a specific lesson (1-5).

Exit codes: 0 always.


trust explain <topic>

Print a plain-english explanation of a concept.

$ trust explain signing

Available topics: overview, hashing, signing, keys, verification, threat-model, supply-chain

See the Progressive Disclosure section for the full content of each topic.

Exit codes: 0 if topic found, 2 if topic not recognized (prints available topics).


Specification

Manifest Format

The file trust-manifest.json is a JSON object with this schema:

{
  "trust_version": 1,
  "timestamp": "2026-04-01T18:30:00Z",
  "files": {
    "Button/Button.css": "d4e5f6a7b8c90123456789abcdef0123456789abcdef0123456789abcdef012345",
    "Button/Button.tsx": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
    "Button/index.ts": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
  },
  "public_key": "aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX",
  "signature": "Mx7p9Q2wE4rT8yU3iO6pA1sD5fG9hJ3kL7zX2cV0bN4mR8qW6eY1tU5iO9pA3sD7fG2hJ6kL0zX4cV8b"
}

Field definitions:

Field Type Description
trust_version integer Always 1. Allows future format changes.
timestamp string ISO 8601 UTC timestamp of when the stamp was created. Format: YYYY-MM-DDTHH:MM:SSZ.
files object Map of relative file paths to lowercase hex-encoded SHA-256 hashes. Keys sorted lexicographically by byte value.
public_key string The signer's Ed25519 public key, base64url-encoded without padding. 43 characters for a 32-byte key.
signature string Ed25519 signature over the signed payload, base64url-encoded without padding. 86 characters for a 64-byte signature.

Constraints:

  • All hash values are exactly 64 lowercase hex characters (256 bits).
  • File paths use / as separator regardless of OS. No leading ./ or /. No trailing /.
  • The public_key field uses base64url encoding (RFC 4648 §5) WITHOUT = padding.
  • The signature field uses base64url encoding (RFC 4648 §5) WITHOUT = padding.
  • The public_key in the manifest does NOT include the ed25519: prefix used in the CLI. The prefix is a CLI display convention only.
  • The manifest file itself (trust-manifest.json) is never included in the files map.

Canonical JSON Serialization (for signing)

The signature is computed over a signed payload — a canonical JSON byte string derived from the manifest. The signed payload includes ONLY the trust_version, timestamp, and files fields:

{"files":{"Button/Button.css":"d4e5...","Button/Button.tsx":"a1b2...","Button/index.ts":"e3b0..."},"timestamp":"2026-04-01T18:30:00Z","trust_version":1}

Canonical JSON follows RFC 8785 (JSON Canonicalization Scheme — JCS):

  1. No whitespace between tokens.
  2. Object keys sorted lexicographically by their UTF-8 byte values. This applies recursively to nested objects.
  3. Strings use minimal escaping: only ", \, and control characters (U+0000–U+001F) are escaped. Characters above U+001F appear as literal UTF-8.
  4. Integers have no leading zeros, no decimal point, no exponent notation.
  5. UTF-8 encoding, no BOM.

The signed payload byte string is the input to Ed25519_Sign.

To construct the signed payload from a manifest:

  1. Take the trust_version, timestamp, and files fields.
  2. Serialize as canonical JSON per the rules above.
  3. The resulting byte string is the signed payload.

To verify: reconstruct the signed payload from the manifest's fields using the same rules, then verify the signature against this byte string and the provided public key.

Signing Protocol

Stamp (sign):

  1. Walk the directory and hash all files → files map.
  2. Get current UTC time → timestamp.
  3. Set trust_version to 1.
  4. Construct the signed payload: canonical JSON of {"files":{...},"timestamp":"...","trust_version":1}.
  5. Sign: signature = Ed25519_Sign(private_key, signed_payload_bytes).
  6. Derive public key from private key.
  7. Assemble the full manifest JSON (with all fields including public_key and signature).
  8. Write to trust-manifest.json (pretty-printed with 2-space indentation for human readability — the pretty-printing does not affect the signed payload, which is always canonical).

Ed25519 specifics:

  • RFC 8032, Ed25519 pure mode (not pre-hashed Ed25519ph).
  • Private key: 32 bytes (the seed). The actual signing key and public key are derived from this seed per RFC 8032.
  • Public key: 32 bytes.
  • Signature: 64 bytes.
  • The message signed is the raw signed payload bytes (not a hash of them — Ed25519 hashes internally with SHA-512).

Verification Protocol

Check (verify):

  1. Parse the --key argument. Strip the ed25519: prefix. Decode base64url → 32 bytes. This is the trusted public key.
  2. Fetch or read trust-manifest.json from the target.
  3. Parse the manifest JSON. Validate: all required fields present, trust_version is 1, files is a non-empty object, all hashes are 64 hex characters.
  4. Extract signature from the manifest. Decode base64url → 64 bytes.
  5. Reconstruct the signed payload from the manifest's trust_version, timestamp, and files fields using canonical JSON (RFC 8785).
  6. Verify: Ed25519_Verify(trusted_public_key, signed_payload_bytes, signature). If false → exit 1 with signature failure message.
  7. Optionally: compare the public_key in the manifest to the --key argument. If they differ, print a warning (the manifest claims a different signer), but the signature verification in step 6 is authoritative.
  8. URL mode — strict. For each file in manifest.files:
    • Fetch <base_url>/<filepath> (or read from local directory).
    • Compute SHA-256 of the raw bytes.
    • Compare to the manifest hash. Record pass/fail.
    • If a file cannot be fetched (404, network error) → record as fail.
  9. Local directory mode — partial. For each file in manifest.files:
    • If the file exists at <path>/<filepath>: read it, compute SHA-256, compare to manifest hash. Record pass/fail.
    • If the file does NOT exist: record as "not present" (skip, not fail).
    • After checking manifest files, scan the directory for files that exist but are NOT in the manifest. Record these as "unrecognized."
  10. Print per-file results.
  11. URL mode: if all files passed → exit 0. If any failed → exit 1.
  12. Local directory mode: if all PRESENT files passed → exit 0. If any present file failed → exit 1. "Not present" files do not affect the exit code. "Unrecognized" files do not affect the exit code (they are warnings only).

File Discovery and Hashing

When stamping a directory, these rules determine which files are included:

  1. Walk the directory recursively.
  2. Include all regular files.
  3. Exclude the following (hardcoded, not configurable in v1):
    • trust-manifest.json (the output file itself)
    • .DS_Store
    • Thumbs.db
    • Any file or directory whose name starts with .git (.git/, .gitkeep, .gitignore, etc.)
  4. Skip symbolic links. Print a warning: Warning: skipping symlink <path>.
  5. Compute the relative path from the stamp root using / as separator.
  6. Sort file entries by path (lexicographic, byte-order).

Hashing:

  • Algorithm: SHA-256.
  • Input: the raw file bytes (not text — no line ending normalization).
  • Output: 64 lowercase hex characters.
  • Files should be streamed (hashed in chunks) to support large files without loading them entirely into memory. Recommended chunk size: 8 KB.

Key Storage

Location: ~/.config/trust/private.key

Format: Raw 32-byte Ed25519 seed. No headers, no framing, no encryption in v1.

Permissions: File created with mode 0600. Directory ~/.config/trust/ created with mode 0700.

Deriving the public key: The public key is computed from the 32-byte seed per RFC 8032 §5.1.5. It is derived every time it is needed — it is not stored separately.

Platform notes:

  • On macOS, ~/.config/trust/ is used (not ~/Library/Application Support/) for cross-platform consistency.
  • On Linux, respects $XDG_CONFIG_HOME if set; otherwise ~/.config/trust/.
  • On Windows, uses %APPDATA%\trust\.

Public Key Display Format

When displayed to a user (in CLI output, for sharing), the public key is formatted as:

ed25519:<base64url_no_padding>

Example: ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX

The ed25519: prefix is a display convention for humans. It is NOT stored in the manifest's public_key field (which contains only the base64url string).

When parsing a --key argument, the CLI must accept both:

  • ed25519:aB3x9... (with prefix)
  • aB3x9... (without prefix)

Key Pinning (TOFU — Trust On First Use)

The first time trust check succeeds for a given base URL or local directory path, the CLI records the association between that target and the public key used:

Location: ~/.config/trust/pins.json

Format:

{
  "pins": {
    "https://example.com": {
      "public_key": "aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX",
      "first_seen": "2026-04-01T18:30:00Z",
      "last_verified": "2026-04-01T20:00:00Z"
    }
  }
}

Behavior on subsequent checks:

  • If --key matches the pinned key → proceed normally. Update last_verified.
  • If --key differs from the pinned key → print a warning before verification:
    ⚠ Key mismatch for https://example.com
      Pinned key (first seen 2026-04-01): ed25519:aB3x9Kf2Q...
      Provided key:                       ed25519:zZ8yY7xX6...
    
      This could mean the developer rotated keys, or that someone
      is attempting to impersonate them.
    
      If the developer rotated keys, verify through multiple channels
      (their GitHub bio, personal site, in person) that the new key
      is legitimate before continuing.
    
      Continue with the new key? [y/N]
    
  • If user confirms → update the pin. If user denies → exit 2.
  • --force flag skips the confirmation.

Key pinning is optional in v1 but recommended. If not implemented, the CLI simply doesn't record or compare keys across runs. The security properties of individual verifications are unaffected — pinning only adds continuity protection across multiple checks over time.

Error Handling and Exit Codes

Exit code Meaning
0 Success (stamp created, or all present files verified)
1 Verification failed (signature invalid or hash mismatch)
2 Error (bad arguments, missing files, network failure, parse error)

All error messages go to stderr. Normal output goes to stdout.


Integrating trust Into a CLI Tool

A copy-paste component CLI (like strand-ui) can integrate trust verification directly, so the user doesn't need to run a separate command.

How a CLI author integrates trust

At publish time, the library author stamps the component distribution directory:

trust stamp ./packages/strand-ui/components

The resulting trust-manifest.json is committed to the repo and included in the published package. The author's public key is published in the project README, the CLI's --version output, and any other human-readable channel.

In the CLI source, the add command includes a verification step after copying files:

npx strand-ui add button

Internally, the CLI:

  1. Copies the component files to the user's project (existing behavior).
  2. Also copies trust-manifest.json to the component directory (new).
  3. Reads the manifest, reconstructs the signed payload, and verifies the signature against the author's public key (which is either hardcoded in the CLI source or provided by the user via --trust-key).
  4. Hashes each copied file and compares to the manifest.
  5. If verification passes: prints a confirmation line.
  6. If verification fails: prints a warning and asks the user whether to keep or delete the copied files.

Example CLI output with integrated verification:

$ npx strand-ui add button

  Adding Button to ./src/components/strand...
    Button/Button.tsx    ✓ stamped by dillingerstaffing
    Button/Button.css    ✓ stamped by dillingerstaffing
    Button/index.ts      ✓ stamped by dillingerstaffing

  ✓ 3 files verified against ed25519:aB3x9Kf2Q...

Example with verification failure:

$ npx strand-ui add button

  Adding Button to ./src/components/strand...
    Button/Button.tsx    ✗ MISMATCH — file differs from stamp
    Button/Button.css    ✓ stamped by dillingerstaffing
    Button/index.ts      ✓ stamped by dillingerstaffing

  ✗ 1 file does not match the author's stamp.
    This could mean the file was modified in transit.
    Keep files anyway? [y/N]

How a CLI author embeds the public key

The author's public key should be available through multiple channels. The CLI itself can embed it as a constant:

// In the CLI source code — the user trusts the CLI by running it,
// so embedding the key here is consistent with that trust boundary.
const AUTHOR_PUBLIC_KEY = "ed25519:aB3x9Kf2Q7mN4pR8sT6wY1zA5cE9gH3jK7oL2qU0vX";

For users who want to verify without trusting the CLI's embedded key, the --trust-key flag accepts a key obtained through an independent human channel:

npx strand-ui add button --trust-key ed25519:aB3x9Kf2Q...

Or the user skips the CLI entirely and uses trust directly:

npx strand-ui add button
trust check ./src/components/strand --key ed25519:aB3x9Kf2Q...

What trust verification adds

Distribution method Integrity under zero trust Visibility With trust
npm install None (checksums from npm's servers — an untrusted HTTP system) Hidden in node_modules/, gitignored, un-auditable Signed provenance, but code still hidden
npx strand-ui add (without trust) None Source in your project, in git diff, in code review
npx strand-ui add (with trust) Ed25519 signature from author's machine Source in your project, in git diff, in code review Signed provenance AND visible source

Under zero trust, npm install and npx strand-ui add have identical integrity: none. Both receive code over HTTP from systems you don't control. The difference is what happens after: npm hides the code in an opaque node_modules/ tree, while copy-paste puts it in your source directory where you can read and audit it.

trust adds the missing piece — proof that the files came from the developer's machine, not from a compromised intermediary. Combined with copy-paste distribution, you get the best of both properties: verifiable origin and visible source.


Progressive Disclosure

trust has two speeds: do the thing silently, or show your work like a teacher. Every command accepts --explain to annotate what it's doing. Additional commands (inspect, learn, explain) provide deeper layers.

The --explain Flag

Available on trust stamp and trust check. Prints each step as it happens with plain-english annotations.

trust stamp ./dist --explain output:

STEP 1 — Reading your build output
  Found 4 files in ./dist (152 KB total)
  Excluded: trust-manifest.json, .DS_Store (see trust explain hashing)

STEP 2 — Fingerprinting each file
  Every file gets a unique fingerprint (SHA-256 hash).
  Change one byte and the fingerprint is completely different.

    index.html  → sha256:e3b0c442...
    app.js      → sha256:a1b2c3d4...
    style.css   → sha256:d4e5f6a7...
    favicon.ico → sha256:789abcde...

  ☞ Verify yourself: shasum -a 256 ./dist/app.js

STEP 3 — Signing
  Your private key creates a signature proving you authored this
  file list. The key never leaves your machine.

  ☞ See your public key: trust keys
  ☞ Learn more: trust explain signing

STEP 4 — Writing trust-manifest.json
  Written to ./dist/trust-manifest.json.
  This file ships with your site or package. It's not trusted
  by verifiers — it's verified against your public key.

  ☞ View it: trust inspect ./dist
  ☞ Verify it yourself: trust inspect ./dist --verify

✓ Stamped 4 files → dist/trust-manifest.json

trust check <url> --key <key> --explain output:

STEP 1 — Parsing the public key
  Key: ed25519:aB3x9Kf2Q...
  This is the ONLY trusted input. Everything else is verified against it.

STEP 2 — Fetching the manifest
  GET https://example.com/trust-manifest.json
  Found manifest with 4 files.

  This manifest came from the internet — we do NOT trust it.
  We'll verify it against the public key you provided.

STEP 3 — Verifying the signature
  Reconstructing the signed payload from the manifest fields...
  Checking Ed25519 signature against your provided public key...
  ✓ Signature valid.

  This means: the private key matching ed25519:aB3x9Kf2Q...
  DID sign this exact file list. Forging this would require the
  private key, which never left the developer's machine.

  ☞ Learn more: trust explain signing

STEP 4 — Fetching and fingerprinting each file
  GET https://example.com/index.html → sha256:e3b0c442...
    Expected: sha256:e3b0c442... ✓

  GET https://example.com/app.js → sha256:a1b2c3d4...
    Expected: sha256:a1b2c3d4... ✓

  GET https://example.com/style.css → sha256:d4e5f6a7...
    Expected: sha256:d4e5f6a7... ✓

  GET https://example.com/favicon.ico → sha256:789abcde...
    Expected: sha256:789abcde... ✓

  ☞ Verify any file yourself: curl -s https://example.com/app.js | shasum -a 256

STEP 5 — Result
  Every file fetched from the live URL matches the developer's
  signed file list. Nothing was modified between the developer's
  machine and this URL.

✓ All 4 files match. Signed by ed25519:aB3x9Kf2Q...

trust check ./src/components/strand --key <key> --explain output:

STEP 1 — Parsing the public key
  Key: ed25519:aB3x9Kf2Q...
  This is the ONLY trusted input. Everything else is verified against it.

STEP 2 — Reading the manifest
  Found trust-manifest.json in ./src/components/strand
  Manifest covers 31 files.

  This manifest was copied alongside the component files.
  We do NOT trust it — we'll verify it against your public key.

STEP 3 — Verifying the signature
  Reconstructing the signed payload from the manifest fields...
  Checking Ed25519 signature against your provided public key...
  ✓ Signature valid.

  This means: the private key matching ed25519:aB3x9Kf2Q...
  DID sign this exact file list. The manifest hasn't been tampered with.

STEP 4 — Checking local files against manifest
  Of 31 files in the manifest, 3 exist on disk:

  Button/Button.tsx  disk:a1b2c3d4...  manifest:a1b2c3d4...  ✓
  Button/Button.css  disk:d4e5f6a7...  manifest:d4e5f6a7...  ✓
  Button/index.ts    disk:e3b0c442...  manifest:e3b0c442...  ✓

  28 files in the manifest are not present on disk — these are
  components you haven't installed yet. That's normal.

  ☞ Verify any hash: shasum -a 256 ./src/components/strand/Button/Button.tsx

STEP 5 — Result
  Every installed component file matches the author's stamp.
  Nothing was modified between the author's machine and yours.

✓ Verified 3 of 31 signed files. 28 not present (not installed).
  Signed by ed25519:aB3x9Kf2Q...

trust inspect --verify Output

Steps through the cryptographic verification interactively:

$ trust inspect ./dist --verify

Verifying the stamp in ./dist/trust-manifest.json...

1. Reconstructing the signed payload
   The trust_version, timestamp, and files fields are serialized
   as canonical JSON — sorted keys, no whitespace — so the exact
   bytes are deterministic.

   Signed payload (148 bytes):
   {"files":{"app.js":"a1b2...","favicon.ico":"789a...","index.html":"e3b0...","style.css":"d4e5..."},"timestamp":"2026-04-01T18:30:00Z","trust_version":1}

   ☞ Reproduce this: trust inspect ./dist --raw | trust canonical
   (trust canonical is a helper that extracts and canonicalizes the signed fields)

2. Verifying the Ed25519 signature
   Public key: ed25519:aB3x9Kf2Q...
   Signature:  ed25519:Mx7p9Q2wE4...

   ✓ Signature is valid for this payload and this key.

   This means: the holder of the private key corresponding to
   ed25519:aB3x9Kf2Q... signed this exact byte string. No one
   else could have produced this signature.

3. Comparing file hashes to disk
   Each file on disk is hashed and compared to the manifest.

   index.html    disk:e3b0c442...  manifest:e3b0c442...  ✓
   app.js        disk:a1b2c3d4...  manifest:a1b2c3d4...  ✓
   style.css     disk:d4e5f6a7...  manifest:d4e5f6a7...  ✓
   favicon.ico   disk:789abcde...  manifest:789abcde...  ✓

   ☞ Verify any hash: shasum -a 256 ./dist/app.js

All checks passed.

trust learn Lessons

Interactive, hands-on. Uses temporary files, never modifies the user's actual code.

Lesson 1: Fingerprints

LESSON 1: Fingerprints

  I've created a temporary file with this text:
    "Hello, world."

  Run: shasum -a 256 on it.
  Result: sha256:f8c3bf62...

  Now I'll change just one character — lowercase 'h' instead of 'H':
    "hello, world."

  Run it again.
  Result: sha256:09ca7e4e...  ← completely different

  That's called a hash — a unique fingerprint for a file's exact
  contents. Any change, no matter how small, produces a completely
  different fingerprint.

  You already use this every day. Every git commit has a SHA —
  that's a hash of your code. trust does the same thing for
  your deploy or your component library.

  ☞ Try it yourself: echo "Hello, world." | shasum -a 256

  [press enter for Lesson 2]

Lesson 2: Signing

LESSON 2: Signing

  A signature proves who created something.

  I've generated a temporary keypair for this lesson:
    Public key:  ed25519:temp1234...
    Private key: (held in memory, not saved)

  Here's a manifest with two file fingerprints:
    index.html → sha256:aaaa...
    app.js     → sha256:bbbb...

  Signing this manifest with the private key produces:
    signature: ed25519:xxxx...

  Now let's verify: does this signature match this manifest
  and this public key?
    Ed25519_Verify(public_key, manifest, signature) → ✓ YES

  What if someone changes a fingerprint in the manifest?
    app.js → sha256:cccc...  (changed one character)
    Ed25519_Verify(public_key, modified_manifest, signature) → ✗ NO

  The signature locks the manifest to the signer. Change anything
  and the lock breaks.

  You know the "Verified" badge on signed git commits?
  Same math, same idea. trust applies it to your deploys.

  [press enter for Lesson 3]

Lesson 3: The Full Flow

LESSON 3: Putting It Together

  Let's walk through exactly what happens when someone runs
  trust check.

  Imagine you stamped a site with 2 files.
  Your public key: ed25519:temp1234...

  A verifier runs:
    trust check https://example.com --key ed25519:temp1234...

  Step 1: Fetch trust-manifest.json from the URL.
    This file came from the internet. We do NOT trust it.

  Step 2: Check the signature.
    Reconstruct the signed payload from the manifest fields.
    Verify: does the signature match this payload + the public key?
    ✓ Yes. The manifest was signed by ed25519:temp1234...

  Step 3: Fetch each file from the URL.
    GET index.html → compute sha256 → compare to manifest → ✓
    GET app.js → compute sha256 → compare to manifest → ✓

  If anything was changed between your machine and the URL —
  by a hacked server, a compromised CDN, a man-in-the-middle,
  or anyone else — at least one hash would differ, and the
  check would fail.

  The only way to fool this: steal the developer's private key,
  or trick the verifier into using the wrong public key.

  [press enter for Lesson 4]

Lesson 4: The Supply Chain

LESSON 4: Why Copy-Paste Distribution + trust

  npm install checks a SHA-512 hash from package-lock.json.
  But that hash came from npm's servers — an HTTP system.
  Under zero trust, those checksums are meaningless. The
  registry itself could be compromised.

  npx strand-ui add button doesn't check hashes either.
  But under zero trust, both methods have the same integrity:
  none. Both receive code over HTTP from systems you don't
  control.

  The difference is visibility. npm hides the code in
  node_modules/ — gitignored, opaque, un-auditable. Copy-paste
  puts it in your src/, in your git diff, in your code review.
  You can actually read what you installed.

  trust adds the missing piece: proof of origin. The author
  stamps their component files:
    trust stamp ./components
  This creates a signed manifest — every file's fingerprint,
  locked by the author's private key.

  After you copy components into your project, you verify:
    trust check ./src/components/strand --key ed25519:...

  trust hashes every file on YOUR disk and checks the signature.
  If anything was modified in transit, the check fails.

  Copy-paste + trust gives you both properties:
    • Signed provenance (it came from the author)
    • Visible source (it's in your project, not hidden)

  The only thing you need from the author: their public key.
  43 characters. Fits in a tweet, a README, a business card.

  [press enter for Lesson 5]

Lesson 5: Threat Model

LESSON 5: What Can Go Wrong

  trust isn't magic. Here's what it protects and what it doesn't.

  Scenario: GitHub is hacked. Attacker modifies your source code.
  ▸ Does the deployed site change? Only if you rebuild from the
    hacked source. If you build from your local machine (which
    isn't hacked) and stamp it, trust detects no problem because
    there is no problem — the deploy came from your machine.
  ▸ If you DO rebuild from hacked source and stamp it — trust
    can't help. You signed the bad build yourself.

  Scenario: Your CDN modifies app.js (injecting ads, crypto miners).
  ▸ trust check catches this immediately. The hash of the
    modified app.js won't match the manifest. ✓ Detected.

  Scenario: An npm registry is compromised and serves a modified
  version of a component you install via copy-paste CLI.
  ▸ trust check catches this. The component files were modified
    in transit, so their hashes won't match the author's stamp.
    ✓ Detected.

  Scenario: An attacker compromises the server and replaces
  trust-manifest.json with a new one.
  ▸ They can't produce a valid signature without your private
    key. The signature check fails. ✓ Detected.

  Scenario: Your laptop is stolen with the private key on it.
  ▸ The thief can sign anything as you. trust can't help.
    This is the fundamental limit: trust protects everything
    between your machine and the user. It doesn't protect
    your machine itself.

  Scenario: Cloaking — the server detects the verification
  request and serves correct files, but tampered files to
  normal visitors.
  ▸ trust cannot detect this. Mitigate by: verifying from
    multiple networks/IPs, or mirroring the site locally first
    (wget --mirror) and verifying the local copy.

  [lessons complete]

trust explain Topics

Each topic is a concise explainer printed to stdout.

trust explain overview

WHAT TRUST DOES

  trust lets a developer sign their files — a website deploy,
  a component library, any set of static assets — so anyone
  can verify that what they received is exactly what the
  developer shipped.

  The developer runs: trust stamp ./dist
  This fingerprints every file and signs the fingerprints.

  A verifier runs: trust check <url-or-directory> --key <key>
  This hashes every file locally and checks the signature.

  If anything changed between the developer's machine and the
  verifier's, the check fails.

  This works for websites (check a live URL), for component
  libraries (check files copied via npx strand-ui add button),
  or for any directory of files distributed through any channel.

  The only trusted input is the public key — a short string the
  developer shares through a human channel (in person, on a
  business card, in a bio, verbally).

  Everything else is verified, not trusted.

trust explain hashing

FINGERPRINTING (HASHING)

  A hash function takes any input and produces a fixed-length
  fingerprint. trust uses SHA-256, which produces a 64-character
  hex string for any input.

  Properties:
    1. Deterministic: same input → same fingerprint, always.
    2. One-way: you can't reconstruct the file from the fingerprint.
    3. Collision-resistant: two different files won't produce the
       same fingerprint (in practice — the odds are astronomically
       small, like 1 in 2^128).
    4. Avalanche: changing one bit in the input completely changes
       the fingerprint.

  You already use hashing every day:
    • git commit SHAs are hashes of your code
    • package-lock.json "integrity" fields are hashes
    • Downloading software and checking "SHA-256 checksum" is this

  trust applies the same idea: hash every file in your deploy
  or component library, then sign the hashes to prove they
  came from you.

  ☞ Try it: echo "test" | shasum -a 256

trust explain signing

DIGITAL SIGNATURES

  A digital signature is proof that a specific person created
  a specific piece of data.

  It uses a keypair:
    Private key — secret, stays on your machine, like a wax seal
                  stamp that only you own
    Public key  — shared freely, like a photo of the seal pattern
                  that anyone can use to check the impression

  When you sign:
    Sign(private_key, data) → signature

  When someone verifies:
    Verify(public_key, data, signature) → yes or no

  Key properties:
    • Only the private key can produce valid signatures
    • The public key can verify, but not produce
    • Changing even one byte of the signed data invalidates
      the signature
    • The private key cannot be derived from the public key

  trust uses Ed25519, the same algorithm used by:
    • SSH keys (ssh-keygen -t ed25519)
    • Signal protocol
    • WireGuard
    • Signed git commits (when using SSH signing)

  ☞ Try it: trust learn --lesson 2

trust explain keys

YOUR KEYS

  Your private key is at: ~/.config/trust/private.key
  It's 32 bytes. It never leaves your machine.

  Your public key is derived from it:
    ed25519:<base64url string, 43 characters>

  Share the public key through channels you control:
    • GitHub bio or pinned gist
    • Personal website (on a different host than what you stamp)
    • Business card, QR code
    • Verbally, at a conference or in a video
    • In your component library's README

  The more places you publish it, the harder it is for someone
  to impersonate you — an attacker would need to compromise
  multiple independent platforms.

  If your private key is lost or stolen:
    • Run trust init to generate a new keypair
    • Share the new public key through ALL your channels —
      GitHub bio, personal site, README, in person
    • The more channels that agree on the new key, the harder
      it is for an attacker to impersonate the rotation
    • Old stamps remain valid under the old key
    • New stamps use the new key
    • Verifiers who previously checked your work will see a
      key mismatch warning (TOFU pinning) and should cross-check
      the new key through multiple channels before accepting

  ☞ Show your public key: trust keys

trust explain verification

HOW VERIFICATION WORKS

  When you run trust check <target> --key <key>, here's what happens:

  1. The CLI reads trust-manifest.json from the target.
     Whether from a URL or a local directory — it is NOT trusted.
     It's just data from an untrusted source.

  2. The CLI verifies the signature in the manifest against YOUR
     provided public key. If the signature is invalid, the manifest
     was tampered with or signed by someone else. Check fails.

  3. The CLI hashes every file at the target (fetched from URL or
     read from disk) with SHA-256.

  4. The CLI compares each hash to the manifest's hash.
     Any mismatch means the file was modified. Check fails.

  Trust chain:
    Public key (you provided it) ── verifies ──▸ Signature
    Signature ── verifies ──▸ Manifest (file list + hashes)
    Manifest ── verifies ──▸ File contents

  Every link is verified against the one above.
  Only the public key is trusted. Everything else is checked.

  This works the same whether you're checking a live website
  or a directory of components you just copied into your project.

  ☞ Watch it happen: trust check <target> --key <key> --explain

trust explain threat-model

WHAT TRUST PROTECTS (AND WHAT IT DOESN'T)

  ✓ PROTECTED:
    • Git host compromised (GitHub, GitLab, Bitbucket)
    • CI/CD pipeline compromised (injected build steps)
    • CDN or hosting provider tampering with files
    • DNS hijacking redirecting the domain
    • Man-in-the-middle modifying files in transit
    • Server compromise (attacker replaces files)
    • npm/package registry compromise (modified packages)
    • Copy-paste CLI serving tampered component source

    All detected because: the hashes won't match the signed
    manifest, or the signature won't match the public key.

  ✗ NOT PROTECTED:
    • Developer's machine compromised
      (attacker has the private key, can sign anything)
    • Verifier has the wrong public key
      (they're checking against the wrong identity)
    • Cloaking: server serves different content to the verifier
      vs. normal users (detectable by checking from multiple
      locations, or by mirroring the site with wget first)
    • The developer signs and deploys the wrong thing
      (trust verifies what you stamped, not what you intended)
    • Dynamic content (API responses, server-rendered HTML)
      (trust verifies static files only)

  trust moves the trust boundary to the developer's machine
  and the public key. Protect both.

trust explain supply-chain

THE SUPPLY CHAIN PROBLEM

  When you install code from the internet, you're trusting a
  long chain of systems:

    Author's machine → git host → CI/CD → registry → CDN → you

  Any link in that chain can modify what you receive.

  Under zero trust, npm's package-lock.json checksums don't
  help — those hashes came from npm's servers, an HTTP system
  in the same untrusted chain. If the registry is compromised,
  the checksums are compromised too.

  Copy-paste CLIs (npx strand-ui add, npx shadcn-ui add) also
  have no integrity mechanism. But they have an advantage:
  the code lands in your project directory, visible in git diff,
  auditable in code review. npm hides code in node_modules/ —
  gitignored and practically un-readable.

  trust gives both distribution methods the missing piece:
  proof that the files came from the author's machine.

    The author stamps their files:
      trust stamp ./components

    You verify after receiving them:
      trust check ./my-project/components --key ed25519:...

  The verification is against the author's signature, not a
  registry's checksum. The registry is not in the trust chain.
  Copy-paste + trust gives you signed provenance AND visible
  source — the best of both properties.

  ☞ See the full threat model: trust explain threat-model

Threat Model

What trust guarantees

If trust check returns exit code 0 (all present files match), then:

  1. The files are byte-identical to the files that were in the directory when trust stamp was run.
  2. The stamp was created by the holder of the private key corresponding to the provided public key.
  3. Neither the files nor the manifest were modified after stamping.

What trust does NOT guarantee

  1. The developer's machine wasn't compromised at stamp time.
  2. The developer intended to stamp those files (they might have stamped the wrong directory).
  3. The files don't contain vulnerabilities or malicious code.
  4. The server isn't serving different content to different clients (cloaking).
  5. Dynamic content (API responses, WebSocket data) matches any expectation.

Attack scenarios

Attack Detected? How
CDN injects script into app.js app.js hash doesn't match manifest
Attacker replaces all files on server Signature doesn't match provided public key
Attacker replaces manifest AND files New manifest not signed by developer's key
npm registry compromised, serves modified package File hashes don't match author's signed manifest
Copy-paste CLI serves tampered component source Copied file hashes don't match author's signed manifest
DNS hijacking to a fake server Signature check fails (fake server doesn't have private key)
Attacker replays old manifest + old files Signature is valid — but timestamp field shows the age. The verifier can notice the old date. v1 limitation: no automatic rollback detection.
Developer's laptop stolen Attacker can sign anything. Requires key rotation + re-publishing public key through multiple human channels. Verifiers with pinned keys will see a key mismatch warning.
Key rotation (legitimate) Verifiers with pinned keys see a warning. They should cross-check the new key through multiple independent channels (GitHub bio, personal site, in person) before accepting.
Cloaking (serve different files to verifier) Mitigate by verifying from multiple IPs or mirroring with wget first
Attacker adds a new file not in manifest Reported as "unrecognized" in local dir mode. Not checked in URL mode. However, existing files that reference it (e.g. HTML adding a <script> tag) will have a hash mismatch.

Implementation Guide

This section is for implementers. It specifies the technical requirements for building the trust CLI.

Language and Dependencies

Recommended: Rust

Core crates:

  • ed25519-dalek (Ed25519 signing and verification)
  • sha2 (SHA-256 hashing)
  • base64 (base64url encoding/decoding, use URL_SAFE_NO_PAD alphabet)
  • serde + serde_json (JSON parsing, but NOT for canonical serialization — implement canonical serialization manually or use a JCS library)
  • reqwest (HTTP client, with blocking feature for v1 simplicity, or async)
  • clap (CLI argument parsing)
  • walkdir (recursive directory traversal)
  • chrono (UTC timestamp generation, ISO 8601 formatting)
  • dirs (cross-platform config directory resolution)

Alternative: TypeScript/Node

If faster implementation is preferred:

  • node:crypto for Ed25519 (crypto.generateKeyPairSync('ed25519'), crypto.sign, crypto.verify) and SHA-256 (crypto.createHash('sha256'))
  • node:fs and node:path for file operations
  • Native fetch (Node 18+) for HTTP
  • Bundle to single binary with bun build --compile or pkg

Build and Distribution

  • Ship as a single static binary. No runtime dependencies.
  • Binary name: trust.
  • Support: macOS (arm64, x86_64), Linux (x86_64, arm64), Windows (x86_64).
  • Provide via: cargo install trust-cli, GitHub releases, and optionally brew install dillingerstaffing/tap/trust.

Project Structure

trust/
├── src/
│   ├── main.rs              # CLI entry point, argument parsing
│   ├── commands/
│   │   ├── mod.rs
│   │   ├── init.rs           # Key generation
│   │   ├── stamp.rs          # Directory hashing + signing + manifest writing
│   │   ├── check.rs          # Manifest fetching + verification (URL and local)
│   │   ├── inspect.rs        # Manifest display + signature walkthrough
│   │   ├── keys.rs           # Public key display
│   │   ├── learn.rs          # Interactive lessons
│   │   └── explain.rs        # Topic explainers
│   ├── crypto/
│   │   ├── mod.rs
│   │   ├── keys.rs           # Keypair generation, storage, loading
│   │   ├── sign.rs           # Payload construction + Ed25519 signing
│   │   └── verify.rs         # Signature verification
│   ├── manifest/
│   │   ├── mod.rs
│   │   ├── build.rs          # Walk directory, hash files, build manifest
│   │   ├── canonical.rs      # RFC 8785 canonical JSON serialization
│   │   ├── format.rs         # Manifest struct, serialization, parsing
│   │   └── discover.rs       # File discovery, exclusion rules, path normalization
│   ├── http.rs               # HTTP client for fetching URL resources
│   └── output.rs             # Formatted output, --explain logic, --quiet logic
├── tests/
│   ├── stamp_and_check.rs    # Integration: stamp a dir, check it, modify, re-check
│   ├── partial_check.rs      # Integration: stamp 31 files, check dir with 3 present
│   ├── canonical_json.rs     # Unit: canonical JSON output matches expected bytes
│   ├── manifest_format.rs    # Unit: manifest parsing and validation
│   └── crypto.rs             # Unit: sign/verify round-trip, tamper detection
├── Cargo.toml
└── README.md

Test Requirements

The implementation must include tests for:

  1. Round-trip: stamp a directory → check the same directory → passes.
  2. Tamper detection (file): stamp → modify one file → check → fails with hash mismatch.
  3. Tamper detection (manifest): stamp → modify a hash in the manifest → check → fails with signature invalid.
  4. Tamper detection (signature): stamp → modify the signature in the manifest → check → fails with signature invalid.
  5. Wrong key: stamp with key A → check with key B → fails with signature invalid.
  6. Empty directory: stamp on empty dir → error.
  7. Excluded files: .DS_Store and trust-manifest.json are not in the manifest.
  8. Canonical JSON determinism: same file set → same canonical JSON bytes → same signature, every time.
  9. Binary files: images/fonts are hashed correctly (raw bytes, no text encoding).
  10. Path normalization: backslashes on Windows become forward slashes, no leading ./.
  11. HTTP verification: stamp a directory, serve it with a local HTTP server, check against localhost.
  12. Partial verification (local directory): stamp a directory with 10 files, copy 3 files + manifest to a new directory, check → passes for 3 files, reports 7 not present, exit code 0.
  13. Partial verification with mismatch: same as above, but modify one of the 3 files → fails with hash mismatch for that file, exit code 1.
  14. Unrecognized files: stamp a directory, copy to new location, add a file not in the manifest → check reports the extra file as "unrecognized" warning, exit code still 0.
  15. URL mode strict: stamp a directory, serve it via HTTP, delete one file from the server → check fails (file missing), exit code 1.
  16. TOFU pinning: check a URL with key A → succeeds, pin recorded. Check same URL with key A again → succeeds, no warning. Check same URL with key B → prints key mismatch warning.
  17. TOFU pin update: after confirming key mismatch, pin is updated to new key. Subsequent checks with new key proceed without warning.

Canonical JSON Implementation Notes

Do NOT rely on serde_json serialization order for the signed payload. serde_json does not guarantee key ordering by default. Either:

  • Use serde_json::Value and sort keys manually before serialization, or
  • Use BTreeMap (which iterates in sorted order) for the files map and serialize manually, or
  • Implement a minimal canonical JSON serializer that takes a BTreeMap<String, String> and produces the exact bytes.

The canonical serializer must produce output that matches RFC 8785 exactly. Test against known inputs:

Input map: {"b": "2", "a": "1"}
Expected canonical JSON: {"a":"1","b":"2"}
Input: {"files": {"z.js": "hash1", "a.html": "hash2"}, "timestamp": "2026-04-01T18:30:00Z", "trust_version": 1}
Expected canonical JSON: {"files":{"a.html":"hash2","z.js":"hash1"},"timestamp":"2026-04-01T18:30:00Z","trust_version":1}

HTTP Verification Implementation Notes

When trust check is given a URL:

  1. Manifest fetch: GET <url>/trust-manifest.json. If the URL doesn't end with /, append /trust-manifest.json. If it already ends with a path, append /trust-manifest.json to it.
  2. File fetch: For each file in the manifest, GET <url>/<filepath>. Use the same base URL logic.
  3. Redirect handling: Follow HTTP 301, 302, 307, 308 redirects up to 10 hops.
  4. Content decoding: Transparently decompress gzip, brotli, deflate responses. Hash the decompressed bytes. Send Accept-Encoding: identity to prefer uncompressed responses (simpler and avoids edge cases with encoding), but handle compressed responses gracefully if the server sends them anyway.
  5. TLS: Verify certificates by default. No option to skip verification in v1 — this is a security tool.
  6. Timeouts: 30 seconds per request. 5 minutes total for all requests.
  7. Concurrency: Fetch files concurrently (up to 8 parallel requests) for performance. The hashing and comparison can happen as responses arrive.

When trust check is given a local directory path (detected by checking if the path exists on disk):

  1. Read <path>/trust-manifest.json.
  2. For each file in the manifest, attempt to read <path>/<filepath>. If the file doesn't exist, record as "not present" and continue.
  3. After checking manifest files, walk the directory (using the same exclusion rules as trust stamp) and identify files not listed in the manifest. Report these as "unrecognized."
  4. No HTTP involved. All file reads are local.

Principles

  1. Nothing trust does requires trust in trust. Every operation can be replicated with shasum -a 256, an Ed25519 library, and curl. The CLI is a convenience wrapper over operations you could do by hand.

  2. The only trusted input is the public key, carried by a human. Everything fetched from any URL, read from any directory, or received through any distribution channel is verified, not trusted.

  3. Simple by default, transparent on demand. trust stamp and trust check are two commands. --explain, inspect, learn, and explain are there when you want to see how it works.

  4. The tool should teach you not to need it. Every hint shows you how to verify the same thing with standard tools. The goal is understanding, not dependency.

  5. Same mechanism, any distribution channel. A deployed website, a component copied via npx strand-ui add button, a tarball downloaded from a release page — trust doesn't care how files got from the developer to you. It verifies they arrived intact.

About

Sign your source code, build output, or component library -- and let anyone verify it -- with nothing trusted except a short public key passed between humans.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages