@ethernity-cloud/sdk-js 1.5.5 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/info.js +306 -0
- package/nodenithy/build/securelock/src/ecld_state.js +251 -18
- package/nodenithy/build/securelock/src/etny_crypto.js +211 -211
- package/nodenithy/build/securelock/src/etny_exec.js +13 -2
- package/nodenithy/build/securelock/src/models.js +275 -275
- package/nodenithy/build/securelock/src/securelock.js.tmpl +632 -586
- package/nodenithy/build/securelock/src/task_status.js +4 -0
- package/nodenithy/build.mjs +30 -0
- package/nodenithy/run/__pycache__/swift_stream_service.cpython-312.pyc +0 -0
- package/nodenithy/run/image_registry.js +371 -371
- package/nodenithy/src/preStart.js +44 -44
- package/package.json +7 -4
- package/pynithy/build/docker-compose.yml +67 -67
- package/pynithy/run/certs/cert.pem +31 -31
- package/pynithy/run/certs/challenge.crt +10 -10
- package/pynithy/run/certs/key.pem +52 -52
- package/pynithy/run/docker-compose-final.yml +59 -59
- package/pynithy/run/docker-compose-swift-stream.yml +27 -27
- package/pynithy/run/docker-compose.yml +55 -55
- package/pynithy/run/image_registry.js +371 -371
|
@@ -1,586 +1,632 @@
|
|
|
1
|
-
// Force stdout/stderr to be synchronous (blocking). Under SCONE 6 + Node, when
|
|
2
|
-
// the process exits quickly (e.g. the PUBLIC_KEY cert-harvest run, which prints
|
|
3
|
-
// the cert then returns) Node's pipe-buffered stdout can be torn down before it
|
|
4
|
-
// flushes, so the harvested "PUBLIC_KEY:" line is lost and the CI cannot read
|
|
5
|
-
// the certificate. Setting the fd handles to blocking makes every console.log
|
|
6
|
-
// write synchronously.
|
|
7
|
-
try { if (process.stdout._handle && process.stdout._handle.setBlocking) process.stdout._handle.setBlocking(true); } catch (e) {}
|
|
8
|
-
try { if (process.stderr._handle && process.stderr._handle.setBlocking) process.stderr._handle.setBlocking(true); } catch (e) {}
|
|
9
|
-
|
|
10
|
-
require('dotenv').config();
|
|
11
|
-
require('dotenv').config({path: './app/.env'});
|
|
12
|
-
require('dotenv').config({path: '/app/.env'});
|
|
13
|
-
|
|
14
|
-
const ethers = require('ethers');
|
|
15
|
-
const fs = require("fs");
|
|
16
|
-
const etny_crypto = require("./etny_crypto");
|
|
17
|
-
const {DOReqMetadata, Order} = require("./models");
|
|
18
|
-
const {getWalletAddress} = require("./wallet");
|
|
19
|
-
const {executeTask} = require("./etny_exec");
|
|
20
|
-
const {TaskStatus} = require("./task_status");
|
|
21
|
-
const SwiftStreamService = require("./swift_stream_service");
|
|
22
|
-
|
|
23
|
-
const contract_abi = require('./pox_abi');
|
|
24
|
-
const image_registry_abi = require('./image_registry_abi');
|
|
25
|
-
const {sha256} = require("./etny_crypto");
|
|
26
|
-
const {generateCertFromMrEnclave} = require("./self_sign");
|
|
27
|
-
const esr_wallet = require("./esr_wallet");
|
|
28
|
-
|
|
29
|
-
// Baked per network by CI (render_network.sh): "mainnet" | "testnet". On testnet
|
|
30
|
-
// the enclave self-signs its cert from MR_ENCLAVE (non-CAS mode); on mainnet the
|
|
31
|
-
// cert/key are provisioned by CAS into the injection paths.
|
|
32
|
-
const NETWORK_TYPE = "__ETNY_NETWORK_TYPE__";
|
|
33
|
-
|
|
34
|
-
class EtnySecureLock {
|
|
35
|
-
static debug = false;
|
|
36
|
-
|
|
37
|
-
constructor(swiftStreamClient) {
|
|
38
|
-
console.log('Initializing...');
|
|
39
|
-
this.version = 'v3';
|
|
40
|
-
this.swiftStreamService = swiftStreamClient;
|
|
41
|
-
this.etny_bucket = "__ETNY_BUCKET__";
|
|
42
|
-
this.setInitializers();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
setInitializers() {
|
|
46
|
-
this.is_valid_client_data = true;
|
|
47
|
-
// Testnet holds the self-derived identity key HERE, in memory only, and
|
|
48
|
-
// never writes it. Stays null on mainnet, where CAS injects the key into
|
|
49
|
-
// key_file and every consumer reads that path exactly as before.
|
|
50
|
-
this.identity_private_key_pem = null;
|
|
51
|
-
this.key_file = "/private/__ENCLAVE_NAME_SECURELOCK__/key.pem";
|
|
52
|
-
this.cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/cert.pem";
|
|
53
|
-
this.pub_cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/enclave_pub_cert.pem";
|
|
54
|
-
this.payload = "payload.etny.securelock";
|
|
55
|
-
this.input = "input.txt.securelock";
|
|
56
|
-
this.result_file = "/app/result.txt";
|
|
57
|
-
this.transaction_file = "/app/transaction.txt";
|
|
58
|
-
this.smart_contract_address = "__ETNY_SMART_CONTRACT_ADDRESS__";
|
|
59
|
-
this.image_registry_address = '__ETNY_IMAGE_REGISTRY_ADDRESS__';
|
|
60
|
-
this.chain_id = __ETNY_CHAIN_ID__;
|
|
61
|
-
this.web3_provider = "__ETNY_WEB3_PROVIDER__";
|
|
62
|
-
this.signed_tx_as_bytes = null;
|
|
63
|
-
this.client_challenge = "";
|
|
64
|
-
|
|
65
|
-
if (this.etny_mode === "TEST") {
|
|
66
|
-
this.key_file = "/private/key.pem";
|
|
67
|
-
this.cert_file = "/app/cert.pem";
|
|
68
|
-
this.payload = "./app/payload.js";
|
|
69
|
-
this.input = "./app/input.txt";
|
|
70
|
-
this.result_file = "./app/result.txt";
|
|
71
|
-
this.transaction_file = "./app/transaction.txt";
|
|
72
|
-
this.pub_cert_file = "./app/enclave_pub_cert.pem";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (this.etny_mode === "LOCAL_TEST") {
|
|
76
|
-
this.key_file = "./certs/key.pem";
|
|
77
|
-
this.cert_file = "./certs/cert.pem";
|
|
78
|
-
this.payload = "./app/payload.js";
|
|
79
|
-
this.input = "./app/input.txt";
|
|
80
|
-
this.result_file = "./app/result.txt";
|
|
81
|
-
this.transaction_file = "./app/transaction.txt";
|
|
82
|
-
this.pub_cert_file = "./app/enclave_pub_cert.pem";
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async create() {
|
|
87
|
-
//overriding default values with those read from .env file
|
|
88
|
-
this.loadEnv();
|
|
89
|
-
this.printEnv();
|
|
90
|
-
await this.savePublicCert();
|
|
91
|
-
this.initWeb3();
|
|
92
|
-
if (this.etny_mode === "PRODUCTION") {
|
|
93
|
-
this.resetPrivateCertFile();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
loadEnv() {
|
|
100
|
-
if (process.env.ETNY_CHAIN_ID) {
|
|
101
|
-
this.chain_id = parseInt(process.env.ETNY_CHAIN_ID);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (process.env.ETNY_SMART_CONTRACT_ADDRESS) {
|
|
105
|
-
this.smart_contract_address = process.env.ETNY_SMART_CONTRACT_ADDRESS.trim();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (process.env.ETNY_WEB3_PROVIDER) {
|
|
109
|
-
this.web3_provider = process.env.ETNY_WEB3_PROVIDER.trim();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (process.env.IMAGE_REGISTRY_ADDRESS) {
|
|
113
|
-
this.image_registry_address = process.env.IMAGE_REGISTRY_ADDRESS.trim();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (process.env.ETNY_BUCKET) {
|
|
117
|
-
this.etny_bucket = process.env.ETNY_BUCKET.trim();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (process.env.ETNY_CLIENT_CHALLENGE) {
|
|
121
|
-
this.client_challenge = process.env.ETNY_CLIENT_CHALLENGE.trim();
|
|
122
|
-
}
|
|
123
|
-
if (process.env.ETNY_ORDER_ID) {
|
|
124
|
-
this.order_id = parseInt(process.env.ETNY_ORDER_ID);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
printEnv() {
|
|
129
|
-
console.log('CHAIN ID:', this.chain_id);
|
|
130
|
-
console.log('SMART CONTRACT ADDRESS:', this.smart_contract_address);
|
|
131
|
-
console.log('WEB3 PROVIDER:', this.web3_provider);
|
|
132
|
-
console.log('ENCRYPTED CHALLENGE:', this.client_challenge);
|
|
133
|
-
console.log('ORDER ID:', this.order_id);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
initWeb3() {
|
|
137
|
-
// Create an ethers.js provider connected to the Bloxberg network
|
|
138
|
-
this.w3 = new ethers.providers.JsonRpcProvider(this.web3_provider);
|
|
139
|
-
// Create an instance of the contract using the ABI and address
|
|
140
|
-
this.etny = new ethers.Contract(this.smart_contract_address, contract_abi, this.w3);
|
|
141
|
-
this.image_registry = new ethers.Contract(this.image_registry_address, image_registry_abi, this.w3);
|
|
142
|
-
// this.iamge_registry_signer = new ethers.Contract(this.image_registry_address, image_registry_abi.abi, this.walletSigner);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async readEnv() {
|
|
146
|
-
try {
|
|
147
|
-
console.log('Getting .env if exists...');
|
|
148
|
-
const envFile = await this.swiftStreamService.getFileContent(this.etny_bucket, ".env");
|
|
149
|
-
console.log("Env:", envFile);
|
|
150
|
-
console.log("Saving .env file...");
|
|
151
|
-
await fs.promises.writeFile('.env', envFile);
|
|
152
|
-
require('dotenv').config();
|
|
153
|
-
|
|
154
|
-
this.version = 'v3';
|
|
155
|
-
this.etny_mode = process.env.ETNY_MODE;
|
|
156
|
-
console.log(`ETNY Nodenithy [${this.version}] - running in ${this.etny_mode} MODE`);
|
|
157
|
-
//console.log(process.env);
|
|
158
|
-
|
|
159
|
-
return true;
|
|
160
|
-
} catch (e) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// v4 checksum validation: the DO-request is submitted on-chain by the owner,
|
|
166
|
-
// so the metadata checksum is already authenticated by the transaction sender;
|
|
167
|
-
// we only confirm the file content hashes to that recorded checksum. Empty
|
|
168
|
-
// input carries no checksum (has_checksum === false) and is skipped. Kept
|
|
169
|
-
// identical across securelock / trustedzone / validator so all three agree.
|
|
170
|
-
async validateClientPayload(payload_data, input_data) {
|
|
171
|
-
await this.getDORequestMetadata();
|
|
172
|
-
|
|
173
|
-
const pmeta = this._metadata.payload_metadata_obj;
|
|
174
|
-
if (pmeta.has_checksum) {
|
|
175
|
-
const expected = pmeta.checksum.toLowerCase().replace(/^0x/, '');
|
|
176
|
-
if (sha256(payload_data) !== expected) {
|
|
177
|
-
this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
|
|
178
|
-
this.task_result = 'PAYLOAD CHECKSUM DOESN\'T MATCH';
|
|
179
|
-
this.is_valid_client_data = false;
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
} else {
|
|
183
|
-
// payload is required and must carry a checksum
|
|
184
|
-
this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
|
|
185
|
-
this.task_result = 'PAYLOAD CHECKSUM DOESN\'T EXIST';
|
|
186
|
-
this.is_valid_client_data = false;
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const imeta = this._metadata.input_metadata_obj;
|
|
191
|
-
if (imeta.has_checksum) {
|
|
192
|
-
const expected = imeta.checksum.toLowerCase().replace(/^0x/, '');
|
|
193
|
-
if (sha256(input_data) !== expected) {
|
|
194
|
-
this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
|
|
195
|
-
this.task_result = 'INPUT CHECKSUM DOESN\'T MATCH';
|
|
196
|
-
this.is_valid_client_data = false;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
wait(timeout) {
|
|
202
|
-
return new Promise(resolve => {
|
|
203
|
-
setTimeout(resolve, timeout);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async waitForObjectInBucket(bucketName, objectName, timeout = 600) {
|
|
208
|
-
let i = 0;
|
|
209
|
-
console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
|
|
210
|
-
while (true) {
|
|
211
|
-
await this.wait(1000);
|
|
212
|
-
i++;
|
|
213
|
-
if (i % 20 === 0)
|
|
214
|
-
console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
|
|
215
|
-
if (i > timeout) {
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
const [status, result] = await this.swiftStreamService.isObjectInBucket(bucketName, objectName);
|
|
219
|
-
if (result) {
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
console.log('TrustedZone finished the execution');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async waitForPayloadAndInput() {
|
|
228
|
-
console.log('Waiting for payload and input');
|
|
229
|
-
await this.waitForObjectInBucket(this.etny_bucket, this.payload);
|
|
230
|
-
await this.waitForObjectInBucket(this.etny_bucket, this.input);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async getFileContentAndDecrypt(bucketName, objectName) {
|
|
234
|
-
try {
|
|
235
|
-
const encryptedBase64 = await this.swiftStreamService.getFileContent(bucketName, objectName);
|
|
236
|
-
|
|
237
|
-
const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
|
|
238
|
-
const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
|
|
239
|
-
return decryptedResult.toString();
|
|
240
|
-
} catch (e) {
|
|
241
|
-
console.log(`Failed to get ${objectName} file`);
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async getTrustedZonePublicKey() {
|
|
247
|
-
try {
|
|
248
|
-
console.log('Getting the public key of the trusted zone enclave');
|
|
249
|
-
let imageHash = this._metadata.image_hash;
|
|
250
|
-
console.log('imageHash:', imageHash);
|
|
251
|
-
if (EtnySecureLock.debug) {
|
|
252
|
-
imageHash = 'v3-hash-1';
|
|
253
|
-
}
|
|
254
|
-
this.trustedZonePublicKey = await this.image_registry.getTrustedZoneImageCertPublicKey(imageHash);
|
|
255
|
-
console.log('Public key of the TrustedZone enclave from smart contract:', this.trustedZonePublicKey);
|
|
256
|
-
} catch (e) {
|
|
257
|
-
console.log('getTrustedZonePublicKey', e);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async encryptFileAndPushToSwiftStream(fileData, fileName) {
|
|
262
|
-
try {
|
|
263
|
-
console.log(`Started saving file ${fileName}.securelock...`);
|
|
264
|
-
const encryptedInput = etny_crypto.encrypt(this.trustedZonePublicKey, fileData);
|
|
265
|
-
const encryptedInputBase64 = etny_crypto.encryptedDataToBase64Json(encryptedInput);
|
|
266
|
-
|
|
267
|
-
// File name should end with the '.securelock' extension
|
|
268
|
-
fileName += '.securelock';
|
|
269
|
-
// const data = Buffer.from(encryptedInputBase64, 'utf8');
|
|
270
|
-
const status = this.swiftStreamService.putFileContent(this.etny_bucket, fileName, '', encryptedInputBase64);
|
|
271
|
-
if (status) {
|
|
272
|
-
console.log(`File ${fileName} encrypted and saved to swift stream successfully`);
|
|
273
|
-
}
|
|
274
|
-
} catch (e) {
|
|
275
|
-
console.log('encryptFileAndPushToSwiftStream', e);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async getDORequestMetadata() {
|
|
280
|
-
const o_result = await this.etny.functions._getOrder(this.order_id);
|
|
281
|
-
const order = new Order(o_result, this.order_id);
|
|
282
|
-
//console.log('order', order);
|
|
283
|
-
this._order_metadata = order;
|
|
284
|
-
const o_metadata = await this.etny.functions._getDORequestMetadata(order.do_req);
|
|
285
|
-
this._metadata = new DOReqMetadata(o_metadata, order.do_req);
|
|
286
|
-
//console.log('metadata', this._metadata);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
resetPrivateCertFile() {
|
|
290
|
-
// Wipes the CAS-injected key from disk after it has been loaded
|
|
291
|
-
// (mainnet PRODUCTION hygiene). On testnet the key was never written --
|
|
292
|
-
// it lives only in memory -- so there is nothing on disk to wipe and the
|
|
293
|
-
// truncate/read below would throw on a file that does not exist.
|
|
294
|
-
//
|
|
295
|
-
// The in-memory key is deliberately NOT cleared here: waitForPayloadAndInput
|
|
296
|
-
// still needs it later to decrypt the payload/input, which runs after
|
|
297
|
-
// create() has called this. Clearing it would break the task on testnet only.
|
|
298
|
-
if (this.identity_private_key_pem) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
try {
|
|
302
|
-
fs.truncate(this.key_file, 0, function () {
|
|
303
|
-
//console.log('Private certificate file was cleaned.')
|
|
304
|
-
});
|
|
305
|
-
} catch (e) {
|
|
306
|
-
//console.log('Error while removing the private key file content', e);
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
fs.readFileSync(this.key_file, 'utf8');
|
|
310
|
-
} catch (e) {
|
|
311
|
-
// Nothing to read back; the wipe above is what mattered.
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Testnet non-CAS mode: derive a deterministic self-signed cert/key from this
|
|
316
|
-
// enclave's identity. The CERTIFICATE is written (it is public, and
|
|
317
|
-
// savePublicCert reads it back to publish it); the PRIVATE KEY is kept ONLY
|
|
318
|
-
// in memory and never touches the filesystem.
|
|
319
|
-
//
|
|
320
|
-
// Why memory-only: on testnet there is no CAS and no attestation, so nothing
|
|
321
|
-
// provisions or protects the key from outside the enclave. Not writing it at
|
|
322
|
-
// all removes every at-rest attack on it outright, instead of relying on the
|
|
323
|
-
// file protection being keyed by something trustworthy on this path.
|
|
324
|
-
//
|
|
325
|
-
// MAINNET IS UNCHANGED: this method returns immediately there, CAS still
|
|
326
|
-
// injects cert+key into key_file/cert_file, and every consumer still reads
|
|
327
|
-
// key_file exactly as before (this.identity_private_key_pem stays null, and
|
|
328
|
-
// privateKeyMaterial() falls back to the path).
|
|
329
|
-
//
|
|
330
|
-
// Idempotent: the key is a pure function of the enclave identity, so it is
|
|
331
|
-
// identical across restarts without needing to persist it.
|
|
332
|
-
generateSelfSignedCertIfTestnet() {
|
|
333
|
-
if (NETWORK_TYPE !== "testnet") return;
|
|
334
|
-
console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
|
|
335
|
-
const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
|
|
336
|
-
console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
|
|
337
|
-
for (const f of [this.cert_file, this.pub_cert_file]) {
|
|
338
|
-
try { fs.mkdirSync(require('path').dirname(f), {recursive: true}); } catch (e) {}
|
|
339
|
-
}
|
|
340
|
-
// Private key: memory only. Deliberately NOT written to this.key_file.
|
|
341
|
-
this.identity_private_key_pem = privateKeyPem;
|
|
342
|
-
fs.writeFileSync(this.cert_file, certPem);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// The enclave's identity private key, in whatever form this network provides
|
|
346
|
-
// it: the in-memory PEM on testnet, or the CAS-injected key_file path on
|
|
347
|
-
// mainnet. etny_crypto accepts either, so callers do not branch on network.
|
|
348
|
-
privateKeyMaterial() {
|
|
349
|
-
return this.identity_private_key_pem || this.key_file;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async savePublicCert() {
|
|
353
|
-
try {
|
|
354
|
-
const content = await fs.promises.readFile(this.cert_file, 'utf8');
|
|
355
|
-
await fs.promises.writeFile(this.pub_cert_file, content);
|
|
356
|
-
//console.log(`Public certificate saved to ${this.pub_cert_file}`);
|
|
357
|
-
console.log(`PUBLIC_KEY:`);
|
|
358
|
-
console.log(content);
|
|
359
|
-
|
|
360
|
-
// ESR (RFC §5.2): emit the enclave's ESR wallet ADDRESS on the same
|
|
361
|
-
// harvest channel as the cert, so publish (or the remote extraction
|
|
362
|
-
// service) can capture it. It MUST be derived here rather than by the
|
|
363
|
-
// service: on mainnet the identity key is CAS-provisioned and exists
|
|
364
|
-
// only inside this enclave, so nobody outside can compute the address.
|
|
365
|
-
// Only the address is emitted — the private key never leaves.
|
|
366
|
-
this.emitEsrWalletAddress();
|
|
367
|
-
|
|
368
|
-
await this.swiftStreamService.putFileContent(this.etny_bucket, 'cert.pem', this.pub_cert_file);
|
|
369
|
-
} catch (err) {
|
|
370
|
-
console.error(err);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ESR wallet address emission (RFC §5.2). Gated on ESR_CONTRACT_ADDRESS being
|
|
375
|
-
// set, exactly like the Python securelock, so non-ESR images print nothing.
|
|
376
|
-
// Derived from the enclave's own identity private key (the certificate key);
|
|
377
|
-
// only the ADDRESS is logged, never the key. Any failure is reported as
|
|
378
|
-
// ESR_WALLET_ERROR and never breaks cert harvest or a task.
|
|
379
|
-
emitEsrWalletAddress() {
|
|
380
|
-
if (!String(process.env.ESR_CONTRACT_ADDRESS || "").trim()) return;
|
|
381
|
-
try {
|
|
382
|
-
// The identity key the enclave actually holds: the CAS-provisioned
|
|
383
|
-
// key_file on mainnet (nothing in-enclave can re-derive it), or the
|
|
384
|
-
// in-memory PEM on testnet (never written to disk). Hashed as-is,
|
|
385
|
-
// matching the Python SDK.
|
|
386
|
-
const identityPriv = this.identity_private_key_pem
|
|
387
|
-
|| fs.readFileSync(this.key_file, 'utf8');
|
|
388
|
-
const esrAddress = esr_wallet.deriveWalletAddress(identityPriv);
|
|
389
|
-
console.log(`ESR_WALLET_ADDRESS: ${esrAddress}`);
|
|
390
|
-
if (!esr_wallet.isSecretIdentity(NETWORK_TYPE)) {
|
|
391
|
-
console.warn(esr_wallet.INSECURE_IDENTITY_WARNING);
|
|
392
|
-
}
|
|
393
|
-
} catch (e) {
|
|
394
|
-
// Never let ESR derivation break harvest/a task: report it the way
|
|
395
|
-
// every other enclave-side config problem is reported.
|
|
396
|
-
console.error(`ESR_WALLET_ERROR: ${e && e.message ? e.message : e}`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
async saveResult() {
|
|
401
|
-
try {
|
|
402
|
-
await this.getTrustedZonePublicKey();
|
|
403
|
-
await this.encryptFileAndPushToSwiftStream(this.task_result, 'result.txt');
|
|
404
|
-
await this.encryptFileAndPushToSwiftStream(this.task_code.toString(), 'result_code.txt');
|
|
405
|
-
|
|
406
|
-
const encrypted = etny_crypto.encryptWithPublicKey(this._metadata.public_key, this.task_result);
|
|
407
|
-
//console.log('Encrypted result:', encrypted.toString('hex'));
|
|
408
|
-
const result = await this.swiftStreamService.putFileContent(this.etny_bucket, 'result.txt', this.result_file, encrypted.toString('hex'));
|
|
409
|
-
//console.log('Save result put: ', result);
|
|
410
|
-
} catch (e) {
|
|
411
|
-
console.log('Save result: ', e);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Make StateRegistry usable from payload code, if ESR is enabled.
|
|
416
|
-
//
|
|
417
|
-
// Wires ecld_state to this enclave's identity key, the SwiftStream client
|
|
418
|
-
// and bucket the node shares with us, the registry address baked in at build
|
|
419
|
-
// time, and the provider. Payload code then just does:
|
|
420
|
-
//
|
|
421
|
-
// const {StateRegistry} = require('./ecld_state');
|
|
422
|
-
// await new StateRegistry().commit('key', s => ...);
|
|
423
|
-
//
|
|
424
|
-
// No-op when ESR is disabled, so non-ESR enclaves are unaffected. Never
|
|
425
|
-
// throws: a setup problem must not stop the task from running -- payload
|
|
426
|
-
// code that then uses StateRegistry gets a clear error from the registry
|
|
427
|
-
// itself rather than a task that died before starting.
|
|
428
|
-
configureStateRegistry() {
|
|
429
|
-
const contractAddress = String(process.env.ESR_CONTRACT_ADDRESS || "").trim();
|
|
430
|
-
if (!contractAddress) return;
|
|
431
|
-
try {
|
|
432
|
-
const ecld_state = require("./ecld_state");
|
|
433
|
-
ecld_state.configure({
|
|
434
|
-
identityPriv: this.identity_private_key_pem || fs.readFileSync(this.key_file, 'utf8'),
|
|
435
|
-
swiftStreamService: this.swiftStreamService,
|
|
436
|
-
bucket: this.etny_bucket,
|
|
437
|
-
contractAddress: contractAddress,
|
|
438
|
-
provider: this.w3,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
console.log('');
|
|
525
|
-
console.log('');
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
console.log(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1
|
+
// Force stdout/stderr to be synchronous (blocking). Under SCONE 6 + Node, when
|
|
2
|
+
// the process exits quickly (e.g. the PUBLIC_KEY cert-harvest run, which prints
|
|
3
|
+
// the cert then returns) Node's pipe-buffered stdout can be torn down before it
|
|
4
|
+
// flushes, so the harvested "PUBLIC_KEY:" line is lost and the CI cannot read
|
|
5
|
+
// the certificate. Setting the fd handles to blocking makes every console.log
|
|
6
|
+
// write synchronously.
|
|
7
|
+
try { if (process.stdout._handle && process.stdout._handle.setBlocking) process.stdout._handle.setBlocking(true); } catch (e) {}
|
|
8
|
+
try { if (process.stderr._handle && process.stderr._handle.setBlocking) process.stderr._handle.setBlocking(true); } catch (e) {}
|
|
9
|
+
|
|
10
|
+
require('dotenv').config();
|
|
11
|
+
require('dotenv').config({path: './app/.env'});
|
|
12
|
+
require('dotenv').config({path: '/app/.env'});
|
|
13
|
+
|
|
14
|
+
const ethers = require('ethers');
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const etny_crypto = require("./etny_crypto");
|
|
17
|
+
const {DOReqMetadata, Order} = require("./models");
|
|
18
|
+
const {getWalletAddress} = require("./wallet");
|
|
19
|
+
const {executeTask} = require("./etny_exec");
|
|
20
|
+
const {TaskStatus} = require("./task_status");
|
|
21
|
+
const SwiftStreamService = require("./swift_stream_service");
|
|
22
|
+
|
|
23
|
+
const contract_abi = require('./pox_abi');
|
|
24
|
+
const image_registry_abi = require('./image_registry_abi');
|
|
25
|
+
const {sha256} = require("./etny_crypto");
|
|
26
|
+
const {generateCertFromMrEnclave} = require("./self_sign");
|
|
27
|
+
const esr_wallet = require("./esr_wallet");
|
|
28
|
+
|
|
29
|
+
// Baked per network by CI (render_network.sh): "mainnet" | "testnet". On testnet
|
|
30
|
+
// the enclave self-signs its cert from MR_ENCLAVE (non-CAS mode); on mainnet the
|
|
31
|
+
// cert/key are provisioned by CAS into the injection paths.
|
|
32
|
+
const NETWORK_TYPE = "__ETNY_NETWORK_TYPE__";
|
|
33
|
+
|
|
34
|
+
class EtnySecureLock {
|
|
35
|
+
static debug = false;
|
|
36
|
+
|
|
37
|
+
constructor(swiftStreamClient) {
|
|
38
|
+
console.log('Initializing...');
|
|
39
|
+
this.version = 'v3';
|
|
40
|
+
this.swiftStreamService = swiftStreamClient;
|
|
41
|
+
this.etny_bucket = "__ETNY_BUCKET__";
|
|
42
|
+
this.setInitializers();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setInitializers() {
|
|
46
|
+
this.is_valid_client_data = true;
|
|
47
|
+
// Testnet holds the self-derived identity key HERE, in memory only, and
|
|
48
|
+
// never writes it. Stays null on mainnet, where CAS injects the key into
|
|
49
|
+
// key_file and every consumer reads that path exactly as before.
|
|
50
|
+
this.identity_private_key_pem = null;
|
|
51
|
+
this.key_file = "/private/__ENCLAVE_NAME_SECURELOCK__/key.pem";
|
|
52
|
+
this.cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/cert.pem";
|
|
53
|
+
this.pub_cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/enclave_pub_cert.pem";
|
|
54
|
+
this.payload = "payload.etny.securelock";
|
|
55
|
+
this.input = "input.txt.securelock";
|
|
56
|
+
this.result_file = "/app/result.txt";
|
|
57
|
+
this.transaction_file = "/app/transaction.txt";
|
|
58
|
+
this.smart_contract_address = "__ETNY_SMART_CONTRACT_ADDRESS__";
|
|
59
|
+
this.image_registry_address = '__ETNY_IMAGE_REGISTRY_ADDRESS__';
|
|
60
|
+
this.chain_id = __ETNY_CHAIN_ID__;
|
|
61
|
+
this.web3_provider = "__ETNY_WEB3_PROVIDER__";
|
|
62
|
+
this.signed_tx_as_bytes = null;
|
|
63
|
+
this.client_challenge = "";
|
|
64
|
+
|
|
65
|
+
if (this.etny_mode === "TEST") {
|
|
66
|
+
this.key_file = "/private/key.pem";
|
|
67
|
+
this.cert_file = "/app/cert.pem";
|
|
68
|
+
this.payload = "./app/payload.js";
|
|
69
|
+
this.input = "./app/input.txt";
|
|
70
|
+
this.result_file = "./app/result.txt";
|
|
71
|
+
this.transaction_file = "./app/transaction.txt";
|
|
72
|
+
this.pub_cert_file = "./app/enclave_pub_cert.pem";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (this.etny_mode === "LOCAL_TEST") {
|
|
76
|
+
this.key_file = "./certs/key.pem";
|
|
77
|
+
this.cert_file = "./certs/cert.pem";
|
|
78
|
+
this.payload = "./app/payload.js";
|
|
79
|
+
this.input = "./app/input.txt";
|
|
80
|
+
this.result_file = "./app/result.txt";
|
|
81
|
+
this.transaction_file = "./app/transaction.txt";
|
|
82
|
+
this.pub_cert_file = "./app/enclave_pub_cert.pem";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async create() {
|
|
87
|
+
//overriding default values with those read from .env file
|
|
88
|
+
this.loadEnv();
|
|
89
|
+
this.printEnv();
|
|
90
|
+
await this.savePublicCert();
|
|
91
|
+
this.initWeb3();
|
|
92
|
+
if (this.etny_mode === "PRODUCTION") {
|
|
93
|
+
this.resetPrivateCertFile();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
loadEnv() {
|
|
100
|
+
if (process.env.ETNY_CHAIN_ID) {
|
|
101
|
+
this.chain_id = parseInt(process.env.ETNY_CHAIN_ID);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (process.env.ETNY_SMART_CONTRACT_ADDRESS) {
|
|
105
|
+
this.smart_contract_address = process.env.ETNY_SMART_CONTRACT_ADDRESS.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (process.env.ETNY_WEB3_PROVIDER) {
|
|
109
|
+
this.web3_provider = process.env.ETNY_WEB3_PROVIDER.trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (process.env.IMAGE_REGISTRY_ADDRESS) {
|
|
113
|
+
this.image_registry_address = process.env.IMAGE_REGISTRY_ADDRESS.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (process.env.ETNY_BUCKET) {
|
|
117
|
+
this.etny_bucket = process.env.ETNY_BUCKET.trim();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (process.env.ETNY_CLIENT_CHALLENGE) {
|
|
121
|
+
this.client_challenge = process.env.ETNY_CLIENT_CHALLENGE.trim();
|
|
122
|
+
}
|
|
123
|
+
if (process.env.ETNY_ORDER_ID) {
|
|
124
|
+
this.order_id = parseInt(process.env.ETNY_ORDER_ID);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
printEnv() {
|
|
129
|
+
console.log('CHAIN ID:', this.chain_id);
|
|
130
|
+
console.log('SMART CONTRACT ADDRESS:', this.smart_contract_address);
|
|
131
|
+
console.log('WEB3 PROVIDER:', this.web3_provider);
|
|
132
|
+
console.log('ENCRYPTED CHALLENGE:', this.client_challenge);
|
|
133
|
+
console.log('ORDER ID:', this.order_id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
initWeb3() {
|
|
137
|
+
// Create an ethers.js provider connected to the Bloxberg network
|
|
138
|
+
this.w3 = new ethers.providers.JsonRpcProvider(this.web3_provider);
|
|
139
|
+
// Create an instance of the contract using the ABI and address
|
|
140
|
+
this.etny = new ethers.Contract(this.smart_contract_address, contract_abi, this.w3);
|
|
141
|
+
this.image_registry = new ethers.Contract(this.image_registry_address, image_registry_abi, this.w3);
|
|
142
|
+
// this.iamge_registry_signer = new ethers.Contract(this.image_registry_address, image_registry_abi.abi, this.walletSigner);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async readEnv() {
|
|
146
|
+
try {
|
|
147
|
+
console.log('Getting .env if exists...');
|
|
148
|
+
const envFile = await this.swiftStreamService.getFileContent(this.etny_bucket, ".env");
|
|
149
|
+
console.log("Env:", envFile);
|
|
150
|
+
console.log("Saving .env file...");
|
|
151
|
+
await fs.promises.writeFile('.env', envFile);
|
|
152
|
+
require('dotenv').config();
|
|
153
|
+
|
|
154
|
+
this.version = 'v3';
|
|
155
|
+
this.etny_mode = process.env.ETNY_MODE;
|
|
156
|
+
console.log(`ETNY Nodenithy [${this.version}] - running in ${this.etny_mode} MODE`);
|
|
157
|
+
//console.log(process.env);
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// v4 checksum validation: the DO-request is submitted on-chain by the owner,
|
|
166
|
+
// so the metadata checksum is already authenticated by the transaction sender;
|
|
167
|
+
// we only confirm the file content hashes to that recorded checksum. Empty
|
|
168
|
+
// input carries no checksum (has_checksum === false) and is skipped. Kept
|
|
169
|
+
// identical across securelock / trustedzone / validator so all three agree.
|
|
170
|
+
async validateClientPayload(payload_data, input_data) {
|
|
171
|
+
await this.getDORequestMetadata();
|
|
172
|
+
|
|
173
|
+
const pmeta = this._metadata.payload_metadata_obj;
|
|
174
|
+
if (pmeta.has_checksum) {
|
|
175
|
+
const expected = pmeta.checksum.toLowerCase().replace(/^0x/, '');
|
|
176
|
+
if (sha256(payload_data) !== expected) {
|
|
177
|
+
this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
|
|
178
|
+
this.task_result = 'PAYLOAD CHECKSUM DOESN\'T MATCH';
|
|
179
|
+
this.is_valid_client_data = false;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// payload is required and must carry a checksum
|
|
184
|
+
this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
|
|
185
|
+
this.task_result = 'PAYLOAD CHECKSUM DOESN\'T EXIST';
|
|
186
|
+
this.is_valid_client_data = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const imeta = this._metadata.input_metadata_obj;
|
|
191
|
+
if (imeta.has_checksum) {
|
|
192
|
+
const expected = imeta.checksum.toLowerCase().replace(/^0x/, '');
|
|
193
|
+
if (sha256(input_data) !== expected) {
|
|
194
|
+
this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
|
|
195
|
+
this.task_result = 'INPUT CHECKSUM DOESN\'T MATCH';
|
|
196
|
+
this.is_valid_client_data = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
wait(timeout) {
|
|
202
|
+
return new Promise(resolve => {
|
|
203
|
+
setTimeout(resolve, timeout);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async waitForObjectInBucket(bucketName, objectName, timeout = 600) {
|
|
208
|
+
let i = 0;
|
|
209
|
+
console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
|
|
210
|
+
while (true) {
|
|
211
|
+
await this.wait(1000);
|
|
212
|
+
i++;
|
|
213
|
+
if (i % 20 === 0)
|
|
214
|
+
console.log(`Checking if object ${objectName} exists in bucket ${bucketName}`);
|
|
215
|
+
if (i > timeout) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
const [status, result] = await this.swiftStreamService.isObjectInBucket(bucketName, objectName);
|
|
219
|
+
if (result) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('TrustedZone finished the execution');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async waitForPayloadAndInput() {
|
|
228
|
+
console.log('Waiting for payload and input');
|
|
229
|
+
await this.waitForObjectInBucket(this.etny_bucket, this.payload);
|
|
230
|
+
await this.waitForObjectInBucket(this.etny_bucket, this.input);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getFileContentAndDecrypt(bucketName, objectName) {
|
|
234
|
+
try {
|
|
235
|
+
const encryptedBase64 = await this.swiftStreamService.getFileContent(bucketName, objectName);
|
|
236
|
+
|
|
237
|
+
const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
|
|
238
|
+
const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
|
|
239
|
+
return decryptedResult.toString();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.log(`Failed to get ${objectName} file`);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getTrustedZonePublicKey() {
|
|
247
|
+
try {
|
|
248
|
+
console.log('Getting the public key of the trusted zone enclave');
|
|
249
|
+
let imageHash = this._metadata.image_hash;
|
|
250
|
+
console.log('imageHash:', imageHash);
|
|
251
|
+
if (EtnySecureLock.debug) {
|
|
252
|
+
imageHash = 'v3-hash-1';
|
|
253
|
+
}
|
|
254
|
+
this.trustedZonePublicKey = await this.image_registry.getTrustedZoneImageCertPublicKey(imageHash);
|
|
255
|
+
console.log('Public key of the TrustedZone enclave from smart contract:', this.trustedZonePublicKey);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.log('getTrustedZonePublicKey', e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async encryptFileAndPushToSwiftStream(fileData, fileName) {
|
|
262
|
+
try {
|
|
263
|
+
console.log(`Started saving file ${fileName}.securelock...`);
|
|
264
|
+
const encryptedInput = etny_crypto.encrypt(this.trustedZonePublicKey, fileData);
|
|
265
|
+
const encryptedInputBase64 = etny_crypto.encryptedDataToBase64Json(encryptedInput);
|
|
266
|
+
|
|
267
|
+
// File name should end with the '.securelock' extension
|
|
268
|
+
fileName += '.securelock';
|
|
269
|
+
// const data = Buffer.from(encryptedInputBase64, 'utf8');
|
|
270
|
+
const status = this.swiftStreamService.putFileContent(this.etny_bucket, fileName, '', encryptedInputBase64);
|
|
271
|
+
if (status) {
|
|
272
|
+
console.log(`File ${fileName} encrypted and saved to swift stream successfully`);
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.log('encryptFileAndPushToSwiftStream', e);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async getDORequestMetadata() {
|
|
280
|
+
const o_result = await this.etny.functions._getOrder(this.order_id);
|
|
281
|
+
const order = new Order(o_result, this.order_id);
|
|
282
|
+
//console.log('order', order);
|
|
283
|
+
this._order_metadata = order;
|
|
284
|
+
const o_metadata = await this.etny.functions._getDORequestMetadata(order.do_req);
|
|
285
|
+
this._metadata = new DOReqMetadata(o_metadata, order.do_req);
|
|
286
|
+
//console.log('metadata', this._metadata);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
resetPrivateCertFile() {
|
|
290
|
+
// Wipes the CAS-injected key from disk after it has been loaded
|
|
291
|
+
// (mainnet PRODUCTION hygiene). On testnet the key was never written --
|
|
292
|
+
// it lives only in memory -- so there is nothing on disk to wipe and the
|
|
293
|
+
// truncate/read below would throw on a file that does not exist.
|
|
294
|
+
//
|
|
295
|
+
// The in-memory key is deliberately NOT cleared here: waitForPayloadAndInput
|
|
296
|
+
// still needs it later to decrypt the payload/input, which runs after
|
|
297
|
+
// create() has called this. Clearing it would break the task on testnet only.
|
|
298
|
+
if (this.identity_private_key_pem) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
fs.truncate(this.key_file, 0, function () {
|
|
303
|
+
//console.log('Private certificate file was cleaned.')
|
|
304
|
+
});
|
|
305
|
+
} catch (e) {
|
|
306
|
+
//console.log('Error while removing the private key file content', e);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
fs.readFileSync(this.key_file, 'utf8');
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// Nothing to read back; the wipe above is what mattered.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Testnet non-CAS mode: derive a deterministic self-signed cert/key from this
|
|
316
|
+
// enclave's identity. The CERTIFICATE is written (it is public, and
|
|
317
|
+
// savePublicCert reads it back to publish it); the PRIVATE KEY is kept ONLY
|
|
318
|
+
// in memory and never touches the filesystem.
|
|
319
|
+
//
|
|
320
|
+
// Why memory-only: on testnet there is no CAS and no attestation, so nothing
|
|
321
|
+
// provisions or protects the key from outside the enclave. Not writing it at
|
|
322
|
+
// all removes every at-rest attack on it outright, instead of relying on the
|
|
323
|
+
// file protection being keyed by something trustworthy on this path.
|
|
324
|
+
//
|
|
325
|
+
// MAINNET IS UNCHANGED: this method returns immediately there, CAS still
|
|
326
|
+
// injects cert+key into key_file/cert_file, and every consumer still reads
|
|
327
|
+
// key_file exactly as before (this.identity_private_key_pem stays null, and
|
|
328
|
+
// privateKeyMaterial() falls back to the path).
|
|
329
|
+
//
|
|
330
|
+
// Idempotent: the key is a pure function of the enclave identity, so it is
|
|
331
|
+
// identical across restarts without needing to persist it.
|
|
332
|
+
generateSelfSignedCertIfTestnet() {
|
|
333
|
+
if (NETWORK_TYPE !== "testnet") return;
|
|
334
|
+
console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
|
|
335
|
+
const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
|
|
336
|
+
console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
|
|
337
|
+
for (const f of [this.cert_file, this.pub_cert_file]) {
|
|
338
|
+
try { fs.mkdirSync(require('path').dirname(f), {recursive: true}); } catch (e) {}
|
|
339
|
+
}
|
|
340
|
+
// Private key: memory only. Deliberately NOT written to this.key_file.
|
|
341
|
+
this.identity_private_key_pem = privateKeyPem;
|
|
342
|
+
fs.writeFileSync(this.cert_file, certPem);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The enclave's identity private key, in whatever form this network provides
|
|
346
|
+
// it: the in-memory PEM on testnet, or the CAS-injected key_file path on
|
|
347
|
+
// mainnet. etny_crypto accepts either, so callers do not branch on network.
|
|
348
|
+
privateKeyMaterial() {
|
|
349
|
+
return this.identity_private_key_pem || this.key_file;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async savePublicCert() {
|
|
353
|
+
try {
|
|
354
|
+
const content = await fs.promises.readFile(this.cert_file, 'utf8');
|
|
355
|
+
await fs.promises.writeFile(this.pub_cert_file, content);
|
|
356
|
+
//console.log(`Public certificate saved to ${this.pub_cert_file}`);
|
|
357
|
+
console.log(`PUBLIC_KEY:`);
|
|
358
|
+
console.log(content);
|
|
359
|
+
|
|
360
|
+
// ESR (RFC §5.2): emit the enclave's ESR wallet ADDRESS on the same
|
|
361
|
+
// harvest channel as the cert, so publish (or the remote extraction
|
|
362
|
+
// service) can capture it. It MUST be derived here rather than by the
|
|
363
|
+
// service: on mainnet the identity key is CAS-provisioned and exists
|
|
364
|
+
// only inside this enclave, so nobody outside can compute the address.
|
|
365
|
+
// Only the address is emitted — the private key never leaves.
|
|
366
|
+
this.emitEsrWalletAddress();
|
|
367
|
+
|
|
368
|
+
await this.swiftStreamService.putFileContent(this.etny_bucket, 'cert.pem', this.pub_cert_file);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error(err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ESR wallet address emission (RFC §5.2). Gated on ESR_CONTRACT_ADDRESS being
|
|
375
|
+
// set, exactly like the Python securelock, so non-ESR images print nothing.
|
|
376
|
+
// Derived from the enclave's own identity private key (the certificate key);
|
|
377
|
+
// only the ADDRESS is logged, never the key. Any failure is reported as
|
|
378
|
+
// ESR_WALLET_ERROR and never breaks cert harvest or a task.
|
|
379
|
+
emitEsrWalletAddress() {
|
|
380
|
+
if (!String(process.env.ESR_CONTRACT_ADDRESS || "").trim()) return;
|
|
381
|
+
try {
|
|
382
|
+
// The identity key the enclave actually holds: the CAS-provisioned
|
|
383
|
+
// key_file on mainnet (nothing in-enclave can re-derive it), or the
|
|
384
|
+
// in-memory PEM on testnet (never written to disk). Hashed as-is,
|
|
385
|
+
// matching the Python SDK.
|
|
386
|
+
const identityPriv = this.identity_private_key_pem
|
|
387
|
+
|| fs.readFileSync(this.key_file, 'utf8');
|
|
388
|
+
const esrAddress = esr_wallet.deriveWalletAddress(identityPriv);
|
|
389
|
+
console.log(`ESR_WALLET_ADDRESS: ${esrAddress}`);
|
|
390
|
+
if (!esr_wallet.isSecretIdentity(NETWORK_TYPE)) {
|
|
391
|
+
console.warn(esr_wallet.INSECURE_IDENTITY_WARNING);
|
|
392
|
+
}
|
|
393
|
+
} catch (e) {
|
|
394
|
+
// Never let ESR derivation break harvest/a task: report it the way
|
|
395
|
+
// every other enclave-side config problem is reported.
|
|
396
|
+
console.error(`ESR_WALLET_ERROR: ${e && e.message ? e.message : e}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async saveResult() {
|
|
401
|
+
try {
|
|
402
|
+
await this.getTrustedZonePublicKey();
|
|
403
|
+
await this.encryptFileAndPushToSwiftStream(this.task_result, 'result.txt');
|
|
404
|
+
await this.encryptFileAndPushToSwiftStream(this.task_code.toString(), 'result_code.txt');
|
|
405
|
+
|
|
406
|
+
const encrypted = etny_crypto.encryptWithPublicKey(this._metadata.public_key, this.task_result);
|
|
407
|
+
//console.log('Encrypted result:', encrypted.toString('hex'));
|
|
408
|
+
const result = await this.swiftStreamService.putFileContent(this.etny_bucket, 'result.txt', this.result_file, encrypted.toString('hex'));
|
|
409
|
+
//console.log('Save result put: ', result);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.log('Save result: ', e);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Make StateRegistry usable from payload code, if ESR is enabled.
|
|
416
|
+
//
|
|
417
|
+
// Wires ecld_state to this enclave's identity key, the SwiftStream client
|
|
418
|
+
// and bucket the node shares with us, the registry address baked in at build
|
|
419
|
+
// time, and the provider. Payload code then just does:
|
|
420
|
+
//
|
|
421
|
+
// const {StateRegistry} = require('./ecld_state');
|
|
422
|
+
// await new StateRegistry().commit('key', s => ...);
|
|
423
|
+
//
|
|
424
|
+
// No-op when ESR is disabled, so non-ESR enclaves are unaffected. Never
|
|
425
|
+
// throws: a setup problem must not stop the task from running -- payload
|
|
426
|
+
// code that then uses StateRegistry gets a clear error from the registry
|
|
427
|
+
// itself rather than a task that died before starting.
|
|
428
|
+
configureStateRegistry() {
|
|
429
|
+
const contractAddress = String(process.env.ESR_CONTRACT_ADDRESS || "").trim();
|
|
430
|
+
if (!contractAddress) return;
|
|
431
|
+
try {
|
|
432
|
+
const ecld_state = require("./ecld_state");
|
|
433
|
+
ecld_state.configure({
|
|
434
|
+
identityPriv: this.identity_private_key_pem || fs.readFileSync(this.key_file, 'utf8'),
|
|
435
|
+
swiftStreamService: this.swiftStreamService,
|
|
436
|
+
bucket: this.etny_bucket,
|
|
437
|
+
contractAddress: contractAddress,
|
|
438
|
+
provider: this.w3,
|
|
439
|
+
caller: this.task_caller || null,
|
|
440
|
+
});
|
|
441
|
+
console.log(`StateRegistry ready (ESR ${contractAddress})`);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error(`ESR_STATE_ERROR: could not configure StateRegistry: ${e && e.message ? e.message : e}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async execute() {
|
|
448
|
+
const payload_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.payload);
|
|
449
|
+
const input_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.input);
|
|
450
|
+
console.log('Validating client payload and input...');
|
|
451
|
+
await this.validateClientPayload(payload_data, input_data);
|
|
452
|
+
|
|
453
|
+
if (this.is_valid_client_data) {
|
|
454
|
+
console.log('Client payload and input are valid');
|
|
455
|
+
} else {
|
|
456
|
+
console.log('Client payload and input are NOT valid');
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Attested task caller: the DO-request owner, forwarded by the
|
|
461
|
+
// trustedzone as caller.securelock + .sig. Accepted ONLY with a valid
|
|
462
|
+
// trustedzone signature -- this build's crypto module has no verify
|
|
463
|
+
// helper yet, so until one lands the caller stays unset (anonymous)
|
|
464
|
+
// rather than trusting an unverifiable value.
|
|
465
|
+
this.task_caller = null;
|
|
466
|
+
try {
|
|
467
|
+
const callerValue = await this.getFileContentAndDecrypt(this.etny_bucket, 'caller.securelock');
|
|
468
|
+
const callerSig = await this.getFileContentAndDecrypt(this.etny_bucket, 'caller.securelock.sig');
|
|
469
|
+
if (typeof etny_crypto.verifySignature === 'function') {
|
|
470
|
+
await this.getTrustedZonePublicKey();
|
|
471
|
+
if (etny_crypto.verifySignature(this.trustedZonePublicKey, callerValue, callerSig)) {
|
|
472
|
+
this.task_caller = String(callerValue).trim();
|
|
473
|
+
console.log(`Task caller attested by trustedzone: ${this.task_caller}`);
|
|
474
|
+
} else {
|
|
475
|
+
console.error('caller.securelock signature validation FAILED; treating task as anonymous');
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
console.log('No signature-verify support in this build; ignoring caller.securelock (anonymous)');
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
console.log('No attested task caller supplied; treating task as anonymous');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.configureStateRegistry();
|
|
485
|
+
|
|
486
|
+
const task_result = await executeTask(payload_data, input_data);
|
|
487
|
+
console.log('TASK RESULT (code, data):', task_result);
|
|
488
|
+
this.task_code = task_result[0];
|
|
489
|
+
this.task_result = task_result[1];
|
|
490
|
+
|
|
491
|
+
// Post-payload: re-assert the attested caller across the ESR ledger from
|
|
492
|
+
// THIS securelock's private copy (this.task_caller), captured before the
|
|
493
|
+
// payload ran and never handed to the payload. Corrects any callerUsed
|
|
494
|
+
// the payload may have forged (so the trustedzone reads the truth) and
|
|
495
|
+
// fails the task locally if a forgery is detected. Defence in depth;
|
|
496
|
+
// the trustedzone (separate enclave) and the CID-bound caller remain the
|
|
497
|
+
// authoritative layers.
|
|
498
|
+
try {
|
|
499
|
+
const ecld_state = require("./ecld_state");
|
|
500
|
+
if (typeof ecld_state.restampLedgerCaller === 'function') {
|
|
501
|
+
const tampered = await ecld_state.restampLedgerCaller(this.task_caller || null);
|
|
502
|
+
if (tampered) {
|
|
503
|
+
console.error('ESR ledger caller was tampered with by the payload -- failing the task (SECURITY_VIOLATION).');
|
|
504
|
+
this.task_code = TaskStatus.SECURITY_VIOLATION;
|
|
505
|
+
this.task_result = 'SECURITY_VIOLATION: the task attempted to forge the state commit caller. The attested submitter was re-asserted and the task was terminated.';
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
console.error('ESR ledger re-stamp skipped:', e && e.message ? e.message : e);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function initSwiftStreamClient() {
|
|
518
|
+
console.log('Initializing SwiftStream Service...');
|
|
519
|
+
const swiftStreamClient = new SwiftStreamService(
|
|
520
|
+
EtnySecureLock.debug ? "localhost" : "etny-swift-stream",
|
|
521
|
+
9000,
|
|
522
|
+
"swiftstreamadmin",
|
|
523
|
+
"swiftstreamadmin");
|
|
524
|
+
console.log('SwiftStream Service started.');
|
|
525
|
+
console.log('################################');
|
|
526
|
+
|
|
527
|
+
return swiftStreamClient;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Set as soon as the enclave object exists so the top-level catch can report a
|
|
531
|
+
// failure result even when the throw happens deep inside execute().
|
|
532
|
+
let currentApp = null;
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Best-effort: write a failure result so the trustedzone stops waiting.
|
|
536
|
+
*
|
|
537
|
+
* Securelock only publishes the result and its code; the trustedzone reads
|
|
538
|
+
* those and builds the signed transaction. Without them that wait runs to its
|
|
539
|
+
* timeout and the whole order stalls with no diagnosis.
|
|
540
|
+
*
|
|
541
|
+
* Guarded throughout: this runs while already handling a failure, so it must
|
|
542
|
+
* never throw or hang.
|
|
543
|
+
*/
|
|
544
|
+
async function reportFailure(app, err) {
|
|
545
|
+
if (!app) {
|
|
546
|
+
console.log('cannot report failure: enclave died before initialization');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
// Keep a specific code if one was already set (checksum mismatch, ...).
|
|
551
|
+
if (app.task_code === undefined || app.task_code === null || app.task_code === '') {
|
|
552
|
+
app.task_code = TaskStatus.SYSTEM_ERROR;
|
|
553
|
+
}
|
|
554
|
+
if (!app.task_result) {
|
|
555
|
+
app.task_result = `ENCLAVE ERROR: ${err && err.message ? err.message : err}`;
|
|
556
|
+
}
|
|
557
|
+
await app.saveResult();
|
|
558
|
+
console.log(`failure reported (task_code=${app.task_code})`);
|
|
559
|
+
} catch (reportErr) {
|
|
560
|
+
console.log('could not report the failure result:', reportErr);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function execute() {
|
|
565
|
+
console.log('########################################');
|
|
566
|
+
console.log('########################################');
|
|
567
|
+
console.log('########## [ETNY SecureLock] ##########');
|
|
568
|
+
console.log('########################################');
|
|
569
|
+
console.log('########################################');
|
|
570
|
+
console.log('');
|
|
571
|
+
console.log('');
|
|
572
|
+
console.log('');
|
|
573
|
+
|
|
574
|
+
const swiftStreamClient = initSwiftStreamClient();
|
|
575
|
+
|
|
576
|
+
const app = new EtnySecureLock(swiftStreamClient);
|
|
577
|
+
// Exposed so the top-level handler can still publish a failure result if
|
|
578
|
+
// anything below throws (see reportFailure).
|
|
579
|
+
currentApp = app;
|
|
580
|
+
|
|
581
|
+
// Testnet: materialize the self-signed cert/key from MR_ENCLAVE into the
|
|
582
|
+
// file paths before anything reads them (harvest + decrypt). No-op on mainnet
|
|
583
|
+
// (CAS provides those files).
|
|
584
|
+
app.generateSelfSignedCertIfTestnet();
|
|
585
|
+
|
|
586
|
+
const hasReadEnvFile = await app.readEnv();
|
|
587
|
+
if (!hasReadEnvFile && !EtnySecureLock.debug) {
|
|
588
|
+
console.log(`Failed reading .env file.`);
|
|
589
|
+
await app.savePublicCert();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (process.env.ETNY_MODE === "PUBLIC_KEY") {
|
|
594
|
+
await app.savePublicCert();
|
|
595
|
+
} else {
|
|
596
|
+
console.log('Creating the application...');
|
|
597
|
+
const created = await app.create();
|
|
598
|
+
|
|
599
|
+
if (!created) {
|
|
600
|
+
console.log(`Failed at creation step, not able decoding input metadata, exiting.`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
console.log('Waiting for payload and input to be downloaded...');
|
|
605
|
+
await app.waitForPayloadAndInput();
|
|
606
|
+
|
|
607
|
+
if (app.is_valid_client_data) {
|
|
608
|
+
console.log('Executing client code...');
|
|
609
|
+
const wasExecuted = await app.execute();
|
|
610
|
+
if (!wasExecuted) {
|
|
611
|
+
console.log('Failed execution of SecureLock enclave.')
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
console.log(`Failed client payload and input validation, ${app.task_result}, exiting.`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
await app.saveResult();
|
|
620
|
+
console.log('Finished process.');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
(async () => {
|
|
625
|
+
try {
|
|
626
|
+
await execute();
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.log(e);
|
|
629
|
+
await reportFailure(currentApp, e);
|
|
630
|
+
process.exitCode = 1;
|
|
631
|
+
}
|
|
632
|
+
})();
|