@ethernity-cloud/sdk-js 1.1.1 → 1.4.0
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/build.js +29 -0
- package/init.js +1 -1
- package/nodenithy/build/securelock/Dockerfile.tmpl +21 -5
- package/nodenithy/build/securelock/src/README-keygen.md +41 -0
- package/nodenithy/build/securelock/src/etny_exec.js +46 -18
- package/nodenithy/build/securelock/src/models.js +275 -261
- package/nodenithy/build/securelock/src/securelock.js.tmpl +444 -421
- package/nodenithy/build/securelock/src/self_sign.js +76 -0
- package/nodenithy/build/securelock/src/sgx_report.js +50 -0
- package/nodenithy/build/securelock/src/sgx_report.node +0 -0
- package/nodenithy/build/securelock/src/task_status.js +3 -0
- package/nodenithy/build.js +22 -0
- package/nodenithy/build.mjs +78 -5
- package/nodenithy/ipfs.mjs +340 -426
- package/nodenithy/run/docker-compose-final.yml.tmpl +3 -0
- package/nodenithy/run/docker-compose.yml.tmpl +10 -8
- package/nodenithy/run.js +44 -1
- package/package.json +50 -48
- package/pynithy/build/securelock/Dockerfile.tmpl +18 -2
- package/pynithy/build.mjs +31 -5
- package/pynithy/ipfs.mjs +376 -155
- package/pynithy/run/docker-compose-final.yml.tmpl +9 -0
- package/pynithy/run/docker-compose.yml.tmpl +10 -6
- package/pynithy/run.js +40 -1
- package/test.js +204 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# Publish-time compose: used by ecld-publish to compute the MRENCLAVE
|
|
2
|
+
# (SCONE_HASH=1) and extract the enclave certificate LOCALLY. It intentionally
|
|
3
|
+
# does NOT mount /dev/sgx_enclave so it runs on a developer machine WITHOUT SGX
|
|
4
|
+
# (SCONE_HASH is a build measurement; enclaves reach SGX via LAS over TCP). When
|
|
5
|
+
# no local SGX is present, cert extraction falls back to the remote
|
|
6
|
+
# publickey.ethernity.cloud service. The on-node runtime uses
|
|
7
|
+
# docker-compose-final.yml, which targets real SGX hardware.
|
|
1
8
|
version: '3.2'
|
|
2
9
|
services:
|
|
3
10
|
las:
|
|
@@ -5,8 +12,6 @@ services:
|
|
|
5
12
|
privileged: true
|
|
6
13
|
image: localhost:5000/etny-las
|
|
7
14
|
command: "bash -c '/las_entrypoint.sh && /usr/local/bin/las | tee /var/log/las.log'"
|
|
8
|
-
devices:
|
|
9
|
-
- "/dev/sgx_enclave:/dev/sgx_enclave"
|
|
10
15
|
restart: unless-stopped
|
|
11
16
|
ports:
|
|
12
17
|
- target: 18766
|
|
@@ -25,12 +30,13 @@ services:
|
|
|
25
30
|
- SCONE_LAS_ADDR=las
|
|
26
31
|
- SCONE_CONFIG_ID=__ENCLAVE_NAME_SECURELOCK__/application
|
|
27
32
|
- SCONE_HEAP=1024M
|
|
33
|
+
# Must match the --stack the securelock binary was signed with (build.mjs),
|
|
34
|
+
# otherwise SCONE recomputes MRENCLAVE at load and re-signs as debug.
|
|
35
|
+
- SCONE_STACK=4M
|
|
28
36
|
- SCONE_LOG=INFO
|
|
29
37
|
- SCONE_ALLOW_DLOPEN=1
|
|
30
38
|
- SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
|
|
31
39
|
restart: on-failure:5
|
|
32
|
-
devices:
|
|
33
|
-
- "/dev/sgx_enclave:/dev/sgx_enclave"
|
|
34
40
|
depends_on:
|
|
35
41
|
- las
|
|
36
42
|
|
|
@@ -49,7 +55,5 @@ services:
|
|
|
49
55
|
- SCONE_ALLOW_DLOPEN=1
|
|
50
56
|
- SCONE_EXTENSIONS_PATH=/lib/libbinary-fs.so
|
|
51
57
|
restart: on-failure:5
|
|
52
|
-
devices:
|
|
53
|
-
- "/dev/sgx_enclave:/dev/sgx_enclave"
|
|
54
58
|
depends_on:
|
|
55
59
|
- las
|
package/pynithy/run.js
CHANGED
|
@@ -73,6 +73,29 @@ const runDockerCommand = (service) => {
|
|
|
73
73
|
return output.split('\n').filter(line => !/Creating|Pulling|latest|Digest/.test(line)).join('');
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
// Extract the runtime enclave measurement (SCONE_HASH) as a bare 64-hex string.
|
|
77
|
+
// Mirrors Python extract_scone_hash: pick the first 64-hex token in the output.
|
|
78
|
+
const extractSconeHash = (service) => {
|
|
79
|
+
const output = runDockerCommand(service);
|
|
80
|
+
const m = output.match(/\b[a-fA-F0-9]{64}\b/);
|
|
81
|
+
if (!m) throw new Error(`No SHA256 MRENCLAVE found in ${service} SCONE_HASH output.`);
|
|
82
|
+
return m[0];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Read the MRENCLAVE signed at build time (baked into the image as
|
|
86
|
+
// /signed_mrenclave.txt on mainnet). Mirrors Python extract_signed_mrenclave.
|
|
87
|
+
// Returns '' if the file is absent (testnet builds).
|
|
88
|
+
const extractSignedMrenclave = (service) => {
|
|
89
|
+
try {
|
|
90
|
+
const command = `docker-compose -f docker-compose.yml run --no-deps --entrypoint cat ${service} /signed_mrenclave.txt`;
|
|
91
|
+
const output = execSync(command, { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
92
|
+
const m = output.match(/\b[a-fA-F0-9]{64}\b/);
|
|
93
|
+
return m ? m[0] : '';
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
76
99
|
const main = async () => {
|
|
77
100
|
process.env.NODE_NO_WARNINGS = 1
|
|
78
101
|
const backupFiles = ['docker-compose.yml.tmpl', 'docker-compose-final.yml.tmpl'];
|
|
@@ -86,13 +109,29 @@ const main = async () => {
|
|
|
86
109
|
});
|
|
87
110
|
|
|
88
111
|
|
|
89
|
-
process.env.MRENCLAVE_SECURELOCK =
|
|
112
|
+
process.env.MRENCLAVE_SECURELOCK = extractSconeHash('etny-securelock');
|
|
90
113
|
console.log(`MRENCLAVE_SECURELOCK: ${process.env.MRENCLAVE_SECURELOCK}`);
|
|
91
114
|
// process.env.MRENCLAVE_TRUSTEDZONE = runDockerCommand('etny-trustedzone');
|
|
92
115
|
// console.log(`MRENCLAVE_TRUSTEDZONE: ${process.env.MRENCLAVE_TRUSTEDZONE}`);
|
|
93
116
|
// process.env.MRENCLAVE_VALIDATOR = runDockerCommand('etny-validator');
|
|
94
117
|
// console.log(`MRENCLAVE_VALIDATOR: ${process.env.MRENCLAVE_VALIDATOR}`);
|
|
95
118
|
|
|
119
|
+
// ----- MRENCLAVE match-gate (mainnet) -----
|
|
120
|
+
// The runtime enclave measured just above (SCONE_HASH) MUST match the
|
|
121
|
+
// MRENCLAVE signed --production at build time (/signed_mrenclave.txt). A
|
|
122
|
+
// mismatch means SCONE recomputed the measurement at load (params drift) and
|
|
123
|
+
// re-signed as DEBUG, which CAS rejects on mainnet. Refuse to publish.
|
|
124
|
+
if (isMainnet) {
|
|
125
|
+
const signedMrenclave = extractSignedMrenclave('etny-securelock');
|
|
126
|
+
if (!signedMrenclave || signedMrenclave !== process.env.MRENCLAVE_SECURELOCK) {
|
|
127
|
+
console.error(`Error: securelock runtime MRENCLAVE (${process.env.MRENCLAVE_SECURELOCK}) != signed MRENCLAVE (${signedMrenclave}).`);
|
|
128
|
+
console.error(' The runtime enclave differs from the --production-signed binary (SCONE recomputed -> debug).');
|
|
129
|
+
console.error(' Refusing to publish. Rebuild with the current SDK (ecld-build) and retry.');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
console.log('\t✔ MRENCLAVE match-gate passed: runtime enclave matches the --production-signed MRENCLAVE');
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
writeEnv('MRENCLAVE_SECURELOCK', process.env.MRENCLAVE_SECURELOCK);
|
|
97
136
|
// writeEnv('MRENCLAVE_TRUSTEDZONE', process.env.MRENCLAVE_TRUSTEDZONE);
|
|
98
137
|
// writeEnv('MRENCLAVE_VALIDATOR', process.env.MRENCLAVE_VALIDATOR);
|
package/test.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ecld-test — run a Nodenithy payload locally, without SGX, exactly as the
|
|
3
|
+
* enclave would.
|
|
4
|
+
*
|
|
5
|
+
* Stages the project's src/serverless next to the vendored securelock
|
|
6
|
+
* executor (the same layout ecld-build produces inside the enclave image) and
|
|
7
|
+
* executes the payload with that executor: same backend reflection, same
|
|
8
|
+
* ___etny_result___ / ___etny_data_set___ scope, same TaskStatus codes.
|
|
9
|
+
* Validates the CALL — names, arguments, result wiring — before anything is
|
|
10
|
+
* paid for on-chain. It does not validate attestation, checksums or
|
|
11
|
+
* encryption; those need a real (testnet) run.
|
|
12
|
+
*
|
|
13
|
+
* Usage, from the project root:
|
|
14
|
+
*
|
|
15
|
+
* npx ecld-test 'hello("World")' one-shot, in-process
|
|
16
|
+
* npx ecld-test --file payload.js
|
|
17
|
+
* npx ecld-test --input data.json 'processData(___etny_data_set___)'
|
|
18
|
+
* npx ecld-test --input-text '{"x":1}' 'processData(___etny_data_set___)'
|
|
19
|
+
*
|
|
20
|
+
* npx ecld-test serve [--port 8745] local API for runner LOCAL mode
|
|
21
|
+
*
|
|
22
|
+
* `serve` starts the local test API so a dApp can exercise its real runner
|
|
23
|
+
* integration end to end: new EthernityCloudRunner('LOCAL') in
|
|
24
|
+
* @ethernity-cloud/runner >= 0.3.6 executes every run() against this API
|
|
25
|
+
* instead of the blockchain.
|
|
26
|
+
*
|
|
27
|
+
* Exit code 0 on TaskStatus SUCCESS, 1 otherwise.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const http = require('http');
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
|
|
36
|
+
const TASK_STATUS_NAMES = {
|
|
37
|
+
0: 'SUCCESS',
|
|
38
|
+
1: 'SYSTEM_ERROR',
|
|
39
|
+
2: 'KEY_ERROR',
|
|
40
|
+
3: 'SYNTAX_WARNING',
|
|
41
|
+
4: 'BASE_EXCEPTION',
|
|
42
|
+
5: 'PAYLOAD_NOT_DEFINED',
|
|
43
|
+
6: 'PAYLOAD_CHECKSUM_ERROR',
|
|
44
|
+
7: 'INPUT_CHECKSUM_ERROR',
|
|
45
|
+
8: 'EVAL_ERROR',
|
|
46
|
+
28: 'IMPORT_ERROR',
|
|
47
|
+
32: 'CONFIG_ERROR',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const VENDORED_SRC = path.join(__dirname, 'nodenithy', 'build', 'securelock', 'src');
|
|
51
|
+
|
|
52
|
+
function copyDir(src, dest) {
|
|
53
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
54
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
55
|
+
const from = path.join(src, entry.name);
|
|
56
|
+
const to = path.join(dest, entry.name);
|
|
57
|
+
if (entry.isDirectory()) copyDir(from, to);
|
|
58
|
+
else fs.copyFileSync(from, to);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Stage the executor + the project's backend the way ecld-build does inside
|
|
63
|
+
* the enclave image, then load the REAL executor from the staged copy. */
|
|
64
|
+
function loadExecutor(projectSrc) {
|
|
65
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'ecld-test-'));
|
|
66
|
+
for (const f of ['etny_exec.js', 'task_status.js']) {
|
|
67
|
+
fs.copyFileSync(path.join(VENDORED_SRC, f), path.join(stage, f));
|
|
68
|
+
}
|
|
69
|
+
const backendDir = path.join(projectSrc, 'serverless');
|
|
70
|
+
const backendJs = path.join(backendDir, 'backend.js');
|
|
71
|
+
if (fs.existsSync(backendJs)) {
|
|
72
|
+
copyDir(backendDir, path.join(stage, 'serverless'));
|
|
73
|
+
console.log(`backend : ${backendJs}`);
|
|
74
|
+
} else if (fs.existsSync(path.join(backendDir, 'backend.py'))) {
|
|
75
|
+
console.log('backend : backend.py found — this is a Pynithy project; use');
|
|
76
|
+
console.log(' ecld-test from ethernity-cloud-sdk-py instead.');
|
|
77
|
+
console.log(' Continuing with bare globals (stock-enclave mode).');
|
|
78
|
+
} else {
|
|
79
|
+
console.log('backend : none found — running with bare globals, like a stock enclave');
|
|
80
|
+
}
|
|
81
|
+
return require(path.join(stage, 'etny_exec.js'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runTask(executor, payload, input) {
|
|
85
|
+
const [code, resultRaw] = executor.executeTask(payload, input || '');
|
|
86
|
+
const result = typeof resultRaw === 'string' ? resultRaw : JSON.stringify(resultRaw);
|
|
87
|
+
const checksum = crypto.createHash('sha256').update(result, 'utf8').digest('hex');
|
|
88
|
+
const challenge = Array.from({ length: 20 }, () =>
|
|
89
|
+
'abcdefghijklmnopqrstuvwxyz0123456789'[Math.floor(Math.random() * 36)]
|
|
90
|
+
).join('');
|
|
91
|
+
return {
|
|
92
|
+
task_code: code,
|
|
93
|
+
task_code_name: TASK_STATUS_NAMES[code] || `UNKNOWN_${code}`,
|
|
94
|
+
result,
|
|
95
|
+
checksum,
|
|
96
|
+
enclave_challenge: challenge,
|
|
97
|
+
result_string: `v3:${code}:${checksum}:${challenge}:`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function serve(executor, host, port) {
|
|
102
|
+
const server = http.createServer((req, res) => {
|
|
103
|
+
const send = (status, obj) => {
|
|
104
|
+
const body = JSON.stringify(obj);
|
|
105
|
+
res.writeHead(status, {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'Content-Length': Buffer.byteLength(body),
|
|
108
|
+
'Access-Control-Allow-Origin': '*',
|
|
109
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
110
|
+
});
|
|
111
|
+
res.end(body);
|
|
112
|
+
};
|
|
113
|
+
if (req.method === 'OPTIONS') return send(204, {});
|
|
114
|
+
if (req.method === 'GET' && req.url === '/v1/health') {
|
|
115
|
+
// Probe with an expression that lists scope? Keep it simple: report ok.
|
|
116
|
+
return send(200, { status: 'ok', backend: serve.backendFunctions || [] });
|
|
117
|
+
}
|
|
118
|
+
if (req.method === 'POST' && req.url === '/v1/task') {
|
|
119
|
+
let raw = '';
|
|
120
|
+
req.on('data', (c) => { raw += c; });
|
|
121
|
+
req.on('end', () => {
|
|
122
|
+
try {
|
|
123
|
+
const body = JSON.parse(raw || '{}');
|
|
124
|
+
if (typeof body.payload !== 'string' || !body.payload.trim()) {
|
|
125
|
+
return send(400, { error: 'payload (string) is required' });
|
|
126
|
+
}
|
|
127
|
+
let input = body.input;
|
|
128
|
+
if (input !== undefined && input !== null && typeof input !== 'string') {
|
|
129
|
+
input = JSON.stringify(input);
|
|
130
|
+
}
|
|
131
|
+
const out = runTask(executor, body.payload, input);
|
|
132
|
+
console.log(`[local-api] task -> ${out.task_code} (${out.task_code_name})`);
|
|
133
|
+
return send(200, out);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (e instanceof SyntaxError) return send(400, { error: 'invalid JSON body' });
|
|
136
|
+
return send(500, { error: e.message });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
send(404, { error: 'not found' });
|
|
142
|
+
});
|
|
143
|
+
server.listen(port, host, () => {
|
|
144
|
+
console.log(`[local-api] listening on http://${host}:${port} (POST /v1/task, GET /v1/health)`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function main() {
|
|
149
|
+
const argv = process.argv.slice(2);
|
|
150
|
+
const opts = { src: 'src', host: '127.0.0.1', port: 8745 };
|
|
151
|
+
const positional = [];
|
|
152
|
+
for (let i = 0; i < argv.length; i++) {
|
|
153
|
+
const a = argv[i];
|
|
154
|
+
if (a === '--file' || a === '-f') opts.file = argv[++i];
|
|
155
|
+
else if (a === '--input' || a === '-i') opts.inputFile = argv[++i];
|
|
156
|
+
else if (a === '--input-text') opts.inputText = argv[++i];
|
|
157
|
+
else if (a === '--src') opts.src = argv[++i];
|
|
158
|
+
else if (a === '--host') opts.host = argv[++i];
|
|
159
|
+
else if (a === '--port') opts.port = parseInt(argv[++i], 10);
|
|
160
|
+
else positional.push(a);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const projectSrc = path.resolve(opts.src);
|
|
164
|
+
const executor = loadExecutor(projectSrc);
|
|
165
|
+
// Record the backend's exported names once for /v1/health.
|
|
166
|
+
try {
|
|
167
|
+
serve.backendFunctions = Object.keys(require(path.join(projectSrc, 'serverless', 'backend.js')));
|
|
168
|
+
} catch (e) {
|
|
169
|
+
serve.backendFunctions = [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (positional[0] === 'serve') {
|
|
173
|
+
return serve(executor, opts.host, opts.port);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let payload;
|
|
177
|
+
if (opts.file) payload = fs.readFileSync(opts.file, 'utf8');
|
|
178
|
+
else if (positional.length) payload = positional.join(' ');
|
|
179
|
+
else {
|
|
180
|
+
console.error("usage: ecld-test 'hello(\"World\")' | ecld-test --file payload.js | ecld-test serve");
|
|
181
|
+
process.exit(2);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let input = null;
|
|
185
|
+
if (opts.inputFile && opts.inputText !== undefined) {
|
|
186
|
+
console.error('--input and --input-text are mutually exclusive');
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
if (opts.inputFile) input = fs.readFileSync(opts.inputFile, 'utf8');
|
|
190
|
+
else if (opts.inputText !== undefined) input = opts.inputText;
|
|
191
|
+
|
|
192
|
+
console.log(`payload : ${JSON.stringify(payload)}`);
|
|
193
|
+
console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
|
|
194
|
+
const out = runTask(executor, payload, input);
|
|
195
|
+
console.log(`task code: ${out.task_code} (${out.task_code_name})`);
|
|
196
|
+
console.log(`result : ${JSON.stringify(out.result)}`);
|
|
197
|
+
if (out.task_code !== 0) {
|
|
198
|
+
console.log('\nThis is exactly what the network would write on-chain — the failed');
|
|
199
|
+
console.log('attempt would still cost gas. Fix locally, then submit.');
|
|
200
|
+
}
|
|
201
|
+
process.exit(out.task_code === 0 ? 0 : 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main();
|