@reclaimprotocol/client 0.1.29 → 0.1.32
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 +255 -234
- package/lib/api.d.ts +2 -0
- package/lib/api.js +2 -0
- package/lib/client.js +2 -0
- package/lib/enckeys.d.ts +1 -1
- package/lib/enckeys.js +2 -2
- package/lib/encoding.d.ts +4 -0
- package/lib/encoding.js +10 -0
- package/lib/feature-flags.d.ts +9 -0
- package/lib/feature-flags.js +19 -0
- package/lib/index.d.ts +4 -4
- package/lib/index.js +4 -4
- package/lib/jwks.d.ts +8 -8
- package/lib/jwks.js +38 -17
- package/lib/launch/destination.d.ts +14 -0
- package/lib/launch/destination.js +89 -0
- package/lib/launch/device.d.ts +30 -0
- package/lib/launch/device.js +168 -0
- package/lib/launch/environment.d.ts +224 -0
- package/lib/launch/environment.js +98 -0
- package/lib/launch/errors.d.ts +20 -0
- package/lib/launch/errors.js +25 -0
- package/lib/launch/extension.d.ts +42 -0
- package/lib/launch/extension.js +72 -0
- package/lib/launch/flow.d.ts +82 -0
- package/lib/launch/flow.js +222 -0
- package/lib/launch/index.d.ts +16 -0
- package/lib/launch/index.js +12 -0
- package/lib/launch/mobile.d.ts +77 -0
- package/lib/launch/mobile.js +124 -0
- package/lib/launch/modal.d.ts +50 -0
- package/lib/launch/modal.js +258 -0
- package/lib/launch/options.d.ts +74 -0
- package/lib/launch/options.js +20 -0
- package/lib/launch/portal.d.ts +28 -0
- package/lib/launch/portal.js +68 -0
- package/lib/launch/qr.d.ts +5 -0
- package/lib/launch/qr.js +28 -0
- package/lib/launch/target.d.ts +46 -0
- package/lib/launch/target.js +128 -0
- package/lib/launch/vendor/qrcodegen.d.ts +134 -0
- package/lib/launch/vendor/qrcodegen.js +822 -0
- package/lib/legacy-proof.d.ts +2 -0
- package/lib/legacy-proof.js +37 -7
- package/lib/proof-hash.d.ts +5 -5
- package/lib/proof-hash.js +1 -1
- package/lib/proof-recipe.d.ts +3 -3
- package/lib/proof-recipe.js +1 -1
- package/lib/proof-types.d.ts +35 -8
- package/lib/reclaim.d.ts +81 -70
- package/lib/reclaim.js +79 -130
- package/lib/request-proof.d.ts +60 -0
- package/lib/request-proof.js +241 -0
- package/lib/result.d.ts +46 -38
- package/lib/result.js +160 -100
- package/lib/routes.gen.d.ts +76 -28
- package/lib/routes.gen.js +24 -11
- package/lib/tee-binding.d.ts +1 -1
- package/lib/types/openapi.d.ts +1 -0
- package/lib/types/openapi.gen.d.ts +1598 -485
- package/lib/types/openapi.gen.js +5 -0
- package/lib/verification-clients.d.ts +28 -14
- package/lib/verification-clients.js +31 -11
- package/lib/verify-attestor-tee.d.ts +19 -2
- package/lib/verify-attestor-tee.js +40 -4
- package/lib/verify-tee.d.ts +10 -1
- package/lib/verify-tee.js +34 -10
- package/package.json +5 -1
- package/src/api.ts +9 -0
- package/src/client.ts +2 -0
- package/src/enckeys.ts +2 -2
- package/src/encoding.ts +12 -0
- package/src/feature-flags.ts +39 -0
- package/src/index.ts +13 -14
- package/src/jwks.ts +52 -30
- package/src/launch/destination.ts +111 -0
- package/src/launch/device.ts +242 -0
- package/src/launch/environment.ts +337 -0
- package/src/launch/errors.ts +49 -0
- package/src/launch/extension.ts +124 -0
- package/src/launch/flow.ts +369 -0
- package/src/launch/index.ts +42 -0
- package/src/launch/mobile.ts +184 -0
- package/src/launch/modal.ts +378 -0
- package/src/launch/options.ts +99 -0
- package/src/launch/portal.ts +104 -0
- package/src/launch/qr.ts +37 -0
- package/src/launch/target.ts +150 -0
- package/src/launch/vendor/qrcodegen.ts +1034 -0
- package/src/proof-hash.ts +6 -6
- package/src/proof-recipe.ts +3 -3
- package/src/proof-types.ts +36 -8
- package/src/reclaim.ts +185 -218
- package/src/{legacy-proof.ts → request-proof.ts} +81 -36
- package/src/result.ts +238 -172
- package/src/routes.gen.ts +24 -11
- package/src/tee-binding.ts +1 -1
- package/src/types/openapi.gen.ts +1603 -485
- package/src/types/openapi.ts +3 -1
- package/src/verification-clients.ts +33 -19
- package/src/verify-attestor-tee.ts +66 -8
- package/src/verify-tee.ts +44 -13
- package/lib/api-client.d.ts +0 -31
- package/lib/api-client.js +0 -112
- package/lib/api-consts.d.ts +0 -4
- package/lib/api-consts.js +0 -4
- package/lib/api-errors.d.ts +0 -13
- package/lib/api-errors.js +0 -30
- package/lib/eip712.d.ts +0 -20
- package/lib/eip712.js +0 -108
package/lib/reclaim.js
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import { ReclaimClient } from "./client.js";
|
|
2
2
|
import { loadDecryptionKey, } from "./enckeys.js";
|
|
3
|
-
import { isProblemError } from "./errors.js";
|
|
4
3
|
import { proofHashesFromBuilderRecipe } from "./proof-recipe.js";
|
|
5
4
|
import { decryptCallback, verifyResultFull, } from "./result.js";
|
|
6
5
|
import { createBuilderTeeBinding, } from "./tee-binding.js";
|
|
7
|
-
/** Terminal lifecycle events selected by default for callback subscriptions. */
|
|
8
|
-
export const TERMINAL_EVENTS = [
|
|
9
|
-
'verification_success',
|
|
10
|
-
'verification_rejected',
|
|
11
|
-
'verification_error',
|
|
12
|
-
'verification_cancelled',
|
|
13
|
-
'session_expired',
|
|
14
|
-
];
|
|
15
6
|
const SIGNED_RESULT_EVENTS = [
|
|
16
7
|
'verification_success',
|
|
17
8
|
'verification_rejected',
|
|
18
9
|
'verification_error',
|
|
19
10
|
];
|
|
20
|
-
export
|
|
11
|
+
export const ReclaimVerification = Object.freeze({
|
|
12
|
+
create: createReclaimVerification,
|
|
13
|
+
import: importReclaimVerification,
|
|
14
|
+
});
|
|
15
|
+
function importReclaimVerification(serialized, config) {
|
|
16
|
+
const state = parseReclaimVerificationState(serialized);
|
|
17
|
+
return createReclaimVerification({ ...config, baseUrl: state.baseUrl });
|
|
18
|
+
}
|
|
19
|
+
function createReclaimVerification(config) {
|
|
21
20
|
if (!config?.orgSecret) {
|
|
22
|
-
throw new Error('
|
|
23
|
-
+ '
|
|
21
|
+
throw new Error('ReclaimVerification.create requires an org secret — pass '
|
|
22
|
+
+ 'ReclaimVerification.create({ orgSecret: <rorg_… org secret> })');
|
|
24
23
|
}
|
|
25
24
|
const client = new ReclaimClient({
|
|
26
25
|
baseUrl: config.baseUrl,
|
|
@@ -32,99 +31,42 @@ export function createReclaim(config) {
|
|
|
32
31
|
// plaintext-only consumers never touch the loader.
|
|
33
32
|
const resolveKey = (input) => (input === undefined ? undefined : loadDecryptionKey(input));
|
|
34
33
|
return {
|
|
34
|
+
export() {
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
version: 1,
|
|
37
|
+
baseUrl: client.baseUrl,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
35
40
|
api: client.call.bind(client),
|
|
36
41
|
sessions: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const body = {
|
|
40
|
-
...request,
|
|
41
|
-
verificationClientUrl: request.verificationClientUrl ?? `${client.baseUrl}/v/{sessionId}`,
|
|
42
|
-
};
|
|
43
|
-
const { data } = await client.call('CreateVerificationSession', {
|
|
44
|
-
body,
|
|
45
|
-
});
|
|
46
|
-
return teeAttestation
|
|
47
|
-
? bindTeeNonce(data, teeAttestation)
|
|
48
|
-
: data;
|
|
49
|
-
},
|
|
50
|
-
async bindTeeNonce(sessionId, input) {
|
|
42
|
+
create: createSession,
|
|
43
|
+
async bindTeeNonce(reclaimSessionId, input) {
|
|
51
44
|
const { data } = await client.call('GetVerificationSession', {
|
|
52
|
-
params: { sessionId },
|
|
45
|
+
params: { sessionId: reclaimSessionId },
|
|
53
46
|
});
|
|
54
47
|
return bindTeeNonce(data, input);
|
|
55
48
|
},
|
|
56
|
-
async get(
|
|
49
|
+
async get(reclaimSessionId) {
|
|
57
50
|
const { data } = await client.call('GetVerificationSession', {
|
|
58
|
-
params: { sessionId },
|
|
51
|
+
params: { sessionId: reclaimSessionId },
|
|
59
52
|
});
|
|
60
53
|
return data;
|
|
61
54
|
},
|
|
62
|
-
async listEvents(
|
|
55
|
+
async listEvents(reclaimSessionId) {
|
|
63
56
|
const { data } = await client.call('ListVerificationEvents', {
|
|
64
|
-
params: { sessionId },
|
|
57
|
+
params: { sessionId: reclaimSessionId },
|
|
65
58
|
});
|
|
66
59
|
return data.items;
|
|
67
60
|
},
|
|
68
61
|
},
|
|
69
|
-
callbacks: {
|
|
70
|
-
async register(input) {
|
|
71
|
-
const events = input.events ?? [...TERMINAL_EVENTS];
|
|
72
|
-
const { data } = await client.call('CreateCallbackSubscription', {
|
|
73
|
-
body: { callbackUrl: input.callbackUrl, events },
|
|
74
|
-
});
|
|
75
|
-
return data;
|
|
76
|
-
},
|
|
77
|
-
async list() {
|
|
78
|
-
const { data } = await client.call('ListCallbackSubscriptions');
|
|
79
|
-
return data.items;
|
|
80
|
-
},
|
|
81
|
-
async remove(subscriptionId) {
|
|
82
|
-
await client.call('DeleteCallbackSubscription', {
|
|
83
|
-
params: { subscriptionId },
|
|
84
|
-
});
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
keypair: {
|
|
88
|
-
async set(orgId, input) {
|
|
89
|
-
// Forward the eth public key as given (hex or JWK); the API stores
|
|
90
|
-
// the secp256k1 JWK + derived address.
|
|
91
|
-
const body = {
|
|
92
|
-
...(input.publicKeyJwk
|
|
93
|
-
? { publicKeyJwk: input.publicKeyJwk }
|
|
94
|
-
: { publicKey: input.publicKey }),
|
|
95
|
-
canEncryptResult: input.canEncryptResult ?? false,
|
|
96
|
-
...(input.label ? { label: input.label } : {}),
|
|
97
|
-
};
|
|
98
|
-
const { data } = await client.call('SetOrgKeypair', {
|
|
99
|
-
params: { orgId },
|
|
100
|
-
body,
|
|
101
|
-
});
|
|
102
|
-
return data;
|
|
103
|
-
},
|
|
104
|
-
async get(orgId) {
|
|
105
|
-
try {
|
|
106
|
-
const { data } = await client.call('GetOrgKeypair', {
|
|
107
|
-
params: { orgId },
|
|
108
|
-
});
|
|
109
|
-
return data;
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
// 404 ⇒ keyless org (plaintext delivery).
|
|
113
|
-
if (isNotFound(err)) {
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
62
|
results: {
|
|
121
|
-
async decrypt(body,
|
|
122
|
-
return decryptCallback(resolveKey(
|
|
63
|
+
async decrypt(body, orgEthPrivateKey) {
|
|
64
|
+
return decryptCallback(resolveKey(orgEthPrivateKey), body);
|
|
123
65
|
},
|
|
124
66
|
verify(jws, opts) {
|
|
125
67
|
return verifyResultFull(jws, {
|
|
126
68
|
fetchImpl: config.fetch,
|
|
127
|
-
...
|
|
69
|
+
...withBuilderTrust(opts),
|
|
128
70
|
});
|
|
129
71
|
},
|
|
130
72
|
async receive(body, opts) {
|
|
@@ -145,24 +87,44 @@ export function createReclaim(config) {
|
|
|
145
87
|
};
|
|
146
88
|
}
|
|
147
89
|
}
|
|
148
|
-
const {
|
|
149
|
-
const key = resolveKey(
|
|
90
|
+
const { orgEthPrivateKey, ...verifyOpts } = opts ?? {};
|
|
91
|
+
const key = resolveKey(orgEthPrivateKey);
|
|
150
92
|
const jws = await decryptCallback(key, body);
|
|
151
93
|
const result = await verifyResultFull(jws, {
|
|
152
94
|
fetchImpl: config.fetch,
|
|
153
|
-
...
|
|
95
|
+
...withBuilderTrust({ ...verifyOpts, orgEthPrivateKey }),
|
|
154
96
|
});
|
|
155
97
|
return { kind: 'result', result };
|
|
156
98
|
},
|
|
157
99
|
},
|
|
100
|
+
flow: Object.assign((session) => makeSessionFlow(session), {
|
|
101
|
+
async create(input) {
|
|
102
|
+
return makeSessionFlow(await createSession(input));
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
158
105
|
};
|
|
106
|
+
async function createSession(input) {
|
|
107
|
+
const { orgEthPrivateKey, teeNonceMode, ...request } = input;
|
|
108
|
+
const body = {
|
|
109
|
+
...request,
|
|
110
|
+
};
|
|
111
|
+
const { data } = await client.call('CreateVerificationSession', {
|
|
112
|
+
body,
|
|
113
|
+
});
|
|
114
|
+
return orgEthPrivateKey
|
|
115
|
+
? bindTeeNonce(data, {
|
|
116
|
+
orgEthPrivateKey,
|
|
117
|
+
teeNonceMode,
|
|
118
|
+
})
|
|
119
|
+
: data;
|
|
120
|
+
}
|
|
159
121
|
async function bindTeeNonce(session, input) {
|
|
160
122
|
const binding = createBuilderTeeBinding({
|
|
161
123
|
organizationId: session.orgId,
|
|
162
|
-
organizationVerificationPrivateKey: input.
|
|
124
|
+
organizationVerificationPrivateKey: input.orgEthPrivateKey,
|
|
163
125
|
sessionId: session.id,
|
|
164
126
|
timestamp: input.timestamp ?? Date.now().toString(),
|
|
165
|
-
nonceMode: input.
|
|
127
|
+
nonceMode: input.teeNonceMode,
|
|
166
128
|
});
|
|
167
129
|
const { data } = await client.call('BindVerificationSessionTeeNonce', {
|
|
168
130
|
params: { sessionId: session.id },
|
|
@@ -202,10 +164,9 @@ export function createReclaim(config) {
|
|
|
202
164
|
}, proofs),
|
|
203
165
|
}];
|
|
204
166
|
}
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
const requestedProofValidation = opts.proofValidation
|
|
208
|
-
?? opts.proofVerification;
|
|
167
|
+
function withBuilderTrust(opts = {}) {
|
|
168
|
+
const reclaimSessionId = opts.reclaimSessionId;
|
|
169
|
+
const requestedProofValidation = opts.proofValidation;
|
|
209
170
|
const resolvedOpts = {
|
|
210
171
|
...opts,
|
|
211
172
|
proofValidation: {
|
|
@@ -213,51 +174,30 @@ export function createReclaim(config) {
|
|
|
213
174
|
resolveProvider: requestedProofValidation?.resolveProvider
|
|
214
175
|
?? resolvedProvider,
|
|
215
176
|
},
|
|
216
|
-
...(
|
|
177
|
+
...(reclaimSessionId && !opts.resolveSession && !opts.proofRecipes
|
|
217
178
|
? { resolveSession: resolvedSession }
|
|
218
179
|
: {}),
|
|
219
180
|
};
|
|
220
181
|
if (opts.signerPublicKey
|
|
221
|
-
|| opts.issuer
|
|
222
|
-
|| opts.trustedIssuer
|
|
223
|
-
|| opts.issuers?.length
|
|
224
182
|
|| opts.trustedIssuers?.length
|
|
225
|
-
|| opts.
|
|
226
|
-
|| opts.issuerResolver
|
|
227
|
-
|| opts.issuersByVerificationClientId
|
|
228
|
-
|| opts.jwksUrl
|
|
229
|
-
|| opts.jwksUrlsByVerificationClientId
|
|
230
|
-
|| opts.resolveJwksUrl) {
|
|
183
|
+
|| opts.jwksUrl) {
|
|
231
184
|
return resolvedOpts;
|
|
232
185
|
}
|
|
233
186
|
return {
|
|
234
187
|
...resolvedOpts,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (!session) {
|
|
238
|
-
return [];
|
|
239
|
-
}
|
|
240
|
-
return session.verificationClientIssuer
|
|
241
|
-
? [session.verificationClientIssuer]
|
|
242
|
-
: [];
|
|
243
|
-
},
|
|
244
|
-
resolveJwksUrl: async (verificationClientId) => {
|
|
245
|
-
const session = await registeredClient(verificationClientId);
|
|
246
|
-
return session?.verificationClientJwksUrl;
|
|
247
|
-
},
|
|
188
|
+
trustedIssuers: [client.baseUrl],
|
|
189
|
+
jwksUrl: `${client.baseUrl}/.well-known/jwks.json`,
|
|
248
190
|
};
|
|
249
|
-
async function registeredClient(verificationClientId) {
|
|
250
|
-
if (!sessionId) {
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
const { data: session } = await client.call('GetVerificationSession', { params: { sessionId } });
|
|
254
|
-
if (session.verificationClientId !== verificationClientId) {
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
return session;
|
|
258
|
-
}
|
|
259
191
|
}
|
|
260
192
|
}
|
|
193
|
+
function makeSessionFlow(session) {
|
|
194
|
+
return {
|
|
195
|
+
session,
|
|
196
|
+
async getUrl() {
|
|
197
|
+
return session.verificationUrl;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
261
201
|
function canonicalVersion(version) {
|
|
262
202
|
const base = `${version.major}.${version.minor}.${version.patch}`;
|
|
263
203
|
return version.branch
|
|
@@ -279,7 +219,16 @@ function parseEventData(data) {
|
|
|
279
219
|
return {};
|
|
280
220
|
}
|
|
281
221
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
222
|
+
function parseReclaimVerificationState(serialized) {
|
|
223
|
+
const state = JSON.parse(serialized);
|
|
224
|
+
if (!state
|
|
225
|
+
|| typeof state !== 'object'
|
|
226
|
+
|| Array.isArray(state)
|
|
227
|
+
|| !('version' in state)
|
|
228
|
+
|| state.version !== 1
|
|
229
|
+
|| !('baseUrl' in state)
|
|
230
|
+
|| typeof state.baseUrl !== 'string') {
|
|
231
|
+
throw new Error('Invalid ReclaimVerification state');
|
|
232
|
+
}
|
|
233
|
+
return { version: 1, baseUrl: state.baseUrl };
|
|
285
234
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { HashRequirement, ReclaimRequestProof, TrustedData } from './proof-types.ts';
|
|
2
|
+
import { type AttestorTeeAttestationConfig } from './verify-attestor-tee.ts';
|
|
3
|
+
import { type TeeAttestationConfig } from './verify-tee.ts';
|
|
4
|
+
export type { ReclaimRequestProof } from './proof-types.ts';
|
|
5
|
+
/** A proof hash requirement accepted by `verifyRequestProof`. */
|
|
6
|
+
export type ProofHashRequirement = string | HashRequirement;
|
|
7
|
+
export interface ResolvedSessionProofValidation {
|
|
8
|
+
hashes?: ProofHashRequirement[];
|
|
9
|
+
providerId?: string;
|
|
10
|
+
providerVersion?: string;
|
|
11
|
+
allowedTags?: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Exact provider requirements returned by a caller-controlled resolver. */
|
|
14
|
+
export interface ResolvedProviderProofValidation {
|
|
15
|
+
hashes: ProofHashRequirement[];
|
|
16
|
+
}
|
|
17
|
+
/** Field names match the request spec a provider recipe declares, so a
|
|
18
|
+
* caller configuring verification reads the same vocabulary it authored. */
|
|
19
|
+
export interface VerifyRequestProofOptions {
|
|
20
|
+
hashes?: ProofHashRequirement[];
|
|
21
|
+
providerId?: string;
|
|
22
|
+
providerVersion?: string;
|
|
23
|
+
allowedTags?: string[];
|
|
24
|
+
dangerouslyDisableContentValidation?: true;
|
|
25
|
+
/** Read `providerHash` from context, while still verifying signatures. */
|
|
26
|
+
hasNoPii?: true;
|
|
27
|
+
teeAttestation?: TeeAttestationConfig;
|
|
28
|
+
attestorTeeAttestation?: AttestorTeeAttestationConfig;
|
|
29
|
+
/** Explicit trusted attestor ETH addresses. */
|
|
30
|
+
attestorAddresses?: string[];
|
|
31
|
+
/** Trusted Builder JWKS registry containing attestor-purpose keys. */
|
|
32
|
+
attestorJwksUrl?: string;
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
/** Builder session id. Requires a resolver without hashes or a provider. */
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
resolveSession?: (sessionId: string, proofs: ReclaimRequestProof[]) => Promise<ResolvedSessionProofValidation> | ResolvedSessionProofValidation;
|
|
37
|
+
/** Resolve an exact provider without coupling this verifier to HTTP. */
|
|
38
|
+
resolveProvider?: (providerId: string, providerVersion: string | undefined, allowedTags: string[] | undefined, proofs: ReclaimRequestProof[]) => Promise<ResolvedProviderProofValidation[]> | ResolvedProviderProofValidation[];
|
|
39
|
+
expectedSessionId?: string;
|
|
40
|
+
requireSessionBinding?: boolean;
|
|
41
|
+
expectedContext?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface ReclaimRequestProofOutcome {
|
|
44
|
+
valid: boolean;
|
|
45
|
+
reason?: string;
|
|
46
|
+
proofHashes?: string[];
|
|
47
|
+
isTeeAttestationVerified?: boolean;
|
|
48
|
+
/** True when at least one witness's `attestation_report` verified; false
|
|
49
|
+
* when the check ran and no witness carried one. */
|
|
50
|
+
isAttestorTeeAttestationVerified?: boolean;
|
|
51
|
+
data?: TrustedData[];
|
|
52
|
+
publicData?: unknown[];
|
|
53
|
+
}
|
|
54
|
+
/** Return the provider hashes encoded in an exact request proof. */
|
|
55
|
+
export declare function proofHashes(proof: ReclaimRequestProof): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Verify exact request proofs without depending on `@reclaimprotocol/js-sdk`.
|
|
58
|
+
* The signed claim is never renamed or reconstructed.
|
|
59
|
+
*/
|
|
60
|
+
export declare function verifyRequestProof(proofOrProofs: ReclaimRequestProof | ReclaimRequestProof[], config: VerifyRequestProofOptions): Promise<ReclaimRequestProofOutcome>;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createSignDataForClaim } from '@reclaimprotocol/attestor-core';
|
|
2
|
+
import { recoverEip191Address } from "./eip191.js";
|
|
3
|
+
import { fetchReclaimJwks, trustedAttestorAddresses } from "./jwks.js";
|
|
4
|
+
import { assertValidProofHashes, getHashFromProof } from "./proof-hash.js";
|
|
5
|
+
import { runAttestorTeeVerification, } from "./verify-attestor-tee.js";
|
|
6
|
+
import { runTeeVerification } from "./verify-tee.js";
|
|
7
|
+
/** Return the provider hashes encoded in an exact request proof. */
|
|
8
|
+
export function proofHashes(proof) {
|
|
9
|
+
return getHashFromProof(proof);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verify exact request proofs without depending on `@reclaimprotocol/js-sdk`.
|
|
13
|
+
* The signed claim is never renamed or reconstructed.
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyRequestProof(proofOrProofs, config) {
|
|
16
|
+
const proofs = Array.isArray(proofOrProofs) ? proofOrProofs : [proofOrProofs];
|
|
17
|
+
if (!proofs.length) {
|
|
18
|
+
return { valid: false, reason: 'no-proofs' };
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const effective = await resolveValidation(config, proofs);
|
|
22
|
+
assertSessionAndContext(proofs, effective);
|
|
23
|
+
const trust = await resolveAttestorTrust(effective);
|
|
24
|
+
assertVerifiedSignatures(proofs, trust.addresses);
|
|
25
|
+
await assertContent(proofs, effective);
|
|
26
|
+
let isTeeAttestationVerified;
|
|
27
|
+
if (effective.teeAttestation) {
|
|
28
|
+
await runTeeVerification(proofs, effective.teeAttestation);
|
|
29
|
+
isTeeAttestationVerified = true;
|
|
30
|
+
}
|
|
31
|
+
let isAttestorTeeAttestationVerified;
|
|
32
|
+
const attestorTeeAttestation = resolveAttestorTeeAttestation(effective.attestorTeeAttestation, trust.imageDigests);
|
|
33
|
+
if (attestorTeeAttestation) {
|
|
34
|
+
// `verified` counts witnesses whose attestation report was checked.
|
|
35
|
+
// A proof whose witnesses carry no report still passes the address
|
|
36
|
+
// and signature bindings, but nothing was attested — say so rather
|
|
37
|
+
// than reporting a TEE attestation that never happened.
|
|
38
|
+
const summary = await runAttestorTeeVerification(proofs, attestorTeeAttestation);
|
|
39
|
+
isAttestorTeeAttestationVerified = summary.verified > 0;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
valid: true,
|
|
43
|
+
proofHashes: collectProofHashes(proofs, effective.hasNoPii),
|
|
44
|
+
isTeeAttestationVerified,
|
|
45
|
+
isAttestorTeeAttestationVerified,
|
|
46
|
+
data: proofs.map(trustedData),
|
|
47
|
+
publicData: proofs.map((proof) => proof.publicData ?? {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
reason: failureReason(error),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function resolveValidation(config, proofs) {
|
|
58
|
+
let effective = config;
|
|
59
|
+
if (config.sessionId && !hasContentValidation(config)) {
|
|
60
|
+
if (!config.resolveSession) {
|
|
61
|
+
throw new Error('session-resolver-required');
|
|
62
|
+
}
|
|
63
|
+
effective = {
|
|
64
|
+
...config,
|
|
65
|
+
...await config.resolveSession(config.sessionId, proofs),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return effective;
|
|
69
|
+
}
|
|
70
|
+
async function resolveAttestorTrust(config) {
|
|
71
|
+
const addresses = config.attestorAddresses?.map(normalized) ?? [];
|
|
72
|
+
const imageDigests = [];
|
|
73
|
+
if (config.attestorJwksUrl) {
|
|
74
|
+
const registry = await fetchReclaimJwks(config.attestorJwksUrl, config.fetchImpl);
|
|
75
|
+
addresses.push(...(registry['x-reclaim-attestors'] ?? []), ...trustedAttestorAddresses(registry.keys));
|
|
76
|
+
imageDigests.push(...(registry['x-reclaim-attestor-image-digests'] ?? []));
|
|
77
|
+
}
|
|
78
|
+
const unique = [...new Set(addresses)];
|
|
79
|
+
if (!unique.length) {
|
|
80
|
+
throw new Error('attestor-trust-required');
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
addresses: unique,
|
|
84
|
+
imageDigests: [...new Set(imageDigests)],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function resolveAttestorTeeAttestation(configured, registryImageDigests) {
|
|
88
|
+
if (!registryImageDigests.length) {
|
|
89
|
+
return configured;
|
|
90
|
+
}
|
|
91
|
+
const configuredDigests = configured?.expectedImageDigests;
|
|
92
|
+
if (configuredDigests?.length) {
|
|
93
|
+
const registry = new Set(registryImageDigests);
|
|
94
|
+
const expectedImageDigests = [...new Set(configuredDigests
|
|
95
|
+
.map((digest) => digest.trim().toLowerCase())
|
|
96
|
+
.filter((digest) => registry.has(digest)))];
|
|
97
|
+
if (!expectedImageDigests.length) {
|
|
98
|
+
throw new Error('attestor-image-trust-mismatch');
|
|
99
|
+
}
|
|
100
|
+
return { ...configured, expectedImageDigests };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...configured,
|
|
104
|
+
expectedImageDigests: registryImageDigests,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function assertVerifiedSignatures(proofs, trustedAttestors) {
|
|
108
|
+
for (const proof of proofs) {
|
|
109
|
+
if (!proof.signatures?.length || !proof.witnesses?.length) {
|
|
110
|
+
throw new Error('proof-signature-missing');
|
|
111
|
+
}
|
|
112
|
+
const signData = createSignDataForClaim(proof.claimData);
|
|
113
|
+
const signers = proof.signatures.map((signature) => (normalized(recoverEip191Address(signData, signature))));
|
|
114
|
+
const witnesses = proof.witnesses.map(({ id }) => normalized(id));
|
|
115
|
+
if (witnesses.some((witness) => !trustedAttestors.includes(witness))) {
|
|
116
|
+
throw new Error('attestor-not-allowed');
|
|
117
|
+
}
|
|
118
|
+
if (!signers.some((signer) => trustedAttestors.includes(signer))) {
|
|
119
|
+
throw new Error('attestor-not-allowed');
|
|
120
|
+
}
|
|
121
|
+
if (!signers.every((signer) => witnesses.includes(signer))) {
|
|
122
|
+
throw new Error('witness-signature-mismatch');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function assertContent(proofs, config) {
|
|
127
|
+
if (config.dangerouslyDisableContentValidation) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (config.hashes) {
|
|
131
|
+
assertValidProofHashes(proofs, config.hashes, config.hasNoPii);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (config.providerId) {
|
|
135
|
+
if (!config.resolveProvider) {
|
|
136
|
+
throw new Error('provider-resolver-required');
|
|
137
|
+
}
|
|
138
|
+
const candidates = await config.resolveProvider(config.providerId, config.providerVersion, config.allowedTags, proofs);
|
|
139
|
+
if (!candidates.length) {
|
|
140
|
+
throw new Error('provider-not-found');
|
|
141
|
+
}
|
|
142
|
+
let lastError;
|
|
143
|
+
for (const candidate of candidates) {
|
|
144
|
+
try {
|
|
145
|
+
assertValidProofHashes(proofs, candidate.hashes, config.hasNoPii);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
lastError = error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw lastError;
|
|
153
|
+
}
|
|
154
|
+
throw new Error('proof-validation-required');
|
|
155
|
+
}
|
|
156
|
+
function assertSessionAndContext(proofs, config) {
|
|
157
|
+
const expectedSessionId = config.sessionId ?? config.expectedSessionId;
|
|
158
|
+
for (const proof of proofs) {
|
|
159
|
+
if (expectedSessionId) {
|
|
160
|
+
const ids = sessionIds(proof);
|
|
161
|
+
if (ids.some((id) => id !== expectedSessionId)) {
|
|
162
|
+
throw new Error('session-mismatch');
|
|
163
|
+
}
|
|
164
|
+
if ((config.sessionId || config.requireSessionBinding) && !ids.length) {
|
|
165
|
+
throw new Error('session-missing');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!contextMatches(proof, config.expectedContext)) {
|
|
169
|
+
throw new Error('context-mismatch');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function trustedData(proof) {
|
|
174
|
+
const context = contextFrom(proof);
|
|
175
|
+
const extractedParameters = isRecord(context.extractedParameters)
|
|
176
|
+
? Object.fromEntries(Object.entries(context.extractedParameters).map(([key, value]) => [key, String(value)]))
|
|
177
|
+
: {};
|
|
178
|
+
delete context.extractedParameters;
|
|
179
|
+
return { context, extractedParameters };
|
|
180
|
+
}
|
|
181
|
+
function collectProofHashes(proofs, hasNoPii) {
|
|
182
|
+
try {
|
|
183
|
+
return proofs.flatMap((proof) => getHashFromProof(proof, hasNoPii));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function hasContentValidation(config) {
|
|
190
|
+
return Boolean(config.hashes
|
|
191
|
+
|| config.providerId
|
|
192
|
+
|| config.dangerouslyDisableContentValidation);
|
|
193
|
+
}
|
|
194
|
+
function failureReason(error) {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
const lower = message.toLowerCase();
|
|
197
|
+
if (lower.includes('multiple')
|
|
198
|
+
|| lower.includes('duplicate')
|
|
199
|
+
|| lower.includes('more than once')) {
|
|
200
|
+
return 'duplicate-proof-hash';
|
|
201
|
+
}
|
|
202
|
+
return message || 'proof-not-verified';
|
|
203
|
+
}
|
|
204
|
+
function normalized(value) {
|
|
205
|
+
return value.trim().toLowerCase();
|
|
206
|
+
}
|
|
207
|
+
function contextFrom(proof) {
|
|
208
|
+
const raw = proof.claimData.context;
|
|
209
|
+
if (!raw) {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const context = JSON.parse(raw);
|
|
214
|
+
return isRecord(context) ? { ...context } : {};
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function sessionIds(proof) {
|
|
221
|
+
const context = contextFrom(proof);
|
|
222
|
+
const ids = [context.reclaimSessionId];
|
|
223
|
+
try {
|
|
224
|
+
const parameters = JSON.parse(proof.claimData.parameters);
|
|
225
|
+
ids.push(parameters.proxySessionId, parameters.sessionId);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// A non-HTTP claim is valid when session binding wasn't requested.
|
|
229
|
+
}
|
|
230
|
+
return ids.filter((value) => (typeof value === 'string' && value.length > 0));
|
|
231
|
+
}
|
|
232
|
+
function contextMatches(proof, expected) {
|
|
233
|
+
if (!expected) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
const context = contextFrom(proof);
|
|
237
|
+
return Object.entries(expected).every(([key, value]) => (JSON.stringify(context[key]) === JSON.stringify(value)));
|
|
238
|
+
}
|
|
239
|
+
function isRecord(value) {
|
|
240
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
241
|
+
}
|