@lemmaoracle/seal
v0.3.0
Published
Seal — ZK auth circuit for Lemma dashboard Proof-based sign-in. Proves knowledge of a secret pre-image without revealing the secret or its hash (v2: Poseidon nullifier).
Readme
@lemmaoracle/seal
ZK auth circuit for Proof-based sign-in to the Lemma developer dashboard.
seal lets a developer prove they hold a valid Lemma API key without
revealing the key or even which key it is. It produces a per-session
Poseidon nullifier that is unique to the (secret, nonce) pair but reveals
nothing about the underlying secret or its SHA-256 hash.
How it works
The dashboard's sign-in flow:
- The dashboard BFF issues a challenge
nonce. - The developer generates a
sealproof: it proves knowledge of a secret whose SHA-256 hash is registered in the workersapi_keystable, bound to thatnonce. The public output is a nullifier —Poseidon(keyHash_hi, keyHash_lo, nonce)— not the key hash itself. - The BFF verifies the proof, then iterates registered
key_hashvalues in D1, computing the expected nullifier for each until it finds a match (O(N), sub-millisecond per check). - The BFF issues a session token tied to the resolved scope.
The proof reveals neither the secret nor its hash — only the nullifier
and the nonce. Because nonce changes every session, nullifiers cannot
be correlated across sign-ins, even for the same secret.
Proof-based sign-in requires an existing API key. First-time users onboard via GitHub OAuth, which issues their first key.
Package layout
seal/
├── circuits/
│ ├── src/seal-identity.circom Circuit: SHA-256 pre-image proof + Poseidon nullifier
│ ├── src/seal-identity.test.ts Circuit witness tests (needs a build)
│ └── scripts/build.sh Compile circom → wasm + groth16 setup
├── scripts/
│ ├── register-circuit.ts Pin artifacts to IPFS, register via SDK
│ └── setup-toolchain.sh Install Rust + circom
├── src/ TypeScript proof helpers (published)
│ ├── bits.ts Secret ↔ circuit signal conversions
│ ├── proof.ts prove / verify (delegates to @lemmaoracle/sdk)
│ ├── vkey.ts Bundled verification key + named export
│ └── index.ts Public API
└── .env.example Credentials for register-circuit.tsseal is a reference definition. It is published to npm so
developers can generate proofs; it is not imported by workers or the
dashboard at runtime — the circuit reaches them through the normal
Lemma circuit-registration path.
The circuit
seal-identity.circom (v2) proves knowledge of a registered secret and
outputs a per-session nullifier:
- Private input —
keyBits[512]: the 64-byte ASCII secret as bits. Lemma keys are 32 random bytes rendered as 64 hex characters (see the workersgenerate_api_key.js). - Public input —
nonce: the dashboard challenge, bound into the constraint system for replay protection. - Public output —
nullifier:Poseidon(keyHash_hi, keyHash_lo, nonce). A single BN254 field element; unique per (secret, nonce) pair. Add ~300 constraints on top of the SHA-256 (~60k total).
The SHA-256 hashing matches the workers middleware/auth.ts
(SHA-256(utf8_bytes(secret))), but keyHash is now an intermediate
signal — it never appears in public signals.
Build the circuit
The circom toolchain is not required to install or test this package
— only to compile the circuit and run register-circuit.ts.
# One-time: install Rust + circom (≈ a few minutes)
./scripts/setup-toolchain.sh
# Compile → build/seal-identity_js/seal-identity.wasm + _final.zkey
cd circuits && npm install && npm run buildbuild.sh downloads the 2^17 Hermez powers-of-tau file (~290 MB) on
first run. The v2 circuit adds Poseidon (~300 constraints) to the SHA-256
base (~60k), staying well within the 2^17 budget.
Register the circuit
cp .env.example .env # then fill in LEMMA_API_KEY + PINATA_* keys
npm run register:circuitThis pins the .wasm / .zkey to IPFS via Pinata and registers a
CircuitMeta (circuitId: seal-identity-v1, schema: passthrough-v1,
off-chain groth16-bn254-snarkjs verifier) with the workers API. The
dashboard BFF then fetches verification params at runtime via
GET /v1/circuits/seal-identity-v1.
Generate a proof (developer usage)
import * as seal from "@lemmaoracle/seal";
const { proof, publicSignals, nullifier } = await seal.prove({
secret: process.env.LEMMA_API_KEY!,
nonce: challengeNonce,
});
// POST { proof, publicSignals, token } to the dashboard sign-in endpoint.Circuit artifacts (wasm, zkey) are resolved automatically via the
@lemmaoracle/sdk from the registered circuit metadata — no local
artifact paths are required.
Verify a proof
import * as seal from "@lemmaoracle/seal";
const result = await seal.verify({ proof, publicSignals, nullifier });
// result: { nullifier, nonce } | nullThe verification key is bundled internally — no additional arguments are
needed. Verification delegates to @lemmaoracle/sdk verifier.
Access the verification key
import { sealVkey } from "@lemmaoracle/seal";
// Or via the dedicated sub-export:
import vkey from "@lemmaoracle/seal/vkey";v2 migration notes
v1 exposed keyHash[256] as a public output, making the key hash
readable by any observer of the proof transcript. v2 replaces this with
a Poseidon nullifier that is uncorrelatable across sessions.
Breaking changes:
SEAL_CIRCUIT_IDis now"seal-identity-v1"(requires re-registration).SealProof.keyHashremoved; replaced bySealProof.nullifier.- Server-side:
scopeIdForKeyHash()replaced byscopeIdForNullifier()(full D1 scan +poseidon-litecomputation). - Circuit artifacts (wasm, zkey, vkey) must be regenerated.
v3 migration notes:
generateSealProofrenamed toprove;verifySealProofrenamed toverify.SealProofInput.apiKeyrenamed toSealProofInput.secret.apiKeyToBitsrenamed tosecretToBits;SEAL_KEY_BYTES/SEAL_KEY_BITSrenamed toSEAL_SECRET_BYTES/SEAL_SECRET_BITS.SealArtifactstype removed — circuit artifacts are resolved automatically via@lemmaoracle/sdk.proveno longer requires a secondartifactsargument.verifyno longer requires averificationKeyargument — the vkey is bundled internally.sealVkeynamed export added for direct vkey access.snarkjsis no longer a direct dependency — it is used via@lemmaoracle/sdk.
Scripts
| Command | Description |
| :------------------------ | :----------------------------------------- |
| npm run build | Compile the TypeScript proof helpers |
| npm test | Run the pure unit tests (no toolchain) |
| npm run build:circuit | Compile the circom circuit |
| npm run test:circuit | Run circuit witness tests (needs a build) |
| npm run register:circuit| Pin artifacts to IPFS and register via SDK |
License
MIT
