the most abused word in SOFTWARE
This is a sub-lesson under toolkit, and it exists to cash one cheque the parent page wrote. The toolkit lesson promised every toolkit is sandboxed, signed, shareable, and its FAQ said the signature tells you who shipped it. Fair — but you've heard that before. npm has signatures. App stores have signatures. Package registries have signatures. And supply-chain attacks happen anyway, constantly, on signed ecosystems.
So the honest reader's question isn't is it signed — that word has been drained of meaning. The question is sharper, two parts: what does the signature actually bind, and what concretely refuses to run when it fails. A signature that proves "someone with an account uploaded these bytes" stops almost nothing. A signature that proves "this exact set of promises was made by this exact author, and the build aborts the instant a byte of those promises changes" stops a real class of attack. This page is the difference between the two, in mechanism.
the DEFINITION
1. for a toolkit: an Ed25519 did:key
signature over the canonical manifest body, binding the declared
contract — EXEC, CAPS, BUILD_SRC — to a
self-certifying author identity; enforced by a build gate that refuses
any unsigned or tampered third-party toolkit, and backed by the sandbox
that bounds what runs regardless.
Read that definition twice, because the surprising word is manifest, not code. The signature doesn't wrap the compiled bytes — it wraps the document where the toolkit states what it does. Everything else on this page falls out of that one choice.
two layers, distinct JOBS
The parent FAQ gave the compressed answer: "the signature tells you who shipped it, and the sandbox bounds what it can do regardless of who shipped it." That sentence is this whole page folded up. Unfolded, it's two layers that answer two different questions and defeat two different threats — and crucially, neither one substitutes for the other.
| layer | question it answers | what defeats it | what catches that |
|---|---|---|---|
| provenance the signature | who promised exactly what? | an author who mislabels their own toolkit, or runs code you imported knowingly | the sandbox — layer 2 bounds it regardless of the label |
| sandbox caps + isolation | what can happen, no matter who? | nothing about identity — it doesn't ask who, only what | provenance — layer 1 tells you who to blame and detects tampering |
The shape of that table is the thesis. Provenance is identity-aware and tamper-detecting, but it cannot stop a malicious author who honestly signs malicious code — it only proves the code is theirs. The sandbox is identity-blind, but it doesn't care whose code it is: a granted-caps box bounds a stranger's toolkit and a saint's toolkit identically. Stack them and the gap in each is covered by the other. The mistake every "we're signed, you're safe" pitch makes is shipping layer one and implying it does layer two's job. It can't. They're different jobs.
an identity that carries its own PROOF
Before the signature, the identity. In most signing systems the identity is a name that points at a key — you look up "acme-corp" in a registry, a server hands you their public key, and you trust the lookup. That lookup is the attack surface: a compromised registry hands you the wrong key. Here there is no lookup, because the identity is the public key. A tenant's identity is a real W3C did:key, built like this:
flowchart LR pk["Ed25519 public key
32 raw bytes"] mc["prefix multicodec
0xED 0x01"] b58["base58btc encode
(Bitcoin alphabet)"] tag["prepend multibase tag
did:key:z…"] pk --> mc --> b58 --> tag did["did:key:z6Mk…"] tag --> did style pk fill:#f3c5a3,stroke:#121316 style did fill:#13d943,stroke:#121316,stroke-width:2.5px
Walk that chain: take the 32-byte Ed25519 public key, glue the multicodec
prefix 0xed01 in front of it, base58btc-encode the result with the
Bitcoin alphabet, and prepend the multibase tag z. The string you
get — did:key:z6Mk… — is the key, encoded. It's the same
construction Radicle uses, the same the W3C spec describes. Nothing was
registered anywhere.
Which makes verification almost shockingly small. To check a signature, the
verifier decodes the public key straight back out of the DID string —
pattern-matching the 0xED,0x01 prefix off the front — and runs one
Erlang crypto call. That's the entire PKI:
def verify_sig(did, msg, sig),
do: :crypto.verify(:eddsa, :none, msg, sig, [pub_of_did(did), :ed25519])
defp pub_of_did("did:key:z" <> b58),
do: (<<0xED, 0x01, pub::binary-size(32)>> = decode58(b58); pub)
No certificate authority. No registry server. No account. No network call.
The verifier needs nothing but the manifest in front of it — the DID
carries its own proof, so anyone, including a browser with no server behind it,
can re-implement verification in a few lines. did:key is self-certifying;
the identifier and the key it certifies are the same object. (One detail for the
operators: WB_SIGNING_KEY, a base64 seed held as a Fly secret,
restores the same keypair — and therefore the same DID — across deploys, so a
toolkit signed last month still verifies this month.)
what the signature actually BINDS
Now the first aha. The signed bytes are the whole manifest body — and the manifest body is the declared contract. It's where the toolkit states, in plain org keywords, everything a consumer would audit before trusting it. The design comment in the source is blunt about why: the signature binds the declared contract to an author identity "so tampering with a vetted third-party toolkit is detectable." Here is a real manifest, with the signed region bracketed:
┌─ signed region (every byte below is under the signature) ─┐ #+TITLE: icons #+EXEC: command ← what it runs #+TRUST: third-party ← its declared posture #+CLI_BIN: icons ← the name it claims on the PATH #+BUILD_LANG: js #+BUILD_SRC: path:src ← where its bytes come from #+ARG_MODE: stdin1 #+CAPS: ← (empty — claims no capabilities) #+AUTHOR_DID: did:key:z6Mk… ← the identity, INSIDE the signed bytes └──────────────────────────────────────────────────────────┘ #+SIGNATURE: pSEsQk9…base64… ← the only line OUTSIDE
Every promise a consumer cares about is inside that bracket. EXEC
(the shape it runs as), CAPS (what it may touch), BUILD_SRC
and BUILD_LANG (where its bytes come from and how they're built),
CLI_BIN and ARG_MODE (how it's invoked),
SHA256 and WASM_PATH (the pinned artifact), and
TRUST itself. Flip any one of them after signing and the bytes no
longer match the signature.
Notice the empty #+CAPS: line is also inside the
bracket. That's the point of the whole scheme made concrete: an attacker's most
valuable edit — quietly adding net-fetch to a toolkit that
declared no network access — is exactly the byte change the signature catches.
The thing worth protecting was never the binary. It's the claim, because
the claim is what the consumer audited.
And the binding is transitive. When #+BUILD_SRC points at a
prebuilt artifact — wasm:<url> or archive:<url> —
the #+SHA256: pin sits inside the signed manifest too. The
source comment calls it plainly: "the sha pin is the supply-chain gate … a
compromised mirror cannot swap the binary." An unpinned fetch doesn't fail
silently either — it prints (UNPINNED — add #+SHA256: <hash> to the
manifest). So the signature reaches past the manifest to the exact bytes
the manifest fetches.
strip-then-SIGN
Depth rung — skippable, but it's where a subtle attack gets closed. There's a
chicken-and-egg problem in signing a file that must contain its own
signature: the moment you append the #+SIGNATURE: line, you've
changed the bytes you just signed. The answer is a small canonicalization dance
called strip-then-sign, and it's worth watching closely because one detail in it
defeats a real forgery.
sequenceDiagram participant A as author · wbx toolkit sign participant M as manifest.org participant C as consumer · manifest_provenance A->>M: read manifest body A->>A: strip existing SIGNATURE + AUTHOR_DID lines A->>A: append fresh #+AUTHOR_DID, then trim → canonical body A->>A: Ed25519 sign(canonical body) A->>M: write body + #+SIGNATURE line Note over M: AUTHOR_DID is INSIDE the signed body;
SIGNATURE is the only line outside C->>M: read manifest C->>C: strip only SIGNATURE lines, trim → same canonical body C->>C: verify_sig(AUTHOR_DID, body, sig) C-->>C: {:ok, did} or a named failure atom
Read the exchange as a story. The author's side strips any
existing signature and author lines first — that's what makes
re-signing idempotent, so signing the same toolkit twice yields a clean result
instead of stacking lines. Then it appends a fresh
#+AUTHOR_DID, trims, and signs that exact string. The consumer's
side strips only the SIGNATURE line, trims, and gets byte-for-byte
the same canonical body the author signed — so verification can succeed.
Here's the load-bearing detail. The #+AUTHOR_DID line is
inside the signed bytes. Why that matters: imagine an attacker who wants
to claim a vetted toolkit as their own. They swap the AUTHOR_DID to
their DID — but now the signed body has changed, so the original signature no
longer verifies, and they can't produce a new one without the original author's
private key. The DID can't be swapped without re-signing the whole manifest, and
re-signing needs a key they don't have. The identity is welded to the contract,
not stapled beside it.
the gate that says REFUSED
A signature nobody enforces is decoration. This one has teeth at three points in a toolkit's life — and the strongest one stops a bad toolkit before a single byte compiles. Trace a manifest through them:
flowchart TD
m["manifest.org"] --> t{"#+TRUST?"}
t -- "first-party
(or absent)" --> b1["build — no signature checked"]
t -- "third-party" --> p{"provenance valid?"}
p -- "ok :ok, did" --> b2["build · set_trust(:node)"]
p -- "fail" --> r["REFUSED — third-party toolkit
with invalid provenance"]
b2 --> esc["run escalated to :node
a separate BEAM VM"]
style m fill:#f3c5a3,stroke:#121316
style r fill:#e8857a,stroke:#121316,stroke-width:2.5px
style esc fill:#13d943,stroke:#121316
Follow the branches. A first-party toolkit (or one with no
#+TRUST line at all — absent defaults to first-party) takes the
left path and builds with no signature check. A third-party toolkit takes
the right: its provenance must verify, and the three enforcement points are
these.
- Verify —
wbx toolkit verifyappends one line to its report:✓ third-party signature valid (did:key:…)or✗ third-party provenance: <why> (sign with wb toolkit sign <id>). First-party toolkits get no such line — there's nothing to check. This is the advisory point; it tells you, but doesn't stop you. - Build — the hard gate. A third-party toolkit whose provenance
doesn't verify is refused with an exact string:
<id>: REFUSED — third-party toolkit with invalid provenance (<why>); author must wb toolkit sign <id>. The source comment: "an unsigned/tampered third-party toolkit never builds." Nothing downstream happens — the bytes never compile. - Run — even after a clean build, posture follows the toolkit into
execution.
set_trustrecords it, and a third-party command escalates one isolation rung to:node— a separate BEAM VM — instead of the normal local subprocess. If distribution can't come up it falls back gracefully to the local path: "fail-open on availability, never on the work." A stranger's toolkit runs one wall further out by default.
reading a FAILURE
Depth rung. When a build prints REFUSED … (<why>), the
<why> is one of four atoms, checked in order, each telling you
exactly which rung of verification fell through and how to fix it:
| atom | what it means | the fix |
|---|---|---|
:no_author_did | no #+AUTHOR_DID line — the manifest was never signed at all | wb toolkit sign <id> |
:no_signature | an author DID is present but no #+SIGNATURE line | wb toolkit sign <id> |
:bad_signature_encoding | the signature line isn't valid Base64 — corrupted or hand-edited | re-sign (wb toolkit sign) to overwrite it cleanly |
:bad_signature | decodes fine, but doesn't verify against the body — the manifest was changed after signing | re-sign — and ask who changed which line |
The verdict of that table: the first two atoms mean never signed,
and the fix is just to sign. The fourth, :bad_signature, is the
one that matters — it means the bytes and the signature disagree, which is
precisely the tamper case the whole scheme exists to catch. If you see it on a
toolkit you didn't change, the manifest changed under you, and the next question
isn't "how do I make this build" — it's "what got edited."
Put it all together as one real round trip. You sign a toolkit; an attacker
edits a single line — say flipping an empty #+CAPS: to
#+CAPS: net-fetch; the build refuses:
$ wbx toolkit sign huniq signed huniq as did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH # attacker edits one line: #+CAPS: → #+CAPS: net-fetch $ wbx toolkit build huniq huniq: REFUSED — third-party toolkit with invalid provenance (bad_signature); author must `wb toolkit sign huniq`
The edit could have been any manifest byte — EXEC,
BUILD_SRC, the SHA256 pin, even the
AUTHOR_DID itself. Every one of them lands you at
bad_signature, and every one of them is refused before compilation.
one key, three RAILS
Depth rung. The same tenant did:key isn't just a toolkit thing — it signs three surfaces across the ecosystem from one keypair, with three verify points. One identity, three rails:
flowchart TD k["one tenant keypair
Ed25519 · did:key"] k --> r1["toolkit manifests
Toolkits.sign_text"] k --> r2["published workbook HTML
embedded c2pa-style manifest
+ asset_sha256"] k --> r3["agent run ledgers
the attributable signed half"] r1 --> v1["build gate · wb toolkit verify"] r2 --> v2["install gate · require_signature: true
wbx verify <file.html>"] r3 --> v3["ledger verification"] style k fill:#13d943,stroke:#121316,stroke-width:2.5px style r2 fill:#f3c5a3,stroke:#121316
Read the fan-out. The middle rail is the one a consumer meets at install
time: a published workbook carries an embedded
application/workbooks-c2pa+json manifest — an Ed25519 signature
over canonical key-sorted JSON plus an asset_sha256 of the file
itself. At install, Library.install(blob, require_signature: true)
checks both the signature and that the artifact hasn't changed since:
a tampered bundle fails {:error, {:bad_signature, _}}, an unsigned
one {:error, {:unverifiable, _}}. A test proves it the honest way —
append a comment after signing and the integrity check catches it. On
the CLI, wbx verify <file.html> is pure-read, no key needed,
and prints valid=… signature=… asset_integrity=… issuer=did:key:….
The deliberate part: the manifest module states it outright — "no C2PA/X.509 stack, no Rust signer binary." The did:key supersedes the planned X.509 path. It is, roughly, C2PA's idea — provenance that travels with the bytes — without C2PA's certificate machinery. One small self-certifying key, three places it proves authorship, zero infrastructure to stand up.
why your own toolkits SKIP this
The obvious objection: if signing is so important, why does a first-party
toolkit skip every check? The answer is a deliberate threat-model decision, and
the source is candid about the tension. The discovery root —
$WB_TOOLKITS_ROOT or ./toolkits — is, in the comment's
own words, "an UNAUTHENTICATED, writable directory. A toolkit dropped there is
UNTRUSTED supply-chain input — NOT first-party by virtue of its location."
So location does not confer trust. What confers it is something else: for your own in-tree toolkits, the repo's own review-and-commit process is the trust mechanism. The comment finishes the thought — "location-trust is fine for our own repo." The signature exists for code that crossed an org boundary: a toolkit from a stranger, fetched from somewhere, that your review process never touched. For that, you need cryptographic provenance. For code that went through your pull requests, you already have a trust mechanism, and it's a better one than a signature: human review with history.
This is why promotion writes
#+TRUST: first-party — when you promote a toolkit you authored, it's
yours, and trust is relative to who's running it. The same toolkit a
third-party consumer installs would have them granting its #+CAPS
at install time. Trust isn't a property baked into the toolkit; it's the
relationship between the toolkit and the person running it.
the honest EDGES
This page owes some real limits, and stating them is the whole reason the thesis is two layers and not one.
- The trust label is self-declared.
#+TRUSTlives in the manifest the author controls. The gate detects tampering with a vetted third-party toolkit — it does not detect mislabeling. A malicious author who writesfirst-partyskips the signature check entirely. What saves you then isn't provenance — it's that you imported it knowingly, and that the sandbox and granted caps still bound it. Layer two, doing its job. - The signature covers the manifest, not the prose. Skills
(
skills/*.org) andpath:source directories aren't under the signature. What bounds them is the sha-pinned content-addressed artifact for prebuilts, the sandbox and granted caps at run time, and registration containment that rejects wasm outsidebuild/commands/. - There's no signed third-party catalog yet. Honestly: no in-tree toolkit currently carries a signature — all of them are first-party, by design. The third-party gate is exercised by tests and the install rail, not yet by a public signed catalog. The mechanism is real and tested; the ecosystem around it is young.
- Isolation fails open. The run-time escalation to
:nodefalls back to the local subprocess if distribution can't start. That's a deliberate availability choice — fail-open on availability, never on the work — but it means the extra wall is best-effort, not a guarantee.
Every one of those edges points at the same conclusion: the signature is a sharp tool for one job — who promised exactly what, and has it been changed — and a useless tool for the other — what can this code actually do to me. That second job belongs to the sandbox, explicit capability grants, reserved built-in names that can never be shadowed, and the sha pins. Provenance and sandbox: distinct jobs, layered, neither pretending to be the other.
questions people actually ASK
Do I need to sign my own first-party toolkits?
No — and not because we got lazy. Your repo's review-and-commit process is the trust mechanism for code you authored, and it's stronger than a signature (it has human judgment and history). The signature is for code that crossed an org boundary, where you have no review relationship with the author. Sign before you share a toolkit as third-party; you don't sign your own in-tree work.
What if I lose my signing key?
A fresh keypair mints a fresh DID — a new identity, with no claim on
anything the old one signed. That's why WB_SIGNING_KEY exists: a
base64 seed, held as a deploy secret, restores the same keypair and therefore
the same DID across redeploys, so prior signatures keep verifying. Lose the
seed and you don't lose the toolkits — you lose the ability to re-sign as the
same author.
Can someone re-sign my toolkit as theirs?
Yes — and that's honest, not a hole. They can strip your signature, write their own DID, and sign it with their key. What they cannot do is keep your signature while claiming authorship, because your DID is inside the signed bytes. The signature proves who shipped this exact contract, not copyright. If they re-sign, the manifest now provably says they shipped it — which is exactly what a provenance system should record.
Is this C2PA?
It's C2PA's idea without C2PA's machinery. Provenance that travels with the bytes — yes. Certificate chains, X.509, a Rust signer binary — deliberately no. The manifest module says so in as many words. A self-certifying did:key does the job a cert chain was going to do, in one crypto call and zero infrastructure.
What exactly breaks the signature?
Any byte of the manifest body. Flip an empty CAPS to grant
network, change the SHA256 pin, edit the EXEC shape,
even alter the AUTHOR_DID — every one lands at
bad_signature and is refused before a build. The
SIGNATURE line itself is the one line outside the signed
region; everything above it is welded shut.
If the signature can't stop a malicious author, what's the point?
Different job. The signature stops silent tampering with a toolkit you already vetted — the supply-chain attack where a mirror swaps bytes under a trusted name. It does not stop a known-bad author shipping known-bad code; the sandbox does that, by bounding what any toolkit can touch regardless of who wrote it. Two layers, distinct jobs — install neither alone.
keep GOING
Trust is one cheek of a two-layer face — provenance here, the sandbox next door. The parent page holds both.