Skip to content

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

  1. Signed manifests. A ROBOT.md can be cryptographically signed by its owner such that a third party can verify the manifest has not been altered since it was published.
  2. Registry-hosted retrieval. A signed ROBOT.md can be fetched from rcan.dev via the RRN, so consumers do not depend on the owner's personal hosting being up.
  3. 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.
  4. 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.
  5. 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.

bob.ROBOT.md
bob.ROBOT.md.sig     # base64-encoded signature envelope (§4)

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's crypto.sign, Go's crypto/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:

pqc-hybrid-v1 = ed25519_signature || ml_dsa_65_signature

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 POST to 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.

  1. 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.
  2. 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."
  3. Governance coupling. Ties RRF's trust posture to a specific chain's operator set, token economics, and regulatory exposure.
  4. Operational surface. Wallet custody, gas management, chain reorgs — each is its own production-hardening project.
  5. 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:

  1. Caller owns the RRN (Authorization token).
  2. X-Manifest-Key-Fingerprint matches the currently bound key.
  3. Signature in X-Manifest-Signature verifies against the stored public key over the raw body bytes.
  4. Frontmatter parses; metadata.signature.key_fingerprint equals X-Manifest-Key-Fingerprint; metadata.signature.algorithm equals the envelope algorithm.
  5. manifest_version in frontmatter > currently stored manifest_version.

Responses:

  • 201 Created on first upload
  • 200 OK on update
  • 401 Unauthorized on auth failure
  • 403 Forbidden on key mismatch
  • 409 Conflict on non-monotonic manifest_version
  • 422 Unprocessable Entity on 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.

  1. Ed25519 in Cloudflare Pages Functions (Workers runtime). The Workers SubtleCrypto exposes Ed25519 as a supported curve for importKey / verify per 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).
  2. 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.
  3. Fingerprint canonicalization. sha256 of 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)

  1. Land this design doc. Get user sign-off on the Decision Required items.
  2. Implement and smoke-test Ed25519 sign+verify in CLI in isolation (offline, no registry).
  3. Add the Cloudflare-side verification path; prove it against known test vectors.
  4. Implement the D1 schema and the /key, /manifest, /transparency-log/sth endpoints.
  5. Wire robot-md register end-to-end.
  6. Migrate Bob as the first signed robot, dogfood.
  7. Tag cli 0.2.0 and spec 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:

  1. §4.2 — PQ algorithm: pre-commit to ML-DSA-65? Or defer the enum-value name?
  2. §7.1 — Keygen passphrase default: accept "unencrypted on disk behind 0600" as the v0.2 default, with --encrypt opt-in?
  3. §7.4 — Recovery policy: accept "same auth identity + 7-day cooling-off" for lost-key recovery?
  4. §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.