@ethernity-cloud/sdk-js 1.6.2 → 1.7.1
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 +86 -48
- package/cas/config.js +61 -3
- package/info.js +14 -5
- package/init.js +24 -6
- package/network.js +51 -0
- package/nodenithy/build/securelock/Dockerfile.tmpl +34 -32
- package/nodenithy/build/securelock/scripts/binary-fs-build.sh +7 -6
- package/nodenithy/build.mjs +80 -68
- package/nodenithy/run/docker-compose-final.yml.tmpl +36 -31
- package/nodenithy/run/docker-compose.yml.tmpl +34 -45
- package/nodenithy/run/image_registry.js +17 -4
- package/nodenithy/run.js +42 -28
- package/nodenithy/src/ec_helloworld_example.js +46 -42
- package/nodenithy/src/preStart.mjs +41 -51
- package/package.json +6 -3
- package/publish.js +4 -2
- package/pynithy/build/securelock/Dockerfile.base.tpl +13 -0
- package/pynithy/build/securelock/Dockerfile.tpl +131 -0
- package/pynithy/build/securelock/scripts/binary-fs-build.sh +25 -34
- package/pynithy/build/securelock/scripts/pyfreeze.sh +32 -0
- package/pynithy/build/securelock/src/{pox.abi → abi/etnyImplementationV2.abi} +1489 -1489
- package/pynithy/build/securelock/src/abi/image_registry.abi +1 -0
- package/pynithy/build/securelock/src/ecld_state.py +906 -0
- package/pynithy/build/securelock/src/esr.abi +1 -0
- package/pynithy/build/securelock/src/esr_local.py +267 -0
- package/pynithy/build/securelock/src/esr_wallet.py +150 -0
- package/pynithy/build/securelock/src/etny_crypto.py +208 -167
- package/pynithy/build/securelock/src/etny_exec.py +385 -79
- package/pynithy/build/securelock/src/etny_exec_flask.py +535 -0
- package/pynithy/build/securelock/src/etny_exec_serv.py +353 -0
- package/pynithy/build/securelock/src/etny_session_exec.py +245 -0
- package/pynithy/build/securelock/src/get_sgx_report.so +0 -0
- package/pynithy/build/securelock/src/image_registry.abi +1 -1
- package/pynithy/build/securelock/src/key_generation.py +17 -17
- package/pynithy/build/securelock/src/local_api.py +189 -0
- package/pynithy/build/securelock/src/models.py +200 -176
- package/pynithy/build/securelock/src/securelock.py.tmpl +1037 -731
- package/pynithy/build/securelock/src/swift_stream_service.py +206 -186
- package/pynithy/build.mjs +139 -89
- package/pynithy/run/docker-compose-final.yml.tmpl +58 -51
- package/pynithy/run/docker-compose.yml.tmpl +62 -48
- package/pynithy/run/etny-securelock-test.yaml.tpl +4 -1
- package/pynithy/run/image_registry.js +17 -4
- package/pynithy/run.js +44 -28
- package/pynithy/src/ec_helloworld_example.js +46 -42
- package/pynithy/src/preStart.mjs +41 -51
- package/pynithy/src/serverless/Dockerfile.serverless +8 -0
- package/pynithy/src/serverless/requirements.txt +6 -0
- package/run.js +37 -34
- package/nodenithy/build/las/Dockerfile +0 -5
- package/nodenithy/src/preStart.js +0 -45
- package/pynithy/build/docker-compose.yml +0 -70
- package/pynithy/build/las/Dockerfile +0 -11
- package/pynithy/build/las/src/etny_entrypoint.sh +0 -6
- package/pynithy/build/las/src/las_entrypoint.sh +0 -26
- package/pynithy/build/scripts/binary-fs-build.sh +0 -28
- package/pynithy/build/securelock/Dockerfile.tmpl +0 -95
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"inputs": [], "name": "BadSignature", "type": "error"}, {"inputs": [], "name": "EmptyCID", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "stored", "type": "uint256"}, {"internalType": "uint256", "name": "given", "type": "uint256"}], "name": "NonceOutOfOrder", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "expected", "type": "uint256"}, {"internalType": "uint256", "name": "actual", "type": "uint256"}], "name": "RelayNonceMismatch", "type": "error"}, {"inputs": [{"internalType": "uint256", "name": "expected", "type": "uint256"}, {"internalType": "uint256", "name": "actual", "type": "uint256"}], "name": "VersionMismatch", "type": "error"}, {"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "enclave", "type": "address"}, {"indexed": true, "internalType": "bytes32", "name": "key", "type": "bytes32"}, {"indexed": false, "internalType": "string", "name": "cid", "type": "string"}, {"indexed": false, "internalType": "uint256", "name": "version", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "seq", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "nonce", "type": "uint256"}], "name": "StateCommitted", "type": "event"}, {"inputs": [{"internalType": "bytes32", "name": "key", "type": "bytes32"}, {"internalType": "string", "name": "newCID", "type": "string"}, {"internalType": "uint256", "name": "expectedVersion", "type": "uint256"}, {"internalType": "uint256", "name": "nonce", "type": "uint256"}], "name": "commit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}, {"internalType": "string", "name": "newCID", "type": "string"}, {"internalType": "uint256", "name": "expectedVersion", "type": "uint256"}, {"internalType": "uint256", "name": "relayNonce", "type": "uint256"}, {"internalType": "uint256", "name": "nonce", "type": "uint256"}], "name": "commitDigest", "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}, {"internalType": "string", "name": "newCID", "type": "string"}, {"internalType": "uint256", "name": "expectedVersion", "type": "uint256"}, {"internalType": "uint256", "name": "relayNonce", "type": "uint256"}, {"internalType": "uint256", "name": "nonce", "type": "uint256"}, {"internalType": "bytes", "name": "signature", "type": "bytes"}], "name": "commitFor", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "commitSeq", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "index", "type": "uint256"}], "name": "entryAt", "outputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "entryCount", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "exists", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "startIndex", "type": "uint256"}, {"internalType": "uint256", "name": "limit", "type": "uint256"}], "name": "getEntriesFrom", "outputs": [{"internalType": "address[]", "name": "enclaves", "type": "address[]"}, {"internalType": "bytes32[]", "name": "keys", "type": "bytes32[]"}, {"internalType": "string[]", "name": "cids", "type": "string[]"}, {"internalType": "uint256[]", "name": "versions", "type": "uint256[]"}, {"internalType": "uint64[]", "name": "updatedAts", "type": "uint64[]"}, {"internalType": "uint256", "name": "total", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "getNonce", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "getState", "outputs": [{"internalType": "string", "name": "cid", "type": "string"}, {"internalType": "uint256", "name": "version", "type": "uint256"}, {"internalType": "uint64", "name": "updatedAt", "type": "uint64"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "getVersion", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "address", "name": "enclave", "type": "address"}, {"internalType": "bytes32", "name": "key", "type": "bytes32"}], "name": "relayNonce", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}]
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Local ESR emulator for `ecld-test` -- run state-using backends with no chain.
|
|
2
|
+
|
|
3
|
+
Provides in-memory stand-ins for the two external dependencies StateRegistry
|
|
4
|
+
talks to, so `get`/`commit`/`esr_grant`/... behave EXACTLY as on-chain but
|
|
5
|
+
in-process and instantly, with zero orders and zero gas:
|
|
6
|
+
|
|
7
|
+
* MemSwift -- the state-blob store (replaces the SwiftStream service):
|
|
8
|
+
put_file_content / get_file_content_bytes over a dict,
|
|
9
|
+
optionally mirrored to a JSON file so state survives across
|
|
10
|
+
`ecld-test` runs (the counter keeps counting).
|
|
11
|
+
* MemRegistry -- the ESR contract (getState / getVersion / relayNonce /
|
|
12
|
+
commitDigest, and the commit APPLY that the node would do
|
|
13
|
+
on-chain), keyed by (enclave, keccak(key)) -> (cid, version).
|
|
14
|
+
|
|
15
|
+
`install(ecld_state, caller=..., file=...)` wires these into the ecld_state
|
|
16
|
+
module and sets the local commit-apply hook, so no chain, node, or SGX is
|
|
17
|
+
needed. Used only by the local test API; the real enclave never imports this.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---- storage ---------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class MemSwift:
|
|
28
|
+
"""SwiftStream stand-in: (bucket, object) -> bytes, over a dict.
|
|
29
|
+
|
|
30
|
+
Mirrors to `file` (base64 JSON) when given, so committed state persists
|
|
31
|
+
across separate `ecld-test` invocations.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, file=None):
|
|
35
|
+
self._file = file
|
|
36
|
+
self._store = {}
|
|
37
|
+
if file and os.path.exists(file):
|
|
38
|
+
try:
|
|
39
|
+
with open(file, encoding="utf-8") as f:
|
|
40
|
+
raw = json.load(f)
|
|
41
|
+
self._store = {k: base64.b64decode(v) for k, v in raw.items()}
|
|
42
|
+
except Exception:
|
|
43
|
+
self._store = {}
|
|
44
|
+
|
|
45
|
+
def _persist(self):
|
|
46
|
+
if not self._file:
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
enc = {k: base64.b64encode(v).decode("ascii") for k, v in self._store.items()}
|
|
50
|
+
tmp = self._file + ".tmp"
|
|
51
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
52
|
+
json.dump(enc, f)
|
|
53
|
+
os.replace(tmp, self._file)
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def put_file_content(self, bucket, object_name, _prefix, io_obj):
|
|
58
|
+
data = io_obj.getvalue() if hasattr(io_obj, "getvalue") else bytes(io_obj)
|
|
59
|
+
self._store["%s/%s" % (bucket, object_name)] = data
|
|
60
|
+
self._persist()
|
|
61
|
+
return True, None
|
|
62
|
+
|
|
63
|
+
def get_file_content_bytes(self, bucket, object_name):
|
|
64
|
+
data = self._store.get("%s/%s" % (bucket, object_name))
|
|
65
|
+
if data is None:
|
|
66
|
+
return False, None
|
|
67
|
+
return True, data
|
|
68
|
+
|
|
69
|
+
# Some code paths use the string variant; provide it too.
|
|
70
|
+
def get_file_content(self, bucket, object_name):
|
|
71
|
+
ok, data = self.get_file_content_bytes(bucket, object_name)
|
|
72
|
+
return (ok, data.decode("utf-8") if ok else None)
|
|
73
|
+
|
|
74
|
+
def create_bucket(self, *a, **k):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---- registry --------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
class _Callable:
|
|
81
|
+
"""A web3-style bound function: .call() returns the precomputed value."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, value):
|
|
84
|
+
self._value = value
|
|
85
|
+
|
|
86
|
+
def call(self, *a, **k):
|
|
87
|
+
return self._value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _Functions:
|
|
91
|
+
def __init__(self, reg):
|
|
92
|
+
self._reg = reg
|
|
93
|
+
|
|
94
|
+
def getState(self, enclave, key_hash):
|
|
95
|
+
cid, version, updated = self._reg.state(enclave, key_hash)
|
|
96
|
+
return _Callable([cid, version, updated])
|
|
97
|
+
|
|
98
|
+
def getVersion(self, enclave, key_hash):
|
|
99
|
+
_cid, version, _u = self._reg.state(enclave, key_hash)
|
|
100
|
+
return _Callable(version)
|
|
101
|
+
|
|
102
|
+
def relayNonce(self, enclave, key_hash):
|
|
103
|
+
return _Callable(self._reg.relay_nonce(enclave, key_hash))
|
|
104
|
+
|
|
105
|
+
def commitDigest(self, enclave, key_hash, cid, expected_version, relay_nonce,
|
|
106
|
+
nonce=0):
|
|
107
|
+
# Deterministic 32-byte digest over the fields (no real signing needed
|
|
108
|
+
# locally; the securelock signs it with the local test key). The
|
|
109
|
+
# idempotency nonce is part of the digest, like on-chain.
|
|
110
|
+
import hashlib
|
|
111
|
+
m = hashlib.sha256()
|
|
112
|
+
m.update(str((enclave, key_hash, cid, expected_version, relay_nonce,
|
|
113
|
+
nonce)).encode())
|
|
114
|
+
return _Callable(m.digest())
|
|
115
|
+
|
|
116
|
+
def getNonce(self, enclave, key_hash):
|
|
117
|
+
return _Callable(self._reg.idem_nonce(enclave, key_hash))
|
|
118
|
+
|
|
119
|
+
def entryCount(self):
|
|
120
|
+
return _Callable(self._reg.entry_count())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class _Contract:
|
|
124
|
+
def __init__(self, reg):
|
|
125
|
+
self.functions = _Functions(reg)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MemRegistry:
|
|
129
|
+
"""In-memory ESR registry: (enclave, keyHash) -> (cid, version, updatedAt).
|
|
130
|
+
|
|
131
|
+
Applies commits directly (the job the node's commitFor does on-chain),
|
|
132
|
+
enforcing monotonic versions with optimistic concurrency.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, file=None):
|
|
136
|
+
self._file = file
|
|
137
|
+
# key: "enclave|0xkeyhash" -> {"cid":..., "version":int, "updatedAt":int}
|
|
138
|
+
self._entries = {}
|
|
139
|
+
self._nonce = {}
|
|
140
|
+
if file and os.path.exists(file):
|
|
141
|
+
try:
|
|
142
|
+
with open(file, encoding="utf-8") as f:
|
|
143
|
+
d = json.load(f)
|
|
144
|
+
self._entries = d.get("entries", {})
|
|
145
|
+
self._nonce = d.get("nonce", {})
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def _persist(self):
|
|
150
|
+
if not self._file:
|
|
151
|
+
return
|
|
152
|
+
try:
|
|
153
|
+
tmp = self._file + ".tmp"
|
|
154
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
155
|
+
json.dump({"entries": self._entries, "nonce": self._nonce}, f)
|
|
156
|
+
os.replace(tmp, self._file)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _k(enclave, key_hash):
|
|
162
|
+
kh = key_hash.hex() if isinstance(key_hash, (bytes, bytearray)) else str(key_hash)
|
|
163
|
+
return "%s|%s" % (str(enclave).lower(), kh)
|
|
164
|
+
|
|
165
|
+
def state(self, enclave, key_hash):
|
|
166
|
+
e = self._entries.get(self._k(enclave, key_hash))
|
|
167
|
+
if not e:
|
|
168
|
+
return ("", 0, 0)
|
|
169
|
+
return (e["cid"], int(e["version"]), int(e.get("updatedAt", 0)))
|
|
170
|
+
|
|
171
|
+
def idem_nonce(self, enclave, key_hash):
|
|
172
|
+
"""Last accepted idempotency nonce for (enclave, key) -- public data,
|
|
173
|
+
mirroring the on-chain getNonce view."""
|
|
174
|
+
e = self._entries.get(self._k(enclave, key_hash))
|
|
175
|
+
return int(e.get("nonce", 0)) if e else 0
|
|
176
|
+
|
|
177
|
+
def relay_nonce(self, enclave, key_hash):
|
|
178
|
+
# Per (enclave, key), like the contract: same-key commits serialize,
|
|
179
|
+
# different keys are independent.
|
|
180
|
+
return int(self._nonce.get(self._k(enclave, key_hash), 0))
|
|
181
|
+
|
|
182
|
+
def entry_count(self):
|
|
183
|
+
return len(self._entries)
|
|
184
|
+
|
|
185
|
+
def apply_commit(self, enclave, key_hash, cid, expected_version, nonce=0):
|
|
186
|
+
"""The on-chain commitFor: bump version if expected matches current.
|
|
187
|
+
Raises 'VersionMismatch' (string in message) on a race, exactly like
|
|
188
|
+
the contract, so StateRegistry.commit's retry loop behaves the same.
|
|
189
|
+
|
|
190
|
+
The per-key nonce advances by EXACTLY 1 on every commit, like the
|
|
191
|
+
contract: nonce == 0 ("omitted") gets the next value assigned by the
|
|
192
|
+
registry itself; a non-zero nonce is a client-pinned guard and must
|
|
193
|
+
be EXACTLY the stored value + 1 (no gaps, no reuse) or
|
|
194
|
+
'NonceOutOfOrder' is raised."""
|
|
195
|
+
k = self._k(enclave, key_hash)
|
|
196
|
+
entry = self._entries.get(k, {})
|
|
197
|
+
cur = int(entry.get("version", 0))
|
|
198
|
+
if int(expected_version) != cur:
|
|
199
|
+
raise RuntimeError("VersionMismatch: expected %s but current is %s"
|
|
200
|
+
% (expected_version, cur))
|
|
201
|
+
stored_nonce = int(entry.get("nonce", 0))
|
|
202
|
+
n = int(nonce or 0)
|
|
203
|
+
if n == 0:
|
|
204
|
+
stored_nonce += 1
|
|
205
|
+
else:
|
|
206
|
+
if n != stored_nonce + 1:
|
|
207
|
+
raise RuntimeError("NonceOutOfOrder: stored %s, given %s"
|
|
208
|
+
% (stored_nonce, n))
|
|
209
|
+
stored_nonce = n
|
|
210
|
+
self._entries[k] = {"cid": cid, "version": cur + 1, "updatedAt": 0,
|
|
211
|
+
"nonce": stored_nonce}
|
|
212
|
+
self._nonce[k] = self.relay_nonce(enclave, key_hash) + 1
|
|
213
|
+
self._persist()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---- web3 stand-in ---------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
class MemWeb3:
|
|
219
|
+
"""Just enough of a web3 to satisfy ecld_state._contract()."""
|
|
220
|
+
|
|
221
|
+
def __init__(self, reg):
|
|
222
|
+
self._reg = reg
|
|
223
|
+
|
|
224
|
+
class _Eth:
|
|
225
|
+
def contract(_self, address=None, abi=None):
|
|
226
|
+
return _Contract(reg)
|
|
227
|
+
self.eth = _Eth()
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def to_checksum_address(addr):
|
|
231
|
+
return addr
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---- installer -------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def install(ecld_state, caller=None, identity_priv=None, bucket="ecld-local",
|
|
237
|
+
contract_address="0xLOCAL", file_prefix=".ecld-esr-local"):
|
|
238
|
+
"""Wire the emulator into the ecld_state module for local testing.
|
|
239
|
+
|
|
240
|
+
After this, StateRegistry() works fully in-process. `file_prefix` gives two
|
|
241
|
+
small JSON files (state blobs + registry) so state persists across runs;
|
|
242
|
+
pass None to keep everything in-memory only.
|
|
243
|
+
"""
|
|
244
|
+
swift_file = (file_prefix + ".swift.json") if file_prefix else None
|
|
245
|
+
reg_file = (file_prefix + ".registry.json") if file_prefix else None
|
|
246
|
+
swift = MemSwift(file=swift_file)
|
|
247
|
+
reg = MemRegistry(file=reg_file)
|
|
248
|
+
web3 = MemWeb3(reg)
|
|
249
|
+
|
|
250
|
+
if identity_priv is None:
|
|
251
|
+
# A fixed, well-known LOCAL test identity -- stable wallet_address across
|
|
252
|
+
# runs. NOT a real key; local testing only.
|
|
253
|
+
identity_priv = b"ecld-local-test-identity-key-0001"
|
|
254
|
+
|
|
255
|
+
ecld_state.configure(
|
|
256
|
+
identity_priv=identity_priv,
|
|
257
|
+
swift_stream_service=swift,
|
|
258
|
+
bucket=bucket,
|
|
259
|
+
contract_address=contract_address,
|
|
260
|
+
web3=web3,
|
|
261
|
+
caller=caller,
|
|
262
|
+
)
|
|
263
|
+
# The node applies commitFor on-chain in real mode; locally, apply it here.
|
|
264
|
+
ecld_state.set_local_commit_apply(
|
|
265
|
+
lambda enclave, key_hash, cid, expected_version, nonce=0:
|
|
266
|
+
reg.apply_commit(enclave, key_hash, cid, expected_version, nonce))
|
|
267
|
+
return {"swift": swift, "registry": reg, "web3": web3}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""ESR identity wallet derivation (ESR RFC §5.1).
|
|
2
|
+
|
|
3
|
+
The enclave's Ethereum wallet is derived from the enclave's IDENTITY PRIVATE
|
|
4
|
+
KEY (the cert key), never from MR_ENCLAVE:
|
|
5
|
+
|
|
6
|
+
eth_priv = keccak256(DOMAIN_SEP || identity_priv_der) # secp256k1 scalar
|
|
7
|
+
eth_address = address(secp256k1(eth_priv))
|
|
8
|
+
|
|
9
|
+
Why derive instead of reusing the cert key: the cert is secp384r1 (P-384) and
|
|
10
|
+
Ethereum signing requires secp256k1, so a P-384 key can never sign a
|
|
11
|
+
transaction. Deriving from the identity key means the wallet inherits the
|
|
12
|
+
identity's security properties automatically:
|
|
13
|
+
|
|
14
|
+
* mainnet — identity key is the CAS-provisioned secret, so the wallet is
|
|
15
|
+
enclave-only and stable across every node running this MRENCLAVE.
|
|
16
|
+
* testnet — identity key is derived from MR_SIGNER/MR_ENCLAVE, both PUBLIC,
|
|
17
|
+
so the wallet is public too. Functional testing only; is_secret_identity()
|
|
18
|
+
reports False and callers must warn loudly (RFC §7).
|
|
19
|
+
|
|
20
|
+
DOMAIN_SEP keeps this key from colliding with any other use of the identity
|
|
21
|
+
key; a second separator derives the state-encryption key in a later phase.
|
|
22
|
+
|
|
23
|
+
SECURITY: nothing here may serialize, log or return the private key. The only
|
|
24
|
+
value that ever leaves the enclave is the ADDRESS (RFC §5.2) — that is what
|
|
25
|
+
makes the wallet fundable and readable without weakening secrecy.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
|
|
30
|
+
# The wallet PRIVATE key derivation is deliberately not part of the public
|
|
31
|
+
# surface: the client payload is exec'd inside this image and can import any
|
|
32
|
+
# module here, so an exported key-derivation helper would hand untrusted code a
|
|
33
|
+
# ready-made way to materialize the ESR private key. Only the ADDRESS leaves
|
|
34
|
+
# this module (RFC §5.2); _derive_wallet_private_key stays an implementation
|
|
35
|
+
# detail of derive_wallet_address.
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DOMAIN_SEP",
|
|
38
|
+
"derive_wallet_address",
|
|
39
|
+
"is_secret_identity",
|
|
40
|
+
"INSECURE_IDENTITY_WARNING",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
DOMAIN_SEP = b"ethernity-cloud/esr-wallet/v1"
|
|
44
|
+
|
|
45
|
+
# secp256k1 group order; a valid private scalar is in [1, N-1].
|
|
46
|
+
_SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _keccak256(data: bytes) -> bytes:
|
|
50
|
+
"""Keccak-256 (Ethereum's hash), not SHA3-256."""
|
|
51
|
+
try:
|
|
52
|
+
from Crypto.Hash import keccak # pycryptodome
|
|
53
|
+
|
|
54
|
+
h = keccak.new(digest_bits=256)
|
|
55
|
+
h.update(data)
|
|
56
|
+
return h.digest()
|
|
57
|
+
except ImportError:
|
|
58
|
+
from eth_hash.auto import keccak as eth_keccak # web3 dependency
|
|
59
|
+
|
|
60
|
+
return eth_keccak(data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _derive_wallet_private_key(identity_priv_der: bytes) -> bytes:
|
|
64
|
+
"""32-byte secp256k1 private key derived from the enclave identity key.
|
|
65
|
+
|
|
66
|
+
Kept private to this module's callers; never log or transmit the result.
|
|
67
|
+
"""
|
|
68
|
+
if not identity_priv_der:
|
|
69
|
+
raise ValueError("empty enclave identity key; cannot derive ESR wallet")
|
|
70
|
+
digest = _keccak256(DOMAIN_SEP + identity_priv_der)
|
|
71
|
+
# Astronomically unlikely, but a scalar of 0 or >= N is invalid: re-hash
|
|
72
|
+
# until valid so derivation is total rather than probabilistically broken.
|
|
73
|
+
while True:
|
|
74
|
+
scalar = int.from_bytes(digest, "big")
|
|
75
|
+
if 0 < scalar < _SECP256K1_N:
|
|
76
|
+
return digest
|
|
77
|
+
digest = _keccak256(DOMAIN_SEP + digest)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def derive_wallet_address(identity_priv_der: bytes) -> str:
|
|
81
|
+
"""Checksummed 0x address of the enclave's ESR wallet (safe to publish)."""
|
|
82
|
+
priv = _derive_wallet_private_key(identity_priv_der)
|
|
83
|
+
try:
|
|
84
|
+
from eth_keys import keys
|
|
85
|
+
|
|
86
|
+
return keys.PrivateKey(priv).public_key.to_checksum_address()
|
|
87
|
+
except ImportError:
|
|
88
|
+
# eth_account ships with the runner dependency chain and is always
|
|
89
|
+
# present in the enclave image; use it when eth_keys is unavailable.
|
|
90
|
+
from eth_account import Account
|
|
91
|
+
|
|
92
|
+
return Account.from_key(priv).address
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_secret_identity(network_type: str, attested: bool = False) -> bool:
|
|
96
|
+
"""True only when the identity key is genuinely unpredictable to outsiders.
|
|
97
|
+
|
|
98
|
+
`attested` is whether this build is provisioned by a CAS (its key comes
|
|
99
|
+
from the CAS session): true on mainnet and on a testnet whose enclaves are
|
|
100
|
+
attested by the ethernity-cas validator set.
|
|
101
|
+
|
|
102
|
+
This is NOT "is the enclave genuine" — SGX protects enclave memory on every
|
|
103
|
+
network. It is the narrower question the ESR wallet depends on: could
|
|
104
|
+
someone outside the enclave reproduce the private key?
|
|
105
|
+
|
|
106
|
+
The distinction is ATTESTATION, not SGX. Both networks run the identical
|
|
107
|
+
in-enclave generator (get_sgx_report.c) on real SGX hardware, so at runtime
|
|
108
|
+
a node operator cannot read the key out of enclave memory on either.
|
|
109
|
+
|
|
110
|
+
* mainnet — CAS attests the enclave (DCAP quote verified) before the
|
|
111
|
+
identity is trusted, so a key is only accepted from a genuine, measured
|
|
112
|
+
enclave. The private key is effectively enclave-only: reproducing the
|
|
113
|
+
derivation outside a real attested enclave yields a key nothing accepts.
|
|
114
|
+
Intended posture; needs nothing from ESR.
|
|
115
|
+
* testnet — NO attestation. The enclave still runs on SGX and still
|
|
116
|
+
generates the keypair with the same algorithm, but nothing verifies the
|
|
117
|
+
MRENCLAVE, and the derivation input (MR_ENCLAVE) is public. So anyone
|
|
118
|
+
who rebuilds the image can reproduce the keypair. This is a deliberate
|
|
119
|
+
testnet tradeoff (real enclave execution for functional testing without
|
|
120
|
+
the CAS dependency) — not a bug. Treat testnet ESR wallets as
|
|
121
|
+
disposable; do not fund them with real value.
|
|
122
|
+
|
|
123
|
+
What would make the testnet key genuinely exclusive is a secret INPUT that
|
|
124
|
+
never leaves the CPU (EGETKEY / sealing key) OR requiring attestation on
|
|
125
|
+
testnet too. Encrypting or obfuscating the generator does NOT achieve this:
|
|
126
|
+
without attestation, an attacker doesn't reverse the code — they run the
|
|
127
|
+
published image (it is on IPFS, MRENCLAVE on-chain) and observe the key.
|
|
128
|
+
"""
|
|
129
|
+
import os
|
|
130
|
+
|
|
131
|
+
override = os.getenv("ESR_IDENTITY_SECRET", "").strip().lower()
|
|
132
|
+
if override in ("1", "true", "yes"):
|
|
133
|
+
return True
|
|
134
|
+
if override in ("0", "false", "no"):
|
|
135
|
+
return False
|
|
136
|
+
# Secrecy tracks attestation, not the network name: mainnet is attested,
|
|
137
|
+
# and so is a testnet whose securelock is provisioned by a CAS.
|
|
138
|
+
return attested or str(network_type).strip().lower() == "mainnet"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
INSECURE_IDENTITY_WARNING = (
|
|
142
|
+
"[TESTNET-INSECURE] This network runs the enclave on SGX but WITHOUT "
|
|
143
|
+
"attestation, so the ESR wallet's keypair — though generated inside the "
|
|
144
|
+
"enclave — is reproducible by anyone who rebuilds the image (its derivation "
|
|
145
|
+
"input, MR_ENCLAVE, is public and nothing verifies the enclave). This is a "
|
|
146
|
+
"deliberate testnet tradeoff, not a defect. Treat the wallet as disposable "
|
|
147
|
+
"and do not fund it with real value. (An attested testnet build can set "
|
|
148
|
+
"ESR_IDENTITY_SECRET=1 to "
|
|
149
|
+
"suppress this warning.)"
|
|
150
|
+
)
|