@ethernity-cloud/sdk-js 1.4.0 → 1.5.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/init.js +49 -0
- package/nodenithy/build/securelock/Dockerfile.tmpl +5 -0
- package/nodenithy/build/securelock/src/ecld_state.js +304 -0
- package/nodenithy/build/securelock/src/esr_wallet.js +147 -0
- package/nodenithy/build/securelock/src/etny_crypto.js +211 -201
- package/nodenithy/build/securelock/src/etny_exec.js +22 -0
- package/nodenithy/build/securelock/src/securelock.js.tmpl +115 -12
- package/nodenithy/build/securelock/src/task_status.js +4 -0
- package/nodenithy/build.mjs +19 -1
- package/nodenithy/run/public_key_service.js +11 -0
- package/nodenithy/run.js +22 -0
- package/package.json +49 -49
|
@@ -1,201 +1,211 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const elliptic = require('elliptic');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const rs = require('jsrsasign');
|
|
5
|
-
const ECKey = require('ec-key');
|
|
6
|
-
const curve = new elliptic.ec('p384');
|
|
7
|
-
const ascii85 = require('ascii85');
|
|
8
|
-
const ethSigUtil = require('@metamask/eth-sig-util');
|
|
9
|
-
const {createHash} = require('crypto');
|
|
10
|
-
|
|
11
|
-
function sha256(content) {
|
|
12
|
-
return createHash('sha256').update(content).digest('hex');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function fileChecksum(file) {
|
|
16
|
-
const fileContent = fs.readFileSync(file, 'utf8');
|
|
17
|
-
return sha256(fileContent);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function fileChecksumFromSwiftStream(swiftStreamClient, bucketName, fileName) {
|
|
21
|
-
const fileContent = await swiftStreamClient.getFileContent(bucketName, fileName);
|
|
22
|
-
//console.log(fileContent);
|
|
23
|
-
return sha256(fileContent);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const getPEMFromPublicFile = (publicKeyPath) => {
|
|
27
|
-
const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
28
|
-
return crypto.createPublicKey(publicKey).export({type: 'spki', format: 'pem'});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const getPEMFromPublic = (publicKey) => {
|
|
32
|
-
return crypto.createPublicKey(publicKey).export({type: 'spki', format: 'pem'});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const loadCertificate = (certificate) => {
|
|
36
|
-
//console.log(certificate)
|
|
37
|
-
const publicKeyPEM = getPEMFromPublic(certificate);
|
|
38
|
-
const key = new ECKey(publicKeyPEM, 'pem');
|
|
39
|
-
//console.log(key);
|
|
40
|
-
return key;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// We
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const elliptic = require('elliptic');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const rs = require('jsrsasign');
|
|
5
|
+
const ECKey = require('ec-key');
|
|
6
|
+
const curve = new elliptic.ec('p384');
|
|
7
|
+
const ascii85 = require('ascii85');
|
|
8
|
+
const ethSigUtil = require('@metamask/eth-sig-util');
|
|
9
|
+
const {createHash} = require('crypto');
|
|
10
|
+
|
|
11
|
+
function sha256(content) {
|
|
12
|
+
return createHash('sha256').update(content).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fileChecksum(file) {
|
|
16
|
+
const fileContent = fs.readFileSync(file, 'utf8');
|
|
17
|
+
return sha256(fileContent);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fileChecksumFromSwiftStream(swiftStreamClient, bucketName, fileName) {
|
|
21
|
+
const fileContent = await swiftStreamClient.getFileContent(bucketName, fileName);
|
|
22
|
+
//console.log(fileContent);
|
|
23
|
+
return sha256(fileContent);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getPEMFromPublicFile = (publicKeyPath) => {
|
|
27
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
|
28
|
+
return crypto.createPublicKey(publicKey).export({type: 'spki', format: 'pem'});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getPEMFromPublic = (publicKey) => {
|
|
32
|
+
return crypto.createPublicKey(publicKey).export({type: 'spki', format: 'pem'});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const loadCertificate = (certificate) => {
|
|
36
|
+
//console.log(certificate)
|
|
37
|
+
const publicKeyPEM = getPEMFromPublic(certificate);
|
|
38
|
+
const key = new ECKey(publicKeyPEM, 'pem');
|
|
39
|
+
//console.log(key);
|
|
40
|
+
return key;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Accepts EITHER a path to a PEM file or the PEM text itself.
|
|
44
|
+
//
|
|
45
|
+
// mainnet keeps passing a path: CAS injects the key into key_file after
|
|
46
|
+
// attestation, and that flow is unchanged. On testnet the enclave self-derives
|
|
47
|
+
// its key and now holds it only in memory (never writes it), so it passes the
|
|
48
|
+
// PEM straight through -- nothing about the key ever reaches the filesystem.
|
|
49
|
+
// Detection is on the PEM header, not on file existence, so a path that does
|
|
50
|
+
// not exist still fails loudly instead of being mistaken for key material.
|
|
51
|
+
function getPrivatePointFromPrivateKey(privateKeyOrFile) {
|
|
52
|
+
const value = String(privateKeyOrFile || '');
|
|
53
|
+
const privateKey = value.includes('-----BEGIN')
|
|
54
|
+
? value
|
|
55
|
+
: fs.readFileSync(value, 'utf8');
|
|
56
|
+
let privateKeyHex = rs.KEYUTIL.getKey(privateKey).prvKeyHex;
|
|
57
|
+
const privateKeyObject = curve.keyFromPrivate(privateKeyHex, 'hex');
|
|
58
|
+
return privateKeyObject.getPrivate();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const generatePublicKey = (x, y) => {
|
|
62
|
+
const pub = {x: x, y};
|
|
63
|
+
return curve.keyFromPublic(pub, 'hex');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPublicPointFromCoords(x, y) {
|
|
67
|
+
const publicKey = generatePublicKey(x, y);
|
|
68
|
+
return publicKey.getPublic();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ecc_point_to_256_bit_key(point) {
|
|
72
|
+
const value = point.getX().toString() + point.getY().toString();
|
|
73
|
+
const hash = crypto.createHash('sha256');
|
|
74
|
+
hash.update(value);
|
|
75
|
+
return hash.digest();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function encryptedDataToBase64Json(encryptedMsg) {
|
|
79
|
+
const key = curve.keyFromPublic(encryptedMsg.cipherTextPublicKey, 'hex');
|
|
80
|
+
const jsonObj = {
|
|
81
|
+
ciphertext: encryptedMsg.ciphertext.toString('hex'),
|
|
82
|
+
nonce: encryptedMsg.nonce.toString('hex'),
|
|
83
|
+
authTag: encryptedMsg.authTag.toString('hex'),
|
|
84
|
+
x: key.getPublic().getX(),
|
|
85
|
+
y: key.getPublic().getY()
|
|
86
|
+
};
|
|
87
|
+
const jsonString = JSON.stringify(jsonObj);
|
|
88
|
+
const binaryData = Buffer.from(jsonString, 'utf8');
|
|
89
|
+
return binaryData.toString('base64');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function encryptedDataFromBase64Json(base64Data) {
|
|
93
|
+
// decode the base64-encoded string back to a JSON string
|
|
94
|
+
const decodedJsonData = Buffer.from(base64Data, 'base64').toString('utf8');
|
|
95
|
+
return JSON.parse(decodedJsonData);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
function encrypt(certificate, message) {
|
|
100
|
+
const key = loadCertificate(certificate);
|
|
101
|
+
return encrypt_ecc(message, generatePublicKey(key.x, key.y));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function encrypt_ecc(msg, publicKey) {
|
|
105
|
+
const cipherTextPrivateKey = crypto.randomBytes(32);
|
|
106
|
+
const sharedEccKey = publicKey.getPublic().mul(cipherTextPrivateKey);
|
|
107
|
+
const secretKey = ecc_point_to_256_bit_key(sharedEccKey);
|
|
108
|
+
const iv = crypto.randomBytes(16);
|
|
109
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv);
|
|
110
|
+
let encrypted = cipher.update(msg, 'utf8', 'hex');
|
|
111
|
+
encrypted += cipher.final('hex');
|
|
112
|
+
const cipherTextPublicKey = curve.g.mul(cipherTextPrivateKey);
|
|
113
|
+
return {
|
|
114
|
+
ciphertext: encrypted,
|
|
115
|
+
secretKey: secretKey,
|
|
116
|
+
nonce: iv,
|
|
117
|
+
authTag: cipher.getAuthTag().toString("hex"),
|
|
118
|
+
cipherTextPublicKey: cipherTextPublicKey.encode('hex')
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function decrypt(privateKeyFile, encryptedMsg) {
|
|
123
|
+
//console.log(privateKeyFile);
|
|
124
|
+
const privatePoint = getPrivatePointFromPrivateKey(privateKeyFile);
|
|
125
|
+
return decrypt_ecc(encryptedMsg, privatePoint);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function decrypt_ecc(encryptedMsg, privatePoint) {
|
|
129
|
+
try {
|
|
130
|
+
const {ciphertext, nonce, authTag, x, y} = encryptedMsg;
|
|
131
|
+
const publicPoint = getPublicPointFromCoords(`${x}`, `${y}`);
|
|
132
|
+
const sharedEccKey = publicPoint.mul(privatePoint);
|
|
133
|
+
const secretKey = ecc_point_to_256_bit_key(sharedEccKey);
|
|
134
|
+
|
|
135
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, Buffer.from(nonce, 'hex'));
|
|
136
|
+
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
|
|
137
|
+
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
|
|
138
|
+
decrypted += decipher.final('utf8');
|
|
139
|
+
return decrypted;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.log(`Error while decrypting ECC:`, e.message);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hexToBytes(hexStr) {
|
|
147
|
+
if (hexStr.startsWith("0x")) {
|
|
148
|
+
hexStr = hexStr.slice(2);
|
|
149
|
+
}
|
|
150
|
+
return Buffer.from(hexStr, "hex");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function encryptWithPublicKey(publicKey, data) {
|
|
154
|
+
// Returned object contains 4 properties: version, ephemPublicKey, nonce, ciphertext
|
|
155
|
+
// Each contains data encoded using base64, version is always the same string
|
|
156
|
+
const enc = ethSigUtil.encrypt({
|
|
157
|
+
publicKey: hexToBytes(publicKey).toString('base64'),
|
|
158
|
+
data: ascii85.encode(Buffer.from(data)).toString(),
|
|
159
|
+
version: 'x25519-xsalsa20-poly1305',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// We want to store the data in smart contract, therefore we concatenate them
|
|
163
|
+
// into single Buffer
|
|
164
|
+
const buf = Buffer.concat([
|
|
165
|
+
Buffer.from(enc.ephemPublicKey, 'base64'),
|
|
166
|
+
Buffer.from(enc.nonce, 'base64'),
|
|
167
|
+
Buffer.from(enc.ciphertext, 'base64'),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
// In smart contract we are using `bytes[112]` variable (fixed size byte array)
|
|
171
|
+
// you might need to use `bytes` type for dynamic sized array
|
|
172
|
+
// We are also using ethers.js which requires type `number[]` when passing data
|
|
173
|
+
// for argument of type `bytes` to the smart contract function
|
|
174
|
+
// Next line just converts the buffer to `number[]` required by contract function
|
|
175
|
+
// THIS LINE IS USED IN OUR ORIGINAL CODE:
|
|
176
|
+
// return buf.toJSON().data;
|
|
177
|
+
|
|
178
|
+
// Return just the Buffer to make the function directly compatible with decryptData function
|
|
179
|
+
return buf;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// (async () => {
|
|
183
|
+
// try {
|
|
184
|
+
// const message = 'testabc1';
|
|
185
|
+
// console.log('initial message:', message);
|
|
186
|
+
// const publicKeyPEM = getPEMFromPublic('./app/cert1-ca1-clean.crt');
|
|
187
|
+
// const ECKey = require('ec-key');
|
|
188
|
+
// const key = new ECKey(publicKeyPEM, 'pem');
|
|
189
|
+
// const encrypted_message = encrypt(key.x, key.y, message);
|
|
190
|
+
// console.log(encrypted_message);
|
|
191
|
+
// console.log(encryptedDataToBase64Json(encrypted_message));
|
|
192
|
+
// //
|
|
193
|
+
// // const encrypted_message = 'eyJjaXBoZXJ0ZXh0IjoiZTYzZTcyNDY0MWY4YWRmN2QyNjNmMTI0ZGYxZTdlYzZmNGM5NGU1NCIsIm5vbmNlIjoiYmE0YWU5YjMyOWFhZWRiM2ViZTM5MGU4YTY3YWFiNmMiLCJhdXRoVGFnIjoiOGU2NmI4NTVmNTE3N2E0NzZlMjkwNTE3Y2Q2ZDE3ZmUiLCJ4IjoiN2E1MjhhNGI5YzAzODM4Mjc0ZmFiYzVlM2RiOWQ2ZWFiNzBiZTFiMDA3YWJiOTcyYzI0YmFhNGE1ZmI4OGJjNTVmNDVlNGNkMjZjY2NhNTFhOTk3OGZkNDRhZmE1YjQ0IiwieSI6IjE5M2NjMWYwMzVmOGMxZjEzNGU4MTEzOGIyOTRiODBiZWQyYjkyMzdkZmIyOTMyOGRjOTg1MzlmMjhkNjIzMTJiYTQ2OTU2NDFkYjIyNjk0ZWUyNGFjZjQ4MjBiZDlhMSJ9';
|
|
194
|
+
// // const messageDecrypted = decrypt('./app/cert1-ca1-clean.key', encryptedDataToBase64Json(encrypted_message));
|
|
195
|
+
// // console.log('decrypted message:', messageDecrypted);
|
|
196
|
+
// } catch (e) {
|
|
197
|
+
// console.log(e);
|
|
198
|
+
// }
|
|
199
|
+
//
|
|
200
|
+
// })();
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
encryptedDataFromBase64Json,
|
|
204
|
+
encryptedDataToBase64Json,
|
|
205
|
+
encryptWithPublicKey,
|
|
206
|
+
decrypt,
|
|
207
|
+
encrypt,
|
|
208
|
+
sha256,
|
|
209
|
+
fileChecksum,
|
|
210
|
+
fileChecksumFromSwiftStream
|
|
211
|
+
};
|
|
@@ -34,7 +34,29 @@ function ___etny_result___(data) {
|
|
|
34
34
|
return [0, data];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Names of *_ADDRESS-style enclave config vars that are set but EMPTY.
|
|
38
|
+
// The enclave is sealed: env can only come from the image, so a present-but-
|
|
39
|
+
// empty required value is always a build/render defect, never a runtime
|
|
40
|
+
// choice. ESR_* vars only exist when the project enabled ESR at build time.
|
|
41
|
+
function emptyRequiredConfig() {
|
|
42
|
+
const required = ['ESR_CONTRACT_ADDRESS'];
|
|
43
|
+
return required.filter(
|
|
44
|
+
(name) => name in process.env && !String(process.env[name]).trim()
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
function executeTask(payload, input) {
|
|
49
|
+
const missing = emptyRequiredConfig();
|
|
50
|
+
if (missing.length) {
|
|
51
|
+
return [
|
|
52
|
+
TaskStatus.CONFIG_ERROR,
|
|
53
|
+
'ENCLAVE CONFIG ERROR: required value(s) empty inside the enclave: ' +
|
|
54
|
+
missing.join(', ') +
|
|
55
|
+
' | The enclave is sealed, so this value had to be baked at build' +
|
|
56
|
+
' time and was not. Re-run ecld-build (it validates ESR config)' +
|
|
57
|
+
' and republish.'
|
|
58
|
+
];
|
|
59
|
+
}
|
|
38
60
|
if (backendImportError !== null) {
|
|
39
61
|
return [
|
|
40
62
|
TaskStatus.IMPORT_ERROR,
|
|
@@ -24,6 +24,7 @@ const contract_abi = require('./pox_abi');
|
|
|
24
24
|
const image_registry_abi = require('./image_registry_abi');
|
|
25
25
|
const {sha256} = require("./etny_crypto");
|
|
26
26
|
const {generateCertFromMrEnclave} = require("./self_sign");
|
|
27
|
+
const esr_wallet = require("./esr_wallet");
|
|
27
28
|
|
|
28
29
|
// Baked per network by CI (render_network.sh): "mainnet" | "testnet". On testnet
|
|
29
30
|
// the enclave self-signs its cert from MR_ENCLAVE (non-CAS mode); on mainnet the
|
|
@@ -43,6 +44,10 @@ class EtnySecureLock {
|
|
|
43
44
|
|
|
44
45
|
setInitializers() {
|
|
45
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;
|
|
46
51
|
this.key_file = "/private/__ENCLAVE_NAME_SECURELOCK__/key.pem";
|
|
47
52
|
this.cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/cert.pem";
|
|
48
53
|
this.pub_cert_file = "/app/__ENCLAVE_NAME_SECURELOCK__/enclave_pub_cert.pem";
|
|
@@ -230,7 +235,7 @@ class EtnySecureLock {
|
|
|
230
235
|
const encryptedBase64 = await this.swiftStreamService.getFileContent(bucketName, objectName);
|
|
231
236
|
|
|
232
237
|
const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
|
|
233
|
-
const decryptedResult = etny_crypto.decrypt(this.
|
|
238
|
+
const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
|
|
234
239
|
return decryptedResult.toString();
|
|
235
240
|
} catch (e) {
|
|
236
241
|
console.log(`Failed to get ${objectName} file`);
|
|
@@ -282,37 +287,68 @@ class EtnySecureLock {
|
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
resetPrivateCertFile() {
|
|
285
|
-
//
|
|
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
|
+
}
|
|
286
301
|
try {
|
|
287
|
-
// const fd = fs.openSync(this.private_key_file, 'w+');
|
|
288
302
|
fs.truncate(this.key_file, 0, function () {
|
|
289
303
|
//console.log('Private certificate file was cleaned.')
|
|
290
304
|
});
|
|
291
|
-
// fs.closeSync(fd);
|
|
292
305
|
} catch (e) {
|
|
293
306
|
//console.log('Error while removing the private key file content', e);
|
|
294
307
|
}
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
try {
|
|
309
|
+
fs.readFileSync(this.key_file, 'utf8');
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// Nothing to read back; the wipe above is what mattered.
|
|
312
|
+
}
|
|
297
313
|
}
|
|
298
314
|
|
|
299
315
|
// Testnet non-CAS mode: derive a deterministic self-signed cert/key from this
|
|
300
|
-
// enclave's
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
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.
|
|
304
332
|
generateSelfSignedCertIfTestnet() {
|
|
305
333
|
if (NETWORK_TYPE !== "testnet") return;
|
|
306
334
|
console.log("[SecureLock] TESTNET: self-signing certificate from MR_ENCLAVE (no CAS) [TESTNET-INSECURE]");
|
|
307
335
|
const {privateKeyPem, certPem, mrEnclaveHex} = generateCertFromMrEnclave("SecureLock Enclave");
|
|
308
336
|
console.log(`[SecureLock] MR_ENCLAVE: ${mrEnclaveHex}`);
|
|
309
|
-
for (const f of [this.
|
|
337
|
+
for (const f of [this.cert_file, this.pub_cert_file]) {
|
|
310
338
|
try { fs.mkdirSync(require('path').dirname(f), {recursive: true}); } catch (e) {}
|
|
311
339
|
}
|
|
312
|
-
|
|
340
|
+
// Private key: memory only. Deliberately NOT written to this.key_file.
|
|
341
|
+
this.identity_private_key_pem = privateKeyPem;
|
|
313
342
|
fs.writeFileSync(this.cert_file, certPem);
|
|
314
343
|
}
|
|
315
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
|
+
|
|
316
352
|
async savePublicCert() {
|
|
317
353
|
try {
|
|
318
354
|
const content = await fs.promises.readFile(this.cert_file, 'utf8');
|
|
@@ -321,12 +357,46 @@ class EtnySecureLock {
|
|
|
321
357
|
console.log(`PUBLIC_KEY:`);
|
|
322
358
|
console.log(content);
|
|
323
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
|
+
|
|
324
368
|
await this.swiftStreamService.putFileContent(this.etny_bucket, 'cert.pem', this.pub_cert_file);
|
|
325
369
|
} catch (err) {
|
|
326
370
|
console.error(err);
|
|
327
371
|
}
|
|
328
372
|
}
|
|
329
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
|
+
|
|
330
400
|
async saveResult() {
|
|
331
401
|
try {
|
|
332
402
|
await this.getTrustedZonePublicKey();
|
|
@@ -342,6 +412,37 @@ class EtnySecureLock {
|
|
|
342
412
|
}
|
|
343
413
|
}
|
|
344
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
|
+
console.log(`StateRegistry ready (ESR ${contractAddress})`);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error(`ESR_STATE_ERROR: could not configure StateRegistry: ${e && e.message ? e.message : e}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
345
446
|
async execute() {
|
|
346
447
|
const payload_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.payload);
|
|
347
448
|
const input_data = await this.getFileContentAndDecrypt(this.etny_bucket, this.input);
|
|
@@ -355,6 +456,8 @@ class EtnySecureLock {
|
|
|
355
456
|
return false;
|
|
356
457
|
}
|
|
357
458
|
|
|
459
|
+
this.configureStateRegistry();
|
|
460
|
+
|
|
358
461
|
const task_result = executeTask(payload_data, input_data);
|
|
359
462
|
console.log('TASK RESULT (code, data):', task_result);
|
|
360
463
|
this.task_code = task_result[0];
|