sgx.py 0.12.dev1__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.dev1/sgx.py.egg-info → sgx_py-0.12.dev2}/PKG-INFO +1 -1
- sgx_py-0.12.dev2/README.md +112 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/setup.py +1 -1
- sgx_py-0.12.dev2/sgx/__init__.py +5 -0
- sgx_py-0.12.dev2/sgx/certificates.py +134 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/http.py +9 -3
- sgx_py-0.12.dev2/sgx/options.py +104 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/sgx.py +13 -2
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/sgx_rpc_handler.py +34 -6
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2/sgx.py.egg-info}/PKG-INFO +1 -1
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx.py.egg-info/SOURCES.txt +2 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/tests/test_core.py +21 -1
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/tests/test_http.py +12 -0
- sgx_py-0.12.dev1/README.md +0 -75
- sgx_py-0.12.dev1/sgx/__init__.py +0 -2
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/LICENSE +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/setup.cfg +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/constants.py +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/generate.sh +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/sgx_zmq.py +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx/utils.py +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx.py.egg-info/dependency_links.txt +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx.py.egg-info/requires.txt +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/sgx.py.egg-info/top_level.txt +0 -0
- {sgx_py-0.12.dev1 → sgx_py-0.12.dev2}/tests/test_dkg.py +0 -0
|
@@ -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
|
|
@@ -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)
|
|
@@ -11,6 +11,7 @@ import requests
|
|
|
11
11
|
import urllib3
|
|
12
12
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
13
13
|
|
|
14
|
+
from sgx.certificates import SgxCertificateError
|
|
14
15
|
from sgx.constants import (
|
|
15
16
|
CRT_FILENAME,
|
|
16
17
|
CSR_FILENAME,
|
|
@@ -56,11 +57,15 @@ def enrollment_lock(crt_dir_path: str):
|
|
|
56
57
|
os.close(fd)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def get_certificate_credentials(crt_dir_path, csr_server):
|
|
60
|
+
def get_certificate_credentials(crt_dir_path, csr_server, allow_registration=True):
|
|
60
61
|
key_path = os.path.join(crt_dir_path, KEY_FILENAME)
|
|
61
62
|
crt_path = os.path.join(crt_dir_path, CRT_FILENAME)
|
|
62
63
|
if certificate_exists(crt_dir_path):
|
|
63
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
|
+
)
|
|
64
69
|
with enrollment_lock(crt_dir_path):
|
|
65
70
|
if not certificate_exists(crt_dir_path):
|
|
66
71
|
csr_path = os.path.join(crt_dir_path, CSR_FILENAME)
|
|
@@ -104,7 +109,7 @@ def sign_certificate(csr_server, csr_path):
|
|
|
104
109
|
return csr_hash
|
|
105
110
|
|
|
106
111
|
|
|
107
|
-
def send_request(url, method, params, path_to_cert=None):
|
|
112
|
+
def send_request(url, method, params, path_to_cert=None, allow_registration=True):
|
|
108
113
|
headers = {'content-type': 'application/json'}
|
|
109
114
|
call_data = {
|
|
110
115
|
"id": 0,
|
|
@@ -117,7 +122,8 @@ def send_request(url, method, params, path_to_cert=None):
|
|
|
117
122
|
if path_to_cert:
|
|
118
123
|
cert = get_certificate_credentials(
|
|
119
124
|
path_to_cert,
|
|
120
|
-
get_cert_provider(url)
|
|
125
|
+
get_cert_provider(url),
|
|
126
|
+
allow_registration
|
|
121
127
|
)
|
|
122
128
|
|
|
123
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)
|
|
@@ -53,9 +53,11 @@ class ComplaintResponse:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
class SgxClient:
|
|
56
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self, sgx_endpoint, path_to_cert=None, n=None, t=None, zmq=False, allow_registration=True
|
|
58
|
+
):
|
|
57
59
|
self.sgx_endpoint = sgx_endpoint
|
|
58
|
-
self.sgx_rpc_server = SgxRPCHandler(sgx_endpoint, path_to_cert)
|
|
60
|
+
self.sgx_rpc_server = SgxRPCHandler(sgx_endpoint, path_to_cert, allow_registration)
|
|
59
61
|
if zmq:
|
|
60
62
|
self.zmq = SgxZmq(sgx_endpoint, path_to_cert, n, t)
|
|
61
63
|
if not path_to_cert:
|
|
@@ -151,6 +153,15 @@ class SgxClient:
|
|
|
151
153
|
def get_server_version(self):
|
|
152
154
|
return self.sgx_rpc_server.get_server_version()
|
|
153
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
|
+
|
|
154
165
|
def verify_secret_share(self, public_shares, eth_key_name, secret_share, index):
|
|
155
166
|
return self.sgx_rpc_server.verify_secret_share(
|
|
156
167
|
public_shares, eth_key_name, secret_share, self.n, self.t, index
|
|
@@ -21,16 +21,24 @@ import logging
|
|
|
21
21
|
from enum import Enum
|
|
22
22
|
from urllib.parse import urlparse
|
|
23
23
|
|
|
24
|
+
from sgx.certificates import IssuedCertificatesInfo, load_local_certificate
|
|
24
25
|
from sgx.http import certificate_exists, send_request
|
|
26
|
+
from sgx.options import ServerOptions
|
|
25
27
|
from sgx.utils import SgxError
|
|
26
28
|
|
|
27
29
|
logger = logging.getLogger(__name__)
|
|
28
30
|
|
|
31
|
+
METHOD_NOT_FOUND = -32601
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
class SgxServerError(SgxError):
|
|
31
35
|
pass
|
|
32
36
|
|
|
33
37
|
|
|
38
|
+
class SgxMethodNotFoundError(SgxServerError):
|
|
39
|
+
"""The server does not implement the method, e.g. an sgxwallet older than 1.10.4."""
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
class DkgPolyStatus(Enum):
|
|
35
43
|
FAIL = 0
|
|
36
44
|
NEW_GENERATED = 1
|
|
@@ -38,10 +46,11 @@ class DkgPolyStatus(Enum):
|
|
|
38
46
|
|
|
39
47
|
|
|
40
48
|
class SgxRPCHandler:
|
|
41
|
-
def __init__(self, sgx_endpoint, path_to_cert):
|
|
49
|
+
def __init__(self, sgx_endpoint, path_to_cert, allow_registration=True):
|
|
42
50
|
self.sgx_endpoint = check_provider(sgx_endpoint)
|
|
43
51
|
self.path_to_cert = path_to_cert
|
|
44
|
-
|
|
52
|
+
self.allow_registration = allow_registration
|
|
53
|
+
if allow_registration and path_to_cert and not certificate_exists(path_to_cert):
|
|
45
54
|
self.get_server_status()
|
|
46
55
|
|
|
47
56
|
def ecdsa_sign(self, key_name, transaction_hash):
|
|
@@ -130,6 +139,21 @@ class SgxRPCHandler:
|
|
|
130
139
|
response = self.__send_request("getServerVersion")
|
|
131
140
|
return response['result']['version']
|
|
132
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
|
+
|
|
133
157
|
def verify_secret_share(self, public_shares, eth_key_name, secret_share, n, t, index):
|
|
134
158
|
params = dict()
|
|
135
159
|
params['publicShares'] = public_shares
|
|
@@ -267,10 +291,14 @@ class SgxRPCHandler:
|
|
|
267
291
|
|
|
268
292
|
return result
|
|
269
293
|
|
|
270
|
-
def __send_request(self, method, params=None):
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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'])
|
|
274
302
|
if response['result']['status']:
|
|
275
303
|
raise SgxServerError(response['result']['errorMessage'])
|
|
276
304
|
return response
|
|
@@ -11,13 +11,14 @@ from eth_keys import keys
|
|
|
11
11
|
from hexbytes import HexBytes
|
|
12
12
|
from web3 import Web3
|
|
13
13
|
|
|
14
|
-
from sgx import SgxClient
|
|
14
|
+
from sgx import CertificateMatch, SgxClient
|
|
15
15
|
|
|
16
16
|
load_dotenv()
|
|
17
17
|
|
|
18
18
|
SGX_URL = os.getenv('SERVER')
|
|
19
19
|
GETH_URL = os.getenv('GETH')
|
|
20
20
|
ETH_PRIVATE_KEY = os.getenv('ETH_PRIVATE_KEY')
|
|
21
|
+
EXPECT_ZMQ_OWNERSHIP = os.getenv('SGX_EXPECT_ZMQ_OWNERSHIP') == '1'
|
|
21
22
|
|
|
22
23
|
ETH_VALUE_FOR_TESTS = 5 * 10**18
|
|
23
24
|
|
|
@@ -147,6 +148,25 @@ def test_get_server_version(sgx):
|
|
|
147
148
|
assert isinstance(sgx.get_server_version(), str)
|
|
148
149
|
|
|
149
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
|
+
|
|
150
170
|
def test_sign_message(sgx, account, w3):
|
|
151
171
|
key, address = account
|
|
152
172
|
txn = generate_tx(w3, tx_type=None, no_fee=True)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
import mock
|
|
2
4
|
import pytest
|
|
3
5
|
import requests
|
|
@@ -31,3 +33,13 @@ def test_send_request_failed_sgx_up():
|
|
|
31
33
|
with mock.patch('requests.post', post_mock):
|
|
32
34
|
with pytest.raises(SgxUnreachableError):
|
|
33
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.dev1/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.dev1/sgx/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|