saro-dat 1.0.0__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.
- saro_dat-1.0.0/LICENSE +21 -0
- saro_dat-1.0.0/PKG-INFO +76 -0
- saro_dat-1.0.0/README.md +60 -0
- saro_dat-1.0.0/pyproject.toml +22 -0
- saro_dat-1.0.0/setup.cfg +4 -0
- saro_dat-1.0.0/src/saro_dat/__init__.py +17 -0
- saro_dat-1.0.0/src/saro_dat/crypto.py +79 -0
- saro_dat-1.0.0/src/saro_dat/dat.py +60 -0
- saro_dat-1.0.0/src/saro_dat/dat_certificate.py +57 -0
- saro_dat-1.0.0/src/saro_dat/dat_manager.py +115 -0
- saro_dat-1.0.0/src/saro_dat/signature.py +174 -0
- saro_dat-1.0.0/src/saro_dat/util.py +30 -0
- saro_dat-1.0.0/src/saro_dat.egg-info/PKG-INFO +76 -0
- saro_dat-1.0.0/src/saro_dat.egg-info/SOURCES.txt +22 -0
- saro_dat-1.0.0/src/saro_dat.egg-info/dependency_links.txt +1 -0
- saro_dat-1.0.0/src/saro_dat.egg-info/requires.txt +2 -0
- saro_dat-1.0.0/src/saro_dat.egg-info/top_level.txt +1 -0
- saro_dat-1.0.0/tests/test_bench.py +94 -0
- saro_dat-1.0.0/tests/test_certificate.py +74 -0
- saro_dat-1.0.0/tests/test_crypto.py +48 -0
- saro_dat-1.0.0/tests/test_example.py +66 -0
- saro_dat-1.0.0/tests/test_manager.py +75 -0
- saro_dat-1.0.0/tests/test_signature.py +48 -0
- saro_dat-1.0.0/tests/test_util.py +26 -0
saro_dat-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SARO Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
saro_dat-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saro-dat
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Distributed Access Token
|
|
5
|
+
Author-email: Marker Seoul <j@saro.me>
|
|
6
|
+
Project-URL: Homepage, https://dat.saro.me
|
|
7
|
+
Project-URL: Repository, https://github.com/saro-lab/dat-pypi
|
|
8
|
+
Project-URL: Documentation, https://dat.saro.me/ko/libs/pypi-saro-dat
|
|
9
|
+
Keywords: dat,distributed,access,token,jwt,security,authentication
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: cryptography>=47.0.0
|
|
14
|
+
Requires-Dist: readerwriterlock>=1.0.9
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# DAT - Distributed Access Token
|
|
18
|
+
|
|
19
|
+
## Document
|
|
20
|
+
|
|
21
|
+
### [DAT Run Online](https://dat.saro.me)
|
|
22
|
+
|
|
23
|
+
### [What is DAT](https://dat.saro.me/--/intro)
|
|
24
|
+
|
|
25
|
+
### [Example](https://dat.saro.me/--/libs/pypi-saro-dat)
|
|
26
|
+
|
|
27
|
+
## support signature algorithm
|
|
28
|
+
| name | algorithm |
|
|
29
|
+
|--------|------------|
|
|
30
|
+
| P256 | secp256r1 |
|
|
31
|
+
| P384 | secp384r1 |
|
|
32
|
+
| P521 | secp521r1 |
|
|
33
|
+
|
|
34
|
+
## support crypto algorithm
|
|
35
|
+
| name | algorithm |
|
|
36
|
+
|------------|-----------------------------|
|
|
37
|
+
| AES128GCMN | aes-128-gcm n(nonce + body) |
|
|
38
|
+
| AES256GCMN | aes-256-cbc n(nonce + body) |
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Performance
|
|
42
|
+
- random plain and secure test
|
|
43
|
+
- mac mini m4 2024 basic (10 core)
|
|
44
|
+
- [test_bench.py](tests/test_bench.py)
|
|
45
|
+
```
|
|
46
|
+
Plain: A9wAt86DfDVQCXzfijnfB7j5GWk9TOO4lapQS7AzYenPoRAELwaiqKa8IikDmT8FAfNZJFocR66Rvqcae3JcSf3OVIppE0lYDg4G
|
|
47
|
+
Secure: R2KJfHAFClJdS0je4TkU5JoTDfRMGjp5y5zn5sG2iCwsL6IVhjzCOXaVcQPAJwqiEJGDmd3Rdl6tCI0KwAcsjD4ZrqL3IEL5Jr2Q
|
|
48
|
+
|
|
49
|
+
Multi-Thread
|
|
50
|
+
P256 AES128GCMN Issue * 10000 : 192ms
|
|
51
|
+
P256 AES128GCMN Parse * 10000 : 171ms
|
|
52
|
+
P256 AES256GCMN Issue * 10000 : 191ms
|
|
53
|
+
P256 AES256GCMN Parse * 10000 : 168ms
|
|
54
|
+
P384 AES128GCMN Issue * 10000 : 847ms
|
|
55
|
+
P384 AES128GCMN Parse * 10000 : 1841ms
|
|
56
|
+
P384 AES256GCMN Issue * 10000 : 789ms
|
|
57
|
+
P384 AES256GCMN Parse * 10000 : 1829ms
|
|
58
|
+
P521 AES128GCMN Issue * 10000 : 684ms
|
|
59
|
+
P521 AES128GCMN Parse * 10000 : 1355ms
|
|
60
|
+
P521 AES256GCMN Issue * 10000 : 683ms
|
|
61
|
+
P521 AES256GCMN Parse * 10000 : 1343ms
|
|
62
|
+
|
|
63
|
+
Single-Thread
|
|
64
|
+
P256 AES128GCMN Issue * 10000 : 223ms
|
|
65
|
+
P256 AES128GCMN Parse * 10000 : 447ms
|
|
66
|
+
P256 AES256GCMN Issue * 10000 : 213ms
|
|
67
|
+
P256 AES256GCMN Parse * 10000 : 450ms
|
|
68
|
+
P384 AES128GCMN Issue * 10000 : 4915ms
|
|
69
|
+
P384 AES128GCMN Parse * 10000 : 11750ms
|
|
70
|
+
P384 AES256GCMN Issue * 10000 : 4915ms
|
|
71
|
+
P384 AES256GCMN Parse * 10000 : 11705ms
|
|
72
|
+
P521 AES128GCMN Issue * 10000 : 3576ms
|
|
73
|
+
P521 AES128GCMN Parse * 10000 : 7219ms
|
|
74
|
+
P521 AES256GCMN Issue * 10000 : 3623ms
|
|
75
|
+
P521 AES256GCMN Parse * 10000 : 7246ms
|
|
76
|
+
```
|
saro_dat-1.0.0/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# DAT - Distributed Access Token
|
|
2
|
+
|
|
3
|
+
## Document
|
|
4
|
+
|
|
5
|
+
### [DAT Run Online](https://dat.saro.me)
|
|
6
|
+
|
|
7
|
+
### [What is DAT](https://dat.saro.me/--/intro)
|
|
8
|
+
|
|
9
|
+
### [Example](https://dat.saro.me/--/libs/pypi-saro-dat)
|
|
10
|
+
|
|
11
|
+
## support signature algorithm
|
|
12
|
+
| name | algorithm |
|
|
13
|
+
|--------|------------|
|
|
14
|
+
| P256 | secp256r1 |
|
|
15
|
+
| P384 | secp384r1 |
|
|
16
|
+
| P521 | secp521r1 |
|
|
17
|
+
|
|
18
|
+
## support crypto algorithm
|
|
19
|
+
| name | algorithm |
|
|
20
|
+
|------------|-----------------------------|
|
|
21
|
+
| AES128GCMN | aes-128-gcm n(nonce + body) |
|
|
22
|
+
| AES256GCMN | aes-256-cbc n(nonce + body) |
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Performance
|
|
26
|
+
- random plain and secure test
|
|
27
|
+
- mac mini m4 2024 basic (10 core)
|
|
28
|
+
- [test_bench.py](tests/test_bench.py)
|
|
29
|
+
```
|
|
30
|
+
Plain: A9wAt86DfDVQCXzfijnfB7j5GWk9TOO4lapQS7AzYenPoRAELwaiqKa8IikDmT8FAfNZJFocR66Rvqcae3JcSf3OVIppE0lYDg4G
|
|
31
|
+
Secure: R2KJfHAFClJdS0je4TkU5JoTDfRMGjp5y5zn5sG2iCwsL6IVhjzCOXaVcQPAJwqiEJGDmd3Rdl6tCI0KwAcsjD4ZrqL3IEL5Jr2Q
|
|
32
|
+
|
|
33
|
+
Multi-Thread
|
|
34
|
+
P256 AES128GCMN Issue * 10000 : 192ms
|
|
35
|
+
P256 AES128GCMN Parse * 10000 : 171ms
|
|
36
|
+
P256 AES256GCMN Issue * 10000 : 191ms
|
|
37
|
+
P256 AES256GCMN Parse * 10000 : 168ms
|
|
38
|
+
P384 AES128GCMN Issue * 10000 : 847ms
|
|
39
|
+
P384 AES128GCMN Parse * 10000 : 1841ms
|
|
40
|
+
P384 AES256GCMN Issue * 10000 : 789ms
|
|
41
|
+
P384 AES256GCMN Parse * 10000 : 1829ms
|
|
42
|
+
P521 AES128GCMN Issue * 10000 : 684ms
|
|
43
|
+
P521 AES128GCMN Parse * 10000 : 1355ms
|
|
44
|
+
P521 AES256GCMN Issue * 10000 : 683ms
|
|
45
|
+
P521 AES256GCMN Parse * 10000 : 1343ms
|
|
46
|
+
|
|
47
|
+
Single-Thread
|
|
48
|
+
P256 AES128GCMN Issue * 10000 : 223ms
|
|
49
|
+
P256 AES128GCMN Parse * 10000 : 447ms
|
|
50
|
+
P256 AES256GCMN Issue * 10000 : 213ms
|
|
51
|
+
P256 AES256GCMN Parse * 10000 : 450ms
|
|
52
|
+
P384 AES128GCMN Issue * 10000 : 4915ms
|
|
53
|
+
P384 AES128GCMN Parse * 10000 : 11750ms
|
|
54
|
+
P384 AES256GCMN Issue * 10000 : 4915ms
|
|
55
|
+
P384 AES256GCMN Parse * 10000 : 11705ms
|
|
56
|
+
P521 AES128GCMN Issue * 10000 : 3576ms
|
|
57
|
+
P521 AES128GCMN Parse * 10000 : 7219ms
|
|
58
|
+
P521 AES256GCMN Issue * 10000 : 3623ms
|
|
59
|
+
P521 AES256GCMN Parse * 10000 : 7246ms
|
|
60
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "saro-dat"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Distributed Access Token"
|
|
5
|
+
keywords = ["dat", "distributed", "access", "token", "jwt", "security", "authentication"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
authors = [{name = "Marker Seoul", email = "j@saro.me"}]
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"cryptography>=47.0.0",
|
|
11
|
+
"readerwriterlock>=1.0.9",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
"Homepage" = "https://dat.saro.me"
|
|
16
|
+
"Repository" = "https://github.com/saro-lab/dat-pypi"
|
|
17
|
+
"Documentation" = "https://dat.saro.me/ko/libs/pypi-saro-dat"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.4.2",
|
|
22
|
+
]
|
saro_dat-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .crypto import DatCryptoAlgorithm, DatCryptoKey
|
|
2
|
+
from .dat import Dat, DatPayload
|
|
3
|
+
from .dat_certificate import DatCertificate
|
|
4
|
+
from .dat_manager import DatManager
|
|
5
|
+
from .signature import DatSignatureAlgorithm, DatSignatureKey, DatSignatureKeyOutOption
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DatManager",
|
|
9
|
+
"DatCertificate",
|
|
10
|
+
"Dat",
|
|
11
|
+
"DatPayload",
|
|
12
|
+
"DatCryptoKey",
|
|
13
|
+
"DatCryptoAlgorithm",
|
|
14
|
+
"DatSignatureKey",
|
|
15
|
+
"DatSignatureAlgorithm",
|
|
16
|
+
"DatSignatureKeyOutOption",
|
|
17
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Union, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
6
|
+
|
|
7
|
+
from .util import decode_base64_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DatCryptoAlgorithm(str, Enum):
|
|
11
|
+
AES128GCMN = "AES128GCMN"
|
|
12
|
+
AES256GCMN = "AES256GCMN"
|
|
13
|
+
|
|
14
|
+
CRYPTO_CONFIG: Dict[str, dict] = {
|
|
15
|
+
"AES128GCMN": {"name": "AES-GCM", "length": 16},
|
|
16
|
+
"AES256GCMN": {"name": "AES-GCM", "length": 32},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def get_crypto_config(algorithm: str) -> dict:
|
|
20
|
+
config = CRYPTO_CONFIG.get(algorithm)
|
|
21
|
+
if config:
|
|
22
|
+
return config
|
|
23
|
+
raise ValueError(f"Unsupported DAT Crypto Algorithm: {algorithm}")
|
|
24
|
+
|
|
25
|
+
class DatCryptoKey:
|
|
26
|
+
def __init__(self, algorithm: DatCryptoAlgorithm, key_bytes: bytes, config: Optional[Dict[str, dict]] = None):
|
|
27
|
+
if config is None:
|
|
28
|
+
config = get_crypto_config(algorithm)
|
|
29
|
+
self.algorithm = algorithm
|
|
30
|
+
self._config = config
|
|
31
|
+
self._key_bytes = key_bytes
|
|
32
|
+
self._cipher = AESGCM(key_bytes)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def generate(cls, algorithm: DatCryptoAlgorithm) -> DatCryptoKey:
|
|
36
|
+
config = get_crypto_config(algorithm)
|
|
37
|
+
key_bytes = AESGCM.generate_key(bit_length=config['length'] * 8)
|
|
38
|
+
return cls(algorithm, key_bytes, config)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def imports(cls, algorithm: str, raw: bytes) -> DatCryptoKey:
|
|
42
|
+
return cls(DatCryptoAlgorithm(algorithm), raw)
|
|
43
|
+
|
|
44
|
+
def exports(self) -> bytes:
|
|
45
|
+
return self._key_bytes
|
|
46
|
+
|
|
47
|
+
def encrypt(self, data: Union[bytes, str, None]) -> bytes:
|
|
48
|
+
if isinstance(data, str):
|
|
49
|
+
data = data.encode('utf-8')
|
|
50
|
+
|
|
51
|
+
if not data:
|
|
52
|
+
return b""
|
|
53
|
+
|
|
54
|
+
#if self._config["name"] == "AES-GCM":
|
|
55
|
+
|
|
56
|
+
nonce = os.urandom(12)
|
|
57
|
+
ciphertext = self._cipher.encrypt(nonce, data, None)
|
|
58
|
+
return nonce + ciphertext
|
|
59
|
+
|
|
60
|
+
#raise ValueError(f"Unsupported DAT Crypto Algorithm: {self.algorithm}")
|
|
61
|
+
|
|
62
|
+
def decrypt(self, data: Union[bytes, str, None]) -> bytes:
|
|
63
|
+
if isinstance(data, str):
|
|
64
|
+
data = decode_base64_url(data)
|
|
65
|
+
|
|
66
|
+
if not data:
|
|
67
|
+
return b""
|
|
68
|
+
|
|
69
|
+
#if self._config["name"] == "AES-GCM":
|
|
70
|
+
|
|
71
|
+
if len(data) <= 12:
|
|
72
|
+
raise ValueError("Invalid data length")
|
|
73
|
+
|
|
74
|
+
nonce = data[:12]
|
|
75
|
+
ciphertext_with_tag = data[12:]
|
|
76
|
+
|
|
77
|
+
return self._cipher.decrypt(nonce, ciphertext_with_tag, None)
|
|
78
|
+
|
|
79
|
+
#raise ValueError(f"Unsupported DAT Crypto Algorithm: {self.algorithm}")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from .util import decode_base64_url
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Dat:
|
|
8
|
+
def __init__(self, dat_str: Optional[str]):
|
|
9
|
+
self.dat = dat_str or ''
|
|
10
|
+
self._format = False
|
|
11
|
+
self._expire = 0
|
|
12
|
+
self._cid = 0
|
|
13
|
+
self._plain = b''
|
|
14
|
+
self._secure = b''
|
|
15
|
+
self._signature = b''
|
|
16
|
+
|
|
17
|
+
if self.dat:
|
|
18
|
+
parts = self.dat.split('.')
|
|
19
|
+
if len(parts) == 5:
|
|
20
|
+
try:
|
|
21
|
+
# parts: [expire, cid(hex), plain(b64), secure(b64), signature(b64)]
|
|
22
|
+
self._expire = int(parts[0])
|
|
23
|
+
self._cid = int(parts[1], 16)
|
|
24
|
+
self._plain = decode_base64_url(parts[2])
|
|
25
|
+
self._secure = decode_base64_url(parts[3])
|
|
26
|
+
self._signature = decode_base64_url(parts[4])
|
|
27
|
+
self._format = (len(self._signature) > 0 and self._expire >= 0)
|
|
28
|
+
except (ValueError, RuntimeError):
|
|
29
|
+
self._format = False
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_value(cls, value: Union[Dat, str, None]) -> Dat:
|
|
33
|
+
if isinstance(value, Dat):
|
|
34
|
+
return value
|
|
35
|
+
return cls(value)
|
|
36
|
+
|
|
37
|
+
def expired(self) -> bool:
|
|
38
|
+
if not self._format:
|
|
39
|
+
return True
|
|
40
|
+
return int(time.time()) > self._expire
|
|
41
|
+
|
|
42
|
+
def body_string(self) -> str:
|
|
43
|
+
"""서명 검증을 위한 서명 제외 나머지 본문 반환"""
|
|
44
|
+
if '.' not in self.dat:
|
|
45
|
+
return ""
|
|
46
|
+
return self.dat.rsplit('.', 1)[0]
|
|
47
|
+
|
|
48
|
+
class DatPayload:
|
|
49
|
+
def __init__(self, expire: int, plain: bytes, secure: bytes):
|
|
50
|
+
self.expire = expire
|
|
51
|
+
self.plain_bytes = plain
|
|
52
|
+
self.secure_bytes = secure
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def plain(self) -> str:
|
|
56
|
+
return self.plain_bytes.decode('utf-8')
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def secure(self) -> str:
|
|
60
|
+
return self.secure_bytes.decode('utf-8')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from .crypto import DatCryptoKey
|
|
4
|
+
from .signature import DatSignatureKey, DatSignatureKeyOutOption
|
|
5
|
+
from .util import encode_base64_url_str, decode_base64_url
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatCertificate:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
cid: int,
|
|
12
|
+
signature_key: DatSignatureKey,
|
|
13
|
+
crypto_key: DatCryptoKey,
|
|
14
|
+
dat_issue_begin: int,
|
|
15
|
+
dat_issue_end: int,
|
|
16
|
+
dat_ttl: int
|
|
17
|
+
):
|
|
18
|
+
self.cid = cid
|
|
19
|
+
self._signature_key = signature_key
|
|
20
|
+
self._crypto_key = crypto_key
|
|
21
|
+
self._dat_issue_begin = dat_issue_begin
|
|
22
|
+
self._dat_issue_end = dat_issue_end
|
|
23
|
+
self._dat_ttl = dat_ttl
|
|
24
|
+
|
|
25
|
+
def exports(self, option: DatSignatureKeyOutOption) -> str:
|
|
26
|
+
cid_hex = hex(self.cid)[2:]
|
|
27
|
+
sig_alg = self._signature_key.algorithm.value
|
|
28
|
+
sig_key = self._signature_key.exports(option)
|
|
29
|
+
cry_alg = self._crypto_key.algorithm.value
|
|
30
|
+
cry_key = encode_base64_url_str(self._crypto_key.exports())
|
|
31
|
+
|
|
32
|
+
return f"{cid_hex}.{sig_alg}.{sig_key}.{cry_alg}.{cry_key}.{self._dat_issue_begin}.{self._dat_issue_end}.{self._dat_ttl}"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def imports(cls, format_str: str) -> DatCertificate:
|
|
36
|
+
split = format_str.split(".")
|
|
37
|
+
if len(split) != 8:
|
|
38
|
+
raise ValueError("Invalid Certificate format")
|
|
39
|
+
|
|
40
|
+
cid = int(split[0], 16)
|
|
41
|
+
sig_key = DatSignatureKey.imports(split[1], split[2])
|
|
42
|
+
cry_key = DatCryptoKey.imports(split[3], decode_base64_url(split[4]))
|
|
43
|
+
|
|
44
|
+
return cls(
|
|
45
|
+
cid, sig_key, cry_key,
|
|
46
|
+
int(split[5]), int(split[6]), int(split[7])
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def issuable(self) -> bool:
|
|
50
|
+
now = int(time.time())
|
|
51
|
+
return self.has_signing_key() and self._dat_issue_begin <= now <= self._dat_issue_end
|
|
52
|
+
|
|
53
|
+
def expired(self) -> bool:
|
|
54
|
+
return int(time.time()) > (self._dat_issue_end + self._dat_ttl)
|
|
55
|
+
|
|
56
|
+
def has_signing_key(self) -> bool:
|
|
57
|
+
return self._signature_key.has_signing_key()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
|
|
4
|
+
from readerwriterlock import rwlock
|
|
5
|
+
|
|
6
|
+
from . import DatCertificate, Dat, DatPayload
|
|
7
|
+
from .signature import DatSignatureKeyOutOption
|
|
8
|
+
from .util import encode_base64_url_str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DatManager:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._issuer = None
|
|
14
|
+
self._certificates = []
|
|
15
|
+
self._lock = rwlock.RWLockFairD()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def import_certificates(self, input_certs: List[DatCertificate], clear: bool = False):
|
|
19
|
+
certificates = []
|
|
20
|
+
|
|
21
|
+
if not clear:
|
|
22
|
+
with self._lock.gen_rlock():
|
|
23
|
+
certificates.extend(self._certificates)
|
|
24
|
+
|
|
25
|
+
before_cids = set(map(lambda x: x.cid,certificates))
|
|
26
|
+
seen_cids = set()
|
|
27
|
+
|
|
28
|
+
for cert in input_certs:
|
|
29
|
+
if cert.cid in seen_cids:
|
|
30
|
+
raise ValueError(f"Duplicate CID: {cert.cid}")
|
|
31
|
+
seen_cids.add(cert.cid)
|
|
32
|
+
if cert.expired():
|
|
33
|
+
continue
|
|
34
|
+
if cert.cid in before_cids:
|
|
35
|
+
continue
|
|
36
|
+
certificates.append(cert)
|
|
37
|
+
|
|
38
|
+
certificates.sort(key=lambda x: x._dat_issue_end)
|
|
39
|
+
issuer = next((c for c in reversed(certificates) if c.issuable()), None)
|
|
40
|
+
|
|
41
|
+
with self._lock.gen_wlock():
|
|
42
|
+
self._issuer = issuer
|
|
43
|
+
self._certificates = certificates
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def imports(self, format_str: str, clear: bool = False):
|
|
47
|
+
certs = []
|
|
48
|
+
for line in format_str.strip().split('\n'):
|
|
49
|
+
if line.strip():
|
|
50
|
+
certs.append(DatCertificate.imports(line.strip()))
|
|
51
|
+
self.import_certificates(certs, clear)
|
|
52
|
+
|
|
53
|
+
def exports(self, option: DatSignatureKeyOutOption) -> str:
|
|
54
|
+
lines = []
|
|
55
|
+
|
|
56
|
+
with self._lock.gen_rlock():
|
|
57
|
+
for cert in self._certificates:
|
|
58
|
+
lines.append(cert.exports(option))
|
|
59
|
+
|
|
60
|
+
return '\n'.join(lines)
|
|
61
|
+
|
|
62
|
+
def _find_unsafe(self, cid: int) -> Optional[DatCertificate]:
|
|
63
|
+
return next((c for c in self._certificates if c.cid == cid), None)
|
|
64
|
+
|
|
65
|
+
def issue(self, plain: Union[bytes, str, None], secure: Union[bytes, str, None]) -> str:
|
|
66
|
+
with self._lock.gen_rlock():
|
|
67
|
+
if self._issuer:
|
|
68
|
+
return self._issue(self._issuer, plain, secure)
|
|
69
|
+
raise RuntimeError("Invalid DAT: Signing Key Does Not Exist")
|
|
70
|
+
|
|
71
|
+
def parse(self, dat_input: Union[Dat, str, None]) -> DatPayload:
|
|
72
|
+
dat = Dat.from_value(dat_input)
|
|
73
|
+
if not dat._format:
|
|
74
|
+
raise ValueError("Invalid DAT: Format")
|
|
75
|
+
|
|
76
|
+
with self._lock.gen_rlock():
|
|
77
|
+
certificate = self._find_unsafe(dat._cid)
|
|
78
|
+
if certificate is not None:
|
|
79
|
+
return self._parse(certificate, dat)
|
|
80
|
+
raise ValueError("Invalid DAT: CID(Certificate ID) Not Found")
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _issue(cert: DatCertificate, plain: Union[bytes, str, None], secure: Union[bytes, str, None]) -> str:
|
|
84
|
+
now = int(time.time())
|
|
85
|
+
expire = now + cert._dat_ttl
|
|
86
|
+
cid_hex = hex(cert.cid)[2:]
|
|
87
|
+
|
|
88
|
+
# Plain 데이터 처리 (문자열인 경우 utf-8 바이트로)
|
|
89
|
+
plain_bytes = plain.encode() if isinstance(plain, str) else (plain or b'')
|
|
90
|
+
plain_b64 = encode_base64_url_str(plain_bytes)
|
|
91
|
+
|
|
92
|
+
# Secure 데이터 암호화
|
|
93
|
+
encrypted_secure = cert._crypto_key.encrypt(secure)
|
|
94
|
+
secure_b64 = encode_base64_url_str(encrypted_secure)
|
|
95
|
+
|
|
96
|
+
body = f"{expire}.{cid_hex}.{plain_b64}.{secure_b64}"
|
|
97
|
+
signature = encode_base64_url_str(cert._signature_key.sign(body))
|
|
98
|
+
|
|
99
|
+
return f"{body}.{signature}"
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _parse(cert: DatCertificate, dat_input: Union[Dat, str, None]) -> DatPayload:
|
|
103
|
+
dat = Dat.from_value(dat_input)
|
|
104
|
+
if not dat._format:
|
|
105
|
+
raise RuntimeError("Invalid DAT: Format")
|
|
106
|
+
if dat.expired():
|
|
107
|
+
raise RuntimeError("Invalid DAT: Expired")
|
|
108
|
+
|
|
109
|
+
# 서명 검증
|
|
110
|
+
if not cert._signature_key.verify(dat.body_string(), dat._signature):
|
|
111
|
+
raise RuntimeError("Invalid DAT: Signature")
|
|
112
|
+
|
|
113
|
+
# 데이터 복호화
|
|
114
|
+
decrypted_secure = cert._crypto_key.decrypt(dat._secure)
|
|
115
|
+
return DatPayload(dat._expire, dat._plain, decrypted_secure)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Union, Optional, TypeAlias
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives import hashes
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
|
|
8
|
+
from cryptography.hazmat.primitives.hashes import HashAlgorithm
|
|
9
|
+
|
|
10
|
+
from .util import decode_base64_url, encode_base64_url_str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatSignatureAlgorithm(str, Enum):
|
|
14
|
+
P256 = "P256"
|
|
15
|
+
P384 = "P384"
|
|
16
|
+
P521 = "P521"
|
|
17
|
+
|
|
18
|
+
class DatSignatureKeyOutOption(str, Enum):
|
|
19
|
+
FULL = "FULL"
|
|
20
|
+
SIGNING = "SIGNING"
|
|
21
|
+
VERIFYING = "VERIFYING"
|
|
22
|
+
|
|
23
|
+
CurveType: TypeAlias = Union[EllipticCurve]
|
|
24
|
+
HashType: TypeAlias = Union[HashAlgorithm]
|
|
25
|
+
ConfigValue: TypeAlias = dict[str, Union[CurveType, HashAlgorithm]]
|
|
26
|
+
|
|
27
|
+
SIGNATURE_CONFIG: dict[str, ConfigValue] = {
|
|
28
|
+
"P256": {"curve": ec.SECP256R1(), "hash": hashes.SHA256()},
|
|
29
|
+
"P384": {"curve": ec.SECP384R1(), "hash": hashes.SHA384()},
|
|
30
|
+
"P521": {"curve": ec.SECP521R1(), "hash": hashes.SHA512()},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def get_signature_config(algorithm: str) -> dict:
|
|
34
|
+
config = SIGNATURE_CONFIG.get(algorithm)
|
|
35
|
+
if config:
|
|
36
|
+
return config
|
|
37
|
+
raise ValueError(f"Unsupported DAT Crypto Algorithm: {algorithm}")
|
|
38
|
+
|
|
39
|
+
class DatSignatureKey:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
algorithm: DatSignatureAlgorithm,
|
|
43
|
+
signing_key: Optional[ec.EllipticCurvePrivateKey],
|
|
44
|
+
verifying_key: ec.EllipticCurvePublicKey,
|
|
45
|
+
config: ConfigValue
|
|
46
|
+
):
|
|
47
|
+
if config is None:
|
|
48
|
+
config = get_signature_config(algorithm)
|
|
49
|
+
self.algorithm = algorithm
|
|
50
|
+
self.signing_key = signing_key
|
|
51
|
+
self.verifying_key = verifying_key
|
|
52
|
+
self._config = config
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def generate(algorithm: Union[DatSignatureAlgorithm, str]) -> DatSignatureKey:
|
|
56
|
+
config = get_signature_config(algorithm)
|
|
57
|
+
if isinstance(algorithm, str):
|
|
58
|
+
algorithm = DatSignatureAlgorithm(algorithm)
|
|
59
|
+
private_key = ec.generate_private_key(config["curve"])
|
|
60
|
+
public_key = private_key.public_key()
|
|
61
|
+
return DatSignatureKey(algorithm, private_key, public_key, config)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def imports(algorithm: Union[DatSignatureAlgorithm, str], format_str: str) -> DatSignatureKey:
|
|
65
|
+
config = get_signature_config(algorithm)
|
|
66
|
+
if isinstance(algorithm, str):
|
|
67
|
+
algorithm = DatSignatureAlgorithm(algorithm)
|
|
68
|
+
|
|
69
|
+
parts = format_str.split("~")
|
|
70
|
+
parts_len = len(parts)
|
|
71
|
+
|
|
72
|
+
if not (parts_len in range(1, 3)):
|
|
73
|
+
raise ValueError("Invalid DAT Signature Key Format: No keys found")
|
|
74
|
+
|
|
75
|
+
signing_key = None
|
|
76
|
+
verifying_key = None
|
|
77
|
+
|
|
78
|
+
if parts[0]: # 첫 번째 파트가 비어있지 않으면 개인키 존재
|
|
79
|
+
d_value = int.from_bytes(decode_base64_url(parts[0]), 'big')
|
|
80
|
+
signing_key = ec.derive_private_key(d_value, config["curve"])
|
|
81
|
+
|
|
82
|
+
# 2. Public Key (Verifying Key) 처리
|
|
83
|
+
if parts_len == 2 and parts[1]:
|
|
84
|
+
public_bytes = decode_base64_url(parts[1])
|
|
85
|
+
verifying_key = ec.EllipticCurvePublicKey.from_encoded_point(
|
|
86
|
+
config["curve"], public_bytes
|
|
87
|
+
)
|
|
88
|
+
elif signing_key:
|
|
89
|
+
verifying_key = signing_key.public_key()
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError("Invalid DAT Signature Key Format: No keys found")
|
|
92
|
+
|
|
93
|
+
return DatSignatureKey(algorithm, signing_key, verifying_key, config)
|
|
94
|
+
|
|
95
|
+
def exports(self, option: DatSignatureKeyOutOption) -> str:
|
|
96
|
+
rv_parts = ["", ""] # [signing, verifying]
|
|
97
|
+
|
|
98
|
+
if option in ["FULL", "SIGNING"]:
|
|
99
|
+
if self.signing_key:
|
|
100
|
+
# Private Key 'd' 값을 고정된 길이의 바이트로 추출
|
|
101
|
+
private_numbers = self.signing_key.private_numbers()
|
|
102
|
+
curve_size = (self.signing_key.curve.key_size + 7) // 8
|
|
103
|
+
d_bytes = private_numbers.private_value.to_bytes(curve_size, 'big')
|
|
104
|
+
rv_parts[0] = encode_base64_url_str(d_bytes)
|
|
105
|
+
elif option == "SIGNING":
|
|
106
|
+
raise ValueError("Signature key is not supported - verifying only key")
|
|
107
|
+
|
|
108
|
+
if option in ["FULL", "VERIFYING"]:
|
|
109
|
+
# Public Key를 Raw Bytes(Uncompressed)로 추출
|
|
110
|
+
public_bytes = self.verifying_key.public_bytes(
|
|
111
|
+
encoding=serialization.Encoding.X962,
|
|
112
|
+
format=serialization.PublicFormat.UncompressedPoint
|
|
113
|
+
)
|
|
114
|
+
rv_parts[1] = encode_base64_url_str(public_bytes)
|
|
115
|
+
|
|
116
|
+
if option == "SIGNING":
|
|
117
|
+
return rv_parts[0]
|
|
118
|
+
if option == "VERIFYING":
|
|
119
|
+
return "~" + rv_parts[1]
|
|
120
|
+
return f"{rv_parts[0]}~{rv_parts[1]}"
|
|
121
|
+
|
|
122
|
+
def sign(self, body: Union[bytes, str]) -> bytes:
|
|
123
|
+
if not self.signing_key:
|
|
124
|
+
raise ValueError("Signature key is not supported - verifying only key")
|
|
125
|
+
|
|
126
|
+
if isinstance(body, str):
|
|
127
|
+
body = body.encode()
|
|
128
|
+
|
|
129
|
+
if not body:
|
|
130
|
+
raise ValueError("Sign Error - body is empty")
|
|
131
|
+
|
|
132
|
+
signature = self.signing_key.sign(
|
|
133
|
+
body,
|
|
134
|
+
ec.ECDSA(self._config["hash"])
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return self._der_to_raw_signature(signature)
|
|
138
|
+
|
|
139
|
+
def verify(self, body: Union[bytes, str], signature: Union[bytes, str]) -> bool:
|
|
140
|
+
if isinstance(body, str):
|
|
141
|
+
body = body.encode('utf-8')
|
|
142
|
+
if not body:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
sig_bytes = decode_base64_url(signature) if isinstance(signature, str) else signature
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# Raw (R|S) -> DER 변환 후 검증
|
|
149
|
+
der_sig = self._raw_to_der_signature(sig_bytes)
|
|
150
|
+
self.verifying_key.verify(
|
|
151
|
+
der_sig,
|
|
152
|
+
body,
|
|
153
|
+
ec.ECDSA(self._config["hash"])
|
|
154
|
+
)
|
|
155
|
+
return True
|
|
156
|
+
except Exception:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def has_signing_key(self) -> bool:
|
|
160
|
+
return self.signing_key is not None
|
|
161
|
+
|
|
162
|
+
# --- 유틸리티: Web Crypto (Raw R|S) <-> OpenSSL (DER) 변환 ---
|
|
163
|
+
def _der_to_raw_signature(self, signature: bytes) -> bytes:
|
|
164
|
+
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
|
165
|
+
r, s = decode_dss_signature(signature)
|
|
166
|
+
size = (self.verifying_key.curve.key_size + 7) // 8
|
|
167
|
+
return r.to_bytes(size, 'big') + s.to_bytes(size, 'big')
|
|
168
|
+
|
|
169
|
+
def _raw_to_der_signature(self, signature: bytes) -> bytes:
|
|
170
|
+
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
|
|
171
|
+
size = len(signature) // 2
|
|
172
|
+
r = int.from_bytes(signature[:size], 'big')
|
|
173
|
+
s = int.from_bytes(signature[size:], 'big')
|
|
174
|
+
return encode_dss_signature(r, s)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def encode_base64_url(s: Union[bytes, str, None]) -> bytes:
|
|
6
|
+
if isinstance(s, str):
|
|
7
|
+
if s == "":
|
|
8
|
+
return b""
|
|
9
|
+
s = s.encode('utf-8')
|
|
10
|
+
if s is None:
|
|
11
|
+
return b""
|
|
12
|
+
return base64.urlsafe_b64encode(s).rstrip(b'=')
|
|
13
|
+
|
|
14
|
+
def encode_base64_url_str(s: Union[bytes, str, None]) -> str:
|
|
15
|
+
return encode_base64_url(s).decode('ascii')
|
|
16
|
+
|
|
17
|
+
def decode_base64_url(s: Union[bytes, str, None]) -> bytes:
|
|
18
|
+
if isinstance(s, str):
|
|
19
|
+
if s == "":
|
|
20
|
+
return b""
|
|
21
|
+
s = s.encode('utf-8')
|
|
22
|
+
if s is None:
|
|
23
|
+
return b""
|
|
24
|
+
rem = len(s) % 4
|
|
25
|
+
if rem > 0:
|
|
26
|
+
s += b'=' * (4 - rem)
|
|
27
|
+
return base64.urlsafe_b64decode(s)
|
|
28
|
+
|
|
29
|
+
def decode_base64_url_str(s: Union[bytes, str, None]) -> str:
|
|
30
|
+
return decode_base64_url(s).decode('utf-8')
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saro-dat
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Distributed Access Token
|
|
5
|
+
Author-email: Marker Seoul <j@saro.me>
|
|
6
|
+
Project-URL: Homepage, https://dat.saro.me
|
|
7
|
+
Project-URL: Repository, https://github.com/saro-lab/dat-pypi
|
|
8
|
+
Project-URL: Documentation, https://dat.saro.me/ko/libs/pypi-saro-dat
|
|
9
|
+
Keywords: dat,distributed,access,token,jwt,security,authentication
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: cryptography>=47.0.0
|
|
14
|
+
Requires-Dist: readerwriterlock>=1.0.9
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# DAT - Distributed Access Token
|
|
18
|
+
|
|
19
|
+
## Document
|
|
20
|
+
|
|
21
|
+
### [DAT Run Online](https://dat.saro.me)
|
|
22
|
+
|
|
23
|
+
### [What is DAT](https://dat.saro.me/--/intro)
|
|
24
|
+
|
|
25
|
+
### [Example](https://dat.saro.me/--/libs/pypi-saro-dat)
|
|
26
|
+
|
|
27
|
+
## support signature algorithm
|
|
28
|
+
| name | algorithm |
|
|
29
|
+
|--------|------------|
|
|
30
|
+
| P256 | secp256r1 |
|
|
31
|
+
| P384 | secp384r1 |
|
|
32
|
+
| P521 | secp521r1 |
|
|
33
|
+
|
|
34
|
+
## support crypto algorithm
|
|
35
|
+
| name | algorithm |
|
|
36
|
+
|------------|-----------------------------|
|
|
37
|
+
| AES128GCMN | aes-128-gcm n(nonce + body) |
|
|
38
|
+
| AES256GCMN | aes-256-cbc n(nonce + body) |
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Performance
|
|
42
|
+
- random plain and secure test
|
|
43
|
+
- mac mini m4 2024 basic (10 core)
|
|
44
|
+
- [test_bench.py](tests/test_bench.py)
|
|
45
|
+
```
|
|
46
|
+
Plain: A9wAt86DfDVQCXzfijnfB7j5GWk9TOO4lapQS7AzYenPoRAELwaiqKa8IikDmT8FAfNZJFocR66Rvqcae3JcSf3OVIppE0lYDg4G
|
|
47
|
+
Secure: R2KJfHAFClJdS0je4TkU5JoTDfRMGjp5y5zn5sG2iCwsL6IVhjzCOXaVcQPAJwqiEJGDmd3Rdl6tCI0KwAcsjD4ZrqL3IEL5Jr2Q
|
|
48
|
+
|
|
49
|
+
Multi-Thread
|
|
50
|
+
P256 AES128GCMN Issue * 10000 : 192ms
|
|
51
|
+
P256 AES128GCMN Parse * 10000 : 171ms
|
|
52
|
+
P256 AES256GCMN Issue * 10000 : 191ms
|
|
53
|
+
P256 AES256GCMN Parse * 10000 : 168ms
|
|
54
|
+
P384 AES128GCMN Issue * 10000 : 847ms
|
|
55
|
+
P384 AES128GCMN Parse * 10000 : 1841ms
|
|
56
|
+
P384 AES256GCMN Issue * 10000 : 789ms
|
|
57
|
+
P384 AES256GCMN Parse * 10000 : 1829ms
|
|
58
|
+
P521 AES128GCMN Issue * 10000 : 684ms
|
|
59
|
+
P521 AES128GCMN Parse * 10000 : 1355ms
|
|
60
|
+
P521 AES256GCMN Issue * 10000 : 683ms
|
|
61
|
+
P521 AES256GCMN Parse * 10000 : 1343ms
|
|
62
|
+
|
|
63
|
+
Single-Thread
|
|
64
|
+
P256 AES128GCMN Issue * 10000 : 223ms
|
|
65
|
+
P256 AES128GCMN Parse * 10000 : 447ms
|
|
66
|
+
P256 AES256GCMN Issue * 10000 : 213ms
|
|
67
|
+
P256 AES256GCMN Parse * 10000 : 450ms
|
|
68
|
+
P384 AES128GCMN Issue * 10000 : 4915ms
|
|
69
|
+
P384 AES128GCMN Parse * 10000 : 11750ms
|
|
70
|
+
P384 AES256GCMN Issue * 10000 : 4915ms
|
|
71
|
+
P384 AES256GCMN Parse * 10000 : 11705ms
|
|
72
|
+
P521 AES128GCMN Issue * 10000 : 3576ms
|
|
73
|
+
P521 AES128GCMN Parse * 10000 : 7219ms
|
|
74
|
+
P521 AES256GCMN Issue * 10000 : 3623ms
|
|
75
|
+
P521 AES256GCMN Parse * 10000 : 7246ms
|
|
76
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/saro_dat/__init__.py
|
|
5
|
+
src/saro_dat/crypto.py
|
|
6
|
+
src/saro_dat/dat.py
|
|
7
|
+
src/saro_dat/dat_certificate.py
|
|
8
|
+
src/saro_dat/dat_manager.py
|
|
9
|
+
src/saro_dat/signature.py
|
|
10
|
+
src/saro_dat/util.py
|
|
11
|
+
src/saro_dat.egg-info/PKG-INFO
|
|
12
|
+
src/saro_dat.egg-info/SOURCES.txt
|
|
13
|
+
src/saro_dat.egg-info/dependency_links.txt
|
|
14
|
+
src/saro_dat.egg-info/requires.txt
|
|
15
|
+
src/saro_dat.egg-info/top_level.txt
|
|
16
|
+
tests/test_bench.py
|
|
17
|
+
tests/test_certificate.py
|
|
18
|
+
tests/test_crypto.py
|
|
19
|
+
tests/test_example.py
|
|
20
|
+
tests/test_manager.py
|
|
21
|
+
tests/test_signature.py
|
|
22
|
+
tests/test_util.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
saro_dat
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import secrets
|
|
3
|
+
import string
|
|
4
|
+
import time
|
|
5
|
+
import unittest
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
|
|
8
|
+
from saro_dat import DatSignatureAlgorithm, DatCryptoAlgorithm, DatCertificate, DatManager, DatSignatureKey, DatCryptoKey
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_base62(length: int) -> str:
|
|
12
|
+
characters = string.ascii_letters + string.digits
|
|
13
|
+
return ''.join(secrets.choice(characters) for _ in range(length))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_issue(cert, plain, secure):
|
|
17
|
+
return DatManager._issue(cert, plain, secure)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_parse(cert, dat_str):
|
|
21
|
+
return DatManager._parse(cert, dat_str)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def loops(multi_thread: bool, loop_size: int, certificates: list[DatCertificate], plain: str, secure: str):
|
|
25
|
+
mode_name = "Multi-Thread" if multi_thread else "Single-Thread"
|
|
26
|
+
print(f"\n--- {mode_name} ---")
|
|
27
|
+
|
|
28
|
+
for cert in certificates:
|
|
29
|
+
pre = f"{cert._signature_key.algorithm.name} {cert._crypto_key.algorithm.name}"
|
|
30
|
+
|
|
31
|
+
start = time.perf_counter()
|
|
32
|
+
last_dat = ""
|
|
33
|
+
|
|
34
|
+
if multi_thread:
|
|
35
|
+
# 파이썬의 GIL 우회를 위해 ProcessPoolExecutor 사용 (실제 멀티코어 활용)
|
|
36
|
+
with ThreadPoolExecutor() as executor:
|
|
37
|
+
futures = [executor.submit(run_issue, cert, plain, secure) for _ in range(loop_size)]
|
|
38
|
+
for fut in futures:
|
|
39
|
+
last_dat = fut.result()
|
|
40
|
+
else:
|
|
41
|
+
for _ in range(loop_size):
|
|
42
|
+
last_dat = DatManager._issue(cert, plain, secure)
|
|
43
|
+
|
|
44
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
45
|
+
print(f"{pre} Issue * {loop_size} : {duration_ms:.2f}ms")
|
|
46
|
+
|
|
47
|
+
# 2. Parse Benchmark
|
|
48
|
+
start = time.perf_counter()
|
|
49
|
+
last_payload = None
|
|
50
|
+
|
|
51
|
+
if multi_thread:
|
|
52
|
+
with ThreadPoolExecutor() as executor:
|
|
53
|
+
futures = [executor.submit(run_parse, cert, last_dat) for _ in range(loop_size)]
|
|
54
|
+
for fut in futures:
|
|
55
|
+
last_payload = fut.result()
|
|
56
|
+
else:
|
|
57
|
+
for _ in range(loop_size):
|
|
58
|
+
last_payload = DatManager._parse(cert, last_dat)
|
|
59
|
+
|
|
60
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
61
|
+
print(f"{pre} Parse * {loop_size} : {duration_ms:.2f}ms")
|
|
62
|
+
|
|
63
|
+
# 검증
|
|
64
|
+
assert last_payload.plain == plain
|
|
65
|
+
assert last_payload.secure == secure
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def benchmark(loop_size: int):
|
|
69
|
+
plain = generate_base62(100)
|
|
70
|
+
secure = generate_base62(100)
|
|
71
|
+
|
|
72
|
+
print("Performance Test (Plain, Secure)")
|
|
73
|
+
print(f"Plain: {plain}")
|
|
74
|
+
print(f"Secure: {secure}")
|
|
75
|
+
|
|
76
|
+
certificates = []
|
|
77
|
+
now = int(time.time())
|
|
78
|
+
|
|
79
|
+
for sa in DatSignatureAlgorithm:
|
|
80
|
+
for ca in DatCryptoAlgorithm:
|
|
81
|
+
certificates.append(DatCertificate(0, DatSignatureKey.generate(sa), DatCryptoKey.generate(ca), now - 10, now + 600, 60))
|
|
82
|
+
|
|
83
|
+
await loops(True, loop_size, certificates, plain, secure)
|
|
84
|
+
await loops(False, loop_size, certificates, plain, secure)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestBench(unittest.TestCase):
|
|
88
|
+
def test(self):
|
|
89
|
+
loop_size = 100
|
|
90
|
+
asyncio.run(benchmark(loop_size))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
unittest.main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import string
|
|
3
|
+
import time
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from saro_dat import DatSignatureAlgorithm, DatCryptoAlgorithm, DatCertificate, DatSignatureKey, DatCryptoKey, \
|
|
7
|
+
DatSignatureKeyOutOption, DatManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_base62(length: int) -> str:
|
|
11
|
+
characters = string.ascii_letters + string.digits
|
|
12
|
+
return ''.join(secrets.choice(characters) for _ in range(length))
|
|
13
|
+
|
|
14
|
+
def generate_certificate(cid: int, sa: DatSignatureAlgorithm, ca: DatCryptoAlgorithm) -> DatCertificate:
|
|
15
|
+
now = int(time.time())
|
|
16
|
+
return DatCertificate(cid, DatSignatureKey.generate(sa), DatCryptoKey.generate(ca), now - 10, now + 100, 1800)
|
|
17
|
+
|
|
18
|
+
def cert_test(test: TestCertificate, fail_cert: DatCertificate, cid: int, sa: DatSignatureAlgorithm, ca: DatCryptoAlgorithm):
|
|
19
|
+
tag = f"CERT {sa.value} {ca.value}"
|
|
20
|
+
original_plain = generate_base62(100)
|
|
21
|
+
original_secure = generate_base62(100)
|
|
22
|
+
|
|
23
|
+
new_cert = generate_certificate(cid, sa, ca)
|
|
24
|
+
export_full_cert: str = new_cert.exports(DatSignatureKeyOutOption.FULL)
|
|
25
|
+
export_signing_cert = new_cert.exports(DatSignatureKeyOutOption.SIGNING)
|
|
26
|
+
export_verifying_cert = new_cert.exports(DatSignatureKeyOutOption.VERIFYING)
|
|
27
|
+
|
|
28
|
+
reimport_full_cert = DatCertificate.imports(export_full_cert)
|
|
29
|
+
reimport_signing_cert = DatCertificate.imports(export_signing_cert)
|
|
30
|
+
reimport_verifying_cert = DatCertificate.imports(export_verifying_cert)
|
|
31
|
+
|
|
32
|
+
print(f"{tag} Generated-Imported cert: {export_full_cert}")
|
|
33
|
+
|
|
34
|
+
dat_1 = DatManager._issue(new_cert, original_plain, original_secure)
|
|
35
|
+
dat_2 = DatManager._issue(reimport_full_cert, original_plain, original_secure)
|
|
36
|
+
dat_3 = DatManager._issue(reimport_signing_cert, original_plain, original_secure)
|
|
37
|
+
dat_empty = DatManager._issue(new_cert, "", "")
|
|
38
|
+
|
|
39
|
+
print(f"{tag} Issue DAT: {dat_1}")
|
|
40
|
+
print(f"{tag} Issue DAT: {dat_2}")
|
|
41
|
+
print(f"{tag} Issue DAT: {dat_3}")
|
|
42
|
+
|
|
43
|
+
payload_1 = DatManager._parse(reimport_verifying_cert, dat_1)
|
|
44
|
+
payload_2 = DatManager._parse(reimport_signing_cert, dat_2)
|
|
45
|
+
payload_3 = DatManager._parse(reimport_full_cert, dat_3)
|
|
46
|
+
payload_empty = DatManager._parse(reimport_verifying_cert, dat_empty)
|
|
47
|
+
|
|
48
|
+
assert payload_1.plain == original_plain
|
|
49
|
+
assert payload_2.plain == original_plain
|
|
50
|
+
assert payload_3.plain == original_plain
|
|
51
|
+
assert payload_empty.plain == ""
|
|
52
|
+
assert payload_1.secure == original_secure
|
|
53
|
+
assert payload_2.secure == original_secure
|
|
54
|
+
assert payload_3.secure == original_secure
|
|
55
|
+
assert payload_empty.secure == ""
|
|
56
|
+
print(f"{tag} Verify DAT")
|
|
57
|
+
|
|
58
|
+
with test.assertRaises(RuntimeError):
|
|
59
|
+
DatManager._parse(fail_cert, dat_1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestCertificate(unittest.TestCase):
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test(self):
|
|
66
|
+
fail_cert = generate_certificate(3424342, DatSignatureAlgorithm.P256, DatCryptoAlgorithm.AES128GCMN)
|
|
67
|
+
for sa in DatSignatureAlgorithm:
|
|
68
|
+
for ca in DatCryptoAlgorithm:
|
|
69
|
+
for i in range(30):
|
|
70
|
+
cert_test(self, fail_cert, i, sa, ca)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
unittest.main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import string
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from saro_dat import util
|
|
6
|
+
from saro_dat.crypto import DatCryptoKey, DatCryptoAlgorithm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_base62(length: int) -> str:
|
|
10
|
+
characters = string.ascii_letters + string.digits
|
|
11
|
+
return ''.join(secrets.choice(characters) for _ in range(length))
|
|
12
|
+
|
|
13
|
+
def algorithm_test(algorithm: DatCryptoAlgorithm):
|
|
14
|
+
tag = algorithm.value
|
|
15
|
+
gen_key = DatCryptoKey.generate(algorithm)
|
|
16
|
+
out_key_bytes = gen_key.exports()
|
|
17
|
+
out_key_base64 = util.encode_base64_url_str(out_key_bytes)
|
|
18
|
+
copy_key = DatCryptoKey.imports(algorithm, util.decode_base64_url(out_key_base64))
|
|
19
|
+
print(f"{tag} Generated-Imported key [{len(out_key_bytes)}] {out_key_base64}")
|
|
20
|
+
|
|
21
|
+
original_text = ">!#2 유니코드" + generate_base62(80)
|
|
22
|
+
encrypted = util.encode_base64_url_str(gen_key.encrypt(original_text))
|
|
23
|
+
print(f"{tag} Encrypted: {encrypted}")
|
|
24
|
+
|
|
25
|
+
decrypted = copy_key.decrypt(encrypted).decode('utf-8')
|
|
26
|
+
print(f"{tag} Decrypted: {decrypted}")
|
|
27
|
+
|
|
28
|
+
# empty
|
|
29
|
+
original_text = ""
|
|
30
|
+
encrypted = util.encode_base64_url_str(gen_key.encrypt(original_text))
|
|
31
|
+
print(f"{tag} Encrypted: {encrypted}")
|
|
32
|
+
|
|
33
|
+
decrypted = copy_key.decrypt(encrypted).decode('utf-8')
|
|
34
|
+
print(f"{tag} Decrypted: {decrypted}")
|
|
35
|
+
|
|
36
|
+
assert(original_text == decrypted)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestDatCrypto(unittest.TestCase):
|
|
40
|
+
|
|
41
|
+
def test(self):
|
|
42
|
+
for algorithm in DatCryptoAlgorithm:
|
|
43
|
+
for i in range(30):
|
|
44
|
+
algorithm_test(algorithm)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
unittest.main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from saro_dat import DatManager, DatCertificate, DatSignatureKey, DatSignatureAlgorithm, DatCryptoKey, DatCryptoAlgorithm
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestDatManager(unittest.TestCase):
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_issue_and_parse(self):
|
|
11
|
+
dat_manager = DatManager()
|
|
12
|
+
|
|
13
|
+
# create certificate
|
|
14
|
+
now = int(time.time())
|
|
15
|
+
cert = DatCertificate(0, DatSignatureKey.generate(DatSignatureAlgorithm.P256), DatCryptoKey.generate(DatCryptoAlgorithm.AES128GCMN), now - 10, now + 10, 1800)
|
|
16
|
+
|
|
17
|
+
# import certificate
|
|
18
|
+
dat_manager.import_certificates([cert])
|
|
19
|
+
|
|
20
|
+
example_plain = "plain text = 평문"
|
|
21
|
+
example_secure = "secure = 암호문"
|
|
22
|
+
|
|
23
|
+
dat = dat_manager.issue(example_plain, example_secure)
|
|
24
|
+
payload = dat_manager.parse(dat)
|
|
25
|
+
|
|
26
|
+
assert payload.plain == example_plain
|
|
27
|
+
assert payload.secure == example_secure
|
|
28
|
+
print(f"PARSE DAT: {dat}")
|
|
29
|
+
print(f"plain: {payload.plain}")
|
|
30
|
+
print(f"secure: {payload.secure}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_hard(self):
|
|
35
|
+
example_certs = """0.P256.JH0wsVa7I1P2nyMKNi-REWeIyJ_ja30c8N7JmQpE2Ns.AES128GCMN.29dmalXRXXQT-YNV95HFvg.1.17786821880.1800
|
|
36
|
+
1.P256.Gh9fb16c8XYdzKobpUwytD446_cMy3gWyncvViqeIZo.AES128GCMN.zX8sV8zO73-lDSfT-x8I1Q.1.17786821880.1800
|
|
37
|
+
2.P256.fsufFa3ByNffCTG0fSQhEZqGF2z8pWxC3aLbZR6F0f8.AES128GCMN.e4h8InqvA5VvE8OtpjxRMQ.1.17786821880.1800
|
|
38
|
+
3.P256.koYkmCZYmvLISg4-tPbksoZ71ay8hEVEWB6aBbexliA.AES128GCMN.7nQ7R5yuFQ3S5Vb07Brawg.1.17786821880.1800
|
|
39
|
+
4.P256.Cda0iq0axjTeKhpnqfRFapk4innUjwbd2HEXAzYipAM.AES128GCMN.ttE0ydNaePVMVsGhRmyq8Q.1.17786821880.1800
|
|
40
|
+
5.P256.Tf4ZFbD9ZmY_9kyQRlSXKpgy18uyMGAImh48_z4e9XE.AES128GCMN.ifsTUtiUzZY6-CUielQWkw.1.17786821880.1800
|
|
41
|
+
6.P256.fRz08K1e3mtebqWwHADKl3G4nZR1V2v6knLU6-r7jG4.AES128GCMN.cEPBfYQVKKSd_adfRkrTJQ.1.17786821880.1800
|
|
42
|
+
7.P256.HPDwUUi44EWESaenixl1uOkafSc5Yshi9Q5ht5JV-7o.AES128GCMN._WwtaqkFgvqkxv8T5-WHsg.1.17786821880.1800
|
|
43
|
+
8.P256.2TSn2rl4Ucc7_tcgLFoba81zMGpdN4zInbgjD_IA0dM.AES128GCMN.zgj82h8PpS0nGxlhJBbUQQ.1.17786821880.1800
|
|
44
|
+
9.P256.uKfLtmwS9-4o5_H5fflSoaiCULwHRQXdHan2JSK6-o0.AES128GCMN.w0WXojP3LeCCl68C_KTBMQ.1.1708680188.1800"""
|
|
45
|
+
|
|
46
|
+
example_plain = "plain text = 평문"
|
|
47
|
+
example_secure = "secure = 암호문"
|
|
48
|
+
example_dat = "19565503600.8.cGxhaW4gdGV4dCA9IO2PieusuA.1YswrtsIMWMz9dJjR2M1sRAZWxzcfTu7CQo-GXaCDMh6nF6g72HQn_F9HnK-kg.kgY9pxi5nITg-8Wk_vRFaUDDldyoSrrOP0-BBxVrwbP0-ZXlpuCmKQ1YiBZd4qv3KrO9vvwZdghpsiYHITbybA"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
manager = DatManager()
|
|
52
|
+
|
|
53
|
+
manager.imports(example_certs, True)
|
|
54
|
+
|
|
55
|
+
payload = manager.parse(example_dat)
|
|
56
|
+
|
|
57
|
+
assert payload.plain == example_plain
|
|
58
|
+
assert payload.secure == example_secure
|
|
59
|
+
print(f"PARSE DAT: {example_dat}")
|
|
60
|
+
print(f"plain: {payload.plain}")
|
|
61
|
+
print(f"secure: {payload.secure}")
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import string
|
|
3
|
+
import time
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from saro_dat import DatSignatureAlgorithm, DatCryptoAlgorithm, DatCertificate, DatSignatureKey, DatCryptoKey, \
|
|
7
|
+
DatSignatureKeyOutOption, DatManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_base62(length: int) -> str:
|
|
11
|
+
characters = string.ascii_letters + string.digits
|
|
12
|
+
return ''.join(secrets.choice(characters) for _ in range(length))
|
|
13
|
+
|
|
14
|
+
def generate_certificate(cid: int, sa: DatSignatureAlgorithm, ca: DatCryptoAlgorithm) -> DatCertificate:
|
|
15
|
+
now = int(time.time())
|
|
16
|
+
return DatCertificate(cid, DatSignatureKey.generate(sa), DatCryptoKey.generate(ca), now - 10, now + 100, 1800)
|
|
17
|
+
|
|
18
|
+
class TestDatManager(unittest.TestCase):
|
|
19
|
+
|
|
20
|
+
def test(self):
|
|
21
|
+
original_plain = generate_base62(100)
|
|
22
|
+
original_secure = generate_base62(100)
|
|
23
|
+
dat_list: list[str] = []
|
|
24
|
+
manager = DatManager()
|
|
25
|
+
|
|
26
|
+
i = 0
|
|
27
|
+
for sa in DatSignatureAlgorithm:
|
|
28
|
+
for ca in DatCryptoAlgorithm:
|
|
29
|
+
for _ in range(30):
|
|
30
|
+
i += 1
|
|
31
|
+
cert = generate_certificate(i, sa, ca)
|
|
32
|
+
manager.import_certificates([cert], False)
|
|
33
|
+
dat = DatManager._issue(cert, original_plain, original_secure)
|
|
34
|
+
dat_list.append(dat)
|
|
35
|
+
|
|
36
|
+
print(f"DAT Manager Import : {len(dat_list)} Certificates")
|
|
37
|
+
|
|
38
|
+
manager_full_export = manager.exports(DatSignatureKeyOutOption.FULL)
|
|
39
|
+
manager_signing_export = manager.exports(DatSignatureKeyOutOption.SIGNING)
|
|
40
|
+
manager_verifying_export = manager.exports(DatSignatureKeyOutOption.VERIFYING)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
reimport_full_manager = DatManager()
|
|
44
|
+
reimport_full_manager.imports(manager_full_export)
|
|
45
|
+
reimport_signing_manager = DatManager()
|
|
46
|
+
reimport_signing_manager.imports(manager_signing_export)
|
|
47
|
+
reimport_verifying_manager = DatManager()
|
|
48
|
+
reimport_verifying_manager.imports(manager_verifying_export)
|
|
49
|
+
|
|
50
|
+
dat_list.append(reimport_full_manager.issue(original_plain, original_secure))
|
|
51
|
+
dat_list.append(reimport_signing_manager.issue(original_plain, original_secure))
|
|
52
|
+
|
|
53
|
+
with self.assertRaises(RuntimeError):
|
|
54
|
+
dat_list.append(reimport_verifying_manager.issue(original_plain, original_secure))
|
|
55
|
+
|
|
56
|
+
print(f"DAT Manager Re-Import")
|
|
57
|
+
print(f"ISSUE {len(dat_list)} DAT")
|
|
58
|
+
|
|
59
|
+
for dat in dat_list:
|
|
60
|
+
dat1 = manager.parse(dat)
|
|
61
|
+
dat2 = reimport_verifying_manager.parse(dat)
|
|
62
|
+
dat3 = reimport_signing_manager.parse(dat)
|
|
63
|
+
dat4 = reimport_full_manager.parse(dat)
|
|
64
|
+
assert dat1.plain == original_plain
|
|
65
|
+
assert dat1.secure == original_secure
|
|
66
|
+
assert dat2.plain == original_plain
|
|
67
|
+
assert dat2.secure == original_secure
|
|
68
|
+
assert dat3.plain == original_plain
|
|
69
|
+
assert dat3.secure == original_secure
|
|
70
|
+
assert dat4.plain == original_plain
|
|
71
|
+
assert dat4.secure == original_secure
|
|
72
|
+
print(f"PARSE DAT: {dat}")
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
unittest.main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import string
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from saro_dat import DatSignatureAlgorithm, DatSignatureKey, DatSignatureKeyOutOption
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_base62(length: int) -> str:
|
|
9
|
+
characters = string.ascii_letters + string.digits
|
|
10
|
+
return ''.join(secrets.choice(characters) for _ in range(length))
|
|
11
|
+
|
|
12
|
+
def algorithm_test(algorithm: DatSignatureAlgorithm):
|
|
13
|
+
tag = algorithm.value
|
|
14
|
+
gen_key = DatSignatureKey.generate(algorithm)
|
|
15
|
+
|
|
16
|
+
out_key_full = gen_key.exports(DatSignatureKeyOutOption.FULL)
|
|
17
|
+
out_key_signing = gen_key.exports(DatSignatureKeyOutOption.SIGNING)
|
|
18
|
+
out_key_verifying = gen_key.exports(DatSignatureKeyOutOption.VERIFYING)
|
|
19
|
+
|
|
20
|
+
copy_out_key_full = DatSignatureKey.imports(algorithm, out_key_full)
|
|
21
|
+
copy_out_key_signing = DatSignatureKey.imports(algorithm, out_key_signing)
|
|
22
|
+
copy_out_key_verifying = DatSignatureKey.imports(algorithm, out_key_verifying)
|
|
23
|
+
|
|
24
|
+
print(f"{tag} Generated-Imported key: {out_key_full}")
|
|
25
|
+
|
|
26
|
+
original_text = ">!#2 유니코드" + generate_base62(80)
|
|
27
|
+
sign1 = gen_key.sign(original_text)
|
|
28
|
+
sign2 = copy_out_key_full.sign(original_text)
|
|
29
|
+
sign3 = copy_out_key_signing.sign(original_text)
|
|
30
|
+
|
|
31
|
+
assert gen_key.verify(original_text, sign3)
|
|
32
|
+
assert copy_out_key_full.verify(original_text, sign2)
|
|
33
|
+
assert copy_out_key_signing.verify(original_text, sign1)
|
|
34
|
+
assert copy_out_key_verifying.verify(original_text, sign1)
|
|
35
|
+
assert not copy_out_key_verifying.verify(b"", sign1)
|
|
36
|
+
|
|
37
|
+
print(f"{tag} Signing-Verify key")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestSignature(unittest.TestCase):
|
|
41
|
+
|
|
42
|
+
def test(self):
|
|
43
|
+
for algorithm in DatSignatureAlgorithm:
|
|
44
|
+
for i in range(30):
|
|
45
|
+
algorithm_test(algorithm)
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
unittest.main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from saro_dat.util import encode_base64_url_str, decode_base64_url_str
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestBase64(unittest.TestCase):
|
|
7
|
+
def test_base64(self):
|
|
8
|
+
text = "$$><'2 ABC 유니코드"
|
|
9
|
+
b64 = "JCQ-PCcyICAgIEFCQyAg7Jyg64uI7L2U65Oc"
|
|
10
|
+
|
|
11
|
+
b64_1 = encode_base64_url_str(text)
|
|
12
|
+
self.assertEqual(b64, b64_1)
|
|
13
|
+
print(b64_1)
|
|
14
|
+
|
|
15
|
+
de_b64_1 = decode_base64_url_str(b64_1)
|
|
16
|
+
self.assertEqual(text, de_b64_1)
|
|
17
|
+
print(de_b64_1)
|
|
18
|
+
|
|
19
|
+
print("Test passed successfully")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
unittest.main()
|
|
26
|
+
|