mosskeys
Public · no auth

Read & verify keys

The public, unauthenticated read API. Every artifact it returns is already public and independently verifiable: a client-signed checkpoint (signed tree head), canonical leaf bytes, RFC 6962 inclusion and consistency proofs, and raw tlog-tiles bytes. No token is needed, and the server never signs. It only serves bytes the log already committed to.

Why verify?

Reading a key from an API you trust is easy. The point of mosskeys is that you do not have to trust the server. A verifier fetches three public things (a signed checkpoint, the leaf, and an inclusion proof) and checks the math locally. If the server ever tried to show one key to Alice and a different key to Bob, or to quietly drop a rotation from history, the proofs would fail to reconstruct the signed root, and the tampering would be detected.

You do not need a mosskeys account to verify: everything below is public, and verification is deliberately account-less so any third party can check your log. You are also not required to use our libraries. The formats are open standards (RFC 6962 Merkle proofs, C2SP tlog-tiles and signed-checkpoint notes), so a determined team can verify straight from the specs. In practice we strongly recommend the audited metamorphic-log verifier: the hybrid post-quantum signature (Ed25519 + ML-DSA) and canonical hashing are fiddly to reimplement, and the shared verifier guarantees your computation matches the server byte-for-byte. See verification libraries below.

Endpoints

All read endpoints are GET, need no authentication, and are relative to https://mosskeys.com.

Endpoint Returns
GET /api/:slug/checkpoint Latest client-signed checkpoint (signed tree head).
GET /api/:slug/log/entries/:index Canonical leaf bytes + metadata for one entry.
GET /api/:slug/log/label/:label Newest entry for an exact label + a bundled inclusion proof (stopgap).
GET /api/:slug/lookup?label= Privacy-preserving CONIKS lookup: a presence or absence proof for an identity.
GET /api/:slug/directory-proof CONIKS directory head (root + VRF public key) every lookup proof verifies against.
GET /api/:slug/log/proof/inclusion?index=&size= RFC 6962 inclusion proof for a leaf within a tree.
GET /api/:slug/log/proof/consistency?from=&to= RFC 6962 consistency proof between two tree sizes.
GET /api/:slug/log/reconcile?since=<size> One-shot reconcile bundle: latest checkpoint + consistency proof from a pinned size.
GET /api/:slug/log/tile/*path Raw, immutable tlog-tiles bytes (witness/CDN friendly).

Get the latest checkpoint

GET /api/:slug/checkpoint returns the most recent client-signed tree head. This is your trust anchor: verify its note signature against the namespace's published public key before trusting anything else.

shell
curl https://mosskeys.com/api/acme/checkpoint

Response 200 OK:

200 OK
{
  "origin": "mosskeys.com/acme",
  "size": 42,
  "root": "<base64 32-byte Merkle root>",
  "note": "<C2SP signed checkpoint note>"
}

root is the base64 Merkle root at size leaves. note is the full C2SP signed note; its signature is what makes the tree head trustworthy. Notes are dual-signed: one hybrid post-quantum line (what our SDKs verify) plus one classical Ed25519 (0x01) line under the same origin name, so stock C2SP witness software can verify and cosign the checkpoint. Your verifier checks whichever line matches its trusted keys and ignores the rest; witness cosignatures (0x04 Ed25519 or 0x06 ML-DSA-44) may appear as additional lines once witnesses are configured.

Fetch a leaf

GET /api/:slug/log/entries/:index returns the canonical bytes and metadata for the leaf at a zero-based index. It never serves the entry's cleartext label: walking indices must not enumerate a namespace's identities. To resolve an identity, use the exact-label endpoint below (you must already know the label).

shell
curl https://mosskeys.com/api/acme/log/entries/0

Response 200 OK:

200 OK
{
  "index": 0,
  "leaf": "<base64 canonical leaf bytes>",
  "leaf_hash": "<base64 32-byte RFC 6962 leaf hash>",
  "entry_hash": "<base64>",
  "prev_entry_hash": null
}

leaf is the canonical leaf bytes (base64) that hash to leaf_hash. Use leaf_hash as the input to the inclusion proof below. prev_entry_hash is null for the first entry.

Look up the latest key for a label

GET /api/:slug/log/label/:label resolves an identity or label straight to its newest key-history entry, so a relying party does not have to walk the log by index to find the current key. The label is matched exactly. Everything it returns is already public in the served leaves, so this exposes nothing new.

shell
curl https://mosskeys.com/api/acme/log/label/alice@example.com

Response 200 OK:

200 OK
{
  "index": 41,
  "label": "alice@example.com",
  "leaf": "<base64 canonical leaf bytes>",
  "leaf_hash": "<base64 32-byte RFC 6962 leaf hash>",
  "entry_hash": "<base64>",
  "prev_entry_hash": "<base64>",
  "tree_size": 42,
  "checkpoint": {
    "origin": "mosskeys.com/acme",
    "size": 42,
    "root": "<base64 32-byte Merkle root>",
    "note": "<C2SP signed checkpoint note>"
  },
  "inclusion_proof": {
    "index": 41,
    "size": 42,
    "leaf_hash": "<base64>",
    "proof": ["<base64>", "<base64>", "…"]
  }
}

The entry fields match the leaf endpoint, plus the current tree_size, the latest signed checkpoint, and a bundled inclusion_proof built against that checkpoint so you can fetch and verify in one round trip. inclusion_proof is null when the head entry is newer than the latest checkpoint (just appended, not yet anchored); re-fetch once the checkpoint advances, or request an inclusion proof directly.

JavaScript (WASM verifier)

javascript
// Stopgap label lookup: resolve an identity/label to its current key +
// proof in one request, then verify against the bundled signed checkpoint.
// No token is needed: every byte below is already public.
import init, { checkpointVerifyInclusion } from "metamorphic-log";

const base = "https://mosskeys.com/api/acme";
const VKEYS = ["<namespace vkey>"]; // pinned out of band; your root of trust

await init();

// 1. Resolve the label to its newest entry + a bundled inclusion proof.
const res = await fetch(`${base}/log/label/${encodeURIComponent("alice@example.com")}`);
const head = await res.json();

// 2. If the head is already anchored in the signed checkpoint, verify the
//    bundled proof. Throws unless the checkpoint is validly signed AND the
//    leaf is provably included under its signed root.
if (head.inclusion_proof) {
  checkpointVerifyInclusion(
    head.checkpoint.note,          // the signed checkpoint note (C2SP)
    VKEYS,                         // trusted verifier key(s) for this namespace
    BigInt(head.inclusion_proof.index),
    head.inclusion_proof.leaf_hash,
    head.inclusion_proof.proof,
  );
} else {
  // The head was appended after the latest checkpoint, so it is not yet
  // anchored in a signed tree head. Re-fetch once the checkpoint advances,
  // or request GET /api/:slug/log/proof/inclusion?index=&size= directly.
}

This label lookup is a plaintext convenience on the RFC 6962 log substrate: it returns the newest entry for an exact label. For a privacy-preserving resolution that also proves a label has no key, use the CONIKS lookup, which blinds the identity through the namespace VRF and returns presence or absence proofs.

Private lookup (CONIKS)

GET /api/:slug/lookup?label= resolves an identity the privacy-preserving way. The identity is blinded through the namespace's VRF, and the response is a CONIKS proof that reveals no other identity in the directory. Unlike the label lookup above, it can also prove a label has no key (an absence proof).

shell
curl "https://mosskeys.com/api/acme/lookup?label=alice@example.com"

Present 200 OK:

200 OK
{
  "directory_mode": "coniks",
  "namespace": "acme",
  "slug": "acme",
  "label": "alice@example.com",
  "status": "present",
  "value": "<base64 entry_hash the directory binds to this identity>",
  "proof": "<base64 CONIKS presence proof>",
  "root": "<base64 64-byte SHA3-512 directory root>",
  "vrf_public": "<base64 VRF public key>",
  "entries": 128
}

Absent 200 OK:

200 OK
{
  "directory_mode": "coniks",
  "namespace": "acme",
  "slug": "acme",
  "label": "ghost@example.com",
  "status": "absent",
  "value": null,
  "proof": "<base64 CONIKS absence proof>",
  "root": "<base64 64-byte SHA3-512 directory root>",
  "vrf_public": "<base64 VRF public key>",
  "entries": 128
}

value is the entry_hash the directory binds to this identity: the commitment to the label's current key-history head. Verify the proof against root under vrf_public, then fetch the label head to read the actual keys and confirm their entry_hash matches value (with an RFC 6962 inclusion proof).

Elixir (NIF verifier)

elixir
# Privacy-preserving CONIKS lookup: resolve an identity to the value the
# directory binds it to, and verify the proof locally. The identity is
# blinded through the namespace VRF, and the proof reveals no other
# identity in the directory. No token is needed.
base = "https://mosskeys.com/api/acme"
label = "alice@example.com"

%{body: r} = Req.get!("#{base}/lookup", params: [label: label])
identity = Base.encode64(label)

case r["status"] do
  "present" ->
    # {:ok, value} unless the presence proof reconstructs `root` under
    # `vrf_public`. `value` equals r["value"]: the entry_hash of the
    # label's current head. Then GET /api/:slug/log/label/:label to read
    # the keys and confirm their entry_hash matches (with an inclusion proof).
    {:ok, _value} =
      MetamorphicLog.Coniks.verify_lookup(
        r["namespace"], r["vrf_public"], r["root"], identity, r["proof"]
      )

  "absent" ->
    # :ok unless the absence proof verifies: the directory binds no value.
    :ok =
      MetamorphicLog.Coniks.verify_absence(
        r["namespace"], r["vrf_public"], r["root"], identity, r["proof"]
      )
end

GET /api/:slug/directory-proof returns the directory head those proofs verify against: the root, the published vrf_public key, and the number of entries.

shell
curl https://mosskeys.com/api/acme/directory-proof
200 OK
{
  "directory_mode": "coniks",
  "namespace": "acme",
  "slug": "acme",
  "root": "<base64 64-byte SHA3-512 directory root>",
  "vrf_public": "<base64 VRF public key>",
  "entries": 128
}

Lookups are served on the CONIKS backend. A namespace on the experimental KEYTRANS backend is answered 501 (its proof wire is not yet byte-frozen). The directory root is stable while the directory is served but is re-randomized if the serving process restarts, and it is not yet witness-anchored, so verify the {root, proof, value} a response returns together rather than pinning a root across time.

Inclusion proof

GET /api/:slug/log/proof/inclusion?index=&size= proves that the leaf at index is committed to by the tree of size leaves. Use the size from the signed checkpoint.

shell
curl "https://mosskeys.com/api/acme/log/proof/inclusion?index=0&size=42"

Response 200 OK:

200 OK
{
  "index": 0,
  "size": 42,
  "leaf_hash": "<base64>",
  "proof": ["<base64>", "<base64>", "…"]
}

proof is the ordered list of base64 sibling hashes an RFC 6962 verifier folds together with leaf_hash to reconstruct the root at size. If the reconstructed root equals the signed checkpoint's root, the key is provably in the log.

Consistency proof

GET /api/:slug/log/proof/consistency?from=&to= proves the tree of to leaves is an append-only extension of the tree of from leaves: nothing already published was rewritten or removed. Use it to check that a newer checkpoint is consistent with one you previously pinned.

shell
curl "https://mosskeys.com/api/acme/log/proof/consistency?from=42&to=57"

Response 200 OK:

200 OK
{
  "from": 42,
  "to": 57,
  "proof": ["<base64>", "<base64>", "…"]
}

Offline verification and reconciliation

Verifiers do not have to be online to trust a key. The pattern below lets an offline or intermittently connected client (a commercial offline-first app, or an edge-resident node on a denied, disrupted, intermittent, or limited-bandwidth link) verify keys with no network, then cheaply catch up when it reconnects. It has three steps: pin, verify offline, and reconcile.

  1. Pin a signed checkpoint while online: GET /api/:slug/checkpoint. Verify its note signature under the namespace's verifier key, then store {note, size, root} together with the pinned vkey. That pinned, signed root is your offline trust anchor.
  2. Verify offline. With the pin cached, verify any cached inclusion proofs against the pinned root, and re-check the pinned note signature, entirely on-device. No server is contacted, so this works air-gapped or through a full comms blackout.
  3. Reconcile on reconnect with a single request to GET /api/:slug/log/reconcile?since=<pinned size>. It returns the current signed checkpoint and the RFC 6962 consistency proof from your pinned size up to that head. Verify append-only continuity from your pin to the new head, then re-pin the served checkpoint.

The reconcile bundle exists purely to save round trips. Without it, catching up is two serial requests: first GET /checkpoint to learn the new head size, then GET /log/proof/consistency from your pinned size to that head. The bundle collapses both into one server-side request, a real latency win on high-latency or intermittent links.

shell
curl "https://mosskeys.com/api/acme/log/reconcile?since=42"

Response 200 OK:

200 OK
{
  "checkpoint": {
    "origin": "mosskeys.com/acme",
    "size": 57,
    "root": "<base64 32-byte Merkle root>",
    "note": "<C2SP signed checkpoint note>"
  },
  "consistency_proof": {
    "from": 42,
    "to": 57,
    "proof": ["<base64>", "<base64>", "…"]
  }
}

since equal to the head (or 0) returns the empty proof, the RFC 6962 degenerate consistency case, so a re-pin that finds nothing new still verifies. A since beyond the head is a 400; a namespace with no checkpoint yet is a 404.

Elixir (NIF verifier)

elixir
# Offline verification + one-shot reconcile, via the audited metamorphic_log
# NIF (Hex). No token is needed: every byte below is already public.
base = "https://mosskeys.com/api/acme"
vkeys = ["<namespace vkey>"] # pinned out of band; your root of trust

# --- 1. PIN (online, once) -------------------------------------------
# Fetch the current signed checkpoint, verify its signature, then store
# {note, size, root} locally alongside the pinned vkeys.
pinned = Req.get!("#{base}/checkpoint").body
:ok = MetamorphicLog.Checkpoint.verify(pinned["note"], vkeys)

# --- 2. OFFLINE (no network) -----------------------------------------
# Verify cached inclusion proofs against the PINNED signed root. Because
# the pinned note was signature-checked above, this needs no server.
:ok =
  MetamorphicLog.Checkpoint.verify_inclusion(
    pinned["note"], vkeys,
    cached_entry["index"], cached_entry["leaf_hash"], cached_proof["proof"]
  )

# --- 3. ON RECONNECT: one-shot reconcile -----------------------------
# A single request returns the new head + the consistency proof from our
# pinned size, collapsing what was two serial round trips into one.
bundle = Req.get!("#{base}/log/reconcile", params: [since: pinned["size"]]).body

# Verify BOTH notes' signatures AND append-only continuity from the pin to
# the served head in one call. The degenerate empty proof (pin already
# current) verifies trivially.
:ok =
  MetamorphicLog.Checkpoint.verify_consistency(
    pinned["note"],               # older, pinned checkpoint note
    bundle["checkpoint"]["note"], # newer, served checkpoint note
    vkeys,
    bundle["consistency_proof"]["proof"]
  )

# Continuity holds relative to your pin: RE-PIN to the new checkpoint and
# keep going. (The lower-level MetamorphicLog.Proof.verify_consistency/5
# checks the proof alone if you have already verified both notes.)
pinned = bundle["checkpoint"]

What this proves, stated honestly: a consistency check against your own pin detects a rollback or a rewrite of history relative to what you already saw. It does not on its own detect a split view, where the server shows a different, internally consistent history to someone else. Catching that needs independent witnesses. Pro checkpoints are automatically relayed to the C2SP witnesses configured for the service and their cosignatures merged in, but this deployment has none configured yet, so no checkpoint carries one today. The CONIKS directory root is a separate structure and is likewise not yet witness-anchored.

Raw tiles

GET /api/:slug/log/tile/*path serves raw, immutable tlog-tiles bytes (concatenated 32-byte node hashes) with Content-Type: application/octet-stream. Tiles are content-addressed and never mutate, so they are served with a one-year immutable cache header, ideal for witnesses and CDNs assembling proofs at scale.

shell
curl https://mosskeys.com/api/acme/log/tile/0/000 -o tile-0-000.bin

Errors

The read API is intentionally minimal. Unknown slugs, out-of-range indices or sizes, and missing checkpoints or tiles are all 404; malformed query parameters are 400. The body is a small JSON object:

404 Not Found
{ "error": "not found" }

Note this is a simpler shape than the write path's { error: { code, message } } envelope: read responses carry no capability or quota semantics, so a flat error string is enough.

Full walkthrough: fetch a key, then verify it

Putting it together: pin to the signed checkpoint, fetch the leaf, fetch an inclusion proof sized to that checkpoint, and verify the proof reconstructs the checkpoint's root. Only then do you trust the key.

CLI (one command, no token)

shell
# The one-liner: verify a label's key history read-only, no token, no
# account. It fetches the label head + inclusion proof + signed checkpoint
# and checks them through the SAME metamorphic-log verifier used everywhere
# else. Exit code 0 = verified; non-zero maps the failure (a rewritten head
# exits with the head_mismatch code).
mosskeys verify --namespace acme --label alice@example.com

# Pin the namespace's published verifier key(s) to also check the
# checkpoint co-signatures, and a previously saved checkpoint to prove
# append-only continuity. --json emits a machine-readable result.
mosskeys verify -n acme --label alice@example.com \
  --verifier-key "<namespace vkey>" \
  --pin ./pinned-checkpoint.note --json

JavaScript (WASM verifier)

javascript
// A full "fetch a key, then verify it against a signed checkpoint" flow.
// No token is needed: every byte below is already public.
//
// The verifier comes from the audited metamorphic-log WASM package. One call
// (checkpointVerifyInclusion) verifies the checkpoint SIGNATURE against the
// namespace's trusted key AND that the leaf is included under the signed
// root; it throws on any mismatch. That is the whole trust check.
import init, { checkpointVerifyInclusion } from "metamorphic-log";

const base = "https://mosskeys.com/api/acme";
// The namespace's published verifier key(s), in C2SP vkey form. You obtain
// these out of band (e.g. from the namespace page) and pin them; this is
// your root of trust, so never fetch them from the same response you verify.
const VKEYS = ["<namespace vkey>"];

await init();

// 1. Pin to the log's current signed tree head.
const checkpoint = await (await fetch(`${base}/checkpoint`)).json();

// 2. Fetch the leaf you care about (here, index 0).
const entry = await (await fetch(`${base}/log/entries/0`)).json();

// 3. Fetch an inclusion proof sized to the checkpoint.
const proof = await (
  await fetch(`${base}/log/proof/inclusion?index=${entry.index}&size=${checkpoint.size}`)
).json();

// 4. Verify. Throws unless the checkpoint is validly signed AND the leaf is
//    provably included under its root.
checkpointVerifyInclusion(
  checkpoint.note,     // the signed checkpoint note (C2SP)
  VKEYS,               // trusted verifier key(s) for this namespace
  BigInt(entry.index), // leaf index
  entry.leaf_hash,     // base64 leaf hash
  proof.proof,         // array of base64 sibling hashes
);

// Reached here without throwing: the key in `entry` is provably part of the
// log the checkpoint committed to. You never had to trust the server.

Elixir (NIF verifier)

elixir
# The same check server-side or in any Elixir service, via the audited
# metamorphic_log NIF (Hex). Add {:metamorphic_log, "~> 0.1"} to your deps.
base = "https://mosskeys.com/api/acme"
vkeys = ["<namespace vkey>"] # pinned out of band; your root of trust

checkpoint = Req.get!("#{base}/checkpoint").body
entry = Req.get!("#{base}/log/entries/0").body

proof =
  Req.get!("#{base}/log/proof/inclusion",
    params: [index: entry["index"], size: checkpoint["size"]]
  ).body

# Verifies the checkpoint signature AND inclusion under the signed root.
:ok =
  MetamorphicLog.Checkpoint.verify_inclusion(
    checkpoint["note"],
    vkeys,
    entry["index"],
    entry["leaf_hash"],
    proof["proof"]
  )

The order matters for security: verify the checkpoint's signature first, and always size the inclusion proof to the signed checkpoint. Verifying a proof against a root the server merely asserted (rather than one it signed) proves nothing.

Verify robots, agents, and fleets

Verification needs no human in the loop. A robot, an AI agent, an edge node, or a CI job can check a key or an artifact digest read-only, with no token and no account, and offline once a checkpoint is pinned. A label is any subject you can name, so an agent verifies through the same flow, and the same audited verifier, as a person.

mosskeys verify wraps this for machines. It runs three ways: online against a --label, offline against a pinned --checkpoint note (the pin / verify / reconcile pattern), or against an artifact --digest at a known --index.

CLI (no token, scriptable exit codes)

shell
# ONLINE — verify an agent identity's current key against the latest signed
# checkpoint. No token, no account. Exit 0 = verified; non-zero is scriptable.
mosskeys verify -n acme --label "agent:billing-bot@acme"

# OFFLINE (edge / air-gapped) — verify a checkpoint note you pinned earlier,
# plus its witness co-signatures, with no network at all.
mosskeys verify --checkpoint ./pinned-checkpoint.note --verifier-key "<vkey>"

# OFFLINE continuity (reconcile) — prove a newer pinned checkpoint is an
# append-only extension of an older one, from saved files, no network.
mosskeys verify --checkpoint ./newer.note --pin ./older.note \
  --consistency ./consistency.json --verifier-key "<vkey>"

# SUPPLY-CHAIN — prove an artifact/leaf digest is committed at a known index.
# --json emits a machine-readable result for a fleet controller to parse.
mosskeys verify -n acme --digest <sha-256 hex> --index 42 --json

Every run exits with a stable code, so a script, a CI step, or a fleet controller can branch on the result without parsing text. Add --json for a machine-readable result too.

Exit Meaning
0 Verified: the checks that ran all passed.
6 Not found: the label, leaf, or checkpoint does not exist.
7 Head mismatch: a consistency proof failed, so history was rewritten or rolled back relative to your pin.
12 Verification failed: a bad inclusion proof, a forged co-signature, an unanchored entry, or a digest mismatch.
8 Invalid request: the server rejected a malformed query.
2 Usage error: bad flags or a missing target.

What a pass proves

  • In an append-only log, the history you saw was not rewritten: inclusion, consistency, and witness co-signatures all hold.
  • It does not, by itself, catch a split view, where the log shows a different head to someone else. That needs independent witnesses observing the same head. The relay pipeline ships on Pro; this deployment has no witnesses configured yet.

Why this, and not just MCP or agent auth?

mosskeys is a verifiability layer, not an identity issuer, so it sits alongside agent auth (OAuth-for-agents, MCP, SPIFFE/SPIRE), not instead of it. Those mint and check a credential in the moment. mosskeys makes the binding between an agent and its key publicly checkable over time, and adds what an auth layer alone does not:

  • anyone can verify a key without an account, and offline;
  • lookups leak no other identity in your directory (VRF-blinded, CONIKS-style privacy);
  • keys are post-quantum, and every rotation is tamper-evident in order.

Reach for it for stable agent identity roots, capability and version changes, and provenance, not ephemeral few-minute workload tokens.

Verification libraries

All verifiers share one audited Rust crypto core, so a proof checks identically wherever you run it. Pick the surface that matches your stack:

Surface Package Use for
JavaScript / WASM metamorphic-log (npm) Browsers, Node, edge and serverless verifiers.
Elixir / NIF metamorphic_log (Hex) Elixir and Erlang services and witnesses.
Rust metamorphic-log (crates.io) Native services and the core the others wrap.
CLI mosskeys-cli (crates.io / brew) One-command, offline-capable verification in CI, agents, and fleets (mosskeys verify).
Swift metamorphic-log-swift (SwiftPM) 0.1.11 iOS and macOS verifiers.
Kotlin / JVM io.github.moss-piglet:metamorphic-log 0.1.11 Android and JVM verifiers.
Python metamorphic-log (PyPI) 0.1.11 Desktop, server, and scripting verifiers.

Exact install names and versions live in each package's README. The native mobile and desktop verifiers (Swift, Kotlin, and Python) are generated from the same audited Rust core via UniFFI and are published per ecosystem: SwiftPM, Maven Central (metamorphic-log for Android, metamorphic-log-jvm for desktop JVM), and PyPI.

Verification never touches a private key and never needs an account. The one input you supply out of band is the namespace's verifier key (vkey), which is your root of trust. Pin it; do not read it from the same response you are verifying.

See also