mosskeys
Quickstart & integrate

Start here

Here you'll learn more about what mosskeys is, when to reach for it, and a copy-pasteable path from signup to key to independent verification; complete with two example integrations of in-production applications. For a full reference, check out the write and read & verify pages and use this page if you need help tieing it all together.

What is mosskeys?

mosskeys is a verifiable public-key directory. A namespace is an append-only, tamper-evident log of key-history entries. You publish public keys and client-signed checkpoints and anyone can verify a key is genuinely in the log, without trusting the server.

It is zero-knowledge by construction: keys are generated and encrypted client-side, and the server never sees a private key or a signing key. Everything that crosses the wire is either already public (public keys, tree size, tree root) or something you signed locally first (checkpoints).

Think of it as a phone book for public keys that cannot lie. A relying party does not have to trust that the directory handed it the right key; it can prove it mathematically against a signed snapshot.

What can I prove?

mosskeys proves the following very well: (1) that a label is bound to a public key, (2) that you published that binding, and (3) that the binding lives in a history nobody rewrote. That actually covers a lot of ground, because a "label" is any subject you can name: a person, a service, a device, an autonomous agent, or any other nameable thing.

It also proves the negative: that a label has no key at all (an absence proof). Both presence and absence lookups are privacy-preserving: the label is blinded through the namespace's VRF, so the server learns neither which label you asked for nor whether it exists. See private lookup.

However, it does not prove identity in the abstract, and it never proves anything about a private key. It proves a verifiable, non-equivocating binding: everyone who looks sees the same key for a label, and every rotation is visible in order. What you build on top of that (encrypting to the key, checking a signature made by it) is where the binding turns into a real guarantee.

Example: prove an AI agent is yours

Say you run an agent and want others to trust that a given key really is your agent's, not an impostor's. Publish the agent's public signing key under a label you control, then anchor it with a signed checkpoint:

shell
# Publish the agent's PUBLIC signing key under a label you control. The label
# is any stable name for the subject: an agent id, a DID, a service name.
mosskeys keygen --namespace acme --security-level cat5 \
  --label "agent:billing-bot@acme" --out ./billing-bot.keys

mosskeys publish --file ./billing-bot.keys
mosskeys checkpoint --key ./checkpoint.key   # anchor it in a signed tree head

Now anyone can resolve the label and verify the key against your signed checkpoint before trusting a message the agent signed:

shell
# Anyone interacting with the agent resolves its label and verifies the
# key against your signed checkpoint (see the read & verify walkthrough).
# A passing proof means: this key is the one YOU published for this label,
# in a history that provably was not rewritten. The agent then signs its
# actions with the matching private key, and relying parties check those
# signatures against this verified public key.
curl https://mosskeys.com/api/acme/log/label/agent:billing-bot@acme

The agent signs its actions with the matching private key; relying parties check those signatures against this verified public key. Rotate or revoke by appending a new entry: consistency proofs show the history was never quietly rewritten. This is the same machinery as the human-user examples below, pointed at a non-human subject.

The mental model

A namespace has three moving parts. Once these click, the rest of the API is detail.

  1. Entries (you append). Each entry is the public key material for one identity, keyed by a label (a subject or key id: an email, user id, or device id). Appends are authenticated and idempotent, so a rotation is just another append under the same label.
  2. Checkpoints (you publish). A checkpoint is a signed tree head: a commitment to the entire log at a point in time, signed by your key, never the server's. Verifiers pin to it as their root of trust.
  3. Proofs (anyone verifies). The public read API serves entries, checkpoints, and RFC 6962 inclusion and consistency proofs. An inclusion proof shows a key is in the log a checkpoint committed to; a consistency proof shows the log only grew and never rewrote history. A private lookup proves a label's newest key, or its absence, with the label blinded through the namespace's VRF.

See the glossary below if any of these terms are new.

Policies and attestation

Every namespace carries a policy: the log's public promise of the security profile it runs under. A policy pins the security level (Cat-3 or Cat-5), the checkpoint signature suite (hybrid, hybrid matched, or pure CNSA 2.0), the commitment hash and VRF mode derived from them, and the directory backend (CONIKS or the experimental KEYTRANS route).

Your namespace signing key is what proves the promise, which is why the browser generates the key to match the profile you choose at creation. Attesting then signs the promise into the log itself (one click under Policy → Attest policy). From then on, a checkpoint is accepted only when it comes from that key, under that profile, so clients and monitors can trust the profile has not silently changed.

Changing the profile appends a new policy version; old versions are never edited, so the history of every promise stays auditable. The directory backend changes freely. A security level or signature suite your current key cannot sign is a key rotation: the browser generates a fresh key for the new profile, seals it under your account key, and records the old-to-new rotation in the log, so monitors see the transition and checkpoints signed before it stay verifiable under the retired key. The new version is appended unattested and attested with the new key as a second step, so checkpointing is never interrupted mid-ceremony. If you ever pick a combination the key cannot sign for, attestation refuses loudly instead of letting a bad promise into the log.

When should I use mosskeys?

Reach for it when

  • You distribute public keys and users must trust the right key is being used (E2EE messaging, signing, device pairing).
  • You want key rotations to be auditable and tamper-evident over time.
  • You need independent verification, where a third party can check your log without an account.
  • You want privacy-preserving lookups: prove a label does (or does not) have a key without revealing which label was asked for. See private lookup.
  • Post-quantum readiness matters (hybrid Ed25519 + ML-DSA / ML-KEM).

Probably not when

  • You need to store secrets; mosskeys is public material only.
  • A plain key-value store or your existing PKI already meets your trust needs and nobody needs to independently audit it.

Free vs paid: what you get, and the gap paid closes

The one capability that separates the tiers is checkpoint signing. Everything else (appends, reads, and RFC 6962 proofs) works on every tier; paid tiers raise the limits and unlock publishing signed tree heads.

What the free tier gives you

The free tier is a real, useful append-only directory, not just a trial. With it you can:

  • Create one namespace and publish up to a few thousand key entries a month with one API token.
  • Append and rotate keys, and read them back with full inclusion and consistency proofs against the log's Merkle tree.
  • Wire up and test your whole publish plus verify pipeline end to end.

The gap paid closes

Without a checkpoint, a verifier can only check a proof against a root the server asserts: the math holds, but the trust still rests on the server. A signed checkpoint is what removes that trust: it is a tree head signed by your key, so a third party verifies against a root you committed to, and can detect a server that ever tried to present split views or drop a rotation. Publishing signed checkpoints requires a paid tier ( checkpoint_signing_enabled?); the server itself still never signs.

Rule of thumb: use free to build, prototype, and prove out the flow. Move to a paid tier the moment you need outside parties to verify your keys without trusting mosskeys, or you outgrow the free limits. See billing for current tiers and limits.

Quickstart: from signup to verified key

The fastest path uses the mosskeys CLI for the write side (it handles BYOK checkpoint signing for you) and a plain curl for the read side. Every step maps to a section in the full reference.

  1. Sign up and create a namespace, choosing a security profile (cat3 or cat5) from your namespaces page. The slug you pick (for example acme) is the public handle in every URL.
  2. Issue an API token from Settings → API tokens with the append (and checkpoint) capabilities, then export it:
    shell
    export MOSSKEYS_TOKEN=msk_live_…
    export MOSSKEYS_BASE_URL=https://mosskeys.com   # optional override
  3. Generate keys locally. Fully offline; only the public halves are ever printed or published:
    shell
    # fully offline: generates the key set locally and prints only the PUBLIC
    # halves. --security-level must match your namespace's security profile.
    mosskeys keygen \
      --namespace acme \
      --security-level cat3 \
      --label alice@example.com \
      --out ./alice.keys
  4. Publish an entry and a checkpoint.
    shell
    # publish the public halves; server dedups so retries are safe.
    mosskeys publish --file ./alice.keys
    
    # publish a client-signed checkpoint (fetch head, sign locally, publish).
    mosskeys checkpoint --key ./checkpoint.key
    Publishing a signed checkpoint requires a paid tier (see free vs paid); on the free tier you can still append and read entries with proofs.
  5. Fetch and verify from another client. No token, no account:
    shell
    # from any other client, fetch the current key for a label and verify it
    # against the signed checkpoint. No token, no account needed.
    curl https://mosskeys.com/api/acme/log/label/alice@example.com
    That response bundles the newest key for the label, the latest signed checkpoint, and an inclusion proof. Feed them to any verifier (see the full read & verify walkthrough) and, if it does not throw, the key is provably genuine.
Prefer raw HTTP over the CLI? Every step above has an equivalent request documented on the write and read & verify pages, in curl, Ruby, Python, Go, JavaScript, and Elixir.

Example: Mosslet (Elixir / Phoenix)

Mosslet is a privacy-first social app. It consumes mosskeys server-side through the audited metamorphic_log NIF to publish each user's key rotations and to verify a peer's current key before encrypting to them. Add the dep first:

elixir
{:metamorphic_log, "~> 0.1"}

Publish a user's key rotation

elixir
# Mosslet publishes a user's PUBLIC key set to its namespace whenever the
# user rotates keys. Private halves stay encrypted client-side and never
# reach Mosslet's servers, let alone mosskeys. The label is the stable
# user id, so peers can always resolve a user's current key.
defp publish_key_rotation(user, pub) do
  %{status: 200, body: %{"index" => index}} =
    Req.post!("https://mosskeys.com/api/mosslet/log/entries",
      auth: {:bearer, System.fetch_env!("MOSSKEYS_TOKEN")},
      json: %{
        label: user.id,
        enc_x25519: pub.enc_x25519,
        enc_pq: pub.enc_pq,
        signing_pub: pub.signing_pub
      }
    )

  {:ok, index}
end

Publish a signed checkpoint (BYOK)

elixir
# An Oban job publishes a signed checkpoint on a cadence so peers always
# have a fresh signed tree head to verify against. The checkpoint signing
# key is Mosslet's own (BYOK, held server-side); mosskeys never sees it.
def publish_checkpoint do
  base = "https://mosskeys.com/api/mosslet"
  token = System.fetch_env!("MOSSKEYS_TOKEN")

  # Phase 1: ask mosskeys for the current head to sign.
  %{"origin" => origin, "size" => size, "root" => root} =
    Req.post!("#{base}/log/checkpoints", auth: {:bearer, token}).body

  # Sign that head locally with Mosslet's hybrid PQ key, via the NIF.
  # sign_dual emits the hybrid line plus a classical Ed25519 (0x01)
  # line, so stock C2SP witnesses can verify and cosign the checkpoint.
  {:ok, note} =
    MetamorphicLog.Checkpoint.sign_dual(
      origin,
      size,
      root,
      origin,
      System.fetch_env!("MOSSLET_CHECKPOINT_SK")
    )

  # Phase 2: publish the signed note.
  %{status: 201} =
    Req.post!("#{base}/log/checkpoints",
      auth: {:bearer, token},
      json: %{note_text: note}
    )

  :ok
end

Verify a peer's key before use

elixir
# Before Mosslet encrypts to a peer, it verifies the peer's current key
# against Mosslet's own signed checkpoint. It never trusts the network:
# if mosskeys served a forged key, verify_inclusion would fail.
def fetch_verified_peer_key(peer_id) do
  base = "https://mosskeys.com/api/mosslet"
  # Pinned out of band (config), never read from the response we verify.
  vkeys = Application.fetch_env!(:mosslet, :mosskeys_vkeys)

  # One request resolves the label to its newest entry + a bundled proof.
  head = Req.get!("#{base}/log/label/#{URI.encode(peer_id)}").body

  :ok =
    MetamorphicLog.Checkpoint.verify_inclusion(
      head["checkpoint"]["note"],
      vkeys,
      head["inclusion_proof"]["index"],
      head["inclusion_proof"]["leaf_hash"],
      head["inclusion_proof"]["proof"]
    )

  # `head["leaf"]` is the canonical public-key bytes, now provably the
  # key committed to by the signed checkpoint. Safe to use.
  {:ok, Base.decode64!(head["leaf"])}
end

The vkeys Mosslet verifies against are its namespace's published verifier keys, pinned in config as the root of trust. They are never fetched from the same response being verified.

Example: Metamorphic

Metamorphic integrates the same way against its own namespace slug. Publishing a connection's public identity key is the same single call:

elixir
# Metamorphic integrates identically, against its own namespace slug.
# It keys each entry by the member's user id and publishes that user's
# public halves (X25519 + ML-KEM-768) so fellow group members can resolve
# and verify each other's current key. Same single call.
%{status: 200, body: %{"index" => _index}} =
  Req.post!("https://mosskeys.com/api/metamorphic/log/entries",
    auth: {:bearer, System.fetch_env!("MOSSKEYS_TOKEN")},
    json: %{
      label: user.id,
      enc_x25519: pub.enc_x25519,
      enc_pq: pub.enc_pq,
      signing_pub: pub.signing_pub
    }
  )

Where it differs is monitoring: Metamorphic runs a background monitor that, on every new checkpoint, proves the log only ever grew and never rewrote history, using a consistency proof. This is the split-view / equivocation check.

elixir
# Metamorphic also runs a monitor: on each new checkpoint it proves the
# log only grew and never rewrote history (the split-view / equivocation
# check). It pins the last checkpoint it trusted and verifies consistency.
def advance_pin(pinned_note, pinned_size) do
  base = "https://mosskeys.com/api/metamorphic"
  vkeys = Application.fetch_env!(:metamorphic, :mosskeys_vkeys)

  latest = Req.get!("#{base}/checkpoint").body

  proof =
    Req.get!("#{base}/log/proof/consistency",
      params: [from: pinned_size, to: latest["size"]]
    ).body

  # Verifies BOTH notes' signatures AND that `latest` is an append-only
  # extension of the pinned checkpoint. Raises on any rewrite.
  :ok =
    MetamorphicLog.Checkpoint.verify_consistency(
      pinned_note,
      latest["note"],
      vkeys,
      proof["proof"]
    )

  {:ok, latest} # advance the pin to the newly-verified checkpoint
end

Between them the two apps exercise both verification verbs: Mosslet checks inclusion (is this exact key in the log?) and Metamorphic checks consistency (has the log stayed honest over time?).

Glossary

Term Meaning
label The subject or key id an entry is keyed by (email, user id, device id). Looking up a label returns its newest key-history entry.
entry / leaf One published record of public key material for a label. It is a leaf in the Merkle tree; a rotation appends a new leaf under the same label.
checkpoint / STH Signed tree head: a client-signed commitment to the whole log at a given size and root. Your root of trust for verification.
inclusion proof An RFC 6962 proof that a specific leaf is committed to by a checkpoint's signed root. Answers "is this exact key in the log?"
consistency proof An RFC 6962 proof that a newer checkpoint is an append-only extension of an older one: nothing was rewritten or removed.
security profile The post-quantum parameter set for a namespace's keys: cat3 or cat5. Chosen at namespace creation and matched by keygen --security-level.
verifier key (vkey) The namespace's public checkpoint-signing key, in C2SP form. Pinned out of band; it is what a verifier checks a checkpoint's signature against.
witness An independent party that observes checkpoints and countersigns or monitors them, strengthening the guarantee that the log never presents split views.

See also

  • Write API: append entries and publish client-signed checkpoints, with examples in six languages.
  • Read & verify API: fetch keys and proofs and verify them, with full WASM and Elixir walkthroughs.
  • Offline verification and reconciliation: pin a signed checkpoint, verify keys with no network, and catch up in one request on reconnect (offline-first apps and edge / DDIL nodes).