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 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. They are also CORS-open (access-control-allow-origin: *, GET only, no credentials), because they serve already-public artifacts: a web app can verify keys straight from the browser, cross-origin, with no proxy. The authenticated write API is never CORS-enabled.

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 cleartext-query convenience on the RFC 6962 log substrate: you ask for an exact label and get its newest entry. Labels themselves are encrypted at rest and resolved through a keyed blind index, so the database never stores them in the clear — but the query is still a known-label question with no proof of absence. For a resolution where the server never sees the label at all and that also proves a label has no key, use the oblivious lookup (RFC 9497 POPRF): your client blinds the label, and the proof — presence or absence — reveals no other identity in the directory.

Oblivious lookup (POPRF)

New namespaces serve oblivious lookups (RFC 9497 POPRF): you resolve an identity to its current key-history head — or prove it has no key — and the server never sees the identity you asked about . Your client blinds the identity, the server evaluates only the blinded element, and you unblind and verify the proof locally. Unlike the label lookup above, the response is a CONIKS proof that reveals no other identity in the directory, including a proof of absence.

The flow has three steps (the CLI does all of them for you: mosskeys lookup alice@example.com):

Step 0 — pin the public parameters. GET /api/:slug/directory-proof returns the directory head: the root, the index_version and suite_id, and for a POPRF namespace the poprf_public key and public poprf_info string you blind under.

shell
curl https://mosskeys.com/api/acme/directory-proof
200 OK
{
  "directory_mode": "coniks",
  "index_version": "poprf",
  "namespace": "acme",
  "slug": "acme",
  "root": "<base64 64-byte SHA3-512 directory root>",
  "suite_id": 128,
  "poprf_public": "<base64 32-byte POPRF public key>",
  "poprf_info": "<base64 public info, e.g. bW9zc2tleXMvZGlyZWN0b3J5L3YxOjw4PU…>",
  "entries": 128
}

Step 1 — blind + evaluate. Blind the identity locally (poprfBlind in the browser SDK, MetamorphicCrypto.Poprf.blind/3 in Elixir) and POST only the blinded element to POST /api/:slug/oprf/evaluate. The server returns the evaluated element and a DLEQ proof. It is rate-limited per IP (it is an online dictionary oracle against low-entropy labels).

shell
curl -X POST https://mosskeys.com/api/acme/oprf/evaluate \
       -H "content-type: application/json" \
       -d '{"blinded_element": "<base64 32-byte blinded element from poprfBlind>"}'
200 OK
{
  "namespace": "acme",
  "evaluated_element": "<base64 32-byte evaluated element>",
  "proof": "<base64 64-byte DLEQ proof>"
}

Step 2 — unblind, derive the index, fetch the proof. Unblind and verify the DLEQ proof (poprfFinalize), take the leading 32 bytes of the output as the tree index, and GET /api/:slug/lookup/index?index=. The presence/absence proof verifies against the pinned root with no key material at all.

shell
curl "https://mosskeys.com/api/acme/lookup/index?index=<base64 32-byte derived index>"
200 OK
{
  "directory_mode": "coniks",
  "index_version": "poprf",
  "namespace": "acme",
  "slug": "acme",
  "index": "<base64 32-byte derived index>",
  "status": "present",
  "value": "<base64 entry_hash bound at the index>",
  "proof": "<base64 index-bound CONIKS proof>",
  "root": "<base64 64-byte SHA3-512 directory root>",
  "suite_id": 128,
  "entries": 128
}

value is the entry_hash the directory binds at that index: the commitment to the label's current key-history head. Fetch the label head to read the actual keys and confirm their entry_hash matches value (with an RFC 6962 inclusion proof).

Elixir, end to end (NIF verifier)

elixir
# Oblivious CONIKS lookup (RFC 9497 POPRF): resolve an identity to the
# value the directory binds it to — the server never sees the identity.
# The client blinds, the server evaluates the blinded element, and the
# client unblinds + verifies. No token is needed.
base = "https://mosskeys.com/api/acme"
label = "alice@example.com"

# Step 0: pin the public parameter set (the directory head).
%{body: head} = Req.get!("#{base}/directory-proof")
info = head["poprf_info"]
public = head["poprf_public"]

# Step 1: blind the label locally. Only the blinded element is sent.
{:ok, %{blind: blind, blinded_element: blinded, tweaked_key: tweaked}} =
  MetamorphicCrypto.Poprf.blind(Base.encode64(label), info, public)

# Step 2: the server evaluates the blinded element (never the label).
%{body: eval} =
  Req.post!("#{base}/oprf/evaluate", json: %{blinded_element: blinded})

# Step 3: unblind + verify the DLEQ proof, and derive the tree index.
{:ok, output} =
  MetamorphicCrypto.Poprf.finalize(
    Base.encode64(label), blind, eval["evaluated_element"], blinded,
    eval["proof"], info, tweaked
  )
index = output |> Base.decode64!() |> binary_part(0, 32) |> Base.encode64()

# Step 4: fetch + verify the presence/absence proof by index. The proof
# must verify against the root pinned in step 0 (a mismatch means the
# directory rotated mid-lookup — retry).
%{body: r} = Req.get!("#{base}/lookup/index", params: [index: index])
^index = r["index"]
^true = r["root"] == head["root"]

case r["status"] do
  "present" ->
    # {:ok, value} when the presence proof reconstructs the pinned root.
    # `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_indexed_lookup(
        r["namespace"], r["suite_id"], r["root"], index, r["proof"]
      )

  "absent" ->
    # :ok when the absence proof verifies: the directory binds no value.
    :ok =
      MetamorphicLog.Coniks.verify_indexed_absence(
        r["namespace"], r["suite_id"], r["root"], index, r["proof"]
      )
end

The blinding is classical cryptography (ristretto255) — recorded evaluation transcripts are not post-quantum private (the authenticity chain is: ML-DSA hybrid signatures). And the operator can always dictionary low-entropy labels offline, because it holds the evaluation key — that is inherent to any server-keyed deterministic index, and exactly what the per-IP rate limit bounds online. What POPRF removes completely is the passive query-time exposure: the server no longer sees which label you asked about. The directory root is re-randomized if the serving process restarts, so verify the {root, proof, value} response returns together rather than pinning a root across time.

Legacy VRF namespaces. Namespaces created before oblivious lookups shipped (or not yet upgraded in the owner's policy section) answer GET /api/:slug/lookup?label= with the identity blinded through the namespace VRF — the proof reveals no other identity, but the operator sees the queried label in memory at query time. A POPRF namespace refuses that endpoint by design.

shell
curl "https://mosskeys.com/api/acme/lookup?label=alice@example.com"
200 OK
{
  "directory_mode": "coniks",
  "index_version": "vrf",
  "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>",
  "suite_id": 3,
  "vrf_public": "<base64 VRF public key>",
  "entries": 128
}
200 OK
{
  "directory_mode": "coniks",
  "index_version": "vrf",
  "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>",
  "suite_id": 3,
  "vrf_public": "<base64 VRF public key>",
  "entries": 128
}

Elixir (legacy VRF verifier)

elixir
# Legacy VRF lookup (namespaces not yet upgraded to oblivious lookups).
# NOTE: the label is sent to the server for evaluation — the proof
# verifies client-side, but the operator sees the label at query time.
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

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).

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. On paid tiers, checkpoints are automatically relayed to the service's approved C2SP witnesses and their cosignatures merged into the served note, so the note itself shows which operators saw the same head. The CONIKS directory root is a separate structure and is not 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 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. On paid tiers the relay is automatic and merged cosignatures ride on the served note; count them per the best practices.

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

Verifier integration best practices

The sections above are the mechanics: one call each, verified locally. Running verification inside a real application adds a layer of practice, about what you pin, where it comes from, and how you treat the witness lines riding on a note. These are the rules the first-party verifiers follow.

  1. The namespace vkey is the root of trust. Pin it in config or bake it into the binary, obtained out of band from the namespace owner (their namespace page publishes it). Never read it from the same response you are verifying: a forged checkpoint would simply carry a forged key.
  2. Witness cosignatures are additive, and must never gate ok. A checkpoint stands or falls on the namespace signature alone. Each pinned-witness cosignature that verifies adds confidence that an independent operator saw the same tree head; an unknown name, a malformed line, or a failed cosignature is only ever ignored, never a failure. The reference implementation is Mosslet's transparency_log.js: a cosignature can add confidence but never downgrade the note's own verdict.
  3. Pin every vetted, independent witness there is. Diversity is the metric, not raw count: two witnesses from different operators, on different infrastructure, in different jurisdictions outweigh five from one. The directory and its affiliation labels (independent versus affiliated) are the vetting inputs.
  4. Discover from the roster, pin at release time. The roster is public: the witness directory and its JSON twin, GET https://mosskeys.com/api/witnesses, list every approved witness with its vkeys, operator, jurisdiction, and affiliation. Pull the roster when you cut a release, confirm the vkeys over a second channel (the operator's own site, a signed commit), and ship them pinned in the build. Do not fetch the roster at runtime: a verifier that trusts whatever the network serves has moved its root of trust to the network.
  5. witnesses: 0 is degraded signal, not an error. The checkpoint verified under the namespace key, but no pinned-witness cosignature rode on the note: the network may be young, the relay mid-flight, or your pinned roster stale. The key is still verified; only the independent-attestation layer is quiet. Surface it as a warning at most, never as a failed verification.
  6. Monitor the witnesses you pinned. If a pinned witness stops appearing in served notes, find out why: it may be paused for liveness, retired, or the relay may be unhealthy. A shrinking cosignature count is worth an alert, because the point of pinning witnesses is that they keep showing up.

See also