@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.
Files changed (110) hide show
  1. package/README.md +255 -234
  2. package/lib/api.d.ts +2 -0
  3. package/lib/api.js +2 -0
  4. package/lib/client.js +2 -0
  5. package/lib/enckeys.d.ts +1 -1
  6. package/lib/enckeys.js +2 -2
  7. package/lib/encoding.d.ts +4 -0
  8. package/lib/encoding.js +10 -0
  9. package/lib/feature-flags.d.ts +9 -0
  10. package/lib/feature-flags.js +19 -0
  11. package/lib/index.d.ts +4 -4
  12. package/lib/index.js +4 -4
  13. package/lib/jwks.d.ts +8 -8
  14. package/lib/jwks.js +38 -17
  15. package/lib/launch/destination.d.ts +14 -0
  16. package/lib/launch/destination.js +89 -0
  17. package/lib/launch/device.d.ts +30 -0
  18. package/lib/launch/device.js +168 -0
  19. package/lib/launch/environment.d.ts +224 -0
  20. package/lib/launch/environment.js +98 -0
  21. package/lib/launch/errors.d.ts +20 -0
  22. package/lib/launch/errors.js +25 -0
  23. package/lib/launch/extension.d.ts +42 -0
  24. package/lib/launch/extension.js +72 -0
  25. package/lib/launch/flow.d.ts +82 -0
  26. package/lib/launch/flow.js +222 -0
  27. package/lib/launch/index.d.ts +16 -0
  28. package/lib/launch/index.js +12 -0
  29. package/lib/launch/mobile.d.ts +77 -0
  30. package/lib/launch/mobile.js +124 -0
  31. package/lib/launch/modal.d.ts +50 -0
  32. package/lib/launch/modal.js +258 -0
  33. package/lib/launch/options.d.ts +74 -0
  34. package/lib/launch/options.js +20 -0
  35. package/lib/launch/portal.d.ts +28 -0
  36. package/lib/launch/portal.js +68 -0
  37. package/lib/launch/qr.d.ts +5 -0
  38. package/lib/launch/qr.js +28 -0
  39. package/lib/launch/target.d.ts +46 -0
  40. package/lib/launch/target.js +128 -0
  41. package/lib/launch/vendor/qrcodegen.d.ts +134 -0
  42. package/lib/launch/vendor/qrcodegen.js +822 -0
  43. package/lib/legacy-proof.d.ts +2 -0
  44. package/lib/legacy-proof.js +37 -7
  45. package/lib/proof-hash.d.ts +5 -5
  46. package/lib/proof-hash.js +1 -1
  47. package/lib/proof-recipe.d.ts +3 -3
  48. package/lib/proof-recipe.js +1 -1
  49. package/lib/proof-types.d.ts +35 -8
  50. package/lib/reclaim.d.ts +81 -70
  51. package/lib/reclaim.js +79 -130
  52. package/lib/request-proof.d.ts +60 -0
  53. package/lib/request-proof.js +241 -0
  54. package/lib/result.d.ts +46 -38
  55. package/lib/result.js +160 -100
  56. package/lib/routes.gen.d.ts +76 -28
  57. package/lib/routes.gen.js +24 -11
  58. package/lib/tee-binding.d.ts +1 -1
  59. package/lib/types/openapi.d.ts +1 -0
  60. package/lib/types/openapi.gen.d.ts +1598 -485
  61. package/lib/types/openapi.gen.js +5 -0
  62. package/lib/verification-clients.d.ts +28 -14
  63. package/lib/verification-clients.js +31 -11
  64. package/lib/verify-attestor-tee.d.ts +19 -2
  65. package/lib/verify-attestor-tee.js +40 -4
  66. package/lib/verify-tee.d.ts +10 -1
  67. package/lib/verify-tee.js +34 -10
  68. package/package.json +5 -1
  69. package/src/api.ts +9 -0
  70. package/src/client.ts +2 -0
  71. package/src/enckeys.ts +2 -2
  72. package/src/encoding.ts +12 -0
  73. package/src/feature-flags.ts +39 -0
  74. package/src/index.ts +13 -14
  75. package/src/jwks.ts +52 -30
  76. package/src/launch/destination.ts +111 -0
  77. package/src/launch/device.ts +242 -0
  78. package/src/launch/environment.ts +337 -0
  79. package/src/launch/errors.ts +49 -0
  80. package/src/launch/extension.ts +124 -0
  81. package/src/launch/flow.ts +369 -0
  82. package/src/launch/index.ts +42 -0
  83. package/src/launch/mobile.ts +184 -0
  84. package/src/launch/modal.ts +378 -0
  85. package/src/launch/options.ts +99 -0
  86. package/src/launch/portal.ts +104 -0
  87. package/src/launch/qr.ts +37 -0
  88. package/src/launch/target.ts +150 -0
  89. package/src/launch/vendor/qrcodegen.ts +1034 -0
  90. package/src/proof-hash.ts +6 -6
  91. package/src/proof-recipe.ts +3 -3
  92. package/src/proof-types.ts +36 -8
  93. package/src/reclaim.ts +185 -218
  94. package/src/{legacy-proof.ts → request-proof.ts} +81 -36
  95. package/src/result.ts +238 -172
  96. package/src/routes.gen.ts +24 -11
  97. package/src/tee-binding.ts +1 -1
  98. package/src/types/openapi.gen.ts +1603 -485
  99. package/src/types/openapi.ts +3 -1
  100. package/src/verification-clients.ts +33 -19
  101. package/src/verify-attestor-tee.ts +66 -8
  102. package/src/verify-tee.ts +44 -13
  103. package/lib/api-client.d.ts +0 -31
  104. package/lib/api-client.js +0 -112
  105. package/lib/api-consts.d.ts +0 -4
  106. package/lib/api-consts.js +0 -4
  107. package/lib/api-errors.d.ts +0 -13
  108. package/lib/api-errors.js +0 -30
  109. package/lib/eip712.d.ts +0 -20
  110. 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 a registered issuer, discovers the Verification Client's key
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
- * legacy proof to the in-package, legacy-compatible `verifyProof` verifier.
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 { b64url, base64urlToBytes, hexToBytes } from './encoding.ts'
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
- expectedReclaimSessionId?: string
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 decryption credential was '
165
- + 'configured — pass the eth private key (0x-hex): '
166
- + 'results.receive(body, { credential }) or decrypt(body, credential)',
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 the Verification Client to sign results before encrypting them.
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
- expectedReclaimSessionId?: string
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
- /** One explicitly trusted Verification Client issuer. */
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
- /** Resolve registered issuer URLs for the signed session's client id. */
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
- /** Trusted JWKS URLs keyed by `verificationClientId`. */
264
- jwksUrlsByVerificationClientId?: Record<string, string>
265
- /** Resolve a registered JWKS URL for the signed session's client id. */
266
- resolveJwksUrl?: (verificationClientId: string) =>
267
- Promise<string | undefined> | string | undefined
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, LegacyVerifyProofOptions>
274
- /** Legacy-compatible hash requirements, for the common simple case. */
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 the VC signer key instead of JWKS discovery via header `iss`. */
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, { credential })
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: VerifyResultFullOutcome
358
+ readonly outcome: ResultVerificationFailure
336
359
 
337
- constructor(outcome: VerifyResultFullOutcome) {
338
- super(`result verification failed: ${outcome.reason ?? 'invalid'}`)
360
+ constructor(outcome: ResultVerificationFailure) {
361
+ super(`result verification failed: ${outcome.reason}`)
339
362
  this.name = 'ResultVerificationError'
340
- this.reason = outcome.reason ?? 'invalid'
363
+ this.reason = outcome.reason
341
364
  this.outcome = outcome
342
365
  }
343
366
  }
344
367
 
345
368
  /**
346
- * Full consumer verification: resolves a trusted, registered issuer before
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
- * legacy proof.
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<VerifyResultFullOutcome> = {},
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 the VC signer key. Never use the signed header's `iss` as a
376
- // network destination until it has matched an explicit or registered issuer.
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 verificationClientId = getVerificationClientId(payload) ?? ''
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.replace(/\/+$/, '')}/.well-known/jwks.json`,
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(!verifyJwsSignature(jws, signerPublicKey)) {
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.sessionId
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 legacy proof carried by the signed result. The outer
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: LegacyProof
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(!isLegacyProof(r.proof)) {
526
+ if(!isReclaimRequestProof(r.proof)) {
504
527
  proofs.push({
505
- providerId, requestId, proofValid: false, reason: 'proof-missing',
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 configuredProofValidation = opts.proofValidation
533
- ?? opts.proofVerification
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
- ? { expectedSessionId }
610
+ ? {
611
+ expectedSessionId,
612
+ requireSessionBinding:
613
+ configuredProofValidation?.requireSessionBinding ?? true,
614
+ }
547
615
  : {}),
548
616
  }
549
617
  if(hasContentValidation(commonValidation)) {
550
- await verifyLegacyProofGroup(proofGroups, commonValidation)
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 { valid: true, payload, reclaimSessionId, proofs }
628
+ return { payload, reclaimSessionId, proofs }
561
629
 
562
- async function verifyLegacyProofGroup(
630
+ async function verifyRequestProofGroup(
563
631
  group: typeof proofGroups,
564
- validation: LegacyVerifyProofOptions,
632
+ validation: VerifyRequestProofOptions,
565
633
  ) {
566
- const legacyOutcome = await verifyProof(
634
+ const requestProofOutcome = await verifyRequestProof(
567
635
  group.map(({ proof }) => proof),
568
636
  validation,
569
637
  )
570
- if(legacyOutcome.valid) {
638
+ if(requestProofOutcome.valid) {
571
639
  for(const [index, item] of group.entries()) {
572
- proofs[item.outcomeIndex].data = legacyOutcome.data?.[index]
573
- proofs[item.outcomeIndex].publicData =
574
- legacyOutcome.publicData?.[index]
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 = legacyOutcome.reason
668
+ proofs[outcomeIndex].reason = requestProofOutcome.reason
583
669
  }
584
670
  }
585
671
 
586
672
  async function verifyByResolvedRecipes(
587
- common: LegacyVerifyProofOptions,
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: LegacyVerifyProofOptions,
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
- await verifyLegacyProofGroup(group, {
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
- function hasContentValidation(config: LegacyVerifyProofOptions): boolean {
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 isLegacyProof(value: unknown): value is LegacyProof {
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().replace(/\/+$/, '').toLowerCase()
838
+ return stripTrailingSlashes(issuer.trim()).toLowerCase()
709
839
  }
710
840
 
711
- function getVerificationClientId(
712
- payload: Record<string, unknown>,
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
- valid: false, reason: 'bad-signature', payload, reclaimSessionId,
855
+ reason: 'bad-signature', payload, reclaimSessionId,
789
856
  })
790
857
  }
791
858
 
792
859
  if(
793
- opts.expectedReclaimSessionId &&
794
- reclaimSessionId !== opts.expectedReclaimSessionId
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 { valid: true, payload, reclaimSessionId }
870
+ return { payload, reclaimSessionId }
805
871
  }