@reclaimprotocol/client 0.1.29 → 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.
- 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 +1594 -488
- 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 +1599 -488
- package/src/types/openapi.ts +3 -1
- package/src/verification-clients.ts +33 -19
- package/src/verify-attestor-tee.ts +66 -8
- package/src/verify-tee.ts +44 -13
- package/lib/api-client.d.ts +0 -31
- package/lib/api-client.js +0 -112
- package/lib/api-consts.d.ts +0 -4
- package/lib/api-consts.js +0 -4
- package/lib/api-errors.d.ts +0 -13
- package/lib/api-errors.js +0 -30
- package/lib/eip712.d.ts +0 -20
- package/lib/eip712.js +0 -108
package/lib/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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
117
|
-
/**
|
|
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
|
|
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, {
|
|
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:
|
|
177
|
-
constructor(outcome:
|
|
184
|
+
readonly outcome: ResultVerificationFailure;
|
|
185
|
+
constructor(outcome: ResultVerificationFailure);
|
|
178
186
|
}
|
|
179
187
|
/**
|
|
180
|
-
* Full consumer verification: resolves a
|
|
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
|
-
*
|
|
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
|
|
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
|
*/
|
|
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 {
|
|
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
|
|
83
|
-
+ '
|
|
84
|
-
+ 'results.receive(body, {
|
|
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
|
|
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, {
|
|
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
|
|
164
|
+
super(`result verification failed: ${outcome.reason}`);
|
|
155
165
|
this.name = 'ResultVerificationError';
|
|
156
|
-
this.reason = outcome.reason
|
|
166
|
+
this.reason = outcome.reason;
|
|
157
167
|
this.outcome = outcome;
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
/**
|
|
161
|
-
* Full consumer verification: resolves a
|
|
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
|
-
*
|
|
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
|
|
184
|
-
// network destination until it has matched an
|
|
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
|
|
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
|
|
219
|
-
|
|
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 ?? '')
|
|
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 (
|
|
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.
|
|
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
|
|
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 (!
|
|
287
|
+
if (!isReclaimRequestProof(r.proof)) {
|
|
274
288
|
proofs.push({
|
|
275
|
-
providerId,
|
|
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
|
|
300
|
-
|
|
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
|
-
? {
|
|
364
|
+
? {
|
|
365
|
+
expectedSessionId,
|
|
366
|
+
requireSessionBinding: configuredProofValidation?.requireSessionBinding ?? true,
|
|
367
|
+
}
|
|
314
368
|
: {}),
|
|
315
369
|
};
|
|
316
370
|
if (hasContentValidation(commonValidation)) {
|
|
317
|
-
await
|
|
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 {
|
|
327
|
-
async function
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
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]
|
|
332
|
-
|
|
333
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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()
|
|
535
|
+
return stripTrailingSlashes(issuer.trim()).toLowerCase();
|
|
432
536
|
}
|
|
433
|
-
function
|
|
434
|
-
|
|
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
|
-
|
|
546
|
+
reason: 'bad-signature', payload, reclaimSessionId,
|
|
486
547
|
});
|
|
487
548
|
}
|
|
488
|
-
if (opts.
|
|
489
|
-
reclaimSessionId !== opts.
|
|
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 {
|
|
557
|
+
return { payload, reclaimSessionId };
|
|
498
558
|
}
|