@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.
@@ -0,0 +1,76 @@
1
+ // Testnet non-CAS self-sign: derive a DETERMINISTIC secp384r1 key + self-signed
2
+ // X509 from the enclave's own MR_ENCLAVE / MR_SIGNER (read via the sgx_report
3
+ // N-API addon). Only the exact attested enclave can regenerate this key, so the
4
+ // harvested cert (registered on-chain) survives restarts and stays consistent.
5
+ // [TESTNET-INSECURE] -- testnet runs pre-release SGX; NOT a production guarantee.
6
+ //
7
+ // Cert-compatibility note: nodenithy's trustedzone fetches the securelock public
8
+ // key from the on-chain image registry (getImageCertPublicKey) and does NOT
9
+ // re-derive it from MR_ENCLAVE. Both enclaves are Node. So this cert only needs
10
+ // to be a valid P-384 X509 that is STABLE per MR_ENCLAVE and readable by
11
+ // etny_crypto (jsrsasign KEYUTIL for the key, crypto.createPublicKey/ec-key for
12
+ // the cert). It does NOT need to be byte-identical to pynithy's Python DER.
13
+ const rs = require('jsrsasign');
14
+ const sgx = require('./sgx_report');
15
+
16
+ // The identity-key derivation lives ENTIRELY in the compiled addon
17
+ // (get_sgx_report.c, via sgx_report.deriveIdentityScalar). It is not present in
18
+ // this interpreted source, so reading self_sign.js reveals nothing about how
19
+ // the key is derived. Each enclave uses its own role tag inside the addon, so
20
+ // the derivations are mutually decoupled. role: 0=securelock 1=trustedzone
21
+ // 2=validator, selected from the subject CN.
22
+ function roleForSubject(subjectCN) {
23
+ const cn = String(subjectCN || '').toLowerCase();
24
+ if (cn.includes('securelock')) return 0;
25
+ if (cn.includes('validator')) return 2;
26
+ return 1;
27
+ }
28
+
29
+ /**
30
+ * Generate the deterministic self-signed cert for this enclave.
31
+ * @param {string} subjectCN e.g. "SecureLock Enclave" / "TrustedZone Enclave [TESTNET-INSECURE]"
32
+ * @returns {{privateKeyPem: string, certPem: string, mrEnclaveHex: string}}
33
+ */
34
+ function generateCertFromMrEnclave(subjectCN) {
35
+ const mrEnclaveHex = sgx.getMrEnclave(); // 64 hex chars (32 bytes)
36
+
37
+ // Derivation performed inside the compiled addon; role from the subject CN.
38
+ const privHex = sgx.deriveIdentityScalar(roleForSubject(subjectCN));
39
+
40
+ // Build the EC keypair from the deterministic private scalar (jsrsasign).
41
+ const ecKeypair = rs.KEYUTIL.generateKeypair('EC', 'secp384r1');
42
+ const prvKey = new rs.KJUR.crypto.ECDSA({ curve: 'secp384r1' });
43
+ prvKey.setPrivateKeyHex(privHex);
44
+ prvKey.generatePublicKeyHex(); // derive pub from priv
45
+ void ecKeypair;
46
+
47
+ const pubKey = new rs.KJUR.crypto.ECDSA({ curve: 'secp384r1' });
48
+ pubKey.setPublicKeyHex(prvKey.pubKeyHex);
49
+
50
+ // Self-signed X509: fixed serial/validity so the cert is stable per key. No
51
+ // custom MR_ENCLAVE extension -- unlike pynithy (whose cert carries it as an
52
+ // informational OID), nothing in nodenithy reads it: the trustedzone uses the
53
+ // securelock public key fetched from the on-chain image registry, not the cert
54
+ // extensions. Keeping the cert minimal avoids jsrsasign generic-extension API
55
+ // fragility across versions. mrEnclaveHex is still returned for logging.
56
+ const cert = new rs.KJUR.asn1.x509.Certificate({
57
+ version: 3,
58
+ serial: { int: 31337 },
59
+ issuer: { str: '/CN=' + subjectCN },
60
+ subject: { str: '/CN=' + subjectCN },
61
+ notbefore: '250101000000Z',
62
+ notafter: '350101000000Z',
63
+ sbjpubkey: pubKey,
64
+ sigalg: 'SHA256withECDSA',
65
+ cakey: prvKey,
66
+ });
67
+
68
+ const certPem = cert.getPEM();
69
+
70
+ // Private key as PKCS8 PEM (etny_crypto reads it via rs.KEYUTIL.getKey().prvKeyHex).
71
+ const privateKeyPem = rs.KEYUTIL.getPEM(prvKey, 'PKCS8PRV');
72
+
73
+ return { privateKeyPem, certPem, mrEnclaveHex };
74
+ }
75
+
76
+ module.exports = { generateCertFromMrEnclave };
@@ -0,0 +1,50 @@
1
+ // Loader for the SGX EREPORT N-API addon (sgx_report.node), which exposes the
2
+ // enclave's own MR_ENCLAVE / MR_SIGNER (used to deterministically derive the
3
+ // testnet self-signed cert). The addon is compiled from sgx_report_napi.c +
4
+ // get_sgx_report.c at build time and baked into binary-fs (see the Dockerfile).
5
+ const path = require('path');
6
+
7
+ let addon = null;
8
+ function load() {
9
+ if (addon) return addon;
10
+ // The addon sits next to the enclave sources in the image; try a couple of
11
+ // resolutions so it works in the enclave (/etny-securelock) and in local test.
12
+ const candidates = [
13
+ path.join(__dirname, 'sgx_report.node'),
14
+ '/etny-trustedzone/sgx_report.node',
15
+ './sgx_report.node',
16
+ ];
17
+ let lastErr;
18
+ for (const p of candidates) {
19
+ try {
20
+ addon = require(p);
21
+ return addon;
22
+ } catch (e) {
23
+ lastErr = e;
24
+ }
25
+ }
26
+ throw new Error('Failed to load sgx_report.node addon: ' + (lastErr && lastErr.message));
27
+ }
28
+
29
+ function getMrEnclave() {
30
+ const v = load().getMrEnclave();
31
+ if (!v) throw new Error('get_mr_enclave() returned null (EREPORT failed)');
32
+ return v;
33
+ }
34
+
35
+ function getMrSigner() {
36
+ const v = load().getMrSigner();
37
+ if (!v) throw new Error('get_mr_signer() returned null (EREPORT failed)');
38
+ return v;
39
+ }
40
+
41
+ // Consolidated identity-key derivation, performed inside the compiled addon
42
+ // (get_sgx_report.c). role: 0=securelock, 1=trustedzone, 2=validator.
43
+ // Returns the 96-hex secp384r1 scalar. The algorithm is NOT in self_sign.js.
44
+ function deriveIdentityScalar(role) {
45
+ const v = load().deriveIdentityScalar(role | 0);
46
+ if (!v) throw new Error('deriveIdentityScalar() returned null');
47
+ return v;
48
+ }
49
+
50
+ module.exports = { getMrEnclave, getMrSigner, deriveIdentityScalar };
@@ -8,6 +8,9 @@ class TaskStatus {
8
8
  static PAYLOAD_CHECKSUM_ERROR = 6;
9
9
  static INPUT_CHECKSUM_ERROR = 7;
10
10
  static EVAL_ERROR = 8;
11
+ // Extended diagnostic (matches the trustedzone's extended enum): the
12
+ // serverless backend failed to load, so none of its functions exist.
13
+ static IMPORT_ERROR = 28;
11
14
  }
12
15
 
13
16
  module.exports = {
@@ -67,6 +67,28 @@ runCommand(`docker rm registry -f`);
67
67
 
68
68
  const srcDir = './src/serverless';
69
69
  const destDir = path.join(buildDir, 'securelock/src/serverless');
70
+
71
+ // Fail the build NOW if the backend is missing or unparseable. An enclave
72
+ // built without a valid backend runs every task into "X is not defined" /
73
+ // IMPORT_ERROR on-chain — expensive to discover after building, publishing
74
+ // and paying for a task.
75
+ const backendFile = path.join(srcDir, 'backend.js');
76
+ if (!fs.existsSync(backendFile)) {
77
+ console.error(`ERROR: ${backendFile} not found.`);
78
+ console.error(' The securelock enclave loads your functions from src/serverless/backend.js;');
79
+ console.error(' without it no backend function will exist inside the enclave.');
80
+ console.error(' Run ecld-init to scaffold it, or create the file before building.');
81
+ process.exit(1);
82
+ }
83
+ try {
84
+ require(path.resolve(backendFile));
85
+ } catch (e) {
86
+ console.error('ERROR: src/serverless/backend.js failed to load and would fail inside the enclave:');
87
+ console.error(` ${e.name || 'Error'}: ${e.message}`);
88
+ console.error(' Fix the module (missing dependency? bad require?) before building.');
89
+ process.exit(1);
90
+ }
91
+
70
92
  console.log(`Creating destination directory: ${destDir}`);
71
93
  fs.mkdirSync(destDir, { recursive: true });
72
94
 
@@ -92,6 +92,29 @@ runCommand(`docker rm registry -f`);
92
92
 
93
93
  const srcDir = './src/serverless';
94
94
  const destDir = path.join(buildDir, 'securelock/src/serverless');
95
+
96
+ // Fail the build NOW if the backend is missing or unloadable. An enclave
97
+ // built without a valid backend runs every task into "X is not defined" /
98
+ // IMPORT_ERROR on-chain — expensive to discover after building, publishing
99
+ // and paying for a task.
100
+ const backendFile = path.join(srcDir, 'backend.js');
101
+ if (!fs.existsSync(backendFile)) {
102
+ console.error(`ERROR: ${backendFile} not found.`);
103
+ console.error(' The securelock enclave loads your functions from src/serverless/backend.js;');
104
+ console.error(' without it no backend function will exist inside the enclave.');
105
+ console.error(' Run ecld-init to scaffold it, or create the file before building.');
106
+ process.exit(1);
107
+ }
108
+ try {
109
+ const { createRequire } = await import('module');
110
+ createRequire(import.meta.url)(path.resolve(backendFile));
111
+ } catch (e) {
112
+ console.error('ERROR: src/serverless/backend.js failed to load and would fail inside the enclave:');
113
+ console.error(` ${e.name || 'Error'}: ${e.message}`);
114
+ console.error(' Fix the module (missing dependency? bad require?) before building.');
115
+ process.exit(1);
116
+ }
117
+
95
118
  console.log(`Creating destination directory: ${destDir}`);
96
119
  fs.mkdirSync(destDir, { recursive: true });
97
120
 
@@ -120,19 +143,69 @@ writeEnv('ENCLAVE_NAME_SECURELOCK', ENCLAVE_NAME_SECURELOCK);
120
143
 
121
144
  console.log('Building etny-securelock');
122
145
  process.chdir('securelock');
146
+
147
+ // The SGX key-gen module ships as a PREBUILT sgx_report.node (never the .c
148
+ // source). It is a build artifact of the etny-nodenithy pipeline; the
149
+ // securelock Dockerfile bakes it into binary-fs but does not compile it. Fail
150
+ // fast with a clear message if it is missing, rather than producing an enclave
151
+ // that crashes on the missing addon at runtime.
152
+ if (!fs.existsSync('src/sgx_report.node')) {
153
+ console.error('ERROR: securelock/src/sgx_report.node not found.');
154
+ console.error(' The SDK ships the SGX key-gen module as a prebuilt .node, not source.');
155
+ console.error(' It is a build artifact of the etny-nodenithy pipeline; copy it into');
156
+ console.error(' securelock/src/ (see src/README-keygen.md) before building.');
157
+ process.exit(1);
158
+ }
123
159
  // runCommand(`cat Dockerfile.tmpl | sed s/"__ENCLAVE_NAME_SECURELOCK__"/"${ENCLAVE_NAME_SECURELOCK}"/g > Dockerfile`);
124
160
  const dockerfileSecureTemplate = fs.readFileSync('Dockerfile.tmpl', 'utf8');
125
- const dockerfileSecureContent = dockerfileSecureTemplate.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK).replace(/__BUCKET_NAME__/g, templateName + "-v3").replace(/__SMART_CONTRACT_ADDRESS__/g, ECRunner[templateName][0]).replace(/__IMAGE_REGISTRY_ADDRESS__/g, ECRunner[templateName][1]).replace(/__RPC_URL__/g, ECRunner[templateName][2]).replace(/__CHAIN_ID__/g, ECRunner[templateName][3]).replace(/__TRUSTED_ZONE_IMAGE__/g, templateName);
126
-
127
- fs.writeFileSync('Dockerfile', dockerfileSecureContent);
161
+ let dockerfileSecureContent = dockerfileSecureTemplate.replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK).replace(/__BUCKET_NAME__/g, templateName + "-v3").replace(/__SMART_CONTRACT_ADDRESS__/g, ECRunner[templateName][0]).replace(/__IMAGE_REGISTRY_ADDRESS__/g, ECRunner[templateName][1]).replace(/__RPC_URL__/g, ECRunner[templateName][2]).replace(/__CHAIN_ID__/g, ECRunner[templateName][3]).replace(/__TRUSTED_ZONE_IMAGE__/g, templateName);
162
+
163
+ // Amount of enclave heap to allocate (SCONE_HEAP). Kept in sync with the
164
+ // run/docker-compose securelock service; overridable via ECLD_MEMORY_TO_ALLOCATE.
165
+ const MEMORY_TO_ALLOCATE = (process.env.ECLD_MEMORY_TO_ALLOCATE || '1024M').trim();
166
+
167
+ // CRITICAL (mainnet DCAP): sign /usr/local/bin/node -- the binary the enclave
168
+ // actually EXECUTES (ENTRYPOINT + the run/publish compose command both run
169
+ // /usr/local/bin/node). Signing anything else leaves the executed binary as the
170
+ // base image's DEBUG-signed one, so at load SCONE recomputes MRENCLAVE and
171
+ // re-signs it as debug -> CAS rejects the DCAP quote ("Debug mode enabled").
172
+ //
173
+ // The enclave-creation params (--heap/--stack/--dlopen/--extensions) MUST be
174
+ // passed explicitly and match the runtime env exactly (scone-signer embeds SCONE
175
+ // defaults for anything not passed as a flag) -- any drift triggers the same
176
+ // debug re-sign. Values mirror the run/docker-compose securelock service.
177
+ const signFlags =
178
+ `--key=/enclave-key.pem --env --heap=${MEMORY_TO_ALLOCATE} ` +
179
+ `--stack=4M --dlopen=1 --extensions=/lib/libbinary-fs.so`;
180
+
181
+ // scone-signer prints two tab-indented "MRENCLAVE:" lines (non-EDMM first;
182
+ // runtime has EDMM disabled, so the first is what SCONE_HASH produces). Capture
183
+ // to a file first -- piping into head/grep kills scone-signer with SIGPIPE
184
+ // (exit 141) under buildkit's pipefail shell.
185
+ const signedMrenclaveStep =
186
+ 'RUN scone-signer info /usr/local/bin/node > /tmp/siginfo.txt 2>&1; \\\n' +
187
+ ' grep -iE "MRENCLAVE:" /tmp/siginfo.txt | sed -n \'1p\' \\\n' +
188
+ ' | sed -E \'s/.*MRENCLAVE:[[:space:]]*//I\' | tr -d \'[:space:]\' > /signed_mrenclave.txt && \\\n' +
189
+ ' echo "SIGNED_MRENCLAVE=$(cat /signed_mrenclave.txt)"';
128
190
 
129
191
  let imagesTag = process.env.BLOCKCHAIN_NETWORK.toLowerCase();
130
192
 
131
193
  if (isMainnet) {
132
- fs.writeFileSync('Dockerfile', dockerfileSecureContent.replace('# RUN scone-signer sign', 'RUN scone-signer sign'));
133
- imagesTag = process.env.BLOCKCHAIN_NETWORK.split("_")[0].toLowerCase()
194
+ // Mainnet: production sign the executed node binary + bake the signed MRENCLAVE.
195
+ dockerfileSecureContent = dockerfileSecureContent
196
+ .replace('__SCONE_SIGN__', `RUN scone-signer sign ${signFlags} --production /usr/local/bin/node`)
197
+ .replace('__SIGNED_MRENCLAVE__', signedMrenclaveStep);
198
+ imagesTag = process.env.BLOCKCHAIN_NETWORK.split("_")[0].toLowerCase();
199
+ } else {
200
+ // Testnet: non-CAS self-sign path -- no --production sign and no signed
201
+ // MRENCLAVE baking (the enclave self-signs from MR_ENCLAVE at runtime).
202
+ dockerfileSecureContent = dockerfileSecureContent
203
+ .replace('__SCONE_SIGN__', '# testnet: non-CAS self-sign (no --production sign at build time)')
204
+ .replace('__SIGNED_MRENCLAVE__', '# testnet: no signed MRENCLAVE (self-signed from MR_ENCLAVE at runtime)');
134
205
  }
135
206
 
207
+ fs.writeFileSync('Dockerfile', dockerfileSecureContent);
208
+
136
209
 
137
210
 
138
211