@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/src/result.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* - {@link verifyResult}: the offline primitive — decrypt + decode + optional
|
|
4
4
|
* ES256K signature check (caller-supplied key) + `reclaimSessionId` nonce.
|
|
5
5
|
* - {@link verifyResultFull}: the recommended server-side verification —
|
|
6
|
-
* resolves
|
|
6
|
+
* resolves Builder's trusted issuer, discovers Builder's key
|
|
7
7
|
* via JWKS, checks
|
|
8
8
|
* `reclaimSessionId`/`aud` and any legacy `exp`, and passes every exact
|
|
9
|
-
*
|
|
9
|
+
* proof to the in-package `verifyRequestProof` verifier.
|
|
10
10
|
*
|
|
11
11
|
* See docs/verification-session.md → "Verification by Consumer".
|
|
12
12
|
*/
|
|
@@ -14,26 +14,35 @@ import { sha256 } from '@noble/hashes/sha2.js'
|
|
|
14
14
|
import { decrypt as eciesDecrypt, encrypt as eciesEncrypt } from 'eciesjs'
|
|
15
15
|
import type { components } from './types/openapi.gen.ts'
|
|
16
16
|
import type { DecryptionKey } from './enckeys.ts'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
b64url,
|
|
19
|
+
base64urlToBytes,
|
|
20
|
+
hexToBytes,
|
|
21
|
+
stripTrailingSlashes,
|
|
22
|
+
} from './encoding.ts'
|
|
18
23
|
import {
|
|
19
24
|
fetchReclaimJwks,
|
|
20
25
|
type Jwk,
|
|
21
26
|
jwkToUncompressedPublicKey,
|
|
22
27
|
selectVerificationResultKey,
|
|
23
28
|
} from './jwks.ts'
|
|
24
|
-
import {
|
|
25
|
-
type LegacyProof,
|
|
26
|
-
type LegacyVerifyProofOptions,
|
|
27
|
-
type ProofHashRequirement,
|
|
28
|
-
verifyProof,
|
|
29
|
-
} from './legacy-proof.ts'
|
|
29
|
+
import { getHashFromProof } from './proof-hash.ts'
|
|
30
30
|
import {
|
|
31
31
|
type BuilderProofRecipe,
|
|
32
32
|
proofHashesFromBuilderRecipe,
|
|
33
33
|
} from './proof-recipe.ts'
|
|
34
34
|
import type { TrustedData } from './proof-types.ts'
|
|
35
|
+
import {
|
|
36
|
+
type ProofHashRequirement,
|
|
37
|
+
type ReclaimRequestProof,
|
|
38
|
+
verifyRequestProof,
|
|
39
|
+
type VerifyRequestProofOptions,
|
|
40
|
+
} from './request-proof.ts'
|
|
35
41
|
import { addressFromUncompressedPubkey, secp } from './secp.ts'
|
|
36
42
|
|
|
43
|
+
/** Longest `iss` worth comparing. Real issuers are short origins. */
|
|
44
|
+
const MAX_ISSUER_LENGTH = 2048
|
|
45
|
+
|
|
37
46
|
export interface Jws {
|
|
38
47
|
payload: string
|
|
39
48
|
signatures: {
|
|
@@ -49,12 +58,10 @@ export interface VerifyResultOptions {
|
|
|
49
58
|
signerPublicKey?: string
|
|
50
59
|
/** The `reclaimSessionId` the consumer expects (the session id they got
|
|
51
60
|
* back at creation). When given, mismatch fails verification. */
|
|
52
|
-
|
|
61
|
+
reclaimSessionId?: string
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export interface VerifyResultOutcome {
|
|
56
|
-
valid: boolean
|
|
57
|
-
reason?: string
|
|
58
65
|
payload: Record<string, unknown>
|
|
59
66
|
reclaimSessionId?: string
|
|
60
67
|
}
|
|
@@ -161,9 +168,10 @@ export async function decryptCallback(
|
|
|
161
168
|
// Otherwise it's an ECIES ciphertext (base64url).
|
|
162
169
|
if(!privateKey) {
|
|
163
170
|
throw new Error(
|
|
164
|
-
'received an encrypted (ECIES) delivery but no
|
|
165
|
-
+ '
|
|
166
|
-
+ 'results.receive(body, {
|
|
171
|
+
'received an encrypted (ECIES) delivery but no org ETH private key was '
|
|
172
|
+
+ 'provided — pass the 0x-hex private key: '
|
|
173
|
+
+ 'results.receive(body, { orgEthPrivateKey }) or '
|
|
174
|
+
+ 'decrypt(body, orgEthPrivateKey)',
|
|
167
175
|
)
|
|
168
176
|
}
|
|
169
177
|
|
|
@@ -175,7 +183,7 @@ export async function decryptCallback(
|
|
|
175
183
|
* Sign a verification result as a flattened JWS (JOSE ES256K = ECDSA/secp256k1
|
|
176
184
|
* over SHA-256 of the `<protected>.<payload>` signing input — a single hash).
|
|
177
185
|
* `kid` is the signer's ETH address (published via JWKS); `iss` is the issuer
|
|
178
|
-
* URL. Used by
|
|
186
|
+
* URL. Used by Builder to sign results before encrypting them.
|
|
179
187
|
*/
|
|
180
188
|
export function signResultJws(
|
|
181
189
|
privateKeyHex: string,
|
|
@@ -219,6 +227,13 @@ export function verifyJwsSignature(jws: Jws, signerPublicKey: string): boolean {
|
|
|
219
227
|
const pub = hexToBytes(signerPublicKey)
|
|
220
228
|
return jws.signatures.some((s) => {
|
|
221
229
|
try {
|
|
230
|
+
const header = JSON.parse(
|
|
231
|
+
new TextDecoder().decode(base64urlToBytes(s.protected)),
|
|
232
|
+
) as Record<string, unknown>
|
|
233
|
+
if(header.alg !== 'ES256K') {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
222
237
|
const signingInput = new TextEncoder().encode(
|
|
223
238
|
`${s.protected}.${jws.payload}`
|
|
224
239
|
)
|
|
@@ -234,44 +249,27 @@ export function verifyJwsSignature(jws: Jws, signerPublicKey: string): boolean {
|
|
|
234
249
|
}
|
|
235
250
|
|
|
236
251
|
export interface VerifyResultFullOptions {
|
|
237
|
-
/** Builder session id. Alias for `expectedReclaimSessionId`. */
|
|
238
|
-
sessionId?: string
|
|
239
252
|
/** The `reclaimSessionId` the consumer expects (mismatch fails). */
|
|
240
|
-
|
|
253
|
+
reclaimSessionId?: string
|
|
254
|
+
/** Optional organization ETH private key used to verify legacy TEE binding.
|
|
255
|
+
* Callers must provide it for each verification; it isn't retained. */
|
|
256
|
+
orgEthPrivateKey?: string
|
|
241
257
|
/** The consumer's `orgId`; the result's `aud` must match it. */
|
|
242
258
|
expectedAud?: string
|
|
243
259
|
/** Allowlist of trusted attestor ETH addresses (lowercased compared). */
|
|
244
260
|
attestorAddresses?: string[]
|
|
245
|
-
/**
|
|
246
|
-
issuer?: string
|
|
247
|
-
/** Alias for `issuer`. */
|
|
248
|
-
trustedIssuer?: string
|
|
249
|
-
/** Explicitly trusted Verification Client issuers. */
|
|
250
|
-
issuers?: string[]
|
|
251
|
-
/** Alias for `issuers`, retained for security-focused callers. */
|
|
261
|
+
/** Explicitly trusted Builder issuers. */
|
|
252
262
|
trustedIssuers?: string[]
|
|
253
|
-
/**
|
|
254
|
-
resolveIssuers?: (verificationClientId: string) =>
|
|
255
|
-
Promise<readonly string[]> | readonly string[]
|
|
256
|
-
/** Alias for `resolveIssuers`. */
|
|
257
|
-
issuerResolver?: (verificationClientId: string) =>
|
|
258
|
-
Promise<readonly string[]> | readonly string[]
|
|
259
|
-
/** Registered issuer URLs keyed by `verificationClientId`. */
|
|
260
|
-
issuersByVerificationClientId?: Record<string, string[]>
|
|
261
|
-
/** Explicit trusted JWKS URL. This value must come from your registry. */
|
|
263
|
+
/** Explicit trusted Builder JWKS URL. */
|
|
262
264
|
jwksUrl?: string
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
/** Legacy-compatible proof validation options applied to all proofs. */
|
|
269
|
-
proofValidation?: LegacyVerifyProofOptions
|
|
270
|
-
/** Alias for `proofValidation`. */
|
|
271
|
-
proofVerification?: LegacyVerifyProofOptions
|
|
265
|
+
/**
|
|
266
|
+
* Proof validation options applied to all proofs.
|
|
267
|
+
* `requireSessionBinding` defaults to true when an expected session is set.
|
|
268
|
+
*/
|
|
269
|
+
proofValidation?: VerifyRequestProofOptions
|
|
272
270
|
/** Per-provider overrides for sessions containing different providers. */
|
|
273
|
-
proofValidationByProviderId?: Record<string,
|
|
274
|
-
/**
|
|
271
|
+
proofValidationByProviderId?: Record<string, VerifyRequestProofOptions>
|
|
272
|
+
/** Hash requirements, for the common simple case. */
|
|
275
273
|
hashes?: ProofHashRequirement[]
|
|
276
274
|
/** Resolved Builder recipes, used to derive strict proof hashes offline. */
|
|
277
275
|
proofRecipes?: BuilderProofRecipe[]
|
|
@@ -280,7 +278,8 @@ export interface VerifyResultFullOptions {
|
|
|
280
278
|
sessionId: string,
|
|
281
279
|
) => Promise<{ recipes: BuilderProofRecipe[] }>
|
|
282
280
|
| { recipes: BuilderProofRecipe[] }
|
|
283
|
-
/** Override
|
|
281
|
+
/** Override Builder's signer key instead of JWKS discovery.
|
|
282
|
+
* The header `iss` is otherwise used only after trust checks. */
|
|
284
283
|
signerPublicKey?: string
|
|
285
284
|
/** Injectable fetch (for JWKS); defaults to global `fetch`. */
|
|
286
285
|
fetchImpl?: typeof fetch
|
|
@@ -297,10 +296,28 @@ export interface VerifyResultFullOptions {
|
|
|
297
296
|
export interface ResultProofOutcome {
|
|
298
297
|
/** Which provider this proof belongs to (results grouped per provider). */
|
|
299
298
|
providerId?: string
|
|
299
|
+
/** Exact immutable provider version used to validate this proof. */
|
|
300
|
+
resolvedVersion?: string
|
|
300
301
|
requestId?: string
|
|
302
|
+
/** Exact proof carried by the Builder-signed result. */
|
|
303
|
+
proof?: ReclaimRequestProof
|
|
301
304
|
proofValid: boolean
|
|
302
305
|
reason?: string
|
|
306
|
+
/** @deprecated Use `attestorAddresses`; retained for compatibility. */
|
|
303
307
|
attestorAddress?: string
|
|
308
|
+
/** Every attestor address declared by the proof. */
|
|
309
|
+
attestorAddresses?: string[]
|
|
310
|
+
/** Provider hashes derived from this exact proof. */
|
|
311
|
+
proofHashes?: string[]
|
|
312
|
+
/** True when the claimant/session TEE attestation was requested and
|
|
313
|
+
* verified. */
|
|
314
|
+
isTeeAttestationVerified?: boolean
|
|
315
|
+
/** True when the attestor TEE attestation was requested and at least one
|
|
316
|
+
* witness's `attestation_report` verified. False when the check ran but no
|
|
317
|
+
* witness carried a report — the common production shape, where the
|
|
318
|
+
* attestor address and claim signature are still verified. Undefined when
|
|
319
|
+
* the check didn't run. */
|
|
320
|
+
isAttestorTeeAttestationVerified?: boolean
|
|
304
321
|
/** Trusted only when `proofValid` is true. */
|
|
305
322
|
data?: TrustedData
|
|
306
323
|
/** Provider-supplied data. It isn't cryptographically validated. */
|
|
@@ -311,13 +328,19 @@ export interface VerifyResultFullOutcome extends VerifyResultOutcome {
|
|
|
311
328
|
proofs?: ResultProofOutcome[]
|
|
312
329
|
}
|
|
313
330
|
|
|
331
|
+
export interface ResultVerificationFailure extends VerifyResultFullOutcome {
|
|
332
|
+
reason: string
|
|
333
|
+
}
|
|
334
|
+
|
|
314
335
|
/**
|
|
315
336
|
* Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
|
|
316
337
|
* `reclaim.results.verify`/`receive`) when a result fails verification. Carries
|
|
317
338
|
* the full invalid outcome so callers can inspect it in a `catch`:
|
|
318
339
|
*
|
|
319
340
|
* try {
|
|
320
|
-
* const { result } = await reclaim.results.receive(body, {
|
|
341
|
+
* const { result } = await reclaim.results.receive(body, {
|
|
342
|
+
* orgEthPrivateKey,
|
|
343
|
+
* })
|
|
321
344
|
* // result is verified — use result.payload
|
|
322
345
|
* } catch (err) {
|
|
323
346
|
* if (err instanceof ResultVerificationError) {
|
|
@@ -332,21 +355,21 @@ export class ResultVerificationError extends Error {
|
|
|
332
355
|
readonly reason: string
|
|
333
356
|
/** The full (invalid) outcome: `payload`, `reclaimSessionId`, and per-proof
|
|
334
357
|
* detail (`proofs`) when verification got that far. */
|
|
335
|
-
readonly outcome:
|
|
358
|
+
readonly outcome: ResultVerificationFailure
|
|
336
359
|
|
|
337
|
-
constructor(outcome:
|
|
338
|
-
super(`result verification failed: ${outcome.reason
|
|
360
|
+
constructor(outcome: ResultVerificationFailure) {
|
|
361
|
+
super(`result verification failed: ${outcome.reason}`)
|
|
339
362
|
this.name = 'ResultVerificationError'
|
|
340
|
-
this.reason = outcome.reason
|
|
363
|
+
this.reason = outcome.reason
|
|
341
364
|
this.outcome = outcome
|
|
342
365
|
}
|
|
343
366
|
}
|
|
344
367
|
|
|
345
368
|
/**
|
|
346
|
-
* Full consumer verification: resolves a
|
|
369
|
+
* Full consumer verification: resolves a configured Builder issuer before
|
|
347
370
|
* using JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
|
|
348
371
|
* `reclaimSessionId`/`aud` and any legacy `exp`, then verifies each exact
|
|
349
|
-
*
|
|
372
|
+
* proof.
|
|
350
373
|
* Network access (JWKS and optional provider resolution) makes this async.
|
|
351
374
|
* Use {@link verifyResult} for the offline primitive.
|
|
352
375
|
*/
|
|
@@ -357,14 +380,14 @@ export async function verifyResultFull(
|
|
|
357
380
|
const payload = decodeJwsPayload(jws)
|
|
358
381
|
const reclaimSessionId = payload.reclaimSessionId as string | undefined
|
|
359
382
|
let jwksAttestorAddresses: string[] = []
|
|
383
|
+
let jwksAttestorImageDigests: string[] = []
|
|
360
384
|
/** Throw with the full invalid outcome — verification never *returns* an
|
|
361
385
|
* invalid result. */
|
|
362
386
|
const fail = (
|
|
363
387
|
reason: string,
|
|
364
|
-
extra: Partial<
|
|
388
|
+
extra: Partial<ResultVerificationFailure> = {},
|
|
365
389
|
): never => {
|
|
366
390
|
throw new ResultVerificationError({
|
|
367
|
-
valid: false,
|
|
368
391
|
reason,
|
|
369
392
|
payload,
|
|
370
393
|
reclaimSessionId,
|
|
@@ -372,8 +395,8 @@ export async function verifyResultFull(
|
|
|
372
395
|
})
|
|
373
396
|
}
|
|
374
397
|
|
|
375
|
-
// 1. Resolve
|
|
376
|
-
// network destination until it has matched an
|
|
398
|
+
// 1. Resolve Builder's signer key. Never use the signed header's `iss` as a
|
|
399
|
+
// network destination until it has matched an explicitly trusted issuer.
|
|
377
400
|
let signerPublicKey = opts.signerPublicKey
|
|
378
401
|
let header: Record<string, unknown> = {}
|
|
379
402
|
const sig0 = jws.signatures[0]
|
|
@@ -388,16 +411,7 @@ export async function verifyResultFull(
|
|
|
388
411
|
}
|
|
389
412
|
|
|
390
413
|
if(!signerPublicKey) {
|
|
391
|
-
const
|
|
392
|
-
let configuredIssuers: string[]
|
|
393
|
-
try {
|
|
394
|
-
configuredIssuers = await resolveTrustedIssuers(
|
|
395
|
-
opts,
|
|
396
|
-
verificationClientId,
|
|
397
|
-
)
|
|
398
|
-
} catch{
|
|
399
|
-
return fail('issuer-resolution-failed')
|
|
400
|
-
}
|
|
414
|
+
const configuredIssuers = resolveTrustedIssuers(opts)
|
|
401
415
|
|
|
402
416
|
if(!configuredIssuers.length) {
|
|
403
417
|
return fail('issuer-not-configured')
|
|
@@ -408,6 +422,12 @@ export async function verifyResultFull(
|
|
|
408
422
|
return fail('no-issuer')
|
|
409
423
|
}
|
|
410
424
|
|
|
425
|
+
// Read before the signature is checked, so it is attacker-supplied: a
|
|
426
|
+
// real issuer is a short origin, never kilobytes.
|
|
427
|
+
if(iss.length > MAX_ISSUER_LENGTH) {
|
|
428
|
+
return fail('issuer-not-trusted')
|
|
429
|
+
}
|
|
430
|
+
|
|
411
431
|
const trustedIssuer = configuredIssuers.find((candidate) => (
|
|
412
432
|
normalizeIssuer(candidate) === normalizeIssuer(iss)
|
|
413
433
|
))
|
|
@@ -417,17 +437,15 @@ export async function verifyResultFull(
|
|
|
417
437
|
|
|
418
438
|
let keys: Jwk[]
|
|
419
439
|
try {
|
|
420
|
-
const jwksUrl = await resolveTrustedJwksUrl(
|
|
421
|
-
opts,
|
|
422
|
-
verificationClientId,
|
|
423
|
-
)
|
|
424
440
|
const registry = await fetchReclaimJwks(
|
|
425
|
-
jwksUrl
|
|
426
|
-
?? `${trustedIssuer
|
|
441
|
+
opts.jwksUrl
|
|
442
|
+
?? `${stripTrailingSlashes(trustedIssuer)}/.well-known/jwks.json`,
|
|
427
443
|
opts.fetchImpl,
|
|
428
444
|
)
|
|
429
445
|
keys = registry.keys
|
|
430
446
|
jwksAttestorAddresses = registry['x-reclaim-attestors'] ?? []
|
|
447
|
+
jwksAttestorImageDigests =
|
|
448
|
+
registry['x-reclaim-attestor-image-digests'] ?? []
|
|
431
449
|
} catch{
|
|
432
450
|
return fail('jwks-fetch-failed')
|
|
433
451
|
}
|
|
@@ -435,7 +453,6 @@ export async function verifyResultFull(
|
|
|
435
453
|
const jwk = selectVerificationResultKey(
|
|
436
454
|
keys,
|
|
437
455
|
String(header.kid ?? ''),
|
|
438
|
-
verificationClientId,
|
|
439
456
|
)
|
|
440
457
|
if(!jwk) {
|
|
441
458
|
return fail('signer-key-not-found')
|
|
@@ -444,7 +461,14 @@ export async function verifyResultFull(
|
|
|
444
461
|
signerPublicKey = jwkToUncompressedPublicKey(jwk)
|
|
445
462
|
}
|
|
446
463
|
|
|
447
|
-
if(
|
|
464
|
+
if(header.alg !== 'ES256K') {
|
|
465
|
+
return fail('unsupported-algorithm')
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if(!sig0 || !verifyJwsSignature({
|
|
469
|
+
payload: jws.payload,
|
|
470
|
+
signatures: [sig0],
|
|
471
|
+
}, signerPublicKey)) {
|
|
448
472
|
return fail('bad-signature')
|
|
449
473
|
}
|
|
450
474
|
|
|
@@ -457,8 +481,7 @@ export async function verifyResultFull(
|
|
|
457
481
|
}
|
|
458
482
|
|
|
459
483
|
// 2. Session binding, audience, and optional legacy result expiry.
|
|
460
|
-
const expectedResultSessionId = opts.
|
|
461
|
-
?? opts.expectedReclaimSessionId
|
|
484
|
+
const expectedResultSessionId = opts.reclaimSessionId
|
|
462
485
|
if(expectedResultSessionId && reclaimSessionId !== expectedResultSessionId) {
|
|
463
486
|
return fail('reclaimSessionId-mismatch')
|
|
464
487
|
}
|
|
@@ -476,7 +499,7 @@ export async function verifyResultFull(
|
|
|
476
499
|
return fail('expired')
|
|
477
500
|
}
|
|
478
501
|
|
|
479
|
-
// 3. Verify every exact
|
|
502
|
+
// 3. Verify every exact proof carried by the signed result. The outer
|
|
480
503
|
// envelope groups proofs by provider, but it never changes the proof shape.
|
|
481
504
|
const providerResults = Array.isArray(payload.results)
|
|
482
505
|
? (payload.results as Record<string, unknown>[])
|
|
@@ -485,7 +508,7 @@ export async function verifyResultFull(
|
|
|
485
508
|
const proofGroups: {
|
|
486
509
|
providerId?: string
|
|
487
510
|
providerVersion?: string
|
|
488
|
-
proof:
|
|
511
|
+
proof: ReclaimRequestProof
|
|
489
512
|
outcomeIndex: number
|
|
490
513
|
}[] = []
|
|
491
514
|
for(const prov of providerResults) {
|
|
@@ -500,18 +523,25 @@ export async function verifyResultFull(
|
|
|
500
523
|
for(const r of requests) {
|
|
501
524
|
const requestId =
|
|
502
525
|
typeof r.request_id === 'string' ? r.request_id : undefined
|
|
503
|
-
if(!
|
|
526
|
+
if(!isReclaimRequestProof(r.proof)) {
|
|
504
527
|
proofs.push({
|
|
505
|
-
providerId,
|
|
528
|
+
providerId,
|
|
529
|
+
resolvedVersion: providerVersion,
|
|
530
|
+
requestId,
|
|
531
|
+
proofValid: false,
|
|
532
|
+
reason: 'proof-missing',
|
|
506
533
|
})
|
|
507
534
|
continue
|
|
508
535
|
}
|
|
509
536
|
|
|
510
537
|
proofs.push({
|
|
511
538
|
providerId,
|
|
539
|
+
resolvedVersion: providerVersion,
|
|
512
540
|
requestId,
|
|
541
|
+
proof: r.proof,
|
|
513
542
|
proofValid: true,
|
|
514
543
|
attestorAddress: r.proof.witnesses[0]?.id,
|
|
544
|
+
attestorAddresses: r.proof.witnesses.map(({ id }) => id),
|
|
515
545
|
})
|
|
516
546
|
proofGroups.push({
|
|
517
547
|
providerId,
|
|
@@ -529,25 +559,63 @@ export async function verifyResultFull(
|
|
|
529
559
|
|
|
530
560
|
if(proofGroups.length) {
|
|
531
561
|
const expectedSessionId = expectedResultSessionId
|
|
532
|
-
const
|
|
533
|
-
|
|
562
|
+
const hasTeeBoundProof = proofGroups.some(({ proof }) => (
|
|
563
|
+
proofHasTeeBinding(proof)
|
|
564
|
+
))
|
|
565
|
+
const configuredProofValidation = {
|
|
566
|
+
...opts.proofValidation,
|
|
567
|
+
...(opts.orgEthPrivateKey && hasTeeBoundProof
|
|
568
|
+
? {
|
|
569
|
+
teeAttestation: {
|
|
570
|
+
appSecret: opts.orgEthPrivateKey,
|
|
571
|
+
},
|
|
572
|
+
}
|
|
573
|
+
: {}),
|
|
574
|
+
}
|
|
534
575
|
const attestorAddresses = [...new Set([
|
|
535
576
|
...(configuredProofValidation?.attestorAddresses ?? []),
|
|
536
577
|
...(opts.attestorAddresses ?? []),
|
|
537
578
|
...jwksAttestorAddresses,
|
|
538
579
|
])]
|
|
580
|
+
const configuredAttestorTee =
|
|
581
|
+
configuredProofValidation?.attestorTeeAttestation
|
|
582
|
+
const configuredImageDigests =
|
|
583
|
+
configuredAttestorTee?.expectedImageDigests
|
|
584
|
+
let expectedImageDigests = jwksAttestorImageDigests
|
|
585
|
+
if(configuredImageDigests?.length && jwksAttestorImageDigests.length) {
|
|
586
|
+
const registry = new Set(jwksAttestorImageDigests)
|
|
587
|
+
expectedImageDigests = [...new Set(configuredImageDigests
|
|
588
|
+
.map((digest) => digest.trim().toLowerCase())
|
|
589
|
+
.filter((digest) => registry.has(digest)))]
|
|
590
|
+
if(!expectedImageDigests.length) {
|
|
591
|
+
return fail('attestor-image-trust-mismatch', { proofs })
|
|
592
|
+
}
|
|
593
|
+
} else if(configuredImageDigests?.length) {
|
|
594
|
+
expectedImageDigests = configuredImageDigests
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const attestorTeeAttestation = expectedImageDigests.length
|
|
598
|
+
? { ...configuredAttestorTee, expectedImageDigests }
|
|
599
|
+
: configuredAttestorTee
|
|
539
600
|
const commonValidation = {
|
|
540
601
|
...configuredProofValidation,
|
|
541
602
|
...(opts.hashes ? { hashes: opts.hashes } : {}),
|
|
542
603
|
...(attestorAddresses.length
|
|
543
604
|
? { attestorAddresses }
|
|
544
605
|
: {}),
|
|
606
|
+
...(attestorTeeAttestation
|
|
607
|
+
? { attestorTeeAttestation }
|
|
608
|
+
: {}),
|
|
545
609
|
...(expectedSessionId
|
|
546
|
-
? {
|
|
610
|
+
? {
|
|
611
|
+
expectedSessionId,
|
|
612
|
+
requireSessionBinding:
|
|
613
|
+
configuredProofValidation?.requireSessionBinding ?? true,
|
|
614
|
+
}
|
|
547
615
|
: {}),
|
|
548
616
|
}
|
|
549
617
|
if(hasContentValidation(commonValidation)) {
|
|
550
|
-
await
|
|
618
|
+
await verifyRequestProofGroup(proofGroups, commonValidation)
|
|
551
619
|
} else {
|
|
552
620
|
await verifyByResolvedRecipes(commonValidation, expectedSessionId)
|
|
553
621
|
}
|
|
@@ -557,21 +625,39 @@ export async function verifyResultFull(
|
|
|
557
625
|
return fail('invalid-proof', { proofs })
|
|
558
626
|
}
|
|
559
627
|
|
|
560
|
-
return {
|
|
628
|
+
return { payload, reclaimSessionId, proofs }
|
|
561
629
|
|
|
562
|
-
async function
|
|
630
|
+
async function verifyRequestProofGroup(
|
|
563
631
|
group: typeof proofGroups,
|
|
564
|
-
validation:
|
|
632
|
+
validation: VerifyRequestProofOptions,
|
|
565
633
|
) {
|
|
566
|
-
const
|
|
634
|
+
const requestProofOutcome = await verifyRequestProof(
|
|
567
635
|
group.map(({ proof }) => proof),
|
|
568
636
|
validation,
|
|
569
637
|
)
|
|
570
|
-
if(
|
|
638
|
+
if(requestProofOutcome.valid) {
|
|
571
639
|
for(const [index, item] of group.entries()) {
|
|
572
|
-
proofs[item.outcomeIndex]
|
|
573
|
-
|
|
574
|
-
|
|
640
|
+
const outcome = proofs[item.outcomeIndex]
|
|
641
|
+
const hashes = resultProofHashes(
|
|
642
|
+
item.proof,
|
|
643
|
+
validation.hasNoPii,
|
|
644
|
+
)
|
|
645
|
+
if(hashes) {
|
|
646
|
+
outcome.proofHashes = hashes
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if(requestProofOutcome.isTeeAttestationVerified !== undefined) {
|
|
650
|
+
outcome.isTeeAttestationVerified =
|
|
651
|
+
requestProofOutcome.isTeeAttestationVerified
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if(requestProofOutcome.isAttestorTeeAttestationVerified !== undefined) {
|
|
655
|
+
outcome.isAttestorTeeAttestationVerified =
|
|
656
|
+
requestProofOutcome.isAttestorTeeAttestationVerified
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
outcome.data = requestProofOutcome.data?.[index]
|
|
660
|
+
outcome.publicData = requestProofOutcome.publicData?.[index]
|
|
575
661
|
}
|
|
576
662
|
|
|
577
663
|
return
|
|
@@ -579,12 +665,12 @@ export async function verifyResultFull(
|
|
|
579
665
|
|
|
580
666
|
for(const { outcomeIndex } of group) {
|
|
581
667
|
proofs[outcomeIndex].proofValid = false
|
|
582
|
-
proofs[outcomeIndex].reason =
|
|
668
|
+
proofs[outcomeIndex].reason = requestProofOutcome.reason
|
|
583
669
|
}
|
|
584
670
|
}
|
|
585
671
|
|
|
586
672
|
async function verifyByResolvedRecipes(
|
|
587
|
-
common:
|
|
673
|
+
common: VerifyRequestProofOptions,
|
|
588
674
|
expectedSessionId: string | undefined,
|
|
589
675
|
) {
|
|
590
676
|
let recipes = opts.proofRecipes
|
|
@@ -611,7 +697,7 @@ export async function verifyResultFull(
|
|
|
611
697
|
async function verifyResolvedGroup(
|
|
612
698
|
group: typeof proofGroups,
|
|
613
699
|
recipes: BuilderProofRecipe[] | undefined,
|
|
614
|
-
common:
|
|
700
|
+
common: VerifyRequestProofOptions,
|
|
615
701
|
) {
|
|
616
702
|
const providerId = group[0]?.providerId
|
|
617
703
|
const providerVersion = group[0]?.providerVersion
|
|
@@ -635,7 +721,7 @@ export async function verifyResultFull(
|
|
|
635
721
|
}
|
|
636
722
|
}
|
|
637
723
|
|
|
638
|
-
|
|
724
|
+
const validation = {
|
|
639
725
|
...common,
|
|
640
726
|
...(recipe
|
|
641
727
|
? { hashes }
|
|
@@ -643,7 +729,15 @@ export async function verifyResultFull(
|
|
|
643
729
|
? { providerId, providerVersion }
|
|
644
730
|
: {}),
|
|
645
731
|
...providerValidation,
|
|
646
|
-
|
|
732
|
+
...(common.expectedSessionId
|
|
733
|
+
? {
|
|
734
|
+
sessionId: common.sessionId,
|
|
735
|
+
expectedSessionId: common.expectedSessionId,
|
|
736
|
+
requireSessionBinding: common.requireSessionBinding,
|
|
737
|
+
}
|
|
738
|
+
: {}),
|
|
739
|
+
}
|
|
740
|
+
await verifyRequestProofGroup(group, validation)
|
|
647
741
|
}
|
|
648
742
|
|
|
649
743
|
function markInvalid(group: typeof proofGroups, reason: string) {
|
|
@@ -654,7 +748,43 @@ export async function verifyResultFull(
|
|
|
654
748
|
}
|
|
655
749
|
}
|
|
656
750
|
|
|
657
|
-
|
|
751
|
+
/** Whether a proof declares the session TEE nonce that the org key verifies. */
|
|
752
|
+
function proofHasTeeBinding(proof: ReclaimRequestProof) {
|
|
753
|
+
// A `confidential-space` witness carries the attestation itself, so the
|
|
754
|
+
// proof is TEE-bound even before the context is read. Triggering on it too
|
|
755
|
+
// means a proof that ships the evidence can't skip the check by omitting
|
|
756
|
+
// `attestationNonce` — `verifyTeeAttestation` needs that nonce and fails
|
|
757
|
+
// loudly without it, which is the right outcome.
|
|
758
|
+
const hasTeeWitness = (proof.witnesses ?? []).some((witness) => (
|
|
759
|
+
witness.claimAttestation?.type === 'confidential-space'
|
|
760
|
+
&& !!witness.claimAttestation.tee_attestation
|
|
761
|
+
))
|
|
762
|
+
if(hasTeeWitness) {
|
|
763
|
+
return true
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const context = JSON.parse(proof.claimData.context)
|
|
768
|
+
return context && typeof context === 'object'
|
|
769
|
+
&& typeof context.attestationNonce === 'string'
|
|
770
|
+
&& context.attestationNonce.length > 0
|
|
771
|
+
} catch{
|
|
772
|
+
return false
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function resultProofHashes(
|
|
777
|
+
proof: ReclaimRequestProof,
|
|
778
|
+
hasNoPii: true | undefined,
|
|
779
|
+
) {
|
|
780
|
+
try {
|
|
781
|
+
return getHashFromProof(proof, hasNoPii)
|
|
782
|
+
} catch{
|
|
783
|
+
return undefined
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function hasContentValidation(config: VerifyRequestProofOptions): boolean {
|
|
658
788
|
return Boolean(
|
|
659
789
|
config.hashes
|
|
660
790
|
|| config.providerId
|
|
@@ -680,7 +810,7 @@ function recipeGroupsMatch(
|
|
|
680
810
|
&& [...groups.keys()].every((key) => expected.has(key))
|
|
681
811
|
}
|
|
682
812
|
|
|
683
|
-
function
|
|
813
|
+
function isReclaimRequestProof(value: unknown): value is ReclaimRequestProof {
|
|
684
814
|
if(!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
685
815
|
return false
|
|
686
816
|
}
|
|
@@ -705,74 +835,11 @@ function getVerificationStatus(payload: Record<string, unknown>) {
|
|
|
705
835
|
}
|
|
706
836
|
|
|
707
837
|
function normalizeIssuer(issuer: string): string {
|
|
708
|
-
return issuer.trim()
|
|
838
|
+
return stripTrailingSlashes(issuer.trim()).toLowerCase()
|
|
709
839
|
}
|
|
710
840
|
|
|
711
|
-
function
|
|
712
|
-
|
|
713
|
-
): string | undefined {
|
|
714
|
-
if(typeof payload.verificationClientId === 'string') {
|
|
715
|
-
return payload.verificationClientId
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if(typeof payload.verification_client_id === 'string') {
|
|
719
|
-
return payload.verification_client_id
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const session = payload.verification_session
|
|
723
|
-
if(!session || typeof session !== 'object' || Array.isArray(session)) {
|
|
724
|
-
return undefined
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const sessionRecord = session as Record<string, unknown>
|
|
728
|
-
const id = sessionRecord.verificationClientId
|
|
729
|
-
?? sessionRecord.verification_client_id
|
|
730
|
-
return typeof id === 'string' ? id : undefined
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
async function resolveTrustedIssuers(
|
|
734
|
-
opts: VerifyResultFullOptions,
|
|
735
|
-
verificationClientId: string | undefined,
|
|
736
|
-
): Promise<string[]> {
|
|
737
|
-
const explicit = [
|
|
738
|
-
opts.issuer,
|
|
739
|
-
opts.trustedIssuer,
|
|
740
|
-
...(opts.issuers ?? []),
|
|
741
|
-
...(opts.trustedIssuers ?? []),
|
|
742
|
-
].filter((issuer): issuer is string => typeof issuer === 'string')
|
|
743
|
-
if(explicit.length) {
|
|
744
|
-
return [...new Set(explicit.map(normalizeIssuer))]
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if(!verificationClientId) {
|
|
748
|
-
return []
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const registered =
|
|
752
|
-
opts.issuersByVerificationClientId?.[verificationClientId] ?? []
|
|
753
|
-
const resolve = opts.resolveIssuers ?? opts.issuerResolver
|
|
754
|
-
const resolved = resolve
|
|
755
|
-
? await resolve(verificationClientId)
|
|
756
|
-
: []
|
|
757
|
-
return [...new Set([...registered, ...resolved].map(normalizeIssuer))]
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
async function resolveTrustedJwksUrl(
|
|
761
|
-
opts: VerifyResultFullOptions,
|
|
762
|
-
verificationClientId: string | undefined,
|
|
763
|
-
): Promise<string | undefined> {
|
|
764
|
-
if(opts.jwksUrl) {
|
|
765
|
-
return opts.jwksUrl
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if(!verificationClientId) {
|
|
769
|
-
return undefined
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const registered = opts.jwksUrlsByVerificationClientId?.[
|
|
773
|
-
verificationClientId
|
|
774
|
-
]
|
|
775
|
-
return registered ?? await opts.resolveJwksUrl?.(verificationClientId)
|
|
841
|
+
function resolveTrustedIssuers(opts: VerifyResultFullOptions): string[] {
|
|
842
|
+
return [...new Set((opts.trustedIssuers ?? []).map(normalizeIssuer))]
|
|
776
843
|
}
|
|
777
844
|
|
|
778
845
|
/** Decrypt-and-verify in one step. */
|
|
@@ -785,21 +852,20 @@ export function verifyResult(
|
|
|
785
852
|
|
|
786
853
|
if(opts.signerPublicKey && !verifyJwsSignature(jws, opts.signerPublicKey)) {
|
|
787
854
|
throw new ResultVerificationError({
|
|
788
|
-
|
|
855
|
+
reason: 'bad-signature', payload, reclaimSessionId,
|
|
789
856
|
})
|
|
790
857
|
}
|
|
791
858
|
|
|
792
859
|
if(
|
|
793
|
-
opts.
|
|
794
|
-
reclaimSessionId !== opts.
|
|
860
|
+
opts.reclaimSessionId &&
|
|
861
|
+
reclaimSessionId !== opts.reclaimSessionId
|
|
795
862
|
) {
|
|
796
863
|
throw new ResultVerificationError({
|
|
797
|
-
valid: false,
|
|
798
864
|
reason: 'reclaimSessionId-mismatch',
|
|
799
865
|
payload,
|
|
800
866
|
reclaimSessionId,
|
|
801
867
|
})
|
|
802
868
|
}
|
|
803
869
|
|
|
804
|
-
return {
|
|
870
|
+
return { payload, reclaimSessionId }
|
|
805
871
|
}
|