ROBOT.md v0.2 Design — Signing, Registry Ingestion, Tamper-Evidence¶
Status: Draft design — awaiting user review
Author: Craig (craigm26) + Claude (drafting)
Target release: robot-md v0.2 / CLI 0.2.0
Supersedes (scope): SECURITY.md → "Known v0.1 Limitations"
Last updated: 2026-04-17
0. Preamble — what this document is and is not¶
This is a design document only. It commits to zero code. Every load-bearing cryptographic and registry decision below has at least one unresolved question that requires explicit user sign-off before implementation begins.
The v0.1.1 CLI shipped today fixed two silent-failure bugs (schema packaging, Cloudflare Pages SPA fallback). Those were small, reversible, and verifiable. v0.2 is different: it introduces long-lived public keys, persistent manifests served to third-party validators, and a claim of tamper-evidence that the ecosystem will rely on. Shipping any piece of that wrong — or rushed — degrades the trust posture of every robot that has already adopted v0.1. So this document is deliberately over-specified for its length: we want the reader to catch design flaws in prose, not in a post-mortem.
Implementation begins only after the sections marked "Decision required" are resolved in writing (issue, PR comment, or amendment to this doc).
1. Goals and non-goals for v0.2¶
Goals¶
- Signed manifests. A
ROBOT.mdcan be cryptographically signed by its owner such that a third party can verify the manifest has not been altered since it was published. - Registry-hosted retrieval. A signed
ROBOT.mdcan be fetched fromrcan.devvia the RRN, so consumers do not depend on the owner's personal hosting being up. - Key-binding at mint time. The public key that verifies a manifest is bound to the RRN when the RRN is issued, not TOFU'd at first upload. This prevents the "I got there first" attack on a newly minted RRN.
- Tamper-evidence for key→RRN bindings. A third party can check whether the public key associated with an RRN today is the same one that was bound at mint time, without relying solely on the RRF saying so.
- Quantum-hedging option. The format leaves room for a post-quantum signature algorithm alongside Ed25519 without a breaking change.
Non-goals¶
- Signing the code inside OpenCastor (that is OpenCastor's concern).
- A general-purpose PKI. We issue one key per RRN, owned by the RRN holder; that is all.
- Mandatory signing in v0.2. Unsigned manifests remain valid at Tier 0 — signing unlocks verified-tier display on rcan.dev, not basic operation.
- Revocation-at-distance. If a key is compromised, the owner rotates the key by making a signed rotation statement from the old key. No CRL, no OCSP. (Revocation without the old key requires an out-of-band recovery flow; see §9.)
2. Threat model — what changes from v0.1¶
v0.1's threat model is "trusted operator + trusted planner." v0.2 expands the trust surface outward to third parties who consume a ROBOT.md they did not author. The attackers we now care about:
| Attacker | Goal | Defense in this design |
|---|---|---|
| Impersonator with write access to owner's webhost | Serve a malicious ROBOT.md under the owner's URL | Signature on the manifest; consumer verifies against RRN-bound pubkey |
| RRN-squatter | Mint an RRN before the legitimate owner and hold it | Key bound at mint time — squatter's key ≠ owner's key, and RRF can refuse mint without a key |
| Compromised RRF operator | Silently swap the key bound to an RRN | Accepted residual risk in v0.2 (see §6). Transparency-log upgrade path documented for v0.3+ |
| Downgrade attacker | Serve an old, benign-looking signed manifest to hide a newer signed warning | Monotonic manifest_version in signed payload, enforced by registry on upload |
| Algorithm-break attacker | Break Ed25519 in N years and forge historical manifests | Algorithm tag in signature format allows migration; optional PQ hedge (§4) |
What we still do not defend against:
- Compromise of the owner's private key. (They need to rotate it.)
- Compromise of the owner's signing machine at the moment of signing.
- A planner (Claude Code or OpenCastor) that chooses to ignore the signature. Verification is advisory; consumption is the consumer's choice.
3. Format decisions¶
3.1 Detached signature, not embedded¶
Decision: signatures live in a separate file next to the manifest, not inside the manifest YAML frontmatter.
Rationale:
- Embedding forces a "zero out the signature field, sign, put it back" canonicalization dance. That dance is a common source of signature-verification bugs (YAML round-tripping is not byte-stable).
- Detached signing lets us sign the exact bytes of the ROBOT.md file as they exist on disk / as served by the registry. No canonicalization. No "which YAML dumper did you use."
- The registry already hosts files; hosting a second file next to each manifest is trivial.
Consequence for the spec: the frontmatter does gain a
metadata.key_fingerprint field (see §3.3) so a consumer can tell
which key should have signed this file without having to fetch
something separate. The fingerprint is not the signature; it is
metadata that lets the verifier refuse a manifest signed by an
unexpected key.
3.2 What exactly gets signed¶
The signature covers the raw UTF-8 bytes of bob.ROBOT.md, start
to finish, including frontmatter and prose. No exclusions. No
normalization. If a byte changes, the signature fails.
This is deliberate and restrictive. It means: re-rendering the markdown, stripping trailing whitespace, or changing YAML quote style will all invalidate the signature. Good. The owner signs what they intend to publish, exactly.
Signing consumes the file via a streaming SHA-512 (Ed25519's internal hash) — no full load required for large manifests.
3.3 Frontmatter additions¶
metadata:
# existing fields...
signature:
algorithm: "ed25519" # or "pqc-hybrid-v1" (§4.2)
key_fingerprint: "sha256:ab12..." # hex, 64 chars
signed_at: "2026-04-17T12:34:56Z" # ISO-8601 UTC
manifest_version: 3 # monotonically increases (§3.4)
signature is self-declarative metadata, not the signature itself.
It tells a verifier what to verify and which public key to use.
The actual signature bytes are in the detached .sig file.
3.4 manifest_version — monotonic counter¶
A non-negative integer. MUST increase with every re-publish of the
same RRN. The registry refuses uploads where manifest_version does
not exceed the currently stored version. This prevents a downgrade
attack where an attacker re-publishes a stale signed manifest to hide
a newer one.
4. Algorithm choice¶
4.1 Default: Ed25519¶
Decision: v0.2 ships with Ed25519 as the sole required algorithm.
Rationale:
- Universally available: libsodium, NaCl, Python's
cryptography, Node'scrypto.sign, Go'scrypto/ed25519, OpenSSL 1.1.1+. - Small keys (32 bytes) and signatures (64 bytes) fit the "one small file" ethos.
- Deterministic signing — no RNG-failure foot-guns.
- No curve-choice bikeshed.
4.2 Optional: pqc-hybrid-v1¶
Decision: the algorithm enum accepts pqc-hybrid-v1 as a second
value from day one, but the v0.2 CLI does not implement it. This
reserves the format slot so that when ML-DSA-65 tooling is stable we
can add it without a breaking change to the frontmatter.
The hybrid format, when implemented, will be:
Verifiers MUST verify both components and MUST reject if either fails. This is a belt-and-suspenders posture for the transition era.
Decision required — PQ choice. We are pre-committing to ML-DSA-65 (FIPS 204) rather than Falcon or SLH-DSA. User to confirm or push back before we freeze the enum value name.
4.3 Algorithm tag is load-bearing¶
The algorithm field appears both in the frontmatter metadata and in
the detached signature envelope (§5). The two MUST match; verifiers
MUST reject if they differ. This prevents a substitution attack where
an attacker swaps the envelope for one using a weaker algorithm while
leaving the frontmatter claiming a stronger one.
5. Detached signature file format¶
bob.ROBOT.md.sig is a small JSON file (not base64'd raw signature —
we need room for algorithm tag, timestamp, and future multi-algorithm
envelopes):
{
"v": 1,
"algorithm": "ed25519",
"key_fingerprint": "sha256:ab12...",
"signed_at": "2026-04-17T12:34:56Z",
"manifest_sha256": "cd34...",
"signature": "base64(sig_bytes)"
}
manifest_sha256 is redundant with the signature (Ed25519 internally
hashes) but lets a consumer cheaply check manifest identity before
paying the verification cost. It is NOT a substitute for signature
verification.
v: 1 is the envelope version, separate from the ROBOT.md spec
version. It lets us evolve the envelope (add fields, add algorithms)
without touching the ROBOT.md frontmatter format.
6. Tamper-evidence — the blockchain question¶
User asked us to "consider using some blockchain technology" and clarified: the goal is ease of use and cheapness for everyone. That constraint decides the answer. We evaluated four postures; only the simplest one ships in v0.2.
| Option | Role | v0.2 decision |
|---|---|---|
| A. RRF D1 only (centralized) | Baseline storage for RRN→pubkey + manifest bytes | Ships as v0.2 |
| B. Merkle transparency log | RRN→pubkey binding audit trail | Deferred to v0.3+ (optional upgrade, no format break) |
| C. IPFS / content-addressed storage | Manifest distribution | Deferred — opt-in future, not on-by-default |
| D. Public blockchain (L1/L2) | Anchor RRN→pubkey bindings | Rejected |
6.1 Option A — centralized D1 (what actually ships)¶
D1 stores RRN → current-pubkey bindings. D1 stores the signed manifest bytes. That is the whole storage story for v0.2.
Why this is the right call given the ease/cheap constraint:
- Zero extra cost to adopters. No gas. No external accounts. No
node to run. Registering a robot is one
POSTto rcan.dev. - Zero extra cost to RRF. D1 is already deployed; adding two columns and a table is operations we already do.
- Zero new mental model for users. A user signs their ROBOT.md locally and uploads it. The registry verifies with the key it already bound at mint time. That is the entire flow.
- Ed25519 already gives the main property we actually need: anyone who downloads a signed ROBOT.md can verify the owner signed it, without trusting the registry at all. The registry is just a convenient host.
The trust assumption is "RRF will not silently swap a bound key." For every robot operator at v0.2 scale, this is an acceptable floor — it is already the trust floor of the entire RRN namespace, identical to the trust floor of any domain-registrar-issued certificate.
Limitation we accept: a compromised RRF could rebind a key and the rebind would not be externally provable. We document this honestly in SECURITY.md and note it is the upgrade path for v0.3 (see §6.2).
6.2 Option B — Merkle transparency log (noted, not built)¶
A certificate-transparency-style append-only log of RRN→pubkey binding events would give consumers "proof of no-swap" against a compromised registry. The architecture is the one Google uses behind HTTPS certificates today. It is not a blockchain: one writer, no tokens, no consensus, just a signed Merkle tree served over HTTP.
Why we defer it to v0.3+ anyway:
- It is more code, more operational surface, and more concepts for users to understand. "What's an inclusion proof" is not a question a robot hobbyist should have to answer in their first hour.
- It adds zero value for users who aren't worried about a compromised RRF. For a v0.2-era ecosystem where the threat model is "random attacker on the internet," Ed25519 + centralized key binding is already strictly stronger than the status quo (unsigned manifests served from operator webhosts).
- It can be added later without a breaking change to ROBOT.md or the CLI. The frontmatter already carries enough metadata; the transparency log is a purely server-side + verifier-side addition. We lose nothing by waiting.
If and when the log lands, the CLI gains an optional
robot-md verify --transparency flag and users who care get stronger
evidence. Users who don't care see no change.
6.3 Option C — IPFS (deferred)¶
Attractive in theory for content-addressed mirroring, but every IPFS integration we've seen in practice adds a "why is this failing to pin" support-load on the maintainer. Users who want IPFS can pin the signed manifest themselves — the signature works identically whether the file is served from rcan.dev or from an IPFS gateway. We do not need to take on gateway operations in v0.2.
6.4 Option D — public blockchain (rejected)¶
Directly violates the ease/cheap constraint. Rejected.
- Cost. Per-binding on-chain writes cost real money (gas on L1, bridge costs on L2). That cost falls on either the user or the RRF, and we don't have a subsidy stream. Not cheap for anyone.
- Latency. Finality measured in minutes (L1) or seconds under chain-specific assumptions (L2). Our use case does not need consensus; it needs "the owner signed this."
- Governance coupling. Ties RRF's trust posture to a specific chain's operator set, token economics, and regulatory exposure.
- Operational surface. Wallet custody, gas management, chain reorgs — each is its own production-hardening project.
- Solves the wrong problem. We do not have a double-spend problem. Ed25519 + a centralized registry solves our actual problem (owner-authenticated manifests) with none of the above costs.
We remain open to anchoring the root hash of a future Merkle log into a public chain as a cheap, infrequent belt-and-suspenders move later. That's a once-a-day transaction, not a once-per-binding one, and it's entirely optional. v0.3+ material at earliest.
7. Key lifecycle¶
7.1 Keygen¶
robot-md keygen generates an Ed25519 keypair and writes:
~/.robot-md/keys/<fingerprint>.priv(mode 0600, owner-only read)~/.robot-md/keys/<fingerprint>.pub(mode 0644)
~/.robot-md/ is created with mode 0700 if it does not exist. If it
already exists with wider permissions, the CLI refuses to write
and prints a fix-up command. No silently-broadening-permissions.
Decision required — passphrase. We propose making passphrase
protection opt-in via --encrypt. Default is unencrypted on disk
(relying on filesystem perms). Rationale: the single-robot developer
path should be frictionless; passphrase keys imply an agent or
GUI prompt and that is out of scope for v0.2. Users who want
passphrase protection opt in explicitly. User to confirm.
7.2 Binding at RRN mint¶
robot-md register uploads the public key as part of the mint
request, not after. The RRF issues an RRN only if the request
includes a public key; the binding is final at issuance.
Benefit: there is no window where an RRN exists without a key and could be TOFU'd by the first uploader.
7.3 Rotation¶
A key rotation is a signed statement by the old key saying "the new
key for this RRN is K_new." The RRF verifies the statement against
the currently-bound key, then updates the binding in D1. Rotation
events are timestamped and retained in a key_history column so
historical verification (was K_old bound at time T?) remains
possible from registry-queryable state.
7.4 Revocation / compromise recovery¶
If the old key is lost or compromised and the owner cannot sign a rotation, the RRF provides an out-of-band recovery flow (human review, proof of RRN ownership via account auth, etc.). Recovery is logged with a registry-signed note distinguishable from a user-signed rotation.
Decision required — recovery policy. What counts as "proof of RRN ownership" for recovery? Proposal: the same auth identity that minted the RRN, plus a cooling-off period (e.g., 7 days) before the rebind takes effect, during which the old key can still veto. User to confirm.
8. CLI surface¶
New verbs shipped in CLI 0.2.0:
robot-md keygen [--encrypt] [--out DIR]
robot-md sign PATH [--key FINGERPRINT]
robot-md verify PATH [--against-rrn RRN]
robot-md register PATH [--ipfs-pin]
robot-md rotate --rrn RRN --new-key FINGERPRINT
Existing verbs (validate, render, context) are unchanged.
sign writes PATH.sig and adds the metadata.signature block
to the manifest. It refuses to run if the manifest already has a
different key_fingerprint than the key being used, unless
--force-rebind is passed (which is separately logged).
verify checks the detached signature against the public key
indicated by the frontmatter fingerprint. With --against-rrn, it
additionally fetches the current bound key from rcan.dev and confirms
it matches.
register is the REST client for §9.
9. Registry API changes¶
9.1 POST /api/v1/robots (mint)¶
Extend the mint request body with:
{
"metadata": { ... existing ... },
"public_key": {
"algorithm": "ed25519",
"key_material": "base64(32 bytes)",
"fingerprint": "sha256:ab12..."
}
}
RRF verifies the fingerprint matches the key material, then atomically (1) issues the RRN and (2) inserts the binding into D1.
9.2 PUT /api/v1/robots/:rrn/manifest¶
(Placeholder endpoint already scaffolded in rcan-spec at
functions/api/v1/robots/[rrn]/manifest.ts returning 501 in v0.1.1.)
Request body: raw ROBOT.md bytes. Required headers:
Content-Type: text/markdown
X-Manifest-Signature: base64(envelope) # the .sig file, base64'd
X-Manifest-Key-Fingerprint: sha256:<hex>
Authorization: Bearer <rrn-owner-token>
Server validates:
- Caller owns the RRN (Authorization token).
X-Manifest-Key-Fingerprintmatches the currently bound key.- Signature in
X-Manifest-Signatureverifies against the stored public key over the raw body bytes. - Frontmatter parses;
metadata.signature.key_fingerprintequalsX-Manifest-Key-Fingerprint;metadata.signature.algorithmequals the envelope algorithm. manifest_versionin frontmatter > currently storedmanifest_version.
Responses:
201 Createdon first upload200 OKon update401 Unauthorizedon auth failure403 Forbiddenon key mismatch409 Conflicton non-monotonic manifest_version422 Unprocessable Entityon schema / signature failure
9.3 GET /api/v1/robots/:rrn/manifest¶
Returns the raw manifest bytes as text/markdown with:
X-Manifest-Signature: base64(envelope)
X-Manifest-Key-Fingerprint: sha256:<hex>
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=300
Third-party validators can verify without any registry-trusted state: they pull the signature, pull the current bound key (§9.4), and verify locally.
9.4 GET /api/v1/robots/:rrn/key¶
Returns the currently bound public key:
{
"rrn": "RRN-000000000001",
"algorithm": "ed25519",
"key_material": "base64(32 bytes)",
"fingerprint": "sha256:ab12...",
"bound_at": "2026-04-17T12:00:00Z"
}
Transparency-log fields may be added in v0.3+ without breaking v0.2 clients — unknown fields are permitted.
10. D1 schema additions¶
-- key material bound to RRNs (current binding)
CREATE TABLE robot_keys (
rrn TEXT PRIMARY KEY,
algorithm TEXT NOT NULL,
key_material BLOB NOT NULL,
fingerprint TEXT NOT NULL,
bound_at TEXT NOT NULL,
rotated_at TEXT,
FOREIGN KEY (rrn) REFERENCES robots(rrn)
);
-- historical binding events (rotation audit trail)
CREATE TABLE robot_key_history (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
rrn TEXT NOT NULL,
event_type TEXT NOT NULL, -- 'BIND' | 'ROTATE' | 'REVOKE'
algorithm TEXT NOT NULL,
key_material BLOB NOT NULL,
fingerprint TEXT NOT NULL,
signed_by_fingerprint TEXT, -- old key for ROTATE, NULL for BIND, 'RRF' for admin REVOKE
recorded_at TEXT NOT NULL,
FOREIGN KEY (rrn) REFERENCES robots(rrn)
);
-- signed manifest bodies
CREATE TABLE robot_manifests (
rrn TEXT PRIMARY KEY,
body BLOB NOT NULL,
signature_envelope TEXT NOT NULL, -- JSON string
key_fingerprint TEXT NOT NULL,
manifest_version INTEGER NOT NULL,
uploaded_at TEXT NOT NULL,
FOREIGN KEY (rrn) REFERENCES robots(rrn)
);
Note: D1 exec() does not work for DDL; use prepare().run() per
CLAUDE.md memory.
11. Things that need verification before we write the code¶
These are the items where "sounds right on paper" is not enough and we must prove the capability exists before committing to the design.
- Ed25519 in Cloudflare Pages Functions (Workers runtime). The
Workers
SubtleCryptoexposes Ed25519 as a supported curve forimportKey/verifyper 2023 runtime docs, but behavior in Pages Functions specifically should be smoke-tested with a real signature before we commit the verify path there. If unavailable, we fall back to a WASM library (noble-ed25519 has a Workers-compatible build). - D1 BLOB column size. A signed ROBOT.md plus envelope can easily be 10–50 KB. D1's per-row limit is 1 MB but we should confirm real insert/select performance at the expected size.
- Fingerprint canonicalization.
sha256of what exactly? We propose:sha256(algorithm_bytes || 0x00 || key_material_bytes)so that different-algorithm keys with numerically equal material cannot collide. User to confirm this is not overkill.
12. Rollout plan (design-level, not a commit plan)¶
- Land this design doc. Get user sign-off on the Decision Required items.
- Implement and smoke-test Ed25519 sign+verify in CLI in isolation (offline, no registry).
- Add the Cloudflare-side verification path; prove it against known test vectors.
- Implement the D1 schema and the
/key,/manifest,/transparency-log/sthendpoints. - Wire
robot-md registerend-to-end. - Migrate Bob as the first signed robot, dogfood.
- Tag
cli 0.2.0andspec v0.2.
Each of steps 2–5 is an independent PR with its own code review. No step ships without tests proving the cryptographic invariants.
13. Decisions required from user before implementation¶
Collected in one place for ease of review:
- §4.2 — PQ algorithm: pre-commit to ML-DSA-65? Or defer the enum-value name?
- §7.1 — Keygen passphrase default: accept "unencrypted on disk
behind 0600" as the v0.2 default, with
--encryptopt-in? - §7.4 — Recovery policy: accept "same auth identity + 7-day cooling-off" for lost-key recovery?
- §11.3 — Fingerprint canonicalization: accept
sha256(algorithm_bytes || 0x00 || key_material_bytes)?
Blockchain-tech evaluation (§6) resolved per user feedback: ship centralized D1 baseline, defer transparency log / IPFS / L1-anchoring to v0.3+. No further decision needed on that axis.
14. What this design does not attempt¶
For clarity about what is out of scope for v0.2:
- No multi-signature / threshold schemes (one key per RRN).
- No hardware-security-module integration (TPM, YubiKey). A future CLI plugin point could add this; not v0.2.
- No fleet-level parent/child key derivation.
- No manifest encryption at rest. Manifests are public by design.
- No support for RRN transfer between owners. Out of scope; likely requires a signed transfer ceremony modeled on rotation.
15. Status¶
Implementation pending user review of these decisions — no crypto code will ship until you sign off.
When you are ready, mark the five Decision Required items in §13 as accepted (or propose edits), and the implementation plan of §12 begins at step 2.