@reclaimprotocol/client 0.1.27 → 0.1.28
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 +634 -42
- package/lib/api-client.d.ts +31 -0
- package/lib/api-client.js +112 -0
- package/lib/api-consts.d.ts +4 -0
- package/lib/api-consts.js +4 -0
- package/lib/api-errors.d.ts +13 -0
- package/lib/api-errors.js +30 -0
- package/lib/api.d.ts +7 -0
- package/lib/api.js +3 -0
- package/lib/attestor-proof.d.ts +6 -0
- package/lib/attestor-proof.js +45 -18
- package/lib/client.d.ts +1 -6
- package/lib/client.js +12 -33
- package/lib/consts.d.ts +1 -1
- package/lib/consts.js +1 -1
- package/lib/eip191.d.ts +4 -2
- package/lib/eip191.js +17 -1
- package/lib/errors.d.ts +38 -4
- package/lib/errors.js +99 -16
- package/lib/index.d.ts +9 -7
- package/lib/index.js +8 -4
- package/lib/jwks.d.ts +25 -1
- package/lib/jwks.js +69 -4
- package/lib/legacy-proof.d.ts +57 -0
- package/lib/legacy-proof.js +211 -0
- package/lib/proof-hash.d.ts +35 -0
- package/lib/proof-hash.js +216 -0
- package/lib/proof-recipe.d.ts +18 -0
- package/lib/proof-recipe.js +37 -0
- package/lib/proof-trust-constants.d.ts +3 -0
- package/lib/proof-trust-constants.js +43 -0
- package/lib/proof-types.d.ts +62 -0
- package/lib/proof-types.js +1 -0
- package/lib/proof-verification-error.d.ts +13 -0
- package/lib/proof-verification-error.js +11 -0
- package/lib/reclaim.d.ts +27 -5
- package/lib/reclaim.js +140 -14
- package/lib/result.d.ts +58 -6
- package/lib/result.js +252 -61
- package/lib/routes.gen.d.ts +54 -6
- package/lib/routes.gen.js +15 -3
- package/lib/tee-binding.d.ts +82 -0
- package/lib/tee-binding.js +112 -0
- package/lib/types/openapi.gen.d.ts +1343 -206
- package/lib/verification-clients.d.ts +17 -0
- package/lib/verification-clients.js +14 -0
- package/lib/verify-attestor-tee.d.ts +76 -0
- package/lib/verify-attestor-tee.js +243 -0
- package/lib/verify-tee.d.ts +32 -0
- package/lib/verify-tee.js +347 -0
- package/package.json +5 -1
- package/src/api.ts +15 -0
- package/src/attestor-proof.ts +57 -20
- package/src/client.ts +15 -38
- package/src/consts.ts +1 -2
- package/src/eip191.ts +31 -3
- package/src/errors.ts +132 -18
- package/src/index.ts +50 -5
- package/src/jwks.ts +128 -5
- package/src/legacy-proof.ts +367 -0
- package/src/proof-hash.ts +316 -0
- package/src/proof-recipe.ts +75 -0
- package/src/proof-trust-constants.ts +45 -0
- package/src/proof-types.ts +57 -0
- package/src/proof-verification-error.ts +13 -0
- package/src/reclaim.ts +206 -19
- package/src/result.ts +403 -68
- package/src/routes.gen.ts +15 -3
- package/src/tee-binding.ts +192 -0
- package/src/types/openapi.gen.ts +1343 -206
- package/src/verification-clients.ts +22 -0
- package/src/verify-attestor-tee.ts +365 -0
- package/src/verify-tee.ts +507 -0
package/lib/errors.js
CHANGED
|
@@ -1,22 +1,105 @@
|
|
|
1
|
-
import { Boom } from '@hapi/boom';
|
|
2
1
|
import { PROBLEM_CONTENT_TYPE } from "./consts.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/** Returns whether a thrown value is a Builder {@link ProblemError}. */
|
|
3
|
+
export function isProblemError(error) {
|
|
4
|
+
if (!(error instanceof Error) || !('isBoom' in error)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
if (error.isBoom !== true
|
|
8
|
+
|| !('isServer' in error)
|
|
9
|
+
|| typeof error.isServer !== 'boolean'
|
|
10
|
+
|| !('data' in error)
|
|
11
|
+
|| !('output' in error)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return isProblemDetails(error.data)
|
|
15
|
+
&& isProblemErrorOutput(error.output);
|
|
16
|
+
}
|
|
17
|
+
/** Returns whether a value follows the RFC 9457 problem-details schema. */
|
|
18
|
+
export function isProblemDetails(value) {
|
|
19
|
+
if (!isRecord(value)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return isOptionalString(value.type)
|
|
23
|
+
&& isOptionalString(value.title)
|
|
24
|
+
&& isOptionalStatus(value.status)
|
|
25
|
+
&& isOptionalString(value.detail)
|
|
26
|
+
&& isOptionalString(value.instance)
|
|
27
|
+
&& isOptionalString(value.event)
|
|
28
|
+
&& isOptionalString(value.sessionId);
|
|
29
|
+
}
|
|
30
|
+
/** Reads an RFC 9457 response, with a status-only fallback for other bodies. */
|
|
31
|
+
export async function readProblemDetails(res) {
|
|
6
32
|
const contentType = res.headers.get('Content-Type') || '';
|
|
7
|
-
let problem;
|
|
8
33
|
if (contentType.includes(PROBLEM_CONTENT_TYPE)) {
|
|
9
|
-
|
|
34
|
+
try {
|
|
35
|
+
const value = await res.json();
|
|
36
|
+
if (isProblemDetails(value)) {
|
|
37
|
+
return {
|
|
38
|
+
...value,
|
|
39
|
+
type: value.type || 'about:blank',
|
|
40
|
+
title: value.title || res.statusText,
|
|
41
|
+
status: value.status || res.status,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Fall back to the HTTP status when the problem body isn't valid JSON.
|
|
47
|
+
}
|
|
10
48
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
49
|
+
return {
|
|
50
|
+
type: 'about:blank',
|
|
51
|
+
status: res.status,
|
|
52
|
+
title: res.statusText,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Throws a browser-safe, Boom-compatible RFC 9457 error. */
|
|
56
|
+
export async function throwProblemError(res) {
|
|
57
|
+
const problem = await readProblemDetails(res);
|
|
58
|
+
const title = problem.title || res.statusText || 'Request failed';
|
|
59
|
+
const fields = {
|
|
60
|
+
isBoom: true,
|
|
61
|
+
isServer: res.status >= 500,
|
|
20
62
|
data: problem,
|
|
21
|
-
|
|
63
|
+
output: {
|
|
64
|
+
statusCode: res.status,
|
|
65
|
+
headers: { 'content-type': PROBLEM_CONTENT_TYPE },
|
|
66
|
+
payload: {
|
|
67
|
+
...problem,
|
|
68
|
+
statusCode: res.status,
|
|
69
|
+
error: title,
|
|
70
|
+
message: problem.detail || title,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const error = Object.assign(new Error(title), fields);
|
|
75
|
+
error.name = 'ProblemError';
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function isProblemErrorOutput(value) {
|
|
82
|
+
if (!isRecord(value) || !isRecord(value.payload)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return typeof value.statusCode === 'number'
|
|
86
|
+
&& isStringRecord(value.headers)
|
|
87
|
+
&& isProblemDetails(value.payload)
|
|
88
|
+
&& typeof value.payload.statusCode === 'number'
|
|
89
|
+
&& typeof value.payload.error === 'string'
|
|
90
|
+
&& typeof value.payload.message === 'string';
|
|
91
|
+
}
|
|
92
|
+
function isStringRecord(value) {
|
|
93
|
+
return isRecord(value)
|
|
94
|
+
&& Object.values(value).every(item => typeof item === 'string');
|
|
95
|
+
}
|
|
96
|
+
function isOptionalString(value) {
|
|
97
|
+
return value === undefined || typeof value === 'string';
|
|
98
|
+
}
|
|
99
|
+
function isOptionalStatus(value) {
|
|
100
|
+
return value === undefined
|
|
101
|
+
|| typeof value === 'number'
|
|
102
|
+
&& Number.isInteger(value)
|
|
103
|
+
&& value >= 100
|
|
104
|
+
&& value <= 599;
|
|
22
105
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export
|
|
3
|
-
export { throwProblemError } from './errors.ts';
|
|
4
|
-
export type { ProblemDetails, ProblemError } from './errors.ts';
|
|
5
|
-
export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, USER_AGENT } from './consts.ts';
|
|
1
|
+
export * from './api.ts';
|
|
2
|
+
export { USER_AGENT } from './consts.ts';
|
|
6
3
|
export { addressFromPublicKey, deriveFromPrivateKey, type EthKeypair, generateKeypair, } from './keypair.ts';
|
|
7
4
|
export { canonicalJson } from './canonical.ts';
|
|
8
5
|
export { b64url, base64urlToBytes, bytesToHex, hexToBytes, } from './encoding.ts';
|
|
@@ -10,6 +7,11 @@ export { addressFromUncompressedPubkey, secp, } from './secp.ts';
|
|
|
10
7
|
export { eip191Digest, signEip191 } from './eip191.ts';
|
|
11
8
|
export { type CallbackBody, decodeJwsPayload, decryptCallback, type DeliveryEnvelope, encryptForEthPublicKey, type Jws, ResultVerificationError, type ResultProofOutcome, signResultJws, verifyJwsSignature, verifyResult, verifyResultFull, type VerifyResultFullOptions, type VerifyResultFullOutcome, type VerifyResultOptions, type VerifyResultOutcome, } from './result.ts';
|
|
12
9
|
export { type DecryptionKey, type DecryptionKeyInput, generateEthKeypair, type GeneratedEthKeypair, loadDecryptionKey, pubkeyJwkToEthAddress, uncompressedPubkeyToJwk, } from './enckeys.ts';
|
|
13
|
-
export { createReclaim, type CreateSessionInput, type Reclaim, type ReclaimConfig, type ReceiveOptions, type ReceivedDelivery, type RegisterCallbackInput, type SetKeypairInput, TERMINAL_EVENTS, } from './reclaim.ts';
|
|
14
|
-
export { fetchJwks, type Jwk, jwksFromPrivateKey, jwkToUncompressedPublicKey, selectKey, } from './jwks.ts';
|
|
10
|
+
export { createReclaim, type CreateSessionInput, type CreateSessionTeeAttestation, type Reclaim, type ReclaimConfig, type ReceiveOptions, type ReceivedDelivery, type RegisterCallbackInput, type SetKeypairInput, TERMINAL_EVENTS, } from './reclaim.ts';
|
|
11
|
+
export { fetchJwks, fetchJwksUrl, fetchReclaimJwks, type Jwk, jwksFromPrivateKey, jwkToUncompressedPublicKey, type ReclaimJwks, selectKey, selectVerificationResultKey, trustedAttestorAddresses, validateAttestorAddresses, } from './jwks.ts';
|
|
15
12
|
export { type NestedProofOutcome, verifyNestedProof, } from './attestor-proof.ts';
|
|
13
|
+
export { type LegacyProof, type LegacyProofOutcome, type LegacyVerifyProofOptions, type ProofHashRequirement, proofHashes, verifyProof, } from './legacy-proof.ts';
|
|
14
|
+
export { type BuilderProofRecipe, proofHashesFromBuilderRecipe, } from './proof-recipe.ts';
|
|
15
|
+
export { getHashFromProof, hashProofClaimParams, hashRequestSpec, type HttpProofParams, type ProofRequestSpec, } from './proof-hash.ts';
|
|
16
|
+
export { builderTeeBindingPayload, BUILDER_TEE_BINDING_DOMAIN, createBuilderTeeBinding, createLegacyTeeNonceContext, deriveLegacySignatureTeeNonce, deriveLegacyTeeNonce, LEGACY_TEE_ATTESTATION_VERSION, LEGACY_TEE_NONCE_DOMAIN, legacyApplicationIdFromPrivateKey, signBuilderTeeBinding, type BuilderTeeBinding, type BuilderTeeBindingPayload, type LegacyTeeNonceContext, type LegacyTeeNonceData, type LegacyTeeNonceMode, } from './tee-binding.ts';
|
|
17
|
+
export { KNOWN_VERIFICATION_CLIENTS, type KnownVerificationClientName, type KnownVerificationClientUrlMap, } from './verification-clients.ts';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export {
|
|
3
|
-
export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, USER_AGENT } from "./consts.js";
|
|
1
|
+
export * from "./api.js";
|
|
2
|
+
export { USER_AGENT } from "./consts.js";
|
|
4
3
|
export { addressFromPublicKey, deriveFromPrivateKey, generateKeypair, } from "./keypair.js";
|
|
5
4
|
export { canonicalJson } from "./canonical.js";
|
|
6
5
|
export { b64url, base64urlToBytes, bytesToHex, hexToBytes, } from "./encoding.js";
|
|
@@ -9,5 +8,10 @@ export { eip191Digest, signEip191 } from "./eip191.js";
|
|
|
9
8
|
export { decodeJwsPayload, decryptCallback, encryptForEthPublicKey, ResultVerificationError, signResultJws, verifyJwsSignature, verifyResult, verifyResultFull, } from "./result.js";
|
|
10
9
|
export { generateEthKeypair, loadDecryptionKey, pubkeyJwkToEthAddress, uncompressedPubkeyToJwk, } from "./enckeys.js";
|
|
11
10
|
export { createReclaim, TERMINAL_EVENTS, } from "./reclaim.js";
|
|
12
|
-
export { fetchJwks, jwksFromPrivateKey, jwkToUncompressedPublicKey, selectKey, } from "./jwks.js";
|
|
11
|
+
export { fetchJwks, fetchJwksUrl, fetchReclaimJwks, jwksFromPrivateKey, jwkToUncompressedPublicKey, selectKey, selectVerificationResultKey, trustedAttestorAddresses, validateAttestorAddresses, } from "./jwks.js";
|
|
13
12
|
export { verifyNestedProof, } from "./attestor-proof.js";
|
|
13
|
+
export { proofHashes, verifyProof, } from "./legacy-proof.js";
|
|
14
|
+
export { proofHashesFromBuilderRecipe, } from "./proof-recipe.js";
|
|
15
|
+
export { getHashFromProof, hashProofClaimParams, hashRequestSpec, } from "./proof-hash.js";
|
|
16
|
+
export { builderTeeBindingPayload, BUILDER_TEE_BINDING_DOMAIN, createBuilderTeeBinding, createLegacyTeeNonceContext, deriveLegacySignatureTeeNonce, deriveLegacyTeeNonce, LEGACY_TEE_ATTESTATION_VERSION, LEGACY_TEE_NONCE_DOMAIN, legacyApplicationIdFromPrivateKey, signBuilderTeeBinding, } from "./tee-binding.js";
|
|
17
|
+
export { KNOWN_VERIFICATION_CLIENTS, } from "./verification-clients.js";
|
package/lib/jwks.d.ts
CHANGED
|
@@ -6,19 +6,43 @@ export interface Jwk {
|
|
|
6
6
|
kid?: string;
|
|
7
7
|
alg?: string;
|
|
8
8
|
use?: string;
|
|
9
|
+
key_ops?: string[];
|
|
10
|
+
/** Restricts how Builder consumers may use this key. */
|
|
11
|
+
'x-reclaim-purpose'?: 'verification-result' | 'attestor-proof';
|
|
12
|
+
/** Verification Client that owns a result-signing key. */
|
|
13
|
+
'x-reclaim-verification-client-id'?: string;
|
|
14
|
+
/** Verification Clients that share a built-in result-signing key. */
|
|
15
|
+
'x-reclaim-verification-client-ids'?: string[];
|
|
16
|
+
/** Attestor proofs use EIP-191 rather than JOSE ES256K. */
|
|
17
|
+
'x-reclaim-signature-scheme'?: 'EIP-191';
|
|
18
|
+
}
|
|
19
|
+
export interface ReclaimJwks {
|
|
20
|
+
keys: Jwk[];
|
|
21
|
+
/** Trusted EIP-191 attestor signer addresses (public-key hashes). */
|
|
22
|
+
'x-reclaim-attestors'?: string[];
|
|
9
23
|
}
|
|
10
24
|
/**
|
|
11
25
|
* Build the JWK set for an ES256K signing key (the Verification Client's
|
|
12
26
|
* result-signing key). `kid` is the key's ETH address; `x`/`y` are the
|
|
13
27
|
* uncompressed public-key coordinates. Empty set when no key is configured.
|
|
14
28
|
*/
|
|
15
|
-
export declare function jwksFromPrivateKey(privateKeyHex?: string): {
|
|
29
|
+
export declare function jwksFromPrivateKey(privateKeyHex?: string, metadata?: Pick<Jwk, 'x-reclaim-purpose' | 'x-reclaim-verification-client-id' | 'x-reclaim-verification-client-ids'>): {
|
|
16
30
|
keys: Jwk[];
|
|
17
31
|
};
|
|
18
32
|
/** Fetch the JWK set published at `<iss>/.well-known/jwks.json`. */
|
|
19
33
|
export declare function fetchJwks(iss: string, fetchImpl?: typeof fetch): Promise<Jwk[]>;
|
|
34
|
+
/** Fetch a JWK set from an explicitly trusted registry URL. */
|
|
35
|
+
export declare function fetchJwksUrl(url: string, fetchImpl?: typeof fetch): Promise<Jwk[]>;
|
|
36
|
+
/** Fetch Builder's JWKS plus its namespaced attestor trust registry. */
|
|
37
|
+
export declare function fetchReclaimJwks(url: string, fetchImpl?: typeof fetch): Promise<ReclaimJwks>;
|
|
20
38
|
/** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
|
|
21
39
|
export declare function selectKey(keys: Jwk[], kid: string): Jwk | undefined;
|
|
40
|
+
/** Select a result key and enforce its Verification Client ownership. */
|
|
41
|
+
export declare function selectVerificationResultKey(keys: Jwk[], kid: string, verificationClientId: string): Jwk | undefined;
|
|
42
|
+
/** Return ETH addresses from keys explicitly registered for attestor proofs. */
|
|
43
|
+
export declare function trustedAttestorAddresses(keys: Jwk[]): string[];
|
|
44
|
+
/** Validate and normalize the JWKS address-only trust extension. */
|
|
45
|
+
export declare function validateAttestorAddresses(addresses: string[]): string[];
|
|
22
46
|
/**
|
|
23
47
|
* Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
|
|
24
48
|
* EC JWK's base64url `x`/`y` coordinates, for signature verification.
|
package/lib/jwks.js
CHANGED
|
@@ -10,7 +10,7 @@ import { addressFromUncompressedPubkey, secp } from "./secp.js";
|
|
|
10
10
|
* result-signing key). `kid` is the key's ETH address; `x`/`y` are the
|
|
11
11
|
* uncompressed public-key coordinates. Empty set when no key is configured.
|
|
12
12
|
*/
|
|
13
|
-
export function jwksFromPrivateKey(privateKeyHex) {
|
|
13
|
+
export function jwksFromPrivateKey(privateKeyHex, metadata = {}) {
|
|
14
14
|
if (!privateKeyHex) {
|
|
15
15
|
return { keys: [] };
|
|
16
16
|
}
|
|
@@ -23,6 +23,8 @@ export function jwksFromPrivateKey(privateKeyHex) {
|
|
|
23
23
|
crv: 'secp256k1',
|
|
24
24
|
alg: 'ES256K',
|
|
25
25
|
use: 'sig',
|
|
26
|
+
key_ops: ['verify'],
|
|
27
|
+
...metadata,
|
|
26
28
|
kid: addressFromUncompressedPubkey(pub),
|
|
27
29
|
x: b64url(pub.subarray(1, 33)),
|
|
28
30
|
y: b64url(pub.subarray(33, 65)),
|
|
@@ -33,18 +35,65 @@ export function jwksFromPrivateKey(privateKeyHex) {
|
|
|
33
35
|
/** Fetch the JWK set published at `<iss>/.well-known/jwks.json`. */
|
|
34
36
|
export async function fetchJwks(iss, fetchImpl = fetch) {
|
|
35
37
|
const base = iss.replace(/\/+$/, '');
|
|
36
|
-
|
|
38
|
+
return fetchJwksUrl(`${base}/.well-known/jwks.json`, fetchImpl);
|
|
39
|
+
}
|
|
40
|
+
/** Fetch a JWK set from an explicitly trusted registry URL. */
|
|
41
|
+
export async function fetchJwksUrl(url, fetchImpl = fetch) {
|
|
42
|
+
const jwks = await fetchReclaimJwks(url, fetchImpl);
|
|
43
|
+
return jwks.keys;
|
|
44
|
+
}
|
|
45
|
+
/** Fetch Builder's JWKS plus its namespaced attestor trust registry. */
|
|
46
|
+
export async function fetchReclaimJwks(url, fetchImpl = fetch) {
|
|
47
|
+
const res = await fetchImpl(url);
|
|
37
48
|
if (!res.ok) {
|
|
38
|
-
throw new Error(`JWKS fetch failed for ${
|
|
49
|
+
throw new Error(`JWKS fetch failed for ${url}: HTTP ${res.status}`);
|
|
39
50
|
}
|
|
40
51
|
const body = (await res.json());
|
|
41
|
-
return
|
|
52
|
+
return {
|
|
53
|
+
keys: body.keys ?? [],
|
|
54
|
+
'x-reclaim-attestors': validateAttestorAddresses(body['x-reclaim-attestors'] ?? []),
|
|
55
|
+
};
|
|
42
56
|
}
|
|
43
57
|
/** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
|
|
44
58
|
export function selectKey(keys, kid) {
|
|
45
59
|
const want = kid.toLowerCase();
|
|
46
60
|
return keys.find((k) => (k.kid ?? '').toLowerCase() === want);
|
|
47
61
|
}
|
|
62
|
+
/** Select a result key and enforce its Verification Client ownership. */
|
|
63
|
+
export function selectVerificationResultKey(keys, kid, verificationClientId) {
|
|
64
|
+
return keys.find((key) => ((key.kid ?? '').toLowerCase() === kid.toLowerCase()
|
|
65
|
+
&& key['x-reclaim-purpose'] !== 'attestor-proof'
|
|
66
|
+
&& belongsToVerificationClient(key, verificationClientId)));
|
|
67
|
+
}
|
|
68
|
+
/** Return ETH addresses from keys explicitly registered for attestor proofs. */
|
|
69
|
+
export function trustedAttestorAddresses(keys) {
|
|
70
|
+
return keys
|
|
71
|
+
.filter((key) => key['x-reclaim-purpose'] === 'attestor-proof')
|
|
72
|
+
.map((key) => {
|
|
73
|
+
if (key.kty !== 'EC'
|
|
74
|
+
|| key.crv !== 'secp256k1'
|
|
75
|
+
|| key.use !== 'sig'
|
|
76
|
+
|| key['x-reclaim-signature-scheme'] !== 'EIP-191'
|
|
77
|
+
|| (key.key_ops && !key.key_ops.includes('verify'))) {
|
|
78
|
+
throw new Error('invalid attestor JWK metadata');
|
|
79
|
+
}
|
|
80
|
+
const address = addressFromUncompressedPubkey(hexPublicKeyBytes(jwkToUncompressedPublicKey(key))).toLowerCase();
|
|
81
|
+
if ((key.kid ?? '').toLowerCase() !== address) {
|
|
82
|
+
throw new Error('attestor JWK kid does not match its ETH public key');
|
|
83
|
+
}
|
|
84
|
+
return address;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Validate and normalize the JWKS address-only trust extension. */
|
|
88
|
+
export function validateAttestorAddresses(addresses) {
|
|
89
|
+
return [...new Set(addresses.map((address) => {
|
|
90
|
+
const normalized = address.trim().toLowerCase();
|
|
91
|
+
if (!/^0x[0-9a-f]{40}$/.test(normalized)) {
|
|
92
|
+
throw new Error('invalid attestor address in JWKS trust registry');
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}))];
|
|
96
|
+
}
|
|
48
97
|
/**
|
|
49
98
|
* Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
|
|
50
99
|
* EC JWK's base64url `x`/`y` coordinates, for signature verification.
|
|
@@ -61,3 +110,19 @@ export function jwkToUncompressedPublicKey(jwk) {
|
|
|
61
110
|
out.set(y, 33);
|
|
62
111
|
return '0x' + bytesToHex(out);
|
|
63
112
|
}
|
|
113
|
+
function belongsToVerificationClient(key, verificationClientId) {
|
|
114
|
+
const one = key['x-reclaim-verification-client-id'];
|
|
115
|
+
const many = key['x-reclaim-verification-client-ids'];
|
|
116
|
+
if (one) {
|
|
117
|
+
return one === verificationClientId;
|
|
118
|
+
}
|
|
119
|
+
if (many) {
|
|
120
|
+
return many.includes(verificationClientId);
|
|
121
|
+
}
|
|
122
|
+
// Untagged self-hosted JWKS remain compatible because their URL is itself
|
|
123
|
+
// selected from the registered Verification Client record.
|
|
124
|
+
return key['x-reclaim-purpose'] !== 'verification-result';
|
|
125
|
+
}
|
|
126
|
+
function hexPublicKeyBytes(value) {
|
|
127
|
+
return Uint8Array.from(Buffer.from(value.slice(2), 'hex'));
|
|
128
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HashRequirement, LegacyProof, TrustedData } from './proof-types.ts';
|
|
2
|
+
import { type AttestorTeeAttestationConfig } from './verify-attestor-tee.ts';
|
|
3
|
+
import { type TeeAttestationConfig } from './verify-tee.ts';
|
|
4
|
+
export type { LegacyProof } from './proof-types.ts';
|
|
5
|
+
/** A proof hash requirement accepted by legacy `verifyProof`. */
|
|
6
|
+
export type ProofHashRequirement = string | HashRequirement;
|
|
7
|
+
export interface ResolvedSessionProofValidation {
|
|
8
|
+
hashes?: ProofHashRequirement[];
|
|
9
|
+
providerId?: string;
|
|
10
|
+
providerVersion?: string;
|
|
11
|
+
allowedTags?: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Exact provider requirements returned by a caller-controlled resolver. */
|
|
14
|
+
export interface ResolvedProviderProofValidation {
|
|
15
|
+
hashes: ProofHashRequirement[];
|
|
16
|
+
}
|
|
17
|
+
/** Options intentionally preserve legacy `verifyProof` vocabulary. */
|
|
18
|
+
export interface LegacyVerifyProofOptions {
|
|
19
|
+
hashes?: ProofHashRequirement[];
|
|
20
|
+
providerId?: string;
|
|
21
|
+
providerVersion?: string;
|
|
22
|
+
allowedTags?: string[];
|
|
23
|
+
dangerouslyDisableContentValidation?: true;
|
|
24
|
+
/** Read `providerHash` from context, while still verifying signatures. */
|
|
25
|
+
hasNoPii?: true;
|
|
26
|
+
teeAttestation?: TeeAttestationConfig;
|
|
27
|
+
attestorTeeAttestation?: AttestorTeeAttestationConfig;
|
|
28
|
+
/** Explicit trusted attestor ETH addresses. */
|
|
29
|
+
attestorAddresses?: string[];
|
|
30
|
+
/** Trusted Builder JWKS registry containing attestor-purpose keys. */
|
|
31
|
+
attestorJwksUrl?: string;
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
/** Builder session id. Requires a resolver without hashes or a provider. */
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
resolveSession?: (sessionId: string, proofs: LegacyProof[]) => Promise<ResolvedSessionProofValidation> | ResolvedSessionProofValidation;
|
|
36
|
+
/** Resolve an exact provider without coupling this verifier to HTTP. */
|
|
37
|
+
resolveProvider?: (providerId: string, providerVersion: string | undefined, allowedTags: string[] | undefined, proofs: LegacyProof[]) => Promise<ResolvedProviderProofValidation[]> | ResolvedProviderProofValidation[];
|
|
38
|
+
expectedSessionId?: string;
|
|
39
|
+
requireSessionBinding?: boolean;
|
|
40
|
+
expectedContext?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface LegacyProofOutcome {
|
|
43
|
+
valid: boolean;
|
|
44
|
+
reason?: string;
|
|
45
|
+
proofHashes?: string[];
|
|
46
|
+
isTeeAttestationVerified?: boolean;
|
|
47
|
+
isAttestorTeeAttestationVerified?: boolean;
|
|
48
|
+
data?: TrustedData[];
|
|
49
|
+
publicData?: unknown[];
|
|
50
|
+
}
|
|
51
|
+
/** Return the provider hashes encoded in an exact legacy proof. */
|
|
52
|
+
export declare function proofHashes(proof: LegacyProof): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Verify exact legacy proofs without depending on `@reclaimprotocol/js-sdk`.
|
|
55
|
+
* The signed claim is never renamed or reconstructed.
|
|
56
|
+
*/
|
|
57
|
+
export declare function verifyProof(proofOrProofs: LegacyProof | LegacyProof[], config: LegacyVerifyProofOptions): Promise<LegacyProofOutcome>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createSignDataForClaim } from '@reclaimprotocol/attestor-core';
|
|
2
|
+
import { recoverEip191Address } from "./eip191.js";
|
|
3
|
+
import { fetchReclaimJwks, trustedAttestorAddresses } from "./jwks.js";
|
|
4
|
+
import { assertValidProofHashes, getHashFromProof } from "./proof-hash.js";
|
|
5
|
+
import { runAttestorTeeVerification, } from "./verify-attestor-tee.js";
|
|
6
|
+
import { runTeeVerification } from "./verify-tee.js";
|
|
7
|
+
/** Return the provider hashes encoded in an exact legacy proof. */
|
|
8
|
+
export function proofHashes(proof) {
|
|
9
|
+
return getHashFromProof(proof);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verify exact legacy proofs without depending on `@reclaimprotocol/js-sdk`.
|
|
13
|
+
* The signed claim is never renamed or reconstructed.
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyProof(proofOrProofs, config) {
|
|
16
|
+
const proofs = Array.isArray(proofOrProofs) ? proofOrProofs : [proofOrProofs];
|
|
17
|
+
if (!proofs.length) {
|
|
18
|
+
return { valid: false, reason: 'no-proofs' };
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const effective = await resolveValidation(config, proofs);
|
|
22
|
+
assertSessionAndContext(proofs, effective);
|
|
23
|
+
const attestors = await resolveTrustedAttestors(effective);
|
|
24
|
+
assertVerifiedSignatures(proofs, attestors);
|
|
25
|
+
await assertContent(proofs, effective);
|
|
26
|
+
let isTeeAttestationVerified;
|
|
27
|
+
if (effective.teeAttestation) {
|
|
28
|
+
await runTeeVerification(proofs, effective.teeAttestation);
|
|
29
|
+
isTeeAttestationVerified = true;
|
|
30
|
+
}
|
|
31
|
+
let isAttestorTeeAttestationVerified;
|
|
32
|
+
if (effective.attestorTeeAttestation) {
|
|
33
|
+
await runAttestorTeeVerification(proofs, effective.attestorTeeAttestation);
|
|
34
|
+
isAttestorTeeAttestationVerified = true;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
valid: true,
|
|
38
|
+
proofHashes: collectProofHashes(proofs, effective.hasNoPii),
|
|
39
|
+
isTeeAttestationVerified,
|
|
40
|
+
isAttestorTeeAttestationVerified,
|
|
41
|
+
data: proofs.map(trustedData),
|
|
42
|
+
publicData: proofs.map((proof) => proof.publicData ?? {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
reason: failureReason(error),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function resolveValidation(config, proofs) {
|
|
53
|
+
let effective = config;
|
|
54
|
+
if (config.sessionId && !hasContentValidation(config)) {
|
|
55
|
+
if (!config.resolveSession) {
|
|
56
|
+
throw new Error('session-resolver-required');
|
|
57
|
+
}
|
|
58
|
+
effective = {
|
|
59
|
+
...config,
|
|
60
|
+
...await config.resolveSession(config.sessionId, proofs),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return effective;
|
|
64
|
+
}
|
|
65
|
+
async function resolveTrustedAttestors(config) {
|
|
66
|
+
const addresses = config.attestorAddresses?.map(normalized) ?? [];
|
|
67
|
+
if (config.attestorJwksUrl) {
|
|
68
|
+
const registry = await fetchReclaimJwks(config.attestorJwksUrl, config.fetchImpl);
|
|
69
|
+
addresses.push(...(registry['x-reclaim-attestors'] ?? []), ...trustedAttestorAddresses(registry.keys));
|
|
70
|
+
}
|
|
71
|
+
const unique = [...new Set(addresses)];
|
|
72
|
+
if (!unique.length) {
|
|
73
|
+
throw new Error('attestor-trust-required');
|
|
74
|
+
}
|
|
75
|
+
return unique;
|
|
76
|
+
}
|
|
77
|
+
function assertVerifiedSignatures(proofs, trustedAttestors) {
|
|
78
|
+
for (const proof of proofs) {
|
|
79
|
+
if (!proof.signatures?.length || !proof.witnesses?.length) {
|
|
80
|
+
throw new Error('proof-signature-missing');
|
|
81
|
+
}
|
|
82
|
+
const signData = createSignDataForClaim(proof.claimData);
|
|
83
|
+
const signers = proof.signatures.map((signature) => (normalized(recoverEip191Address(signData, signature))));
|
|
84
|
+
const witnesses = proof.witnesses.map(({ id }) => normalized(id));
|
|
85
|
+
if (witnesses.some((witness) => !trustedAttestors.includes(witness))) {
|
|
86
|
+
throw new Error('attestor-not-allowed');
|
|
87
|
+
}
|
|
88
|
+
if (!signers.some((signer) => trustedAttestors.includes(signer))) {
|
|
89
|
+
throw new Error('attestor-not-allowed');
|
|
90
|
+
}
|
|
91
|
+
if (!signers.every((signer) => witnesses.includes(signer))) {
|
|
92
|
+
throw new Error('witness-signature-mismatch');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function assertContent(proofs, config) {
|
|
97
|
+
if (config.dangerouslyDisableContentValidation) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (config.hashes) {
|
|
101
|
+
assertValidProofHashes(proofs, config.hashes, config.hasNoPii);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (config.providerId) {
|
|
105
|
+
if (!config.resolveProvider) {
|
|
106
|
+
throw new Error('provider-resolver-required');
|
|
107
|
+
}
|
|
108
|
+
const candidates = await config.resolveProvider(config.providerId, config.providerVersion, config.allowedTags, proofs);
|
|
109
|
+
if (!candidates.length) {
|
|
110
|
+
throw new Error('provider-not-found');
|
|
111
|
+
}
|
|
112
|
+
let lastError;
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
try {
|
|
115
|
+
assertValidProofHashes(proofs, candidate.hashes, config.hasNoPii);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
lastError = error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw lastError;
|
|
123
|
+
}
|
|
124
|
+
throw new Error('proof-validation-required');
|
|
125
|
+
}
|
|
126
|
+
function assertSessionAndContext(proofs, config) {
|
|
127
|
+
const expectedSessionId = config.sessionId ?? config.expectedSessionId;
|
|
128
|
+
for (const proof of proofs) {
|
|
129
|
+
if (expectedSessionId) {
|
|
130
|
+
const ids = sessionIds(proof);
|
|
131
|
+
if (ids.some((id) => id !== expectedSessionId)) {
|
|
132
|
+
throw new Error('session-mismatch');
|
|
133
|
+
}
|
|
134
|
+
if ((config.sessionId || config.requireSessionBinding) && !ids.length) {
|
|
135
|
+
throw new Error('session-missing');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!contextMatches(proof, config.expectedContext)) {
|
|
139
|
+
throw new Error('context-mismatch');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function trustedData(proof) {
|
|
144
|
+
const context = contextFrom(proof);
|
|
145
|
+
const extractedParameters = isRecord(context.extractedParameters)
|
|
146
|
+
? Object.fromEntries(Object.entries(context.extractedParameters).map(([key, value]) => [key, String(value)]))
|
|
147
|
+
: {};
|
|
148
|
+
delete context.extractedParameters;
|
|
149
|
+
return { context, extractedParameters };
|
|
150
|
+
}
|
|
151
|
+
function collectProofHashes(proofs, hasNoPii) {
|
|
152
|
+
try {
|
|
153
|
+
return proofs.flatMap((proof) => getHashFromProof(proof, hasNoPii));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function hasContentValidation(config) {
|
|
160
|
+
return Boolean(config.hashes
|
|
161
|
+
|| config.providerId
|
|
162
|
+
|| config.dangerouslyDisableContentValidation);
|
|
163
|
+
}
|
|
164
|
+
function failureReason(error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
const lower = message.toLowerCase();
|
|
167
|
+
if (lower.includes('multiple')
|
|
168
|
+
|| lower.includes('duplicate')
|
|
169
|
+
|| lower.includes('more than once')) {
|
|
170
|
+
return 'duplicate-proof-hash';
|
|
171
|
+
}
|
|
172
|
+
return message || 'proof-not-verified';
|
|
173
|
+
}
|
|
174
|
+
function normalized(value) {
|
|
175
|
+
return value.trim().toLowerCase();
|
|
176
|
+
}
|
|
177
|
+
function contextFrom(proof) {
|
|
178
|
+
const raw = proof.claimData.context;
|
|
179
|
+
if (!raw) {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const context = JSON.parse(raw);
|
|
184
|
+
return isRecord(context) ? { ...context } : {};
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function sessionIds(proof) {
|
|
191
|
+
const context = contextFrom(proof);
|
|
192
|
+
const ids = [context.reclaimSessionId];
|
|
193
|
+
try {
|
|
194
|
+
const parameters = JSON.parse(proof.claimData.parameters);
|
|
195
|
+
ids.push(parameters.proxySessionId, parameters.sessionId);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// A non-HTTP claim is valid when session binding wasn't requested.
|
|
199
|
+
}
|
|
200
|
+
return ids.filter((value) => (typeof value === 'string' && value.length > 0));
|
|
201
|
+
}
|
|
202
|
+
function contextMatches(proof, expected) {
|
|
203
|
+
if (!expected) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
const context = contextFrom(proof);
|
|
207
|
+
return Object.entries(expected).every(([key, value]) => (JSON.stringify(context[key]) === JSON.stringify(value)));
|
|
208
|
+
}
|
|
209
|
+
function isRecord(value) {
|
|
210
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
211
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HashRequirement, LegacyProof } from './proof-types.ts';
|
|
2
|
+
export interface HttpProofParams {
|
|
3
|
+
url: string;
|
|
4
|
+
method: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
responseMatches: Array<{
|
|
7
|
+
type?: string;
|
|
8
|
+
value?: string;
|
|
9
|
+
invert?: boolean;
|
|
10
|
+
isOptional?: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
responseRedactions: Array<{
|
|
13
|
+
xPath?: string;
|
|
14
|
+
jsonPath?: string;
|
|
15
|
+
regex?: string;
|
|
16
|
+
hash?: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface ProofRequestSpec extends HttpProofParams {
|
|
20
|
+
urlType: string;
|
|
21
|
+
bodySniff: {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
template: string;
|
|
24
|
+
};
|
|
25
|
+
required?: boolean;
|
|
26
|
+
multiple?: boolean;
|
|
27
|
+
templateParams?: string[];
|
|
28
|
+
templateParamsMode?: 'separate' | 'merge';
|
|
29
|
+
}
|
|
30
|
+
/** Exact legacy canonical HTTP-provider hashes, including optional matches. */
|
|
31
|
+
export declare function hashProofClaimParams(params: HttpProofParams): string[];
|
|
32
|
+
export declare function getHashFromProof(proof: LegacyProof, hasNoPii?: boolean): string[];
|
|
33
|
+
export declare function hashRequestSpec(request: ProofRequestSpec): HashRequirement;
|
|
34
|
+
export declare function hashRequirementsFromSpecs(requestData: ProofRequestSpec[], allowedInjectedRequestData: ProofRequestSpec[], proofs: LegacyProof[]): HashRequirement[];
|
|
35
|
+
export declare function assertValidProofHashes(proofs: LegacyProof[], requirements: Array<string | HashRequirement>, hasNoPii?: boolean): void;
|