zklicensing 0.1.0-beta.2 → 0.1.0-beta.21
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 +417 -201
- package/build/agent.d.ts +371 -0
- package/build/agent.d.ts.map +1 -0
- package/build/agent.js +553 -0
- package/build/agent.js.map +1 -0
- package/build/agentActions.d.ts +841 -0
- package/build/agentActions.d.ts.map +1 -0
- package/build/agentActions.js +581 -0
- package/build/agentActions.js.map +1 -0
- package/build/agentRecord.d.ts +83 -0
- package/build/agentRecord.d.ts.map +1 -0
- package/build/agentRecord.js +69 -0
- package/build/agentRecord.js.map +1 -0
- package/build/agentVendor.d.ts +220 -0
- package/build/agentVendor.d.ts.map +1 -0
- package/build/agentVendor.js +306 -0
- package/build/agentVendor.js.map +1 -0
- package/build/appsLookup.d.ts +14 -0
- package/build/appsLookup.d.ts.map +1 -0
- package/build/appsLookup.js +12 -0
- package/build/appsLookup.js.map +1 -0
- package/build/archiveBootstrap.d.ts +31 -5
- package/build/archiveBootstrap.d.ts.map +1 -0
- package/build/archiveBootstrap.js +140 -47
- package/build/archiveBootstrap.js.map +1 -0
- package/build/atomicJson.d.ts +5 -0
- package/build/atomicJson.d.ts.map +1 -0
- package/build/atomicJson.js +55 -0
- package/build/atomicJson.js.map +1 -0
- package/build/bindClient.d.ts +85 -0
- package/build/bindClient.d.ts.map +1 -0
- package/build/bindClient.js +187 -0
- package/build/bindClient.js.map +1 -0
- package/build/buyClient.d.ts +27 -4
- package/build/buyClient.d.ts.map +1 -0
- package/build/buyClient.js +72 -26
- package/build/buyClient.js.map +1 -0
- package/build/chainHead.d.ts +61 -0
- package/build/chainHead.d.ts.map +1 -0
- package/build/chainHead.js +88 -0
- package/build/chainHead.js.map +1 -0
- package/build/compactMerkleMap.d.ts +72 -0
- package/build/compactMerkleMap.d.ts.map +1 -0
- package/build/compactMerkleMap.js +224 -0
- package/build/compactMerkleMap.js.map +1 -0
- package/build/contractInterface.d.ts +191 -51
- package/build/contractInterface.d.ts.map +1 -0
- package/build/contractInterface.js +234 -43
- package/build/contractInterface.js.map +1 -0
- package/build/contractTypes.d.ts +9 -0
- package/build/contractTypes.d.ts.map +1 -0
- package/build/contractTypes.js +28 -0
- package/build/contractTypes.js.map +1 -0
- package/build/countryStats.d.ts +12 -3
- package/build/countryStats.d.ts.map +1 -0
- package/build/countryStats.js +87 -23
- package/build/countryStats.js.map +1 -0
- package/build/deployClient.d.ts +138 -24
- package/build/deployClient.d.ts.map +1 -0
- package/build/deployClient.js +91 -31
- package/build/deployClient.js.map +1 -0
- package/build/deviceIdentity.d.ts +68 -0
- package/build/deviceIdentity.d.ts.map +1 -0
- package/build/deviceIdentity.js +209 -0
- package/build/deviceIdentity.js.map +1 -0
- package/build/freezeClient.d.ts +26 -6
- package/build/freezeClient.d.ts.map +1 -0
- package/build/freezeClient.js +30 -6
- package/build/freezeClient.js.map +1 -0
- package/build/generationClient.d.ts +6 -3
- package/build/generationClient.d.ts.map +1 -0
- package/build/generationClient.js +25 -38
- package/build/generationClient.js.map +1 -0
- package/build/holderSecret.d.ts +70 -0
- package/build/holderSecret.d.ts.map +1 -0
- package/build/holderSecret.js +76 -0
- package/build/holderSecret.js.map +1 -0
- package/build/index.d.ts +38 -38
- package/build/index.d.ts.map +1 -0
- package/build/index.js +97 -121
- package/build/index.js.map +1 -0
- package/build/keysClient.d.ts +142 -0
- package/build/keysClient.d.ts.map +1 -0
- package/build/keysClient.js +109 -0
- package/build/keysClient.js.map +1 -0
- package/build/licenseLeaf.d.ts +76 -0
- package/build/licenseLeaf.d.ts.map +1 -0
- package/build/licenseLeaf.js +189 -0
- package/build/licenseLeaf.js.map +1 -0
- package/build/licenseRecord.d.ts +68 -5
- package/build/licenseRecord.d.ts.map +1 -0
- package/build/licenseRecord.js +26 -1
- package/build/licenseRecord.js.map +1 -0
- package/build/licenseStore.d.ts +81 -4
- package/build/licenseStore.d.ts.map +1 -0
- package/build/licenseStore.js +400 -30
- package/build/licenseStore.js.map +1 -0
- package/build/licenseTrustless.d.ts +43 -0
- package/build/licenseTrustless.d.ts.map +1 -0
- package/build/licenseTrustless.js +180 -0
- package/build/licenseTrustless.js.map +1 -0
- package/build/manifestVerify.d.ts +29 -1
- package/build/manifestVerify.d.ts.map +1 -0
- package/build/manifestVerify.js +46 -4
- package/build/manifestVerify.js.map +1 -0
- package/build/minaPriceHistory.d.ts +9 -0
- package/build/minaPriceHistory.d.ts.map +1 -0
- package/build/minaPriceHistory.js +152 -0
- package/build/minaPriceHistory.js.map +1 -0
- package/build/onChainRoot.d.ts +60 -0
- package/build/onChainRoot.d.ts.map +1 -0
- package/build/onChainRoot.js +74 -0
- package/build/onChainRoot.js.map +1 -0
- package/build/ownershipLicenseHash.d.ts +25 -0
- package/build/ownershipLicenseHash.d.ts.map +1 -0
- package/build/ownershipLicenseHash.js +29 -0
- package/build/ownershipLicenseHash.js.map +1 -0
- package/build/ownershipSignature.d.ts +153 -0
- package/build/ownershipSignature.d.ts.map +1 -0
- package/build/ownershipSignature.js +297 -0
- package/build/ownershipSignature.js.map +1 -0
- package/build/permitCheckFlow.d.ts +155 -0
- package/build/permitCheckFlow.d.ts.map +1 -0
- package/build/permitCheckFlow.js +196 -0
- package/build/permitCheckFlow.js.map +1 -0
- package/build/permitDescribe.d.ts +124 -0
- package/build/permitDescribe.d.ts.map +1 -0
- package/build/permitDescribe.js +324 -0
- package/build/permitDescribe.js.map +1 -0
- package/build/permitInterface.d.ts +85 -0
- package/build/permitInterface.d.ts.map +1 -0
- package/build/permitInterface.js +121 -0
- package/build/permitInterface.js.map +1 -0
- package/build/permitMessages.d.ts +313 -0
- package/build/permitMessages.d.ts.map +1 -0
- package/build/permitMessages.js +462 -0
- package/build/permitMessages.js.map +1 -0
- package/build/permitProof.d.ts +108 -0
- package/build/permitProof.d.ts.map +1 -0
- package/build/permitProof.js +202 -0
- package/build/permitProof.js.map +1 -0
- package/build/permitRegistryRebuild.d.ts +166 -0
- package/build/permitRegistryRebuild.d.ts.map +1 -0
- package/build/permitRegistryRebuild.js +347 -0
- package/build/permitRegistryRebuild.js.map +1 -0
- package/build/permitTree.d.ts +88 -0
- package/build/permitTree.d.ts.map +1 -0
- package/build/permitTree.js +230 -0
- package/build/permitTree.js.map +1 -0
- package/build/permitsClient.d.ts +680 -0
- package/build/permitsClient.d.ts.map +1 -0
- package/build/permitsClient.js +597 -0
- package/build/permitsClient.js.map +1 -0
- package/build/proveErrors.d.ts +50 -0
- package/build/proveErrors.d.ts.map +1 -0
- package/build/proveErrors.js +76 -0
- package/build/proveErrors.js.map +1 -0
- package/build/receipt.d.ts +95 -0
- package/build/receipt.d.ts.map +1 -0
- package/build/receipt.js +84 -0
- package/build/receipt.js.map +1 -0
- package/build/refundClient.d.ts +7 -12
- package/build/refundClient.d.ts.map +1 -0
- package/build/refundClient.js +52 -21
- package/build/refundClient.js.map +1 -0
- package/build/registryTrustVerify.d.ts +78 -0
- package/build/registryTrustVerify.d.ts.map +1 -0
- package/build/registryTrustVerify.js +129 -0
- package/build/registryTrustVerify.js.map +1 -0
- package/build/releaseClient.d.ts +26 -0
- package/build/releaseClient.d.ts.map +1 -0
- package/build/releaseClient.js +65 -0
- package/build/releaseClient.js.map +1 -0
- package/build/renewClient.d.ts +28 -2
- package/build/renewClient.d.ts.map +1 -0
- package/build/renewClient.js +31 -27
- package/build/renewClient.js.map +1 -0
- package/build/seatName.d.ts +8 -0
- package/build/seatName.d.ts.map +1 -0
- package/build/seatName.js +136 -0
- package/build/seatName.js.map +1 -0
- package/build/slotConstants.d.ts +15 -0
- package/build/slotConstants.d.ts.map +1 -0
- package/build/slotConstants.js +17 -0
- package/build/slotConstants.js.map +1 -0
- package/build/statsClient.d.ts +61 -7
- package/build/statsClient.d.ts.map +1 -0
- package/build/statsClient.js +44 -5
- package/build/statsClient.js.map +1 -0
- package/build/successorChain.d.ts +35 -0
- package/build/successorChain.d.ts.map +1 -0
- package/build/successorChain.js +69 -0
- package/build/successorChain.js.map +1 -0
- package/build/taxId.d.ts +44 -0
- package/build/taxId.d.ts.map +1 -0
- package/build/taxId.js +212 -0
- package/build/taxId.js.map +1 -0
- package/build/trustAnchorVerify.d.ts +49 -0
- package/build/trustAnchorVerify.d.ts.map +1 -0
- package/build/trustAnchorVerify.js +432 -0
- package/build/trustAnchorVerify.js.map +1 -0
- package/build/trustAnchorVerifyCli.d.ts +45 -0
- package/build/trustAnchorVerifyCli.d.ts.map +1 -0
- package/build/trustAnchorVerifyCli.js +235 -0
- package/build/trustAnchorVerifyCli.js.map +1 -0
- package/build/txBroadcastClient.d.ts +26 -0
- package/build/txBroadcastClient.d.ts.map +1 -0
- package/build/txBroadcastClient.js +20 -0
- package/build/txBroadcastClient.js.map +1 -0
- package/build/unbindClient.d.ts +77 -0
- package/build/unbindClient.d.ts.map +1 -0
- package/build/unbindClient.js +169 -0
- package/build/unbindClient.js.map +1 -0
- package/build/units.d.ts +28 -0
- package/build/units.d.ts.map +1 -0
- package/build/units.js +65 -0
- package/build/units.js.map +1 -0
- package/build/vatRates.d.ts +54 -0
- package/build/vatRates.d.ts.map +1 -0
- package/build/vatRates.js +82 -0
- package/build/vatRates.js.map +1 -0
- package/build/vatRatesData.d.ts +5 -0
- package/build/vatRatesData.d.ts.map +1 -0
- package/build/vatRatesData.js +42 -0
- package/build/vatRatesData.js.map +1 -0
- package/build/vendorAccountClient.d.ts +66 -0
- package/build/vendorAccountClient.d.ts.map +1 -0
- package/build/vendorAccountClient.js +47 -0
- package/build/vendorAccountClient.js.map +1 -0
- package/build/verifyCore.d.ts +4 -1
- package/build/verifyCore.d.ts.map +1 -0
- package/build/verifyCore.js +21 -10
- package/build/verifyCore.js.map +1 -0
- package/build/verifyOffline.d.ts +85 -0
- package/build/verifyOffline.d.ts.map +1 -0
- package/build/verifyOffline.js +139 -0
- package/build/verifyOffline.js.map +1 -0
- package/build/verifyService.d.ts.map +1 -0
- package/build/verifyService.js +571 -501
- package/build/verifyService.js.map +1 -0
- package/build/verifyWithAncestors.d.ts +19 -0
- package/build/verifyWithAncestors.d.ts.map +1 -0
- package/build/verifyWithAncestors.js +127 -0
- package/build/verifyWithAncestors.js.map +1 -0
- package/build/zipToState.d.ts +2 -0
- package/build/zipToState.d.ts.map +1 -0
- package/build/zipToState.js +94 -0
- package/build/zipToState.js.map +1 -0
- package/package.json +141 -16
- package/src/agent.ts +682 -0
- package/src/agentActions.ts +637 -0
- package/src/agentRecord.ts +121 -0
- package/src/agentVendor.ts +356 -0
- package/src/appsLookup.ts +48 -0
- package/src/archiveBootstrap.ts +386 -0
- package/src/atomicJson.ts +53 -0
- package/src/bindClient.ts +297 -0
- package/src/buyClient.ts +344 -0
- package/src/chainHead.ts +106 -0
- package/src/compactMerkleMap.ts +222 -0
- package/src/contractInterface.ts +465 -0
- package/src/contractTypes.ts +31 -0
- package/src/countryStats.ts +200 -0
- package/src/deployClient.ts +517 -0
- package/src/deviceIdentity.ts +249 -0
- package/src/freezeClient.ts +162 -0
- package/src/generationClient.ts +145 -0
- package/src/holderSecret.ts +91 -0
- package/src/index.ts +362 -0
- package/src/keysClient.ts +135 -0
- package/src/licenseLeaf.ts +221 -0
- package/src/licenseRecord.ts +190 -0
- package/src/licenseStore.ts +767 -0
- package/src/licenseTrustless.ts +207 -0
- package/src/manifestVerify.ts +164 -0
- package/src/minaPriceHistory.ts +166 -0
- package/src/onChainRoot.ts +90 -0
- package/src/ownershipLicenseHash.ts +31 -0
- package/src/ownershipSignature.ts +421 -0
- package/src/permitCheckFlow.ts +227 -0
- package/src/permitDescribe.ts +362 -0
- package/src/permitInterface.ts +146 -0
- package/src/permitMessages.ts +572 -0
- package/src/permitProof.ts +230 -0
- package/src/permitRegistryRebuild.ts +423 -0
- package/src/permitTree.ts +232 -0
- package/src/permitsClient.ts +746 -0
- package/src/proveErrors.ts +98 -0
- package/src/receipt.ts +156 -0
- package/src/refundClient.ts +177 -0
- package/src/registryTrustVerify.ts +192 -0
- package/src/releaseClient.ts +101 -0
- package/src/renewClient.ts +234 -0
- package/src/seatName.ts +165 -0
- package/src/slotConstants.ts +18 -0
- package/src/statsClient.ts +249 -0
- package/src/successorChain.ts +87 -0
- package/src/taxId.ts +227 -0
- package/src/trustAnchorVerify.ts +550 -0
- package/src/trustAnchorVerifyCli.ts +234 -0
- package/src/txBroadcastClient.ts +48 -0
- package/src/unbindClient.ts +233 -0
- package/src/units.ts +74 -0
- package/src/vatRates.ts +100 -0
- package/src/vatRatesData.ts +43 -0
- package/src/vendorAccountClient.ts +120 -0
- package/src/verifyCore.ts +100 -0
- package/src/verifyOffline.ts +212 -0
- package/src/verifyService.ts +982 -0
- package/src/verifyWithAncestors.ts +163 -0
- package/src/zipToState.ts +94 -0
- package/build/licenseProof.d.ts +0 -26
- package/build/licenseProof.js +0 -41
- package/build/migrateClient.d.ts +0 -43
- package/build/migrateClient.js +0 -47
- package/build/ownership.d.ts +0 -34
- package/build/ownership.js +0 -147
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ownershipSignature.ts
|
|
6
|
+
*
|
|
7
|
+
* Schnorr-over-Pallas signing primitive for the two on-chain flows that
|
|
8
|
+
* need the passphrase-derived owner key:
|
|
9
|
+
*
|
|
10
|
+
* 1. The /prove/bind and /prove/unbind flows — the passphrase-derived
|
|
11
|
+
* keypair authorizes device slot mutations, and the signature is
|
|
12
|
+
* re-verified INSIDE the circuit by `Signature.verify()` so even a
|
|
13
|
+
* compromised licensing service cannot forge a bind.
|
|
14
|
+
* 2. The retained `ActivationMessage` shape (see below) — kept so external
|
|
15
|
+
* tooling can still sign an "I hold the passphrase for this license"
|
|
16
|
+
* assertion off-band. The platform no longer runs a challenge/respond
|
|
17
|
+
* endpoint; the shape is purely an SDK primitive now.
|
|
18
|
+
*
|
|
19
|
+
* Deterministic HKDF-SHA256 derivation of a seed from the passphrase,
|
|
20
|
+
* reduced to a Pallas scalar → o1js `PrivateKey`. The buyer can
|
|
21
|
+
* re-derive the same keypair anywhere from just the passphrase.
|
|
22
|
+
*
|
|
23
|
+
* o1js-dependent by design: the whole point of switching from Ed25519 to
|
|
24
|
+
* Pallas Schnorr is native in-circuit verification. Any client that
|
|
25
|
+
* signs an activation or binding challenge already ships o1js for the
|
|
26
|
+
* downstream buy/verify surface, so the "keep this module o1js-free"
|
|
27
|
+
* constraint that motivated the Ed25519 version no longer applies.
|
|
28
|
+
*
|
|
29
|
+
* HKDF still needs WebCrypto (Node ≥ 20, Chromium 137+, Safari 17+,
|
|
30
|
+
* Firefox 130+). The module throws at derive time if
|
|
31
|
+
* `globalThis.crypto.subtle` is unavailable — callers cannot silently
|
|
32
|
+
* fall back to a weaker path.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Field, PrivateKey, PublicKey, Signature, UInt32 } from 'o1js';
|
|
36
|
+
import { packSlotTimes } from './licenseLeaf.js';
|
|
37
|
+
import { licenseHashFromPubkey } from './ownershipLicenseHash.js';
|
|
38
|
+
|
|
39
|
+
// HKDF info binds the derivation to a specific zkApp address, so the
|
|
40
|
+
// same passphrase yields different keypairs for different apps. This
|
|
41
|
+
// makes credentials per-app: a leaked passphrase for app A cannot be
|
|
42
|
+
// replayed as an activation for app B. RFC 5869 designates `info` as
|
|
43
|
+
// the domain-separation channel (salt is for randomness, which we don't
|
|
44
|
+
// have here — the passphrase IS the secret input).
|
|
45
|
+
//
|
|
46
|
+
// The `v1` prefix reserves room to rotate the KDF or the target key type
|
|
47
|
+
// later without a silent collision — a future scheme bumps to `v2` and
|
|
48
|
+
// derives an unrelated key from the same passphrase.
|
|
49
|
+
const HKDF_INFO_PREFIX = 'zkLicensing/v1/ownership|';
|
|
50
|
+
const HKDF_SALT = new Uint8Array(0);
|
|
51
|
+
|
|
52
|
+
// Domain-separation tags baked into every signed message. Prevents a
|
|
53
|
+
// signature captured on one flow (activation) from being replayed on
|
|
54
|
+
// another (binding) — the first Field the signer hashes is the flow ID,
|
|
55
|
+
// so the two signatures live in disjoint spaces even at identical
|
|
56
|
+
// (licenseHash, nonce).
|
|
57
|
+
//
|
|
58
|
+
// Exported so `LicensingApp.bind()` / `unbind()` can rebuild the exact
|
|
59
|
+
// same Field[] preimage inside the circuit for `Signature.verify()`.
|
|
60
|
+
// Bumping these values is a hardfork — every off-chain signer and every
|
|
61
|
+
// deployed circuit would need to agree.
|
|
62
|
+
export const TAG_ACTIVATION = 1n;
|
|
63
|
+
export const TAG_BINDING = 2n;
|
|
64
|
+
export const TAG_UNBINDING = 3n;
|
|
65
|
+
|
|
66
|
+
function buildHkdfInfo(zkAppAddress: string): Uint8Array {
|
|
67
|
+
return new TextEncoder().encode(HKDF_INFO_PREFIX + zkAppAddress);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function utf8Bytes(s: string): Uint8Array {
|
|
71
|
+
return new TextEncoder().encode(s);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
75
|
+
let s = '';
|
|
76
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
77
|
+
return btoa(s);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function base64ToBytes(b64: string): Uint8Array {
|
|
81
|
+
const bin = atob(b64);
|
|
82
|
+
const out = new Uint8Array(bin.length);
|
|
83
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function subtle(): SubtleCrypto {
|
|
88
|
+
const s = (globalThis.crypto as Crypto | undefined)?.subtle;
|
|
89
|
+
if (!s) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'WebCrypto unavailable — ownershipSignature requires Node ≥ 20 or a modern browser (Chromium 137+, Safari 17+, Firefox 130+).',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return s;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
98
|
+
let n = 0n;
|
|
99
|
+
for (const b of bytes) n = (n << 8n) | BigInt(b);
|
|
100
|
+
return n;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* HKDF-SHA256 with empty salt and a per-app domain-separation info
|
|
105
|
+
* string (`zkLicensing/v1/ownership|<zkAppAddress>`). Deterministic:
|
|
106
|
+
* same (passphrase, zkAppAddress) → same 32-byte seed. Different
|
|
107
|
+
* zkAppAddress → cryptographically unrelated seed even with the same
|
|
108
|
+
* passphrase.
|
|
109
|
+
*/
|
|
110
|
+
export async function deriveSeed(
|
|
111
|
+
passphrase: string,
|
|
112
|
+
zkAppAddress: string,
|
|
113
|
+
): Promise<Uint8Array> {
|
|
114
|
+
const baseKey = await subtle().importKey(
|
|
115
|
+
'raw',
|
|
116
|
+
utf8Bytes(passphrase) as unknown as ArrayBuffer,
|
|
117
|
+
'HKDF',
|
|
118
|
+
false,
|
|
119
|
+
['deriveBits'],
|
|
120
|
+
);
|
|
121
|
+
const info = buildHkdfInfo(zkAppAddress);
|
|
122
|
+
const bits = await subtle().deriveBits(
|
|
123
|
+
{
|
|
124
|
+
name: 'HKDF',
|
|
125
|
+
hash: 'SHA-256',
|
|
126
|
+
salt: HKDF_SALT as unknown as ArrayBuffer,
|
|
127
|
+
info: info as unknown as ArrayBuffer,
|
|
128
|
+
},
|
|
129
|
+
baseKey,
|
|
130
|
+
256,
|
|
131
|
+
);
|
|
132
|
+
return new Uint8Array(bits);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type OwnershipKeypair = {
|
|
136
|
+
privateKey: PrivateKey;
|
|
137
|
+
publicKey: PublicKey;
|
|
138
|
+
publicKeyBase58: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Derive a Pallas Schnorr keypair deterministically from (passphrase,
|
|
143
|
+
* zkAppAddress). The keypair is bound to the specific app: the same
|
|
144
|
+
* passphrase used against a different app yields a different keypair,
|
|
145
|
+
* so a leaked passphrase cannot be replayed as an activation for any
|
|
146
|
+
* other app.
|
|
147
|
+
*
|
|
148
|
+
* The 32-byte HKDF output is interpreted as a big-endian integer and
|
|
149
|
+
* passed to `PrivateKey.fromBigInt`, which reduces it modulo the Pallas
|
|
150
|
+
* scalar field order internally. All 2^256 possible HKDF outputs map to
|
|
151
|
+
* valid private keys with (256 - log2(Fq_order))-bit modular bias — the
|
|
152
|
+
* scalar field is ~254 bits, so bias is <2^-253 and lies well below any
|
|
153
|
+
* practical distinguisher.
|
|
154
|
+
*/
|
|
155
|
+
export async function deriveOwnershipKeypair(
|
|
156
|
+
passphrase: string,
|
|
157
|
+
zkAppAddress: string,
|
|
158
|
+
): Promise<OwnershipKeypair> {
|
|
159
|
+
const seed = await deriveSeed(passphrase, zkAppAddress);
|
|
160
|
+
const privateKey = PrivateKey.fromBigInt(bytesToBigInt(seed));
|
|
161
|
+
const publicKey = privateKey.toPublicKey();
|
|
162
|
+
return { privateKey, publicKey, publicKeyBase58: publicKey.toBase58() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The ownership keypair of license `licenseHash`, trying each candidate app
|
|
167
|
+
* address in turn. The key is derived from the address the license was
|
|
168
|
+
* issued on; after a redeploy the license carries over to the successor
|
|
169
|
+
* unchanged, so its key is the earlier generation's. Pass the app's
|
|
170
|
+
* generation addresses (any order; the live one first is cheapest for new
|
|
171
|
+
* licenses). A wrong address only gives a key that doesn't match, so the
|
|
172
|
+
* list needn't be authenticated. Null when none matches (another passphrase).
|
|
173
|
+
*/
|
|
174
|
+
export async function findOwnershipKeypair(
|
|
175
|
+
passphrase: string,
|
|
176
|
+
licenseHash: string,
|
|
177
|
+
candidateAddresses: readonly string[],
|
|
178
|
+
): Promise<(OwnershipKeypair & { zkAppAddress: string }) | null> {
|
|
179
|
+
for (const zkAppAddress of [...new Set(candidateAddresses)]) {
|
|
180
|
+
const kp = await deriveOwnershipKeypair(passphrase, zkAppAddress);
|
|
181
|
+
if (licenseHashFromPubkey(kp.publicKey) === licenseHash) return { ...kp, zkAppAddress };
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The addresses of every generation of the app at `zkAppAddress` (any of
|
|
188
|
+
* them), live first, from the keeper's generation list; just
|
|
189
|
+
* `zkAppAddress` when the keeper doesn't know it. For findOwnershipKeypair.
|
|
190
|
+
*/
|
|
191
|
+
export async function ownershipCandidateAddresses(keeperUrl: string, zkAppAddress: string, fetcher: typeof fetch = fetch): Promise<string[]> {
|
|
192
|
+
try {
|
|
193
|
+
const r = await fetcher(`${keeperUrl.replace(/\/+$/, '')}/apps/by-address/${encodeURIComponent(zkAppAddress)}`);
|
|
194
|
+
if (!r.ok) return [zkAppAddress];
|
|
195
|
+
const body = await r.json() as { generations?: Array<{ gen: number; zkAppAddress: string }> };
|
|
196
|
+
const gens = Array.isArray(body.generations) ? [...body.generations].sort((a, b) => b.gen - a.gen).map(g => g.zkAppAddress) : [];
|
|
197
|
+
return [...new Set([zkAppAddress, ...gens])];
|
|
198
|
+
} catch {
|
|
199
|
+
return [zkAppAddress];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Message shapes ────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Activation assertion shape. Proves the caller holds the passphrase
|
|
207
|
+
* that derived the license's ownerPubKey. Retained as an SDK primitive
|
|
208
|
+
* so external tooling can still request / verify an off-band "I own
|
|
209
|
+
* this license" signature — the on-chain flows themselves do not use
|
|
210
|
+
* it.
|
|
211
|
+
*/
|
|
212
|
+
export type ActivationMessage = {
|
|
213
|
+
licenseHash: string; // Field as decimal string
|
|
214
|
+
nonce: string; // Field as decimal string — replay defense
|
|
215
|
+
zkAppAddress: string; // base58
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Binding / unbinding message — the /prove/bind and /prove/unbind
|
|
220
|
+
* flows. Authorizes a specific device commitment being placed into or
|
|
221
|
+
* cleared from a specific slot on a specific license.
|
|
222
|
+
*/
|
|
223
|
+
export type BindingMessage = {
|
|
224
|
+
licenseHash: string; // Field as decimal string
|
|
225
|
+
deviceCommitment: string; // Field as decimal string (Poseidon(devicePubKeyHash))
|
|
226
|
+
slotIndex: number; // 0..cap-1
|
|
227
|
+
nonce: string; // Field as decimal string
|
|
228
|
+
// Target slot's current packed (boundAt, freedAt) — see slotStateOf().
|
|
229
|
+
// Every bind/unbind changes it, so a signature authorizes exactly one
|
|
230
|
+
// transition and cannot be replayed once the slot has moved on.
|
|
231
|
+
slotState: string; // Field as decimal string
|
|
232
|
+
// The license leaf's current expirySlot. A refund + re-buy with the same
|
|
233
|
+
// passphrase resets every slot to pristine; the new leaf's expiry differs,
|
|
234
|
+
// so signatures from the earlier purchase stay dead.
|
|
235
|
+
expirySlot: number;
|
|
236
|
+
zkAppAddress: string; // base58 — stops cross-deployment replay
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Packed (boundAt, freedAt) of one slot as the circuit sees it — the
|
|
241
|
+
* `slotState` a BindingMessage must commit to. Takes the slot entry from a
|
|
242
|
+
* LicenseRecord's `devices[]` (as served by GET /license/:zkApp/:hash);
|
|
243
|
+
* an absent entry is a pristine slot (0).
|
|
244
|
+
*/
|
|
245
|
+
export function slotStateOf(
|
|
246
|
+
device?: { boundAtSlot: number; freedAtSlot: number } | null,
|
|
247
|
+
): string {
|
|
248
|
+
if (!device) return '0';
|
|
249
|
+
return packSlotTimes(UInt32.from(device.boundAtSlot), UInt32.from(device.freedAtSlot)).toString();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Retained for back-compat with callers that imported ChallengeMessage
|
|
253
|
+
// from this module before the v2 rename. New code should use
|
|
254
|
+
// ActivationMessage directly.
|
|
255
|
+
export type ChallengeMessage = ActivationMessage;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Canonicalize the activation message into the Field[] that
|
|
259
|
+
* `Signature.create` / `Signature.verify` hash. The domain-separation
|
|
260
|
+
* tag prefix means no captured activation signature can be replayed as
|
|
261
|
+
* a binding signature, even if all other fields collide.
|
|
262
|
+
*/
|
|
263
|
+
export function activationMessageFields(msg: ActivationMessage): Field[] {
|
|
264
|
+
const addr = PublicKey.fromBase58(msg.zkAppAddress);
|
|
265
|
+
return [
|
|
266
|
+
Field(TAG_ACTIVATION),
|
|
267
|
+
Field(msg.licenseHash),
|
|
268
|
+
Field(msg.nonce),
|
|
269
|
+
addr.x,
|
|
270
|
+
addr.isOdd.toField(),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Canonicalize a binding or unbinding message. The `tag` argument
|
|
276
|
+
* distinguishes bind (TAG_BINDING) from unbind (TAG_UNBINDING) so a
|
|
277
|
+
* bind signature captured off-chain cannot be replayed as an unbind
|
|
278
|
+
* even at identical (licenseHash, deviceCommitment, slotIndex, nonce).
|
|
279
|
+
*/
|
|
280
|
+
function bindingLikeFields(tag: bigint, msg: BindingMessage): Field[] {
|
|
281
|
+
const addr = PublicKey.fromBase58(msg.zkAppAddress);
|
|
282
|
+
return [
|
|
283
|
+
Field(tag),
|
|
284
|
+
Field(msg.licenseHash),
|
|
285
|
+
Field(msg.deviceCommitment),
|
|
286
|
+
Field(msg.slotIndex),
|
|
287
|
+
Field(msg.nonce),
|
|
288
|
+
Field(msg.slotState),
|
|
289
|
+
Field(msg.expirySlot),
|
|
290
|
+
addr.x,
|
|
291
|
+
addr.isOdd.toField(),
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function bindingMessageFields(msg: BindingMessage): Field[] {
|
|
296
|
+
return bindingLikeFields(TAG_BINDING, msg);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function unbindingMessageFields(msg: BindingMessage): Field[] {
|
|
300
|
+
return bindingLikeFields(TAG_UNBINDING, msg);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Signing ───────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
export function signActivation(
|
|
306
|
+
privateKey: PrivateKey,
|
|
307
|
+
msg: ActivationMessage,
|
|
308
|
+
): Signature {
|
|
309
|
+
return Signature.create(privateKey, activationMessageFields(msg));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function signBinding(
|
|
313
|
+
privateKey: PrivateKey,
|
|
314
|
+
msg: BindingMessage,
|
|
315
|
+
): Signature {
|
|
316
|
+
return Signature.create(privateKey, bindingMessageFields(msg));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function signUnbinding(
|
|
320
|
+
privateKey: PrivateKey,
|
|
321
|
+
msg: BindingMessage,
|
|
322
|
+
): Signature {
|
|
323
|
+
return Signature.create(privateKey, unbindingMessageFields(msg));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Retained for back-compat with the Ed25519-era name. New code should
|
|
327
|
+
// call signActivation directly.
|
|
328
|
+
export function signChallenge(
|
|
329
|
+
privateKey: PrivateKey,
|
|
330
|
+
msg: ChallengeMessage,
|
|
331
|
+
): Signature {
|
|
332
|
+
return signActivation(privateKey, msg);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Verification ──────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Verify an activation signature. Returns false (never throws) for any
|
|
339
|
+
* failure — malformed pubkey, malformed signature, actual signature
|
|
340
|
+
* mismatch. Callers get a single go/no-go decision without having to
|
|
341
|
+
* distinguish "invalid" from "unparseable."
|
|
342
|
+
*/
|
|
343
|
+
export function verifyActivation(
|
|
344
|
+
publicKey: PublicKey,
|
|
345
|
+
msg: ActivationMessage,
|
|
346
|
+
signature: Signature,
|
|
347
|
+
): boolean {
|
|
348
|
+
try {
|
|
349
|
+
return signature.verify(publicKey, activationMessageFields(msg)).toBoolean();
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function verifyBinding(
|
|
356
|
+
publicKey: PublicKey,
|
|
357
|
+
msg: BindingMessage,
|
|
358
|
+
signature: Signature,
|
|
359
|
+
): boolean {
|
|
360
|
+
try {
|
|
361
|
+
return signature.verify(publicKey, bindingMessageFields(msg)).toBoolean();
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function verifyUnbinding(
|
|
368
|
+
publicKey: PublicKey,
|
|
369
|
+
msg: BindingMessage,
|
|
370
|
+
signature: Signature,
|
|
371
|
+
): boolean {
|
|
372
|
+
try {
|
|
373
|
+
return signature.verify(publicKey, unbindingMessageFields(msg)).toBoolean();
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Back-compat alias, see signChallenge.
|
|
380
|
+
export function verifyChallenge(
|
|
381
|
+
publicKey: PublicKey,
|
|
382
|
+
msg: ChallengeMessage,
|
|
383
|
+
signature: Signature,
|
|
384
|
+
): boolean {
|
|
385
|
+
return verifyActivation(publicKey, msg, signature);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Wire encoding helpers ─────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Encode a signature for the wire (POST body, ProofFile JSON, etc).
|
|
392
|
+
* Base58 matches Mina's canonical serialization for both PublicKey and
|
|
393
|
+
* Signature, keeping the same encoding shape across everything the
|
|
394
|
+
* buyer's SDK produces.
|
|
395
|
+
*/
|
|
396
|
+
export function encodeSignature(sig: Signature): string {
|
|
397
|
+
return sig.toBase58();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function decodeSignature(base58: string): Signature {
|
|
401
|
+
return Signature.fromBase58(base58);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function encodePublicKey(pk: PublicKey): string {
|
|
405
|
+
return pk.toBase58();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function decodePublicKey(base58: string): PublicKey {
|
|
409
|
+
return PublicKey.fromBase58(base58);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Retained so callers that already do `encodeBase64(seed)` for logging
|
|
413
|
+
// or persistence keep working — this module no longer produces raw byte
|
|
414
|
+
// pubkeys or signatures, only o1js objects with .toBase58().
|
|
415
|
+
export function encodeBase64(bytes: Uint8Array): string {
|
|
416
|
+
return bytesToBase64(bytes);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function decodeBase64(b64: string): Uint8Array {
|
|
420
|
+
return base64ToBytes(b64);
|
|
421
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* permitCheckFlow.ts — `zklicensing/permits/check`
|
|
6
|
+
*
|
|
7
|
+
* Anonymous permit checks through the keeper's relay (verify.html's
|
|
8
|
+
* Anonymous mode and prove.html use this; so can any checker or holder app).
|
|
9
|
+
*
|
|
10
|
+
* checker: openCheck (or createCheckEvent + openCheck in it) → show the
|
|
11
|
+
* check's link → verifyCheck once the holder has proved
|
|
12
|
+
* holder: proveForCheck(checkId, permit file, wallet)
|
|
13
|
+
*
|
|
14
|
+
* The challenge is bound to the site both sides are on: context =
|
|
15
|
+
* verifierContext(origin, check nonce). The holder builds its Merkle witness
|
|
16
|
+
* from a compact copy of the whole registry (zklicensing/permits/tree), so
|
|
17
|
+
* the keeper never learns which permit is being proved; the proof itself
|
|
18
|
+
* reveals nothing about it. The checker verifies the proof itself, against roots from the keeper or, with
|
|
19
|
+
* `trustless`, from a Mina node.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Field, MerkleMap, PublicKey } from 'o1js';
|
|
23
|
+
import { permitLeafHash } from './permitMessages.js';
|
|
24
|
+
import { recordToLeaf, permitStatusAt, type PermitRecord } from './permitRegistryRebuild.js';
|
|
25
|
+
import { syncRegistryTree, witnessFromCompactTree, type TreeCache } from './permitTree.js';
|
|
26
|
+
import type { PermitMessageDescription } from './permitDescribe.js';
|
|
27
|
+
|
|
28
|
+
export type Network = 'devnet' | 'mainnet';
|
|
29
|
+
|
|
30
|
+
export type CheckView = {
|
|
31
|
+
id: string; network: Network; registry: string; requiredScope: string; label: string; nonce: string;
|
|
32
|
+
createdAt: string; expiresAt: string; status: 'waiting' | 'proved' | 'expired';
|
|
33
|
+
event?: { id: string; label: string; expiresAt: string };
|
|
34
|
+
};
|
|
35
|
+
export type CheckEventView = { id: string; network: Network; registry: string; requiredScope: string; label: string; createdAt: string; expiresAt: string };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The holder's permit file (from the apply page): no secret in it. `registry`
|
|
39
|
+
* is where it was issued; `lineage`, that registry's first generation's
|
|
40
|
+
* address (what its secret is derived against; `registry` when absent).
|
|
41
|
+
*/
|
|
42
|
+
export type PermitFileRef = { registry: string; subject: string; holderKey: string; network?: string; lineage?: string };
|
|
43
|
+
|
|
44
|
+
export class CheckError extends Error {
|
|
45
|
+
constructor(message: string, readonly code: string, readonly status = 0) { super(message); this.name = 'CheckError'; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The keeper's check relay. */
|
|
49
|
+
export function createCheckRelay(keeperUrl: string, fetcher: typeof fetch = fetch) {
|
|
50
|
+
const base = keeperUrl.replace(/\/+$/, '');
|
|
51
|
+
async function call<T>(method: string, path: string, body?: unknown, headers: Record<string, string> = {}): Promise<T> {
|
|
52
|
+
const r = await fetcher(base + path, {
|
|
53
|
+
method, cache: 'no-store',
|
|
54
|
+
headers: { Accept: 'application/json', ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...headers },
|
|
55
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
const j: any = await r.json().catch(() => null);
|
|
58
|
+
if (!r.ok) throw new CheckError(j?.error ?? `HTTP ${r.status}`, j?.code ?? 'HTTP_ERROR', r.status);
|
|
59
|
+
return j as T;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
/** A single check: its own nonce, so its nullifier never repeats. */
|
|
63
|
+
openCheck: (a: { registry: string; network?: Network; requiredScope?: string; label?: string }) =>
|
|
64
|
+
call<{ check: CheckView }>('POST', '/permits/checks', a).then(r => r.check),
|
|
65
|
+
/** A check in an event (the event's nonce), opened with the event key. */
|
|
66
|
+
openEventCheck: (eventId: string, eventKey: string) =>
|
|
67
|
+
call<{ check: CheckView }>('POST', '/permits/checks', { eventId }, { 'X-Event-Key': eventKey }).then(r => r.check),
|
|
68
|
+
getCheck: (id: string) => call<{ check: CheckView; proof?: unknown; provedAt?: string }>('GET', `/permits/checks/${encodeURIComponent(id)}`),
|
|
69
|
+
submitProof: (id: string, proof: unknown) => call<{ ok: true }>('POST', `/permits/checks/${encodeURIComponent(id)}/proof`, { proof }),
|
|
70
|
+
createEvent: (a: { registry: string; network?: Network; requiredScope?: string; label: string; days?: number }) =>
|
|
71
|
+
call<{ event: CheckEventView; eventKey: string }>('POST', '/permits/check-events', a),
|
|
72
|
+
getEvent: (id: string) => call<{ event: CheckEventView }>('GET', `/permits/check-events/${encodeURIComponent(id)}`).then(r => r.event),
|
|
73
|
+
/** Records a verified nullifier at the event: first use there, or when it was first seen. */
|
|
74
|
+
markSeen: (eventId: string, eventKey: string, nullifier: string) =>
|
|
75
|
+
call<{ firstUse: boolean; firstSeenAt: string; count: number }>('POST', `/permits/check-events/${encodeURIComponent(eventId)}/seen`, { nullifier }, { 'X-Event-Key': eventKey }),
|
|
76
|
+
seenCount: (eventId: string, eventKey: string) =>
|
|
77
|
+
call<{ count: number }>('GET', `/permits/check-events/${encodeURIComponent(eventId)}/seen`, undefined, { 'X-Event-Key': eventKey }).then(r => r.count),
|
|
78
|
+
/** The registry's compact tree (zklicensing/permits/tree), or with `since` the changes since that root. */
|
|
79
|
+
getTree: (registry: string, since?: string) =>
|
|
80
|
+
call<any>('GET', `/permits/registries/${encodeURIComponent(registry)}/tree${since ? `?since=${encodeURIComponent(since)}` : ''}`),
|
|
81
|
+
getRoots: (registry: string) => call<{ roots: Array<{ root: string; slot: number }>; current: string; slot: number }>('GET', `/permits/registries/${encodeURIComponent(registry)}/roots`),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The Merkle witness for `subject`, built locally from all of a registry's permits. */
|
|
86
|
+
export function witnessFromPermits(permits: PermitRecord[], subject: string): { root: string; witness: { isLefts: boolean[]; siblings: string[] } } {
|
|
87
|
+
const map = new MerkleMap();
|
|
88
|
+
for (const p of permits) map.set(Field(p.subject), permitLeafHash(recordToLeaf(p)));
|
|
89
|
+
const w = map.getWitness(Field(subject));
|
|
90
|
+
return { root: map.getRoot().toString(), witness: { isLefts: w.isLefts.map(b => b.toBoolean()), siblings: w.siblings.map(s => s.toString()) } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ProveStep = 'check' | 'registry' | 'sign-secret' | 'sign-proof' | 'compile-prove' | 'send';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Holder: proves to a check that this wallet holds an active permit covering
|
|
97
|
+
* the check's activities, and sends the proof. `origin` is the site the
|
|
98
|
+
* holder is on (the check's verifier). Two wallet signatures: one re-derives
|
|
99
|
+
* the permit's secret, one authorises the proof.
|
|
100
|
+
*/
|
|
101
|
+
export async function proveForCheck(opts: {
|
|
102
|
+
keeperUrl: string;
|
|
103
|
+
network: Network;
|
|
104
|
+
checkId: string;
|
|
105
|
+
permit: PermitFileRef;
|
|
106
|
+
wallet: { address: string; signFields(fields: string[]): Promise<string>; signMessage(message: string): Promise<{ field: string; scalar: string }> };
|
|
107
|
+
origin: string;
|
|
108
|
+
graphqlUrl?: string;
|
|
109
|
+
fetcher?: typeof fetch;
|
|
110
|
+
/** Where to keep registry copies between checks (IndexedDB in a browser); without one, each check downloads the registry. */
|
|
111
|
+
treeCache?: TreeCache;
|
|
112
|
+
onStep?: (step: ProveStep) => void;
|
|
113
|
+
/** Shown before the proof's field signature (as PermitsClientOptions.confirmSignature); false cancels. */
|
|
114
|
+
confirmSignature?: (d: PermitMessageDescription) => Promise<boolean | void> | boolean | void;
|
|
115
|
+
}): Promise<{ nullifier: string }> {
|
|
116
|
+
const step = opts.onStep ?? (() => {});
|
|
117
|
+
const relay = createCheckRelay(opts.keeperUrl, opts.fetcher);
|
|
118
|
+
step('check');
|
|
119
|
+
const { check } = await relay.getCheck(opts.checkId);
|
|
120
|
+
if (check.status !== 'waiting') throw new CheckError(check.status === 'proved' ? 'This check already has a proof.' : 'This check has expired; ask for a new one.', 'CHECK_CLOSED');
|
|
121
|
+
if (check.network !== opts.network) throw new CheckError(`This check is on ${check.network}.`, 'WRONG_NETWORK');
|
|
122
|
+
const p = opts.permit;
|
|
123
|
+
if (p.network && p.network !== opts.network) throw new CheckError(`Your permit is on ${p.network}; this check is on ${opts.network}.`, 'WRONG_NETWORK');
|
|
124
|
+
if (opts.wallet.address !== p.holderKey) throw new CheckError(`Connect the permit's holder key ${p.holderKey}.`, 'WRONG_KEY');
|
|
125
|
+
if (p.registry !== check.registry) {
|
|
126
|
+
// Issued by an earlier generation: fine when, on chain, its successors lead to the registry checked.
|
|
127
|
+
const [{ currentOnChain, PERMIT_LINEAGE }, { CANONICAL_PERMIT_VK_HASHES }, { DEFAULT_MINA_GRAPHQL_URLS }] = await Promise.all([
|
|
128
|
+
import('./successorChain.js'), import('./permitInterface.js'), import('./onChainRoot.js'),
|
|
129
|
+
]);
|
|
130
|
+
const current = await currentOnChain({
|
|
131
|
+
start: p.registry, layout: PERMIT_LINEAGE, canonicalVks: Object.values(CANONICAL_PERMIT_VK_HASHES),
|
|
132
|
+
graphqlUrl: opts.graphqlUrl ?? DEFAULT_MINA_GRAPHQL_URLS[opts.network], fetcher: opts.fetcher,
|
|
133
|
+
}).then(r => r.address, () => null);
|
|
134
|
+
if (current !== check.registry) throw new CheckError('Your permit is from another registry than the one this check asks for.', 'WRONG_REGISTRY');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
step('registry');
|
|
138
|
+
// The whole registry (a compact copy, kept between checks), so the keeper
|
|
139
|
+
// can't tell which permit is proved; the root and slot from a Mina node,
|
|
140
|
+
// so the proof is for the chain's current state.
|
|
141
|
+
const { readChainHead } = await import('./chainHead.js');
|
|
142
|
+
const [tree, head] = await Promise.all([
|
|
143
|
+
syncRegistryTree({ registry: check.registry, fetchTree: since => relay.getTree(check.registry, since), cache: opts.treeCache }),
|
|
144
|
+
readChainHead({ address: check.registry, network: opts.network, graphqlUrl: opts.graphqlUrl, fetcher: opts.fetcher }),
|
|
145
|
+
]);
|
|
146
|
+
if (tree.root !== head.root) throw new CheckError('zkLicensing\'s copy of the registry is behind the blockchain; try again in a minute.', 'ROOT_BEHIND');
|
|
147
|
+
let found;
|
|
148
|
+
try { found = witnessFromCompactTree(tree, p.subject); }
|
|
149
|
+
catch (e: any) { throw new CheckError(e?.message ?? String(e), /no such permit/.test(e?.message) ? 'NOT_FOUND' : 'ROOT_MISMATCH'); }
|
|
150
|
+
const { witness, permit: rec } = found;
|
|
151
|
+
const status = permitStatusAt(rec, head.slot);
|
|
152
|
+
if (status !== 'active') throw new CheckError(`Your permit is ${status}.`, 'NOT_ACTIVE');
|
|
153
|
+
const required = BigInt(check.requiredScope);
|
|
154
|
+
if ((BigInt(rec.scope) & required) !== required) throw new CheckError('Your permit doesn\'t cover the activities this check asks for.', 'SCOPE');
|
|
155
|
+
|
|
156
|
+
step('sign-secret');
|
|
157
|
+
// Re-derived with the wallet against the permit's lineage: nothing from the keeper.
|
|
158
|
+
const { findHolderSecret } = await import('./holderSecret.js');
|
|
159
|
+
let secret: string;
|
|
160
|
+
try { ({ secret } = await findHolderSecret(opts.wallet, { lineage: p.lineage ?? p.registry, network: opts.network, subject: p.subject })); }
|
|
161
|
+
catch (e: any) { throw new CheckError(e?.message ?? String(e), 'WRONG_SECRET'); }
|
|
162
|
+
|
|
163
|
+
step('sign-proof');
|
|
164
|
+
const { holderProofMessage, verifierContext, proveHolder } = await import('./permitProof.js');
|
|
165
|
+
const context = verifierContext(opts.origin, check.nonce);
|
|
166
|
+
const msg = holderProofMessage(PublicKey.fromBase58(check.registry), Field(head.root), Field(context));
|
|
167
|
+
const fields = msg.map(f => f.toString());
|
|
168
|
+
if (opts.confirmSignature) {
|
|
169
|
+
const { fingerprint } = await import('./permitDescribe.js');
|
|
170
|
+
const d: PermitMessageDescription = {
|
|
171
|
+
kind: 'holderProof', title: `Prove to ${new URL(opts.origin).host} that you hold an active permit, without saying which`,
|
|
172
|
+
facts: [['Checker', opts.origin], ['Registry', check.registry], ['Network', opts.network], ['Check', check.label || check.id]],
|
|
173
|
+
network: opts.network, registry: check.registry, verified: true, fingerprint: fingerprint(fields),
|
|
174
|
+
};
|
|
175
|
+
if ((await opts.confirmSignature(d)) === false) throw new CheckError('Cancelled', 'CANCELLED');
|
|
176
|
+
}
|
|
177
|
+
const holderSignature = await opts.wallet.signFields(fields);
|
|
178
|
+
|
|
179
|
+
step('compile-prove');
|
|
180
|
+
const { proof, nullifier } = await proveHolder({
|
|
181
|
+
registry: check.registry, root: head.root, witness, leaf: recordToLeaf(rec), holderSecret: secret,
|
|
182
|
+
slot: head.slot, requiredScope: required, context, holderSignature,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
step('send');
|
|
186
|
+
await relay.submitProof(check.id, proof);
|
|
187
|
+
return { nullifier };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Checker: verifies the proof sent to a check, in this process. Roots from
|
|
192
|
+
* the keeper's recent roots, or with `trustless` only the registry's current
|
|
193
|
+
* root read from a Mina node. `origin` is the site the check was opened on.
|
|
194
|
+
*/
|
|
195
|
+
export async function verifyCheck(opts: {
|
|
196
|
+
keeperUrl: string;
|
|
197
|
+
network: Network;
|
|
198
|
+
checkId: string;
|
|
199
|
+
origin: string;
|
|
200
|
+
trustless?: boolean;
|
|
201
|
+
maxSlotSkew?: number;
|
|
202
|
+
graphqlUrl?: string;
|
|
203
|
+
fetcher?: typeof fetch;
|
|
204
|
+
}): Promise<{ status: CheckView['status']; check: CheckView; valid: boolean; nullifier: string | null; reason: string | null }> {
|
|
205
|
+
const relay = createCheckRelay(opts.keeperUrl, opts.fetcher);
|
|
206
|
+
const { check, proof } = await relay.getCheck(opts.checkId);
|
|
207
|
+
if (!proof) return { status: check.status, check, valid: false, nullifier: null, reason: null };
|
|
208
|
+
let acceptedRoots: string[];
|
|
209
|
+
let currentSlot: number;
|
|
210
|
+
if (opts.trustless) {
|
|
211
|
+
const { readChainHead } = await import('./chainHead.js');
|
|
212
|
+
const head = await readChainHead({ address: check.registry, network: opts.network, graphqlUrl: opts.graphqlUrl, fetcher: opts.fetcher });
|
|
213
|
+
acceptedRoots = [head.root];
|
|
214
|
+
currentSlot = head.slot;
|
|
215
|
+
} else {
|
|
216
|
+
const r = await relay.getRoots(check.registry);
|
|
217
|
+
const { freshRoots } = await import('./agent.js');
|
|
218
|
+
acceptedRoots = freshRoots(r.roots, r.current, r.slot, opts.maxSlotSkew ?? 10);
|
|
219
|
+
currentSlot = r.slot;
|
|
220
|
+
}
|
|
221
|
+
const { verifyHolderProof } = await import('./permitProof.js');
|
|
222
|
+
const res = await verifyHolderProof(proof, {
|
|
223
|
+
registry: check.registry, acceptedRoots, currentSlot, maxSlotSkew: opts.maxSlotSkew,
|
|
224
|
+
requiredScope: BigInt(check.requiredScope), verifier: opts.origin, nonce: check.nonce,
|
|
225
|
+
});
|
|
226
|
+
return { status: 'proved', check, ...res };
|
|
227
|
+
}
|