Docs · Reference
Proof format
Five parameters, one signed line. Everything the registry can conclude comes from what that line binds together.
The signed message
Five fields joined by colons, signed as UTF-8 bytes. The version prefix is part of the message, so a v1 signature can never be replayed as a v2 one.
v2:<site>:<host>:<ts>:<nonce>The signature is Ed25519 over that message, encoded base64url. The five fields plus sig travel as query parameters — nothing else, and nothing secret.
https://registry.liorith.net/verify
?site=demo
&host=demo.liorith.net
&ts=1756800000
&nonce=9f2c1ab4e7d05c31
&sig=MEUCIQD...What each part rules out
- The signature rules out forgery
Only the holder of the private key can produce it. The registry holds only the public key, so a leak of registry data cannot be turned into proofs.
- The timestamp rules out old proofs
A proof is valid for 120 seconds. Timestamps more than 30 seconds in the future are refused as
clock_skewrather than accepted, so a wrong clock fails loudly instead of widening the window. - The nonce rules out reuse
The registry consumes it atomically on the first verification. A second request with the same URL answers
replay, even inside the two minutes. - The host rules out borrowed proofs
The host is inside the signed message and is checked against the site's allowed domains. A valid proof pasted onto another domain answers
host_mismatch.
Signing
Use the SDK if you are on JavaScript. If you are not, the scheme is small enough to implement directly — Ed25519 is in every mainstream crypto library.
import { signProofV2, proofToUrl } from 'liorith-registry-sdk';
const params = signProofV2(siteId, host, privateKeyPem);
const url = proofToUrl(params);import { createPrivateKey, randomBytes, sign } from 'crypto';
const ts = Math.floor(Date.now() / 1000);
const nonce = randomBytes(12).toString('hex');
const msg = Buffer.from(`v2:${siteId}:${host}:${ts}:${nonce}`);
const sig = sign(null, msg, createPrivateKey(pem)).toString('base64url');Keys
Generate a pair with npx liorith-verify keygen. The private key is a PKCS8 PEM and stays on your server; the public key is an SPKI PEM and goes into the admin panel under your site.
A PEM carries line breaks, which a Compose .env cannot transport. Pass it base64-encoded and decode it at startup, or keep it in a real secret store.
LIORITH_REGISTRY_PRIVATE_KEY_B64=LS0tLS1CRUdJTi... # base64 of the PEM
LIORITH_REGISTRY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..." # or the PEM directlyv1, and why it is not the default
The original scheme signs v1:<site>:<host>:<ts>:<nonce> with HMAC-SHA256 and a shared secret, encoded hex. It still verifies, so existing integrations keep working.
It is not what new integrations should use: a shared secret means the registry has to store a value that can produce proofs. With Ed25519 the registry stores only a public key, so nothing on our side can sign for your site. Rotating to v2 is a keygen and a paste into the admin panel.