@reclaimprotocol/attestor-core 5.0.1-beta.21 → 5.0.1-beta.23

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 (133) hide show
  1. package/browser/resources/attestor-browser.min.mjs +9 -9
  2. package/lib/avs/abis/avsDirectoryABI.js +340 -0
  3. package/lib/avs/abis/delegationABI.js +1 -0
  4. package/lib/avs/abis/registryABI.js +725 -0
  5. package/lib/avs/client/create-claim-on-avs.js +140 -0
  6. package/lib/avs/config.js +20 -0
  7. package/lib/avs/contracts/factories/ReclaimServiceManager__factory.js +1166 -0
  8. package/lib/avs/contracts/factories/index.js +4 -0
  9. package/lib/avs/contracts/index.js +2 -0
  10. package/lib/avs/utils/contracts.js +33 -0
  11. package/lib/avs/utils/register.js +79 -0
  12. package/lib/avs/utils/tasks.js +41 -0
  13. package/lib/client/create-claim.js +432 -0
  14. package/lib/client/index.js +3 -0
  15. package/lib/client/tunnels/make-rpc-tcp-tunnel.js +51 -0
  16. package/lib/client/tunnels/make-rpc-tls-tunnel.js +131 -0
  17. package/lib/client/utils/attestor-pool.js +25 -0
  18. package/lib/client/utils/client-socket.js +97 -0
  19. package/lib/client/utils/message-handler.js +87 -0
  20. package/lib/config/index.js +44 -0
  21. package/lib/external-rpc/benchmark.js +69 -0
  22. package/lib/external-rpc/event-bus.js +14 -0
  23. package/lib/external-rpc/handle-incoming-msg.js +232 -0
  24. package/lib/external-rpc/index.js +3 -10399
  25. package/lib/external-rpc/jsc-polyfills/1.js +82 -0
  26. package/lib/external-rpc/jsc-polyfills/2.js +20 -0
  27. package/lib/external-rpc/jsc-polyfills/event.js +14 -0
  28. package/lib/external-rpc/jsc-polyfills/index.js +2 -0
  29. package/lib/external-rpc/jsc-polyfills/ws.js +81 -0
  30. package/lib/external-rpc/setup-browser.js +33 -0
  31. package/lib/external-rpc/setup-jsc.js +22 -0
  32. package/lib/external-rpc/types.d.ts +0 -1
  33. package/lib/external-rpc/utils.js +100 -0
  34. package/lib/external-rpc/zk.js +63 -0
  35. package/lib/index.js +9 -8326
  36. package/lib/mechain/abis/governanceABI.js +458 -0
  37. package/lib/mechain/abis/taskABI.js +509 -0
  38. package/lib/mechain/client/create-claim-on-mechain.js +28 -0
  39. package/lib/mechain/client/index.js +1 -0
  40. package/lib/mechain/constants/index.js +3 -0
  41. package/lib/mechain/index.js +2 -0
  42. package/lib/mechain/types/index.js +1 -0
  43. package/lib/proto/api.js +4363 -0
  44. package/lib/proto/tee-bundle.js +1316 -0
  45. package/lib/providers/http/index.js +653 -0
  46. package/lib/providers/http/patch-parse5-tree.js +32 -0
  47. package/lib/providers/http/utils.js +324 -0
  48. package/lib/providers/index.js +4 -0
  49. package/lib/server/create-server.js +103 -0
  50. package/lib/server/handlers/claimTeeBundle.js +252 -0
  51. package/lib/server/handlers/claimTunnel.js +73 -0
  52. package/lib/server/handlers/completeClaimOnChain.js +24 -0
  53. package/lib/server/handlers/createClaimOnChain.js +26 -0
  54. package/lib/server/handlers/createTaskOnMechain.js +47 -0
  55. package/lib/server/handlers/createTunnel.js +93 -0
  56. package/lib/server/handlers/disconnectTunnel.js +5 -0
  57. package/lib/server/handlers/fetchCertificateBytes.js +41 -0
  58. package/lib/server/handlers/index.js +22 -0
  59. package/lib/server/handlers/init.js +32 -0
  60. package/lib/server/handlers/toprf.js +16 -0
  61. package/lib/server/index.js +4 -0
  62. package/lib/server/socket.js +109 -0
  63. package/lib/server/tunnels/make-tcp-tunnel.js +177 -0
  64. package/lib/server/utils/apm.js +36 -0
  65. package/lib/server/utils/assert-valid-claim-request.js +325 -0
  66. package/lib/server/utils/config-env.js +4 -0
  67. package/lib/server/utils/dns.js +18 -0
  68. package/lib/server/utils/gcp-attestation.js +289 -0
  69. package/lib/server/utils/generics.d.ts +1 -1
  70. package/lib/server/utils/generics.js +51 -0
  71. package/lib/server/utils/iso.js +256 -0
  72. package/lib/server/utils/keep-alive.js +38 -0
  73. package/lib/server/utils/nitro-attestation.js +324 -0
  74. package/lib/server/utils/oprf-raw.js +54 -0
  75. package/lib/server/utils/process-handshake.js +215 -0
  76. package/lib/server/utils/proxy-session.js +6 -0
  77. package/lib/server/utils/tee-oprf-mpc-verification.js +90 -0
  78. package/lib/server/utils/tee-oprf-verification.js +174 -0
  79. package/lib/server/utils/tee-transcript-reconstruction.js +187 -0
  80. package/lib/server/utils/tee-verification.js +421 -0
  81. package/lib/server/utils/validation.js +38 -0
  82. package/lib/types/bgp.js +1 -0
  83. package/lib/types/claims.js +1 -0
  84. package/lib/types/client.js +1 -0
  85. package/lib/types/general.js +1 -0
  86. package/lib/types/handlers.js +1 -0
  87. package/lib/types/index.js +10 -0
  88. package/lib/types/providers.d.ts +3 -2
  89. package/lib/types/providers.gen.js +10 -0
  90. package/lib/types/providers.js +1 -0
  91. package/lib/types/rpc.js +1 -0
  92. package/lib/types/signatures.d.ts +1 -2
  93. package/lib/types/signatures.js +1 -0
  94. package/lib/types/tunnel.js +1 -0
  95. package/lib/types/zk.js +1 -0
  96. package/lib/utils/auth.js +59 -0
  97. package/lib/utils/b64-json.js +17 -0
  98. package/lib/utils/bgp-listener.js +119 -0
  99. package/lib/utils/claims.js +98 -0
  100. package/lib/utils/env.js +15 -0
  101. package/lib/utils/error.js +50 -0
  102. package/lib/utils/generics.js +317 -0
  103. package/lib/utils/http-parser.js +246 -0
  104. package/lib/utils/index.js +13 -0
  105. package/lib/utils/logger.js +91 -0
  106. package/lib/utils/prepare-packets.js +71 -0
  107. package/lib/utils/redactions.js +177 -0
  108. package/lib/utils/retries.js +24 -0
  109. package/lib/utils/signatures/eth.js +32 -0
  110. package/lib/utils/signatures/index.js +7 -0
  111. package/lib/utils/socket-base.js +92 -0
  112. package/lib/utils/tls.js +58 -0
  113. package/lib/utils/ws.js +22 -0
  114. package/lib/utils/zk.js +585 -0
  115. package/package.json +5 -3
  116. package/lib/scripts/check-avs-registration.d.ts +0 -1
  117. package/lib/scripts/fallbacks/crypto.d.ts +0 -1
  118. package/lib/scripts/fallbacks/empty.d.ts +0 -3
  119. package/lib/scripts/fallbacks/re2.d.ts +0 -1
  120. package/lib/scripts/fallbacks/snarkjs.d.ts +0 -1
  121. package/lib/scripts/fallbacks/stwo.d.ts +0 -6
  122. package/lib/scripts/generate-provider-types.d.ts +0 -5
  123. package/lib/scripts/generate-receipt.d.ts +0 -9
  124. package/lib/scripts/jsc-cli-rpc.d.ts +0 -1
  125. package/lib/scripts/register-avs-operator.d.ts +0 -1
  126. package/lib/scripts/start-server.d.ts +0 -1
  127. package/lib/scripts/update-avs-metadata.d.ts +0 -1
  128. package/lib/scripts/utils.d.ts +0 -1
  129. package/lib/scripts/whitelist-operator.d.ts +0 -1
  130. /package/lib/{scripts/build-browser.d.ts → avs/contracts/ReclaimServiceManager.js} +0 -0
  131. /package/lib/{scripts/build-jsc.d.ts → avs/contracts/common.js} +0 -0
  132. /package/lib/{scripts/build-lib.d.ts → avs/types/index.js} +0 -0
  133. /package/lib/{scripts/generate-toprf-keys.d.ts → external-rpc/types.js} +0 -0
@@ -0,0 +1,324 @@
1
+ // noinspection ExceptionCaughtLocallyJS
2
+ import "./patch-parse5-tree.js";
3
+ import { concatenateUint8Arrays } from '@reclaimprotocol/tls';
4
+ import { ArrayExpression, ExpressionStatement, ObjectExpression, parseScript, Property, Syntax } from 'esprima-next';
5
+ import { JSONPath } from 'jsonpath-plus';
6
+ import { parse } from 'parse5';
7
+ import { adapter as htmlAdapter, } from 'parse5-htmlparser2-tree-adapter';
8
+ import RE2 from 're2';
9
+ import xpath from 'xpath';
10
+ import { getHttpRequestDataFromTranscript, isApplicationData, makeHttpResponseParser, REDACTION_CHAR_CODE } from "../../utils/index.js";
11
+ /**
12
+ * Returns only first extracted element
13
+ * @param html
14
+ * @param xpathExpression
15
+ * @param contentsOnly
16
+ */
17
+ export function extractHTMLElement(html, xpathExpression, contentsOnly) {
18
+ const { start, end } = extractHTMLElementIndex(html, xpathExpression, contentsOnly);
19
+ return html.slice(start, end);
20
+ }
21
+ /**
22
+ * Returns all extracted elements
23
+ * @param html
24
+ * @param xpathExpression
25
+ * @param contentsOnly
26
+ */
27
+ export function extractHTMLElements(html, xpathExpression, contentsOnly) {
28
+ const indexes = extractHTMLElementsIndexes(html, xpathExpression, contentsOnly);
29
+ const res = [];
30
+ for (const { start, end } of indexes) {
31
+ res.push(html.slice(start, end));
32
+ }
33
+ return res;
34
+ }
35
+ /**
36
+ * returns a single index of extracted element
37
+ * @param html
38
+ * @param xpathExpression
39
+ * @param contentsOnly
40
+ */
41
+ export function extractHTMLElementIndex(html, xpathExpression, contentsOnly) {
42
+ return extractHTMLElementsIndexes(html, xpathExpression, contentsOnly)[0];
43
+ }
44
+ /**
45
+ * Returns indexes of all extracted elements
46
+ * @param html
47
+ * @param xpathExpression
48
+ * @param contentsOnly indices of the start and end of the element's contents only,
49
+ * not the whole tag
50
+ */
51
+ export function extractHTMLElementsIndexes(html, xpathExpression, contentsOnly) {
52
+ return extractHTMLElementIndexesParse5(html, xpathExpression, contentsOnly);
53
+ }
54
+ function extractHTMLElementIndexesParse5(html, xpathExpression, contentsOnly) {
55
+ const domLight = parse(html, { treeAdapter: htmlAdapter, sourceCodeLocationInfo: true });
56
+ // lets xpath identify this as a node
57
+ domLight['name'] = 'root';
58
+ const parsedPath = xpath.parse(xpathExpression);
59
+ const nodes = parsedPath.select({
60
+ node: domLight,
61
+ allowAnyNamespaceForNoPrefix: true,
62
+ });
63
+ if (!nodes.length) {
64
+ throw new Error(`Failed to find XPath: "${xpathExpression}"`);
65
+ }
66
+ return nodes.map(node => getNodeRange(node, contentsOnly));
67
+ }
68
+ function getNodeRange(node, contentsOnly) {
69
+ if (!contentsOnly) {
70
+ return { start: node.startIndex, end: node.endIndex };
71
+ }
72
+ if (!('firstChild' in node) || !node.firstChild) {
73
+ throw new Error(`Node "${node['name']}" has no children`);
74
+ }
75
+ return {
76
+ start: node.firstChild.startIndex,
77
+ end: node.lastChild.endIndex
78
+ };
79
+ }
80
+ export function extractJSONValueIndex(json, jsonPath) {
81
+ return extractJSONValueIndexes(json, jsonPath)[0];
82
+ }
83
+ export function extractJSONValueIndexes(json, jsonPath) {
84
+ const pointers = JSONPath({
85
+ path: jsonPath,
86
+ json: JSON.parse(json),
87
+ wrap: false,
88
+ resultType: 'pointer',
89
+ eval: 'safe',
90
+ // @ts-ignore
91
+ ignoreEvalErrors: true
92
+ });
93
+ if (!pointers) {
94
+ throw new Error('jsonPath not found');
95
+ }
96
+ //wrap in parentheses for esprima to parse
97
+ const tree = parseScript('(' + json + ')', { range: true });
98
+ if (tree.body[0] instanceof ExpressionStatement
99
+ && (tree.body[0].expression instanceof ObjectExpression || tree.body[0].expression instanceof ArrayExpression)) {
100
+ const traversePointers = Array.isArray(pointers) ? pointers : [pointers];
101
+ const res = [];
102
+ for (const pointer of traversePointers) {
103
+ const index = traverse(tree.body[0].expression, '', [pointer]);
104
+ if (index) {
105
+ res.push({
106
+ start: index.start - 1, //account for '('
107
+ end: index.end - 1,
108
+ });
109
+ }
110
+ }
111
+ return res;
112
+ }
113
+ throw new Error('jsonPath not found');
114
+ }
115
+ /**
116
+ * recursively go through AST tree and build a JSON path while it's not equal to the one we search for
117
+ * @param o - esprima expression for root object
118
+ * @param path - path that is being built
119
+ * @param pointers - JSON pointers to compare to
120
+ */
121
+ function traverse(o, path, pointers) {
122
+ if (o instanceof ObjectExpression) {
123
+ for (const p of o.properties) {
124
+ if (!(p instanceof Property)) {
125
+ continue;
126
+ }
127
+ const localPath = p.key.type === Syntax.Literal
128
+ ? path + '/' + p.key.value
129
+ : path;
130
+ if (pointers.includes(localPath) && 'range' in p && Array.isArray(p.range)) {
131
+ return {
132
+ start: p.range[0],
133
+ end: p.range[1],
134
+ };
135
+ }
136
+ if (p.value instanceof ObjectExpression
137
+ || p.value instanceof ArrayExpression) {
138
+ const res = traverse(p.value, localPath, pointers);
139
+ if (res) {
140
+ return res;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ if (o instanceof ArrayExpression) {
146
+ for (let i = 0; i < o.elements.length; i++) {
147
+ const element = o.elements[i];
148
+ if (!element) {
149
+ continue;
150
+ }
151
+ const localPath = path + '/' + i;
152
+ if (pointers.includes(localPath) &&
153
+ 'range' in element &&
154
+ Array.isArray(element.range)) {
155
+ return {
156
+ start: element.range[0],
157
+ end: element.range[1],
158
+ };
159
+ }
160
+ if (element instanceof ObjectExpression) {
161
+ const res = traverse(element, localPath, pointers);
162
+ if (res) {
163
+ return res;
164
+ }
165
+ }
166
+ if (element instanceof ArrayExpression) {
167
+ const res = traverse(element, localPath, pointers);
168
+ if (res) {
169
+ return res;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ export function buildHeaders(input) {
177
+ const headers = [];
178
+ for (const [key, value] of Object.entries(input || {})) {
179
+ headers.push(`${key}: ${value}`);
180
+ }
181
+ return headers;
182
+ }
183
+ /**
184
+ * Converts position in HTTP response body to an absolute position in TLS transcript considering chunked encoding
185
+ * @param pos
186
+ * @param bodyStartIdx
187
+ * @param chunks
188
+ */
189
+ export function convertResponsePosToAbsolutePos(pos, bodyStartIdx, chunks) {
190
+ if (chunks?.length) {
191
+ let chunkBodyStart = 0;
192
+ for (const chunk of chunks) {
193
+ const chunkSize = chunk.toIndex - chunk.fromIndex;
194
+ if (pos >= chunkBodyStart && pos <= (chunkBodyStart + chunkSize)) {
195
+ return pos - chunkBodyStart + chunk.fromIndex;
196
+ }
197
+ chunkBodyStart += chunkSize;
198
+ }
199
+ throw new Error('position out of range');
200
+ }
201
+ return bodyStartIdx + pos;
202
+ }
203
+ /**
204
+ * If this reveal spans the boundary of two chunks, we'll
205
+ *
206
+ */
207
+ export function getRedactionsForChunkHeaders(from, to, chunks) {
208
+ const res = [];
209
+ if (!chunks?.length) {
210
+ return res;
211
+ }
212
+ for (let i = 1; i < chunks?.length; i++) {
213
+ const chunk = chunks[i];
214
+ if (chunk.fromIndex > from && chunk.fromIndex < to) {
215
+ res.push({
216
+ fromIndex: chunks[i - 1].toIndex,
217
+ toIndex: chunk.fromIndex,
218
+ });
219
+ }
220
+ }
221
+ return res;
222
+ }
223
+ export function parseHttpResponse(buff) {
224
+ const parser = makeHttpResponseParser();
225
+ parser.onChunk(buff);
226
+ parser.streamEnded();
227
+ return parser.res;
228
+ }
229
+ export function makeRegex(str) {
230
+ return RE2(str, 'sgiu');
231
+ }
232
+ const TEMPLATE_START_CHARCODE = '{'.charCodeAt(0);
233
+ const TEMPLATE_END_CHARCODE = '}'.charCodeAt(0);
234
+ /**
235
+ * Try to match strings that contain templates like {{param}}
236
+ * against redacted string that has *** instead of that param
237
+ */
238
+ export function matchRedactedStrings(templateString, redactedString) {
239
+ if (templateString.length === 0 && redactedString?.length === 0) {
240
+ return true;
241
+ }
242
+ if (!redactedString) {
243
+ return false;
244
+ }
245
+ let ts = -1;
246
+ let rs = -1;
247
+ while (ts < templateString.length && rs < redactedString.length) {
248
+ let ct = getTChar();
249
+ let cr = getRChar();
250
+ if (ct !== cr) {
251
+ // only valid if param contains "{" & redacted contains "*"
252
+ if (ct === TEMPLATE_START_CHARCODE && cr === REDACTION_CHAR_CODE) {
253
+ //check that the char after first "{" is also "{"
254
+ if (getTChar() !== TEMPLATE_START_CHARCODE) {
255
+ return false;
256
+ }
257
+ //look for first closing bracket
258
+ while (((ct = getTChar()) !== TEMPLATE_END_CHARCODE) && ct !== -1) {
259
+ }
260
+ //look for second closing bracket
261
+ while (((ct = getTChar()) !== TEMPLATE_END_CHARCODE) && ct !== -1) {
262
+ }
263
+ if (ct === -1) {
264
+ return false;
265
+ }
266
+ //find the end of redaction
267
+ while (((cr = getRChar()) === REDACTION_CHAR_CODE) && cr !== -1) {
268
+ }
269
+ if (cr === -1) {
270
+ //if there's nothing after template too then both ended at the end of strings
271
+ return getTChar() === -1;
272
+ }
273
+ //rewind redacted string position back 1 char because we read one extra
274
+ rs--;
275
+ }
276
+ else {
277
+ return false;
278
+ }
279
+ }
280
+ }
281
+ function getTChar() {
282
+ ts++;
283
+ if (ts < templateString.length) {
284
+ return templateString[ts];
285
+ }
286
+ else {
287
+ return -1;
288
+ }
289
+ }
290
+ function getRChar() {
291
+ if (!redactedString) {
292
+ return -1;
293
+ }
294
+ rs++;
295
+ if (rs < redactedString.length) {
296
+ return redactedString[rs];
297
+ }
298
+ else {
299
+ return -1;
300
+ }
301
+ }
302
+ return ts === templateString.length && rs === redactedString.length;
303
+ }
304
+ export function generateRequstAndResponseFromTranscript(transcript, tlsVersion) {
305
+ const allPackets = transcript;
306
+ const packets = [];
307
+ for (const b of allPackets) {
308
+ if (b.message.type !== 'ciphertext'
309
+ || !isApplicationData(b.message, tlsVersion)) {
310
+ continue;
311
+ }
312
+ const plaintext = tlsVersion === 'TLS1_3'
313
+ ? b.message.plaintext.slice(0, -1)
314
+ : b.message.plaintext;
315
+ packets.push({
316
+ message: plaintext,
317
+ sender: b.sender
318
+ });
319
+ }
320
+ const req = getHttpRequestDataFromTranscript(packets);
321
+ const responsePackets = concatenateUint8Arrays(packets.filter(p => p.sender === 'server').map(p => p.message).filter(b => !b.every(b => b === REDACTION_CHAR_CODE)));
322
+ const res = parseHttpResponse(responsePackets);
323
+ return { req, res };
324
+ }
@@ -0,0 +1,4 @@
1
+ import http from "./http/index.js";
2
+ export const providers = {
3
+ http,
4
+ };
@@ -0,0 +1,103 @@
1
+ import { createServer as createHttpServer } from 'http';
2
+ import serveStatic from 'serve-static';
3
+ import { WebSocketServer } from 'ws';
4
+ import { API_SERVER_PORT, ATTESTOR_ADDRESS_PATHNAME, BROWSER_RPC_PATHNAME, WS_PATHNAME } from "../config/index.js";
5
+ import { AttestorServerSocket } from "./socket.js";
6
+ import { getAttestorAddress } from "./utils/generics.js";
7
+ import { addKeepAlive } from "./utils/keep-alive.js";
8
+ import { createBgpListener } from "../utils/bgp-listener.js";
9
+ import { getEnvVariable } from "../utils/env.js";
10
+ import { logger as LOGGER } from "../utils/index.js";
11
+ import { SelectedServiceSignatureType } from "../utils/signatures/index.js";
12
+ import { promisifySend } from "../utils/ws.js";
13
+ const PORT = +(getEnvVariable('PORT') || API_SERVER_PORT);
14
+ const DISABLE_BGP_CHECKS = getEnvVariable('DISABLE_BGP_CHECKS') === '1';
15
+ const ATTESTOR_ADDRESS_JSON_RES = JSON.stringify({
16
+ address: getAttestorAddress(SelectedServiceSignatureType),
17
+ signatureType: SelectedServiceSignatureType
18
+ });
19
+ /**
20
+ * Creates the WebSocket API server,
21
+ * creates a fileserver to serve the browser RPC client,
22
+ * and listens on the given port.
23
+ */
24
+ export async function createServer(port = PORT) {
25
+ const http = createHttpServer();
26
+ const serveBrowserRpc = serveStatic('browser', {
27
+ index: ['index.html'],
28
+ setHeaders(res) {
29
+ res.setHeader('Access-Control-Allow-Origin', '*');
30
+ },
31
+ });
32
+ const bgpListener = !DISABLE_BGP_CHECKS
33
+ ? createBgpListener(LOGGER.child({ service: 'bgp-listener' }))
34
+ : undefined;
35
+ const wss = new WebSocketServer({ noServer: true });
36
+ http.on('upgrade', handleUpgrade.bind(wss));
37
+ http.on('request', (req, res) => {
38
+ const url = URL.parse(req.url || '', 'http://localhost');
39
+ if (!url) {
40
+ res.statusCode = 422;
41
+ res.end('Invalid URL');
42
+ return;
43
+ }
44
+ if (url.pathname === ATTESTOR_ADDRESS_PATHNAME) {
45
+ res.writeHead(200, { 'Content-Type': 'application/json' });
46
+ res.end(ATTESTOR_ADDRESS_JSON_RES);
47
+ return;
48
+ }
49
+ // simple way to serve files at the browser RPC path
50
+ if (!url.pathname?.startsWith(BROWSER_RPC_PATHNAME)) {
51
+ res.statusCode = 404;
52
+ res.end('Not found');
53
+ return;
54
+ }
55
+ req.url = req.url.slice(BROWSER_RPC_PATHNAME.length) || '/';
56
+ serveBrowserRpc(req, res, (err) => {
57
+ if (err) {
58
+ LOGGER.error({ err, url: req.url }, 'Failed to serve file');
59
+ }
60
+ res.statusCode = err?.statusCode ?? 404;
61
+ res.end(err?.message ?? 'Not found');
62
+ });
63
+ });
64
+ // wait for us to start listening
65
+ http.listen(port);
66
+ await new Promise((resolve, reject) => {
67
+ http.once('listening', () => resolve());
68
+ http.once('error', reject);
69
+ });
70
+ wss.on('connection', (ws, req) => handleNewClient(ws, req, bgpListener));
71
+ LOGGER.info({
72
+ port,
73
+ apiPath: WS_PATHNAME,
74
+ browserRpcPath: BROWSER_RPC_PATHNAME,
75
+ signerAddress: getAttestorAddress(SelectedServiceSignatureType)
76
+ }, 'WS server listening');
77
+ const wssClose = wss.close.bind(wss);
78
+ wss.close = (cb) => {
79
+ wssClose(() => http.close(cb));
80
+ bgpListener?.close();
81
+ };
82
+ return wss;
83
+ }
84
+ async function handleNewClient(ws, req, bgpListener) {
85
+ promisifySend(ws);
86
+ const client = await AttestorServerSocket.acceptConnection(ws, { req, bgpListener, logger: LOGGER });
87
+ // if initialisation fails, don't store the client
88
+ if (!client) {
89
+ return;
90
+ }
91
+ ws.serverSocket = client;
92
+ addKeepAlive(ws, LOGGER.child({ sessionId: client.sessionId }));
93
+ }
94
+ function handleUpgrade(request, socket, head) {
95
+ const { pathname } = new URL(request.url, 'wss://base.url');
96
+ if (pathname === WS_PATHNAME) {
97
+ this.handleUpgrade(request, socket, head, (ws) => {
98
+ this.emit('connection', ws, request);
99
+ });
100
+ return;
101
+ }
102
+ socket.destroy();
103
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * TEE Bundle Claim Handler
3
+ * Handles ClaimTeeBundleRequest by verifying TEE attestations and reconstructing TLS transcript
4
+ */
5
+ import { ClaimTeeBundleResponse } from "../../proto/api.js";
6
+ import { VerificationBundle } from "../../proto/tee-bundle.js";
7
+ import { substituteParamValues } from "../../providers/http/index.js";
8
+ import { assertValidProviderTranscript } from "../utils/assert-valid-claim-request.js";
9
+ import { getAttestorAddress, niceParseJsonObject, signAsAttestor } from "../utils/generics.js";
10
+ import { verifyOprfMpcOutputs } from "../utils/tee-oprf-mpc-verification.js";
11
+ import { verifyOprfProofs } from "../utils/tee-oprf-verification.js";
12
+ import { reconstructTlsTranscript } from "../utils/tee-transcript-reconstruction.js";
13
+ import { verifyTeeBundle } from "../utils/tee-verification.js";
14
+ import { AttestorError } from "../../utils/error.js";
15
+ import { createSignDataForClaim, getIdentifierFromClaimInfo } from "../../utils/index.js";
16
+ export const claimTeeBundle = async (teeBundleRequest, { logger, client }) => {
17
+ const { verificationBundle, data } = teeBundleRequest;
18
+ // Initialize response
19
+ const res = ClaimTeeBundleResponse.create({ request: teeBundleRequest });
20
+ // 1. Verify TEE bundle (attestations + signatures) - this includes timestamp validation
21
+ logger.info('Starting TEE bundle verification');
22
+ const teeData = await verifyTeeBundle(verificationBundle, logger);
23
+ // 2. Extract timestampS from TEE_K bundle for claim signing
24
+ const timestampS = Math.floor(teeData.kOutputPayload.timestampMs / 1000);
25
+ // 3. Verify OPRF proofs first (before transcript reconstruction)
26
+ logger.info('Verifying OPRF proofs');
27
+ // Parse the verification bundle to get OPRF verifications
28
+ const bundle = VerificationBundle.decode(verificationBundle);
29
+ const zkOprfResults = await verifyOprfProofs({ ...teeData, oprfVerifications: bundle.oprfVerifications }, logger);
30
+ // 4. Verify OPRF MPC outputs (TEE-to-TEE computed OPRF)
31
+ logger.info('Verifying OPRF MPC outputs');
32
+ const oprfMpcResults = verifyOprfMpcOutputs(teeData.kOutputPayload, teeData.tOutputPayload, logger);
33
+ // 5. Combine ZK and OPRF MPC results for transcript reconstruction
34
+ const allOprfResults = validateAndCombineOprfResults(zkOprfResults, oprfMpcResults, logger);
35
+ // 6. Reconstruct TLS transcript with all OPRF replacements applied
36
+ logger.info('Starting TLS transcript reconstruction with OPRF replacements');
37
+ const transcriptData = await reconstructTlsTranscript(teeData, logger, allOprfResults);
38
+ // 7. Create plaintext transcript for provider validation (OPRF already applied)
39
+ logger.info('Creating plaintext transcript from TEE data');
40
+ const plaintextTranscript = createPlaintextTranscriptFromTeeData(transcriptData, logger);
41
+ // 8. Direct provider validation
42
+ logger.info('Running direct provider validation on TEE reconstructed data');
43
+ if (!data) {
44
+ throw new AttestorError('ERROR_INVALID_CLAIM', 'No claim data provided in TEE bundle request');
45
+ }
46
+ const validatedClaim = await validateTeeProviderReceipt(plaintextTranscript, data, logger, { version: client.metadata.clientVersion }, transcriptData.certificateInfo);
47
+ const ctx = niceParseJsonObject(validatedClaim.context, 'context');
48
+ // eslint-disable-next-line camelcase
49
+ ctx.pcr0_k = teeData.teekPcr0;
50
+ // eslint-disable-next-line camelcase
51
+ ctx.pcr0_t = teeData.teetPcr0;
52
+ // eslint-disable-next-line camelcase
53
+ ctx.tee_session_id = teeData.teeSessionId;
54
+ validatedClaim.context = JSON.stringify(ctx);
55
+ res.claim = {
56
+ ...validatedClaim,
57
+ identifier: getIdentifierFromClaimInfo(validatedClaim),
58
+ // Use timestampS from TEE_K bundle for claim signing
59
+ timestampS,
60
+ // hardcode for compatibility with V1 claims
61
+ epoch: 1
62
+ };
63
+ logger.info({ claim: res.claim }, 'TEE bundle claim validation successful');
64
+ // 9. Sign the response
65
+ res.signatures = {
66
+ attestorAddress: getAttestorAddress(client.metadata.signatureType),
67
+ claimSignature: res.claim
68
+ ? await signAsAttestor(createSignDataForClaim(res.claim), client.metadata.signatureType)
69
+ : new Uint8Array(),
70
+ resultSignature: await signAsAttestor(ClaimTeeBundleResponse.encode(res).finish(), client.metadata.signatureType)
71
+ };
72
+ logger.info('TEE bundle claim processing completed');
73
+ return res;
74
+ };
75
+ /**
76
+ * Creates a plaintext transcript from TEE reconstructed data
77
+ * This converts the TEE transcript data into the format expected by provider validation
78
+ * NEW: Uses consolidated response instead of individual packets for simplicity
79
+ */
80
+ function createPlaintextTranscriptFromTeeData(transcriptData, logger) {
81
+ const transcript = [];
82
+ // Add reconstructed request (client -> server)
83
+ if (transcriptData.revealedRequest && transcriptData.revealedRequest.length > 0) {
84
+ transcript.push({
85
+ sender: 'client',
86
+ message: transcriptData.revealedRequest
87
+ });
88
+ logger.debug('Added TEE revealed request to plaintext transcript', {
89
+ length: transcriptData.revealedRequest.length
90
+ });
91
+ }
92
+ // Add consolidated reconstructed response (server -> client)
93
+ if (transcriptData.reconstructedResponse && transcriptData.reconstructedResponse.length > 0) {
94
+ transcript.push({
95
+ sender: 'server',
96
+ message: transcriptData.reconstructedResponse
97
+ });
98
+ logger.debug('Added TEE consolidated response to plaintext transcript', {
99
+ length: transcriptData.reconstructedResponse.length
100
+ });
101
+ }
102
+ // Log certificate validation info if available
103
+ if (transcriptData.certificateInfo) {
104
+ logger.info('Certificate information available for validation', {
105
+ commonName: transcriptData.certificateInfo.commonName,
106
+ issuerCommonName: transcriptData.certificateInfo.issuerCommonName,
107
+ dnsNames: transcriptData.certificateInfo.dnsNames,
108
+ notBefore: new Date(transcriptData.certificateInfo.notBeforeUnix * 1000).toISOString(),
109
+ notAfter: new Date(transcriptData.certificateInfo.notAfterUnix * 1000).toISOString()
110
+ });
111
+ }
112
+ logger.info('Created plaintext transcript from TEE data', {
113
+ totalMessages: transcript.length,
114
+ hasRequest: !!transcriptData.revealedRequest?.length,
115
+ hasResponse: !!transcriptData.reconstructedResponse?.length,
116
+ hasCertificateInfo: !!transcriptData.certificateInfo
117
+ });
118
+ return transcript;
119
+ }
120
+ /**
121
+ * Validates TEE provider receipt directly without signature validation
122
+ * This is essentially assertValidProviderTranscript but for TEE data
123
+ * NEW: Includes certificate validation for domain authentication
124
+ */
125
+ async function validateTeeProviderReceipt(plaintextTranscript, claimInfo, logger, providerCtx, certificateInfo) {
126
+ logger.info('Starting direct TEE provider validation', {
127
+ provider: claimInfo.provider,
128
+ transcriptMessages: plaintextTranscript.length,
129
+ hasCertificateInfo: !!certificateInfo
130
+ });
131
+ // Validate certificate if available
132
+ if (certificateInfo) {
133
+ validateTlsCertificate(claimInfo, certificateInfo, logger);
134
+ }
135
+ // Use the existing provider validation logic directly
136
+ const validatedClaim = await assertValidProviderTranscript(plaintextTranscript, claimInfo, logger, providerCtx);
137
+ logger.info('TEE provider validation completed successfully', {
138
+ provider: validatedClaim.provider,
139
+ owner: validatedClaim.owner || 'unknown'
140
+ });
141
+ return validatedClaim;
142
+ }
143
+ /**
144
+ * Checks if a hostname matches a certificate name (with wildcard support)
145
+ * @param hostname - The hostname to check
146
+ * @param certName - The certificate name
147
+ * @returns true if the hostname is valid for this certificate name
148
+ */
149
+ function isHostnameValidForCertificate(hostname, certName) {
150
+ // Exact match
151
+ if (hostname === certName) {
152
+ return true;
153
+ }
154
+ // Wildcard match
155
+ if (certName.startsWith('*.')) {
156
+ // Extract the domain from wildcard
157
+ const wildcardDomain = certName.slice(2);
158
+ // Check if hostname ends with the wildcard domain
159
+ if (hostname.endsWith(wildcardDomain)) {
160
+ // Ensure we're matching a subdomain, not partial domain
161
+ const subdomainPart = hostname.slice(0, -(wildcardDomain.length));
162
+ // Valid if:
163
+ // 1. The subdomain part ends with a dot (proper subdomain boundary)
164
+ // 2. The subdomain part doesn't contain additional dots (single-level wildcard)
165
+ if (subdomainPart.endsWith('.')) {
166
+ const subdomain = subdomainPart.slice(0, -1);
167
+ // Wildcard only matches single level, not multiple subdomains
168
+ return !subdomain.includes('.');
169
+ }
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ /**
175
+ * Validates that the TLS certificate is valid for the domain being claimed
176
+ */
177
+ function validateTlsCertificate(claimInfo, certificateInfo, logger) {
178
+ // Extract hostname from the claim (this varies by provider)
179
+ let claimedHostname;
180
+ const paramsWithTemplates = niceParseJsonObject(claimInfo.parameters, 'params');
181
+ const params = substituteParamValues(paramsWithTemplates, undefined, true).newParams;
182
+ // Different providers store hostname in different places
183
+ if ('url' in params && typeof params.url === 'string') {
184
+ claimedHostname = new URL(params.url).hostname;
185
+ }
186
+ if (!claimedHostname) {
187
+ logger.warn('Could not extract hostname from claim for certificate validation', {
188
+ provider: claimInfo.provider
189
+ });
190
+ throw new AttestorError('ERROR_INVALID_CLAIM', 'Certificate validation failed: hostname not found');
191
+ }
192
+ logger.info('Validating TLS certificate for claimed hostname', {
193
+ claimedHostname,
194
+ certificateCommonName: certificateInfo.commonName,
195
+ certificateDnsNames: certificateInfo.dnsNames
196
+ });
197
+ // Check if claimed hostname matches certificate (including wildcard support)
198
+ const isValidForHostname = isHostnameValidForCertificate(claimedHostname, certificateInfo.commonName) ||
199
+ certificateInfo.dnsNames.some(name => isHostnameValidForCertificate(claimedHostname, name));
200
+ if (!isValidForHostname) {
201
+ throw new AttestorError('ERROR_INVALID_CLAIM', `Certificate validation failed: hostname '${claimedHostname}' not valid for certificate (CN: ${certificateInfo.commonName}, SANs: ${certificateInfo.dnsNames.join(', ')})`);
202
+ }
203
+ // Check certificate validity period
204
+ const now = Date.now() / 1000; // Current time in Unix seconds
205
+ if (now < certificateInfo.notBeforeUnix || now > certificateInfo.notAfterUnix) {
206
+ throw new AttestorError('ERROR_INVALID_CLAIM', `Certificate validation failed: certificate not valid at current time (valid from ${new Date(certificateInfo.notBeforeUnix * 1000).toISOString()} to ${new Date(certificateInfo.notAfterUnix * 1000).toISOString()})`);
207
+ }
208
+ logger.info('TLS certificate validation passed', {
209
+ claimedHostname,
210
+ validatedAgainst: isHostnameValidForCertificate(claimedHostname, certificateInfo.commonName) ?
211
+ `CommonName: ${certificateInfo.commonName}` :
212
+ `SAN: ${certificateInfo.dnsNames.find(name => isHostnameValidForCertificate(claimedHostname, name))}`
213
+ });
214
+ }
215
+ /**
216
+ * Validates OPRF results have no overlapping ranges and combines them
217
+ * SECURITY: Prevents position collisions between ZK and OPRF MPC results
218
+ */
219
+ function validateAndCombineOprfResults(zkOprfResults, oprfMpcResults, logger) {
220
+ const allOprfResults = [...zkOprfResults, ...oprfMpcResults];
221
+ if (allOprfResults.length === 0) {
222
+ return allOprfResults;
223
+ }
224
+ logger.info(`Combined ${zkOprfResults.length} ZK OPRF + ${oprfMpcResults.length} OPRF MPC results`);
225
+ // Check for overlapping ranges (position collision detection)
226
+ const seen = {};
227
+ for (const result of zkOprfResults) {
228
+ seen[result.position] = { length: result.length, source: 'zk' };
229
+ }
230
+ for (const result of oprfMpcResults) {
231
+ const existing = seen[result.position];
232
+ if (existing) {
233
+ // Exact duplicate at same position - verify they match
234
+ if (existing.length !== result.length) {
235
+ throw new AttestorError('ERROR_INVALID_CLAIM', `OPRF range conflict at position ${result.position}: ZK length ${existing.length} vs MPC length ${result.length}`);
236
+ }
237
+ logger.warn(`Duplicate OPRF range at position ${result.position} from both ZK and MPC - using MPC result`);
238
+ }
239
+ // Check for overlapping (but not identical) ranges
240
+ for (const [pos, data] of Object.entries(seen)) {
241
+ const position = Number(pos);
242
+ const existingEnd = position + data.length;
243
+ const newEnd = result.position + result.length;
244
+ const overlaps = (result.position < existingEnd && newEnd > position) && result.position !== position;
245
+ if (overlaps) {
246
+ throw new AttestorError('ERROR_INVALID_CLAIM', `Overlapping OPRF ranges: [${position}:${existingEnd}] (${data.source}) and [${result.position}:${newEnd}] (mpc)`);
247
+ }
248
+ }
249
+ seen[result.position] = { length: result.length, source: 'mpc' };
250
+ }
251
+ return allOprfResults;
252
+ }