@reclaimprotocol/attestor-core 5.0.1-beta.2 → 5.0.1-beta.22

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 (145) hide show
  1. package/browser/resources/attestor-browser.min.mjs +4512 -0
  2. package/lib/avs/abis/avsDirectoryABI.js +338 -341
  3. package/lib/avs/abis/delegationABI.js +1 -4
  4. package/lib/avs/abis/registryABI.js +719 -722
  5. package/lib/avs/client/create-claim-on-avs.js +129 -157
  6. package/lib/avs/config.js +18 -24
  7. package/lib/avs/contracts/ReclaimServiceManager.js +1 -0
  8. package/lib/avs/contracts/common.js +1 -0
  9. package/lib/avs/contracts/factories/ReclaimServiceManager__factory.js +1139 -1156
  10. package/lib/avs/contracts/factories/index.js +4 -4
  11. package/lib/avs/contracts/index.js +2 -6
  12. package/lib/avs/types/index.js +1 -0
  13. package/lib/avs/utils/contracts.js +30 -50
  14. package/lib/avs/utils/register.js +75 -70
  15. package/lib/avs/utils/tasks.js +38 -45
  16. package/lib/client/create-claim.js +402 -431
  17. package/lib/client/tunnels/make-rpc-tcp-tunnel.js +46 -48
  18. package/lib/client/tunnels/make-rpc-tls-tunnel.js +125 -121
  19. package/lib/client/utils/attestor-pool.js +23 -22
  20. package/lib/client/utils/client-socket.js +86 -109
  21. package/lib/client/utils/message-handler.js +79 -89
  22. package/lib/config/index.js +40 -58
  23. package/lib/external-rpc/benchmark.js +61 -74
  24. package/lib/external-rpc/event-bus.js +12 -15
  25. package/lib/external-rpc/handle-incoming-msg.js +216 -225
  26. package/lib/external-rpc/jsc-polyfills/1.js +70 -68
  27. package/lib/external-rpc/jsc-polyfills/2.js +17 -12
  28. package/lib/external-rpc/jsc-polyfills/event.js +10 -15
  29. package/lib/external-rpc/jsc-polyfills/index.js +2 -2
  30. package/lib/external-rpc/jsc-polyfills/ws.js +77 -79
  31. package/lib/external-rpc/setup-browser.js +28 -28
  32. package/lib/external-rpc/setup-jsc.js +17 -17
  33. package/lib/external-rpc/types.js +1 -0
  34. package/lib/external-rpc/utils.js +89 -89
  35. package/lib/external-rpc/zk.js +55 -50
  36. package/lib/index.js +2 -6
  37. package/lib/mechain/abis/governanceABI.js +457 -460
  38. package/lib/mechain/abis/taskABI.js +502 -505
  39. package/lib/mechain/client/create-claim-on-mechain.js +24 -29
  40. package/lib/mechain/constants/index.js +3 -8
  41. package/lib/mechain/types/index.js +1 -0
  42. package/lib/proto/api.js +4200 -4087
  43. package/lib/proto/tee-bundle.js +1261 -1241
  44. package/lib/providers/http/index.js +616 -603
  45. package/lib/providers/http/patch-parse5-tree.js +27 -29
  46. package/lib/providers/http/utils.js +289 -248
  47. package/lib/providers/index.js +3 -6
  48. package/lib/server/create-server.js +89 -91
  49. package/lib/server/handlers/claimTeeBundle.js +231 -211
  50. package/lib/server/handlers/claimTunnel.js +66 -73
  51. package/lib/server/handlers/completeClaimOnChain.js +20 -25
  52. package/lib/server/handlers/createClaimOnChain.js +21 -27
  53. package/lib/server/handlers/createTaskOnMechain.js +40 -50
  54. package/lib/server/handlers/createTunnel.js +85 -90
  55. package/lib/server/handlers/disconnectTunnel.js +4 -7
  56. package/lib/server/handlers/fetchCertificateBytes.js +37 -53
  57. package/lib/server/handlers/index.js +21 -24
  58. package/lib/server/handlers/init.js +27 -28
  59. package/lib/server/handlers/toprf.js +13 -16
  60. package/lib/server/socket.js +97 -100
  61. package/lib/server/tunnels/make-tcp-tunnel.js +161 -186
  62. package/lib/server/utils/apm.js +32 -25
  63. package/lib/server/utils/assert-valid-claim-request.js +305 -334
  64. package/lib/server/utils/config-env.js +2 -2
  65. package/lib/server/utils/dns.js +12 -18
  66. package/lib/server/utils/gcp-attestation.js +233 -181
  67. package/lib/server/utils/generics.d.ts +1 -1
  68. package/lib/server/utils/generics.js +43 -37
  69. package/lib/server/utils/iso.js +253 -256
  70. package/lib/server/utils/keep-alive.js +36 -36
  71. package/lib/server/utils/nitro-attestation.js +295 -220
  72. package/lib/server/utils/oprf-raw.js +48 -55
  73. package/lib/server/utils/process-handshake.js +200 -218
  74. package/lib/server/utils/proxy-session.js +5 -5
  75. package/lib/server/utils/tee-oprf-mpc-verification.js +82 -78
  76. package/lib/server/utils/tee-oprf-verification.js +165 -142
  77. package/lib/server/utils/tee-transcript-reconstruction.js +176 -129
  78. package/lib/server/utils/tee-verification.js +397 -334
  79. package/lib/server/utils/validation.js +30 -37
  80. package/lib/types/bgp.js +1 -0
  81. package/lib/types/claims.js +1 -0
  82. package/lib/types/client.js +1 -0
  83. package/lib/types/general.js +1 -0
  84. package/lib/types/handlers.js +1 -0
  85. package/lib/types/providers.d.ts +3 -2
  86. package/lib/types/providers.gen.js +9 -15
  87. package/lib/types/providers.js +1 -0
  88. package/lib/types/rpc.js +1 -0
  89. package/lib/types/signatures.d.ts +1 -2
  90. package/lib/types/signatures.js +1 -0
  91. package/lib/types/tunnel.js +1 -0
  92. package/lib/types/zk.js +1 -0
  93. package/lib/utils/auth.js +54 -66
  94. package/lib/utils/b64-json.js +15 -15
  95. package/lib/utils/bgp-listener.js +107 -111
  96. package/lib/utils/claims.js +89 -80
  97. package/lib/utils/env.js +13 -17
  98. package/lib/utils/error.js +43 -47
  99. package/lib/utils/generics.js +284 -235
  100. package/lib/utils/http-parser.js +232 -187
  101. package/lib/utils/logger.js +80 -71
  102. package/lib/utils/prepare-packets.js +69 -67
  103. package/lib/utils/redactions.js +163 -121
  104. package/lib/utils/retries.js +22 -24
  105. package/lib/utils/signatures/eth.js +29 -28
  106. package/lib/utils/signatures/index.js +5 -10
  107. package/lib/utils/socket-base.js +84 -88
  108. package/lib/utils/tls.js +28 -28
  109. package/lib/utils/ws.js +19 -19
  110. package/lib/utils/zk.js +542 -582
  111. package/package.json +12 -5
  112. package/lib/external-rpc/global.d.js +0 -0
  113. package/lib/scripts/build-browser.d.ts +0 -1
  114. package/lib/scripts/build-jsc.d.ts +0 -1
  115. package/lib/scripts/build-lib.d.ts +0 -1
  116. package/lib/scripts/check-avs-registration.d.ts +0 -1
  117. package/lib/scripts/check-avs-registration.js +0 -28
  118. package/lib/scripts/fallbacks/crypto.d.ts +0 -1
  119. package/lib/scripts/fallbacks/crypto.js +0 -4
  120. package/lib/scripts/fallbacks/empty.d.ts +0 -3
  121. package/lib/scripts/fallbacks/empty.js +0 -4
  122. package/lib/scripts/fallbacks/re2.d.ts +0 -1
  123. package/lib/scripts/fallbacks/re2.js +0 -7
  124. package/lib/scripts/fallbacks/snarkjs.d.ts +0 -1
  125. package/lib/scripts/fallbacks/snarkjs.js +0 -10
  126. package/lib/scripts/fallbacks/stwo.d.ts +0 -6
  127. package/lib/scripts/fallbacks/stwo.js +0 -159
  128. package/lib/scripts/generate-provider-types.d.ts +0 -5
  129. package/lib/scripts/generate-provider-types.js +0 -101
  130. package/lib/scripts/generate-receipt.d.ts +0 -9
  131. package/lib/scripts/generate-receipt.js +0 -101
  132. package/lib/scripts/generate-toprf-keys.d.ts +0 -1
  133. package/lib/scripts/generate-toprf-keys.js +0 -24
  134. package/lib/scripts/jsc-cli-rpc.d.ts +0 -1
  135. package/lib/scripts/jsc-cli-rpc.js +0 -35
  136. package/lib/scripts/register-avs-operator.d.ts +0 -1
  137. package/lib/scripts/register-avs-operator.js +0 -3
  138. package/lib/scripts/start-server.d.ts +0 -1
  139. package/lib/scripts/start-server.js +0 -11
  140. package/lib/scripts/update-avs-metadata.d.ts +0 -1
  141. package/lib/scripts/update-avs-metadata.js +0 -20
  142. package/lib/scripts/utils.d.ts +0 -1
  143. package/lib/scripts/utils.js +0 -10
  144. package/lib/scripts/whitelist-operator.d.ts +0 -1
  145. package/lib/scripts/whitelist-operator.js +0 -16
@@ -1,4 +1,4 @@
1
- import { config } from "dotenv";
1
+ import { config } from 'dotenv';
2
2
  import { getEnvVariable } from "../../utils/env.js";
3
- const nodeEnv = getEnvVariable("NODE_ENV") || "development";
3
+ const nodeEnv = getEnvVariable('NODE_ENV') || 'development';
4
4
  config({ path: `.env.${nodeEnv}` });
@@ -1,24 +1,18 @@
1
- import { resolve, setServers } from "dns";
1
+ import { resolve, setServers } from 'dns';
2
2
  import { DNS_SERVERS } from "../../config/index.js";
3
3
  setDnsServers();
4
- async function resolveHostnames(hostname) {
5
- return new Promise((_resolve, reject) => {
6
- resolve(hostname, (err, addresses) => {
7
- if (err) {
8
- reject(
9
- new Error(
10
- `Could not resolve hostname: ${hostname}, ${err.message}`
11
- )
12
- );
13
- } else {
14
- _resolve(addresses);
15
- }
4
+ export async function resolveHostnames(hostname) {
5
+ return new Promise((_resolve, reject) => {
6
+ resolve(hostname, (err, addresses) => {
7
+ if (err) {
8
+ reject(new Error(`Could not resolve hostname: ${hostname}, ${err.message}`));
9
+ }
10
+ else {
11
+ _resolve(addresses);
12
+ }
13
+ });
16
14
  });
17
- });
18
15
  }
19
16
  function setDnsServers() {
20
- setServers(DNS_SERVERS);
17
+ setServers(DNS_SERVERS);
21
18
  }
22
- export {
23
- resolveHostnames
24
- };
@@ -1,7 +1,13 @@
1
- import crypto, { X509Certificate } from "crypto";
1
+ /**
2
+ * GCP attestation validation utilities
3
+ * Validates JWT tokens from Google Confidential Computing
4
+ */
5
+ import crypto, { X509Certificate } from 'crypto';
6
+ // Cache for Google's public keys
2
7
  let gcpKeysCache = null;
3
8
  let gcpKeysCacheTime = 0;
4
- const GCP_KEYS_CACHE_TTL = 36e5;
9
+ const GCP_KEYS_CACHE_TTL = 3600000; // 1 hour in milliseconds
10
+ // GCP Confidential Space Root CA
5
11
  const GCP_CONFIDENTIAL_SPACE_ROOT_CA = `-----BEGIN CERTIFICATE-----
6
12
  MIIGCDCCA/CgAwIBAgITYBvRy5g9aYYMh7tJS7pFwafL6jANBgkqhkiG9w0BAQsF
7
13
  ADCBizELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT
@@ -37,201 +43,247 @@ cPmpPmx7pSMkSxEX2Vos2JNaNmCKJd2VaXz8M6F2cxscRdh9TbAYAjGEEjE1nLUH
37
43
  kNPLowCd0NqxYYSLNL7GroYCFPxoBpr+++4vsCaXalbs8iJxdU2EPqG4MB4xWKYg
38
44
  uyT5CnJulxSC5CT1
39
45
  -----END CERTIFICATE-----`;
46
+ /**
47
+ * Base64url decode (RFC 4648, no padding)
48
+ */
40
49
  function base64urlDecode(input) {
41
- let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
42
- while (base64.length % 4) {
43
- base64 += "=";
44
- }
45
- return Buffer.from(base64, "base64");
46
- }
47
- async function fetchGooglePublicKeys(logger) {
48
- const now = Date.now();
49
- if (gcpKeysCache && now - gcpKeysCacheTime < GCP_KEYS_CACHE_TTL) {
50
- if (logger) {
51
- logger.debug("Using cached Google public keys");
50
+ // Add padding if needed
51
+ let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
52
+ while (base64.length % 4) {
53
+ base64 += '=';
52
54
  }
53
- return gcpKeysCache;
54
- }
55
- if (logger) {
56
- logger.info("Fetching Google public keys from https://www.googleapis.com/oauth2/v3/certs");
57
- }
58
- const response = await fetch("https://www.googleapis.com/oauth2/v3/certs");
59
- if (!response.ok) {
60
- throw new Error(`Failed to fetch Google keys: ${response.status} ${response.statusText}`);
61
- }
62
- const keys = await response.json();
63
- gcpKeysCache = keys;
64
- gcpKeysCacheTime = now;
65
- if (logger) {
66
- logger.info(`Fetched ${keys.keys.length} Google public keys`);
67
- }
68
- return keys;
55
+ return Buffer.from(base64, 'base64');
69
56
  }
70
- function jwkToPublicKey(jwk) {
71
- return crypto.createPublicKey({
72
- key: {
73
- kty: "RSA",
74
- n: jwk.n,
75
- e: jwk.e
76
- },
77
- format: "jwk"
78
- });
79
- }
80
- function verifyX5cChain(x5cChain, logger) {
81
- if (!x5cChain || x5cChain.length === 0) {
82
- throw new Error("Empty x5c certificate chain");
83
- }
84
- const leafCertPem = `-----BEGIN CERTIFICATE-----
85
- ${x5cChain[0]}
86
- -----END CERTIFICATE-----`;
87
- const leafCert = new X509Certificate(leafCertPem);
88
- if (logger) {
89
- logger.info(`x5c leaf certificate: subject=${leafCert.subject}, issuer=${leafCert.issuer}`);
90
- }
91
- const rootCert = new X509Certificate(GCP_CONFIDENTIAL_SPACE_ROOT_CA);
92
- let currentCert = leafCert;
93
- for (let i = 1; i < x5cChain.length; i++) {
94
- const intermediatePem = `-----BEGIN CERTIFICATE-----
95
- ${x5cChain[i]}
96
- -----END CERTIFICATE-----`;
97
- const intermediateCert = new X509Certificate(intermediatePem);
98
- const isValid = currentCert.verify(intermediateCert.publicKey);
99
- if (!isValid) {
100
- throw new Error(`Certificate chain verification failed at level ${i}`);
57
+ /**
58
+ * Fetch Google's public keys (with caching)
59
+ */
60
+ async function fetchGooglePublicKeys(logger) {
61
+ const now = Date.now();
62
+ // Return cached keys if still valid
63
+ if (gcpKeysCache && (now - gcpKeysCacheTime) < GCP_KEYS_CACHE_TTL) {
64
+ if (logger) {
65
+ logger.debug('Using cached Google public keys');
66
+ }
67
+ return gcpKeysCache;
101
68
  }
69
+ // Fetch fresh keys
102
70
  if (logger) {
103
- logger.debug(`Verified cert level ${i}: ${intermediateCert.subject}`);
71
+ logger.info('Fetching Google public keys from https://www.googleapis.com/oauth2/v3/certs');
104
72
  }
105
- currentCert = intermediateCert;
106
- }
107
- const isRootValid = currentCert.verify(rootCert.publicKey);
108
- if (!isRootValid) {
109
- throw new Error("Certificate chain does not root to GCP Confidential Space Root CA");
110
- }
111
- if (logger) {
112
- logger.info("x5c certificate chain verified successfully");
113
- }
114
- return leafCert.publicKey;
115
- }
116
- async function validateGcpAttestationAndExtractKey(attestationBytes, logger) {
117
- const errors = [];
118
- try {
119
- const jwtString = Buffer.from(attestationBytes).toString("utf8");
120
- const parts = jwtString.split(".");
121
- if (parts.length !== 3) {
122
- errors.push("Invalid JWT format: expected 3 parts");
123
- return { isValid: false, errors };
73
+ const response = await fetch('https://www.googleapis.com/oauth2/v3/certs');
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch Google keys: ${response.status} ${response.statusText}`);
124
76
  }
125
- const [headerB64, payloadB64, signatureB64] = parts;
126
- const headerJson = base64urlDecode(headerB64).toString("utf8");
127
- const payloadJson = base64urlDecode(payloadB64).toString("utf8");
128
- const header = JSON.parse(headerJson);
129
- const payload = JSON.parse(payloadJson);
77
+ const keys = await response.json();
78
+ // Update cache
79
+ gcpKeysCache = keys;
80
+ gcpKeysCacheTime = now;
130
81
  if (logger) {
131
- logger.info(`GCP JWT header: kid=${header.kid}, alg=${header.alg}`);
132
- logger.info(`GCP JWT payload: iss=${payload.iss}, aud=${payload.aud}`);
133
- }
134
- const now = Math.floor(Date.now() / 1e3);
135
- const validIssuers = [
136
- "https://accounts.google.com",
137
- "https://confidentialcomputing.googleapis.com"
138
- ];
139
- if (!validIssuers.includes(payload.iss)) {
140
- errors.push(`Invalid issuer: expected one of ${validIssuers.join(", ")}, got "${payload.iss}"`);
141
- }
142
- if (payload.exp <= now) {
143
- errors.push(`Token expired: exp=${payload.exp}, now=${now}`);
144
- }
145
- if (payload.iat > now + 60) {
146
- errors.push(`Token issued in future: iat=${payload.iat}, now=${now}`);
82
+ logger.info(`Fetched ${keys.keys.length} Google public keys`);
147
83
  }
148
- const hasReclaimAudience = payload.aud?.includes("reclaimprotocol.org");
149
- const hasGcpStsAudience = payload.aud?.includes("sts.googleapis.com");
150
- if (!hasReclaimAudience && !hasGcpStsAudience) {
151
- errors.push(`Invalid audience: expected "reclaimprotocol.org" or "sts.googleapis.com", got "${payload.aud}"`);
152
- }
153
- if (errors.length > 0) {
154
- return { isValid: false, errors };
155
- }
156
- let publicKey;
157
- if (header.x5c && header.x5c.length > 0) {
158
- if (logger) {
159
- logger.info(`Using x5c certificate chain (${header.x5c.length} certificates)`);
160
- }
161
- publicKey = verifyX5cChain(header.x5c, logger);
162
- } else if (header.kid) {
163
- if (logger) {
164
- logger.info(`Using OIDC token with kid: ${header.kid}`);
165
- }
166
- const jwks = await fetchGooglePublicKeys(logger);
167
- const jwk = jwks.keys.find((k) => k.kid === header.kid);
168
- if (!jwk) {
169
- errors.push(`No public key found for kid: ${header.kid}`);
170
- return { isValid: false, errors };
171
- }
172
- publicKey = jwkToPublicKey(jwk);
173
- } else {
174
- errors.push("JWT header must contain either x5c or kid field");
175
- return { isValid: false, errors };
176
- }
177
- const signedData = `${headerB64}.${payloadB64}`;
178
- const signature = base64urlDecode(signatureB64);
179
- const verify = crypto.createVerify("RSA-SHA256");
180
- verify.update(signedData);
181
- const isSignatureValid = verify.verify(publicKey, signature);
182
- if (!isSignatureValid) {
183
- errors.push("Signature verification failed");
184
- return { isValid: false, errors };
84
+ return keys;
85
+ }
86
+ /**
87
+ * Convert JWK to RSA public key
88
+ */
89
+ function jwkToPublicKey(jwk) {
90
+ // Create RSA public key from modulus and exponent
91
+ return crypto.createPublicKey({
92
+ key: {
93
+ kty: 'RSA',
94
+ n: jwk.n,
95
+ e: jwk.e,
96
+ },
97
+ format: 'jwk'
98
+ });
99
+ }
100
+ /**
101
+ * Verify x5c certificate chain and return leaf certificate's public key
102
+ */
103
+ function verifyX5cChain(x5cChain, logger) {
104
+ if (!x5cChain || x5cChain.length === 0) {
105
+ throw new Error('Empty x5c certificate chain');
185
106
  }
107
+ // Parse leaf certificate (first in chain)
108
+ const leafCertPem = `-----BEGIN CERTIFICATE-----\n${x5cChain[0]}\n-----END CERTIFICATE-----`;
109
+ const leafCert = new X509Certificate(leafCertPem);
186
110
  if (logger) {
187
- logger.info("GCP JWT signature verified successfully");
111
+ logger.info(`x5c leaf certificate: subject=${leafCert.subject}, issuer=${leafCert.issuer}`);
188
112
  }
189
- if (!payload.eat_nonce) {
190
- errors.push("No eat_nonce field found in JWT payload");
191
- return { isValid: false, errors };
113
+ // Parse root CA
114
+ const rootCert = new X509Certificate(GCP_CONFIDENTIAL_SPACE_ROOT_CA);
115
+ // For chain verification with Node.js X509Certificate, we need to verify each cert in sequence
116
+ // Start with leaf and work up to root
117
+ let currentCert = leafCert;
118
+ // Verify intermediate certificates if present
119
+ for (let i = 1; i < x5cChain.length; i++) {
120
+ const intermediatePem = `-----BEGIN CERTIFICATE-----\n${x5cChain[i]}\n-----END CERTIFICATE-----`;
121
+ const intermediateCert = new X509Certificate(intermediatePem);
122
+ // Verify current cert was signed by intermediate
123
+ const isValid = currentCert.verify(intermediateCert.publicKey);
124
+ if (!isValid) {
125
+ throw new Error(`Certificate chain verification failed at level ${i}`);
126
+ }
127
+ if (logger) {
128
+ logger.debug(`Verified cert level ${i}: ${intermediateCert.subject}`);
129
+ }
130
+ currentCert = intermediateCert;
192
131
  }
193
- const match = payload.eat_nonce.match(/^(tee_[kt])_public_key:0x([0-9a-fA-F]{40})$/);
194
- if (!match) {
195
- errors.push(`Invalid eat_nonce format: ${payload.eat_nonce}`);
196
- return { isValid: false, errors };
132
+ // Verify the top cert was signed by root CA
133
+ const isRootValid = currentCert.verify(rootCert.publicKey);
134
+ if (!isRootValid) {
135
+ throw new Error('Certificate chain does not root to GCP Confidential Space Root CA');
197
136
  }
198
- const userDataType = match[1];
199
- const hexAddress = match[2];
200
- const ethAddress = new Uint8Array(Buffer.from(hexAddress, "hex"));
201
137
  if (logger) {
202
- logger.info(`Extracted address from eat_nonce: ${payload.eat_nonce}`);
203
- }
204
- let pcr0 = "gcp-no-digest";
205
- if (payload.google?.compute_engine?.image_digest) {
206
- pcr0 = payload.google.compute_engine.image_digest;
207
- } else if (payload.submods?.container?.image_digest) {
208
- pcr0 = payload.submods.container.image_digest;
138
+ logger.info('x5c certificate chain verified successfully');
209
139
  }
210
- if (payload.dbgstat === "enabled" && pcr0.startsWith("sha256:")) {
211
- pcr0 = "debug_" + pcr0;
140
+ // Return leaf certificate's public key for signature verification
141
+ return leafCert.publicKey;
142
+ }
143
+ /**
144
+ * Validates GCP JWT attestation and extracts ETH address
145
+ */
146
+ export async function validateGcpAttestationAndExtractKey(attestationBytes, logger) {
147
+ const errors = [];
148
+ try {
149
+ // 1. Parse JWT structure
150
+ const jwtString = Buffer.from(attestationBytes).toString('utf8');
151
+ const parts = jwtString.split('.');
152
+ if (parts.length !== 3) {
153
+ errors.push('Invalid JWT format: expected 3 parts');
154
+ return { isValid: false, errors };
155
+ }
156
+ const [headerB64, payloadB64, signatureB64] = parts;
157
+ // Decode header and payload
158
+ const headerJson = base64urlDecode(headerB64).toString('utf8');
159
+ const payloadJson = base64urlDecode(payloadB64).toString('utf8');
160
+ const header = JSON.parse(headerJson);
161
+ const payload = JSON.parse(payloadJson);
162
+ if (logger) {
163
+ logger.info(`GCP JWT header: kid=${header.kid}, alg=${header.alg}`);
164
+ logger.info(`GCP JWT payload: iss=${payload.iss}, aud=${payload.aud}`);
165
+ }
166
+ // 2. Verify claims
167
+ const now = Math.floor(Date.now() / 1000);
168
+ // Check issuer - accept both Google accounts and Confidential Computing
169
+ const validIssuers = [
170
+ 'https://accounts.google.com',
171
+ 'https://confidentialcomputing.googleapis.com'
172
+ ];
173
+ if (!validIssuers.includes(payload.iss)) {
174
+ errors.push(`Invalid issuer: expected one of ${validIssuers.join(', ')}, got "${payload.iss}"`);
175
+ }
176
+ // Check expiration
177
+ if (payload.exp <= now) {
178
+ errors.push(`Token expired: exp=${payload.exp}, now=${now}`);
179
+ }
180
+ // Check issued at (allow 60 second clock skew)
181
+ if (payload.iat > now + 60) {
182
+ errors.push(`Token issued in future: iat=${payload.iat}, now=${now}`);
183
+ }
184
+ // Audience can be:
185
+ // 1. Custom Reclaim audience with data param: https://reclaimprotocol.org/attestation?data=tee_k_public_key:0x...
186
+ // 2. Reclaim domain only: https://reclaim-protocol.com (address in eat_nonce)
187
+ // 3. GCP STS audience: https://sts.googleapis.com (for Confidential Space)
188
+ const hasReclaimAudience = payload.aud?.includes('reclaimprotocol.org');
189
+ const hasGcpStsAudience = payload.aud?.includes('sts.googleapis.com');
190
+ if (!hasReclaimAudience && !hasGcpStsAudience) {
191
+ errors.push(`Invalid audience: expected "reclaimprotocol.org" or "sts.googleapis.com", got "${payload.aud}"`);
192
+ }
193
+ if (errors.length > 0) {
194
+ return { isValid: false, errors };
195
+ }
196
+ // 3. Get public key - either from x5c chain or JWKS
197
+ let publicKey;
198
+ if (header.x5c && header.x5c.length > 0) {
199
+ // PKI token with certificate chain
200
+ if (logger) {
201
+ logger.info(`Using x5c certificate chain (${header.x5c.length} certificates)`);
202
+ }
203
+ publicKey = verifyX5cChain(header.x5c, logger);
204
+ }
205
+ else if (header.kid) {
206
+ // OIDC token with kid
207
+ if (logger) {
208
+ logger.info(`Using OIDC token with kid: ${header.kid}`);
209
+ }
210
+ // Fetch Google's public keys
211
+ const jwks = await fetchGooglePublicKeys(logger);
212
+ // Find matching key
213
+ const jwk = jwks.keys.find(k => k.kid === header.kid);
214
+ if (!jwk) {
215
+ errors.push(`No public key found for kid: ${header.kid}`);
216
+ return { isValid: false, errors };
217
+ }
218
+ publicKey = jwkToPublicKey(jwk);
219
+ }
220
+ else {
221
+ errors.push('JWT header must contain either x5c or kid field');
222
+ return { isValid: false, errors };
223
+ }
224
+ // 4. Verify signature
225
+ const signedData = `${headerB64}.${payloadB64}`;
226
+ const signature = base64urlDecode(signatureB64);
227
+ const verify = crypto.createVerify('RSA-SHA256');
228
+ verify.update(signedData);
229
+ const isSignatureValid = verify.verify(publicKey, signature);
230
+ if (!isSignatureValid) {
231
+ errors.push('Signature verification failed');
232
+ return { isValid: false, errors };
233
+ }
234
+ if (logger) {
235
+ logger.info('GCP JWT signature verified successfully');
236
+ }
237
+ // 5. Extract ETH address from eat_nonce
238
+ if (!payload.eat_nonce) {
239
+ errors.push('No eat_nonce field found in JWT payload');
240
+ return { isValid: false, errors };
241
+ }
242
+ // Format: "tee_k_public_key:0x..." or "tee_t_public_key:0x..."
243
+ const match = payload.eat_nonce.match(/^(tee_[kt])_public_key:0x([0-9a-fA-F]{40})$/);
244
+ if (!match) {
245
+ errors.push(`Invalid eat_nonce format: ${payload.eat_nonce}`);
246
+ return { isValid: false, errors };
247
+ }
248
+ const userDataType = match[1]; // "tee_k" or "tee_t"
249
+ const hexAddress = match[2];
250
+ const ethAddress = new Uint8Array(Buffer.from(hexAddress, 'hex'));
251
+ if (logger) {
252
+ logger.info(`Extracted address from eat_nonce: ${payload.eat_nonce}`);
253
+ }
254
+ // Extract image digest from JWT payload (GCP's equivalent to PCR0)
255
+ let pcr0 = 'gcp-no-digest';
256
+ if (payload.google?.compute_engine?.image_digest) {
257
+ pcr0 = payload.google.compute_engine.image_digest;
258
+ }
259
+ else if (payload.submods?.container?.image_digest) {
260
+ pcr0 = payload.submods.container.image_digest;
261
+ }
262
+ // Add debug prefix if debug mode is enabled
263
+ if (payload.dbgstat === 'enabled' && pcr0.startsWith('sha256:')) {
264
+ pcr0 = 'debug_' + pcr0;
265
+ }
266
+ // Extract environment variables if present
267
+ const envVars = payload.submods?.container?.env || {};
268
+ if (logger) {
269
+ const hexAddr = Buffer.from(ethAddress).toString('hex');
270
+ logger.info(`Extracted ETH address from GCP attestation: 0x${hexAddr}, type: ${userDataType}, pcr0: ${pcr0}`);
271
+ if (Object.keys(envVars).length > 0) {
272
+ logger.debug(`Environment variables: ${Object.keys(envVars).join(', ')}`);
273
+ }
274
+ }
275
+ return {
276
+ isValid: true,
277
+ errors: [],
278
+ ethAddress,
279
+ userDataType,
280
+ pcr0,
281
+ envVars
282
+ };
212
283
  }
213
- const envVars = payload.submods?.container?.env || {};
214
- if (logger) {
215
- const hexAddr = Buffer.from(ethAddress).toString("hex");
216
- logger.info(`Extracted ETH address from GCP attestation: 0x${hexAddr}, type: ${userDataType}, pcr0: ${pcr0}`);
217
- if (Object.keys(envVars).length > 0) {
218
- logger.debug(`Environment variables: ${Object.keys(envVars).join(", ")}`);
219
- }
284
+ catch (error) {
285
+ const errorMsg = error instanceof Error ? error.message : String(error);
286
+ errors.push(`GCP attestation validation error: ${errorMsg}`);
287
+ return { isValid: false, errors };
220
288
  }
221
- return {
222
- isValid: true,
223
- errors: [],
224
- ethAddress,
225
- userDataType,
226
- pcr0,
227
- envVars
228
- };
229
- } catch (error) {
230
- const errorMsg = error instanceof Error ? error.message : String(error);
231
- errors.push(`GCP attestation validation error: ${errorMsg}`);
232
- return { isValid: false, errors };
233
- }
234
289
  }
235
- export {
236
- validateGcpAttestationAndExtractKey
237
- };
@@ -3,7 +3,7 @@ import type { ServiceSignatureType } from '#src/proto/api.ts';
3
3
  /**
4
4
  * Sign message using the PRIVATE_KEY env var.
5
5
  */
6
- export declare function signAsAttestor(data: Uint8Array | string, scheme: ServiceSignatureType): Uint8Array<ArrayBufferLike> | Promise<Uint8Array<ArrayBufferLike>>;
6
+ export declare function signAsAttestor(data: Uint8Array | string, scheme: ServiceSignatureType): import("../../index.ts").Awaitable<Uint8Array<ArrayBufferLike>>;
7
7
  /**
8
8
  * Obtain the address on chain, from the PRIVATE_KEY env var.
9
9
  */
@@ -2,44 +2,50 @@ import { RPCMessages } from "../../proto/api.js";
2
2
  import { getEnvVariable } from "../../utils/env.js";
3
3
  import { AttestorError, strToUint8Array } from "../../utils/index.js";
4
4
  import { SIGNATURES } from "../../utils/signatures/index.js";
5
- const PRIVATE_KEY = getEnvVariable("PRIVATE_KEY");
6
- function signAsAttestor(data, scheme) {
7
- const { sign } = SIGNATURES[scheme];
8
- return sign(
9
- typeof data === "string" ? strToUint8Array(data) : data,
10
- PRIVATE_KEY
11
- );
5
+ const PRIVATE_KEY = getEnvVariable('PRIVATE_KEY');
6
+ /**
7
+ * Sign message using the PRIVATE_KEY env var.
8
+ */
9
+ export function signAsAttestor(data, scheme) {
10
+ const { sign } = SIGNATURES[scheme];
11
+ return sign(typeof data === 'string' ? strToUint8Array(data) : data, PRIVATE_KEY);
12
12
  }
13
- function getAttestorAddress(scheme) {
14
- const { getAddress, getPublicKey } = SIGNATURES[scheme];
15
- const publicKey = getPublicKey(PRIVATE_KEY);
16
- return getAddress(publicKey);
13
+ /**
14
+ * Obtain the address on chain, from the PRIVATE_KEY env var.
15
+ */
16
+ export function getAttestorAddress(scheme) {
17
+ const { getAddress, getPublicKey } = SIGNATURES[scheme];
18
+ const publicKey = getPublicKey(PRIVATE_KEY);
19
+ return getAddress(publicKey);
17
20
  }
18
- function niceParseJsonObject(data, key) {
19
- if (!data) {
20
- return {};
21
- }
22
- try {
23
- return JSON.parse(data);
24
- } catch (e) {
25
- throw AttestorError.badRequest(
26
- `Invalid JSON in ${key}: ${e.message}`
27
- );
28
- }
21
+ /**
22
+ * Nice parse JSON with a key.
23
+ * If the data is empty, returns an empty object.
24
+ * And if the JSON is invalid, throws a bad request error,
25
+ * with the key in the error message.
26
+ */
27
+ export function niceParseJsonObject(data, key) {
28
+ if (!data) {
29
+ return {};
30
+ }
31
+ try {
32
+ return JSON.parse(data);
33
+ }
34
+ catch (e) {
35
+ throw AttestorError.badRequest(`Invalid JSON in ${key}: ${e.message}`);
36
+ }
29
37
  }
30
- function getInitialMessagesFromQuery(req) {
31
- const url = new URL(req.url, "http://localhost");
32
- const messagesB64 = url.searchParams.get("messages");
33
- if (!messagesB64?.length) {
34
- return [];
35
- }
36
- const msgsBytes = Buffer.from(messagesB64, "base64");
37
- const msgs = RPCMessages.decode(msgsBytes);
38
- return msgs.messages;
38
+ /**
39
+ * Extract any initial messages sent via the query string,
40
+ * in the `messages` parameter.
41
+ */
42
+ export function getInitialMessagesFromQuery(req) {
43
+ const url = new URL(req.url, 'http://localhost');
44
+ const messagesB64 = url.searchParams.get('messages');
45
+ if (!messagesB64?.length) {
46
+ return [];
47
+ }
48
+ const msgsBytes = Buffer.from(messagesB64, 'base64');
49
+ const msgs = RPCMessages.decode(msgsBytes);
50
+ return msgs.messages;
39
51
  }
40
- export {
41
- getAttestorAddress,
42
- getInitialMessagesFromQuery,
43
- niceParseJsonObject,
44
- signAsAttestor
45
- };