Go CLI tool that cryptographically verifies Truestamp proof bundle JSON files. Compiles to a single static binary with zero runtime dependencies. No dependency on the Truestamp application.
truestamp verify reads a proof JSON file (generated by Truestamp.Proof.generate/1 in the Truestamp service at truestamp/truestamp-v2) and independently verifies the complete cryptographic chain from user claims through public blockchain commitment. Uses Go stdlib for crypto (crypto/sha256, crypto/ed25519, crypto/md5, crypto/sha1, crypto/sha512) plus golang.org/x/crypto/{sha3,blake2b,blake2s} (via truestamp hash), cobra (CLI), gowebpki/jcs (RFC 8785), koanf (configuration management), gofrs/uuid (UUIDv7 timestamp extraction), oklog/ulid (ULID timestamp extraction), fxamacker/cbor/v2 (CBOR read/write), and lipgloss v2 (terminal styling with table/list/tree subpackages).
Beyond verification, the CLI exposes five Unix-y, pipe-friendly sub-commands that replace the common external tool chain (sha256sum, shasum, xxd, base64, jq, date): truestamp hash (digests), truestamp encode / truestamp decode (byte encodings), truestamp jcs (RFC 8785 canonicalization), truestamp convert {time, proof, id, keyid, merkle} (domain conversions). truestamp create registers a new timestamped item in either of two modes: external-hash (the user submits a hash of a file they keep locally) or claims-as-source-of-truth (no external file — the claims content itself is what gets timestamped, gated by a server-side meaningful-content rule requiring ≥ 32-char description or non-empty metadata). The pair claims.hash / claims.hash_type is co-required: both supplied or both omitted.
truestamp console opens an interactive Bubble Tea TUI that holds a long-lived authenticated WebSocket to the backend (multiplexed Phoenix Channels: console:lobby for commands + stream events, console:clock for server-time ticks). Four panes — Monitor (toggleable stream subscriptions + scrollable waterfall), New Item (two-mode form — external-hash or claims-as-source-of-truth, picked via a leading Submission mode Select; hash + hash_type fields are auto-hidden in claims-content mode and the description field enforces the ≥ 32-char rule inline — plus live items.created → items.committed lifecycle card), Teams (membership list + in-place team switch via scope.switch_team channel push), Connection (diagnostics + log file path). Reconnect-with-backoff, server-side first-event-immediate event coalescing into <resource>.burst summaries, and 24h time-windowed event retention. Full architecture, limits, logging, and testing in docs/engineering/console.md. Server wire protocol authoritative reference in truestamp-v2/docs/console_channel.md.
The truestamp team subcommand (team list / show / set / unset) and the in-TUI Teams pane share a single source of truth: the top-level team key in ~/.config/truestamp/config.toml. team set validates the id by reading the team from /api/json/teams/{id} before persisting, so a typo or revoked membership refuses to write. The console pane's s-to-set confirmation pushes scope.switch_team over the live WebSocket, persists on success, and never reconnects — catalog stream subscriptions get rebound against the new tenant server-side, while item watches keep their original team binding. See "Team management surfaces" below.
task build # -> build/truestamp
./build/truestamp verify [proof.json] [--type item|entropy_nist|entropy_stellar|entropy_bitcoin|block|beacon] [--file [path]] [--url [url]] [--hash hex] [--skip-external] [--skip-signatures] [--silent] [--json] [--remote]
./build/truestamp create [file] [--file [path]] [--claims [path]] [--claims-stdin] [--file-stdin] [--name ...] [--hash ...] [--hash-type ...] [--description ...] [--metadata ...] [--json]
# External-hash mode: truestamp create document.pdf
# Claims-as-source-of-truth (no file, no hash): truestamp create -n "Title" -d "<32+ char description>"
# --hash and --hash-type are co-required (both, or neither).
./build/truestamp download <id> [--type item|entropy_nist|entropy_stellar|entropy_bitcoin|block|beacon] [-f json|cbor] [-o path]
./build/truestamp beacon [latest|list|get|by-hash] [--json|--hash-only|-s] # read-only Truestamp beacons JSON:API
./build/truestamp hash [flags] [path ...] # SHA-2/SHA-3/BLAKE2/MD5/SHA-1 digests; supports --prefix 0xNN, --jcs
./build/truestamp encode [--from <enc>] [--to <enc>] [file] # hex|base64|base64url|binary
./build/truestamp decode [--from <enc>] [--to <enc>] [file] # hex|base64|base64url|binary
./build/truestamp jcs [--newline] [file] # RFC 8785 canonicalization
./build/truestamp convert time|proof|id|keyid|merkle ... # domain conversions (see subcommand help)
./build/truestamp team [list] # default = list
./build/truestamp team show [id] # detail card for active team (or arg)
./build/truestamp team set [id] # validate + persist; interactive picker if no id
./build/truestamp team unset # clear active team in config.toml
./build/truestamp config path|show|init
./build/truestamp upgrade [--check] [--yes] [--version vX.Y.Z]
./build/truestamp version # includes detected install method (homebrew / go install / install.sh / unknown)
# Global flags: --no-color, --no-upgrade-check, --http-timeout 30s, --config path, --api-url, --api-key, --team, --keyring-url# Recompute a Truestamp claims_hash locally (equivalent one-liner)
truestamp jcs < claims.json | truestamp hash --prefix 0x11 -a sha256 --style bare --no-filename
truestamp hash --prefix 0x11 --jcs -a sha256 --style bare --no-filename < claims.json
# Convert a JSON proof to deterministic CBOR and verify
truestamp convert proof --to cbor proof.json | truestamp verify --skip-external
# Derive the 4-byte kid fingerprint from an Ed25519 pubkey
truestamp convert keyid CTwMqDZnPd/QTLSq8aTeSD3a+j2DQxKcGfhhIYJQ65Y=
# sha256sum-compatible drop-in
truestamp hash doc.pdf # "<hex> doc.pdf"
truestamp hash --style bsd -a sha512 doc.pdf # "SHA512 (doc.pdf) = <hex>"Exit code 0 = all checks passed, 1 = failure.
truestamp verify proof.json # Local file path
truestamp verify https://host/p.json # URL (auto-detected from positional arg)
truestamp verify --file proof.json # Explicit file path
truestamp verify --file # Interactive file picker (huh FilePicker)
truestamp verify --url https://... # Explicit URL download
truestamp verify --url # Interactive URL prompt (huh Input)
cat proof.json | truestamp verify # Pipe from stdin
truestamp verify # No input -> shows help
Settings are resolved in priority order (highest priority last):
- Compiled defaults
- Config file (
~/.config/truestamp/config.tomlor$XDG_CONFIG_HOME/truestamp/config.toml) - Environment variables (
TRUESTAMP_prefix) - CLI flags (only explicitly set flags override)
| Command | Description |
|---|---|
truestamp config path |
Print the config file path |
truestamp config show |
Print the resolved configuration (API key masked) |
truestamp config init |
Create default config.toml if it doesn't exist |
| Flag | Env Var | Default | Description |
|---|---|---|---|
--config |
~/.config/truestamp/config.toml |
Path to config file | |
--api-url |
TRUESTAMP_API_URL |
https://www.truestamp.com/api/json |
Base URL of the Truestamp API |
--api-key |
TRUESTAMP_API_KEY |
"" |
API key for authentication |
--keyring-url |
TRUESTAMP_KEYRING_URL |
https://www.truestamp.com/.well-known/keyring.json |
URL of keyring endpoint |
--http-timeout |
TRUESTAMP_HTTP_TIMEOUT |
10s |
HTTP timeout for external API calls |
--no-color |
NO_COLOR |
false |
Disable all color and ANSI output |
--no-upgrade-check |
TRUESTAMP_NO_UPGRADE_CHECK |
false |
Disable the once-per-day "new version available" notice |
(config / env only; cosign_path in TOML) |
TRUESTAMP_COSIGN_PATH |
"" |
Absolute path to the cosign binary used by truestamp upgrade. Empty = $PATH lookup. Relative paths rejected at config load. |
| Flag | Env Var | Default | Description |
|---|---|---|---|
--file [path] |
Proof file path; interactive picker if no path given | ||
--url [url] |
Download proof from URL; interactive prompt if no URL given | ||
--hash |
Expected claims hash (hex) to compare against proof | ||
--type |
Assert expected subject type: item | entropy_nist | entropy_stellar | entropy_bitcoin | block | beacon. Local mode: mismatch surfaces as a "Subject Type" failure step. Remote mode: the value is forwarded to the server's /proof/verify type arg; the server rejects with HTTP 4xx + meta.code=subject_type_mismatch if the posted bundle's t disagrees. Smart default: when --type is omitted and the input filename / URL basename matches the truestamp-<stem>-<id>.<ext> download convention (e.g. truestamp-beacon-019d….json), the stem is translated back to the matching wire type (entropy-nist → entropy_nist etc.) and used as if the flag had been passed. A faint stderr hint (inferred --type X from filename "…") is emitted so a Subject Type mismatch arising from a renamed / swapped file is traceable to the inference. Pass --type explicitly to override. |
||
--silent / -s |
TRUESTAMP_VERIFY_SILENT |
false |
No output, exit code only |
--json |
TRUESTAMP_VERIFY_JSON |
false |
Output results as JSON |
--skip-external |
TRUESTAMP_VERIFY_SKIP_EXTERNAL |
false |
Skip all external API verification |
--skip-signatures |
TRUESTAMP_VERIFY_SKIP_SIGNATURES |
false |
Skip proof signature and keyring verification |
| Flag | Env Var | Default | Description |
|---|---|---|---|
--algorithm / -a |
TRUESTAMP_HASH_ALGORITHM |
sha256 |
md5 | sha1 | sha224 | sha256 | sha384 | sha512 | sha3-224 | sha3-256 | sha3-384 | sha3-512 | blake2s-256 | blake2b-256 | blake2b-384 | blake2b-512 |
--encoding / -e |
TRUESTAMP_HASH_ENCODING |
hex |
Digest encoding: hex | base64 | base64url |
--style |
TRUESTAMP_HASH_STYLE |
gnu |
Output style: gnu (sha256sum-compatible) | bsd (shasum --tag) | bare |
--binary |
false |
gnu style: emit <hex> *<filename> instead of <hex> <filename> |
|
--file [path] |
Input file; interactive picker if no path given | ||
--url [url] |
Download URL; interactive prompt if no URL given | ||
--prefix |
One-byte domain prefix (e.g. 0x11) prepended before hashing |
||
--jcs |
false |
Apply RFC 8785 canonicalization to JSON input before hashing | |
--json |
false |
Structured JSON output | |
--silent / -s |
false |
No output, exit code only | |
--no-filename |
false |
Omit the filename from gnu/bsd output | |
--list |
Print supported algorithms and exit |
MD5 and SHA-1 emit a one-line stderr warning when selected, suppressed under --json or --silent. Algorithm output is byte-identical to sha256sum, md5sum, and shasum --tag for the corresponding vectors; tests in internal/hashing/hashing_test.go anchor against the canonical NIST FIPS 180-4, FIPS 202, RFC 6234, RFC 7693 vectors.
| Flag | Default | Description |
|---|---|---|
--type |
"" (smart default, see below) |
item | entropy_nist | entropy_stellar | entropy_bitcoin | block | beacon. Sent verbatim to the server's /proof/generate type field. |
--format / -f |
json |
json or cbor. |
--output / -o |
auto-generated | Output file path. When unset, the filename is truestamp-<stem>-<id>.<ext>. |
When --type is omitted the CLI applies a client-side smart default based on the id shape: ULID ids default to --type item (the only unambiguous case); UUIDv7 ids fail fast with a helpful error listing the five valid types (entropy_nist | entropy_stellar | entropy_bitcoin | block | beacon). There is no "auto" — the server's strict-type cutover rejects it, and the CLI follows the same contract.
Filename stem convention: wire values use underscores (entropy_nist) to match the server enum; filename stems translate _ → - for readable filenames (truestamp-entropy-nist-<id>.json). Other types (item, block, beacon) contain no underscores and pass through unchanged.
Pre-flight id-shape validation: --type item requires a ULID; every other type requires a UUIDv7. Mismatches are caught client-side before the network call with a targeted error instead of a generic server 422.
Post-download card emits two public-web hint lines:
Details → {host}/<subject-path>/<id>— the subject's own detail page (/items/,/entropy/,/blocks/). Beacon downloads route to/blocks/<id>because the hash-keyed/beacons/<hash>form requires computing the block hash from bundle bytes; the beacon listing card uses that form instead since it has the hash directly from the API.Verify → {host}/verify/<type>/<id>— the typed-sub-path verify landing page from the t=11 cutover. Same URL format thecreatecard and theverifyreport emit.
The beacon, download, create, and verify cards all share the ui.SubjectDetailURL / ui.SubjectVerifyURL / ui.BeaconDetailURL / ui.BeaconVerifyURL helpers in internal/ui/weburls.go. Every helper goes through one publicWebBase function that strips a trailing /api/json and emits the URL unconditionally — localhost, 127.0.0.1, and plain-http hosts all render URLs so developers can click through against their dev server. The small tradeoff (a dev-host URL may appear in a shared transcript) is accepted by design.
Every post-action card uses ui.CompactTable() which returns a lipgloss table with HiddenBorder plus BorderTop/Bottom/Left/Right(false). Without the false flags, HiddenBorder still emits invisible top/bottom border rows that stack with section separators and double the apparent vertical gap between a section header and its first row. Using CompactTable keeps the table content flush to whatever precedes/follows it, letting callers control inter-section spacing explicitly with "" elements in strings.Join.
| Command | Flag | Default | Description |
|---|---|---|---|
encode |
--from |
binary |
Input encoding: binary | hex | base64 | base64url |
encode |
--to |
hex |
Output encoding (same set) |
decode |
--from |
hex |
Input encoding (same set) |
decode |
--to |
binary |
Output encoding (same set) |
jcs |
--newline |
false |
Append a trailing \n to the canonical output |
| all three | --file [path], --url [url], --json, --silent / -s |
Same six-mode input convention as verify |
| Sub-command | Key flags |
|---|---|
convert time [input] |
--from auto|rfc3339|unix-{s,ms,us,ns}, --to-zone <IANA>, --format rfc3339|unix-*|<Go layout> |
convert proof [file] |
--from auto|json|cbor, --to json|cbor (required), --compact |
convert id [value] |
--type auto|ulid|uuid7, --extract time|raw, --to-zone <IANA> |
convert keyid [pubkey] |
--from auto|hex|base64|base64url |
convert merkle [ip-value] |
--json, --silent (positional or stdin) |
Env vars: TRUESTAMP_CONVERT_TIME_ZONE sets the default --to-zone for convert time and convert id.
All Truestamp proofs use a compact format with short keys. Bundle version v is 1. The top-level integer field t discriminates subject types. Normative references in the truestamp-v2 reference repo: docs/PROOF_FORMAT.md (authoritative wire spec) and docs/PROOF_FORMAT_IMPLEMENTERS_GUIDE.md (self-contained cross-implementation guide).
| Code | Name | Category |
|---|---|---|
| 10 | block | subject |
| 11 | beacon | subject |
| 20 | item | subject |
| 30 | entropy_nist | subject |
| 31 | entropy_stellar | subject |
| 32 | entropy_bitcoin | subject |
| 40 | commitment_stellar | external commitment |
| 41 | commitment_bitcoin | external commitment |
The single source of truth lives at internal/proof/ptype/ptype.go. Every caller branches on these integers, never on strings.
Block (t=10) and beacon (t=11) share the same wire shape (no s, no ip, b + cx only) — the type code is the only discriminator. Verify-pipeline guards use ptype.IsBlockLikeSubject / ProofBundle.IsBlockLike() instead of strict IsBlock(). Because the t byte is part of the signing payload, a t=10 and t=11 bundle for the same underlying block produce different signatures; this is intentional cryptographic domain separation.
{
"v": 1,
"t": 20,
"ts": "2026-04-06T23:25:06Z",
"pk": "base64(32-byte Ed25519 pubkey)",
"sig": "base64(64-byte Ed25519 sig over proof_hash)",
"s": { "id": "ULID|UUIDv7", "d": { subject_data... }, "mh": "hex64", "kid": "hex8" },
"ip": "base64url(compact Merkle proof)",
"b": { "id": "uuid", "ph": "hex64", "mr": "hex64", "mh": "hex64", "kid": "hex8" },
"cx": [
{ "t": 40, "net": "testnet|public", "tx": "hex64", "memo": "hex64", "l": 1800000, "ts": "iso8601", "ep": "base64url" },
{ "t": 41, "net": "regtest|testnet|mainnet", "tx": "hex64", "op": "hex64", "h": 850000, "rtx": "hex_var", "txp": "hex_var", "bmr": "hex64", "ep": "base64url" }
]
}Item proofs (t=20) use domain prefixes 0x11 / 0x13 for subject-data / composite. Entropy proofs (t ∈ {30,31,32}) use 0x21 / 0x23.
Block (t=10) and beacon (t=11) share the same wire shape — no s key, no ip key — because the block IS the subject, so subject_hash == block_hash in the signed payload.
{
"v": 1,
"t": 10, // or 11 for beacon
"ts": "2026-04-06T23:25:06Z",
"pk": "base64(...)",
"sig": "base64(...)", // differs between t=10 and t=11 for the same block
"b": { "id": "uuid", "ph": "hex64", "mr": "hex64", "mh": "hex64", "kid": "hex8" },
"cx": [ ... at least one commitment ... ]
}A beacon is now its own subject type code (t=11) alongside plain block (t=10). Both share the structural shape above but are cryptographically distinct: the t byte lives in the signing payload, so a block and beacon bundle for the same underlying block have different signatures. Flipping t from 10 to 11 on a bundle without re-signing breaks verification.
truestamp beacon {latest|list|get|by-hash}reads the compact metadata projection{id, hash, timestamp, previous_hash}atGET /api/json/beacons/*(seetruestamp-v2/docs/BEACONS_API_IMPLEMENTERS_GUIDE.md). The single-beacon card prints two shareable public-web links:Details → {host}/beacons/<hash>(the beacon detail page keyed by hash) andVerify → {host}/verify/beacon/<id>(the typed-sub-path verify landing page introduced in the t=11 cutover). URLs render unconditionally — localhost and plain-http hosts too — so the links are visible when developing against a local server.truestamp download --type beacon <uuidv7>fetches a fullt=11proof bundle labelledtruestamp-beacon-*on disk. The wire request sendsdata.type = "beacon"verbatim; the server returns at=11bundle that self-describes.truestamp verifydispatches beacon proofs through the same pipeline as block proofs (no subject-hash derivation, no inclusion-proof walk,subject_hash == block_hash). The Type row of the report reads "Beacon" fort=11and "Block" fort=10.
v == 1,t ∈ {10, 11, 20, 30, 31, 32},cxnon-empty, everycx[i].t ∈ {40, 41}.t ∈ {10, 11}(block-like: plain block and beacon) ⇒sabsent,ipabsent.t ∉ {10, 11}(item and entropy subtypes) ⇒spresent (id/d/mh/kidall required),ipnon-empty.- Subject
kidmay differ from blockkidunder legitimate key rotation; no equality assertion is made. Subject-kid tampering is still detected becausekidis an input to the 0x13 / 0x23 composite hash. - Stellar
netis strict: only"testnet"and"public"are accepted.
-
Signing Key -- Decode base64
pk, derivekid = truncate4(SHA256(0x51 || pubkey)), verify against keyring endpoint. Skipped with--skip-signaturesor--skip-external. -
Structure --
tis a registered subject code, block present with ID and merkle_root,cxis non-empty. 3-6. Subject Data -- Skipped entirely whent ∈ {10, 11}(block-like subjects).- Item proofs (
t == 20): Claims HashSHA256(0x11 || JCS(claims)), Claims Hash Type validation, Claims Timestamp check, Item HashSHA256(0x13 || len32(id) || len32(claims_hash) || len32(metadata_hash) || len32(signing_key_id)). - Entropy proofs (
t ∈ {30,31,32}): Entropy HashSHA256(0x21 || JCS(entropy)), Observation HashSHA256(0x23 || len32(id) || len32(entropy_hash) || len32(metadata_hash) || len32(signing_key_id)).
- Item proofs (
-
Inclusion Proof -- Decode compact base64url
ip, RFC 6962 walk: leaf =SHA256(0x00 || subject_hash_bytes), internal =SHA256(0x01 || left || right). Root must matchb.mr. Skipped entirely whent ∈ {10, 11}. -
Block Hash -- Derive block hash:
SHA256(0x32 || len32(id) || len32(ph) || len32(mr) || len32(mh) || len32(kid)). For block-like subjects (t ∈ {10, 11}),subject_hash = block_hash. -
Epoch Proofs -- For each
cxentry: decodeep, RFC 6962 Merkle walk using block_hash as leaf, root must matchcx.memo(Stellar,t=40) orcx.op(Bitcoin,t=41). -
Proof Signature -- Build binary payload (big-endian throughout):
v(1) || t(2) || kid(4) || ts_ms(8) || subject_hash(32) || block_hash(32) || N(2) || epoch_roots(32*N)ts_ms= milliseconds since Unix epoch (uint64 BE) fromts. ComputeSHA256(0x61 || payload). Verify Ed25519 signature. Skipped with--skip-signatures. Flippingtin a bundle without re-signing breaks the signature — this is the mechanism that separates beacon (t=11) signatures from block (t=10) signatures for the same underlying block. -
Temporal Window -- Skipped entirely when
t ∈ {10, 11}.- Item proofs: item time (ULID) before committed block time (UUIDv7).
- Entropy proofs: entropy observation time (UUIDv7) before committed block time (UUIDv7).
-
Temporal Info -- Extract display timestamps (submitted/captured/committed).
-
Entropy Source -- For entropy subjects, confirm
s.dmatches the canonical published value at the upstream source. Skipped under--skip-external.
- NIST Beacon (
t=30):GET beacon.nist.gov/beacon/2.0/chain/{chainIndex}/pulse/{pulseIndex}; byte-compareoutputValueandtimeStamp. RSA signature / X.509 chain verification is out of scope — the server stores only the minimal pulse fields (chainIndex,pulseIndex,outputValue,timeStamp,version), so the bytes needed to reconstruct NIST's signed pre-image aren't in the bundle. - Stellar ledger (
t=31):GET {horizon}/ledgers/{sequence}; byte-comparehashandclosed_at. Network is auto-derived from the bundle's Stellar commitment incx[](testnetin dev,publicin prod) — a given Truestamp deployment uses one Stellar network for both entropy and commitment. - Bitcoin block (
t=32):GET blockstream.info/api/block/{hash}; byte-compareheightandtime. Always pinned to mainnet regardless of the commitment chain's network — the server observes mainnet as the authoritative public-randomness source even in dev deployments that commit to regtest/testnet.
- Stellar Commitment -- External: GET Horizon API, verify memo and ledger number. Only
testnet/publicnetworks accepted. - Bitcoin Commitment -- Local: OP_RETURN extraction, txid computation, CMerkleBlock parsing, partial merkle tree verification. External: GET Blockstream API for mainnet/testnet (skipped for regtest).
The legacy "subject kid must equal block kid" check has been removed: key rotation can legitimately produce divergent kids, and subject-kid tampering is still detected because kid is an input to the subject composite hash (0x13 / 0x23).
Enabled by default, skipped with --skip-external:
| Service | When | URL |
|---|---|---|
| Truestamp Keyring | Always (Step 1) | {keyring-url} (full URL, default includes .well-known path) |
| Stellar Horizon | Stellar commitment exists | https://horizon-testnet.stellar.org or https://horizon.stellar.org |
| Blockstream | Bitcoin mainnet/testnet | https://blockstream.info/api or https://blockstream.info/testnet/api |
Bitcoin regtest has no public API -- local crypto verification only.
main.go Entry point
cmd/
root.go Cobra root command, persistent flags, config loading, PostRun hook for passive upgrade notices
verify.go Verify subcommand (uses inputsrc for the six-mode input resolver)
create.go Create subcommand (item registration; shares inputsrc sentinels)
auth.go Auth login/logout/status subcommand
beacon.go beacon parent + `latest` default (maps `truestamp beacon` → latest)
beacon_list.go beacon list (table rendering, TTY-aware hash truncation)
beacon_get.go beacon get (client-side UUIDv7 validation)
beacon_by_hash.go beacon by-hash (client-side 64-hex validation)
beacon_test.go CLI integration tests for beacon (httptest + subprocess)
download.go download subcommand (server auto-routes on id; --type auto|item|entropy|block|beacon)
download_test.go CLI integration tests for download (all six --type variants, shape pre-flight errors, hyphenated entropy filenames)
hash.go Hash subcommand (SHA-2/3, BLAKE2, MD5/SHA-1; --prefix, --jcs, --style gnu|bsd|bare)
codec.go Encode / Decode / JCS sub-commands (all share a thin resolver)
convert.go Convert parent (no-op; aggregates children)
convert_time.go convert time (IANA zones, unix-{s,ms,us,ns}, Go time layouts)
convert_proof.go convert proof (JSON <-> deterministic CBOR round-trip)
convert_id.go convert id (ULID / UUIDv7 timestamp extraction)
convert_keyid.go convert keyid (Ed25519 pubkey -> 4-byte Truestamp kid)
convert_merkle.go convert merkle (decode compact base64url Merkle proof)
config.go Config subcommand (path, show, init)
console.go Console subcommand (Bubble Tea TUI over authenticated WebSocket; --ws-url, --log-level, --log-file flags). See docs/engineering/console.md.
upgrade.go Upgrade subcommand + flags (install-method routing, exitCodeErr contract)
upgrade_test.go upgradeInstructionFor routing + readYes + ExitCode tests
verify_test.go CLI integration tests (builds binary, tests exit codes)
hash_test.go hash integration tests (NIST vectors, sha256sum cross-check)
codec_test.go encode/decode/jcs integration tests
convert_test.go convert integration tests (time zones, ID extraction, proof round-trip)
internal/
config/
config.go koanf-based config loading, XDG paths, validation
defaults/
config.toml Embedded default config (go:embed)
beacons/
client.go Thin HTTP client for the Beacons JSON:API (Latest/List/Get/ByHash); typed errors + APIError
client_test.go httptest tests for all four endpoints incl. envelope variants, JSON:API error parsing, 429 Retry-After
fuzz_test.go Fuzz targets for the JSON parser (single + list) and the UUIDv7 / 64-hex validators
inputsrc/
inputsrc.go Shared six-mode input resolver (positional | --file [path] | --file picker | --url [url] | --url prompt | stdin pipe); Resolve and ResolveStream
inputsrc_test.go Unit tests for all six modes and precedence
encoding/
encoding.go Encoding enum + Encode/Decode for hex/base64/base64url/binary; RFC 4648 semantics
encoding_test.go RFC 4648 §10 "foobar" vectors + cross-encoding rejection
hashing/
hashing.go Algorithm registry (14 algorithms) + streaming Compute + FormatGNU/FormatBSD
hashing_test.go NIST FIPS 180-4 / 202 + RFC 6234 / 7693 canonical vectors + filename escaping tests
install/
detect.go Install-method detection (Homebrew / GoInstall / InstallScript / Unknown) via path + debug.BuildInfo
detect_test.go Detection heuristics + sameDir symlink regression test
selfupgrade/
selfupgrade.go Orchestrator: Check() + Upgrade() with install.sh parity
github.go GitHub Releases API client (honors GITHUB_TOKEN)
semver.go Minimal semver parse + compare (pre-release detection for Layer-2 defense)
verify.go SHA-256 (mandatory, pure Go) + cosign subprocess (best-effort)
extract.go tar.gz extraction with path-traversal rejection
replace_unix.go Atomic rename + .bak.<ts> backup + darwin quarantine clear + 7-day prune
replace_windows.go Stub returning ErrReplaceUnsupported (Windows is print-only)
check_test.go httptest-stubbed Check() tests (upgrade available / up-to-date / pre-release Layer-1 + Layer-2 / --version pin bypass)
extract_test.go, semver_test.go, verify_test.go Unit tests
upgradecheck/
upgradecheck.go Passive once-per-24h check; suppression rules (CI / non-TTY / dev / flag / env)
cache.go $XDG_CACHE_HOME/truestamp/upgrade-check.json with atomic rename
upgradecheck_test.go All suppression paths + cache round-trip + emit logic
proof/
types.go Proof bundle struct types (compact JSON tags)
parse.go JSON parsing with json.RawMessage for JCS
binary.go CBOR parsing (read)
marshal_cbor.go Deterministic CBOR encoding (RFC 8949 §4.2, self-describing tag 55799); per-field byte-string policy
marshal_cbor_test.go CBOR round-trip canonicalization tests
id.go Syntactic id shape detection (ULID vs UUIDv7) — pre-flight only; server decides subject type
download.go URL download + /proof/generate client; GenerateCtx takes subject type (auto|item|entropy|block); InspectBundleType reads returned t
parse_test.go Parsing validation tests
tscrypto/
hash.go Domain-separated hashing (0x11-0x61), len_prefix, key_id, BuildCompactProofPayload
signature.go Ed25519 verification
merkle.go RFC 6962 Merkle proof walk + compact proof decoder
*_test.go Crypto unit tests with known test vectors
bitcoin/
parse.go Raw tx parsing, OP_RETURN, txid computation, txoutproof parsing
merkle.go Bitcoin partial Merkle tree verification
*_test.go Bitcoin unit tests with real transaction data
external/
keyring.go GET keyring endpoint, configurable HTTP timeout
stellar.go GET Horizon API
bitcoin.go GET Blockstream API
external_test.go HTTP mock tests (httptest)
httpclient/
client.go Shared HTTP client, GetJSONCtx for small JSON responses
download.go DownloadCtx (streams to disk, 200MB cap) + DownloadBytesCtx (in-memory, cap configurable)
download_test.go httptest-server tests for happy path, oversize, HTTP error, context cancellation
console/
app.go Bubble Tea root model, header (with reconnect countdown + server-time clock), footer key hints, pane switching
monitor.go Monitor pane: stream toggle list + scrollable / reversible event waterfall (24h time-windowed retention, 100k hard cap)
newitem.go New Item form pane: 4-field input + items.create round-trip + per-item lifecycle card (capped at 100 transitions)
connection.go Connection pane: scope, push counts, reconnect summary, log file path
messages.go tea.Msg types + waitForPush bridge between WS reader goroutine and tea.Update; routes server-time ticks, rejoin events, reconnect status
wschannel/
client.go Homegrown Phoenix Channels V2 client. Multi-topic on one socket; reconnect-with-backoff (1→2→5→10→30s); rejoinAllTopics replay; api_key redaction; two-stage readiness gate (socketReady / sessionReady); pending-call drain on disconnect
codec.go Phoenix V2 array-form Frame encoder/decoder + ParseReply
redact_test.go Security-critical: api_key never leaks in logs OR returned errors
smoke_test.go Live-server tests gated behind `smoke` build tag (TestSmokeConsoleLobby, TestSmokeClockTopic, TestSmokeLiveBlock, TestSmokeReconnect)
logging/
logging.go slog + lumberjack file logger (10MB rotation, 5 backups, 14d retention, gzip). Default path: $UserCacheDir/truestamp/console.log. Also exports DefaultPath() for flag help text.
ui/
ui.go Shared lipgloss v2 styling: renderer, adaptive colors, components
verify/
verify.go Pure verification logic -- returns Report, no I/O
report.go Step, Status, Report types
presenter.go Lipgloss v2 rendering of Report (table, list, tree)
json_output.go Structured JSON output for --json mode
remote.go Remote API verification
verify_test.go Orchestrator unit tests
- Logic/presentation separation:
verify.gobuilds aReportwithStepentries (pure logic, no I/O).presenter.gorenders the report to stdout using lipgloss v2 table/list/tree.report.godefines the types andPassed()/FailedCount()methods. - Shared UI package:
internal/ui/ui.goprovides the shared lipgloss v2 styling foundation (adaptive color palette, HeaderBox, SectionHeader, banners). Usescompat.AdaptiveColorfor light/dark terminal support.--no-colorflag setslipgloss.Writer.Profile = NoTTYto strip all ANSI. TheNO_COLORenv var is natively respected by lipgloss. tscryptopackage: Named to avoid shadowing Go's stdlibcryptopackage. No import alias needed.- Error handling:
Run()returns(*Report, error).errorfor structural failures (missing file, bad JSON). Individual verification failures areStatusFailsteps in the report. - CLI integration tests:
cmd/verify_test.gobuilds the actual binary inTestMainand runs it as a subprocess to test real exit codes and silent mode. - Upgrade subcommand is install-method aware:
internal/install.Detect()classifies the running binary by inspecting the resolved executable path (symlinks followed) against Homebrew prefixes (/opt/homebrew/Cellar/,/usr/local/Cellar/,/home/linuxbrew/.linuxbrew/),$GOBIN/$GOPATH/bin/$HOME/go/bin, and standard install.sh destinations (/usr/local/bin,$HOME/.local/bin). Combined withruntime/debug.BuildInfo(Main.Version + absence of vcs.revision) as a second signal forgo installbuilds. Homebrew andgo installusers get printed instructions; install.sh / manual / Unknown-but-writable users get a native-Go in-place upgrade that mirrorsdocs/install.shbyte-for-byte: SHA-256 mandatory, cosign best-effort (shell-out tocosign verify-blobif on PATH, required whenTRUESTAMP_REQUIRE_COSIGN=1; operators can pin an absolute path viacosign_pathin config.toml /TRUESTAMP_COSIGN_PATHenv var to avoid$PATHhijacking —config.Loadvalidates the path is absolute,selfupgrade.resolveCosignBinaryre-checks existence + exec bit at use time), atomic rename with.bak.<rfc3339>backup, darwin quarantine xattr cleared viaxattr -d com.apple.quarantine. - Pre-release defense is two-layer: GitHub's
/releases/latestendpoint already filters out releases flagged asprerelease: true(Layer 1). As a second defense (Layer 2),selfupgrade.Check()parses the resolved tag and rejects anything with a semver pre-release suffix (v1.0.0-rc.1, etc.) unless the user passed--versionexplicitly. The passive upgrade notice ininternal/upgradecheckalso refuses to surface pre-release "latests". Do not weaken either layer without adding the opt-in--preflag that's listed as future work. - Passive upgrade notices are stderr-only:
internal/upgradecheck.MaybeNotifyis wired into the Cobra root'sPersistentPostRun. It spawns an async goroutine with a 2-second deadline and a 500ms wait budget on the main goroutine — if the GitHub API doesn't answer quickly, nothing is printed this invocation but the background fetch still populates the cache for next time. Suppression triggers:--no-upgrade-checkflag,TRUESTAMP_NO_UPGRADE_CHECKenv, any of 7 CI env vars (CI,GITHUB_ACTIONS,GITLAB_CI,CIRCLECI,BUILDKITE,JENKINS_HOME,TF_BUILD), non-TTY stderr, current version equal todev, and pre-release latests. - Windows is print-only for
upgrade:selfupgrade/replace_windows.gois a stub returningErrReplaceUnsupported.cmd/upgrade.go:upgradeInstructionForshort-circuits onruntime.GOOS == "windows"and always prints thego install ...@latestcommand regardless of detected method. In-place upgrade on Windows (rename-running-exe-to-.bak trick) is explicit future work, not v1. - Exit codes for
upgrade --check: 0 up-to-date, 1 upgrade available, 2 network error, 3 pre-release latest. The contract lives incmd/upgrade.go's constants +cmd/root.go'sExecute()which recognizesexitCodeErrand propagates the code without printing an error message. - Shared input-selection helper:
internal/inputsrc.Resolve/.ResolveStreamare the single place the six-mode input pattern lives.verify,create(via sentinel constants),hash,encode,decode,jcs, and theconvertchildren all route through it. TheFilePickSentinel = "(pick)"andURLPromptSentinel = "(prompt)"constants are pflagNoOptDefValvalues chosen for readable--helpoutput; they are effectively impossible as real filenames/URLs. - Hash command is sha256sum-compatible by default:
--style gnu(the default) emits byte-identical output to GNU coreutils' sha256sum (text mode:<hex> <filename>\n, binary mode with--binary:<hex> *<filename>\n, filenames with\or\nget the standard\line-prefix +\\/\nescaping).--style bsdmatchesshasum --tag. Cross-tool output equivalence is asserted incmd/hash_test.go. - Legacy algorithm warning:
truestamp hash -a md5and-a sha1emit a one-line stderr warning (warning: <algo> is cryptographically broken and unsuitable for security uses). Suppressed under--silentand--json. The algorithm set is driven byinternal/hashing.algorithmsand kept in sync withinternal/tscrypto/hash.go'shashTypesregistry (which is what the Truestamp backend accepts ashash_type). - CBOR marshal is deterministic:
internal/proof.ProofBundle.MarshalCBORusescbor.CoreDetEncOptions(RFC 8949 §4.2 — sorted keys, shortest-form ints, definite-length containers) and prepends the self-describing tag 55799 (0xd9d9f7) soIsCBORProofkeeps detecting the output on round-trip. A first pass throughParseCBOR → MarshalCBORmay normalize a non-deterministic source; a second pass is byte-stable. Byte-valued JSON fields (pk,sig, all hashes, epoch/inclusion proofs) are re-encoded as CBOR major-type-2 byte strings per the per-field policy table above. - Fuzz coverage on every parser: 64
FuzzXxxtargets (cmd:8, tscrypto:11, proof:7, bitcoin:6, selfupgrade:5, encoding:5, hashing:4, beacons:4, items:3, config:3, verify:2, upgradecheck:2, inputsrc:2, external:2) cover every parser that touches attacker-controlled bytes (proof JSON/CBOR, base64/hex/base64url decoders, Merkle compact proof, Bitcoin tx + txoutproof, TOML config, tar.gz extract path traversal, time/ID/URL parsers). Seeds are committed;go test ./...replays them as regression tests on every run. Active fuzzing viatask fuzz-deep(default 15s per target; tune withDURATION=1m task fuzz-deep); discovered reproducers are written by the Go fuzz engine under each package'stestdata/fuzz/FuzzXxx/and MUST be committed so the regular test run replays them as permanent regressions.task fuzz-listprints the target inventory;find . -path '*/testdata/fuzz/*' -type fafter a fuzz-deep session lists any new reproducers to commit.
The gowebpki/jcs library operates on raw JSON bytes. The parser preserves json.RawMessage for the subject data (s.d) -- these are passed directly to jcs.Transform() without re-marshaling through Go structs (which would change field ordering or number formatting).
The signed payload is fixed-width, big-endian:
v(1) || t(2) || kid(4) || ts_ms(8) || subject_hash(32) || block_hash(32) || N(2) || epoch_roots(32*N)
t= subject type code (uint16 BE); cryptographic domain separation across subject types.kid= 4 raw bytes fromb.kidhex-decoded.ts_ms= milliseconds since Unix epoch (uint64 BE) fromtsISO 8601.- For
t == 10,subject_hash == block_hash. - Signed as:
Ed25519.verify(SHA256(0x61 || payload), pk).
The CLI exposes team discovery and selection in two places that share the same on-disk source of truth (the top-level team key in config.toml):
truestamp teamsubcommand —list,show [id],set [id],unset.team listemits a four-column★/ID/NAME/ROLEtable overGET /api/json/memberships?include=team; the policy on the server side filters memberships to the actor's own (truestamp-v2:lib/truestamp/teams/membership.ex:218-224).team set <id>validates by reading/teams/{id}first, so a typo or revoked membership refuses to write.team setwith no arg opens ahuh.NewSelectpicker; cancelled (Esc) does not persist.team unsetclears the key, reverting to the server's personal-team auto-fallback.- Console
Teamspane (key3) — same membership table, plus ans-to-set confirmation that pushesscope.switch_teamover the live WebSocket. On success the catalog stream subscriptions get rebound server-side against the new tenant, item watches keep their original team binding, andconfig.SetTeam(id)persists. The pane auto-activates when the on-startup access check shows the configured team is no longer reachable, surfacing a red "membership lost" banner with a corrective hint.
config show and auth status both render Team Name and Team Role rows in addition to the bare id, falling back to a faint (unavailable) hint when offline so config show stays useful when the network or API key is degraded. The role lookup uses the same internal/teams.GetMyRoleOnTeam helper across all three surfaces so team list, auth status, and the Connection pane's scope row never disagree.
The first-run team picker fires only in two places: truestamp team set with no arg, and truestamp console launch when cfg.Team is empty and stdin is a TTY. Other commands keep their current pipe-friendly behaviour (read cfg.Team and call the API). Picking "Personal" stores the personal team's UUID explicitly so subsequent requests carry the tenant header instead of relying on the server's personal-team auto-fallback.
The CLI does not create teams. team list's empty-state and the "no memberships" picker render a hint pointing at the public-web /teams URL — creation lives in the web app today.
This CLI verifies proofs generated by the Truestamp service, which is maintained in a separate repository at truestamp/truestamp-v2. Paths below are relative to that repo:
- Proof generation:
lib/truestamp/proof.ex—Truestamp.Proof.generate/1 - Proof tests:
test/truestamp/proof_test.exs— tests including JSON round-trip signature verification - Hash spec:
docs/CRYPTOGRAPHY.md— byte prefix registry, wire formats, test vectors - Verification reference:
proof.livemd— interactive Livebook that verifies the chain using the running app - Bitcoin parsers reference:
lib/truestamp/bitcoin/—TransactionParser,TxOutProofParser,BitcoinMerkle,BinaryHelpers - Elixir verifier:
clients/elixir/verify_proof.exs— reference implementation (same verification logic)
When changing verification logic, ensure it stays consistent with:
truestamp-v2/docs/CRYPTOGRAPHY.mdfor hash domain prefixes and wire formatstruestamp-v2/lib/truestamp/proof.exfor the proof bundle structuretruestamp-v2/clients/elixir/verify_proof.exsfor the reference Elixir implementationtruestamp-v2/proof.livemdfor the verification flow
This CLI is standalone. All crypto uses Go stdlib (crypto/sha256, crypto/ed25519) plus gowebpki/jcs for RFC 8785 canonicalization. External deps: cobra (CLI), koanf (config), gofrs/uuid (UUIDv7), oklog/ulid (ULID), lipgloss v2 (terminal styling). The truestamp upgrade subcommand is implemented in-house (internal/selfupgrade, internal/upgradecheck, internal/install) with stdlib-only crypto and an optional shell-out to the system cosign binary — no third-party upgrade library pulled in.