@ethernity-cloud/sdk-js 1.5.5 → 1.5.8
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/nodenithy/build/securelock/src/ecld_state.js +251 -18
- package/nodenithy/build/securelock/src/etny_crypto.js +211 -211
- package/nodenithy/build/securelock/src/etny_exec.js +13 -2
- package/nodenithy/build/securelock/src/models.js +275 -275
- package/nodenithy/build/securelock/src/securelock.js.tmpl +632 -586
- package/nodenithy/build/securelock/src/task_status.js +4 -0
- package/nodenithy/build.mjs +30 -0
- package/nodenithy/run/__pycache__/swift_stream_service.cpython-312.pyc +0 -0
- package/nodenithy/run/image_registry.js +371 -371
- package/nodenithy/src/preStart.js +44 -44
- package/package.json +3 -2
- package/pynithy/build/docker-compose.yml +67 -67
- package/pynithy/run/certs/cert.pem +31 -31
- package/pynithy/run/certs/challenge.crt +10 -10
- package/pynithy/run/certs/key.pem +52 -52
- package/pynithy/run/docker-compose-final.yml +59 -59
- package/pynithy/run/docker-compose-swift-stream.yml +27 -27
- package/pynithy/run/docker-compose.yml +55 -55
- package/pynithy/run/image_registry.js +371 -371
|
@@ -54,7 +54,7 @@ let _provider = null;
|
|
|
54
54
|
* Wire the registry to the enclave's identity, storage and chain access.
|
|
55
55
|
* Called by securelock at task start; payload code never calls this.
|
|
56
56
|
*/
|
|
57
|
-
function configure({ identityPriv, swiftStreamService, bucket, contractAddress, provider }) {
|
|
57
|
+
function configure({ identityPriv, swiftStreamService, bucket, contractAddress, provider, caller = null }) {
|
|
58
58
|
_identityPriv = identityPriv;
|
|
59
59
|
_swift = swiftStreamService;
|
|
60
60
|
_bucket = bucket;
|
|
@@ -62,6 +62,104 @@ function configure({ identityPriv, swiftStreamService, bucket, contractAddress,
|
|
|
62
62
|
_provider = provider;
|
|
63
63
|
_esrLedger = [];
|
|
64
64
|
_taskLedger = {};
|
|
65
|
+
_taskCaller = normAddr(caller);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The authenticated task caller: the wallet that placed the DO request, read
|
|
69
|
+
// from the PoX contract by the TRUSTEDZONE and forwarded over its signed
|
|
70
|
+
// handoff (caller.securelock + .sig, verified by securelock before configure).
|
|
71
|
+
// null when the trustedzone did not supply one -- anonymity is never a
|
|
72
|
+
// privilege: null cannot claim ownership or touch owned state.
|
|
73
|
+
let _taskCaller = null;
|
|
74
|
+
|
|
75
|
+
function taskCaller() {
|
|
76
|
+
return _taskCaller;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normAddr(addr) {
|
|
80
|
+
return typeof addr === 'string' && addr ? addr.toLowerCase() : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* State container + ACL. Stored blobs are either legacy (raw state --
|
|
85
|
+
* "unowned") or an owned container:
|
|
86
|
+
* { _ecld_state: 1, acl: {...}, data: <state> }
|
|
87
|
+
* acl = { owner, read: [], write: [], public_read: false }. Enforcement
|
|
88
|
+
* happens HERE, inside the enclave, against the trustedzone-attested caller.
|
|
89
|
+
*/
|
|
90
|
+
const CONTAINER_MARK = '_ecld_state';
|
|
91
|
+
|
|
92
|
+
class StatePermissionError extends Error {}
|
|
93
|
+
|
|
94
|
+
function unwrapStored(stored) {
|
|
95
|
+
if (stored && typeof stored === 'object' && stored[CONTAINER_MARK] === 1) {
|
|
96
|
+
return [stored.acl || null, stored.data];
|
|
97
|
+
}
|
|
98
|
+
return [null, stored];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function wrapStored(acl, data, committedBy = null) {
|
|
102
|
+
// For owned state, bind the authoring caller (committedBy) INTO the blob, so
|
|
103
|
+
// it lands in the CID -- the field the securelock signs -- making the caller
|
|
104
|
+
// cryptographically bound to the commit, not merely a re-stampable sidecar.
|
|
105
|
+
if (!acl) return data; // unowned keeps the legacy shape
|
|
106
|
+
const container = { [CONTAINER_MARK]: 1, acl, data };
|
|
107
|
+
if (committedBy) container.committedBy = committedBy;
|
|
108
|
+
return container;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Post-payload: re-assert the trusted caller across the ESR ledger. Called by
|
|
113
|
+
* the securelock after the payload returns, with the attested caller captured
|
|
114
|
+
* before execution (outside the payload's reach). Overwrites callerUsed (and
|
|
115
|
+
* re-fixes enclave to the real signer) in every owned entry and RE-STAGES the
|
|
116
|
+
* ledger, so a forged callerUsed cannot reach the trustedzone. Returns true if
|
|
117
|
+
* any entry's callerUsed differed from the trusted value (tamper detected).
|
|
118
|
+
* Defence in depth -- the trustedzone and the CID-bound caller are the sound
|
|
119
|
+
* layers; this runs in the same process the payload subverted.
|
|
120
|
+
*/
|
|
121
|
+
async function restampLedgerCaller(trustedCaller) {
|
|
122
|
+
const trusted = normAddr(trustedCaller);
|
|
123
|
+
if (!_esrLedger.length) return false;
|
|
124
|
+
let tampered = false;
|
|
125
|
+
let signerAddr = null;
|
|
126
|
+
try {
|
|
127
|
+
signerAddr = await new StateRegistry()._signer().getAddress();
|
|
128
|
+
} catch (e) { signerAddr = null; }
|
|
129
|
+
for (const entry of _esrLedger) {
|
|
130
|
+
if (!entry.owned) continue;
|
|
131
|
+
if (normAddr(entry.callerUsed) !== trusted) tampered = true;
|
|
132
|
+
entry.callerUsed = trusted || '';
|
|
133
|
+
if (signerAddr) entry.enclave = signerAddr;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await _swift.putFileContent(
|
|
137
|
+
_bucket, 'esr.authorizations.json', '',
|
|
138
|
+
Buffer.from(JSON.stringify(_esrLedger), 'utf8'));
|
|
139
|
+
} catch (e) { /* best-effort restage; trustedzone still adjudicates */ }
|
|
140
|
+
return tampered;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function aclMembers(acl, field) {
|
|
144
|
+
return new Set((acl[field] || []).map(normAddr).filter(Boolean));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function canRead(acl) {
|
|
148
|
+
if (!acl || acl.public_read) return true;
|
|
149
|
+
const c = _taskCaller;
|
|
150
|
+
if (!c) return false;
|
|
151
|
+
return c === normAddr(acl.owner) || aclMembers(acl, 'read').has(c) || aclMembers(acl, 'write').has(c);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function canWrite(acl) {
|
|
155
|
+
if (!acl) return true; // unowned: writable; claimed when a caller exists
|
|
156
|
+
const c = _taskCaller;
|
|
157
|
+
if (!c) return false;
|
|
158
|
+
return c === normAddr(acl.owner) || aclMembers(acl, 'write').has(c);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function newAcl(owner) {
|
|
162
|
+
return { owner, read: [], write: [], public_read: false };
|
|
65
163
|
}
|
|
66
164
|
|
|
67
165
|
// Task-scoped ledger of every state key this execution touched, recorded by
|
|
@@ -247,10 +345,22 @@ class StateRegistry {
|
|
|
247
345
|
* state" would let the next commit overwrite data that is still recoverable.
|
|
248
346
|
*/
|
|
249
347
|
async get(key, fallback = {}) {
|
|
348
|
+
const [acl, data, version, cid] = await this._readContainer(key, fallback);
|
|
349
|
+
if (!canRead(acl)) {
|
|
350
|
+
throw new StatePermissionError(
|
|
351
|
+
`caller ${_taskCaller || '<anonymous>'} has no read permission on ` +
|
|
352
|
+
`state key '${key}' (owner: ${acl.owner})`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
ledgerRecord(key, version || 0, cid, data);
|
|
356
|
+
return data;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** [acl, data, version, cid] for `key`; NO permission check here. */
|
|
360
|
+
async _readContainer(key, fallback = {}) {
|
|
250
361
|
const [cid, version] = await this._contract().getState(this.walletAddress, keyHash(key));
|
|
251
362
|
if (!version || !cid) {
|
|
252
|
-
|
|
253
|
-
return fallback;
|
|
363
|
+
return [null, fallback, 0, null];
|
|
254
364
|
}
|
|
255
365
|
if (!looksLikeCID(cid)) {
|
|
256
366
|
throw new Error(
|
|
@@ -259,9 +369,9 @@ class StateRegistry {
|
|
|
259
369
|
);
|
|
260
370
|
}
|
|
261
371
|
const blob = await this._fetch(key, cid);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
return
|
|
372
|
+
const stored = JSON.parse(decrypt(blob).toString('utf8'));
|
|
373
|
+
const [acl, data] = unwrapStored(stored);
|
|
374
|
+
return [acl, data, Number(version), cid];
|
|
265
375
|
}
|
|
266
376
|
|
|
267
377
|
/**
|
|
@@ -272,15 +382,39 @@ class StateRegistry {
|
|
|
272
382
|
* retry, so concurrent tasks cannot silently lose updates.
|
|
273
383
|
*/
|
|
274
384
|
async commit(key, mutate, attempts = 3) {
|
|
385
|
+
const transform = async (acl, data) => {
|
|
386
|
+
if (!canWrite(acl)) {
|
|
387
|
+
throw new StatePermissionError(
|
|
388
|
+
`caller ${_taskCaller || '<anonymous>'} has no write permission on ` +
|
|
389
|
+
`state key '${key}' (owner: ${acl.owner})`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
let nextAcl = acl;
|
|
393
|
+
if (!nextAcl && _taskCaller) nextAcl = newAcl(_taskCaller); // first-writer-owns
|
|
394
|
+
return [nextAcl, await mutate(data)];
|
|
395
|
+
};
|
|
396
|
+
const [, newData] = await this._commitTransform(key, transform, attempts);
|
|
397
|
+
return newData;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Optimistic-concurrency commit of transform(acl, data, version) ->
|
|
402
|
+
* [newAcl, newData]. The stored blob is the wrapped container (or the bare
|
|
403
|
+
* data while unowned). On a version race we re-read and retry, so
|
|
404
|
+
* concurrent tasks cannot silently lose updates.
|
|
405
|
+
*/
|
|
406
|
+
async _commitTransform(key, transform, attempts = 3) {
|
|
275
407
|
let lastError = null;
|
|
276
408
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
277
409
|
// eslint-disable-next-line no-await-in-loop
|
|
278
|
-
const currentVersion = await this.
|
|
410
|
+
const [acl, data, currentVersion] = await this._readContainer(key);
|
|
279
411
|
// eslint-disable-next-line no-await-in-loop
|
|
280
|
-
const
|
|
281
|
-
|
|
412
|
+
const [nextAcl, newData] = await transform(acl, data, currentVersion);
|
|
413
|
+
// Bind the authoring caller into the blob (hence the CID/signature) for
|
|
414
|
+
// owned state.
|
|
415
|
+
const stored = wrapStored(nextAcl, newData, nextAcl ? _taskCaller : null);
|
|
282
416
|
|
|
283
|
-
const blob = encrypt(Buffer.from(JSON.stringify(
|
|
417
|
+
const blob = encrypt(Buffer.from(JSON.stringify(stored), 'utf8'));
|
|
284
418
|
// Compute the CID from OUR bytes — never accept one from the node.
|
|
285
419
|
const cid = cidv1Raw(blob);
|
|
286
420
|
|
|
@@ -291,11 +425,12 @@ class StateRegistry {
|
|
|
291
425
|
|
|
292
426
|
try {
|
|
293
427
|
// eslint-disable-next-line no-await-in-loop
|
|
294
|
-
await this._sendCommit(keyHash(key), cid, currentVersion
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
428
|
+
await this._sendCommit(keyHash(key), cid, currentVersion,
|
|
429
|
+
nextAcl ? normAddr(nextAcl.owner) : null);
|
|
430
|
+
// Record the POST-commit values (the DATA -- the ACL never leaves in
|
|
431
|
+
// results): this is what the chain shows once the relay lands.
|
|
432
|
+
ledgerRecord(key, currentVersion + 1, cid, newData);
|
|
433
|
+
return [nextAcl, newData];
|
|
299
434
|
} catch (e) {
|
|
300
435
|
const msg = String(e && e.message ? e.message : e);
|
|
301
436
|
if (/VersionMismatch/i.test(msg) || /version/i.test(msg)) {
|
|
@@ -339,7 +474,7 @@ class StateRegistry {
|
|
|
339
474
|
throw new Error(`Could not read state object for '${key}' (${cid})`);
|
|
340
475
|
}
|
|
341
476
|
|
|
342
|
-
async _sendCommit(keyHashHex, cid, expectedVersion) {
|
|
477
|
+
async _sendCommit(keyHashHex, cid, expectedVersion, ownerAddr = null) {
|
|
343
478
|
// The enclave never pays gas and does NO gas math: it SIGNS the commit
|
|
344
479
|
// (commitFor) and stages the signed authorization for the node, which
|
|
345
480
|
// relays it and pays. The trustedzone independently re-prices the whole
|
|
@@ -362,7 +497,15 @@ class StateRegistry {
|
|
|
362
497
|
cid,
|
|
363
498
|
expectedVersion: Number(expectedVersion),
|
|
364
499
|
relayNonce: Number(relayNonce),
|
|
365
|
-
signature
|
|
500
|
+
signature,
|
|
501
|
+
// IMPERSONATION GUARD: the trustedzone requires callerUsed to equal the
|
|
502
|
+
// DO owner it reads from the PoX contract, so a payload that forged the
|
|
503
|
+
// in-enclave caller to impersonate another user is rejected outside the
|
|
504
|
+
// payload's process. It does NOT verify the caller was a permitted
|
|
505
|
+
// writer (it cannot read the encrypted ACL). Unowned commits carry no
|
|
506
|
+
// constraint.
|
|
507
|
+
callerUsed: _taskCaller || '',
|
|
508
|
+
owned: Boolean(ownerAddr)
|
|
366
509
|
};
|
|
367
510
|
|
|
368
511
|
_esrLedger.push(auth);
|
|
@@ -376,10 +519,100 @@ class StateRegistry {
|
|
|
376
519
|
}
|
|
377
520
|
}
|
|
378
521
|
|
|
522
|
+
/** Owner-only ACL mutation; an unowned key is claimed first (owner = caller). */
|
|
523
|
+
async function ownerOp(key, mutator) {
|
|
524
|
+
if (!_taskCaller) {
|
|
525
|
+
throw new StatePermissionError(
|
|
526
|
+
'state management requires an authenticated caller (the trustedzone supplied none)'
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const reg = new StateRegistry();
|
|
530
|
+
const transform = async (acl, data) => {
|
|
531
|
+
let nextAcl = acl || newAcl(_taskCaller);
|
|
532
|
+
if (normAddr(nextAcl.owner) !== _taskCaller) {
|
|
533
|
+
throw new StatePermissionError(
|
|
534
|
+
`caller ${_taskCaller} is not the owner of state key '${key}' (owner: ${nextAcl.owner})`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
nextAcl = mutator({ ...nextAcl });
|
|
538
|
+
return [nextAcl, data];
|
|
539
|
+
};
|
|
540
|
+
const [acl] = await reg._commitTransform(key, transform);
|
|
541
|
+
return { ...acl };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function assertLevel(level) {
|
|
545
|
+
if (level !== 'read' && level !== 'write') throw new Error("level must be 'read' or 'write'");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function esrGrant(key, address, level = 'read') {
|
|
549
|
+
assertLevel(level);
|
|
550
|
+
const addr = normAddr(address);
|
|
551
|
+
if (!addr) throw new Error('a grantee address is required');
|
|
552
|
+
return ownerOp(key, (acl) => {
|
|
553
|
+
const members = new Set((acl[level] || []).map(normAddr).filter(Boolean));
|
|
554
|
+
members.add(addr);
|
|
555
|
+
acl[level] = [...members].sort();
|
|
556
|
+
return acl;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function esrRevoke(key, address, level = 'read') {
|
|
561
|
+
assertLevel(level);
|
|
562
|
+
const addr = normAddr(address);
|
|
563
|
+
return ownerOp(key, (acl) => {
|
|
564
|
+
acl[level] = (acl[level] || []).map(normAddr).filter((a) => a && a !== addr).sort();
|
|
565
|
+
return acl;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function esrSetPublicRead(key, enabled = true) {
|
|
570
|
+
return ownerOp(key, (acl) => {
|
|
571
|
+
acl.public_read = Boolean(enabled);
|
|
572
|
+
return acl;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function esrTransfer(key, newOwner) {
|
|
577
|
+
const addr = normAddr(newOwner);
|
|
578
|
+
if (!addr) throw new Error('a new owner address is required');
|
|
579
|
+
return ownerOp(key, (acl) => {
|
|
580
|
+
acl.owner = addr;
|
|
581
|
+
return acl;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Owner address of `key`, or null while unowned. Exposes nothing else. */
|
|
586
|
+
async function esrOwner(key) {
|
|
587
|
+
const [acl] = await new StateRegistry()._readContainer(key);
|
|
588
|
+
return acl ? acl.owner : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Owner-only: the full ACL of `key`. */
|
|
592
|
+
async function esrAcl(key) {
|
|
593
|
+
const [acl] = await new StateRegistry()._readContainer(key);
|
|
594
|
+
if (!acl) return null;
|
|
595
|
+
if (!_taskCaller || normAddr(acl.owner) !== _taskCaller) {
|
|
596
|
+
throw new StatePermissionError(
|
|
597
|
+
`caller ${_taskCaller || '<anonymous>'} is not the owner of state key '${key}'`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return { ...acl };
|
|
601
|
+
}
|
|
602
|
+
|
|
379
603
|
module.exports = {
|
|
380
604
|
StateRegistry,
|
|
381
605
|
configure,
|
|
382
606
|
cidv1Raw,
|
|
383
607
|
looksLikeCID,
|
|
384
|
-
ledgerSnapshot
|
|
608
|
+
ledgerSnapshot,
|
|
609
|
+
taskCaller,
|
|
610
|
+
restampLedgerCaller,
|
|
611
|
+
StatePermissionError,
|
|
612
|
+
esrGrant,
|
|
613
|
+
esrRevoke,
|
|
614
|
+
esrSetPublicRead,
|
|
615
|
+
esrTransfer,
|
|
616
|
+
esrOwner,
|
|
617
|
+
esrAcl
|
|
385
618
|
};
|