@lemmaoracle/seal 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/circuits/scripts/build.sh +65 -0
- package/circuits/src/seal-identity.circom +65 -0
- package/dist/bits.d.ts +23 -0
- package/dist/bits.d.ts.map +1 -0
- package/dist/bits.js +38 -0
- package/dist/bits.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/proof.d.ts +30 -0
- package/dist/proof.d.ts.map +1 -0
- package/dist/proof.js +50 -0
- package/dist/proof.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/vkey.d.ts +10 -0
- package/dist/vkey.d.ts.map +1 -0
- package/dist/vkey.js +10 -0
- package/dist/vkey.js.map +1 -0
- package/dist/vkeys/seal-identity-v1.json +1374 -0
- package/package.json +54 -0
- package/scripts/setup-toolchain.sh +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @lemmaoracle/seal
|
|
2
|
+
|
|
3
|
+
ZK auth circuit for **Proof-based sign-in** to the Lemma developer dashboard.
|
|
4
|
+
|
|
5
|
+
`seal` lets a developer prove they hold a valid Lemma API key **without
|
|
6
|
+
revealing the key**. It is a [zero-knowledge proof](https://lemma.frame00.com)
|
|
7
|
+
of the pre-image of the key's SHA-256 hash.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
The dashboard's sign-in flow:
|
|
12
|
+
|
|
13
|
+
1. The dashboard BFF issues a challenge `nonce`.
|
|
14
|
+
2. The developer generates a `seal` proof: it proves knowledge of the
|
|
15
|
+
API key whose SHA-256 hash equals the `key_hash` stored in the
|
|
16
|
+
workers `api_keys` table, bound to that `nonce`.
|
|
17
|
+
3. The BFF verifies the proof, reads the attested `key_hash`, looks it
|
|
18
|
+
up in `api_keys`, and resolves the caller's `scope_id`.
|
|
19
|
+
4. The BFF issues a session token tied to the scope.
|
|
20
|
+
|
|
21
|
+
The proof reveals only the `key_hash` (already public, since it is what
|
|
22
|
+
the API stores) and the `nonce` — never the API key itself.
|
|
23
|
+
|
|
24
|
+
> Proof-based sign-in requires an existing API key. First-time users
|
|
25
|
+
> onboard via GitHub OAuth, which issues their first key.
|
|
26
|
+
|
|
27
|
+
## Package layout
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
seal/
|
|
31
|
+
├── circuits/
|
|
32
|
+
│ ├── src/seal-identity.circom Circuit: SHA-256 pre-image proof
|
|
33
|
+
│ ├── src/seal-identity.test.ts Circuit witness tests (needs a build)
|
|
34
|
+
│ └── scripts/build.sh Compile circom → wasm + groth16 setup
|
|
35
|
+
├── scripts/
|
|
36
|
+
│ ├── register-circuit.ts Pin artifacts to IPFS, register via SDK
|
|
37
|
+
│ └── setup-toolchain.sh Install Rust + circom
|
|
38
|
+
├── src/ TypeScript proof helpers (published)
|
|
39
|
+
│ ├── bits.ts API key ↔ circuit signal conversions
|
|
40
|
+
│ ├── proof.ts generateSealProof / verifySealProof
|
|
41
|
+
│ └── index.ts Public API
|
|
42
|
+
└── .env.example Credentials for register-circuit.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
There is no `normalize/` step: the circuit's witness input is the raw
|
|
46
|
+
API key string (already canonical), so seal uses the existing
|
|
47
|
+
[`passthrough-v1`](../passthrough) schema rather than a bespoke
|
|
48
|
+
normalizer.
|
|
49
|
+
|
|
50
|
+
`seal` is a **reference definition**. It is published to npm so
|
|
51
|
+
developers can generate proofs; it is *not* imported by `workers` or the
|
|
52
|
+
dashboard at runtime — the circuit reaches them through the normal
|
|
53
|
+
Lemma circuit-registration path.
|
|
54
|
+
|
|
55
|
+
## The circuit
|
|
56
|
+
|
|
57
|
+
`seal-identity.circom` proves `SHA-256(apiKey) == keyHash`:
|
|
58
|
+
|
|
59
|
+
- **Private input** — `keyBits[512]`: the 64-byte ASCII API key as bits.
|
|
60
|
+
Lemma keys are 32 random bytes rendered as 64 hex characters (see the
|
|
61
|
+
workers `generate_api_key.js`).
|
|
62
|
+
- **Public input** — `nonce`: the dashboard challenge, bound into the
|
|
63
|
+
constraint system for replay protection.
|
|
64
|
+
- **Public output** — `keyHash[256]`: the SHA-256 digest bits.
|
|
65
|
+
|
|
66
|
+
The hashing matches the workers `middleware/auth.ts`
|
|
67
|
+
(`SHA-256(utf8_bytes(apiKey))`), so a proof's `keyHash` is directly
|
|
68
|
+
comparable to `api_keys.key_hash`.
|
|
69
|
+
|
|
70
|
+
## Build the circuit
|
|
71
|
+
|
|
72
|
+
The circom toolchain is **not** required to install or test this package
|
|
73
|
+
— only to compile the circuit and run `register-circuit.ts`.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# One-time: install Rust + circom (≈ a few minutes)
|
|
77
|
+
./scripts/setup-toolchain.sh
|
|
78
|
+
|
|
79
|
+
# Compile → build/seal-identity_js/seal-identity.wasm + _final.zkey
|
|
80
|
+
cd circuits && npm install && npm run build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`build.sh` downloads the 2^17 Hermez powers-of-tau file (~290 MB) on
|
|
84
|
+
first run — SHA-256 over a 512-bit pre-image is ~60k constraints.
|
|
85
|
+
|
|
86
|
+
## Register the circuit
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cp .env.example .env # then fill in LEMMA_API_KEY + PINATA_* keys
|
|
90
|
+
npm run register:circuit
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This pins the `.wasm` / `.zkey` to IPFS via Pinata and registers a
|
|
94
|
+
`CircuitMeta` (`circuitId: seal-identity-v1`, `schema: passthrough-v1`,
|
|
95
|
+
off-chain `groth16-bn254-snarkjs` verifier) with the workers API. The
|
|
96
|
+
dashboard BFF then fetches verification params at runtime via
|
|
97
|
+
`GET /v1/circuits/seal-identity-v1`.
|
|
98
|
+
|
|
99
|
+
## Generate a proof (developer usage)
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { generateSealProof } from "@lemmaoracle/seal";
|
|
103
|
+
|
|
104
|
+
const { proof, publicSignals, keyHash } = await generateSealProof(
|
|
105
|
+
{ apiKey: process.env.LEMMA_API_KEY!, nonce: challengeNonce },
|
|
106
|
+
{ wasm: "seal-identity.wasm", zkey: "seal-identity_final.zkey" },
|
|
107
|
+
);
|
|
108
|
+
// POST { proof, publicSignals } to the dashboard sign-in endpoint.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Scripts
|
|
112
|
+
|
|
113
|
+
| Command | Description |
|
|
114
|
+
| :------------------------ | :----------------------------------------- |
|
|
115
|
+
| `npm run build` | Compile the TypeScript proof helpers |
|
|
116
|
+
| `npm test` | Run the pure unit tests (no toolchain) |
|
|
117
|
+
| `npm run build:circuit` | Compile the circom circuit |
|
|
118
|
+
| `npm run test:circuit` | Run circuit witness tests (needs a build) |
|
|
119
|
+
| `npm run register:circuit`| Pin artifacts to IPFS and register via SDK |
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Compile the seal-identity circuit and run the groth16 trusted setup.
|
|
4
|
+
# Prerequisite: circom + the circuits/ npm dependencies — install them
|
|
5
|
+
# with ../scripts/setup-toolchain.sh.
|
|
6
|
+
#
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
CIRCUIT_NAME="seal-identity"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
|
+
SRC_DIR="$SCRIPT_DIR/src"
|
|
12
|
+
BUILD_DIR="$SCRIPT_DIR/build"
|
|
13
|
+
PTAU_DIR="$BUILD_DIR/ptau"
|
|
14
|
+
CIRCOMLIB_DIR="$SCRIPT_DIR/node_modules"
|
|
15
|
+
SNARKJS="npx snarkjs"
|
|
16
|
+
|
|
17
|
+
# SHA-256 over a 512-bit pre-image compiles to ~60k R1CS constraints
|
|
18
|
+
# (two 512-bit blocks), so phase-1 powers of tau must cover 2^17.
|
|
19
|
+
PTAU_POWER=17
|
|
20
|
+
PTAU="$PTAU_DIR/pot${PTAU_POWER}_final.ptau"
|
|
21
|
+
PTAU_URL="https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_${PTAU_POWER}.ptau"
|
|
22
|
+
|
|
23
|
+
mkdir -p "$BUILD_DIR" "$PTAU_DIR"
|
|
24
|
+
|
|
25
|
+
command -v circom >/dev/null 2>&1 || {
|
|
26
|
+
echo "✗ circom not found — run ../scripts/setup-toolchain.sh first" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
echo "→ compiling $CIRCUIT_NAME"
|
|
31
|
+
circom "$SRC_DIR/$CIRCUIT_NAME.circom" \
|
|
32
|
+
--r1cs \
|
|
33
|
+
--wasm \
|
|
34
|
+
--sym \
|
|
35
|
+
-l "$CIRCOMLIB_DIR" \
|
|
36
|
+
-o "$BUILD_DIR"
|
|
37
|
+
|
|
38
|
+
echo "→ constraint info"
|
|
39
|
+
$SNARKJS r1cs info "$BUILD_DIR/$CIRCUIT_NAME.r1cs"
|
|
40
|
+
|
|
41
|
+
# Phase 1 — powers of tau. The 2^17 file is ~290 MB; downloading the
|
|
42
|
+
# pre-generated Hermez ceremony output is far faster and safer than
|
|
43
|
+
# generating one locally.
|
|
44
|
+
if [ ! -f "$PTAU" ]; then
|
|
45
|
+
echo "→ downloading powers of tau (2^${PTAU_POWER})"
|
|
46
|
+
curl -fL "$PTAU_URL" -o "$PTAU"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "→ groth16 setup"
|
|
50
|
+
$SNARKJS groth16 setup \
|
|
51
|
+
"$BUILD_DIR/$CIRCUIT_NAME.r1cs" \
|
|
52
|
+
"$PTAU" \
|
|
53
|
+
"$BUILD_DIR/${CIRCUIT_NAME}_0000.zkey"
|
|
54
|
+
|
|
55
|
+
$SNARKJS zkey contribute \
|
|
56
|
+
"$BUILD_DIR/${CIRCUIT_NAME}_0000.zkey" \
|
|
57
|
+
"$BUILD_DIR/${CIRCUIT_NAME}_final.zkey" \
|
|
58
|
+
--name="lemma seal-identity" -v -e="lemma seal-identity $(date +%s)"
|
|
59
|
+
|
|
60
|
+
echo "→ exporting verification key"
|
|
61
|
+
$SNARKJS zkey export verificationkey \
|
|
62
|
+
"$BUILD_DIR/${CIRCUIT_NAME}_final.zkey" \
|
|
63
|
+
"$BUILD_DIR/${CIRCUIT_NAME}_vkey.json"
|
|
64
|
+
|
|
65
|
+
echo "✓ $CIRCUIT_NAME built → $BUILD_DIR"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
pragma circom 2.1.0;
|
|
2
|
+
|
|
3
|
+
include "circomlib/circuits/sha256/sha256.circom";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SealIdentity — Proof-based sign-in for the Lemma developer dashboard.
|
|
7
|
+
*
|
|
8
|
+
* Proves knowledge of the pre-image of an API key hash without revealing
|
|
9
|
+
* the key. The pre-image is the raw API key string; the hash is the
|
|
10
|
+
* SHA-256 `key_hash` stored in the workers `api_keys` D1 table.
|
|
11
|
+
*
|
|
12
|
+
* This matches the hashing in workers `middleware/auth.ts`:
|
|
13
|
+
* key_hash = SHA-256( utf8_bytes(apiKey) )
|
|
14
|
+
*
|
|
15
|
+
* Lemma API keys are 32 random bytes rendered as a 64-character
|
|
16
|
+
* lowercase hex string (see workers `generate_api_key.js`), so the
|
|
17
|
+
* SHA-256 pre-image is exactly 64 ASCII bytes = 512 bits.
|
|
18
|
+
*
|
|
19
|
+
* Private input:
|
|
20
|
+
* keyBits[512] Bit decomposition of the 64-byte ASCII API key,
|
|
21
|
+
* most-significant-bit first within each byte.
|
|
22
|
+
*
|
|
23
|
+
* Public input:
|
|
24
|
+
* nonce Dashboard-issued challenge nonce. Bound into the
|
|
25
|
+
* constraint system so a proof cannot be replayed
|
|
26
|
+
* against a different challenge.
|
|
27
|
+
*
|
|
28
|
+
* Public output:
|
|
29
|
+
* keyHash[256] SHA-256 digest bits. The dashboard BFF reassembles
|
|
30
|
+
* these into the hex `key_hash` and looks it up in
|
|
31
|
+
* `api_keys` to resolve the caller's `scope_id`.
|
|
32
|
+
*/
|
|
33
|
+
template SealIdentity(keyBytes) {
|
|
34
|
+
var keyBitLen = keyBytes * 8;
|
|
35
|
+
|
|
36
|
+
signal input keyBits[keyBitLen];
|
|
37
|
+
signal input nonce;
|
|
38
|
+
signal output keyHash[256];
|
|
39
|
+
|
|
40
|
+
// Constrain every pre-image bit to be boolean. Without this a
|
|
41
|
+
// malicious prover could feed non-binary field values into the
|
|
42
|
+
// SHA-256 gadget and forge a witness.
|
|
43
|
+
for (var i = 0; i < keyBitLen; i++) {
|
|
44
|
+
keyBits[i] * (keyBits[i] - 1) === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// SHA-256 over the API key bytes. circomlib's Sha256 handles message
|
|
48
|
+
// padding internally; a 512-bit pre-image spans two 512-bit blocks.
|
|
49
|
+
component sha = Sha256(keyBitLen);
|
|
50
|
+
for (var i = 0; i < keyBitLen; i++) {
|
|
51
|
+
sha.in[i] <== keyBits[i];
|
|
52
|
+
}
|
|
53
|
+
for (var i = 0; i < 256; i++) {
|
|
54
|
+
keyHash[i] <== sha.out[i];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Bind the challenge nonce into the constraint system. The squaring
|
|
58
|
+
// is otherwise meaningless — its only purpose is to make `nonce` a
|
|
59
|
+
// constrained public signal, so the verifier knows the proof was
|
|
60
|
+
// generated for this specific challenge.
|
|
61
|
+
signal nonceBinding;
|
|
62
|
+
nonceBinding <== nonce * nonce;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
component main {public [nonce]} = SealIdentity(64);
|
package/dist/bits.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bit-level conversions between API key strings, circuit witness
|
|
3
|
+
* signals, and SHA-256 hashes. Pure and dependency-free.
|
|
4
|
+
*/
|
|
5
|
+
import type { Bit } from "./types.js";
|
|
6
|
+
/** The seal circuit's fixed pre-image length: a 64-byte ASCII API key. */
|
|
7
|
+
export declare const SEAL_KEY_BYTES = 64;
|
|
8
|
+
/** The seal circuit's pre-image bit length (512). */
|
|
9
|
+
export declare const SEAL_KEY_BITS: number;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a raw API key string into the `keyBits` witness signal — the
|
|
12
|
+
* UTF-8 bytes of the key, MSB-first per byte.
|
|
13
|
+
*
|
|
14
|
+
* Throws if the key is not exactly {@link SEAL_KEY_BYTES} ASCII bytes,
|
|
15
|
+
* since the circuit's pre-image length is fixed.
|
|
16
|
+
*/
|
|
17
|
+
export declare const apiKeyToBits: (apiKey: string) => ReadonlyArray<Bit>;
|
|
18
|
+
/**
|
|
19
|
+
* Reassemble the circuit's 256-bit `keyHash` public output into the
|
|
20
|
+
* lowercase hex `key_hash` string stored in the `api_keys` table.
|
|
21
|
+
*/
|
|
22
|
+
export declare const hashBitsToHex: (bits: ReadonlyArray<string | number>) => string;
|
|
23
|
+
//# sourceMappingURL=bits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bits.d.ts","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,qDAAqD;AACrD,eAAO,MAAM,aAAa,QAAqB,CAAC;AAShD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,KAAG,aAAa,CAAC,GAAG,CAS9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,KACnC,MAKO,CAAC"}
|
package/dist/bits.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bit-level conversions between API key strings, circuit witness
|
|
3
|
+
* signals, and SHA-256 hashes. Pure and dependency-free.
|
|
4
|
+
*/
|
|
5
|
+
/** The seal circuit's fixed pre-image length: a 64-byte ASCII API key. */
|
|
6
|
+
export const SEAL_KEY_BYTES = 64;
|
|
7
|
+
/** The seal circuit's pre-image bit length (512). */
|
|
8
|
+
export const SEAL_KEY_BITS = SEAL_KEY_BYTES * 8;
|
|
9
|
+
/**
|
|
10
|
+
* Decompose a byte into 8 bits, most-significant-bit first — the bit
|
|
11
|
+
* order circomlib's SHA-256 gadget expects.
|
|
12
|
+
*/
|
|
13
|
+
const byteToBits = (byte) => Array.from({ length: 8 }, (_, i) => ((byte >> (7 - i)) & 1));
|
|
14
|
+
/**
|
|
15
|
+
* Convert a raw API key string into the `keyBits` witness signal — the
|
|
16
|
+
* UTF-8 bytes of the key, MSB-first per byte.
|
|
17
|
+
*
|
|
18
|
+
* Throws if the key is not exactly {@link SEAL_KEY_BYTES} ASCII bytes,
|
|
19
|
+
* since the circuit's pre-image length is fixed.
|
|
20
|
+
*/
|
|
21
|
+
export const apiKeyToBits = (apiKey) => {
|
|
22
|
+
const bytes = new TextEncoder().encode(apiKey);
|
|
23
|
+
return bytes.length === SEAL_KEY_BYTES
|
|
24
|
+
? [...bytes].flatMap(byteToBits)
|
|
25
|
+
: (() => {
|
|
26
|
+
throw new Error(`seal: API key must be ${SEAL_KEY_BYTES} bytes, got ${bytes.length}`);
|
|
27
|
+
})();
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Reassemble the circuit's 256-bit `keyHash` public output into the
|
|
31
|
+
* lowercase hex `key_hash` string stored in the `api_keys` table.
|
|
32
|
+
*/
|
|
33
|
+
export const hashBitsToHex = (bits) => bits.length === 256
|
|
34
|
+
? BigInt(`0b${bits.map(String).join("")}`).toString(16).padStart(64, "0")
|
|
35
|
+
: (() => {
|
|
36
|
+
throw new Error(`seal: keyHash must be 256 bits, got ${bits.length}`);
|
|
37
|
+
})();
|
|
38
|
+
//# sourceMappingURL=bits.js.map
|
package/dist/bits.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bits.js","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,qDAAqD;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,GAAG,CAAC,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,IAAY,EAAsB,EAAE,CACtD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAQ,CAAC,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAc,EAAsB,EAAE;IACjE,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,MAAM,KAAK,cAAc;QACpC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QAChC,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,MAAM,IAAI,KAAK,CACb,yBAAyB,cAAc,eAAe,KAAK,CAAC,MAAM,EAAE,CACrE,CAAC;QACJ,CAAC,CAAC,EAAE,CAAC;AACX,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,IAAoC,EAC5B,EAAE,CACV,IAAI,CAAC,MAAM,KAAK,GAAG;IACjB,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACzE,CAAC,CAAC,CAAC,GAAG,EAAE;QACJ,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,EAAE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lemmaoracle/seal — ZK auth circuit for Lemma dashboard sign-in.
|
|
3
|
+
*
|
|
4
|
+
* `seal` lets a developer prove knowledge of their Lemma API key
|
|
5
|
+
* without revealing it. The dashboard BFF issues a challenge nonce, the
|
|
6
|
+
* developer generates a proof, and the BFF resolves the attested
|
|
7
|
+
* SHA-256 `key_hash` to a `scope_id`.
|
|
8
|
+
*
|
|
9
|
+
* The circuit (`circuits/src/seal-identity.circom`) is registered with
|
|
10
|
+
* the Lemma workers API as a normal circuit resource. This package is a
|
|
11
|
+
* reference definition: it is published for developers to generate
|
|
12
|
+
* proofs and is not imported by workers or the dashboard at runtime.
|
|
13
|
+
*/
|
|
14
|
+
export type { Bit, SealArtifacts, SealProof, SealProofInput, } from "./types.js";
|
|
15
|
+
export { SEAL_KEY_BYTES, SEAL_KEY_BITS, apiKeyToBits, hashBitsToHex, } from "./bits.js";
|
|
16
|
+
export { generateSealProof, verifySealProof } from "./proof.js";
|
|
17
|
+
/** The circuit id under which seal is registered with the Lemma API. */
|
|
18
|
+
export declare const SEAL_CIRCUIT_ID = "seal-identity-v1";
|
|
19
|
+
/** The schema the seal circuit is registered against. */
|
|
20
|
+
export declare const SEAL_SCHEMA = "passthrough-v1";
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EACV,GAAG,EACH,aAAa,EACb,SAAS,EACT,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,wEAAwE;AACxE,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,yDAAyD;AACzD,eAAO,MAAM,WAAW,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lemmaoracle/seal — ZK auth circuit for Lemma dashboard sign-in.
|
|
3
|
+
*
|
|
4
|
+
* `seal` lets a developer prove knowledge of their Lemma API key
|
|
5
|
+
* without revealing it. The dashboard BFF issues a challenge nonce, the
|
|
6
|
+
* developer generates a proof, and the BFF resolves the attested
|
|
7
|
+
* SHA-256 `key_hash` to a `scope_id`.
|
|
8
|
+
*
|
|
9
|
+
* The circuit (`circuits/src/seal-identity.circom`) is registered with
|
|
10
|
+
* the Lemma workers API as a normal circuit resource. This package is a
|
|
11
|
+
* reference definition: it is published for developers to generate
|
|
12
|
+
* proofs and is not imported by workers or the dashboard at runtime.
|
|
13
|
+
*/
|
|
14
|
+
export { SEAL_KEY_BYTES, SEAL_KEY_BITS, apiKeyToBits, hashBitsToHex, } from "./bits.js";
|
|
15
|
+
export { generateSealProof, verifySealProof } from "./proof.js";
|
|
16
|
+
/** The circuit id under which seal is registered with the Lemma API. */
|
|
17
|
+
export const SEAL_CIRCUIT_ID = "seal-identity-v1";
|
|
18
|
+
/** The schema the seal circuit is registered against. */
|
|
19
|
+
export const SEAL_SCHEMA = "passthrough-v1";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,EACL,cAAc,EACd,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAElD,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAC"}
|
package/dist/proof.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seal proof generation and verification.
|
|
3
|
+
*
|
|
4
|
+
* Dashboard sign-in flow:
|
|
5
|
+
* 1. The BFF issues a challenge `nonce`.
|
|
6
|
+
* 2. The developer runs {@link generateSealProof} with their API key.
|
|
7
|
+
* 3. The BFF runs {@link verifySealProof} and reads the attested
|
|
8
|
+
* `keyHash` to resolve the caller's scope.
|
|
9
|
+
*
|
|
10
|
+
* snarkjs is imported lazily so that consumers who only need the pure
|
|
11
|
+
* bit helpers ({@link apiKeyToBits} etc.) do not pay for it.
|
|
12
|
+
*/
|
|
13
|
+
import type { SealArtifacts, SealProof, SealProofInput } from "./types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Generate a groth16 proof that the caller knows the API key whose
|
|
16
|
+
* SHA-256 hash is `keyHash`, bound to the given challenge `nonce`.
|
|
17
|
+
*
|
|
18
|
+
* Requires the compiled circuit artifacts — build them with
|
|
19
|
+
* `npm run build` in `packages/seal/circuits` (see the package README).
|
|
20
|
+
*/
|
|
21
|
+
export declare const generateSealProof: (input: SealProofInput, artifacts: SealArtifacts) => Promise<SealProof>;
|
|
22
|
+
/**
|
|
23
|
+
* Verify a seal proof against the circuit verification key. Resolves to
|
|
24
|
+
* the attested `keyHash` and `nonce` on success, or `null` if invalid.
|
|
25
|
+
*/
|
|
26
|
+
export declare const verifySealProof: (proof: SealProof, verificationKey: Readonly<Record<string, unknown>>) => Promise<Readonly<{
|
|
27
|
+
keyHash: string;
|
|
28
|
+
nonce: string;
|
|
29
|
+
}> | null>;
|
|
30
|
+
//# sourceMappingURL=proof.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAK3E;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,cAAc,EACrB,WAAW,aAAa,KACvB,OAAO,CAAC,SAAS,CAgBnB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,SAAS,EAChB,iBAAiB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KACjD,OAAO,CAAC,QAAQ,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAa7D,CAAC"}
|
package/dist/proof.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seal proof generation and verification.
|
|
3
|
+
*
|
|
4
|
+
* Dashboard sign-in flow:
|
|
5
|
+
* 1. The BFF issues a challenge `nonce`.
|
|
6
|
+
* 2. The developer runs {@link generateSealProof} with their API key.
|
|
7
|
+
* 3. The BFF runs {@link verifySealProof} and reads the attested
|
|
8
|
+
* `keyHash` to resolve the caller's scope.
|
|
9
|
+
*
|
|
10
|
+
* snarkjs is imported lazily so that consumers who only need the pure
|
|
11
|
+
* bit helpers ({@link apiKeyToBits} etc.) do not pay for it.
|
|
12
|
+
*/
|
|
13
|
+
import { apiKeyToBits, hashBitsToHex } from "./bits.js";
|
|
14
|
+
/** Number of `keyHash` bits at the head of the public signal vector. */
|
|
15
|
+
const KEY_HASH_BITS = 256;
|
|
16
|
+
/**
|
|
17
|
+
* Generate a groth16 proof that the caller knows the API key whose
|
|
18
|
+
* SHA-256 hash is `keyHash`, bound to the given challenge `nonce`.
|
|
19
|
+
*
|
|
20
|
+
* Requires the compiled circuit artifacts — build them with
|
|
21
|
+
* `npm run build` in `packages/seal/circuits` (see the package README).
|
|
22
|
+
*/
|
|
23
|
+
export const generateSealProof = async (input, artifacts) => {
|
|
24
|
+
const { groth16 } = await import("snarkjs");
|
|
25
|
+
const witness = {
|
|
26
|
+
keyBits: apiKeyToBits(input.apiKey).map(Number),
|
|
27
|
+
nonce: input.nonce,
|
|
28
|
+
};
|
|
29
|
+
const { proof, publicSignals } = await groth16.fullProve(witness, artifacts.wasm, artifacts.zkey);
|
|
30
|
+
return {
|
|
31
|
+
proof,
|
|
32
|
+
publicSignals,
|
|
33
|
+
keyHash: hashBitsToHex(publicSignals.slice(0, KEY_HASH_BITS)),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Verify a seal proof against the circuit verification key. Resolves to
|
|
38
|
+
* the attested `keyHash` and `nonce` on success, or `null` if invalid.
|
|
39
|
+
*/
|
|
40
|
+
export const verifySealProof = async (proof, verificationKey) => {
|
|
41
|
+
const { groth16 } = await import("snarkjs");
|
|
42
|
+
const ok = await groth16.verify(verificationKey, [...proof.publicSignals], proof.proof);
|
|
43
|
+
return ok
|
|
44
|
+
? {
|
|
45
|
+
keyHash: hashBitsToHex(proof.publicSignals.slice(0, KEY_HASH_BITS)),
|
|
46
|
+
nonce: proof.publicSignals[KEY_HASH_BITS] ?? "",
|
|
47
|
+
}
|
|
48
|
+
: null;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=proof.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proof.js","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAGxD,wEAAwE;AACxE,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,KAAqB,EACrB,SAAwB,EACJ,EAAE;IACtB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAmB;QAC9B,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;QAC/C,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC;IACF,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,MAAM,OAAO,CAAC,SAAS,CACtD,OAAO,EACP,SAAS,CAAC,IAAI,EACd,SAAS,CAAC,IAAI,CACf,CAAC;IACF,OAAO;QACL,KAAK;QACL,aAAa;QACb,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;KAC9D,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,KAAgB,EAChB,eAAkD,EACY,EAAE;IAChE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,MAAM,CAC7B,eAAe,EACf,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,EACxB,KAAK,CAAC,KAAK,CACZ,CAAC;IACF,OAAO,EAAE;QACP,CAAC,CAAC;YACE,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;YACnE,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC,IAAI,EAAE;SAChD;QACH,CAAC,CAAC,IAAI,CAAC;AACX,CAAC,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lemmaoracle/seal — public types for generating dashboard sign-in proofs.
|
|
3
|
+
*/
|
|
4
|
+
import type { Groth16Proof } from "snarkjs";
|
|
5
|
+
/** A single bit (0 or 1). */
|
|
6
|
+
export type Bit = 0 | 1;
|
|
7
|
+
/** Inputs required to generate a seal sign-in proof. */
|
|
8
|
+
export type SealProofInput = Readonly<{
|
|
9
|
+
/** The raw API key string — a 64-character lowercase hex string. */
|
|
10
|
+
apiKey: string;
|
|
11
|
+
/** The dashboard-issued challenge nonce, as a decimal string. */
|
|
12
|
+
nonce: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** Filesystem paths (or URLs) of the compiled circuit artifacts. */
|
|
15
|
+
export type SealArtifacts = Readonly<{
|
|
16
|
+
/** Path/URL to `seal-identity.wasm`. */
|
|
17
|
+
wasm: string;
|
|
18
|
+
/** Path/URL to `seal-identity_final.zkey`. */
|
|
19
|
+
zkey: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** A groth16 proof together with its public signals. */
|
|
22
|
+
export type SealProof = Readonly<{
|
|
23
|
+
/** The groth16 proof object (snarkjs shape). */
|
|
24
|
+
proof: Groth16Proof;
|
|
25
|
+
/** Public signals: the 256 `keyHash` bits followed by `nonce`. */
|
|
26
|
+
publicSignals: ReadonlyArray<string>;
|
|
27
|
+
/** The SHA-256 `key_hash` (64-char hex) this proof attests to. */
|
|
28
|
+
keyHash: string;
|
|
29
|
+
}>;
|
|
30
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,6BAA6B;AAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;AAExB,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CAAC;AAEH,wDAAwD;AACxD,MAAM,MAAM,SAAS,GAAG,QAAQ,CAAC;IAC/B,gDAAgD;IAChD,KAAK,EAAE,YAAY,CAAC;IACpB,kEAAkE;IAClE,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACrC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/dist/vkey.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The groth16 verification key for seal-identity-v1.
|
|
3
|
+
*
|
|
4
|
+
* This is public information — it can only verify proofs, not generate them.
|
|
5
|
+
* Imported as a static JSON module — Vite / Astro will inline it at build
|
|
6
|
+
* time, so no filesystem access is needed at runtime (Cloudflare Workers).
|
|
7
|
+
*/
|
|
8
|
+
declare const _default: Readonly<Record<string, unknown>>;
|
|
9
|
+
export default _default;
|
|
10
|
+
//# sourceMappingURL=vkey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vkey.d.ts","sourceRoot":"","sources":["../src/vkey.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;wBAIoB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAAxD,wBAAyD"}
|
package/dist/vkey.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The groth16 verification key for seal-identity-v1.
|
|
3
|
+
*
|
|
4
|
+
* This is public information — it can only verify proofs, not generate them.
|
|
5
|
+
* Imported as a static JSON module — Vite / Astro will inline it at build
|
|
6
|
+
* time, so no filesystem access is needed at runtime (Cloudflare Workers).
|
|
7
|
+
*/
|
|
8
|
+
import vkey from "./vkeys/seal-identity-v1.json" with { type: "json" };
|
|
9
|
+
export default vkey;
|
|
10
|
+
//# sourceMappingURL=vkey.js.map
|
package/dist/vkey.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vkey.js","sourceRoot":"","sources":["../src/vkey.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,IAAI,MAAM,+BAA+B,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEvE,eAAe,IAAyC,CAAC"}
|