@reclaimprotocol/attestor-core 5.0.1-beta.21 → 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 (132) 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.js +1 -0
  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/proto/api.js +4363 -0
  43. package/lib/proto/tee-bundle.js +1316 -0
  44. package/lib/providers/http/index.js +653 -0
  45. package/lib/providers/http/patch-parse5-tree.js +32 -0
  46. package/lib/providers/http/utils.js +324 -0
  47. package/lib/providers/index.js +4 -0
  48. package/lib/server/create-server.js +103 -0
  49. package/lib/server/handlers/claimTeeBundle.js +252 -0
  50. package/lib/server/handlers/claimTunnel.js +73 -0
  51. package/lib/server/handlers/completeClaimOnChain.js +24 -0
  52. package/lib/server/handlers/createClaimOnChain.js +26 -0
  53. package/lib/server/handlers/createTaskOnMechain.js +47 -0
  54. package/lib/server/handlers/createTunnel.js +93 -0
  55. package/lib/server/handlers/disconnectTunnel.js +5 -0
  56. package/lib/server/handlers/fetchCertificateBytes.js +41 -0
  57. package/lib/server/handlers/index.js +22 -0
  58. package/lib/server/handlers/init.js +32 -0
  59. package/lib/server/handlers/toprf.js +16 -0
  60. package/lib/server/index.js +4 -0
  61. package/lib/server/socket.js +109 -0
  62. package/lib/server/tunnels/make-tcp-tunnel.js +177 -0
  63. package/lib/server/utils/apm.js +36 -0
  64. package/lib/server/utils/assert-valid-claim-request.js +325 -0
  65. package/lib/server/utils/config-env.js +4 -0
  66. package/lib/server/utils/dns.js +18 -0
  67. package/lib/server/utils/gcp-attestation.js +289 -0
  68. package/lib/server/utils/generics.d.ts +1 -1
  69. package/lib/server/utils/generics.js +51 -0
  70. package/lib/server/utils/iso.js +256 -0
  71. package/lib/server/utils/keep-alive.js +38 -0
  72. package/lib/server/utils/nitro-attestation.js +324 -0
  73. package/lib/server/utils/oprf-raw.js +54 -0
  74. package/lib/server/utils/process-handshake.js +215 -0
  75. package/lib/server/utils/proxy-session.js +6 -0
  76. package/lib/server/utils/tee-oprf-mpc-verification.js +90 -0
  77. package/lib/server/utils/tee-oprf-verification.js +174 -0
  78. package/lib/server/utils/tee-transcript-reconstruction.js +187 -0
  79. package/lib/server/utils/tee-verification.js +421 -0
  80. package/lib/server/utils/validation.js +38 -0
  81. package/lib/types/bgp.js +1 -0
  82. package/lib/types/claims.js +1 -0
  83. package/lib/types/client.js +1 -0
  84. package/lib/types/general.js +1 -0
  85. package/lib/types/handlers.js +1 -0
  86. package/lib/types/index.js +10 -0
  87. package/lib/types/providers.d.ts +3 -2
  88. package/lib/types/providers.gen.js +10 -0
  89. package/lib/types/providers.js +1 -0
  90. package/lib/types/rpc.js +1 -0
  91. package/lib/types/signatures.d.ts +1 -2
  92. package/lib/types/signatures.js +1 -0
  93. package/lib/types/tunnel.js +1 -0
  94. package/lib/types/zk.js +1 -0
  95. package/lib/utils/auth.js +59 -0
  96. package/lib/utils/b64-json.js +17 -0
  97. package/lib/utils/bgp-listener.js +119 -0
  98. package/lib/utils/claims.js +98 -0
  99. package/lib/utils/env.js +15 -0
  100. package/lib/utils/error.js +50 -0
  101. package/lib/utils/generics.js +317 -0
  102. package/lib/utils/http-parser.js +246 -0
  103. package/lib/utils/index.js +13 -0
  104. package/lib/utils/logger.js +91 -0
  105. package/lib/utils/prepare-packets.js +71 -0
  106. package/lib/utils/redactions.js +177 -0
  107. package/lib/utils/retries.js +24 -0
  108. package/lib/utils/signatures/eth.js +32 -0
  109. package/lib/utils/signatures/index.js +7 -0
  110. package/lib/utils/socket-base.js +92 -0
  111. package/lib/utils/tls.js +58 -0
  112. package/lib/utils/ws.js +22 -0
  113. package/lib/utils/zk.js +585 -0
  114. package/package.json +5 -3
  115. package/lib/scripts/check-avs-registration.d.ts +0 -1
  116. package/lib/scripts/fallbacks/crypto.d.ts +0 -1
  117. package/lib/scripts/fallbacks/empty.d.ts +0 -3
  118. package/lib/scripts/fallbacks/re2.d.ts +0 -1
  119. package/lib/scripts/fallbacks/snarkjs.d.ts +0 -1
  120. package/lib/scripts/fallbacks/stwo.d.ts +0 -6
  121. package/lib/scripts/generate-provider-types.d.ts +0 -5
  122. package/lib/scripts/generate-receipt.d.ts +0 -9
  123. package/lib/scripts/jsc-cli-rpc.d.ts +0 -1
  124. package/lib/scripts/register-avs-operator.d.ts +0 -1
  125. package/lib/scripts/start-server.d.ts +0 -1
  126. package/lib/scripts/update-avs-metadata.d.ts +0 -1
  127. package/lib/scripts/utils.d.ts +0 -1
  128. package/lib/scripts/whitelist-operator.d.ts +0 -1
  129. /package/lib/{scripts/build-browser.d.ts → avs/contracts/ReclaimServiceManager.js} +0 -0
  130. /package/lib/{scripts/build-jsc.d.ts → avs/contracts/common.js} +0 -0
  131. /package/lib/{scripts/build-lib.d.ts → avs/types/index.js} +0 -0
  132. /package/lib/{scripts/generate-toprf-keys.d.ts → mechain/types/index.js} +0 -0
@@ -0,0 +1,177 @@
1
+ import { HttpsProxyAgent } from 'https-proxy-agent';
2
+ import { Socket } from 'net';
3
+ import { CONNECTION_TIMEOUT_MS } from "../../config/index.js";
4
+ import { resolveHostnames } from "../utils/dns.js";
5
+ import { isValidCountryCode } from "../utils/iso.js";
6
+ import { isValidProxySessionId } from "../utils/proxy-session.js";
7
+ import { getEnvVariable } from "../../utils/env.js";
8
+ import { AttestorError } from "../../utils/index.js";
9
+ const HTTPS_PROXY_URL = getEnvVariable('HTTPS_PROXY_URL');
10
+ /**
11
+ * Builds a TCP tunnel to the given host and port.
12
+ * If a geolocation is provided -- an HTTPS proxy is used
13
+ * to connect to the host.
14
+ * If a proxySessionId is provided -- a static ip is used with HTTPS proxy
15
+ * across multiple requests with this same proxySessionId.
16
+ *
17
+ * HTTPS proxy essentially creates an opaque tunnel to the
18
+ * host using the CONNECT method. Any data can be sent through
19
+ * this tunnel to the end host.
20
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT
21
+ *
22
+ * The tunnel also retains a transcript of all messages sent and received.
23
+ */
24
+ export const makeTcpTunnel = async ({ onClose, onMessage, logger, ...opts }) => {
25
+ const transcript = [];
26
+ const socket = await connectTcp({ ...opts, logger });
27
+ let closed = false;
28
+ socket.on('data', message => {
29
+ if (closed) {
30
+ logger.warn('socket is closed, dropping message');
31
+ return;
32
+ }
33
+ onMessage?.(message);
34
+ transcript.push({ sender: 'server', message });
35
+ });
36
+ // socket.once('error', onSocketClose)
37
+ socket.once('close', () => onSocketClose(undefined));
38
+ return {
39
+ socket,
40
+ transcript,
41
+ createRequest: opts,
42
+ async write(data) {
43
+ transcript.push({ sender: 'client', message: data });
44
+ await new Promise((resolve, reject) => {
45
+ socket.write(data, err => {
46
+ if (err) {
47
+ reject(err);
48
+ }
49
+ else {
50
+ resolve();
51
+ }
52
+ });
53
+ });
54
+ },
55
+ close(err) {
56
+ if (closed) {
57
+ return;
58
+ }
59
+ socket.destroy(err);
60
+ }
61
+ };
62
+ function onSocketClose(err) {
63
+ if (closed) {
64
+ return;
65
+ }
66
+ logger.debug({ err }, 'closing socket');
67
+ closed = true;
68
+ onClose?.(err);
69
+ onClose = undefined;
70
+ }
71
+ };
72
+ async function connectTcp({ host, port, geoLocation, proxySessionId, logger }) {
73
+ let connectTimeout;
74
+ let socket;
75
+ try {
76
+ await new Promise(async (resolve, reject) => {
77
+ try {
78
+ // add a timeout to ensure the connection doesn't hang
79
+ // and cause our gateway to send out a 504
80
+ connectTimeout = setTimeout(() => reject(new AttestorError('ERROR_NETWORK_ERROR', 'Server connection timed out')), CONNECTION_TIMEOUT_MS);
81
+ socket = await getSocket({
82
+ host,
83
+ port,
84
+ geoLocation,
85
+ proxySessionId,
86
+ logger
87
+ });
88
+ socket.once('connect', resolve);
89
+ socket.once('error', reject);
90
+ socket.once('end', () => (reject(new AttestorError('ERROR_NETWORK_ERROR', 'connection closed'))));
91
+ }
92
+ catch (err) {
93
+ reject(err);
94
+ }
95
+ });
96
+ logger.debug({ addr: `${host}:${port}` }, 'connected');
97
+ return socket;
98
+ }
99
+ catch (err) {
100
+ socket?.end();
101
+ throw err;
102
+ }
103
+ finally {
104
+ clearTimeout(connectTimeout);
105
+ }
106
+ }
107
+ async function getSocket(opts) {
108
+ const { logger } = opts;
109
+ try {
110
+ return await _getSocket(opts);
111
+ }
112
+ catch (err) {
113
+ // see if the proxy is blocking the connection
114
+ // due to their own arbitrary rules,
115
+ // if so -- we resolve hostname first &
116
+ // connect directly via address to
117
+ // avoid proxy knowing which host we're connecting to
118
+ if (!(err instanceof AttestorError)
119
+ || err.data?.code !== 403) {
120
+ throw err;
121
+ }
122
+ const addrs = await resolveHostnames(opts.host);
123
+ logger.info({ addrs, host: opts.host }, 'failed to connect due to restricted IP, trying via raw addr');
124
+ for (const addr of addrs) {
125
+ try {
126
+ return await _getSocket({ ...opts, host: addr });
127
+ }
128
+ catch (err) {
129
+ logger.error({ addr, err }, 'failed to connect to host');
130
+ }
131
+ }
132
+ throw err;
133
+ }
134
+ }
135
+ async function _getSocket({ host, port, geoLocation, proxySessionId, logger }) {
136
+ const socket = new Socket();
137
+ if ((proxySessionId || geoLocation) && !HTTPS_PROXY_URL) {
138
+ logger.warn({ geoLocation, proxySessionId }, 'geoLocation or proxySessionId provided but no proxy URL found');
139
+ geoLocation = '';
140
+ proxySessionId = '';
141
+ }
142
+ if (!geoLocation && !proxySessionId) {
143
+ socket.connect({ host, port, });
144
+ return socket;
145
+ }
146
+ if (!isValidCountryCode(geoLocation)) {
147
+ throw AttestorError.badRequest(`Geolocation "${geoLocation}" is invalid. Must be 2 letter ISO country code`, { geoLocation });
148
+ }
149
+ if (proxySessionId && !isValidProxySessionId(proxySessionId)) {
150
+ throw AttestorError.badRequest(`proxySessionId "${proxySessionId}" is invalid. Must be a lowercase alphanumeric string of length 8-14 characters. eg. "mystring12345", "something1234".`, { proxySessionId });
151
+ }
152
+ const agentUrl = HTTPS_PROXY_URL.replace('{{geoLocation}}', geoLocation?.toLowerCase() || '').replace('{{proxySessionId}}', proxySessionId ? `-session-${proxySessionId}` : '');
153
+ const agent = new HttpsProxyAgent(agentUrl);
154
+ const waitForProxyRes = new Promise(resolve => {
155
+ // @ts-ignore
156
+ socket.once('proxyConnect', resolve);
157
+ });
158
+ const proxySocket = await agent.connect(
159
+ // ignore, because https-proxy-agent
160
+ // expects an http request object
161
+ // @ts-ignore
162
+ socket, { host, port, timeout: CONNECTION_TIMEOUT_MS });
163
+ const res = await waitForProxyRes;
164
+ if (res.statusCode !== 200) {
165
+ logger.error({ geoLocation, proxySessionId, res }, 'Proxy geo location or session id failed');
166
+ throw new AttestorError('ERROR_PROXY_ERROR', `Proxy via ${geoLocation ? `geo location "${geoLocation}"` : ''}${geoLocation && proxySessionId ? ', or ' : ''}${proxySessionId ? `session id "${proxySessionId}"` : ''} failed with status code: ${res.statusCode}, message: ${res.statusText}`, {
167
+ code: res.statusCode,
168
+ message: res.statusText,
169
+ });
170
+ }
171
+ process.nextTick(() => {
172
+ // ensure connect event is emitted
173
+ // so it can be captured by the caller
174
+ proxySocket.emit('connect');
175
+ });
176
+ return proxySocket;
177
+ }
@@ -0,0 +1,36 @@
1
+ import ElasticAPM from 'elastic-apm-node';
2
+ import { getEnvVariable } from "../../utils/env.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ let apm;
5
+ /**
6
+ * Initialises the APM agent if required,
7
+ * and returns it.
8
+ * If ELASTIC_APM_SERVER_URL & ELASTIC_APM_SECRET_TOKEN
9
+ * are not set will return undefined
10
+ *
11
+ * Utilises the standard env variables mentioned
12
+ * here: https://www.elastic.co/guide/en/apm/agent/nodejs/current/custom-stack.html#custom-stack-advanced-configuration
13
+ */
14
+ export function getApm() {
15
+ if (!getEnvVariable('ELASTIC_APM_SERVER_URL')
16
+ || !getEnvVariable('ELASTIC_APM_SECRET_TOKEN')) {
17
+ logger.info('ELASTIC_APM_SERVER_URL or ELASTIC_APM_SECRET_TOKEN not found'
18
+ + ' in env, APM agent not initialised');
19
+ return undefined;
20
+ }
21
+ if (!apm) {
22
+ const sampleRate = +(getEnvVariable('ELASTIC_APM_SAMPLE_RATE')
23
+ || '0.1');
24
+ apm = ElasticAPM.start({
25
+ serviceName: 'reclaim_attestor',
26
+ serviceVersion: '4.0.0',
27
+ transactionSampleRate: sampleRate,
28
+ instrumentIncomingHTTPRequests: true,
29
+ usePathAsTransactionName: true,
30
+ instrument: true,
31
+ captureHeaders: true,
32
+ });
33
+ logger.info('initialised APM agent');
34
+ }
35
+ return apm;
36
+ }
@@ -0,0 +1,325 @@
1
+ import { areUint8ArraysEqual, concatenateUint8Arrays } from '@reclaimprotocol/tls';
2
+ import { ClaimTunnelRequest, TranscriptMessageSenderType } from "../../proto/api.js";
3
+ import { providers } from "../../providers/index.js";
4
+ import { niceParseJsonObject } from "./generics.js";
5
+ import { computeOPRFRaw } from "./oprf-raw.js";
6
+ import { processHandshake } from "./process-handshake.js";
7
+ import { assertValidateProviderParams } from "./validation.js";
8
+ import { AttestorError, binaryHashToStr, canonicalStringify, decryptDirect, extractApplicationDataFromTranscript, hashProviderParams, SIGNATURES, verifyZkPacket } from "../../utils/index.js";
9
+ import { getEngineString } from "../../utils/zk.js";
10
+ /**
11
+ * Asserts that the claim request is valid.
12
+ *
13
+ * 1. We begin by verifying the signature of the claim request.
14
+ * 2. Next, we produce the transcript of the TLS exchange
15
+ * from the proofs provided by the client.
16
+ * 3. We then pull the provider the client is trying to claim
17
+ * from
18
+ * 4. We then use the provider's verification function to verify
19
+ * whether the claim is valid.
20
+ *
21
+ * If any of these steps fail, we throw an error.
22
+ */
23
+ export async function assertValidClaimRequest(request, metadata, logger) {
24
+ const { data, signatures: { requestSignature } = {}, zkEngine, fixedServerIV, fixedClientIV } = request;
25
+ if (!data) {
26
+ throw new AttestorError('ERROR_INVALID_CLAIM', 'No info provided on claim request');
27
+ }
28
+ if (!requestSignature?.length) {
29
+ throw new AttestorError('ERROR_INVALID_CLAIM', 'No signature provided on claim request');
30
+ }
31
+ // verify request signature
32
+ const serialisedReq = ClaimTunnelRequest
33
+ .encode({ ...request, signatures: undefined })
34
+ .finish();
35
+ const { verify: verifySig } = SIGNATURES[metadata.signatureType];
36
+ const verified = await verifySig(serialisedReq, requestSignature, data.owner);
37
+ if (!verified) {
38
+ throw new AttestorError('ERROR_INVALID_CLAIM', 'Invalid signature on claim request');
39
+ }
40
+ const receipt = await decryptTranscript(request.transcript, logger, getEngineString(zkEngine), fixedServerIV, fixedClientIV);
41
+ const reqHost = request.request?.host;
42
+ if (receipt.hostname !== reqHost) {
43
+ throw new Error(`Expected server name ${reqHost}, got ${receipt.hostname}`);
44
+ }
45
+ // get all application data messages
46
+ const applData = extractApplicationDataFromTranscript(receipt);
47
+ const newData = await assertValidProviderTranscript(applData, data, logger, { version: metadata.clientVersion }, receipt.oprfRawReplacements);
48
+ if (newData !== data) {
49
+ logger.info({ newData }, 'updated claim info');
50
+ }
51
+ return newData;
52
+ }
53
+ /**
54
+ * Verify that the transcript contains a valid claim
55
+ * for the provider.
56
+ */
57
+ export async function assertValidProviderTranscript(applData, info, logger, providerCtx, oprfRawReplacements) {
58
+ const providerName = info.provider;
59
+ const provider = providers[providerName];
60
+ if (!provider) {
61
+ throw new AttestorError('ERROR_INVALID_CLAIM', `Unsupported provider: ${providerName}`);
62
+ }
63
+ let params = niceParseJsonObject(info.parameters, 'params');
64
+ const ctx = niceParseJsonObject(info.context, 'context');
65
+ // Apply oprf-raw replacements to parameters (server-side OPRF)
66
+ if (oprfRawReplacements?.length) {
67
+ let strParams = canonicalStringify(params) ?? '{}';
68
+ for (const { originalText, nullifierText } of oprfRawReplacements) {
69
+ strParams = strParams.replaceAll(originalText, nullifierText);
70
+ }
71
+ params = JSON.parse(strParams);
72
+ // Update info.parameters with replaced values
73
+ info.parameters = strParams;
74
+ logger.debug({ replacements: oprfRawReplacements.length }, 'applied oprf-raw parameter replacements');
75
+ }
76
+ assertValidateProviderParams(providerName, params);
77
+ const rslt = await provider.assertValidProviderReceipt({
78
+ receipt: applData,
79
+ params,
80
+ logger,
81
+ ctx: providerCtx
82
+ });
83
+ ctx.providerHash = hashProviderParams(params);
84
+ const extractedParameters = rslt?.extractedParameters || {};
85
+ if (Object.keys(extractedParameters).length) {
86
+ ctx.extractedParameters = extractedParameters;
87
+ }
88
+ info.context = canonicalStringify(ctx) ?? '';
89
+ return info;
90
+ }
91
+ /**
92
+ * Verify that the transcript provided by the client
93
+ * matches the transcript of the tunnel, the server
94
+ * has created.
95
+ */
96
+ export function assertTranscriptsMatch(clientTranscript, tunnelTranscript) {
97
+ const clientSends = concatenateUint8Arrays(clientTranscript
98
+ .filter(m => m.sender === TranscriptMessageSenderType.TRANSCRIPT_MESSAGE_SENDER_TYPE_CLIENT)
99
+ .map(m => m.message));
100
+ const tunnelSends = concatenateUint8Arrays(tunnelTranscript
101
+ .filter(m => m.sender === 'client')
102
+ .map(m => m.message));
103
+ if (!areUint8ArraysEqual(clientSends, tunnelSends)) {
104
+ throw AttestorError.badRequest('Outgoing messages from client do not match the tunnel transcript');
105
+ }
106
+ const clientRecvs = concatenateUint8Arrays(clientTranscript
107
+ .filter(m => m.sender === TranscriptMessageSenderType.TRANSCRIPT_MESSAGE_SENDER_TYPE_SERVER)
108
+ .map(m => m.message));
109
+ const tunnelRecvs = concatenateUint8Arrays(tunnelTranscript
110
+ .filter(m => m.sender === 'server')
111
+ .map(m => m.message))
112
+ // We only need to compare the first N messages
113
+ // that the client claims to have received
114
+ // the rest are not relevant -- so even if they're
115
+ // not present in the tunnel transcript, it's fine
116
+ .slice(0, clientRecvs.length);
117
+ if (!areUint8ArraysEqual(clientRecvs, tunnelRecvs)) {
118
+ throw AttestorError.badRequest('Incoming messages from server do not match the tunnel transcript');
119
+ }
120
+ }
121
+ export async function decryptTranscript(transcript, logger, zkEngine, serverIV, clientIV) {
122
+ const { tlsVersion, cipherSuite, hostname, nextMsgIndex } = await processHandshake(transcript, logger);
123
+ // TLS 1.3 has already one record encrypted at this point
124
+ let clientRecordNumber = tlsVersion === 'TLS1_3' ? -1 : 0;
125
+ let serverRecordNumber = clientRecordNumber;
126
+ transcript = transcript.slice(nextMsgIndex);
127
+ const overshotMap = {};
128
+ const decryptedTranscript = [];
129
+ const oprfRawReplacements = [];
130
+ // Track pending oprf-raw markers that span multiple packets
131
+ // keyed by packet index that will receive the overshot data
132
+ const pendingOprfRaw = {};
133
+ for (const [i, { sender, message, reveal: { zkReveal, directReveal } = {} }] of transcript.entries()) {
134
+ try {
135
+ //start with first message after last handshake message
136
+ await decryptMessage(sender, message, directReveal, zkReveal, i);
137
+ }
138
+ catch (error) {
139
+ const err = new AttestorError('ERROR_INVALID_CLAIM', `error in handling packet at idx ${i}: ${error}`, { packetIdx: i, error });
140
+ if (error.stack) {
141
+ err.stack = error.stack;
142
+ }
143
+ throw err;
144
+ }
145
+ }
146
+ // Fail if any oprf-raw markers remain incomplete
147
+ const remainingPending = Object.keys(pendingOprfRaw);
148
+ if (remainingPending.length) {
149
+ throw new AttestorError('ERROR_INVALID_CLAIM', `oprf-raw cross-block markers incomplete: pending for packets ${remainingPending.join(', ')}`);
150
+ }
151
+ return {
152
+ transcript: decryptedTranscript,
153
+ hostname: hostname,
154
+ tlsVersion: tlsVersion,
155
+ oprfRawReplacements: oprfRawReplacements.length ? oprfRawReplacements : undefined
156
+ };
157
+ async function decryptMessage(sender, message, directReveal, zkReveal, i) {
158
+ const isServer = sender === TranscriptMessageSenderType
159
+ .TRANSCRIPT_MESSAGE_SENDER_TYPE_SERVER;
160
+ const recordHeader = message.slice(0, 5);
161
+ const content = getWithoutHeader(message);
162
+ if (isServer) {
163
+ serverRecordNumber++;
164
+ }
165
+ else {
166
+ clientRecordNumber++;
167
+ }
168
+ let redacted = true;
169
+ let plaintext = undefined;
170
+ let plaintextLength;
171
+ if (directReveal?.key?.length) {
172
+ const result = await decryptDirect(directReveal, cipherSuite, recordHeader, tlsVersion, content);
173
+ plaintext = result.plaintext;
174
+ redacted = false;
175
+ plaintextLength = plaintext.length;
176
+ }
177
+ else if (zkReveal?.proofs?.length) {
178
+ const iv = sender === TranscriptMessageSenderType
179
+ .TRANSCRIPT_MESSAGE_SENDER_TYPE_SERVER
180
+ ? serverIV
181
+ : clientIV;
182
+ const recordNumber = isServer
183
+ ? serverRecordNumber
184
+ : clientRecordNumber;
185
+ const result = await verifyZkPacket({
186
+ ciphertext: content,
187
+ zkReveal,
188
+ iv,
189
+ recordNumber,
190
+ toprfOvershotNullifier: overshotMap[i]?.data,
191
+ getNextPacket(overshot) {
192
+ const nextIdx = transcript
193
+ .findIndex((t, j) => t.sender === sender && j > i);
194
+ if (nextIdx < 0) {
195
+ return;
196
+ }
197
+ overshotMap[nextIdx] = { data: overshot };
198
+ return getWithoutHeader(transcript[nextIdx].message);
199
+ },
200
+ logger,
201
+ cipherSuite,
202
+ zkEngine: zkEngine,
203
+ });
204
+ plaintext = result.redactedPlaintext;
205
+ // Handle pending oprf-raw data from previous packet (cross-block)
206
+ const pendingForThis = pendingOprfRaw[i];
207
+ if (pendingForThis && zkReveal?.overshotOprfRawLength) {
208
+ const overshootLen = zkReveal.overshotOprfRawLength;
209
+ // Collect the overshot plaintext from this packet
210
+ const overshootData = plaintext.slice(0, overshootLen);
211
+ const fullData = concatenateUint8Arrays([
212
+ pendingForThis.partialData,
213
+ overshootData
214
+ ]);
215
+ // Verify accumulated length matches declared length
216
+ const expectedLen = pendingForThis.dataLocation.length;
217
+ if (fullData.length !== expectedLen) {
218
+ throw new AttestorError('ERROR_INVALID_CLAIM', `oprf-raw cross-block length mismatch: got ${fullData.length}, expected ${expectedLen}`);
219
+ }
220
+ // Compute OPRF for the complete data
221
+ const oprfResults = await computeOPRFRaw(fullData, [{ dataLocation: { fromIndex: 0, length: fullData.length } }], logger);
222
+ if (oprfResults.length) {
223
+ const { nullifier } = oprfResults[0];
224
+ const originalText = new TextDecoder().decode(fullData);
225
+ const nullifierStr = binaryHashToStr(nullifier, fullData.length);
226
+ oprfRawReplacements.push({ originalText, nullifierText: nullifierStr });
227
+ // Replace in original packet (handled when that packet was processed)
228
+ // Replace in current packet
229
+ const nullifierBytes = new TextEncoder().encode(nullifierStr);
230
+ const overshootNullifier = nullifierBytes.slice(pendingForThis.partialData.length);
231
+ plaintext.set(overshootNullifier, 0);
232
+ // Also need to update the previous packet's plaintext
233
+ // The previous packet has the first part of the nullifier
234
+ const prevPkt = decryptedTranscript[pendingForThis.originPktIdx];
235
+ if (prevPkt) {
236
+ const firstPartNullifier = nullifierBytes.slice(0, pendingForThis.partialData.length);
237
+ prevPkt.message.set(firstPartNullifier, pendingForThis.dataLocation.fromIndex);
238
+ }
239
+ }
240
+ delete pendingOprfRaw[i];
241
+ }
242
+ // Process oprf-raw markers: compute OPRF server-side and replace with nullifier
243
+ if (result.oprfRawMarkers?.length) {
244
+ const { markersThisPacket, pendingMarker } = separateOprfRawMarkers(result.oprfRawMarkers, plaintext.length, () => transcript.findIndex((t, j) => t.sender === sender && j > i), decryptedTranscript.length, logger);
245
+ // Store pending marker for cross-block processing
246
+ if (pendingMarker) {
247
+ // Copy partial data from plaintext
248
+ pendingMarker.pending.partialData.set(plaintext.slice(pendingMarker.pending.dataLocation.fromIndex));
249
+ pendingOprfRaw[pendingMarker.nextIdx] = pendingMarker.pending;
250
+ }
251
+ // Process markers that fit in this packet
252
+ if (markersThisPacket.length) {
253
+ const pt = plaintext;
254
+ const oprfResults = await computeOPRFRaw(pt, markersThisPacket, logger);
255
+ // Capture all original texts BEFORE any replacements
256
+ // to avoid reading corrupted data when markers are adjacent
257
+ const originalTexts = oprfResults.map(({ dataLocation }) => new TextDecoder().decode(pt.slice(dataLocation.fromIndex, dataLocation.fromIndex + dataLocation.length)));
258
+ // Now replace plaintext at marker positions with nullifier string
259
+ for (const [idx, { dataLocation, nullifier }] of oprfResults.entries()) {
260
+ const originalText = originalTexts[idx];
261
+ const nullifierStr = binaryHashToStr(nullifier, dataLocation.length);
262
+ oprfRawReplacements.push({ originalText, nullifierText: nullifierStr });
263
+ const nullifierBytes = new TextEncoder().encode(nullifierStr);
264
+ pt.set(nullifierBytes, dataLocation.fromIndex);
265
+ }
266
+ }
267
+ }
268
+ redacted = false;
269
+ plaintextLength = plaintext.length;
270
+ }
271
+ else {
272
+ plaintext = content;
273
+ plaintextLength = plaintext.length;
274
+ }
275
+ decryptedTranscript.push({
276
+ sender: sender === TranscriptMessageSenderType
277
+ .TRANSCRIPT_MESSAGE_SENDER_TYPE_CLIENT
278
+ ? 'client'
279
+ : 'server',
280
+ redacted,
281
+ message: plaintext,
282
+ recordHeader,
283
+ plaintextLength,
284
+ });
285
+ }
286
+ }
287
+ export function getWithoutHeader(message) {
288
+ // strip the record header (xx 03 03 xx xx)
289
+ return message.slice(5);
290
+ }
291
+ /**
292
+ * Separate oprf-raw markers into those that fit in current packet
293
+ * vs those that span to the next packet
294
+ */
295
+ function separateOprfRawMarkers(markers, plaintextLength, findNextPacketIdx, currentTranscriptLength, logger) {
296
+ const markersThisPacket = [];
297
+ let pendingMarker;
298
+ for (const marker of markers) {
299
+ const dataLocation = marker.dataLocation;
300
+ if (!dataLocation) {
301
+ continue;
302
+ }
303
+ const { fromIndex, length } = dataLocation;
304
+ const endInPacket = fromIndex + length;
305
+ if (endInPacket <= plaintextLength) {
306
+ markersThisPacket.push({ dataLocation });
307
+ continue;
308
+ }
309
+ // Spans to next packet
310
+ const nextIdx = findNextPacketIdx();
311
+ if (nextIdx < 0) {
312
+ throw new AttestorError('ERROR_INVALID_CLAIM', 'oprf-raw marker spans packets but no next packet found');
313
+ }
314
+ pendingMarker = {
315
+ nextIdx,
316
+ pending: {
317
+ partialData: new Uint8Array(plaintextLength - fromIndex),
318
+ dataLocation: { fromIndex, length },
319
+ originPktIdx: currentTranscriptLength
320
+ }
321
+ };
322
+ logger.debug({ fromIndex, length, partialLen: plaintextLength - fromIndex, nextIdx }, 'oprf-raw marker spans packets, storing partial data');
323
+ }
324
+ return { markersThisPacket, pendingMarker };
325
+ }
@@ -0,0 +1,4 @@
1
+ import { config } from 'dotenv';
2
+ import { getEnvVariable } from "../../utils/env.js";
3
+ const nodeEnv = getEnvVariable('NODE_ENV') || 'development';
4
+ config({ path: `.env.${nodeEnv}` });
@@ -0,0 +1,18 @@
1
+ import { resolve, setServers } from 'dns';
2
+ import { DNS_SERVERS } from "../../config/index.js";
3
+ setDnsServers();
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
+ });
14
+ });
15
+ }
16
+ function setDnsServers() {
17
+ setServers(DNS_SERVERS);
18
+ }