@ethernity-cloud/sdk-js 1.5.20 → 1.6.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/README.md CHANGED
@@ -39,7 +39,16 @@ The sdk has been tested on the following operating systems:
39
39
 
40
40
  ## Blockchain compatibility
41
41
  - Bloxberg:
42
- - Testnet - tested and working
42
+ - Testnet - tested and working. CAS-attested: the securelock is
43
+ provisioned by the ethernity-cas validator set exactly as on mainnet.
44
+ `ecld-publish` registers the securelock session on-chain in the
45
+ SessionRegistry (`0xcb1F389b...`, see `cas/config.js`), resolves a CAS
46
+ from the ValidatorRegistry (`0xC40102c0...`, or `ECLD_CAS_ADDR=host:port`)
47
+ for the compose and the certificate harvest, takes the trustedzone
48
+ session name from the ImageRegistry, and links the session to the
49
+ published image. `ecld-build` signs the securelock `--production`.
50
+ Harvesting the certificate needs SGX. The other testnets have no CAS
51
+ deployment and self-sign from MR_ENCLAVE as before.
43
52
  - Mainnet - to be provided during the following updates
44
53
  - Polyhon:
45
54
  - Amoy Testnet - to be provided during the following updates
package/cas/config.js ADDED
@@ -0,0 +1,36 @@
1
+ // ethernity-cas deployments, keyed by trustedzone template name (the value of
2
+ // TRUSTED_ZONE_IMAGE), for the networks whose enclaves are attested by the
3
+ // ethernity-cas validator set.
4
+ //
5
+ // A testnet template listed in SESSION_REGISTRY is CAS-attested: ecld-publish
6
+ // registers the securelock session there (body pinned to IPFS as
7
+ // CIDv1/raw/sha2-256) instead of posting it to a CAS, provisions the enclave
8
+ // from a validator resolved out of VALIDATOR_REGISTRY, and the securelock
9
+ // takes its certificate from the CAS session and verifies the CAS against the
10
+ // registry baked into it. A testnet template NOT listed here has no CAS: its
11
+ // securelock self-signs from MR_ENCLAVE. Mainnet templates are provisioned by
12
+ // the Scontain CAS and are not listed.
13
+ //
14
+ // Kept in sync with etny-{pynithy,nodenithy}/v3/networks.yaml
15
+ // (cas_session_registry_address, validator_registry_address).
16
+ const VALIDATOR_REGISTRY = {
17
+ 'etny-pynithy-testnet': '0xC40102c0b3f87663C925083861F38e2498C2038F',
18
+ 'etny-nodenithy-testnet': '0xC40102c0b3f87663C925083861F38e2498C2038F',
19
+ };
20
+
21
+ const SESSION_REGISTRY = {
22
+ 'etny-pynithy-testnet': '0xcb1F389bF4524d1D61EDcbC24eC1F1F9C3FF4Fa6',
23
+ 'etny-nodenithy-testnet': '0xcb1F389bF4524d1D61EDcbC24eC1F1F9C3FF4Fa6',
24
+ };
25
+
26
+ // The chain the registries above live on.
27
+ const CHAIN = {
28
+ 'etny-pynithy-testnet': { rpcUrl: 'https://bloxberg.ethernity.cloud', chainId: 8995 },
29
+ 'etny-nodenithy-testnet': { rpcUrl: 'https://bloxberg.ethernity.cloud', chainId: 8995 },
30
+ };
31
+
32
+ // Whether the securelock takes its certificate from a CAS session: mainnet
33
+ // (Scontain CAS), or a testnet template with a SessionRegistry (ethernity-cas).
34
+ const casProvisioned = (templateName, isMainnet) => isMainnet || Boolean(SESSION_REGISTRY[templateName]);
35
+
36
+ module.exports = { VALIDATOR_REGISTRY, SESSION_REGISTRY, CHAIN, casProvisioned };
@@ -0,0 +1,95 @@
1
+ // Which CAS a CAS-attested testnet's enclaves are provisioned from.
2
+ //
3
+ // The validators are enumerated from the ValidatorRegistry and their enclave
4
+ // endpoints read from the CasKeyStore the registry names via keyStore() --
5
+ // the same chain data the node's own resolver uses before every task. Only
6
+ // /dns4, /dns, /ip4 and /ip6 endpoints are dialled: the SDK runs on a
7
+ // developer's machine with no Tor proxy. An endpoint is taken only after its
8
+ // REST port answered GET /validator/identity.
9
+ //
10
+ // Multiaddrs advertise the ENCLAVE port; the REST port follows the pairing
11
+ // convention rest = 9081 + (enclave - 19765). Enclave ports outside
12
+ // [19765, 19965) fall back to REST 9081.
13
+ //
14
+ // ECLD_CAS_ADDR=host:port overrides resolution.
15
+ const axios = require('axios');
16
+ const { ethers } = require('ethers');
17
+
18
+ const REGISTRY_ABI = [
19
+ 'function validatorCount() view returns (uint256)',
20
+ 'function validatorSet(uint256) view returns (address)',
21
+ 'function isValidator(address) view returns (bool)',
22
+ 'function keyStore() view returns (address)',
23
+ ];
24
+ const STORE_ABI = ['function multiaddrsOf(address) view returns (string[])'];
25
+
26
+ const ENCLAVE_PORT_BASE = 19765;
27
+ const REST_PORT_BASE = 9081;
28
+
29
+ // "/dns4|dns|ip4|ip6/HOST/tcp/PORT" -> {host, port}; anything else null.
30
+ function parseMultiaddr(ma) {
31
+ const parts = String(ma).split('/').filter((p) => p !== '');
32
+ if (parts.length !== 4 || parts[2] !== 'tcp') return null;
33
+ if (!['dns4', 'dns6', 'dns', 'ip4', 'ip6'].includes(parts[0])) return null;
34
+ const port = Number(parts[3]);
35
+ if (!Number.isInteger(port)) return null;
36
+ return { host: parts[1], port };
37
+ }
38
+
39
+ function restPortFor(enclavePort) {
40
+ if (enclavePort >= ENCLAVE_PORT_BASE && enclavePort < ENCLAVE_PORT_BASE + 200) {
41
+ return REST_PORT_BASE + (enclavePort - ENCLAVE_PORT_BASE);
42
+ }
43
+ return REST_PORT_BASE;
44
+ }
45
+
46
+ // The address the CAS REST port reports for itself, or null when it did not answer.
47
+ async function identityAnswers(host, restPort, timeoutMs) {
48
+ try {
49
+ const r = await axios.get(`http://${host}:${restPort}/validator/identity`, { timeout: timeoutMs });
50
+ return (r.data && r.data.address) || null;
51
+ } catch (e) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ // "host:port" (the ENCLAVE port) of the first active validator whose REST port
57
+ // answers, sweeping from a rotating offset; null when none answered.
58
+ async function resolveCas(rpcUrl, registryAddress, probeTimeoutMs = 10000, startAt = null) {
59
+ const provider = new ethers.providers.JsonRpcProvider({ url: rpcUrl, timeout: 30000 });
60
+ const reg = new ethers.Contract(registryAddress, REGISTRY_ABI, provider);
61
+ const total = (await reg.validatorCount()).toNumber();
62
+ if (total === 0) return null;
63
+ const store = new ethers.Contract(await reg.keyStore(), STORE_ABI, provider);
64
+ const offset = startAt === null ? Math.floor(Math.random() * total) : startAt % total;
65
+ for (let step = 0; step < total; step++) {
66
+ const v = await reg.validatorSet((offset + step) % total);
67
+ if (!(await reg.isValidator(v))) continue;
68
+ for (const ma of await store.multiaddrsOf(v)) {
69
+ const parsed = parseMultiaddr(ma);
70
+ if (!parsed) continue;
71
+ if (await identityAnswers(parsed.host, restPortFor(parsed.port), probeTimeoutMs)) {
72
+ return `${parsed.host}:${parsed.port}`;
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ // The CAS to provision against: ECLD_CAS_ADDR when set, else resolved from
80
+ // chain. Throws when neither yields one -- a compose without a reachable CAS
81
+ // cannot harvest a certificate.
82
+ async function casAddressFor(rpcUrl, registryAddress) {
83
+ const override = String(process.env.ECLD_CAS_ADDR || '').trim();
84
+ if (override) return override;
85
+ if (!registryAddress) {
86
+ throw new Error('no ValidatorRegistry is known for this network and ECLD_CAS_ADDR is not set');
87
+ }
88
+ const resolved = await resolveCas(rpcUrl, registryAddress);
89
+ if (!resolved) {
90
+ throw new Error(`no CAS validator registered in ${registryAddress} answered on its REST port; set ECLD_CAS_ADDR=host:port to name one`);
91
+ }
92
+ return resolved;
93
+ }
94
+
95
+ module.exports = { parseMultiaddr, restPortFor, identityAnswers, resolveCas, casAddressFor };
@@ -0,0 +1,142 @@
1
+ // Register a securelock session ON-CHAIN, in the ethernity-cas SessionRegistry.
2
+ //
3
+ // On a CAS-attested testnet the CAS is a validator set that reads its sessions
4
+ // from the chain and never accepts a POST: the publisher pins the session body
5
+ // to IPFS and submits SessionRegistry.register itself, with the wallet that
6
+ // owns the ImageRegistry entry. The first registration of a name fixes its
7
+ // creator; later versions of the same name must come from that wallet.
8
+ //
9
+ // The body is pinned as CIDv1/raw/sha2-256, so with hashAlgo 1 the on-chain
10
+ // sessionHash and the CID commit to the same digest (sha256 of the exact
11
+ // bytes) -- what lets a validator check the body it fetched against the chain
12
+ // without trusting the gateway. The same recipe as etny-pynithy's
13
+ // v3/run/register_session.py and the Python SDK's session_registry.py.
14
+ const crypto = require('crypto');
15
+ const axios = require('axios');
16
+ const { ethers } = require('ethers');
17
+
18
+ const ABI = [
19
+ 'function register(bytes32 sessionHash, string name, string bodyCid, string imageCid, uint8 hashAlgo, (string[] tolerate, string[] ignoreAdvisories, string[] mrenclaves, uint16 minIsvSvn, bool debugAllowed, bool skipQuoteVerification) rulesIn) returns (uint32)',
20
+ 'function latest(string name) view returns (bytes32)',
21
+ 'function linkImage(bytes32 sessionHash, string imageCid)',
22
+ ];
23
+
24
+ const HASH_ALGO_SHA256 = 1;
25
+
26
+ function yamlList(text) {
27
+ let t = text.trim();
28
+ if (t.startsWith('[')) {
29
+ const end = t.indexOf(']');
30
+ t = end >= 0 ? t.slice(1, end) : t.slice(1);
31
+ }
32
+ return t.split(',').map((x) => x.trim().replace(/^["']|["']$/g, '')).filter((x) => x);
33
+ }
34
+
35
+ // The session's name and the SessionRules the registry stores beside it.
36
+ function parseNameAndRules(text) {
37
+ let name = '';
38
+ let tolerate = [], ignoreAdvisories = [], mrenclaves = [];
39
+ let minIsvSvn = 0, debugAllowed = false, skipQuoteVerification = false;
40
+ for (const raw of text.split('\n')) {
41
+ const line = raw.trim();
42
+ if (!name && line.startsWith('name:')) {
43
+ name = line.slice('name:'.length).trim().replace(/^["']|["']$/g, '');
44
+ } else if (line.startsWith('tolerate:')) {
45
+ tolerate = yamlList(line.slice('tolerate:'.length));
46
+ debugAllowed = tolerate.some((t) => t.toLowerCase().includes('debug'));
47
+ } else if (line.startsWith('ignore_advisories:')) {
48
+ ignoreAdvisories = yamlList(line.slice('ignore_advisories:'.length));
49
+ } else if (line.startsWith('mrenclaves:')) {
50
+ for (const m of yamlList(line.slice('mrenclaves:'.length))) {
51
+ if (m && !mrenclaves.includes(m)) mrenclaves.push(m);
52
+ }
53
+ } else if (line.startsWith('isvsvn:') || line.startsWith('min_isvsvn:')) {
54
+ const n = parseInt(line.split(':', 2)[1].trim(), 10);
55
+ if (!Number.isNaN(n)) minIsvSvn = n;
56
+ } else if (line.startsWith('skip_quote_verification:')) {
57
+ skipQuoteVerification = line.slice('skip_quote_verification:'.length).trim().replace(/^["']|["']$/g, '') === 'true';
58
+ }
59
+ }
60
+ return { name, rules: { tolerate, ignoreAdvisories, mrenclaves, minIsvSvn, debugAllowed, skipQuoteVerification } };
61
+ }
62
+
63
+ function sessionHash(body) {
64
+ return '0x' + crypto.createHash('sha256').update(body).digest('hex');
65
+ }
66
+
67
+ // Pin the exact bytes as CIDv1/raw/sha2-256 and return the CID.
68
+ async function pinBody(ipfsApiUrl, body) {
69
+ const form = new FormData();
70
+ form.append('file', new Blob([body]), 'session');
71
+ const r = await axios.post(`${ipfsApiUrl}/api/v0/add?cid-version=1&raw-leaves=true&pin=true`, form, { timeout: 60000 });
72
+ return r.data.Hash;
73
+ }
74
+
75
+ function contract(rpcUrl, registryAddress, key) {
76
+ const provider = new ethers.providers.JsonRpcProvider({ url: rpcUrl, timeout: 30000 });
77
+ const signer = key ? new ethers.Wallet(key.startsWith('0x') ? key : `0x${key}`, provider) : provider;
78
+ return new ethers.Contract(registryAddress, ABI, signer);
79
+ }
80
+
81
+ // Pin `body` and register it under its own `name:`. Returns
82
+ // {name, hash, cid, registered}: `registered` is false when the chain already
83
+ // held these exact bytes as the name's latest version.
84
+ async function register(rpcUrl, chainId, registryAddress, key, body, ipfsApiUrl, imageCid = '') {
85
+ const { name, rules } = parseNameAndRules(body.toString('utf8'));
86
+ if (!name) throw new Error('the session body has no `name:`');
87
+ const hash = sessionHash(body);
88
+ const reg = contract(rpcUrl, registryAddress, key);
89
+ const cid = await pinBody(ipfsApiUrl, body);
90
+ if ((await reg.latest(name)).toLowerCase() === hash) {
91
+ return { name, hash, cid, registered: false };
92
+ }
93
+ const tx = await reg.register(hash, name, cid, imageCid, HASH_ALGO_SHA256, rules, {
94
+ gasLimit: 800000, gasPrice: ethers.utils.parseUnits('1', 'mwei'), chainId,
95
+ });
96
+ const rcpt = await tx.wait();
97
+ if (rcpt.status !== 1) {
98
+ throw new Error(`SessionRegistry.register reverted for ${name} (tx ${tx.hash}): this name's creator may be another wallet, or the SessionRules shape differs from the ABI`);
99
+ }
100
+ return { name, hash, cid, registered: true };
101
+ }
102
+
103
+ // Point the name's latest version at the published image CID.
104
+ async function linkImage(rpcUrl, chainId, registryAddress, key, name, imageCid) {
105
+ const reg = contract(rpcUrl, registryAddress, key);
106
+ const hash = await reg.latest(name);
107
+ if (/^0x0+$/.test(hash)) throw new Error(`no registered session named ${name}`);
108
+ const tx = await reg.linkImage(hash, imageCid, { gasLimit: 300000, gasPrice: ethers.utils.parseUnits('1', 'mwei'), chainId });
109
+ const rcpt = await tx.wait();
110
+ if (rcpt.status !== 1) throw new Error(`SessionRegistry.linkImage reverted for ${name} (tx ${tx.hash})`);
111
+ }
112
+
113
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
114
+
115
+ // Block until the chain reports `name` at `hash`, then hold while the
116
+ // validators pick it up: the elected writer's poll (ECAS_SESSION_WATCH_SECS,
117
+ // 60 s) notices the registration and generates the secrets, its recordKey
118
+ // transaction confirms, and the other validators' next poll adopts the
119
+ // recorded blob. The enclave cannot retry and the validator it dials is the
120
+ // node's choice, so the wait covers the LAST validator to adopt.
121
+ async function waitVisible(rpcUrl, registryAddress, name, hash, timeoutMs = 180000) {
122
+ const reg = contract(rpcUrl, registryAddress, null);
123
+ const deadline = Date.now() + timeoutMs;
124
+ for (;;) {
125
+ let got;
126
+ try { got = (await reg.latest(name)).toLowerCase(); } catch (e) { got = `(read failed: ${e.message})`; }
127
+ if (got === hash.toLowerCase()) {
128
+ console.log(`\t✔ session ${name} visible on chain at ${got}`);
129
+ break;
130
+ }
131
+ if (Date.now() >= deadline) {
132
+ throw new Error(`session ${name} did not become visible within ${timeoutMs / 1000}s (chain reports ${got}, expected ${hash})`);
133
+ }
134
+ await sleep(5000);
135
+ }
136
+ const poll = parseInt(process.env.CAS_SESSION_POLL_SECS || '60', 10);
137
+ const hold = 2 * poll + 60;
138
+ console.log(`\t holding ${hold}s for the CAS validators to generate and mirror it`);
139
+ await sleep(hold * 1000);
140
+ }
141
+
142
+ module.exports = { parseNameAndRules, sessionHash, pinBody, register, linkImage, waitVisible, HASH_ALGO_SHA256 };
@@ -109,15 +109,18 @@ function deriveWalletAddress(identityPrivDer) {
109
109
  * testnet tradeoff — treat these wallets as disposable.
110
110
  *
111
111
  * @param {string} networkType "mainnet" | "testnet"
112
+ * @param {boolean} attested whether this build is provisioned by a CAS (its
113
+ * key comes from the CAS session): true on mainnet and on a testnet whose
114
+ * enclaves are attested by the ethernity-cas validator set
112
115
  * @returns {boolean}
113
116
  */
114
- function isSecretIdentity(networkType) {
117
+ function isSecretIdentity(networkType, attested = false) {
115
118
  const override = String(process.env.ESR_IDENTITY_SECRET || '').trim().toLowerCase();
116
119
  if (override === '1' || override === 'true' || override === 'yes') return true;
117
120
  if (override === '0' || override === 'false' || override === 'no') return false;
118
121
  // Secrecy tracks attestation, not the network name: mainnet is attested,
119
- // testnet is not. The override exists for a future attested-testnet build.
120
- return String(networkType || '').trim().toLowerCase() === 'mainnet';
122
+ // and so is a testnet whose securelock is provisioned by a CAS.
123
+ return attested || String(networkType || '').trim().toLowerCase() === 'mainnet';
121
124
  }
122
125
 
123
126
  const INSECURE_IDENTITY_WARNING =
@@ -111,16 +111,18 @@ class EtnySecureLock {
111
111
  * preferred (matching the node-side resolver). Returns REST base URLs.
112
112
  */
113
113
  async casEndpointsFromChain(casAddress) {
114
- const vrAbi = [
115
- 'function multiaddrsOf(address) view returns (string[])',
116
- 'function ensNamesOf(address) view returns (string[])',
117
- ];
118
- const vr = new ethers.Contract(this.validator_registry_address, vrAbi, this.w3);
114
+ // Endpoints live in the CasKeyStore the registry names via keyStore();
115
+ // the registry's own forwarders were removed when it reached the
116
+ // EIP-170 size limit.
117
+ const registryAbi = ['function keyStore() view returns (address)'];
118
+ const storeAbi = ['function multiaddrsOf(address) view returns (string[])'];
119
119
  let mas;
120
120
  try {
121
- mas = await vr.multiaddrsOf(casAddress);
121
+ const vr = new ethers.Contract(this.validator_registry_address, registryAbi, this.w3);
122
+ const store = new ethers.Contract(await vr.keyStore(), storeAbi, this.w3);
123
+ mas = await store.multiaddrsOf(casAddress);
122
124
  } catch (e) {
123
- console.warn(`CAS attestation: multiaddrsOf failed: ${e.message || e}`);
125
+ console.warn(`CAS attestation: reading the CAS endpoints from chain failed: ${e.message || e}`);
124
126
  return [];
125
127
  }
126
128
  const onion = [], rest = [];
@@ -323,7 +325,9 @@ class EtnySecureLock {
323
325
 
324
326
  const vrAbi = [
325
327
  'function isValidator(address) view returns (bool)',
326
- 'function validators(address) view returns (bytes32 mrenclave, bytes32 certHash, bool active, uint64 admittedBlock, uint64 lastVoteBlock)',
328
+ // The registry's Validator struct: the measurement is `bytes` at
329
+ // its platform width (32 for SGX MRENCLAVE), not bytes32.
330
+ 'function validators(address) view returns (bytes measurement, bytes32 certHash, bool active, uint64 admittedBlock, uint64 lastVoteBlock, uint16 teeType, address operator)',
327
331
  ];
328
332
  let active = false, record = null, lastErr = null;
329
333
  let casAddress = null, boundCert = null;
@@ -362,7 +366,7 @@ class EtnySecureLock {
362
366
  if (!active) {
363
367
  return fault(`CAS ${casAddress} is NOT an active validator`);
364
368
  }
365
- const registeredMr = record.mrenclave.replace(/^0x/, '');
369
+ const registeredMr = record.measurement.replace(/^0x/, '');
366
370
  if (registeredMr !== quoteMrenclave) {
367
371
  return fault(`CAS quote MRENCLAVE ${quoteMrenclave} != registered ${registeredMr}`);
368
372
  }
@@ -693,7 +697,11 @@ class EtnySecureLock {
693
697
  // Idempotent: the key is a pure function of the enclave identity, so it is
694
698
  // identical across restarts without needing to persist it.
695
699
  generateSelfSignedCertIfTestnet() {
696
- if (NETWORK_TYPE !== "testnet") return;
700
+ // A testnet build with a ValidatorRegistry baked in is CAS-provisioned
701
+ // (ethernity-cas): its key and cert are injected into key_file and
702
+ // cert_file by the CAS session, exactly as on mainnet, and the same
703
+ // registry is what verifyCasAttestation checks the CAS against.
704
+ if (NETWORK_TYPE !== "testnet" || this.casProvisioned()) return;
697
705
  console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
698
706
  const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
699
707
  console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
@@ -705,9 +713,17 @@ class EtnySecureLock {
705
713
  fs.writeFileSync(this.cert_file, certPem);
706
714
  }
707
715
 
716
+ // Whether this build's certificate comes from a CAS session: mainnet, or a
717
+ // testnet with a ValidatorRegistry baked in.
718
+ casProvisioned() {
719
+ const registry = String(this.validator_registry_address || '');
720
+ return NETWORK_TYPE === "mainnet" || (registry !== '' && !registry.startsWith('__'));
721
+ }
722
+
708
723
  // The enclave's identity private key, in whatever form this network provides
709
- // it: the in-memory PEM on testnet, or the CAS-injected key_file path on
710
- // mainnet. etny_crypto accepts either, so callers do not branch on network.
724
+ // it: the in-memory PEM on a self-signing testnet, or the CAS-injected
725
+ // key_file path otherwise. etny_crypto accepts either, so callers do not
726
+ // branch on network.
711
727
  privateKeyMaterial() {
712
728
  return this.identity_private_key_pem || this.key_file;
713
729
  }
@@ -750,7 +766,7 @@ class EtnySecureLock {
750
766
  || fs.readFileSync(this.key_file, 'utf8');
751
767
  const esrAddress = esr_wallet.deriveWalletAddress(identityPriv);
752
768
  console.log(`ESR_WALLET_ADDRESS: ${esrAddress}`);
753
- if (!esr_wallet.isSecretIdentity(NETWORK_TYPE)) {
769
+ if (!esr_wallet.isSecretIdentity(NETWORK_TYPE, this.casProvisioned())) {
754
770
  console.warn(esr_wallet.INSECURE_IDENTITY_WARNING);
755
771
  }
756
772
  } catch (e) {
@@ -1,6 +1,7 @@
1
1
  import shell from 'shelljs';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import { createRequire } from 'module';
4
5
  import dotenv from 'dotenv';
5
6
  dotenv.config();
6
7
 
@@ -56,10 +57,11 @@ export const ECRunner = {
56
57
  // can verify the CAS that provisions it (ECAS_CAS_QUOTE self-attestation).
57
58
  // Build-side on purpose: a rogue CAS must not choose the registry that judges
58
59
  // it. Missing key = no registry on that network; the enclave skips the check.
59
- export const VALIDATOR_REGISTRY = {
60
- 'etny-pynithy-testnet': '0xC4Fcd83743b76fB3081328cFe354De89995eaECD',
61
- 'etny-nodenithy-testnet': '0xC4Fcd83743b76fB3081328cFe354De89995eaECD',
62
- };
60
+ // Shared with publish (cas/config.js), which registers the session in the
61
+ // matching SessionRegistry.
62
+ const require = createRequire(import.meta.url);
63
+ const casConfig = require('../cas/config.js');
64
+ export const VALIDATOR_REGISTRY = casConfig.VALIDATOR_REGISTRY;
63
65
 
64
66
  const runCommand = (command, canPass = false) => {
65
67
  if (shell.exec(command).code !== 0 && !canPass) {
@@ -247,12 +249,14 @@ const signedMrenclaveStep =
247
249
 
248
250
  let imagesTag = process.env.BLOCKCHAIN_NETWORK.toLowerCase();
249
251
 
250
- if (isMainnet) {
251
- // Mainnet: production sign the executed node binary + bake the signed MRENCLAVE.
252
+ // A CAS-provisioned securelock (mainnet, or a testnet with a SessionRegistry)
253
+ // is signed --production: the CAS session admits production enclaves only,
254
+ // and a debug-signed one is refused at attestation.
255
+ if (casConfig.casProvisioned(templateName, isMainnet)) {
252
256
  dockerfileSecureContent = dockerfileSecureContent
253
257
  .replace('__SCONE_SIGN__', `RUN scone-signer sign ${signFlags} --production /usr/local/bin/node`)
254
258
  .replace('__SIGNED_MRENCLAVE__', signedMrenclaveStep);
255
- imagesTag = process.env.BLOCKCHAIN_NETWORK.split("_")[0].toLowerCase();
259
+ if (isMainnet) imagesTag = process.env.BLOCKCHAIN_NETWORK.split("_")[0].toLowerCase();
256
260
  } else {
257
261
  // Testnet: non-CAS self-sign path -- no --production sign and no signed
258
262
  // MRENCLAVE baking (the enclave self-signs from MR_ENCLAVE at runtime).
@@ -4,8 +4,13 @@ __PREDECESSOR__
4
4
 
5
5
  security:
6
6
  attestation:
7
- tolerate: [hyperthreading, outdated-tcb, software-hardening-needed, debug-mode]
8
- ignore_advisories: ["INTEL-SA-00220", "INTEL-SA-00270", "INTEL-SA-00293", "INTEL-SA-00320", "INTEL-SA-00329", "INTEL-SA-00334", "INTEL-SA-00381", "INTEL-SA-00389", "INTEL-SA-00477", "INTEL-SA-00614", "INTEL-SA-00615", "INTEL-SA-00617", "INTEL-SA-00828"]
7
+ # Both substituted per network by run.js. Mainnet attests production
8
+ # enclaves only and lists the advisories it accepts; a testnet tolerates a
9
+ # debug-signed enclave and takes the wildcard ["*"], so an operator's
10
+ # platform is not refused for a TCB level that has no bearing on a test
11
+ # network.
12
+ tolerate: __TOLERATE__
13
+ ignore_advisories: __IGNORE_ADVISORIES__
9
14
 
10
15
  services:
11
16
  - name: application
@@ -301,6 +301,15 @@ class ImageRegistry {
301
301
  console.log(public);
302
302
  return public;
303
303
  }
304
+ // The CAS session name recorded for the trustedzone template's latest
305
+ // image: what the compose's SCONE_CONFIG_ID for the trustedzone must name.
306
+ if (action === 'getTrustedZoneSession') {
307
+ const imageRegistry = new ImageRegistry();
308
+ const latest = await imageRegistry.imageRegistryContract.getLatestTrustedZoneImageCertPublicKey(projectName, version);
309
+ const session = await imageRegistry.imageRegistryContract.getTrustedZoneImageSession(latest[0]);
310
+ console.log(session);
311
+ return session;
312
+ }
304
313
 
305
314
  const imageRegistry = new ImageRegistry();
306
315
  if (action === 'registerSecureLockImage') {
package/nodenithy/run.js CHANGED
@@ -5,6 +5,9 @@ const readline = require('readline');
5
5
  const forge = require('node-forge');
6
6
  const axios = require('axios');
7
7
  const https = require('https');
8
+ const casConfig = require('../cas/config.js');
9
+ const casResolver = require('../cas/resolver.js');
10
+ const sessionRegistry = require('../cas/session_registry.js');
8
11
 
9
12
  if (!fs.existsSync('.env')) {
10
13
  console.error("Error: .env file not found");
@@ -58,6 +61,12 @@ const writeEnv = (key, value) => {
58
61
  let templateName = process.env.TRUSTED_ZONE_IMAGE || 'etny-nodenithy-testnet';
59
62
 
60
63
  const isMainnet = !templateName.includes('testnet');
64
+ // A testnet template with a SessionRegistry is CAS-attested by the
65
+ // ethernity-cas validator set: the session is registered on-chain, the
66
+ // enclave is provisioned from a validator, and the certificate comes out of
67
+ // the CAS session. Other testnets self-sign and register nothing.
68
+ const casTestnet = !isMainnet && casConfig.casProvisioned(templateName, isMainnet);
69
+ const casChain = casConfig.CHAIN[templateName] || {};
61
70
 
62
71
  const currentDir = process.cwd();
63
72
  console.log(`currentDir: ${currentDir}`);
@@ -125,7 +134,7 @@ const main = async () => {
125
134
  // from the signed binary -- and a runtime recompute yields a DEBUG enclave
126
135
  // that CAS rejects ("Debug mode is enabled"). Refuse to publish rather than
127
136
  // register an untrusted identity on-chain or fail opaquely later.
128
- if (isMainnet) {
137
+ if (isMainnet || casTestnet) {
129
138
  const signedMrenclave = extractSignedMrenclave('etny-securelock');
130
139
  if (!signedMrenclave || signedMrenclave !== process.env.MRENCLAVE_SECURELOCK) {
131
140
  console.error(`Error: securelock runtime MRENCLAVE (${process.env.MRENCLAVE_SECURELOCK}) != signed MRENCLAVE (${signedMrenclave}).`);
@@ -155,9 +164,15 @@ const main = async () => {
155
164
  const regex = new RegExp(`__${key}__`, 'g');
156
165
  content = content.replace(regex, value);
157
166
  }
158
- if (isMainnet) {
159
- content = content.replace(", debug-mode", "").replace('ignore_advisories: ["INTEL-SA-00220", "INTEL-SA-00270", "INTEL-SA-00293", "INTEL-SA-00320", "INTEL-SA-00329", "INTEL-SA-00334", "INTEL-SA-00381", "INTEL-SA-00389", "INTEL-SA-00477", "INTEL-SA-00614", "INTEL-SA-00615", "INTEL-SA-00617", "INTEL-SA-00828"]', '');
160
- }
167
+ // Mainnet attests production enclaves only; a testnet tolerates a
168
+ // debug-signed one. Both lines were edited by text replacement, which
169
+ // had to repeat the advisory list verbatim to delete it and broke the
170
+ // moment either string moved. Written out per network instead.
171
+ content = content
172
+ .replace(/__TOLERATE__/g, isMainnet
173
+ ? '[hyperthreading, outdated-tcb, software-hardening-needed]'
174
+ : '[hyperthreading, outdated-tcb, software-hardening-needed, debug-mode]')
175
+ .replace(/__IGNORE_ADVISORIES__/g, isMainnet ? '["INTEL-SA-00615"]' : '["*"]');
161
176
 
162
177
  fs.writeFileSync(outputFile, content);
163
178
  // console.log(`Contents of ${outputFile}:`);
@@ -208,7 +223,25 @@ const main = async () => {
208
223
  };
209
224
  processYamlTemplate('etny-securelock-test.yaml.tpl', 'etny-securelock-test.yaml', replacementsSecurelock);
210
225
 
211
-
226
+ // Where the session goes depends on who provisions the securelock. On a
227
+ // CAS-attested testnet it is registered ON-CHAIN in the ethernity-cas
228
+ // SessionRegistry, which the validator set reads and which accepts no
229
+ // POST; the publish then waits until the validators serve it, because the
230
+ // public-key harvest below provisions the enclave from them. Every other
231
+ // network keeps the Scontain CAS registration that follows.
232
+ if (casTestnet) {
233
+ const registered = await sessionRegistry.register(
234
+ casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
235
+ process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
236
+ process.env.IPFS_ENDPOINT, '');
237
+ if (registered.registered) {
238
+ console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
239
+ await sessionRegistry.waitVisible(
240
+ casChain.rpcUrl, casConfig.SESSION_REGISTRY[templateName], registered.name, registered.hash);
241
+ } else {
242
+ console.log(`\t✔ Session ${registered.name} already registered on-chain with this body (${registered.hash})`);
243
+ }
244
+ } else {
212
245
  // don't generate new keys if PREDECESSOR_HASH_SECURELOCK is not empty and the key.pem and cert.pem files exist
213
246
  if (PREDECESSOR_HASH_SECURELOCK !== 'EMPTY' && fs.existsSync('key.pem') && fs.existsSync('cert.pem')) {
214
247
  console.log("Skipping keypair generation and certificate creation.");
@@ -322,6 +355,7 @@ const main = async () => {
322
355
  console.log("Please change the name/version of your project (using ecld-init or by editing .env file) and run the scripts again. Exiting.");
323
356
  process.exit(1);
324
357
  });
358
+ }
325
359
 
326
360
 
327
361
  // const ENCLAVE_NAME_TRUSTEDZONE = generateEnclaveName(process.env.ENCLAVE_NAME_TRUSTEDZONE);
@@ -356,6 +390,23 @@ const main = async () => {
356
390
 
357
391
  console.log("# Update docker-compose files");
358
392
 
393
+ // On a CAS-attested testnet the compose names the CAS both enclaves are
394
+ // provisioned from (a validator resolved from the registry, or
395
+ // ECLD_CAS_ADDR; the node re-resolves it before each task) and the
396
+ // trustedzone session the ImageRegistry records for this template.
397
+ let casAddr = null;
398
+ let trustedZoneSession = null;
399
+ if (casTestnet) {
400
+ casAddr = await casResolver.casAddressFor(casChain.rpcUrl, casConfig.VALIDATOR_REGISTRY[templateName]);
401
+ console.log(`\t✔ CAS for this network: ${casAddr}`);
402
+ trustedZoneSession = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "" "getTrustedZoneSession"`).toString().trim();
403
+ if (!trustedZoneSession) {
404
+ console.error(`ERROR! The ImageRegistry records no trustedzone session for ${templateName}.`);
405
+ process.exit(1);
406
+ }
407
+ console.log(`\t✔ Trustedzone session: ${trustedZoneSession}`);
408
+ }
409
+
359
410
  const files = ['docker-compose.yml', 'docker-compose-final.yml'];
360
411
 
361
412
  files.forEach(file => {
@@ -385,9 +436,15 @@ const main = async () => {
385
436
  if (isMainnet) {
386
437
  ENCLAVE_NAME_TRUSTEDZONE = 'ecld-nodenithy-trustedzone-v3-3.0.0'
387
438
  }
388
- const updatedContent = fileContentBefore
439
+ if (casTestnet) {
440
+ ENCLAVE_NAME_TRUSTEDZONE = trustedZoneSession;
441
+ }
442
+ let updatedContent = fileContentBefore
389
443
  .replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK)
390
444
  .replace(/__ENCLAVE_NAME_TRUSTEDZONE__/g, ENCLAVE_NAME_TRUSTEDZONE);
445
+ if (casTestnet) {
446
+ updatedContent = updatedContent.replace(/SCONE_CAS_ADDR=scone-cas\.cf/g, `SCONE_CAS_ADDR=${casAddr}`);
447
+ }
391
448
 
392
449
  fs.writeFileSync(file, updatedContent, 'utf8');
393
450
 
@@ -596,6 +653,14 @@ const main = async () => {
596
653
  if (!existing) {
597
654
  const res = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "" "" "" "registerSecureLockImage"`, { stdio: "inherit" });
598
655
  }
656
+ // The on-chain session points at the image it admits, so a validator can
657
+ // pin the image beside the body it serves.
658
+ if (casTestnet) {
659
+ await sessionRegistry.linkImage(
660
+ casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
661
+ process.env.PRIVATE_KEY, ENCLAVE_NAME_SECURELOCK, process.env.IPFS_HASH);
662
+ console.log(`\t✔ Session ${ENCLAVE_NAME_SECURELOCK} linked to image ${process.env.IPFS_HASH}`);
663
+ }
599
664
  console.log("Script completed successfully. You can start testing the application now. (eg. npm run start)");
600
665
  process.exit(0);
601
666
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.5.20",
3
+ "version": "1.6.0",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
6
  "ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
@@ -23,6 +23,7 @@
23
23
  "files": [
24
24
  "nodenithy/*",
25
25
  "pynithy/*",
26
+ "cas/*",
26
27
  "build.js",
27
28
  "init.js",
28
29
  "publish.js",
@@ -214,17 +214,25 @@ class EtnySecureLock:
214
214
  itself. /onion3 is preferred, then the rest, matching the node-side
215
215
  resolver. Returns REST base URLs.
216
216
  """
217
- vr_abi = [
217
+ # Endpoints live in the CasKeyStore the registry names via keyStore();
218
+ # the registry's own forwarders were removed when it reached the
219
+ # EIP-170 size limit.
220
+ registry_abi = [
221
+ {"name": "keyStore", "type": "function", "stateMutability": "view",
222
+ "inputs": [], "outputs": [{"type": "address"}]},
223
+ ]
224
+ store_abi = [
218
225
  {"name": "multiaddrsOf", "type": "function", "stateMutability": "view",
219
226
  "inputs": [{"type": "address"}], "outputs": [{"type": "string[]"}]},
220
227
  ]
221
- vr = self.w3.eth.contract(
222
- address=self.w3.toChecksumAddress(self.validator_registry_address),
223
- abi=vr_abi)
224
228
  try:
225
- mas = vr.caller().multiaddrsOf(self.w3.toChecksumAddress(cas_address))
229
+ vr = self.w3.eth.contract(
230
+ address=self.w3.toChecksumAddress(self.validator_registry_address),
231
+ abi=registry_abi)
232
+ store = self.w3.eth.contract(address=vr.caller().keyStore(), abi=store_abi)
233
+ mas = store.caller().multiaddrsOf(self.w3.toChecksumAddress(cas_address))
226
234
  except Exception as e:
227
- print(f'CAS attestation: multiaddrsOf failed: {e}')
235
+ print(f'CAS attestation: reading the CAS endpoints from chain failed: {e}')
228
236
  return []
229
237
  onion, rest = [], []
230
238
  for ma in mas:
@@ -378,13 +386,17 @@ class EtnySecureLock:
378
386
  vr_abi = [
379
387
  {"name": "isValidator", "type": "function", "stateMutability": "view",
380
388
  "inputs": [{"type": "address"}], "outputs": [{"type": "bool"}]},
389
+ # The registry's Validator struct: the measurement is `bytes` at
390
+ # its platform width (32 for SGX MRENCLAVE), not bytes32.
381
391
  {"name": "validators", "type": "function", "stateMutability": "view",
382
392
  "inputs": [{"type": "address"}],
383
- "outputs": [{"name": "mrenclave", "type": "bytes32"},
393
+ "outputs": [{"name": "measurement", "type": "bytes"},
384
394
  {"name": "certHash", "type": "bytes32"},
385
395
  {"name": "active", "type": "bool"},
386
396
  {"name": "admittedBlock", "type": "uint64"},
387
- {"name": "lastVoteBlock", "type": "uint64"}]},
397
+ {"name": "lastVoteBlock", "type": "uint64"},
398
+ {"name": "teeType", "type": "uint16"},
399
+ {"name": "operator", "type": "address"}]},
388
400
  ]
389
401
  last_err = None
390
402
  cas_address, bound_cert, active, record = None, None, False, None
package/pynithy/build.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import shell from 'shelljs';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import { createRequire } from 'module';
4
5
  import dotenv from 'dotenv';
5
6
  dotenv.config();
6
7
 
@@ -56,10 +57,11 @@ export const ECRunner = {
56
57
  // can verify the CAS that provisions it (ECAS_CAS_QUOTE self-attestation).
57
58
  // Build-side on purpose: a rogue CAS must not choose the registry that judges
58
59
  // it. Missing key = no registry on that network; the enclave skips the check.
59
- export const VALIDATOR_REGISTRY = {
60
- 'etny-pynithy-testnet': '0xC4Fcd83743b76fB3081328cFe354De89995eaECD',
61
- 'etny-nodenithy-testnet': '0xC4Fcd83743b76fB3081328cFe354De89995eaECD',
62
- };
60
+ // Shared with publish (cas/config.js), which registers the session in the
61
+ // matching SessionRegistry.
62
+ const require = createRequire(import.meta.url);
63
+ const casConfig = require('../cas/config.js');
64
+ export const VALIDATOR_REGISTRY = casConfig.VALIDATOR_REGISTRY;
63
65
 
64
66
  const runCommand = (command, canPass = false) => {
65
67
  if (shell.exec(command).code !== 0 && !canPass) {
@@ -155,11 +157,14 @@ const signedMrenclaveStep =
155
157
 
156
158
  let imagesTag = process.env.BLOCKCHAIN_NETWORK.toLowerCase();
157
159
 
158
- if (isMainnet) {
160
+ // A CAS-provisioned securelock (mainnet, or a testnet with a SessionRegistry)
161
+ // is signed --production: the CAS session admits production enclaves only,
162
+ // and a debug-signed one is refused at attestation.
163
+ if (casConfig.casProvisioned(templateName, isMainnet)) {
159
164
  dockerfileSecureContent = dockerfileSecureContent
160
165
  .replace('__SCONE_SIGN__', `RUN scone-signer sign ${signFlags} --production /usr/local/bin/python`)
161
166
  .replace('__SIGNED_MRENCLAVE__', signedMrenclaveStep);
162
- imagesTag = process.env.BLOCKCHAIN_NETWORK.split("_")[0].toLowerCase();
167
+ if (isMainnet) imagesTag = process.env.BLOCKCHAIN_NETWORK.split("_")[0].toLowerCase();
163
168
  } else {
164
169
  dockerfileSecureContent = dockerfileSecureContent
165
170
  .replace('__SCONE_SIGN__', '# testnet: non-CAS self-sign (no --production sign at build time)')
@@ -4,8 +4,14 @@ __PREDECESSOR__
4
4
 
5
5
  security:
6
6
  attestation:
7
- tolerate: [debug-mode, hyperthreading, outdated-tcb, software-hardening-needed, debug-mode]
8
- ignore_advisories: "*"
7
+ # Both substituted per network by run.js. Mainnet attests production
8
+ # enclaves only and lists the advisories it accepts; a testnet tolerates a
9
+ # debug-signed enclave and takes the wildcard ["*"], so an operator's
10
+ # platform is not refused for a TCB level that has no bearing on a test
11
+ # network. (A bare "*" is not a list: the CAS reads it as a literal
12
+ # advisory id, which ignores nothing.)
13
+ tolerate: __TOLERATE__
14
+ ignore_advisories: __IGNORE_ADVISORIES__
9
15
 
10
16
  services:
11
17
  - name: application
@@ -301,6 +301,15 @@ class ImageRegistry {
301
301
  console.log(public);
302
302
  return public;
303
303
  }
304
+ // The CAS session name recorded for the trustedzone template's latest
305
+ // image: what the compose's SCONE_CONFIG_ID for the trustedzone must name.
306
+ if (action === 'getTrustedZoneSession') {
307
+ const imageRegistry = new ImageRegistry();
308
+ const latest = await imageRegistry.imageRegistryContract.getLatestTrustedZoneImageCertPublicKey(projectName, version);
309
+ const session = await imageRegistry.imageRegistryContract.getTrustedZoneImageSession(latest[0]);
310
+ console.log(session);
311
+ return session;
312
+ }
304
313
 
305
314
  const imageRegistry = new ImageRegistry();
306
315
  if (action === 'registerSecureLockImage') {
package/pynithy/run.js CHANGED
@@ -5,6 +5,9 @@ const readline = require('readline');
5
5
  const forge = require('node-forge');
6
6
  const axios = require('axios');
7
7
  const https = require('https');
8
+ const casConfig = require('../cas/config.js');
9
+ const casResolver = require('../cas/resolver.js');
10
+ const sessionRegistry = require('../cas/session_registry.js');
8
11
 
9
12
  if (!fs.existsSync('.env')) {
10
13
  console.error("Error: .env file not found");
@@ -58,6 +61,12 @@ const writeEnv = (key, value) => {
58
61
  let templateName = process.env.TRUSTED_ZONE_IMAGE || 'etny-pynithy-testnet';
59
62
 
60
63
  const isMainnet = !templateName.includes('testnet');
64
+ // A testnet template with a SessionRegistry is CAS-attested by the
65
+ // ethernity-cas validator set: the session is registered on-chain, the
66
+ // enclave is provisioned from a validator, and the certificate comes out of
67
+ // the CAS session. Other testnets self-sign and register nothing.
68
+ const casTestnet = !isMainnet && casConfig.casProvisioned(templateName, isMainnet);
69
+ const casChain = casConfig.CHAIN[templateName] || {};
61
70
 
62
71
  const currentDir = process.cwd();
63
72
  console.log(`currentDir: ${currentDir}`);
@@ -121,7 +130,7 @@ const main = async () => {
121
130
  // MRENCLAVE signed --production at build time (/signed_mrenclave.txt). A
122
131
  // mismatch means SCONE recomputed the measurement at load (params drift) and
123
132
  // re-signed as DEBUG, which CAS rejects on mainnet. Refuse to publish.
124
- if (isMainnet) {
133
+ if (isMainnet || casTestnet) {
125
134
  const signedMrenclave = extractSignedMrenclave('etny-securelock');
126
135
  if (!signedMrenclave || signedMrenclave !== process.env.MRENCLAVE_SECURELOCK) {
127
136
  console.error(`Error: securelock runtime MRENCLAVE (${process.env.MRENCLAVE_SECURELOCK}) != signed MRENCLAVE (${signedMrenclave}).`);
@@ -151,9 +160,16 @@ const main = async () => {
151
160
  const regex = new RegExp(`__${key}__`, 'g');
152
161
  content = content.replace(regex, value);
153
162
  }
154
- if (isMainnet) {
155
- content = content.replace(", debug-mode", "");
156
- }
163
+ // Mainnet attests production enclaves only; a testnet tolerates a
164
+ // debug-signed one. The tolerate list was edited by text replacement
165
+ // ('", debug-mode"'), which never matched this template's LEADING
166
+ // "debug-mode," -- so mainnet sessions were published tolerating debug
167
+ // enclaves. Written out per network instead.
168
+ content = content
169
+ .replace(/__TOLERATE__/g, isMainnet
170
+ ? '[hyperthreading, outdated-tcb, software-hardening-needed]'
171
+ : '[debug-mode, hyperthreading, outdated-tcb, software-hardening-needed]')
172
+ .replace(/__IGNORE_ADVISORIES__/g, isMainnet ? '["INTEL-SA-00615"]' : '["*"]');
157
173
 
158
174
  fs.writeFileSync(outputFile, content);
159
175
  // console.log(`Contents of ${outputFile}:`);
@@ -204,7 +220,25 @@ const main = async () => {
204
220
  };
205
221
  processYamlTemplate('etny-securelock-test.yaml.tpl', 'etny-securelock-test.yaml', replacementsSecurelock);
206
222
 
207
-
223
+ // Where the session goes depends on who provisions the securelock. On a
224
+ // CAS-attested testnet it is registered ON-CHAIN in the ethernity-cas
225
+ // SessionRegistry, which the validator set reads and which accepts no
226
+ // POST; the publish then waits until the validators serve it, because the
227
+ // public-key harvest below provisions the enclave from them. Every other
228
+ // network keeps the Scontain CAS registration that follows.
229
+ if (casTestnet) {
230
+ const registered = await sessionRegistry.register(
231
+ casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
232
+ process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
233
+ process.env.IPFS_ENDPOINT, '');
234
+ if (registered.registered) {
235
+ console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
236
+ await sessionRegistry.waitVisible(
237
+ casChain.rpcUrl, casConfig.SESSION_REGISTRY[templateName], registered.name, registered.hash);
238
+ } else {
239
+ console.log(`\t✔ Session ${registered.name} already registered on-chain with this body (${registered.hash})`);
240
+ }
241
+ } else {
208
242
  // don't generate new keys if PREDECESSOR_HASH_SECURELOCK is not empty and the key.pem and cert.pem files exist
209
243
  if (PREDECESSOR_HASH_SECURELOCK !== 'EMPTY' && fs.existsSync('key.pem') && fs.existsSync('cert.pem')) {
210
244
  console.log("Skipping key pair generation and certificate creation.");
@@ -318,6 +352,7 @@ const main = async () => {
318
352
  console.log("Please change the name/version of your project (using ecld-init or by editing .env file) and run the scripts again. Exiting.");
319
353
  process.exit(1);
320
354
  });
355
+ }
321
356
 
322
357
 
323
358
  // const ENCLAVE_NAME_TRUSTEDZONE = generateEnclaveName(process.env.ENCLAVE_NAME_TRUSTEDZONE);
@@ -352,6 +387,23 @@ const main = async () => {
352
387
 
353
388
  console.log("# Update docker-compose files");
354
389
 
390
+ // On a CAS-attested testnet the compose names the CAS both enclaves are
391
+ // provisioned from (a validator resolved from the registry, or
392
+ // ECLD_CAS_ADDR; the node re-resolves it before each task) and the
393
+ // trustedzone session the ImageRegistry records for this template.
394
+ let casAddr = null;
395
+ let trustedZoneSession = null;
396
+ if (casTestnet) {
397
+ casAddr = await casResolver.casAddressFor(casChain.rpcUrl, casConfig.VALIDATOR_REGISTRY[templateName]);
398
+ console.log(`\t✔ CAS for this network: ${casAddr}`);
399
+ trustedZoneSession = execSync(`node ./image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" ${templateName} "v3" "" "getTrustedZoneSession"`).toString().trim();
400
+ if (!trustedZoneSession) {
401
+ console.error(`ERROR! The ImageRegistry records no trustedzone session for ${templateName}.`);
402
+ process.exit(1);
403
+ }
404
+ console.log(`\t✔ Trustedzone session: ${trustedZoneSession}`);
405
+ }
406
+
355
407
  const files = ['docker-compose.yml', 'docker-compose-final.yml'];
356
408
 
357
409
  files.forEach(file => {
@@ -381,9 +433,15 @@ const main = async () => {
381
433
  if (isMainnet) {
382
434
  ENCLAVE_NAME_TRUSTEDZONE = 'ecld-pynithy-trustedzone-v3-3.0.0'
383
435
  }
384
- const updatedContent = fileContentBefore
436
+ if (casTestnet) {
437
+ ENCLAVE_NAME_TRUSTEDZONE = trustedZoneSession;
438
+ }
439
+ let updatedContent = fileContentBefore
385
440
  .replace(/__ENCLAVE_NAME_SECURELOCK__/g, ENCLAVE_NAME_SECURELOCK)
386
441
  .replace(/__ENCLAVE_NAME_TRUSTEDZONE__/g, ENCLAVE_NAME_TRUSTEDZONE);
442
+ if (casTestnet) {
443
+ updatedContent = updatedContent.replace(/SCONE_CAS_ADDR=scone-cas\.cf/g, `SCONE_CAS_ADDR=${casAddr}`);
444
+ }
387
445
 
388
446
  fs.writeFileSync(file, updatedContent, 'utf8');
389
447
 
@@ -570,6 +628,14 @@ const main = async () => {
570
628
  if (!existing) {
571
629
  const res = execSync(`node ${runDir}/image_registry.js "${process.env.BLOCKCHAIN_NETWORK}" "" "" "" "registerSecureLockImage"`, { stdio: "inherit" });
572
630
  }
631
+ // The on-chain session points at the image it admits, so a validator can
632
+ // pin the image beside the body it serves.
633
+ if (casTestnet) {
634
+ await sessionRegistry.linkImage(
635
+ casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
636
+ process.env.PRIVATE_KEY, ENCLAVE_NAME_SECURELOCK, process.env.IPFS_HASH);
637
+ console.log(`\t✔ Session ${ENCLAVE_NAME_SECURELOCK} linked to image ${process.env.IPFS_HASH}`);
638
+ }
573
639
  console.log("Script completed successfully. You can start testing the application now. (eg. npm run start)");
574
640
  process.exit(0);
575
641
  };