@reclaimprotocol/client 0.1.27 → 0.1.29

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.
Files changed (73) hide show
  1. package/README.md +634 -42
  2. package/lib/api-client.d.ts +31 -0
  3. package/lib/api-client.js +112 -0
  4. package/lib/api-consts.d.ts +4 -0
  5. package/lib/api-consts.js +4 -0
  6. package/lib/api-errors.d.ts +13 -0
  7. package/lib/api-errors.js +30 -0
  8. package/lib/api.d.ts +7 -0
  9. package/lib/api.js +3 -0
  10. package/lib/attestor-proof.d.ts +6 -0
  11. package/lib/attestor-proof.js +45 -18
  12. package/lib/client.d.ts +1 -6
  13. package/lib/client.js +12 -33
  14. package/lib/consts.d.ts +1 -1
  15. package/lib/consts.js +1 -1
  16. package/lib/eip191.d.ts +4 -2
  17. package/lib/eip191.js +17 -1
  18. package/lib/errors.d.ts +38 -4
  19. package/lib/errors.js +99 -16
  20. package/lib/index.d.ts +9 -7
  21. package/lib/index.js +8 -4
  22. package/lib/jwks.d.ts +25 -1
  23. package/lib/jwks.js +69 -4
  24. package/lib/legacy-proof.d.ts +57 -0
  25. package/lib/legacy-proof.js +211 -0
  26. package/lib/proof-hash.d.ts +35 -0
  27. package/lib/proof-hash.js +216 -0
  28. package/lib/proof-recipe.d.ts +18 -0
  29. package/lib/proof-recipe.js +37 -0
  30. package/lib/proof-trust-constants.d.ts +3 -0
  31. package/lib/proof-trust-constants.js +43 -0
  32. package/lib/proof-types.d.ts +62 -0
  33. package/lib/proof-types.js +1 -0
  34. package/lib/proof-verification-error.d.ts +13 -0
  35. package/lib/proof-verification-error.js +11 -0
  36. package/lib/reclaim.d.ts +27 -5
  37. package/lib/reclaim.js +140 -14
  38. package/lib/result.d.ts +58 -6
  39. package/lib/result.js +252 -61
  40. package/lib/routes.gen.d.ts +54 -6
  41. package/lib/routes.gen.js +15 -3
  42. package/lib/tee-binding.d.ts +82 -0
  43. package/lib/tee-binding.js +112 -0
  44. package/lib/types/openapi.gen.d.ts +1343 -206
  45. package/lib/verification-clients.d.ts +17 -0
  46. package/lib/verification-clients.js +14 -0
  47. package/lib/verify-attestor-tee.d.ts +76 -0
  48. package/lib/verify-attestor-tee.js +243 -0
  49. package/lib/verify-tee.d.ts +32 -0
  50. package/lib/verify-tee.js +347 -0
  51. package/package.json +5 -1
  52. package/src/api.ts +15 -0
  53. package/src/attestor-proof.ts +57 -20
  54. package/src/client.ts +15 -38
  55. package/src/consts.ts +1 -2
  56. package/src/eip191.ts +31 -3
  57. package/src/errors.ts +132 -18
  58. package/src/index.ts +50 -5
  59. package/src/jwks.ts +128 -5
  60. package/src/legacy-proof.ts +367 -0
  61. package/src/proof-hash.ts +316 -0
  62. package/src/proof-recipe.ts +75 -0
  63. package/src/proof-trust-constants.ts +45 -0
  64. package/src/proof-types.ts +57 -0
  65. package/src/proof-verification-error.ts +13 -0
  66. package/src/reclaim.ts +206 -19
  67. package/src/result.ts +403 -68
  68. package/src/routes.gen.ts +15 -3
  69. package/src/tee-binding.ts +192 -0
  70. package/src/types/openapi.gen.ts +1343 -206
  71. package/src/verification-clients.ts +22 -0
  72. package/src/verify-attestor-tee.ts +365 -0
  73. package/src/verify-tee.ts +507 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Stable names for the Verification Clients seeded by Builder.
3
+ *
4
+ * A name identifies a registry record; session creation still selects a
5
+ * client with that deployment's exact registered URL.
6
+ */
7
+ export declare const KNOWN_VERIFICATION_CLIENTS: {
8
+ readonly builder: "builder";
9
+ readonly portals: "portals";
10
+ readonly verifierApp: "verifier-app";
11
+ readonly inAppSdk: "inapp-sdk";
12
+ readonly reclaimBrowserExtension: "reclaim-browser-extension";
13
+ readonly zkFetch: "zkfetch";
14
+ };
15
+ export type KnownVerificationClientName = typeof KNOWN_VERIFICATION_CLIENTS[keyof typeof KNOWN_VERIFICATION_CLIENTS];
16
+ /** Deployment-specific registered URLs keyed by a known client name. */
17
+ export type KnownVerificationClientUrlMap = Partial<Record<KnownVerificationClientName, string>>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Stable names for the Verification Clients seeded by Builder.
3
+ *
4
+ * A name identifies a registry record; session creation still selects a
5
+ * client with that deployment's exact registered URL.
6
+ */
7
+ export const KNOWN_VERIFICATION_CLIENTS = {
8
+ builder: 'builder',
9
+ portals: 'portals',
10
+ verifierApp: 'verifier-app',
11
+ inAppSdk: 'inapp-sdk',
12
+ reclaimBrowserExtension: 'reclaim-browser-extension',
13
+ zkFetch: 'zkfetch',
14
+ };
@@ -0,0 +1,76 @@
1
+ import type { LegacyProof as Proof } from './proof-types.ts';
2
+ /**
3
+ * Result of verifying an attestor TEE attestation.
4
+ */
5
+ export type AttestorTeeVerificationResult = {
6
+ isVerified: boolean;
7
+ error?: string;
8
+ /** sha256 image digest of the attestor container, on success. */
9
+ imageDigest?: string;
10
+ };
11
+ /**
12
+ * Validates a GCP Confidential Space attestation JWT produced by an
13
+ * attestor running in a Confidential Space VM, and asserts that the
14
+ * attestation binds to the given attestor address.
15
+ *
16
+ * The attestor (running inside the TEE) calls the Confidential Space
17
+ * launcher's attestation endpoint with two nonces:
18
+ * - `attestor_public_key:<eth-address>` - binds to the signing key.
19
+ * - `attestor_cert_hash:<sha256-hex>` - binds to the live TLS cert.
20
+ *
21
+ * This function only verifies the public-key nonce. The TLS cert hash
22
+ * binding is informational and not checked here. Callers that need to
23
+ * pin to a specific attestor image should compare the returned
24
+ * `imageDigest` against a known-good value.
25
+ *
26
+ * The JWT signature is verified by walking the x5c certificate chain
27
+ * to a pinned GCP Confidential Space Root CA. No outbound network
28
+ * calls are made.
29
+ *
30
+ * Node-only (uses node:crypto). Mirrors the environment restriction in
31
+ * the existing `verifyTeeAttestation` helper.
32
+ *
33
+ * @param report - the raw JWT string (header.payload.signature).
34
+ * @param expectedAttestorAddress - hex ETH address (0x-prefixed or
35
+ * unprefixed) that the attestation should be bound to.
36
+ */
37
+ export declare function verifyAttestorTeeAttestation(report: string, expectedAttestorAddress: string): Promise<AttestorTeeVerificationResult>;
38
+ /**
39
+ * Configuration for verifying the attestor's TEE attestation on each
40
+ * witness of the proof.
41
+ */
42
+ export type AttestorTeeAttestationConfig = {
43
+ /**
44
+ * Optional allowlist of expected attestor container image digests
45
+ * (e.g. `"sha256:4906340f..."`). When provided, the attestation's
46
+ * `submods.container.image_digest` must be in this list.
47
+ *
48
+ * Leave undefined to skip image pinning and rely solely on the JWT
49
+ * chain rooting to the GCP Confidential Space Root CA + nonce
50
+ * binding to the attestor address.
51
+ */
52
+ expectedImageDigests?: string[];
53
+ };
54
+ /**
55
+ * Verifies the attestor's TEE attestation for every witness of every
56
+ * provided proof. Throws `AttestorTeeVerificationError` on the first
57
+ * failure.
58
+ *
59
+ * Each witness must carry a `claimAttestation`. For each one, this:
60
+ * 1. Asserts `attestor_address` matches `witness.id`.
61
+ * 2. Asserts `claim_signature` is present in `proof.signatures`.
62
+ * 3. Recovers the signer of `claim_signature` from the claim data and
63
+ * asserts it equals `witness.id` (binds the signature to the
64
+ * attestor that the TEE attestation will cover).
65
+ * 4. Calls `verifyAttestorTeeAttestation` to validate the JWT against
66
+ * the pinned GCP Confidential Space Root CA and the attestor-key
67
+ * nonce.
68
+ * 5. If `expectedImageDigests` is provided, asserts the attestation's
69
+ * container image digest is in the allowlist.
70
+ *
71
+ * Node-only (uses node:crypto), like `verifyTeeAttestation`.
72
+ *
73
+ * @param proofs - The proofs to verify.
74
+ * @param config - Optional config; see {@link AttestorTeeAttestationConfig}.
75
+ */
76
+ export declare function runAttestorTeeVerification(proofs: Proof[], config?: AttestorTeeAttestationConfig): Promise<void>;
@@ -0,0 +1,243 @@
1
+ import { createSignDataForClaim } from '@reclaimprotocol/attestor-core';
2
+ /* eslint-disable @stylistic/max-len */
3
+ import crypto, { X509Certificate } from 'node:crypto';
4
+ import { recoverEip191Address } from "./eip191.js";
5
+ import { ATTESTOR_NONCE_PATTERN, GCP_CONFIDENTIAL_SPACE_ISSUER, GCP_CONFIDENTIAL_SPACE_ROOT_CA, } from "./proof-trust-constants.js";
6
+ import { legacyErrorClass } from "./proof-verification-error.js";
7
+ const AttestorTeeVerificationError = legacyErrorClass('AttestorTeeVerificationError');
8
+ const BROWSER_ENVIRONMENT_ERROR = 'Attestor TEE attestation verification is only supported in non-browser environments. Run verifyAttestorTeeAttestation on your server or API route.';
9
+ function isBrowserEnvironment() {
10
+ if ('window' in globalThis || 'document' in globalThis) {
11
+ return true;
12
+ }
13
+ if (typeof navigator !== 'undefined' && typeof process === 'undefined') {
14
+ return true;
15
+ }
16
+ const workerGlobalScope = globalThis.WorkerGlobalScope;
17
+ if (typeof workerGlobalScope === 'function' &&
18
+ globalThis instanceof workerGlobalScope) {
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ function assertNonBrowserEnvironment() {
24
+ if (isBrowserEnvironment()) {
25
+ throw new Error(BROWSER_ENVIRONMENT_ERROR);
26
+ }
27
+ }
28
+ const TOKEN_CLOCK_SKEW_S = 60;
29
+ function decodeBase64Url(input) {
30
+ const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
31
+ const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
32
+ return Buffer.from(padded, 'base64');
33
+ }
34
+ function normalizeAddress(address) {
35
+ return address.trim().toLowerCase().replace(/^0x/, '');
36
+ }
37
+ /**
38
+ * Walks the x5c certificate chain (leaf first) and verifies each link
39
+ * up to the pinned GCP Confidential Space Root CA. Returns the leaf
40
+ * certificate's public key on success.
41
+ */
42
+ function verifyX5cChain(x5cChain) {
43
+ if (!x5cChain || x5cChain.length === 0) {
44
+ throw new Error('Empty x5c certificate chain');
45
+ }
46
+ const certs = x5cChain.map((b64) => new X509Certificate(`-----BEGIN CERTIFICATE-----\n${b64}\n-----END CERTIFICATE-----`));
47
+ const root = new X509Certificate(GCP_CONFIDENTIAL_SPACE_ROOT_CA);
48
+ for (let i = 0; i < certs.length - 1; i++) {
49
+ if (!certs[i].verify(certs[i + 1].publicKey)) {
50
+ throw new Error(`Certificate chain verification failed at level ${i}`);
51
+ }
52
+ }
53
+ const top = certs[certs.length - 1];
54
+ if (!top.verify(root.publicKey)) {
55
+ throw new Error('Certificate chain does not root to GCP Confidential Space Root CA');
56
+ }
57
+ return certs[0].publicKey;
58
+ }
59
+ /**
60
+ * Validates a GCP Confidential Space attestation JWT produced by an
61
+ * attestor running in a Confidential Space VM, and asserts that the
62
+ * attestation binds to the given attestor address.
63
+ *
64
+ * The attestor (running inside the TEE) calls the Confidential Space
65
+ * launcher's attestation endpoint with two nonces:
66
+ * - `attestor_public_key:<eth-address>` - binds to the signing key.
67
+ * - `attestor_cert_hash:<sha256-hex>` - binds to the live TLS cert.
68
+ *
69
+ * This function only verifies the public-key nonce. The TLS cert hash
70
+ * binding is informational and not checked here. Callers that need to
71
+ * pin to a specific attestor image should compare the returned
72
+ * `imageDigest` against a known-good value.
73
+ *
74
+ * The JWT signature is verified by walking the x5c certificate chain
75
+ * to a pinned GCP Confidential Space Root CA. No outbound network
76
+ * calls are made.
77
+ *
78
+ * Node-only (uses node:crypto). Mirrors the environment restriction in
79
+ * the existing `verifyTeeAttestation` helper.
80
+ *
81
+ * @param report - the raw JWT string (header.payload.signature).
82
+ * @param expectedAttestorAddress - hex ETH address (0x-prefixed or
83
+ * unprefixed) that the attestation should be bound to.
84
+ */
85
+ export async function verifyAttestorTeeAttestation(report, expectedAttestorAddress) {
86
+ try {
87
+ assertNonBrowserEnvironment();
88
+ if (!report || typeof report !== 'string') {
89
+ throw new Error('attestation report is empty or not a string');
90
+ }
91
+ if (!expectedAttestorAddress || typeof expectedAttestorAddress !== 'string') {
92
+ throw new Error('expectedAttestorAddress is required');
93
+ }
94
+ const parts = report.split('.');
95
+ if (parts.length !== 3) {
96
+ throw new Error('attestation report is not a JWT (expected 3 parts)');
97
+ }
98
+ const [headerB64, payloadB64, signatureB64] = parts;
99
+ const header = JSON.parse(decodeBase64Url(headerB64).toString('utf8'));
100
+ const payload = JSON.parse(decodeBase64Url(payloadB64).toString('utf8'));
101
+ if (header.alg !== 'RS256') {
102
+ throw new Error(`unexpected signing algorithm: ${header.alg}`);
103
+ }
104
+ if (!Array.isArray(header.x5c) || header.x5c.length === 0) {
105
+ throw new Error('attestation report is missing x5c certificate chain');
106
+ }
107
+ if (payload.iss !== GCP_CONFIDENTIAL_SPACE_ISSUER) {
108
+ throw new Error(`unexpected issuer: ${payload.iss}`);
109
+ }
110
+ const now = Math.floor(Date.now() / 1000);
111
+ if (typeof payload.nbf === 'number' && now + TOKEN_CLOCK_SKEW_S < payload.nbf) {
112
+ throw new Error(`attestation not yet valid (nbf=${payload.nbf})`);
113
+ }
114
+ if (typeof payload.exp === 'number' && now - TOKEN_CLOCK_SKEW_S > payload.exp) {
115
+ throw new Error(`attestation expired (exp=${payload.exp})`);
116
+ }
117
+ if (typeof payload.iat === 'number' && payload.iat > now + TOKEN_CLOCK_SKEW_S) {
118
+ throw new Error(`attestation issued in future (iat=${payload.iat})`);
119
+ }
120
+ const publicKey = verifyX5cChain(header.x5c);
121
+ const verifier = crypto.createVerify('RSA-SHA256');
122
+ verifier.update(`${headerB64}.${payloadB64}`);
123
+ if (!verifier.verify(publicKey, new Uint8Array(decodeBase64Url(signatureB64)))) {
124
+ throw new Error('attestation signature verification failed');
125
+ }
126
+ if (!payload.eat_nonce) {
127
+ throw new Error('eat_nonce claim is missing');
128
+ }
129
+ const nonces = Array.isArray(payload.eat_nonce)
130
+ ? payload.eat_nonce
131
+ : [payload.eat_nonce];
132
+ let attestedAddress;
133
+ for (const n of nonces) {
134
+ const m = typeof n === 'string' ? n.match(ATTESTOR_NONCE_PATTERN) : null;
135
+ if (m) {
136
+ attestedAddress = m[1];
137
+ break;
138
+ }
139
+ }
140
+ if (!attestedAddress) {
141
+ throw new Error(`attestor_public_key nonce not found in eat_nonce: ${JSON.stringify(payload.eat_nonce)}`);
142
+ }
143
+ if (normalizeAddress(attestedAddress) !== normalizeAddress(expectedAttestorAddress)) {
144
+ throw new Error(`attestor address mismatch: attestation binds to 0x${attestedAddress.toLowerCase()}, ` +
145
+ `expected ${expectedAttestorAddress}`);
146
+ }
147
+ const imageDigest = payload.submods?.container?.image_digest
148
+ ?? payload.google?.compute_engine?.image_digest;
149
+ return { isVerified: true, imageDigest };
150
+ }
151
+ catch (error) {
152
+ return {
153
+ isVerified: false,
154
+ error: error instanceof Error ? error.message : String(error),
155
+ };
156
+ }
157
+ }
158
+ function normalizeAttestorAddress(address) {
159
+ return address.trim().toLowerCase();
160
+ }
161
+ function normalizeSignature(sig) {
162
+ return sig.trim().toLowerCase();
163
+ }
164
+ async function verifyAttestorTeeForProof(proof, config) {
165
+ if (!proof.witnesses || proof.witnesses.length === 0) {
166
+ throw new AttestorTeeVerificationError('Proof has no witnesses');
167
+ }
168
+ const expectedDigests = config.expectedImageDigests?.map(d => d.trim());
169
+ const proofSignatures = new Set((proof.signatures || []).map(normalizeSignature));
170
+ const claimSignData = createSignDataForClaim(proof.claimData);
171
+ for (const witness of proof.witnesses) {
172
+ const att = witness.claimAttestation;
173
+ if (!att) {
174
+ throw new AttestorTeeVerificationError(`Witness ${witness.id} is missing claimAttestation`);
175
+ }
176
+ if (normalizeAttestorAddress(att.attestor_address) !== normalizeAttestorAddress(witness.id)) {
177
+ throw new AttestorTeeVerificationError(`claimAttestation.attestor_address ${att.attestor_address} does not match witness id ${witness.id}`);
178
+ }
179
+ if (!proofSignatures.has(normalizeSignature(att.claim_signature))) {
180
+ throw new AttestorTeeVerificationError(`claimAttestation.claim_signature for witness ${witness.id} is not present in proof.signatures`);
181
+ }
182
+ let recoveredSigner;
183
+ try {
184
+ recoveredSigner = recoverEip191Address(claimSignData, att.claim_signature);
185
+ }
186
+ catch (error) {
187
+ throw new AttestorTeeVerificationError(`Failed to recover signer from claimAttestation.claim_signature for witness ${witness.id}`, error);
188
+ }
189
+ if (normalizeAttestorAddress(recoveredSigner) !== normalizeAttestorAddress(witness.id)) {
190
+ throw new AttestorTeeVerificationError(`claim_signature recovers to ${recoveredSigner}, expected attestor ${witness.id}`);
191
+ }
192
+ const result = await verifyAttestorTeeAttestation(att.attestation_report, witness.id);
193
+ if (!result.isVerified) {
194
+ throw new AttestorTeeVerificationError(`Attestor TEE attestation verification failed for witness ${witness.id}: ${result.error}`);
195
+ }
196
+ if (expectedDigests && expectedDigests.length > 0) {
197
+ if (!result.imageDigest) {
198
+ throw new AttestorTeeVerificationError(`Attestor TEE attestation for witness ${witness.id} did not expose an image digest to check against expectedImageDigests`);
199
+ }
200
+ if (!expectedDigests.includes(result.imageDigest)) {
201
+ throw new AttestorTeeVerificationError(`Attestor image digest ${result.imageDigest} for witness ${witness.id} is not in expectedImageDigests`);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ /**
207
+ * Verifies the attestor's TEE attestation for every witness of every
208
+ * provided proof. Throws `AttestorTeeVerificationError` on the first
209
+ * failure.
210
+ *
211
+ * Each witness must carry a `claimAttestation`. For each one, this:
212
+ * 1. Asserts `attestor_address` matches `witness.id`.
213
+ * 2. Asserts `claim_signature` is present in `proof.signatures`.
214
+ * 3. Recovers the signer of `claim_signature` from the claim data and
215
+ * asserts it equals `witness.id` (binds the signature to the
216
+ * attestor that the TEE attestation will cover).
217
+ * 4. Calls `verifyAttestorTeeAttestation` to validate the JWT against
218
+ * the pinned GCP Confidential Space Root CA and the attestor-key
219
+ * nonce.
220
+ * 5. If `expectedImageDigests` is provided, asserts the attestation's
221
+ * container image digest is in the allowlist.
222
+ *
223
+ * Node-only (uses node:crypto), like `verifyTeeAttestation`.
224
+ *
225
+ * @param proofs - The proofs to verify.
226
+ * @param config - Optional config; see {@link AttestorTeeAttestationConfig}.
227
+ */
228
+ export async function runAttestorTeeVerification(proofs, config = {}) {
229
+ if (!proofs || proofs.length === 0) {
230
+ throw new AttestorTeeVerificationError('No proofs provided for attestor TEE verification');
231
+ }
232
+ try {
233
+ for (const proof of proofs) {
234
+ await verifyAttestorTeeForProof(proof, config);
235
+ }
236
+ }
237
+ catch (error) {
238
+ if (error instanceof AttestorTeeVerificationError) {
239
+ throw error;
240
+ }
241
+ throw new AttestorTeeVerificationError('Attestor TEE attestation verification failed', error);
242
+ }
243
+ }
@@ -0,0 +1,32 @@
1
+ import type { LegacyProof as Proof } from './proof-types.ts';
2
+ export interface TeeAttestationConfig {
3
+ appSecret: string;
4
+ }
5
+ export type TeeVerificationResult = {
6
+ isVerified: boolean;
7
+ error?: string;
8
+ };
9
+ /**
10
+ * Validates the hardware TEE attestation included in the proof.
11
+ * Derives the application ID from `appSecret` and verifies the attestation
12
+ * was generated for your application.
13
+ * Returns a result object with `isVerified` and an optional `error` message.
14
+ *
15
+ * This check is stateless and does not protect against replay of a previously
16
+ * valid proof. Callers must enforce session matching and dedup `sessionId` on
17
+ * their server. See the README's "Replay Protection" section.
18
+ *
19
+ * @param proof - The proof containing TEE attestation data
20
+ * @param appSecret - Your application secret (Ethereum private key). Used to
21
+ * derive the application ID and recompute the attestation nonce.
22
+ */
23
+ export declare function verifyTeeAttestation(proof: Proof, appSecret: string): Promise<TeeVerificationResult>;
24
+ /**
25
+ * Verifies TEE attestation for all proofs.
26
+ * Throws `TeeVerificationError` if any proof is missing TEE data or fails verification.
27
+ *
28
+ * @param proofs - The proofs to verify
29
+ * @param config - TEE attestation configuration containing the app secret
30
+ * @throws {TeeVerificationError} When TEE data is missing or verification fails
31
+ */
32
+ export declare function runTeeVerification(proofs: Proof[], config: TeeAttestationConfig): Promise<void>;