@lemmaoracle/seal 0.1.7 → 0.3.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 +87 -32
- package/circuits/src/seal-identity.circom +50 -12
- package/dist/bits.d.ts +8 -8
- package/dist/bits.d.ts.map +1 -1
- package/dist/bits.js +11 -11
- package/dist/bits.js.map +1 -1
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -12
- package/dist/index.js.map +1 -1
- package/dist/proof.d.ts +31 -18
- package/dist/proof.d.ts.map +1 -1
- package/dist/proof.js +47 -27
- package/dist/proof.js.map +1 -1
- package/dist/types.d.ts +10 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/vkey.d.ts +2 -0
- package/dist/vkey.d.ts.map +1 -1
- package/dist/vkey.js +2 -0
- package/dist/vkey.js.map +1 -1
- package/dist/vkeys/seal-identity-v1.json +11 -1286
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -3,23 +3,27 @@
|
|
|
3
3
|
ZK auth circuit for **Proof-based sign-in** to the Lemma developer dashboard.
|
|
4
4
|
|
|
5
5
|
`seal` lets a developer prove they hold a valid Lemma API key **without
|
|
6
|
-
revealing the key**. It
|
|
7
|
-
|
|
6
|
+
revealing the key or even which key it is**. It produces a per-session
|
|
7
|
+
Poseidon nullifier that is unique to the (secret, nonce) pair but reveals
|
|
8
|
+
nothing about the underlying secret or its SHA-256 hash.
|
|
8
9
|
|
|
9
10
|
## How it works
|
|
10
11
|
|
|
11
12
|
The dashboard's sign-in flow:
|
|
12
13
|
|
|
13
14
|
1. The dashboard BFF issues a challenge `nonce`.
|
|
14
|
-
2. The developer generates a `seal` proof: it proves knowledge of
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
The
|
|
22
|
-
|
|
15
|
+
2. The developer generates a `seal` proof: it proves knowledge of *a*
|
|
16
|
+
secret whose SHA-256 hash is registered in the workers `api_keys`
|
|
17
|
+
table, bound to that `nonce`. The public output is a **nullifier** —
|
|
18
|
+
`Poseidon(keyHash_hi, keyHash_lo, nonce)` — not the key hash itself.
|
|
19
|
+
3. The BFF verifies the proof, then iterates registered `key_hash`
|
|
20
|
+
values in D1, computing the expected nullifier for each until it
|
|
21
|
+
finds a match (O(N), sub-millisecond per check).
|
|
22
|
+
4. The BFF issues a session token tied to the resolved scope.
|
|
23
|
+
|
|
24
|
+
The proof reveals neither the secret nor its hash — only the nullifier
|
|
25
|
+
and the nonce. Because `nonce` changes every session, nullifiers cannot
|
|
26
|
+
be correlated across sign-ins, even for the same secret.
|
|
23
27
|
|
|
24
28
|
> Proof-based sign-in requires an existing API key. First-time users
|
|
25
29
|
> onboard via GitHub OAuth, which issues their first key.
|
|
@@ -29,24 +33,20 @@ the API stores) and the `nonce` — never the API key itself.
|
|
|
29
33
|
```
|
|
30
34
|
seal/
|
|
31
35
|
├── circuits/
|
|
32
|
-
│ ├── src/seal-identity.circom Circuit: SHA-256 pre-image proof
|
|
36
|
+
│ ├── src/seal-identity.circom Circuit: SHA-256 pre-image proof + Poseidon nullifier
|
|
33
37
|
│ ├── src/seal-identity.test.ts Circuit witness tests (needs a build)
|
|
34
38
|
│ └── scripts/build.sh Compile circom → wasm + groth16 setup
|
|
35
39
|
├── scripts/
|
|
36
40
|
│ ├── register-circuit.ts Pin artifacts to IPFS, register via SDK
|
|
37
41
|
│ └── setup-toolchain.sh Install Rust + circom
|
|
38
42
|
├── src/ TypeScript proof helpers (published)
|
|
39
|
-
│ ├── bits.ts
|
|
40
|
-
│ ├── proof.ts
|
|
43
|
+
│ ├── bits.ts Secret ↔ circuit signal conversions
|
|
44
|
+
│ ├── proof.ts prove / verify (delegates to @lemmaoracle/sdk)
|
|
45
|
+
│ ├── vkey.ts Bundled verification key + named export
|
|
41
46
|
│ └── index.ts Public API
|
|
42
47
|
└── .env.example Credentials for register-circuit.ts
|
|
43
48
|
```
|
|
44
49
|
|
|
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
50
|
`seal` is a **reference definition**. It is published to npm so
|
|
51
51
|
developers can generate proofs; it is *not* imported by `workers` or the
|
|
52
52
|
dashboard at runtime — the circuit reaches them through the normal
|
|
@@ -54,18 +54,21 @@ Lemma circuit-registration path.
|
|
|
54
54
|
|
|
55
55
|
## The circuit
|
|
56
56
|
|
|
57
|
-
`seal-identity.circom`
|
|
57
|
+
`seal-identity.circom` (v2) proves knowledge of a registered secret and
|
|
58
|
+
outputs a per-session nullifier:
|
|
58
59
|
|
|
59
|
-
- **Private input** — `keyBits[512]`: the 64-byte ASCII
|
|
60
|
+
- **Private input** — `keyBits[512]`: the 64-byte ASCII secret as bits.
|
|
60
61
|
Lemma keys are 32 random bytes rendered as 64 hex characters (see the
|
|
61
62
|
workers `generate_api_key.js`).
|
|
62
63
|
- **Public input** — `nonce`: the dashboard challenge, bound into the
|
|
63
64
|
constraint system for replay protection.
|
|
64
|
-
- **Public output** — `
|
|
65
|
+
- **Public output** — `nullifier`: `Poseidon(keyHash_hi, keyHash_lo, nonce)`.
|
|
66
|
+
A single BN254 field element; unique per (secret, nonce) pair. Add ~300
|
|
67
|
+
constraints on top of the SHA-256 (~60k total).
|
|
65
68
|
|
|
66
|
-
The hashing matches the workers `middleware/auth.ts`
|
|
67
|
-
(`SHA-256(utf8_bytes(
|
|
68
|
-
|
|
69
|
+
The SHA-256 hashing matches the workers `middleware/auth.ts`
|
|
70
|
+
(`SHA-256(utf8_bytes(secret))`), but `keyHash` is now an intermediate
|
|
71
|
+
signal — it never appears in public signals.
|
|
69
72
|
|
|
70
73
|
## Build the circuit
|
|
71
74
|
|
|
@@ -81,7 +84,8 @@ cd circuits && npm install && npm run build
|
|
|
81
84
|
```
|
|
82
85
|
|
|
83
86
|
`build.sh` downloads the 2^17 Hermez powers-of-tau file (~290 MB) on
|
|
84
|
-
first run
|
|
87
|
+
first run. The v2 circuit adds Poseidon (~300 constraints) to the SHA-256
|
|
88
|
+
base (~60k), staying well within the 2^17 budget.
|
|
85
89
|
|
|
86
90
|
## Register the circuit
|
|
87
91
|
|
|
@@ -99,15 +103,66 @@ dashboard BFF then fetches verification params at runtime via
|
|
|
99
103
|
## Generate a proof (developer usage)
|
|
100
104
|
|
|
101
105
|
```ts
|
|
102
|
-
import
|
|
106
|
+
import * as seal from "@lemmaoracle/seal";
|
|
107
|
+
|
|
108
|
+
const { proof, publicSignals, nullifier } = await seal.prove({
|
|
109
|
+
secret: process.env.LEMMA_API_KEY!,
|
|
110
|
+
nonce: challengeNonce,
|
|
111
|
+
});
|
|
112
|
+
// POST { proof, publicSignals, token } to the dashboard sign-in endpoint.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Circuit artifacts (wasm, zkey) are resolved automatically via the
|
|
116
|
+
`@lemmaoracle/sdk` from the registered circuit metadata — no local
|
|
117
|
+
artifact paths are required.
|
|
118
|
+
|
|
119
|
+
## Verify a proof
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import * as seal from "@lemmaoracle/seal";
|
|
103
123
|
|
|
104
|
-
const { proof, publicSignals,
|
|
105
|
-
|
|
106
|
-
{ wasm: "seal-identity.wasm", zkey: "seal-identity_final.zkey" },
|
|
107
|
-
);
|
|
108
|
-
// POST { proof, publicSignals } to the dashboard sign-in endpoint.
|
|
124
|
+
const result = await seal.verify({ proof, publicSignals, nullifier });
|
|
125
|
+
// result: { nullifier, nonce } | null
|
|
109
126
|
```
|
|
110
127
|
|
|
128
|
+
The verification key is bundled internally — no additional arguments are
|
|
129
|
+
needed. Verification delegates to `@lemmaoracle/sdk` verifier.
|
|
130
|
+
|
|
131
|
+
## Access the verification key
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { sealVkey } from "@lemmaoracle/seal";
|
|
135
|
+
// Or via the dedicated sub-export:
|
|
136
|
+
import vkey from "@lemmaoracle/seal/vkey";
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## v2 migration notes
|
|
140
|
+
|
|
141
|
+
v1 exposed `keyHash[256]` as a public output, making the key hash
|
|
142
|
+
readable by any observer of the proof transcript. v2 replaces this with
|
|
143
|
+
a Poseidon nullifier that is uncorrelatable across sessions.
|
|
144
|
+
|
|
145
|
+
Breaking changes:
|
|
146
|
+
- `SEAL_CIRCUIT_ID` is now `"seal-identity-v1"` (requires re-registration).
|
|
147
|
+
- `SealProof.keyHash` removed; replaced by `SealProof.nullifier`.
|
|
148
|
+
- Server-side: `scopeIdForKeyHash()` replaced by `scopeIdForNullifier()`
|
|
149
|
+
(full D1 scan + `poseidon-lite` computation).
|
|
150
|
+
- Circuit artifacts (wasm, zkey, vkey) must be regenerated.
|
|
151
|
+
|
|
152
|
+
v3 migration notes:
|
|
153
|
+
- `generateSealProof` renamed to `prove`; `verifySealProof` renamed to `verify`.
|
|
154
|
+
- `SealProofInput.apiKey` renamed to `SealProofInput.secret`.
|
|
155
|
+
- `apiKeyToBits` renamed to `secretToBits`; `SEAL_KEY_BYTES`/`SEAL_KEY_BITS`
|
|
156
|
+
renamed to `SEAL_SECRET_BYTES`/`SEAL_SECRET_BITS`.
|
|
157
|
+
- `SealArtifacts` type removed — circuit artifacts are resolved
|
|
158
|
+
automatically via `@lemmaoracle/sdk`.
|
|
159
|
+
- `prove` no longer requires a second `artifacts` argument.
|
|
160
|
+
- `verify` no longer requires a `verificationKey` argument — the vkey is
|
|
161
|
+
bundled internally.
|
|
162
|
+
- `sealVkey` named export added for direct vkey access.
|
|
163
|
+
- `snarkjs` is no longer a direct dependency — it is used via
|
|
164
|
+
`@lemmaoracle/sdk`.
|
|
165
|
+
|
|
111
166
|
## Scripts
|
|
112
167
|
|
|
113
168
|
| Command | Description |
|
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
pragma circom 2.1.0;
|
|
2
2
|
|
|
3
3
|
include "circomlib/circuits/sha256/sha256.circom";
|
|
4
|
+
include "circomlib/circuits/poseidon.circom";
|
|
5
|
+
include "circomlib/circuits/bitify.circom";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* SealIdentity — Proof-based sign-in for the Lemma developer dashboard.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* **v2 (nullifier)** — Replaces the v1 `keyHash` public output with a
|
|
11
|
+
* per-session Poseidon nullifier. The `keyHash` is now an intermediate
|
|
12
|
+
* signal that never appears in public signals, so neither the verifier
|
|
13
|
+
* nor any network observer can link proofs to a specific API key or
|
|
14
|
+
* correlate a user across sessions.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* Proves: "I hold the API key whose SHA-256 hash is registered in
|
|
17
|
+
* `api_keys`," without revealing which key.
|
|
18
|
+
*
|
|
19
|
+
* The nullifier is derived as:
|
|
20
|
+
* nullifier = Poseidon(keyHash_hi, keyHash_lo, nonce)
|
|
21
|
+
*
|
|
22
|
+
* where `keyHash_hi` and `keyHash_lo` are the upper and lower 128 bits
|
|
23
|
+
* of the SHA-256 digest, interpreted as BN254 field elements. Because
|
|
24
|
+
* `nonce` is unique per session, the nullifier is unique per session —
|
|
25
|
+
* even if the same API key generates two proofs in a row, the nullifiers
|
|
26
|
+
* are unrelated.
|
|
27
|
+
*
|
|
28
|
+
* The dashboard BFF verifies the proof, then iterates over registered
|
|
29
|
+
* `key_hash` values in D1, computing the expected nullifier for each
|
|
30
|
+
* until it finds a match (O(N), Poseidon is ~300 constraints but
|
|
31
|
+
* sub-millisecond in JS). This preserves the "key holder proves
|
|
32
|
+
* identity" invariant while removing `keyHash` from the wire entirely.
|
|
14
33
|
*
|
|
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
34
|
*
|
|
19
35
|
* Private input:
|
|
20
36
|
* keyBits[512] Bit decomposition of the 64-byte ASCII API key,
|
|
@@ -26,16 +42,16 @@ include "circomlib/circuits/sha256/sha256.circom";
|
|
|
26
42
|
* against a different challenge.
|
|
27
43
|
*
|
|
28
44
|
* Public output:
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
45
|
+
* nullifier Poseidon-keyed session nullifier. Unique per
|
|
46
|
+
* (key, nonce) pair, reveals nothing about the key
|
|
47
|
+
* or its hash.
|
|
32
48
|
*/
|
|
33
49
|
template SealIdentity(keyBytes) {
|
|
34
50
|
var keyBitLen = keyBytes * 8;
|
|
35
51
|
|
|
36
52
|
signal input keyBits[keyBitLen];
|
|
37
53
|
signal input nonce;
|
|
38
|
-
signal output
|
|
54
|
+
signal output nullifier;
|
|
39
55
|
|
|
40
56
|
// Constrain every pre-image bit to be boolean. Without this a
|
|
41
57
|
// malicious prover could feed non-binary field values into the
|
|
@@ -50,10 +66,32 @@ template SealIdentity(keyBytes) {
|
|
|
50
66
|
for (var i = 0; i < keyBitLen; i++) {
|
|
51
67
|
sha.in[i] <== keyBits[i];
|
|
52
68
|
}
|
|
69
|
+
|
|
70
|
+
// keyHash is an intermediate signal — never exposed publicly.
|
|
71
|
+
signal keyHash[256];
|
|
53
72
|
for (var i = 0; i < 256; i++) {
|
|
54
73
|
keyHash[i] <== sha.out[i];
|
|
55
74
|
}
|
|
56
75
|
|
|
76
|
+
// Split the 256-bit keyHash into two 128-bit field elements.
|
|
77
|
+
// Bits2Num expects LSB-first input; keyHash is MSB-first (SHA-256
|
|
78
|
+
// convention), so we reverse the bit order on connection.
|
|
79
|
+
component hiBits = Bits2Num(128);
|
|
80
|
+
component loBits = Bits2Num(128);
|
|
81
|
+
for (var i = 0; i < 128; i++) {
|
|
82
|
+
hiBits.in[i] <== keyHash[127 - i];
|
|
83
|
+
loBits.in[i] <== keyHash[255 - i];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Per-session nullifier: Poseidon(keyHash_hi, keyHash_lo, nonce).
|
|
87
|
+
// Adds ~300 constraints (vs ~30,000 for an extra SHA-256).
|
|
88
|
+
component hash = Poseidon(3);
|
|
89
|
+
hash.inputs[0] <== hiBits.out;
|
|
90
|
+
hash.inputs[1] <== loBits.out;
|
|
91
|
+
hash.inputs[2] <== nonce;
|
|
92
|
+
|
|
93
|
+
nullifier <== hash.out;
|
|
94
|
+
|
|
57
95
|
// Bind the challenge nonce into the constraint system. The squaring
|
|
58
96
|
// is otherwise meaningless — its only purpose is to make `nonce` a
|
|
59
97
|
// constrained public signal, so the verifier knows the proof was
|
package/dist/bits.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bit-level conversions between
|
|
2
|
+
* Bit-level conversions between secret strings, circuit witness
|
|
3
3
|
* signals, and SHA-256 hashes. Pure and dependency-free.
|
|
4
4
|
*/
|
|
5
5
|
import type { Bit } from "./types.js";
|
|
6
|
-
/** The seal circuit's fixed pre-image length: a 64-byte ASCII
|
|
7
|
-
export declare const
|
|
6
|
+
/** The seal circuit's fixed pre-image length: a 64-byte ASCII secret. */
|
|
7
|
+
export declare const SEAL_SECRET_BYTES = 64;
|
|
8
8
|
/** The seal circuit's pre-image bit length (512). */
|
|
9
|
-
export declare const
|
|
9
|
+
export declare const SEAL_SECRET_BITS: number;
|
|
10
10
|
/**
|
|
11
|
-
* Convert a raw
|
|
12
|
-
* UTF-8 bytes of the
|
|
11
|
+
* Convert a raw secret string into the `keyBits` witness signal — the
|
|
12
|
+
* UTF-8 bytes of the secret, MSB-first per byte.
|
|
13
13
|
*
|
|
14
|
-
* Throws if the
|
|
14
|
+
* Throws if the secret is not exactly {@link SEAL_SECRET_BYTES} ASCII bytes,
|
|
15
15
|
* since the circuit's pre-image length is fixed.
|
|
16
16
|
*/
|
|
17
|
-
export declare const
|
|
17
|
+
export declare const secretToBits: (secret: string) => ReadonlyArray<Bit>;
|
|
18
18
|
/**
|
|
19
19
|
* Reassemble the circuit's 256-bit `keyHash` public output into the
|
|
20
20
|
* lowercase hex `key_hash` string stored in the `api_keys` table.
|
package/dist/bits.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,QAAwB,CAAC;AAStD;;;;;;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
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bit-level conversions between
|
|
2
|
+
* Bit-level conversions between secret strings, circuit witness
|
|
3
3
|
* signals, and SHA-256 hashes. Pure and dependency-free.
|
|
4
4
|
*/
|
|
5
|
-
/** The seal circuit's fixed pre-image length: a 64-byte ASCII
|
|
6
|
-
export const
|
|
5
|
+
/** The seal circuit's fixed pre-image length: a 64-byte ASCII secret. */
|
|
6
|
+
export const SEAL_SECRET_BYTES = 64;
|
|
7
7
|
/** The seal circuit's pre-image bit length (512). */
|
|
8
|
-
export const
|
|
8
|
+
export const SEAL_SECRET_BITS = SEAL_SECRET_BYTES * 8;
|
|
9
9
|
/**
|
|
10
10
|
* Decompose a byte into 8 bits, most-significant-bit first — the bit
|
|
11
11
|
* order circomlib's SHA-256 gadget expects.
|
|
12
12
|
*/
|
|
13
13
|
const byteToBits = (byte) => Array.from({ length: 8 }, (_, i) => ((byte >> (7 - i)) & 1));
|
|
14
14
|
/**
|
|
15
|
-
* Convert a raw
|
|
16
|
-
* UTF-8 bytes of the
|
|
15
|
+
* Convert a raw secret string into the `keyBits` witness signal — the
|
|
16
|
+
* UTF-8 bytes of the secret, MSB-first per byte.
|
|
17
17
|
*
|
|
18
|
-
* Throws if the
|
|
18
|
+
* Throws if the secret is not exactly {@link SEAL_SECRET_BYTES} ASCII bytes,
|
|
19
19
|
* since the circuit's pre-image length is fixed.
|
|
20
20
|
*/
|
|
21
|
-
export const
|
|
22
|
-
const bytes = new TextEncoder().encode(
|
|
23
|
-
return bytes.length ===
|
|
21
|
+
export const secretToBits = (secret) => {
|
|
22
|
+
const bytes = new TextEncoder().encode(secret);
|
|
23
|
+
return bytes.length === SEAL_SECRET_BYTES
|
|
24
24
|
? [...bytes].flatMap(byteToBits)
|
|
25
25
|
: (() => {
|
|
26
|
-
throw new Error(`seal:
|
|
26
|
+
throw new Error(`seal: secret must be ${SEAL_SECRET_BYTES} bytes, got ${bytes.length}`);
|
|
27
27
|
})();
|
|
28
28
|
};
|
|
29
29
|
/**
|
package/dist/bits.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bits.js","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,
|
|
1
|
+
{"version":3,"file":"bits.js","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,qDAAqD;AACrD,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,CAAC,CAAC;AAEtD;;;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,iBAAiB;QACvC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QAChC,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,MAAM,IAAI,KAAK,CACb,wBAAwB,iBAAiB,eAAe,KAAK,CAAC,MAAM,EAAE,CACvE,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
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @lemmaoracle/seal — ZK auth circuit for Lemma dashboard sign-in.
|
|
3
3
|
*
|
|
4
|
-
* `seal` lets a developer prove
|
|
5
|
-
* without revealing it. The dashboard BFF issues
|
|
6
|
-
* developer generates a proof
|
|
7
|
-
*
|
|
4
|
+
* `seal` lets a developer prove they hold a valid Lemma API key
|
|
5
|
+
* without revealing it — or even *which* key. The dashboard BFF issues
|
|
6
|
+
* a challenge nonce; the developer generates a proof whose public
|
|
7
|
+
* output is a per-session Poseidon nullifier. The nullifier is unique
|
|
8
|
+
* to the (secret, nonce) pair but reveals nothing about the secret or
|
|
9
|
+
* its SHA-256 hash, so proofs cannot be correlated across sessions.
|
|
8
10
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The dashboard resolves identity by iterating registered `key_hash`
|
|
12
|
+
* values and computing the expected nullifier until a match is found.
|
|
13
|
+
*
|
|
14
|
+
* The circuit (`circuits/src/seal-identity.circom`, v2) is registered
|
|
15
|
+
* with the Lemma workers API as a normal circuit resource. This
|
|
16
|
+
* package is a reference definition: it is published for developers
|
|
17
|
+
* to generate proofs and is not imported by workers or the dashboard
|
|
18
|
+
* at runtime.
|
|
13
19
|
*/
|
|
14
|
-
export type { Bit,
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
17
|
-
|
|
18
|
-
export declare const SEAL_CIRCUIT_ID = "seal-identity-v1";
|
|
20
|
+
export type { Bit, SealProof, SealProofInput } from "./types.js";
|
|
21
|
+
export { SEAL_SECRET_BYTES, SEAL_SECRET_BITS, secretToBits, hashBitsToHex, } from "./bits.js";
|
|
22
|
+
export { prove, verify, SEAL_CIRCUIT_ID } from "./proof.js";
|
|
23
|
+
export { sealVkey } from "./vkey.js";
|
|
19
24
|
/** The schema the seal circuit is registered against. */
|
|
20
25
|
export declare const SEAL_SCHEMA = "passthrough-v1";
|
|
21
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,yDAAyD;AACzD,eAAO,MAAM,WAAW,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @lemmaoracle/seal — ZK auth circuit for Lemma dashboard sign-in.
|
|
3
3
|
*
|
|
4
|
-
* `seal` lets a developer prove
|
|
5
|
-
* without revealing it. The dashboard BFF issues
|
|
6
|
-
* developer generates a proof
|
|
7
|
-
*
|
|
4
|
+
* `seal` lets a developer prove they hold a valid Lemma API key
|
|
5
|
+
* without revealing it — or even *which* key. The dashboard BFF issues
|
|
6
|
+
* a challenge nonce; the developer generates a proof whose public
|
|
7
|
+
* output is a per-session Poseidon nullifier. The nullifier is unique
|
|
8
|
+
* to the (secret, nonce) pair but reveals nothing about the secret or
|
|
9
|
+
* its SHA-256 hash, so proofs cannot be correlated across sessions.
|
|
8
10
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The dashboard resolves identity by iterating registered `key_hash`
|
|
12
|
+
* values and computing the expected nullifier until a match is found.
|
|
13
|
+
*
|
|
14
|
+
* The circuit (`circuits/src/seal-identity.circom`, v2) is registered
|
|
15
|
+
* with the Lemma workers API as a normal circuit resource. This
|
|
16
|
+
* package is a reference definition: it is published for developers
|
|
17
|
+
* to generate proofs and is not imported by workers or the dashboard
|
|
18
|
+
* at runtime.
|
|
13
19
|
*/
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
|
|
17
|
-
export const SEAL_CIRCUIT_ID = "seal-identity-v1";
|
|
20
|
+
export { SEAL_SECRET_BYTES, SEAL_SECRET_BITS, secretToBits, hashBitsToHex, } from "./bits.js";
|
|
21
|
+
export { prove, verify, SEAL_CIRCUIT_ID } from "./proof.js";
|
|
22
|
+
export { sealVkey } from "./vkey.js";
|
|
18
23
|
/** The schema the seal circuit is registered against. */
|
|
19
24
|
export const SEAL_SCHEMA = "passthrough-v1";
|
|
20
25
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAC"}
|
package/dist/proof.d.ts
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seal proof generation and verification.
|
|
2
|
+
* Seal proof generation and verification (v2: nullifier-based).
|
|
3
3
|
*
|
|
4
4
|
* Dashboard sign-in flow:
|
|
5
5
|
* 1. The BFF issues a challenge `nonce`.
|
|
6
|
-
* 2. The developer runs {@link
|
|
7
|
-
* 3. The BFF
|
|
8
|
-
*
|
|
6
|
+
* 2. The developer runs {@link prove} with their secret.
|
|
7
|
+
* 3. The BFF verifies the proof, reads the `nullifier`, and resolves
|
|
8
|
+
* the caller's scope by matching it against registered key hashes.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Both prove and verify delegate to `@lemmaoracle/sdk`, which handles
|
|
11
|
+
* circuit artifact resolution and snarkjs orchestration. The vkey for
|
|
12
|
+
* verification is resolved internally from the bundled JSON.
|
|
12
13
|
*/
|
|
13
|
-
import type {
|
|
14
|
+
import type { SealProof, SealProofInput } from "./types.js";
|
|
15
|
+
/** The circuit id under which seal v2 is registered with the Lemma API. */
|
|
16
|
+
export declare const SEAL_CIRCUIT_ID = "seal-identity-v1";
|
|
17
|
+
type VerifyResult = Readonly<{
|
|
18
|
+
nullifier: string;
|
|
19
|
+
nonce: string;
|
|
20
|
+
}>;
|
|
14
21
|
/**
|
|
15
|
-
* Generate a groth16 proof that the caller
|
|
16
|
-
*
|
|
22
|
+
* Generate a groth16 proof that the caller holds the secret behind a
|
|
23
|
+
* registered `key_hash`, bound to the given challenge `nonce`.
|
|
24
|
+
*
|
|
25
|
+
* The returned `nullifier` is a per-session Poseidon fingerprint that
|
|
26
|
+
* reveals neither the secret nor its SHA-256 hash. The server resolves
|
|
27
|
+
* identity by iterating registered key hashes and computing the
|
|
28
|
+
* expected nullifier until a match is found.
|
|
17
29
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
30
|
+
* Circuit artifacts (wasm, zkey) are resolved automatically via the
|
|
31
|
+
* Lemma SDK from `circuitId: "seal-identity-v1"`. No local artifact
|
|
32
|
+
* paths are required.
|
|
20
33
|
*/
|
|
21
|
-
export declare const
|
|
34
|
+
export declare const prove: (input: SealProofInput) => Promise<SealProof>;
|
|
22
35
|
/**
|
|
23
|
-
* Verify a seal proof against the circuit verification key.
|
|
24
|
-
*
|
|
36
|
+
* Verify a seal proof against the bundled circuit verification key.
|
|
37
|
+
*
|
|
38
|
+
* Resolves to the attested `nullifier` and `nonce` on success, or
|
|
39
|
+
* `null` if invalid. Delegates to `@lemmaoracle/sdk` verifier.
|
|
25
40
|
*/
|
|
26
|
-
export declare const
|
|
27
|
-
|
|
28
|
-
nonce: string;
|
|
29
|
-
}> | null>;
|
|
41
|
+
export declare const verify: (proof: SealProof) => Promise<VerifyResult | null>;
|
|
42
|
+
export {};
|
|
30
43
|
//# sourceMappingURL=proof.d.ts.map
|
package/dist/proof.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,2EAA2E;AAC3E,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,KAAK,YAAY,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,GAAU,OAAO,cAAc,KAAG,OAAO,CAAC,SAAS,CAepE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GACjB,OAAO,SAAS,KACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAe7B,CAAC"}
|
package/dist/proof.js
CHANGED
|
@@ -1,49 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seal proof generation and verification.
|
|
2
|
+
* Seal proof generation and verification (v2: nullifier-based).
|
|
3
3
|
*
|
|
4
4
|
* Dashboard sign-in flow:
|
|
5
5
|
* 1. The BFF issues a challenge `nonce`.
|
|
6
|
-
* 2. The developer runs {@link
|
|
7
|
-
* 3. The BFF
|
|
8
|
-
*
|
|
6
|
+
* 2. The developer runs {@link prove} with their secret.
|
|
7
|
+
* 3. The BFF verifies the proof, reads the `nullifier`, and resolves
|
|
8
|
+
* the caller's scope by matching it against registered key hashes.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Both prove and verify delegate to `@lemmaoracle/sdk`, which handles
|
|
11
|
+
* circuit artifact resolution and snarkjs orchestration. The vkey for
|
|
12
|
+
* verification is resolved internally from the bundled JSON.
|
|
12
13
|
*/
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import { create, prover, verifier } from "@lemmaoracle/sdk";
|
|
15
|
+
import { secretToBits } from "./bits.js";
|
|
16
|
+
import vkey from "./vkey.js";
|
|
17
|
+
/** The circuit id under which seal v2 is registered with the Lemma API. */
|
|
18
|
+
export const SEAL_CIRCUIT_ID = "seal-identity-v1";
|
|
16
19
|
/**
|
|
17
|
-
* Generate a groth16 proof that the caller
|
|
18
|
-
*
|
|
20
|
+
* Generate a groth16 proof that the caller holds the secret behind a
|
|
21
|
+
* registered `key_hash`, bound to the given challenge `nonce`.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* The returned `nullifier` is a per-session Poseidon fingerprint that
|
|
24
|
+
* reveals neither the secret nor its SHA-256 hash. The server resolves
|
|
25
|
+
* identity by iterating registered key hashes and computing the
|
|
26
|
+
* expected nullifier until a match is found.
|
|
27
|
+
*
|
|
28
|
+
* Circuit artifacts (wasm, zkey) are resolved automatically via the
|
|
29
|
+
* Lemma SDK from `circuitId: "seal-identity-v1"`. No local artifact
|
|
30
|
+
* paths are required.
|
|
22
31
|
*/
|
|
23
|
-
export const
|
|
24
|
-
const
|
|
32
|
+
export const prove = async (input) => {
|
|
33
|
+
const client = create({});
|
|
25
34
|
const witness = {
|
|
26
|
-
keyBits:
|
|
35
|
+
keyBits: secretToBits(input.secret).map(Number),
|
|
27
36
|
nonce: input.nonce,
|
|
28
37
|
};
|
|
29
|
-
const { proof,
|
|
38
|
+
const { proof, inputs } = await prover.prove(client, {
|
|
39
|
+
circuitId: SEAL_CIRCUIT_ID,
|
|
40
|
+
witness,
|
|
41
|
+
});
|
|
30
42
|
return {
|
|
31
|
-
proof,
|
|
32
|
-
publicSignals,
|
|
33
|
-
|
|
43
|
+
proof: JSON.parse(atob(proof)),
|
|
44
|
+
publicSignals: inputs,
|
|
45
|
+
nullifier: inputs[0] ?? "",
|
|
34
46
|
};
|
|
35
47
|
};
|
|
36
48
|
/**
|
|
37
|
-
* Verify a seal proof against the circuit verification key.
|
|
38
|
-
*
|
|
49
|
+
* Verify a seal proof against the bundled circuit verification key.
|
|
50
|
+
*
|
|
51
|
+
* Resolves to the attested `nullifier` and `nonce` on success, or
|
|
52
|
+
* `null` if invalid. Delegates to `@lemmaoracle/sdk` verifier.
|
|
39
53
|
*/
|
|
40
|
-
export const
|
|
41
|
-
const {
|
|
42
|
-
|
|
54
|
+
export const verify = async (proof) => {
|
|
55
|
+
const { ok } = await verifier.verify({
|
|
56
|
+
alg: "groth16-bn254-snarkjs",
|
|
57
|
+
inputs: {
|
|
58
|
+
vkey,
|
|
59
|
+
proof: proof.proof,
|
|
60
|
+
publicSignals: proof.publicSignals,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
43
63
|
return ok
|
|
44
64
|
? {
|
|
45
|
-
|
|
46
|
-
nonce: proof.publicSignals[
|
|
65
|
+
nullifier: proof.publicSignals[0] ?? "",
|
|
66
|
+
nonce: proof.publicSignals[1] ?? "",
|
|
47
67
|
}
|
|
48
68
|
: null;
|
|
49
69
|
};
|