@reclaimprotocol/client 0.1.28 → 0.1.31

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 +1594 -488
  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 +1599 -488
  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/lib/result.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { components } from './types/openapi.gen.ts';
2
2
  import type { DecryptionKey } from './enckeys.ts';
3
3
  import { type Jwk } from './jwks.ts';
4
- import { type LegacyVerifyProofOptions, type ProofHashRequirement } from './legacy-proof.ts';
5
4
  import { type BuilderProofRecipe } from './proof-recipe.ts';
6
5
  import type { TrustedData } from './proof-types.ts';
6
+ import { type ProofHashRequirement, type ReclaimRequestProof, type VerifyRequestProofOptions } from './request-proof.ts';
7
7
  export interface Jws {
8
8
  payload: string;
9
9
  signatures: {
@@ -18,11 +18,9 @@ export interface VerifyResultOptions {
18
18
  signerPublicKey?: string;
19
19
  /** The `reclaimSessionId` the consumer expects (the session id they got
20
20
  * back at creation). When given, mismatch fails verification. */
21
- expectedReclaimSessionId?: string;
21
+ reclaimSessionId?: string;
22
22
  }
23
23
  export interface VerifyResultOutcome {
24
- valid: boolean;
25
- reason?: string;
26
24
  payload: Record<string, unknown>;
27
25
  reclaimSessionId?: string;
28
26
  }
@@ -69,7 +67,7 @@ export declare function decryptCallback(privateKey: DecryptionKey | undefined, b
69
67
  * Sign a verification result as a flattened JWS (JOSE ES256K = ECDSA/secp256k1
70
68
  * over SHA-256 of the `<protected>.<payload>` signing input — a single hash).
71
69
  * `kid` is the signer's ETH address (published via JWKS); `iss` is the issuer
72
- * URL. Used by the Verification Client to sign results before encrypting them.
70
+ * URL. Used by Builder to sign results before encrypting them.
73
71
  */
74
72
  export declare function signResultJws(privateKeyHex: string, payload: Record<string, unknown>, iss: string): Jws;
75
73
  /** Decode the JWS payload (base64url JSON). */
@@ -80,41 +78,27 @@ export declare function decodeJwsPayload(jws: Jws): Record<string, unknown>;
80
78
  */
81
79
  export declare function verifyJwsSignature(jws: Jws, signerPublicKey: string): boolean;
82
80
  export interface VerifyResultFullOptions {
83
- /** Builder session id. Alias for `expectedReclaimSessionId`. */
84
- sessionId?: string;
85
81
  /** The `reclaimSessionId` the consumer expects (mismatch fails). */
86
- expectedReclaimSessionId?: string;
82
+ reclaimSessionId?: string;
83
+ /** Optional organization ETH private key used to verify legacy TEE binding.
84
+ * Callers must provide it for each verification; it isn't retained. */
85
+ orgEthPrivateKey?: string;
87
86
  /** The consumer's `orgId`; the result's `aud` must match it. */
88
87
  expectedAud?: string;
89
88
  /** Allowlist of trusted attestor ETH addresses (lowercased compared). */
90
89
  attestorAddresses?: string[];
91
- /** One explicitly trusted Verification Client issuer. */
92
- issuer?: string;
93
- /** Alias for `issuer`. */
94
- trustedIssuer?: string;
95
- /** Explicitly trusted Verification Client issuers. */
96
- issuers?: string[];
97
- /** Alias for `issuers`, retained for security-focused callers. */
90
+ /** Explicitly trusted Builder issuers. */
98
91
  trustedIssuers?: string[];
99
- /** Resolve registered issuer URLs for the signed session's client id. */
100
- resolveIssuers?: (verificationClientId: string) => Promise<readonly string[]> | readonly string[];
101
- /** Alias for `resolveIssuers`. */
102
- issuerResolver?: (verificationClientId: string) => Promise<readonly string[]> | readonly string[];
103
- /** Registered issuer URLs keyed by `verificationClientId`. */
104
- issuersByVerificationClientId?: Record<string, string[]>;
105
- /** Explicit trusted JWKS URL. This value must come from your registry. */
92
+ /** Explicit trusted Builder JWKS URL. */
106
93
  jwksUrl?: string;
107
- /** Trusted JWKS URLs keyed by `verificationClientId`. */
108
- jwksUrlsByVerificationClientId?: Record<string, string>;
109
- /** Resolve a registered JWKS URL for the signed session's client id. */
110
- resolveJwksUrl?: (verificationClientId: string) => Promise<string | undefined> | string | undefined;
111
- /** Legacy-compatible proof validation options applied to all proofs. */
112
- proofValidation?: LegacyVerifyProofOptions;
113
- /** Alias for `proofValidation`. */
114
- proofVerification?: LegacyVerifyProofOptions;
94
+ /**
95
+ * Proof validation options applied to all proofs.
96
+ * `requireSessionBinding` defaults to true when an expected session is set.
97
+ */
98
+ proofValidation?: VerifyRequestProofOptions;
115
99
  /** Per-provider overrides for sessions containing different providers. */
116
- proofValidationByProviderId?: Record<string, LegacyVerifyProofOptions>;
117
- /** Legacy-compatible hash requirements, for the common simple case. */
100
+ proofValidationByProviderId?: Record<string, VerifyRequestProofOptions>;
101
+ /** Hash requirements, for the common simple case. */
118
102
  hashes?: ProofHashRequirement[];
119
103
  /** Resolved Builder recipes, used to derive strict proof hashes offline. */
120
104
  proofRecipes?: BuilderProofRecipe[];
@@ -124,7 +108,8 @@ export interface VerifyResultFullOptions {
124
108
  }> | {
125
109
  recipes: BuilderProofRecipe[];
126
110
  };
127
- /** Override the VC signer key instead of JWKS discovery via header `iss`. */
111
+ /** Override Builder's signer key instead of JWKS discovery.
112
+ * The header `iss` is otherwise used only after trust checks. */
128
113
  signerPublicKey?: string;
129
114
  /** Injectable fetch (for JWKS); defaults to global `fetch`. */
130
115
  fetchImpl?: typeof fetch;
@@ -140,10 +125,28 @@ export interface VerifyResultFullOptions {
140
125
  export interface ResultProofOutcome {
141
126
  /** Which provider this proof belongs to (results grouped per provider). */
142
127
  providerId?: string;
128
+ /** Exact immutable provider version used to validate this proof. */
129
+ resolvedVersion?: string;
143
130
  requestId?: string;
131
+ /** Exact proof carried by the Builder-signed result. */
132
+ proof?: ReclaimRequestProof;
144
133
  proofValid: boolean;
145
134
  reason?: string;
135
+ /** @deprecated Use `attestorAddresses`; retained for compatibility. */
146
136
  attestorAddress?: string;
137
+ /** Every attestor address declared by the proof. */
138
+ attestorAddresses?: string[];
139
+ /** Provider hashes derived from this exact proof. */
140
+ proofHashes?: string[];
141
+ /** True when the claimant/session TEE attestation was requested and
142
+ * verified. */
143
+ isTeeAttestationVerified?: boolean;
144
+ /** True when the attestor TEE attestation was requested and at least one
145
+ * witness's `attestation_report` verified. False when the check ran but no
146
+ * witness carried a report — the common production shape, where the
147
+ * attestor address and claim signature are still verified. Undefined when
148
+ * the check didn't run. */
149
+ isAttestorTeeAttestationVerified?: boolean;
147
150
  /** Trusted only when `proofValid` is true. */
148
151
  data?: TrustedData;
149
152
  /** Provider-supplied data. It isn't cryptographically validated. */
@@ -152,13 +155,18 @@ export interface ResultProofOutcome {
152
155
  export interface VerifyResultFullOutcome extends VerifyResultOutcome {
153
156
  proofs?: ResultProofOutcome[];
154
157
  }
158
+ export interface ResultVerificationFailure extends VerifyResultFullOutcome {
159
+ reason: string;
160
+ }
155
161
  /**
156
162
  * Thrown by {@link verifyResult} / {@link verifyResultFull} (and therefore
157
163
  * `reclaim.results.verify`/`receive`) when a result fails verification. Carries
158
164
  * the full invalid outcome so callers can inspect it in a `catch`:
159
165
  *
160
166
  * try {
161
- * const { result } = await reclaim.results.receive(body, { credential })
167
+ * const { result } = await reclaim.results.receive(body, {
168
+ * orgEthPrivateKey,
169
+ * })
162
170
  * // result is verified — use result.payload
163
171
  * } catch (err) {
164
172
  * if (err instanceof ResultVerificationError) {
@@ -173,14 +181,14 @@ export declare class ResultVerificationError extends Error {
173
181
  readonly reason: string;
174
182
  /** The full (invalid) outcome: `payload`, `reclaimSessionId`, and per-proof
175
183
  * detail (`proofs`) when verification got that far. */
176
- readonly outcome: VerifyResultFullOutcome;
177
- constructor(outcome: VerifyResultFullOutcome);
184
+ readonly outcome: ResultVerificationFailure;
185
+ constructor(outcome: ResultVerificationFailure);
178
186
  }
179
187
  /**
180
- * Full consumer verification: resolves a trusted, registered issuer before
188
+ * Full consumer verification: resolves a configured Builder issuer before
181
189
  * using JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
182
190
  * `reclaimSessionId`/`aud` and any legacy `exp`, then verifies each exact
183
- * legacy proof.
191
+ * proof.
184
192
  * Network access (JWKS and optional provider resolution) makes this async.
185
193
  * Use {@link verifyResult} for the offline primitive.
186
194
  */
package/lib/result.js CHANGED
@@ -3,20 +3,23 @@
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
  */
13
13
  import { sha256 } from '@noble/hashes/sha2.js';
14
14
  import { decrypt as eciesDecrypt, encrypt as eciesEncrypt } from 'eciesjs';
15
- import { b64url, base64urlToBytes, hexToBytes } from "./encoding.js";
15
+ import { b64url, base64urlToBytes, hexToBytes, stripTrailingSlashes, } from "./encoding.js";
16
16
  import { fetchReclaimJwks, jwkToUncompressedPublicKey, selectVerificationResultKey, } from "./jwks.js";
17
- import { verifyProof, } from "./legacy-proof.js";
17
+ import { getHashFromProof } from "./proof-hash.js";
18
18
  import { proofHashesFromBuilderRecipe, } from "./proof-recipe.js";
19
+ import { verifyRequestProof, } from "./request-proof.js";
19
20
  import { addressFromUncompressedPubkey, secp } from "./secp.js";
21
+ /** Longest `iss` worth comparing. Real issuers are short origins. */
22
+ const MAX_ISSUER_LENGTH = 2048;
20
23
  /**
21
24
  * ECIES-encrypt `plaintext` for the holder of `publicJwk` (the org's secp256k1
22
25
  * **eth** public JWK, `{kty:'EC', crv:'secp256k1', x, y}`) — `eciesjs` defaults
@@ -79,9 +82,10 @@ export async function decryptCallback(privateKey, body) {
79
82
  }
80
83
  // Otherwise it's an ECIES ciphertext (base64url).
81
84
  if (!privateKey) {
82
- throw new Error('received an encrypted (ECIES) delivery but no decryption credential was '
83
- + 'configured — pass the eth private key (0x-hex): '
84
- + 'results.receive(body, { credential }) or decrypt(body, credential)');
85
+ throw new Error('received an encrypted (ECIES) delivery but no org ETH private key was '
86
+ + 'provided — pass the 0x-hex private key: '
87
+ + 'results.receive(body, { orgEthPrivateKey }) or '
88
+ + 'decrypt(body, orgEthPrivateKey)');
85
89
  }
86
90
  const plaintext = eciesDecrypt(privateKey, base64urlToBytes(trimmed));
87
91
  return JSON.parse(new TextDecoder().decode(plaintext));
@@ -90,7 +94,7 @@ export async function decryptCallback(privateKey, body) {
90
94
  * Sign a verification result as a flattened JWS (JOSE ES256K = ECDSA/secp256k1
91
95
  * over SHA-256 of the `<protected>.<payload>` signing input — a single hash).
92
96
  * `kid` is the signer's ETH address (published via JWKS); `iss` is the issuer
93
- * URL. Used by the Verification Client to sign results before encrypting them.
97
+ * URL. Used by Builder to sign results before encrypting them.
94
98
  */
95
99
  export function signResultJws(privateKeyHex, payload, iss) {
96
100
  const priv = hexToBytes(privateKeyHex);
@@ -122,6 +126,10 @@ export function verifyJwsSignature(jws, signerPublicKey) {
122
126
  const pub = hexToBytes(signerPublicKey);
123
127
  return jws.signatures.some((s) => {
124
128
  try {
129
+ const header = JSON.parse(new TextDecoder().decode(base64urlToBytes(s.protected)));
130
+ if (header.alg !== 'ES256K') {
131
+ return false;
132
+ }
125
133
  const signingInput = new TextEncoder().encode(`${s.protected}.${jws.payload}`);
126
134
  const digest = sha256(signingInput);
127
135
  const sig = base64urlToBytes(s.signature);
@@ -140,7 +148,9 @@ export function verifyJwsSignature(jws, signerPublicKey) {
140
148
  * the full invalid outcome so callers can inspect it in a `catch`:
141
149
  *
142
150
  * try {
143
- * const { result } = await reclaim.results.receive(body, { credential })
151
+ * const { result } = await reclaim.results.receive(body, {
152
+ * orgEthPrivateKey,
153
+ * })
144
154
  * // result is verified — use result.payload
145
155
  * } catch (err) {
146
156
  * if (err instanceof ResultVerificationError) {
@@ -151,17 +161,17 @@ export function verifyJwsSignature(jws, signerPublicKey) {
151
161
  */
152
162
  export class ResultVerificationError extends Error {
153
163
  constructor(outcome) {
154
- super(`result verification failed: ${outcome.reason ?? 'invalid'}`);
164
+ super(`result verification failed: ${outcome.reason}`);
155
165
  this.name = 'ResultVerificationError';
156
- this.reason = outcome.reason ?? 'invalid';
166
+ this.reason = outcome.reason;
157
167
  this.outcome = outcome;
158
168
  }
159
169
  }
160
170
  /**
161
- * Full consumer verification: resolves a trusted, registered issuer before
171
+ * Full consumer verification: resolves a configured Builder issuer before
162
172
  * using JWKS (header `iss`/`kid`), verifies the top-level JWS, checks
163
173
  * `reclaimSessionId`/`aud` and any legacy `exp`, then verifies each exact
164
- * legacy proof.
174
+ * proof.
165
175
  * Network access (JWKS and optional provider resolution) makes this async.
166
176
  * Use {@link verifyResult} for the offline primitive.
167
177
  */
@@ -169,19 +179,19 @@ export async function verifyResultFull(jws, opts = {}) {
169
179
  const payload = decodeJwsPayload(jws);
170
180
  const reclaimSessionId = payload.reclaimSessionId;
171
181
  let jwksAttestorAddresses = [];
182
+ let jwksAttestorImageDigests = [];
172
183
  /** Throw with the full invalid outcome — verification never *returns* an
173
184
  * invalid result. */
174
185
  const fail = (reason, extra = {}) => {
175
186
  throw new ResultVerificationError({
176
- valid: false,
177
187
  reason,
178
188
  payload,
179
189
  reclaimSessionId,
180
190
  ...extra,
181
191
  });
182
192
  };
183
- // 1. Resolve the VC signer key. Never use the signed header's `iss` as a
184
- // network destination until it has matched an explicit or registered issuer.
193
+ // 1. Resolve Builder's signer key. Never use the signed header's `iss` as a
194
+ // network destination until it has matched an explicitly trusted issuer.
185
195
  let signerPublicKey = opts.signerPublicKey;
186
196
  let header = {};
187
197
  const sig0 = jws.signatures[0];
@@ -194,14 +204,7 @@ export async function verifyResultFull(jws, opts = {}) {
194
204
  }
195
205
  }
196
206
  if (!signerPublicKey) {
197
- const verificationClientId = getVerificationClientId(payload) ?? '';
198
- let configuredIssuers;
199
- try {
200
- configuredIssuers = await resolveTrustedIssuers(opts, verificationClientId);
201
- }
202
- catch {
203
- return fail('issuer-resolution-failed');
204
- }
207
+ const configuredIssuers = resolveTrustedIssuers(opts);
205
208
  if (!configuredIssuers.length) {
206
209
  return fail('issuer-not-configured');
207
210
  }
@@ -209,28 +212,40 @@ export async function verifyResultFull(jws, opts = {}) {
209
212
  if (!iss) {
210
213
  return fail('no-issuer');
211
214
  }
215
+ // Read before the signature is checked, so it is attacker-supplied: a
216
+ // real issuer is a short origin, never kilobytes.
217
+ if (iss.length > MAX_ISSUER_LENGTH) {
218
+ return fail('issuer-not-trusted');
219
+ }
212
220
  const trustedIssuer = configuredIssuers.find((candidate) => (normalizeIssuer(candidate) === normalizeIssuer(iss)));
213
221
  if (!trustedIssuer) {
214
222
  return fail('issuer-not-trusted');
215
223
  }
216
224
  let keys;
217
225
  try {
218
- const jwksUrl = await resolveTrustedJwksUrl(opts, verificationClientId);
219
- const registry = await fetchReclaimJwks(jwksUrl
220
- ?? `${trustedIssuer.replace(/\/+$/, '')}/.well-known/jwks.json`, opts.fetchImpl);
226
+ const registry = await fetchReclaimJwks(opts.jwksUrl
227
+ ?? `${stripTrailingSlashes(trustedIssuer)}/.well-known/jwks.json`, opts.fetchImpl);
221
228
  keys = registry.keys;
222
229
  jwksAttestorAddresses = registry['x-reclaim-attestors'] ?? [];
230
+ jwksAttestorImageDigests =
231
+ registry['x-reclaim-attestor-image-digests'] ?? [];
223
232
  }
224
233
  catch {
225
234
  return fail('jwks-fetch-failed');
226
235
  }
227
- const jwk = selectVerificationResultKey(keys, String(header.kid ?? ''), verificationClientId);
236
+ const jwk = selectVerificationResultKey(keys, String(header.kid ?? ''));
228
237
  if (!jwk) {
229
238
  return fail('signer-key-not-found');
230
239
  }
231
240
  signerPublicKey = jwkToUncompressedPublicKey(jwk);
232
241
  }
233
- if (!verifyJwsSignature(jws, signerPublicKey)) {
242
+ if (header.alg !== 'ES256K') {
243
+ return fail('unsupported-algorithm');
244
+ }
245
+ if (!sig0 || !verifyJwsSignature({
246
+ payload: jws.payload,
247
+ signatures: [sig0],
248
+ }, signerPublicKey)) {
234
249
  return fail('bad-signature');
235
250
  }
236
251
  if (typeof payload.iss === 'string'
@@ -239,8 +254,7 @@ export async function verifyResultFull(jws, opts = {}) {
239
254
  return fail('issuer-mismatch');
240
255
  }
241
256
  // 2. Session binding, audience, and optional legacy result expiry.
242
- const expectedResultSessionId = opts.sessionId
243
- ?? opts.expectedReclaimSessionId;
257
+ const expectedResultSessionId = opts.reclaimSessionId;
244
258
  if (expectedResultSessionId && reclaimSessionId !== expectedResultSessionId) {
245
259
  return fail('reclaimSessionId-mismatch');
246
260
  }
@@ -253,7 +267,7 @@ export async function verifyResultFull(jws, opts = {}) {
253
267
  && payload.exp < now) {
254
268
  return fail('expired');
255
269
  }
256
- // 3. Verify every exact legacy proof carried by the signed result. The outer
270
+ // 3. Verify every exact proof carried by the signed result. The outer
257
271
  // envelope groups proofs by provider, but it never changes the proof shape.
258
272
  const providerResults = Array.isArray(payload.results)
259
273
  ? payload.results
@@ -270,17 +284,24 @@ export async function verifyResultFull(jws, opts = {}) {
270
284
  : [];
271
285
  for (const r of requests) {
272
286
  const requestId = typeof r.request_id === 'string' ? r.request_id : undefined;
273
- if (!isLegacyProof(r.proof)) {
287
+ if (!isReclaimRequestProof(r.proof)) {
274
288
  proofs.push({
275
- providerId, requestId, proofValid: false, reason: 'proof-missing',
289
+ providerId,
290
+ resolvedVersion: providerVersion,
291
+ requestId,
292
+ proofValid: false,
293
+ reason: 'proof-missing',
276
294
  });
277
295
  continue;
278
296
  }
279
297
  proofs.push({
280
298
  providerId,
299
+ resolvedVersion: providerVersion,
281
300
  requestId,
301
+ proof: r.proof,
282
302
  proofValid: true,
283
303
  attestorAddress: r.proof.witnesses[0]?.id,
304
+ attestorAddresses: r.proof.witnesses.map(({ id }) => id),
284
305
  });
285
306
  proofGroups.push({
286
307
  providerId,
@@ -296,25 +317,58 @@ export async function verifyResultFull(jws, opts = {}) {
296
317
  }
297
318
  if (proofGroups.length) {
298
319
  const expectedSessionId = expectedResultSessionId;
299
- const configuredProofValidation = opts.proofValidation
300
- ?? opts.proofVerification;
320
+ const hasTeeBoundProof = proofGroups.some(({ proof }) => (proofHasTeeBinding(proof)));
321
+ const configuredProofValidation = {
322
+ ...opts.proofValidation,
323
+ ...(opts.orgEthPrivateKey && hasTeeBoundProof
324
+ ? {
325
+ teeAttestation: {
326
+ appSecret: opts.orgEthPrivateKey,
327
+ },
328
+ }
329
+ : {}),
330
+ };
301
331
  const attestorAddresses = [...new Set([
302
332
  ...(configuredProofValidation?.attestorAddresses ?? []),
303
333
  ...(opts.attestorAddresses ?? []),
304
334
  ...jwksAttestorAddresses,
305
335
  ])];
336
+ const configuredAttestorTee = configuredProofValidation?.attestorTeeAttestation;
337
+ const configuredImageDigests = configuredAttestorTee?.expectedImageDigests;
338
+ let expectedImageDigests = jwksAttestorImageDigests;
339
+ if (configuredImageDigests?.length && jwksAttestorImageDigests.length) {
340
+ const registry = new Set(jwksAttestorImageDigests);
341
+ expectedImageDigests = [...new Set(configuredImageDigests
342
+ .map((digest) => digest.trim().toLowerCase())
343
+ .filter((digest) => registry.has(digest)))];
344
+ if (!expectedImageDigests.length) {
345
+ return fail('attestor-image-trust-mismatch', { proofs });
346
+ }
347
+ }
348
+ else if (configuredImageDigests?.length) {
349
+ expectedImageDigests = configuredImageDigests;
350
+ }
351
+ const attestorTeeAttestation = expectedImageDigests.length
352
+ ? { ...configuredAttestorTee, expectedImageDigests }
353
+ : configuredAttestorTee;
306
354
  const commonValidation = {
307
355
  ...configuredProofValidation,
308
356
  ...(opts.hashes ? { hashes: opts.hashes } : {}),
309
357
  ...(attestorAddresses.length
310
358
  ? { attestorAddresses }
311
359
  : {}),
360
+ ...(attestorTeeAttestation
361
+ ? { attestorTeeAttestation }
362
+ : {}),
312
363
  ...(expectedSessionId
313
- ? { expectedSessionId }
364
+ ? {
365
+ expectedSessionId,
366
+ requireSessionBinding: configuredProofValidation?.requireSessionBinding ?? true,
367
+ }
314
368
  : {}),
315
369
  };
316
370
  if (hasContentValidation(commonValidation)) {
317
- await verifyLegacyProofGroup(proofGroups, commonValidation);
371
+ await verifyRequestProofGroup(proofGroups, commonValidation);
318
372
  }
319
373
  else {
320
374
  await verifyByResolvedRecipes(commonValidation, expectedSessionId);
@@ -323,20 +377,32 @@ export async function verifyResultFull(jws, opts = {}) {
323
377
  if (!proofs.every((p) => p.proofValid)) {
324
378
  return fail('invalid-proof', { proofs });
325
379
  }
326
- return { valid: true, payload, reclaimSessionId, proofs };
327
- async function verifyLegacyProofGroup(group, validation) {
328
- const legacyOutcome = await verifyProof(group.map(({ proof }) => proof), validation);
329
- if (legacyOutcome.valid) {
380
+ return { payload, reclaimSessionId, proofs };
381
+ async function verifyRequestProofGroup(group, validation) {
382
+ const requestProofOutcome = await verifyRequestProof(group.map(({ proof }) => proof), validation);
383
+ if (requestProofOutcome.valid) {
330
384
  for (const [index, item] of group.entries()) {
331
- proofs[item.outcomeIndex].data = legacyOutcome.data?.[index];
332
- proofs[item.outcomeIndex].publicData =
333
- legacyOutcome.publicData?.[index];
385
+ const outcome = proofs[item.outcomeIndex];
386
+ const hashes = resultProofHashes(item.proof, validation.hasNoPii);
387
+ if (hashes) {
388
+ outcome.proofHashes = hashes;
389
+ }
390
+ if (requestProofOutcome.isTeeAttestationVerified !== undefined) {
391
+ outcome.isTeeAttestationVerified =
392
+ requestProofOutcome.isTeeAttestationVerified;
393
+ }
394
+ if (requestProofOutcome.isAttestorTeeAttestationVerified !== undefined) {
395
+ outcome.isAttestorTeeAttestationVerified =
396
+ requestProofOutcome.isAttestorTeeAttestationVerified;
397
+ }
398
+ outcome.data = requestProofOutcome.data?.[index];
399
+ outcome.publicData = requestProofOutcome.publicData?.[index];
334
400
  }
335
401
  return;
336
402
  }
337
403
  for (const { outcomeIndex } of group) {
338
404
  proofs[outcomeIndex].proofValid = false;
339
- proofs[outcomeIndex].reason = legacyOutcome.reason;
405
+ proofs[outcomeIndex].reason = requestProofOutcome.reason;
340
406
  }
341
407
  }
342
408
  async function verifyByResolvedRecipes(common, expectedSessionId) {
@@ -377,7 +443,7 @@ export async function verifyResultFull(jws, opts = {}) {
377
443
  return;
378
444
  }
379
445
  }
380
- await verifyLegacyProofGroup(group, {
446
+ const validation = {
381
447
  ...common,
382
448
  ...(recipe
383
449
  ? { hashes }
@@ -385,7 +451,15 @@ export async function verifyResultFull(jws, opts = {}) {
385
451
  ? { providerId, providerVersion }
386
452
  : {}),
387
453
  ...providerValidation,
388
- });
454
+ ...(common.expectedSessionId
455
+ ? {
456
+ sessionId: common.sessionId,
457
+ expectedSessionId: common.expectedSessionId,
458
+ requireSessionBinding: common.requireSessionBinding,
459
+ }
460
+ : {}),
461
+ };
462
+ await verifyRequestProofGroup(group, validation);
389
463
  }
390
464
  function markInvalid(group, reason) {
391
465
  for (const { outcomeIndex } of group) {
@@ -394,6 +468,36 @@ export async function verifyResultFull(jws, opts = {}) {
394
468
  }
395
469
  }
396
470
  }
471
+ /** Whether a proof declares the session TEE nonce that the org key verifies. */
472
+ function proofHasTeeBinding(proof) {
473
+ // A `confidential-space` witness carries the attestation itself, so the
474
+ // proof is TEE-bound even before the context is read. Triggering on it too
475
+ // means a proof that ships the evidence can't skip the check by omitting
476
+ // `attestationNonce` — `verifyTeeAttestation` needs that nonce and fails
477
+ // loudly without it, which is the right outcome.
478
+ const hasTeeWitness = (proof.witnesses ?? []).some((witness) => (witness.claimAttestation?.type === 'confidential-space'
479
+ && !!witness.claimAttestation.tee_attestation));
480
+ if (hasTeeWitness) {
481
+ return true;
482
+ }
483
+ try {
484
+ const context = JSON.parse(proof.claimData.context);
485
+ return context && typeof context === 'object'
486
+ && typeof context.attestationNonce === 'string'
487
+ && context.attestationNonce.length > 0;
488
+ }
489
+ catch {
490
+ return false;
491
+ }
492
+ }
493
+ function resultProofHashes(proof, hasNoPii) {
494
+ try {
495
+ return getHashFromProof(proof, hasNoPii);
496
+ }
497
+ catch {
498
+ return undefined;
499
+ }
500
+ }
397
501
  function hasContentValidation(config) {
398
502
  return Boolean(config.hashes
399
503
  || config.providerId
@@ -407,7 +511,7 @@ function recipeGroupsMatch(recipes, groups) {
407
511
  return expected.size === groups.size
408
512
  && [...groups.keys()].every((key) => expected.has(key));
409
513
  }
410
- function isLegacyProof(value) {
514
+ function isReclaimRequestProof(value) {
411
515
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
412
516
  return false;
413
517
  }
@@ -428,53 +532,10 @@ function getVerificationStatus(payload) {
428
532
  return typeof status === 'string' ? status : undefined;
429
533
  }
430
534
  function normalizeIssuer(issuer) {
431
- return issuer.trim().replace(/\/+$/, '').toLowerCase();
535
+ return stripTrailingSlashes(issuer.trim()).toLowerCase();
432
536
  }
433
- function getVerificationClientId(payload) {
434
- if (typeof payload.verificationClientId === 'string') {
435
- return payload.verificationClientId;
436
- }
437
- if (typeof payload.verification_client_id === 'string') {
438
- return payload.verification_client_id;
439
- }
440
- const session = payload.verification_session;
441
- if (!session || typeof session !== 'object' || Array.isArray(session)) {
442
- return undefined;
443
- }
444
- const sessionRecord = session;
445
- const id = sessionRecord.verificationClientId
446
- ?? sessionRecord.verification_client_id;
447
- return typeof id === 'string' ? id : undefined;
448
- }
449
- async function resolveTrustedIssuers(opts, verificationClientId) {
450
- const explicit = [
451
- opts.issuer,
452
- opts.trustedIssuer,
453
- ...(opts.issuers ?? []),
454
- ...(opts.trustedIssuers ?? []),
455
- ].filter((issuer) => typeof issuer === 'string');
456
- if (explicit.length) {
457
- return [...new Set(explicit.map(normalizeIssuer))];
458
- }
459
- if (!verificationClientId) {
460
- return [];
461
- }
462
- const registered = opts.issuersByVerificationClientId?.[verificationClientId] ?? [];
463
- const resolve = opts.resolveIssuers ?? opts.issuerResolver;
464
- const resolved = resolve
465
- ? await resolve(verificationClientId)
466
- : [];
467
- return [...new Set([...registered, ...resolved].map(normalizeIssuer))];
468
- }
469
- async function resolveTrustedJwksUrl(opts, verificationClientId) {
470
- if (opts.jwksUrl) {
471
- return opts.jwksUrl;
472
- }
473
- if (!verificationClientId) {
474
- return undefined;
475
- }
476
- const registered = opts.jwksUrlsByVerificationClientId?.[verificationClientId];
477
- return registered ?? await opts.resolveJwksUrl?.(verificationClientId);
537
+ function resolveTrustedIssuers(opts) {
538
+ return [...new Set((opts.trustedIssuers ?? []).map(normalizeIssuer))];
478
539
  }
479
540
  /** Decrypt-and-verify in one step. */
480
541
  export function verifyResult(jws, opts = {}) {
@@ -482,17 +543,16 @@ export function verifyResult(jws, opts = {}) {
482
543
  const reclaimSessionId = payload.reclaimSessionId;
483
544
  if (opts.signerPublicKey && !verifyJwsSignature(jws, opts.signerPublicKey)) {
484
545
  throw new ResultVerificationError({
485
- valid: false, reason: 'bad-signature', payload, reclaimSessionId,
546
+ reason: 'bad-signature', payload, reclaimSessionId,
486
547
  });
487
548
  }
488
- if (opts.expectedReclaimSessionId &&
489
- reclaimSessionId !== opts.expectedReclaimSessionId) {
549
+ if (opts.reclaimSessionId &&
550
+ reclaimSessionId !== opts.reclaimSessionId) {
490
551
  throw new ResultVerificationError({
491
- valid: false,
492
552
  reason: 'reclaimSessionId-mismatch',
493
553
  payload,
494
554
  reclaimSessionId,
495
555
  });
496
556
  }
497
- return { valid: true, payload, reclaimSessionId };
557
+ return { payload, reclaimSessionId };
498
558
  }