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.
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.
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.
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.
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.
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.
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.
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.
Three concepts:
-
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.
-
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.
-
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.
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 (permissions0700) - 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.
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:
- If no keypair exists at
~/.config/trust/private.key, auto-runtrust initfirst (generate key, print public key, then continue stamping). - Validate that
<path>exists and is a directory. - 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.
- Compute its path relative to
- Construct the signed payload (see Manifest Format below).
- Sign the payload with Ed25519.
- Write
trust-manifest.jsonto<path>/trust-manifest.json. - Print the number of files stamped and the output path.
- Print a one-line hint:
Tip: run with --explain to see what just happened(suppressed by--quietand after the first stamp in a session). - 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.
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:
- Parse and validate the public key from the
--keyargument. - Normalize the URL: strip trailing
/, ensure scheme ishttps://(orhttp://if explicitly provided — but print a warning for http). - Fetch
<url>/trust-manifest.json. - Parse the manifest JSON. Validate required fields.
- Extract the signature from the manifest.
- Reconstruct the signed payload from the manifest's
trust_version,timestamp, andfilesfields (see Signing Protocol). - Verify the Ed25519 signature against the reconstructed payload and the provided public key. If it fails, stop and report.
- 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).
- Fetch
- Print per-file results.
- 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):
- Read
<path>/trust-manifest.json. If not found, exit 2 with error. - Verify the signature against the provided public key (same as URL mode, steps 4-7).
- 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."
- If the file exists at
- Additionally, scan the directory for files that exist on disk but are NOT listed in the manifest. Report these as "unrecognized" with a warning.
- 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-cacheon all requests. - Set header
User-Agent: trust-cli/<version>. - Timeout per request: 30 seconds.
- Decompress
Content-Encoding: gzip,br,deflatetransparently (hash the decompressed body).
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:
- Read
<path>/trust-manifest.json. - Display file list with hashes and sizes (sizes read from disk).
- Display public key and timestamp.
- 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.
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.
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.
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).
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_keyfield uses base64url encoding (RFC 4648 §5) WITHOUT=padding. - The
signaturefield uses base64url encoding (RFC 4648 §5) WITHOUT=padding. - The
public_keyin the manifest does NOT include theed25519:prefix used in the CLI. The prefix is a CLI display convention only. - The manifest file itself (
trust-manifest.json) is never included in thefilesmap.
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):
- No whitespace between tokens.
- Object keys sorted lexicographically by their UTF-8 byte values. This applies recursively to nested objects.
- Strings use minimal escaping: only
",\, and control characters (U+0000–U+001F) are escaped. Characters above U+001F appear as literal UTF-8. - Integers have no leading zeros, no decimal point, no exponent notation.
- UTF-8 encoding, no BOM.
The signed payload byte string is the input to Ed25519_Sign.
To construct the signed payload from a manifest:
- Take the
trust_version,timestamp, andfilesfields. - Serialize as canonical JSON per the rules above.
- 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.
Stamp (sign):
- Walk the directory and hash all files →
filesmap. - Get current UTC time →
timestamp. - Set
trust_versionto1. - Construct the signed payload: canonical JSON of
{"files":{...},"timestamp":"...","trust_version":1}. - Sign:
signature = Ed25519_Sign(private_key, signed_payload_bytes). - Derive public key from private key.
- Assemble the full manifest JSON (with all fields including
public_keyandsignature). - 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).
Check (verify):
- Parse the
--keyargument. Strip theed25519:prefix. Decode base64url → 32 bytes. This is the trusted public key. - Fetch or read
trust-manifest.jsonfrom the target. - Parse the manifest JSON. Validate: all required fields present,
trust_versionis1,filesis a non-empty object, all hashes are 64 hex characters. - Extract
signaturefrom the manifest. Decode base64url → 64 bytes. - Reconstruct the signed payload from the manifest's
trust_version,timestamp, andfilesfields using canonical JSON (RFC 8785). - Verify:
Ed25519_Verify(trusted_public_key, signed_payload_bytes, signature). If false → exit 1 with signature failure message. - Optionally: compare the
public_keyin the manifest to the--keyargument. If they differ, print a warning (the manifest claims a different signer), but the signature verification in step 6 is authoritative. - 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.
- Fetch
- 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."
- If the file exists at
- Print per-file results.
- URL mode: if all files passed → exit 0. If any failed → exit 1.
- 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).
When stamping a directory, these rules determine which files are included:
- Walk the directory recursively.
- Include all regular files.
- Exclude the following (hardcoded, not configurable in v1):
trust-manifest.json(the output file itself).DS_StoreThumbs.db- Any file or directory whose name starts with
.git(.git/,.gitkeep,.gitignore, etc.)
- Skip symbolic links. Print a warning:
Warning: skipping symlink <path>. - Compute the relative path from the stamp root using
/as separator. - 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.
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_HOMEif set; otherwise~/.config/trust/. - On Windows, uses
%APPDATA%\trust\.
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)
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
--keymatches the pinned key → proceed normally. Updatelast_verified. - If
--keydiffers 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.
--forceflag 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.
| 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.
A copy-paste component CLI (like strand-ui) can integrate trust verification directly, so the user doesn't need to run a separate command.
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:
- Copies the component files to the user's project (existing behavior).
- Also copies
trust-manifest.jsonto the component directory (new). - 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). - Hashes each copied file and compares to the manifest.
- If verification passes: prints a confirmation line.
- 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]
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...
| 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.
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.
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...
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.
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]
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
If trust check returns exit code 0 (all present files match), then:
- The files are byte-identical to the files that were in the directory when
trust stampwas run. - The stamp was created by the holder of the private key corresponding to the provided public key.
- Neither the files nor the manifest were modified after stamping.
- The developer's machine wasn't compromised at stamp time.
- The developer intended to stamp those files (they might have stamped the wrong directory).
- The files don't contain vulnerabilities or malicious code.
- The server isn't serving different content to different clients (cloaking).
- Dynamic content (API responses, WebSocket data) matches any expectation.
| 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. |
This section is for implementers. It specifies the technical requirements for building the trust CLI.
Recommended: Rust
Core crates:
ed25519-dalek(Ed25519 signing and verification)sha2(SHA-256 hashing)base64(base64url encoding/decoding, useURL_SAFE_NO_PADalphabet)serde+serde_json(JSON parsing, but NOT for canonical serialization — implement canonical serialization manually or use a JCS library)reqwest(HTTP client, withblockingfeature 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:cryptofor Ed25519 (crypto.generateKeyPairSync('ed25519'),crypto.sign,crypto.verify) and SHA-256 (crypto.createHash('sha256'))node:fsandnode:pathfor file operations- Native
fetch(Node 18+) for HTTP - Bundle to single binary with
bun build --compileorpkg
- 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 optionallybrew install dillingerstaffing/tap/trust.
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
The implementation must include tests for:
- Round-trip:
stampa directory →checkthe same directory → passes. - Tamper detection (file):
stamp→ modify one file →check→ fails with hash mismatch. - Tamper detection (manifest):
stamp→ modify a hash in the manifest →check→ fails with signature invalid. - Tamper detection (signature):
stamp→ modify the signature in the manifest →check→ fails with signature invalid. - Wrong key:
stampwith key A →checkwith key B → fails with signature invalid. - Empty directory:
stampon empty dir → error. - Excluded files:
.DS_Storeandtrust-manifest.jsonare not in the manifest. - Canonical JSON determinism: same file set → same canonical JSON bytes → same signature, every time.
- Binary files: images/fonts are hashed correctly (raw bytes, no text encoding).
- Path normalization: backslashes on Windows become forward slashes, no leading
./. - HTTP verification: stamp a directory, serve it with a local HTTP server, check against localhost.
- 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.
- Partial verification with mismatch: same as above, but modify one of the 3 files → fails with hash mismatch for that file, exit code 1.
- 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.
- URL mode strict: stamp a directory, serve it via HTTP, delete one file from the server → check fails (file missing), exit code 1.
- 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.
- TOFU pin update: after confirming key mismatch, pin is updated to new key. Subsequent checks with new key proceed without warning.
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::Valueand sort keys manually before serialization, or - Use
BTreeMap(which iterates in sorted order) for thefilesmap 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}
When trust check is given a URL:
- 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.jsonto it. - File fetch: For each file in the manifest, GET
<url>/<filepath>. Use the same base URL logic. - Redirect handling: Follow HTTP 301, 302, 307, 308 redirects up to 10 hops.
- Content decoding: Transparently decompress gzip, brotli, deflate responses. Hash the decompressed bytes. Send
Accept-Encoding: identityto prefer uncompressed responses (simpler and avoids edge cases with encoding), but handle compressed responses gracefully if the server sends them anyway. - TLS: Verify certificates by default. No option to skip verification in v1 — this is a security tool.
- Timeouts: 30 seconds per request. 5 minutes total for all requests.
- 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):
- Read
<path>/trust-manifest.json. - For each file in the manifest, attempt to read
<path>/<filepath>. If the file doesn't exist, record as "not present" and continue. - 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." - No HTTP involved. All file reads are local.
-
Nothing trust does requires trust in trust. Every operation can be replicated with
shasum -a 256, an Ed25519 library, andcurl. The CLI is a convenience wrapper over operations you could do by hand. -
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.
-
Simple by default, transparent on demand.
trust stampandtrust checkare two commands.--explain,inspect,learn, andexplainare there when you want to see how it works. -
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. -
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.