sgx.py 0.12.dev0__tar.gz → 0.12.dev2__tar.gz
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.
- sgx_py-0.12.dev2/PKG-INFO +38 -0
- sgx_py-0.12.dev2/README.md +112 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/setup.py +16 -8
- sgx_py-0.12.dev2/sgx/__init__.py +5 -0
- sgx_py-0.12.dev2/sgx/certificates.py +134 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx/constants.py +1 -0
- sgx_py-0.12.dev2/sgx/generate.sh +18 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx/http.py +66 -25
- sgx_py-0.12.dev2/sgx/options.py +104 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx/sgx.py +23 -10
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx/sgx_rpc_handler.py +38 -11
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx/sgx_zmq.py +5 -9
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx/utils.py +27 -20
- sgx_py-0.12.dev2/sgx.py.egg-info/PKG-INFO +38 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx.py.egg-info/SOURCES.txt +2 -0
- sgx_py-0.12.dev2/sgx.py.egg-info/requires.txt +23 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/tests/test_core.py +24 -8
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/tests/test_dkg.py +10 -8
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/tests/test_http.py +13 -2
- sgx_py-0.12.dev0/PKG-INFO +0 -30
- sgx_py-0.12.dev0/README.md +0 -75
- sgx_py-0.12.dev0/sgx/__init__.py +0 -2
- sgx_py-0.12.dev0/sgx/generate.sh +0 -1
- sgx_py-0.12.dev0/sgx.py.egg-info/PKG-INFO +0 -30
- sgx_py-0.12.dev0/sgx.py.egg-info/requires.txt +0 -15
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/LICENSE +0 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/setup.cfg +0 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx.py.egg-info/dependency_links.txt +0 -0
- {sgx_py-0.12.dev0 → sgx_py-0.12.dev2}/sgx.py.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sgx.py
|
|
3
|
+
Version: 0.12.dev2
|
|
4
|
+
Summary: SGX
|
|
5
|
+
Home-page: http://github.com/skalenetwork/sgx.py
|
|
6
|
+
Author: SKALE Labs
|
|
7
|
+
Author-email: support@skalelabs.com
|
|
8
|
+
Requires-Python: >=3.11,<4
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: cryptography<51,>=50.0.2
|
|
11
|
+
Requires-Dist: cytoolz>=0.12
|
|
12
|
+
Requires-Dist: eth-account<0.15,>=0.13
|
|
13
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.7
|
|
14
|
+
Requires-Dist: eth-utils<7,>=5
|
|
15
|
+
Requires-Dist: hexbytes<3,>=1
|
|
16
|
+
Requires-Dist: pem==23.1.0
|
|
17
|
+
Requires-Dist: pyzmq==27.2.0
|
|
18
|
+
Requires-Dist: requests>=2.33.0
|
|
19
|
+
Requires-Dist: urllib3>=2.8.0
|
|
20
|
+
Provides-Extra: linter
|
|
21
|
+
Requires-Dist: ruff==0.17.2; extra == "linter"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: ruff==0.17.2; extra == "dev"
|
|
24
|
+
Requires-Dist: build==1.6.1; extra == "dev"
|
|
25
|
+
Requires-Dist: coincurve==13.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: python-dotenv==1.2.4; extra == "dev"
|
|
27
|
+
Requires-Dist: twine==7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest==9.1.1; extra == "dev"
|
|
29
|
+
Requires-Dist: mock==5.2.0; extra == "dev"
|
|
30
|
+
Requires-Dist: web3==8.0.0; extra == "dev"
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: author-email
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: provides-extra
|
|
36
|
+
Dynamic: requires-dist
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# sgx.py
|
|
2
|
+
[](https://discord.gg/vvUtWJB)
|
|
3
|
+
|
|
4
|
+
## Requirements
|
|
5
|
+
|
|
6
|
+
|**Tool**|**Minimum Supported Version**|
|
|
7
|
+
|--------|-----------|
|
|
8
|
+
| Python | 3.11 |
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Install Python 3.11
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# confirm prerequisites are isntalled
|
|
18
|
+
sudo apt update
|
|
19
|
+
sudo apt install software-properties-common
|
|
20
|
+
# Add deadsnakes PPA - widely used for Python versions
|
|
21
|
+
sudo add-apt-repository ppa:deadsnakes/ppa
|
|
22
|
+
# update again & install python 3.11
|
|
23
|
+
sudo apt update
|
|
24
|
+
sudo apt install python3.11
|
|
25
|
+
# Check if it was installed
|
|
26
|
+
python3.11 --version
|
|
27
|
+
|
|
28
|
+
pip install sgx.py
|
|
29
|
+
```
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Developers
|
|
33
|
+
### Install all python libraries
|
|
34
|
+
```bash
|
|
35
|
+
sudo apt-get update --fix-missing
|
|
36
|
+
sudo apt-get install libudev-dev
|
|
37
|
+
sudo apt-get install swig
|
|
38
|
+
|
|
39
|
+
# create virtual environment - install dependencies locally
|
|
40
|
+
python3.11 -m venv venv
|
|
41
|
+
source venv/bin/activate
|
|
42
|
+
pip install -e .
|
|
43
|
+
pip install -e .[dev]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Running Tests
|
|
47
|
+
|
|
48
|
+
Create a `.env` file in source directory, like the following:
|
|
49
|
+
```bash
|
|
50
|
+
# SGXWallet listening point (1026 uses https w/ certificates)
|
|
51
|
+
SERVER=https://127.0.0.1:1026
|
|
52
|
+
# path where you want tests to store the auto-generated certificates
|
|
53
|
+
CERT_PATH=.
|
|
54
|
+
# private key for the funded test account used by transaction tests
|
|
55
|
+
ETH_PRIVATE_KEY=
|
|
56
|
+
# Ethereum JSON-RPC endpoint used by transaction tests
|
|
57
|
+
GETH=
|
|
58
|
+
# 1 when sgxwallet runs with -e (ZMQ key ownership)
|
|
59
|
+
SGX_EXPECT_ZMQ_OWNERSHIP=0
|
|
60
|
+
```
|
|
61
|
+
You may alter the fields as needed.
|
|
62
|
+
|
|
63
|
+
Run the tests:
|
|
64
|
+
```bash
|
|
65
|
+
# Run all tests
|
|
66
|
+
pytest
|
|
67
|
+
|
|
68
|
+
# Run specific test file
|
|
69
|
+
pytest ./tests/<test-file-name>.py
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Checking the wallet
|
|
74
|
+
|
|
75
|
+
Available from sgxwallet 1.11.0. Older servers raise `SgxMethodNotFoundError`, a subclass of `SgxServerError`.
|
|
76
|
+
|
|
77
|
+
### Client certificate
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from sgx import CertificateMatch, SgxClient
|
|
81
|
+
|
|
82
|
+
client = SgxClient(endpoint, cert_dir, allow_registration=False)
|
|
83
|
+
check = client.check_local_certificate(expected_number=1)
|
|
84
|
+
if check.outcome is not CertificateMatch.MATCH:
|
|
85
|
+
print(check.outcome, check.newer_issued_at)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| `check.outcome` | Meaning |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `MATCH` | `sgx.crt` is the newest certificate the wallet issued, and the wallet issued `expected_number` client certificates |
|
|
91
|
+
| `UNEXPECTED_NUMBER` | `sgx.crt` is the newest, but the number of issued certificates differs |
|
|
92
|
+
| `NOT_LATEST` | the wallet issued a newer certificate, at `check.newer_issued_at` |
|
|
93
|
+
| `NOT_ISSUED` | unknown to the wallet's CA: wrong endpoint, or its CA database was recreated |
|
|
94
|
+
|
|
95
|
+
`check.info` holds both counts and the newest certificate with its status: `V`, `R` or `E`. The check relies on the server verifying client certificates; otherwise `NOT_LATEST` can also mean a certificate from another CA.
|
|
96
|
+
|
|
97
|
+
Unless `allow_registration=False` is passed, any request registers a new certificate when `sgx.crt` or `sgx.key` is missing, and the constructor makes such a request when the directory does not hold exactly three files. `check_local_certificate` never registers.
|
|
98
|
+
|
|
99
|
+
### Server options
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
options = client.get_server_options()
|
|
103
|
+
options.effective.zmq_key_ownership_enforced # -e is in effect; applies to ZMQ only
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The server reports these values itself; they are not attested. `options.build` is `None` unless the server verifies the caller's certificate. While sgxwallet starts or stops, the call raises `SgxServerError`; retry.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
[](LICENSE)
|
|
111
|
+
|
|
112
|
+
Copyright (C) 2019-present SKALE Labs
|
|
@@ -5,14 +5,16 @@ from setuptools import (
|
|
|
5
5
|
|
|
6
6
|
extras_require = {
|
|
7
7
|
'linter': [
|
|
8
|
-
'ruff==0.
|
|
8
|
+
'ruff==0.17.2',
|
|
9
9
|
],
|
|
10
10
|
'dev': [
|
|
11
|
+
'build==1.6.1',
|
|
11
12
|
'coincurve==13.0.0',
|
|
12
|
-
'python-dotenv==
|
|
13
|
-
'twine==
|
|
14
|
-
'pytest==
|
|
13
|
+
'python-dotenv==1.2.4',
|
|
14
|
+
'twine==7.0.0',
|
|
15
|
+
'pytest==9.1.1',
|
|
15
16
|
'mock==5.2.0',
|
|
17
|
+
'web3==8.0.0',
|
|
16
18
|
],
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -20,16 +22,22 @@ extras_require['dev'] = extras_require['linter'] + extras_require['dev']
|
|
|
20
22
|
|
|
21
23
|
setup(
|
|
22
24
|
name='sgx.py',
|
|
23
|
-
version='0.
|
|
25
|
+
version='0.12dev2',
|
|
24
26
|
description='SGX',
|
|
25
27
|
url='http://github.com/skalenetwork/sgx.py',
|
|
26
28
|
author='SKALE Labs',
|
|
27
29
|
author_email='support@skalelabs.com',
|
|
28
30
|
install_requires=[
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
+
'cryptography>=50.0.2,<51',
|
|
32
|
+
'cytoolz>=0.12',
|
|
33
|
+
'eth-account>=0.13,<0.15',
|
|
34
|
+
'eth-hash[pycryptodome]>=0.7',
|
|
35
|
+
'eth-utils>=5,<7',
|
|
36
|
+
'hexbytes>=1,<3',
|
|
31
37
|
'pem==23.1.0',
|
|
32
|
-
'
|
|
38
|
+
'pyzmq==27.2.0',
|
|
39
|
+
'requests>=2.33.0',
|
|
40
|
+
'urllib3>=2.8.0',
|
|
33
41
|
],
|
|
34
42
|
packages=find_packages(exclude=['tests']),
|
|
35
43
|
python_requires='>=3.11,<4',
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# flake8: noqa: F401
|
|
2
|
+
from sgx.certificates import CertificateMatch, IssuedCertificatesInfo, SgxCertificateError
|
|
3
|
+
from sgx.options import LogLevel, ServerOptions, SgxServerOptionsError
|
|
4
|
+
from sgx.sgx import SgxClient
|
|
5
|
+
from sgx.sgx_rpc_handler import SgxMethodNotFoundError
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of sgx.py
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2026 SKALE Labs
|
|
6
|
+
#
|
|
7
|
+
# sgx.py is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Affero General Public License as published
|
|
9
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# sgx.py is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Affero General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
# along with sgx.py. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from enum import Enum
|
|
25
|
+
|
|
26
|
+
from cryptography import x509
|
|
27
|
+
from cryptography.hazmat.primitives import hashes
|
|
28
|
+
|
|
29
|
+
from sgx.constants import CRT_FILENAME
|
|
30
|
+
from sgx.utils import SgxError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SgxCertificateError(SgxError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CertificateMatch(Enum):
|
|
38
|
+
MATCH = 'match' # the local certificate is the newest issued
|
|
39
|
+
UNEXPECTED_NUMBER = 'unexpected_number' # as MATCH, but the count differs from the expected
|
|
40
|
+
NOT_LATEST = 'not_latest' # a newer certificate was issued after it
|
|
41
|
+
NOT_ISSUED = 'not_issued' # unknown to this wallet's CA database
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class NewestCertificate:
|
|
46
|
+
serial: int
|
|
47
|
+
sha256: bytes
|
|
48
|
+
not_before: datetime
|
|
49
|
+
not_after: datetime
|
|
50
|
+
status: str # V valid, R revoked, E expired
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class IssuedCertificatesInfo:
|
|
55
|
+
"""Client certificates issued by the wallet's CA, from getIssuedCertificatesInfo."""
|
|
56
|
+
|
|
57
|
+
certificates_number: int
|
|
58
|
+
server_certificates_number: int
|
|
59
|
+
newest_certificate: NewestCertificate | None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_result(cls, result):
|
|
63
|
+
"""Parses a result in the format sgxwallet documents and rejects anything else."""
|
|
64
|
+
try:
|
|
65
|
+
number = _count(result['certificatesNumber'])
|
|
66
|
+
newest = result['newestCertificate']
|
|
67
|
+
# sgxwallet sends a newest certificate exactly when it has issued any
|
|
68
|
+
if (newest is None) != (number == 0):
|
|
69
|
+
raise ValueError('newestCertificate does not match certificatesNumber')
|
|
70
|
+
if newest is not None:
|
|
71
|
+
newest = NewestCertificate(
|
|
72
|
+
serial=int(_text('[0-9A-F]+', newest['serial']), 16),
|
|
73
|
+
sha256=bytes.fromhex(_text('[0-9a-f]{64}', newest['sha256'])),
|
|
74
|
+
not_before=_utc(newest['notBefore']),
|
|
75
|
+
not_after=_utc(newest['notAfter']),
|
|
76
|
+
status=_text('[VRE]', newest['status']),
|
|
77
|
+
)
|
|
78
|
+
return cls(number, _count(result['serverCertificatesNumber']), newest)
|
|
79
|
+
except (KeyError, TypeError, ValueError) as err:
|
|
80
|
+
raise SgxCertificateError(
|
|
81
|
+
f'Malformed getIssuedCertificatesInfo result: {result!r}'
|
|
82
|
+
) from err
|
|
83
|
+
|
|
84
|
+
def check(self, certificate, expected_number=None):
|
|
85
|
+
"""Compares an x509.Certificate with the newest certificate the wallet issued. If the
|
|
86
|
+
server does not verify client certificates, NOT_LATEST may be another CA's certificate."""
|
|
87
|
+
newest = self.newest_certificate
|
|
88
|
+
if newest is not None and newest.sha256 == certificate.fingerprint(hashes.SHA256()):
|
|
89
|
+
if expected_number is None or expected_number == self.certificates_number:
|
|
90
|
+
outcome = CertificateMatch.MATCH
|
|
91
|
+
else:
|
|
92
|
+
outcome = CertificateMatch.UNEXPECTED_NUMBER
|
|
93
|
+
elif newest is not None and newest.serial > certificate.serial_number:
|
|
94
|
+
outcome = CertificateMatch.NOT_LATEST
|
|
95
|
+
else:
|
|
96
|
+
outcome = CertificateMatch.NOT_ISSUED
|
|
97
|
+
return CertificateCheck(outcome, self)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class CertificateCheck:
|
|
102
|
+
outcome: CertificateMatch
|
|
103
|
+
info: IssuedCertificatesInfo
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def newer_issued_at(self):
|
|
107
|
+
if self.outcome is CertificateMatch.NOT_LATEST:
|
|
108
|
+
return self.info.newest_certificate.not_before
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_local_certificate(cert_dir):
|
|
113
|
+
try:
|
|
114
|
+
with open(os.path.join(cert_dir, CRT_FILENAME), 'rb') as crt_file:
|
|
115
|
+
return x509.load_pem_x509_certificate(crt_file.read())
|
|
116
|
+
except (OSError, TypeError, ValueError) as err: # TypeError: no cert_dir
|
|
117
|
+
raise SgxCertificateError(f'Cannot load {CRT_FILENAME} from {cert_dir!r}') from err
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _count(value):
|
|
121
|
+
# Exact type, because bool is a subclass of int
|
|
122
|
+
if type(value) is not int or value < 0:
|
|
123
|
+
raise ValueError(f'Invalid count {value!r}')
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _text(pattern, value):
|
|
128
|
+
if not re.fullmatch(pattern, value):
|
|
129
|
+
raise ValueError(f'Invalid value {value!r}')
|
|
130
|
+
return value
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _utc(value):
|
|
134
|
+
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
umask 077
|
|
5
|
+
|
|
6
|
+
CSR_PATH="$1"
|
|
7
|
+
KEY_PATH="$2"
|
|
8
|
+
COMMON_NAME="$3"
|
|
9
|
+
|
|
10
|
+
rm -f -- "$CSR_PATH" "$KEY_PATH"
|
|
11
|
+
|
|
12
|
+
openssl req -new -sha256 -nodes \
|
|
13
|
+
-out "$CSR_PATH" \
|
|
14
|
+
-newkey rsa:2048 \
|
|
15
|
+
-keyout "$KEY_PATH" \
|
|
16
|
+
-subj "/CN=$COMMON_NAME"
|
|
17
|
+
|
|
18
|
+
chmod 600 "$KEY_PATH"
|
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fcntl
|
|
2
|
+
import json
|
|
2
3
|
import logging
|
|
4
|
+
import os
|
|
3
5
|
import secrets
|
|
4
|
-
import
|
|
5
|
-
import json
|
|
6
|
+
from contextlib import contextmanager
|
|
6
7
|
from time import sleep
|
|
7
8
|
from urllib.parse import urlparse
|
|
8
9
|
|
|
10
|
+
import requests
|
|
9
11
|
import urllib3
|
|
10
12
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
13
|
+
|
|
14
|
+
from sgx.certificates import SgxCertificateError
|
|
11
15
|
from sgx.constants import (
|
|
12
|
-
GENERATE_SCRIPT_PATH,
|
|
13
|
-
DEFAULT_TIMEOUT,
|
|
14
|
-
CSR_FILENAME,
|
|
15
16
|
CRT_FILENAME,
|
|
17
|
+
CSR_FILENAME,
|
|
18
|
+
DEFAULT_TIMEOUT,
|
|
19
|
+
GENERATE_SCRIPT_PATH,
|
|
16
20
|
KEY_FILENAME,
|
|
17
|
-
|
|
21
|
+
LOCK_FILENAME,
|
|
22
|
+
SGX_RESPONSE_TIMEOUT,
|
|
18
23
|
)
|
|
19
|
-
from sgx.utils import
|
|
20
|
-
|
|
24
|
+
from sgx.utils import SgxError, print_request_log, print_response_log, run_cmd
|
|
21
25
|
|
|
22
26
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # TODO: Remove
|
|
23
27
|
logger = logging.getLogger(__name__)
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
MAX_RETRIES = 22
|
|
31
|
+
CSR_STATUS_PENDING = 1
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
class SgxSSLError(SgxError):
|
|
@@ -34,15 +39,40 @@ class SgxUnreachableError(SgxError):
|
|
|
34
39
|
pass
|
|
35
40
|
|
|
36
41
|
|
|
37
|
-
def
|
|
42
|
+
def certificate_exists(crt_dir_path: str) -> bool:
|
|
43
|
+
return all(
|
|
44
|
+
os.path.exists(os.path.join(crt_dir_path, filename))
|
|
45
|
+
for filename in (CRT_FILENAME, KEY_FILENAME)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextmanager
|
|
50
|
+
def enrollment_lock(crt_dir_path: str):
|
|
51
|
+
lock_path = os.path.join(crt_dir_path, LOCK_FILENAME)
|
|
52
|
+
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
|
|
53
|
+
try:
|
|
54
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
55
|
+
yield
|
|
56
|
+
finally:
|
|
57
|
+
os.close(fd)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_certificate_credentials(crt_dir_path, csr_server, allow_registration=True):
|
|
38
61
|
key_path = os.path.join(crt_dir_path, KEY_FILENAME)
|
|
39
62
|
crt_path = os.path.join(crt_dir_path, CRT_FILENAME)
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
if certificate_exists(crt_dir_path):
|
|
64
|
+
return crt_path, key_path
|
|
65
|
+
if not allow_registration:
|
|
66
|
+
raise SgxCertificateError(
|
|
67
|
+
f'{crt_path} or {key_path} is missing and registration is disabled'
|
|
68
|
+
)
|
|
69
|
+
with enrollment_lock(crt_dir_path):
|
|
70
|
+
if not certificate_exists(crt_dir_path):
|
|
71
|
+
csr_path = os.path.join(crt_dir_path, CSR_FILENAME)
|
|
72
|
+
if not os.path.exists(csr_path) or not os.path.exists(key_path):
|
|
73
|
+
generate_csr_credentials(csr_path, key_path)
|
|
74
|
+
csr_hash = sign_certificate(csr_server, csr_path)
|
|
75
|
+
write_crt_to_file(crt_path, csr_server, csr_hash)
|
|
46
76
|
return crt_path, key_path
|
|
47
77
|
|
|
48
78
|
|
|
@@ -52,13 +82,23 @@ def generate_csr_credentials(csr_path, key_path):
|
|
|
52
82
|
|
|
53
83
|
|
|
54
84
|
def write_crt_to_file(crt_path, csr_server, csr_hash):
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
response
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
for _ in range(MAX_RETRIES):
|
|
86
|
+
response = send_request(csr_server, 'getCertificate', {'hash': csr_hash})
|
|
87
|
+
if response.get('error'):
|
|
88
|
+
raise SgxSSLError(response['error']['message'])
|
|
89
|
+
result = response['result']
|
|
90
|
+
if result['status'] == CSR_STATUS_PENDING:
|
|
91
|
+
sleep(DEFAULT_TIMEOUT)
|
|
92
|
+
continue
|
|
93
|
+
if result['status'] != 0:
|
|
94
|
+
raise SgxSSLError(result['errorMessage'])
|
|
95
|
+
with open(crt_path, 'w') as f:
|
|
96
|
+
f.write(result['cert'])
|
|
97
|
+
return
|
|
98
|
+
raise SgxSSLError(
|
|
99
|
+
f'Certificate signing request {csr_hash} is still pending '
|
|
100
|
+
f'after {MAX_RETRIES} attempts'
|
|
101
|
+
)
|
|
62
102
|
|
|
63
103
|
|
|
64
104
|
def sign_certificate(csr_server, csr_path):
|
|
@@ -69,7 +109,7 @@ def sign_certificate(csr_server, csr_path):
|
|
|
69
109
|
return csr_hash
|
|
70
110
|
|
|
71
111
|
|
|
72
|
-
def send_request(url, method, params, path_to_cert=None):
|
|
112
|
+
def send_request(url, method, params, path_to_cert=None, allow_registration=True):
|
|
73
113
|
headers = {'content-type': 'application/json'}
|
|
74
114
|
call_data = {
|
|
75
115
|
"id": 0,
|
|
@@ -82,7 +122,8 @@ def send_request(url, method, params, path_to_cert=None):
|
|
|
82
122
|
if path_to_cert:
|
|
83
123
|
cert = get_certificate_credentials(
|
|
84
124
|
path_to_cert,
|
|
85
|
-
get_cert_provider(url)
|
|
125
|
+
get_cert_provider(url),
|
|
126
|
+
allow_registration
|
|
86
127
|
)
|
|
87
128
|
|
|
88
129
|
try:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of sgx.py
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2026 SKALE Labs
|
|
6
|
+
#
|
|
7
|
+
# sgx.py is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Affero General Public License as published
|
|
9
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# sgx.py is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Affero General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
18
|
+
# along with sgx.py. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, fields
|
|
21
|
+
from enum import IntEnum
|
|
22
|
+
|
|
23
|
+
from sgx.utils import SgxError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SgxServerOptionsError(SgxError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LogLevel(IntEnum):
|
|
31
|
+
TRACE = 0
|
|
32
|
+
DEBUG = 1
|
|
33
|
+
INFO = 2
|
|
34
|
+
WARNING = 3
|
|
35
|
+
ERROR = 4
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ServerFlags:
|
|
40
|
+
log_level: LogLevel
|
|
41
|
+
enclave_log_level: LogLevel
|
|
42
|
+
use_https: bool
|
|
43
|
+
autoconfirm: bool
|
|
44
|
+
enter_backup_key: bool
|
|
45
|
+
reencrypt_database_with_new_sek: bool
|
|
46
|
+
check_cert: bool
|
|
47
|
+
check_zmq_sig: bool
|
|
48
|
+
auto_sign: bool
|
|
49
|
+
generate_test_keys: bool
|
|
50
|
+
check_key_ownership: bool
|
|
51
|
+
thread_pool_size: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class EffectiveOptions:
|
|
56
|
+
rpc_port: int # as bound inside the server, before any port mapping
|
|
57
|
+
rpc_client_certificate_required: bool
|
|
58
|
+
zmq_key_ownership_enforced: bool # -e is in effect; applies to ZMQ only
|
|
59
|
+
json_rpc_key_ownership_enforced: bool
|
|
60
|
+
sgx_thread_pool_size: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class BuildInfo:
|
|
65
|
+
sgx_simulation: bool
|
|
66
|
+
sgx_debug_launch: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ServerOptions:
|
|
71
|
+
"""Options sgxwallet reports about itself, from getServerOptions; they are not attested.
|
|
72
|
+
build is sent only to authenticated callers: HTTPS with the client certificate check."""
|
|
73
|
+
|
|
74
|
+
flags: ServerFlags
|
|
75
|
+
effective: EffectiveOptions
|
|
76
|
+
build: BuildInfo | None = None
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_result(cls, result):
|
|
80
|
+
if not isinstance(result, dict):
|
|
81
|
+
raise SgxServerOptionsError(f'Malformed getServerOptions result: {result!r}')
|
|
82
|
+
build = result.get('build')
|
|
83
|
+
return cls(
|
|
84
|
+
_load(ServerFlags, result.get('flags')),
|
|
85
|
+
_load(EffectiveOptions, result.get('effective')),
|
|
86
|
+
None if build is None else _load(BuildInfo, build),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load(cls, data):
|
|
91
|
+
# Fields match the camelCase keys ignoring case and underscores: use_https is useHTTPS
|
|
92
|
+
try:
|
|
93
|
+
values = {key.lower(): value for key, value in data.items()}
|
|
94
|
+
kwargs = {f.name: _typed(f.type, values[f.name.replace('_', '')]) for f in fields(cls)}
|
|
95
|
+
except (AttributeError, KeyError, TypeError, ValueError) as err:
|
|
96
|
+
raise SgxServerOptionsError(f'Malformed {cls.__name__}: {data!r}') from err
|
|
97
|
+
return cls(**kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _typed(kind, value):
|
|
101
|
+
# Exact types, because bool is a subclass of int
|
|
102
|
+
if type(value) is not (int if kind is LogLevel else kind):
|
|
103
|
+
raise TypeError(f'{value!r} is not {kind.__name__}')
|
|
104
|
+
return kind(value)
|
|
@@ -18,20 +18,22 @@
|
|
|
18
18
|
# along with sgx.py. If not, see <https://www.gnu.org/licenses/>.
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
from dataclasses import dataclass
|
|
22
21
|
from collections.abc import Mapping
|
|
23
|
-
from
|
|
24
|
-
|
|
25
|
-
from eth_utils.curried import keccak
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
26
24
|
from cytoolz import dissoc
|
|
27
|
-
from sgx.sgx_rpc_handler import SgxRPCHandler
|
|
28
|
-
from sgx.sgx_zmq import SgxZmq
|
|
29
|
-
from sgx.utils import public_key_to_address
|
|
30
25
|
from eth_account._utils import signing
|
|
31
26
|
from eth_account._utils.legacy_transactions import encode_transaction
|
|
27
|
+
from eth_account.datastructures import SignedMessage, SignedTransaction
|
|
32
28
|
from eth_account.typed_transactions.typed_transaction import TypedTransaction
|
|
33
|
-
from eth_utils.encoding import big_endian_to_int
|
|
34
29
|
from eth_utils.conversions import add_0x_prefix, remove_0x_prefix
|
|
30
|
+
from eth_utils.curried import keccak
|
|
31
|
+
from eth_utils.encoding import big_endian_to_int
|
|
32
|
+
from hexbytes import HexBytes
|
|
33
|
+
|
|
34
|
+
from sgx.sgx_rpc_handler import SgxRPCHandler
|
|
35
|
+
from sgx.sgx_zmq import SgxZmq
|
|
36
|
+
from sgx.utils import public_key_to_address
|
|
35
37
|
|
|
36
38
|
logger = logging.getLogger(__name__)
|
|
37
39
|
|
|
@@ -51,9 +53,11 @@ class ComplaintResponse:
|
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
class SgxClient:
|
|
54
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self, sgx_endpoint, path_to_cert=None, n=None, t=None, zmq=False, allow_registration=True
|
|
58
|
+
):
|
|
55
59
|
self.sgx_endpoint = sgx_endpoint
|
|
56
|
-
self.sgx_rpc_server = SgxRPCHandler(sgx_endpoint, path_to_cert)
|
|
60
|
+
self.sgx_rpc_server = SgxRPCHandler(sgx_endpoint, path_to_cert, allow_registration)
|
|
57
61
|
if zmq:
|
|
58
62
|
self.zmq = SgxZmq(sgx_endpoint, path_to_cert, n, t)
|
|
59
63
|
if not path_to_cert:
|
|
@@ -149,6 +153,15 @@ class SgxClient:
|
|
|
149
153
|
def get_server_version(self):
|
|
150
154
|
return self.sgx_rpc_server.get_server_version()
|
|
151
155
|
|
|
156
|
+
def get_server_options(self):
|
|
157
|
+
return self.sgx_rpc_server.get_server_options()
|
|
158
|
+
|
|
159
|
+
def get_issued_certificates_info(self):
|
|
160
|
+
return self.sgx_rpc_server.get_issued_certificates_info()
|
|
161
|
+
|
|
162
|
+
def check_local_certificate(self, expected_number=None):
|
|
163
|
+
return self.sgx_rpc_server.check_local_certificate(expected_number)
|
|
164
|
+
|
|
152
165
|
def verify_secret_share(self, public_shares, eth_key_name, secret_share, index):
|
|
153
166
|
return self.sgx_rpc_server.verify_secret_share(
|
|
154
167
|
public_shares, eth_key_name, secret_share, self.n, self.t, index
|
|
@@ -18,22 +18,27 @@
|
|
|
18
18
|
# along with sgx.py. If not, see <https://www.gnu.org/licenses/>.
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
import os
|
|
22
21
|
from enum import Enum
|
|
23
|
-
|
|
24
22
|
from urllib.parse import urlparse
|
|
25
23
|
|
|
26
|
-
from sgx.
|
|
24
|
+
from sgx.certificates import IssuedCertificatesInfo, load_local_certificate
|
|
25
|
+
from sgx.http import certificate_exists, send_request
|
|
26
|
+
from sgx.options import ServerOptions
|
|
27
27
|
from sgx.utils import SgxError
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
logger = logging.getLogger(__name__)
|
|
31
30
|
|
|
31
|
+
METHOD_NOT_FOUND = -32601
|
|
32
|
+
|
|
32
33
|
|
|
33
34
|
class SgxServerError(SgxError):
|
|
34
35
|
pass
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
class SgxMethodNotFoundError(SgxServerError):
|
|
39
|
+
"""The server does not implement the method, e.g. an sgxwallet older than 1.10.4."""
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
class DkgPolyStatus(Enum):
|
|
38
43
|
FAIL = 0
|
|
39
44
|
NEW_GENERATED = 1
|
|
@@ -41,10 +46,11 @@ class DkgPolyStatus(Enum):
|
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
class SgxRPCHandler:
|
|
44
|
-
def __init__(self, sgx_endpoint, path_to_cert):
|
|
49
|
+
def __init__(self, sgx_endpoint, path_to_cert, allow_registration=True):
|
|
45
50
|
self.sgx_endpoint = check_provider(sgx_endpoint)
|
|
46
51
|
self.path_to_cert = path_to_cert
|
|
47
|
-
|
|
52
|
+
self.allow_registration = allow_registration
|
|
53
|
+
if allow_registration and path_to_cert and not certificate_exists(path_to_cert):
|
|
48
54
|
self.get_server_status()
|
|
49
55
|
|
|
50
56
|
def ecdsa_sign(self, key_name, transaction_hash):
|
|
@@ -133,6 +139,21 @@ class SgxRPCHandler:
|
|
|
133
139
|
response = self.__send_request("getServerVersion")
|
|
134
140
|
return response['result']['version']
|
|
135
141
|
|
|
142
|
+
def get_server_options(self):
|
|
143
|
+
response = self.__send_request('getServerOptions')
|
|
144
|
+
return ServerOptions.from_result(response['result'])
|
|
145
|
+
|
|
146
|
+
def get_issued_certificates_info(self):
|
|
147
|
+
response = self.__send_request('getIssuedCertificatesInfo')
|
|
148
|
+
return IssuedCertificatesInfo.from_result(response['result'])
|
|
149
|
+
|
|
150
|
+
def check_local_certificate(self, expected_number=None):
|
|
151
|
+
certificate = load_local_certificate(self.path_to_cert)
|
|
152
|
+
# Never registers: a new certificate would replace the one being checked
|
|
153
|
+
response = self.__send_request('getIssuedCertificatesInfo', allow_registration=False)
|
|
154
|
+
info = IssuedCertificatesInfo.from_result(response['result'])
|
|
155
|
+
return info.check(certificate, expected_number)
|
|
156
|
+
|
|
136
157
|
def verify_secret_share(self, public_shares, eth_key_name, secret_share, n, t, index):
|
|
137
158
|
params = dict()
|
|
138
159
|
params['publicShares'] = public_shares
|
|
@@ -179,7 +200,9 @@ class SgxRPCHandler:
|
|
|
179
200
|
response = self.__send_request("createBLSPrivateKeyV2", params)
|
|
180
201
|
return response['result']['status'] == 0
|
|
181
202
|
|
|
182
|
-
def create_bls_private_key_v3(
|
|
203
|
+
def create_bls_private_key_v3(
|
|
204
|
+
self, poly_name, bls_key_name, eth_key_name, secret_contributions, n, t
|
|
205
|
+
):
|
|
183
206
|
params = dict()
|
|
184
207
|
# polyName is optional in V3 - new nodes joining the network do not pass it
|
|
185
208
|
if poly_name:
|
|
@@ -268,10 +291,14 @@ class SgxRPCHandler:
|
|
|
268
291
|
|
|
269
292
|
return result
|
|
270
293
|
|
|
271
|
-
def __send_request(self, method, params=None):
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
294
|
+
def __send_request(self, method, params=None, allow_registration=True):
|
|
295
|
+
registration = self.allow_registration and allow_registration
|
|
296
|
+
response = send_request(self.sgx_endpoint, method, params, self.path_to_cert, registration)
|
|
297
|
+
error = response.get('error')
|
|
298
|
+
if error is not None:
|
|
299
|
+
if error.get('code') == METHOD_NOT_FOUND:
|
|
300
|
+
raise SgxMethodNotFoundError(error['message'])
|
|
301
|
+
raise SgxServerError(error['message'])
|
|
275
302
|
if response['result']['status']:
|
|
276
303
|
raise SgxServerError(response['result']['errorMessage'])
|
|
277
304
|
return response
|
|
@@ -24,20 +24,17 @@ import os
|
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
from enum import Enum
|
|
26
26
|
from time import sleep
|
|
27
|
-
from
|
|
27
|
+
from urllib.parse import urlparse
|
|
28
28
|
|
|
29
|
-
from eth_utils.conversions import add_0x_prefix, remove_0x_prefix
|
|
30
29
|
import pem
|
|
31
30
|
import zmq
|
|
32
31
|
from cryptography.hazmat.primitives import hashes
|
|
33
|
-
from cryptography.hazmat.primitives.asymmetric import ec,
|
|
32
|
+
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
|
|
34
33
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
34
|
+
from eth_utils.conversions import add_0x_prefix, remove_0x_prefix
|
|
35
35
|
|
|
36
|
-
from
|
|
37
|
-
|
|
38
|
-
from sgx.constants import DEFAULT_TIMEOUT, SGX_ZMQ_RESPONSE_TIMEOUT_MS, CRT_FILENAME, KEY_FILENAME
|
|
39
|
-
from sgx.utils import SgxError
|
|
40
|
-
|
|
36
|
+
from sgx.constants import CRT_FILENAME, DEFAULT_TIMEOUT, KEY_FILENAME, SGX_ZMQ_RESPONSE_TIMEOUT_MS
|
|
37
|
+
from sgx.utils import SgxError, public_key_to_address
|
|
41
38
|
|
|
42
39
|
logger = logging.getLogger(__name__)
|
|
43
40
|
|
|
@@ -291,7 +288,6 @@ class SgxZmq:
|
|
|
291
288
|
p_id = os.getpid()
|
|
292
289
|
if not self.sockets.get(p_id):
|
|
293
290
|
socket_p_id = self.ctx.socket(zmq.DEALER)
|
|
294
|
-
socket_p_id.setsockopt_string(zmq.IDENTITY, '135:14603077656239261618')
|
|
295
291
|
socket_p_id.setsockopt(zmq.LINGER, 0)
|
|
296
292
|
socket_p_id.setsockopt(zmq.SNDTIMEO, SGX_ZMQ_RESPONSE_TIMEOUT_MS)
|
|
297
293
|
socket_p_id.setsockopt(zmq.RCVTIMEO, SGX_ZMQ_RESPONSE_TIMEOUT_MS)
|
|
@@ -18,48 +18,55 @@
|
|
|
18
18
|
# along with sgx.py. If not, see <https://www.gnu.org/licenses/>.
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
import copy
|
|
22
21
|
import logging
|
|
23
22
|
import os
|
|
24
23
|
import subprocess
|
|
25
24
|
from subprocess import PIPE
|
|
26
25
|
|
|
27
|
-
from
|
|
28
|
-
|
|
26
|
+
from eth_utils import keccak, to_checksum_address, to_hex
|
|
29
27
|
|
|
30
28
|
logger = logging.getLogger(__name__)
|
|
31
29
|
|
|
30
|
+
REDACTED = '<redacted>'
|
|
31
|
+
SECRET_FIELDS = frozenset({
|
|
32
|
+
'dhKey',
|
|
33
|
+
'key',
|
|
34
|
+
'keyShare',
|
|
35
|
+
'secretContributions',
|
|
36
|
+
'secretShare',
|
|
37
|
+
})
|
|
38
|
+
|
|
32
39
|
|
|
33
40
|
class SgxError(Exception):
|
|
34
41
|
pass
|
|
35
42
|
|
|
36
43
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
def redact(data: object, crop_len: int = 50) -> object:
|
|
45
|
+
if isinstance(data, dict):
|
|
46
|
+
return {
|
|
47
|
+
key: REDACTED if key in SECRET_FIELDS else redact(value, crop_len)
|
|
48
|
+
for key, value in data.items()
|
|
49
|
+
}
|
|
50
|
+
if isinstance(data, list):
|
|
51
|
+
return [redact(item, crop_len) for item in data]
|
|
52
|
+
if isinstance(data, str) and len(data) > crop_len:
|
|
53
|
+
return data[:crop_len] + '...'
|
|
54
|
+
return data
|
|
44
55
|
|
|
45
56
|
|
|
46
57
|
def print_request_log(request):
|
|
47
|
-
|
|
48
|
-
crop_json(cropped_request)
|
|
49
|
-
logger.info(f'Send request: {request}')
|
|
58
|
+
logger.info(f'Send request: {redact(request)}')
|
|
50
59
|
|
|
51
60
|
|
|
52
61
|
def print_response_log(response):
|
|
53
|
-
|
|
54
|
-
crop_json(cropped_response)
|
|
55
|
-
logger.info(f'Response received: {cropped_response}')
|
|
62
|
+
logger.info(f'Response received: {redact(response)}')
|
|
56
63
|
|
|
57
64
|
|
|
58
|
-
def run_cmd(cmd, env=
|
|
65
|
+
def run_cmd(cmd, env=None, shell=False):
|
|
59
66
|
logger.info(f'Running: {cmd}')
|
|
60
67
|
res = subprocess.run(
|
|
61
68
|
cmd, shell=shell, stdout=PIPE, stderr=PIPE,
|
|
62
|
-
env={**
|
|
69
|
+
env={**os.environ, **(env or {})}
|
|
63
70
|
)
|
|
64
71
|
if res.returncode:
|
|
65
72
|
logger.error('Error during shell execution:')
|
|
@@ -69,5 +76,5 @@ def run_cmd(cmd, env={}, shell=False):
|
|
|
69
76
|
|
|
70
77
|
|
|
71
78
|
def public_key_to_address(pk):
|
|
72
|
-
hash_ =
|
|
73
|
-
return
|
|
79
|
+
hash_ = keccak(hexstr=str(pk))
|
|
80
|
+
return to_checksum_address(to_hex(hash_[-20:]))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sgx.py
|
|
3
|
+
Version: 0.12.dev2
|
|
4
|
+
Summary: SGX
|
|
5
|
+
Home-page: http://github.com/skalenetwork/sgx.py
|
|
6
|
+
Author: SKALE Labs
|
|
7
|
+
Author-email: support@skalelabs.com
|
|
8
|
+
Requires-Python: >=3.11,<4
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: cryptography<51,>=50.0.2
|
|
11
|
+
Requires-Dist: cytoolz>=0.12
|
|
12
|
+
Requires-Dist: eth-account<0.15,>=0.13
|
|
13
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.7
|
|
14
|
+
Requires-Dist: eth-utils<7,>=5
|
|
15
|
+
Requires-Dist: hexbytes<3,>=1
|
|
16
|
+
Requires-Dist: pem==23.1.0
|
|
17
|
+
Requires-Dist: pyzmq==27.2.0
|
|
18
|
+
Requires-Dist: requests>=2.33.0
|
|
19
|
+
Requires-Dist: urllib3>=2.8.0
|
|
20
|
+
Provides-Extra: linter
|
|
21
|
+
Requires-Dist: ruff==0.17.2; extra == "linter"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: ruff==0.17.2; extra == "dev"
|
|
24
|
+
Requires-Dist: build==1.6.1; extra == "dev"
|
|
25
|
+
Requires-Dist: coincurve==13.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: python-dotenv==1.2.4; extra == "dev"
|
|
27
|
+
Requires-Dist: twine==7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest==9.1.1; extra == "dev"
|
|
29
|
+
Requires-Dist: mock==5.2.0; extra == "dev"
|
|
30
|
+
Requires-Dist: web3==8.0.0; extra == "dev"
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: author-email
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: provides-extra
|
|
36
|
+
Dynamic: requires-dist
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
cryptography<51,>=50.0.2
|
|
2
|
+
cytoolz>=0.12
|
|
3
|
+
eth-account<0.15,>=0.13
|
|
4
|
+
eth-hash[pycryptodome]>=0.7
|
|
5
|
+
eth-utils<7,>=5
|
|
6
|
+
hexbytes<3,>=1
|
|
7
|
+
pem==23.1.0
|
|
8
|
+
pyzmq==27.2.0
|
|
9
|
+
requests>=2.33.0
|
|
10
|
+
urllib3>=2.8.0
|
|
11
|
+
|
|
12
|
+
[dev]
|
|
13
|
+
ruff==0.17.2
|
|
14
|
+
build==1.6.1
|
|
15
|
+
coincurve==13.0.0
|
|
16
|
+
python-dotenv==1.2.4
|
|
17
|
+
twine==7.0.0
|
|
18
|
+
pytest==9.1.1
|
|
19
|
+
mock==5.2.0
|
|
20
|
+
web3==8.0.0
|
|
21
|
+
|
|
22
|
+
[linter]
|
|
23
|
+
ruff==0.17.2
|
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
import urllib
|
|
2
1
|
import os
|
|
3
2
|
import secrets
|
|
3
|
+
import socket
|
|
4
|
+
import urllib
|
|
4
5
|
|
|
5
6
|
import eth_account._utils.legacy_transactions as transactions
|
|
6
7
|
import pytest
|
|
7
|
-
|
|
8
|
-
from eth_account.messages import defunct_hash_message, encode_defunct
|
|
9
|
-
|
|
10
8
|
from dotenv import load_dotenv
|
|
9
|
+
from eth_account.messages import defunct_hash_message, encode_defunct
|
|
11
10
|
from eth_keys import keys
|
|
12
11
|
from hexbytes import HexBytes
|
|
13
|
-
import socket
|
|
14
12
|
from web3 import Web3
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
from sgx import SgxClient
|
|
18
|
-
|
|
14
|
+
from sgx import CertificateMatch, SgxClient
|
|
19
15
|
|
|
20
16
|
load_dotenv()
|
|
21
17
|
|
|
22
18
|
SGX_URL = os.getenv('SERVER')
|
|
23
19
|
GETH_URL = os.getenv('GETH')
|
|
24
20
|
ETH_PRIVATE_KEY = os.getenv('ETH_PRIVATE_KEY')
|
|
21
|
+
EXPECT_ZMQ_OWNERSHIP = os.getenv('SGX_EXPECT_ZMQ_OWNERSHIP') == '1'
|
|
25
22
|
|
|
26
23
|
ETH_VALUE_FOR_TESTS = 5 * 10**18
|
|
27
24
|
|
|
@@ -151,6 +148,25 @@ def test_get_server_version(sgx):
|
|
|
151
148
|
assert isinstance(sgx.get_server_version(), str)
|
|
152
149
|
|
|
153
150
|
|
|
151
|
+
def test_get_server_options(sgx):
|
|
152
|
+
url = urllib.parse.urlparse(SGX_URL)
|
|
153
|
+
options = sgx.get_server_options()
|
|
154
|
+
assert options.effective.rpc_port == url.port
|
|
155
|
+
assert options.flags.use_https == (url.scheme == 'https')
|
|
156
|
+
assert options.flags.auto_sign and options.flags.autoconfirm
|
|
157
|
+
assert options.effective.zmq_key_ownership_enforced == EXPECT_ZMQ_OWNERSHIP
|
|
158
|
+
assert (options.build is not None) == options.effective.rpc_client_certificate_required
|
|
159
|
+
assert options.build is None or options.build.sgx_simulation
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_check_local_certificate(sgx):
|
|
163
|
+
# A fresh simulator, and every test shares CERT_PATH: one client certificate
|
|
164
|
+
check = sgx.check_local_certificate(expected_number=1)
|
|
165
|
+
assert check.outcome is CertificateMatch.MATCH
|
|
166
|
+
assert check.info.server_certificates_number == 1
|
|
167
|
+
assert check.info.newest_certificate.status == 'V'
|
|
168
|
+
|
|
169
|
+
|
|
154
170
|
def test_sign_message(sgx, account, w3):
|
|
155
171
|
key, address = account
|
|
156
172
|
txn = generate_tx(w3, tx_type=None, no_fee=True)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from sgx.sgx_zmq import SgxZmqServerError
|
|
1
|
+
import binascii
|
|
2
|
+
import hashlib
|
|
4
3
|
import os
|
|
5
|
-
from time import sleep
|
|
6
|
-
from dotenv import load_dotenv
|
|
7
4
|
import random
|
|
8
|
-
import coincurve
|
|
9
|
-
import binascii
|
|
10
5
|
import secrets
|
|
11
|
-
import
|
|
6
|
+
from time import sleep
|
|
7
|
+
|
|
8
|
+
import coincurve
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from sgx import SgxClient
|
|
12
|
+
from sgx.sgx_rpc_handler import SgxServerError
|
|
13
|
+
from sgx.sgx_zmq import SgxZmqServerError
|
|
12
14
|
|
|
13
15
|
load_dotenv()
|
|
14
16
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
import mock
|
|
2
4
|
import pytest
|
|
3
5
|
import requests
|
|
4
6
|
import urllib3
|
|
5
7
|
|
|
6
|
-
from sgx.http import send_request
|
|
7
|
-
from sgx.http import SgxUnreachableError
|
|
8
|
+
from sgx.http import SgxUnreachableError, send_request
|
|
8
9
|
|
|
9
10
|
URL = 'http://127.0.0.1:1026'
|
|
10
11
|
INVALID_URL = 'http://127.0.0.1:1033'
|
|
@@ -32,3 +33,13 @@ def test_send_request_failed_sgx_up():
|
|
|
32
33
|
with mock.patch('requests.post', post_mock):
|
|
33
34
|
with pytest.raises(SgxUnreachableError):
|
|
34
35
|
send_request(URL, method, params)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_send_request_logs_redacted_params(caplog):
|
|
39
|
+
response = mock.Mock(**{'json.return_value': {'result': {'status': 0}}})
|
|
40
|
+
with mock.patch('requests.post', return_value=response), caplog.at_level(logging.INFO):
|
|
41
|
+
send_request(URL, 'importBLSKeyShare', {'keyShare': 'a' * 64, 'keyShareName': 'b' * 64})
|
|
42
|
+
assert "'keyShare': '<redacted>'" in caplog.text
|
|
43
|
+
assert 'a' * 50 not in caplog.text
|
|
44
|
+
assert 'b' * 50 + '...' in caplog.text
|
|
45
|
+
assert 'b' * 51 not in caplog.text
|
sgx_py-0.12.dev0/PKG-INFO
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sgx.py
|
|
3
|
-
Version: 0.12.dev0
|
|
4
|
-
Summary: SGX
|
|
5
|
-
Home-page: http://github.com/skalenetwork/sgx.py
|
|
6
|
-
Author: SKALE Labs
|
|
7
|
-
Author-email: support@skalelabs.com
|
|
8
|
-
Requires-Python: >=3.11,<4
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: web3<8.0.0,>=6.20.2
|
|
11
|
-
Requires-Dist: pyzmq==27.1.0
|
|
12
|
-
Requires-Dist: pem==23.1.0
|
|
13
|
-
Requires-Dist: cryptography<47.0.0,>=46.0.1
|
|
14
|
-
Provides-Extra: linter
|
|
15
|
-
Requires-Dist: ruff==0.13.2; extra == "linter"
|
|
16
|
-
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: ruff==0.13.2; extra == "dev"
|
|
18
|
-
Requires-Dist: coincurve==13.0.0; extra == "dev"
|
|
19
|
-
Requires-Dist: python-dotenv==0.13.0; extra == "dev"
|
|
20
|
-
Requires-Dist: twine==6.2.0; extra == "dev"
|
|
21
|
-
Requires-Dist: pytest==8.4.2; extra == "dev"
|
|
22
|
-
Requires-Dist: mock==5.2.0; extra == "dev"
|
|
23
|
-
Dynamic: author
|
|
24
|
-
Dynamic: author-email
|
|
25
|
-
Dynamic: home-page
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
Dynamic: provides-extra
|
|
28
|
-
Dynamic: requires-dist
|
|
29
|
-
Dynamic: requires-python
|
|
30
|
-
Dynamic: summary
|
sgx_py-0.12.dev0/README.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# sgx.py
|
|
2
|
-
[](https://discord.gg/vvUtWJB)
|
|
3
|
-
|
|
4
|
-
## Requirements
|
|
5
|
-
|
|
6
|
-
|**Tool**|**Minimum Supported Version**|
|
|
7
|
-
|--------|-----------|
|
|
8
|
-
| Python | 3.11 |
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
### Install Python 3.11
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
# confirm prerequisites are isntalled
|
|
18
|
-
sudo apt update
|
|
19
|
-
sudo apt install software-properties-common
|
|
20
|
-
# Add deadsnakes PPA - widely used for Python versions
|
|
21
|
-
sudo add-apt-repository ppa:deadsnakes/ppa
|
|
22
|
-
# update again & install python 3.11
|
|
23
|
-
sudo apt update
|
|
24
|
-
sudo apt install python3.11
|
|
25
|
-
# Check if it was installed
|
|
26
|
-
python3.11 --version
|
|
27
|
-
|
|
28
|
-
pip install sgx.py
|
|
29
|
-
```
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Developers
|
|
33
|
-
### Install all python libraries
|
|
34
|
-
```bash
|
|
35
|
-
sudo apt-get update --fix-missing
|
|
36
|
-
sudo apt-get install libudev-dev
|
|
37
|
-
sudo apt-get install swig
|
|
38
|
-
|
|
39
|
-
# create virtual environment - install dependencies locally
|
|
40
|
-
python3.11 -m venv venv
|
|
41
|
-
source venv/bin/activate
|
|
42
|
-
pip install -e .
|
|
43
|
-
pip install -e .[dev]
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Running Tests
|
|
47
|
-
|
|
48
|
-
Create a `.env` file in source directory, like the following:
|
|
49
|
-
```bash
|
|
50
|
-
# SGXWallet listening point (1026 uses https w/ certificates)
|
|
51
|
-
SERVER=https://127.0.0.1:1026
|
|
52
|
-
# path where you want tests to store the auto-generated certificates
|
|
53
|
-
CERT_PATH=.
|
|
54
|
-
# private key for the funded test account used by transaction tests
|
|
55
|
-
ETH_PRIVATE_KEY=
|
|
56
|
-
# Ethereum JSON-RPC endpoint used by transaction tests
|
|
57
|
-
GETH=
|
|
58
|
-
```
|
|
59
|
-
You may alter the fields as needed.
|
|
60
|
-
|
|
61
|
-
Run the tests:
|
|
62
|
-
```bash
|
|
63
|
-
# Run all tests
|
|
64
|
-
pytest
|
|
65
|
-
|
|
66
|
-
# Run specific test file
|
|
67
|
-
pytest ./tests/<test-file-name>.py
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## License
|
|
72
|
-
|
|
73
|
-
[](LICENSE)
|
|
74
|
-
|
|
75
|
-
Copyright (C) 2019-present SKALE Labs
|
sgx_py-0.12.dev0/sgx/__init__.py
DELETED
sgx_py-0.12.dev0/sgx/generate.sh
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
openssl req -new -sha256 -nodes -out $1 -newkey rsa:2048 -keyout $2 -subj /CN=$3
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sgx.py
|
|
3
|
-
Version: 0.12.dev0
|
|
4
|
-
Summary: SGX
|
|
5
|
-
Home-page: http://github.com/skalenetwork/sgx.py
|
|
6
|
-
Author: SKALE Labs
|
|
7
|
-
Author-email: support@skalelabs.com
|
|
8
|
-
Requires-Python: >=3.11,<4
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: web3<8.0.0,>=6.20.2
|
|
11
|
-
Requires-Dist: pyzmq==27.1.0
|
|
12
|
-
Requires-Dist: pem==23.1.0
|
|
13
|
-
Requires-Dist: cryptography<47.0.0,>=46.0.1
|
|
14
|
-
Provides-Extra: linter
|
|
15
|
-
Requires-Dist: ruff==0.13.2; extra == "linter"
|
|
16
|
-
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: ruff==0.13.2; extra == "dev"
|
|
18
|
-
Requires-Dist: coincurve==13.0.0; extra == "dev"
|
|
19
|
-
Requires-Dist: python-dotenv==0.13.0; extra == "dev"
|
|
20
|
-
Requires-Dist: twine==6.2.0; extra == "dev"
|
|
21
|
-
Requires-Dist: pytest==8.4.2; extra == "dev"
|
|
22
|
-
Requires-Dist: mock==5.2.0; extra == "dev"
|
|
23
|
-
Dynamic: author
|
|
24
|
-
Dynamic: author-email
|
|
25
|
-
Dynamic: home-page
|
|
26
|
-
Dynamic: license-file
|
|
27
|
-
Dynamic: provides-extra
|
|
28
|
-
Dynamic: requires-dist
|
|
29
|
-
Dynamic: requires-python
|
|
30
|
-
Dynamic: summary
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|