ratchetkit 1.2.0__py3-none-any.whl
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.
- ratchetkit/__init__.py +3 -0
- ratchetkit/algorithms/__init__.py +1 -0
- ratchetkit/algorithms/cipher/__init__.py +17 -0
- ratchetkit/algorithms/cipher/aes128.py +15 -0
- ratchetkit/algorithms/cipher/aes256.py +15 -0
- ratchetkit/algorithms/cipher/base.py +65 -0
- ratchetkit/algorithms/cipher/camellia128.py +18 -0
- ratchetkit/algorithms/cipher/camellia256.py +18 -0
- ratchetkit/algorithms/cipher/chacha20.py +17 -0
- ratchetkit/algorithms/hash/__init__.py +17 -0
- ratchetkit/algorithms/hash/base.py +30 -0
- ratchetkit/algorithms/hash/blake2b.py +26 -0
- ratchetkit/algorithms/hash/sha256.py +26 -0
- ratchetkit/algorithms/hash/sha3256.py +26 -0
- ratchetkit/algorithms/hash/sha384.py +26 -0
- ratchetkit/algorithms/hash/sha512.py +26 -0
- ratchetkit/algorithms/kem/__init__.py +12 -0
- ratchetkit/algorithms/kem/base.py +63 -0
- ratchetkit/algorithms/kem/ec_base.py +104 -0
- ratchetkit/algorithms/kem/mlkem1024.py +15 -0
- ratchetkit/algorithms/kem/mlkem768.py +15 -0
- ratchetkit/algorithms/kem/mlkem_base.py +97 -0
- ratchetkit/algorithms/kem/rsa2048.py +10 -0
- ratchetkit/algorithms/kem/rsa4096.py +10 -0
- ratchetkit/algorithms/kem/rsa_base.py +151 -0
- ratchetkit/algorithms/kem/secp256r1.py +12 -0
- ratchetkit/algorithms/kem/secp384r1.py +12 -0
- ratchetkit/algorithms/kem/secp521r1.py +12 -0
- ratchetkit/algorithms/kem/x25519.py +107 -0
- ratchetkit/algorithms/kem/x448.py +107 -0
- ratchetkit/errors.py +61 -0
- ratchetkit/handshake/__init__.py +15 -0
- ratchetkit/handshake/message.py +177 -0
- ratchetkit/handshake/pqxdh.py +251 -0
- ratchetkit/handshake/x3dh.py +301 -0
- ratchetkit/kdf.py +70 -0
- ratchetkit/keys/__init__.py +5 -0
- ratchetkit/keys/chain.py +57 -0
- ratchetkit/keys/identity.py +36 -0
- ratchetkit/keys/keypair.py +98 -0
- ratchetkit/keys/message.py +87 -0
- ratchetkit/keys/prekey.py +372 -0
- ratchetkit/keys/root.py +102 -0
- ratchetkit/packing.py +154 -0
- ratchetkit/py.typed +0 -0
- ratchetkit/ratchet/__init__.py +22 -0
- ratchetkit/ratchet/chains.py +68 -0
- ratchetkit/ratchet/header.py +51 -0
- ratchetkit/ratchet/limits.py +35 -0
- ratchetkit/ratchet/message/__init__.py +11 -0
- ratchetkit/ratchet/message/base.py +44 -0
- ratchetkit/ratchet/message/double.py +65 -0
- ratchetkit/ratchet/message/triple.py +91 -0
- ratchetkit/ratchet/protocol/__init__.py +11 -0
- ratchetkit/ratchet/protocol/base.py +49 -0
- ratchetkit/ratchet/protocol/double.py +504 -0
- ratchetkit/ratchet/protocol/triple.py +258 -0
- ratchetkit/ratchet/skipped.py +123 -0
- ratchetkit/ratchet/spqr.py +555 -0
- ratchetkit/scka/__init__.py +18 -0
- ratchetkit/scka/chunk.py +124 -0
- ratchetkit/scka/kem_scka.py +410 -0
- ratchetkit/session.py +235 -0
- ratchetkit/suite/__init__.py +33 -0
- ratchetkit/suite/algs.py +94 -0
- ratchetkit/suite/double.py +135 -0
- ratchetkit/suite/triple.py +118 -0
- ratchetkit-1.2.0.dist-info/METADATA +268 -0
- ratchetkit-1.2.0.dist-info/RECORD +71 -0
- ratchetkit-1.2.0.dist-info/WHEEL +4 -0
- ratchetkit-1.2.0.dist-info/licenses/LICENSE +661 -0
ratchetkit/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pluggable crypto primitives: hash, cipher, KEM."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Cipher algorithm classes."""
|
|
2
|
+
|
|
3
|
+
from ratchetkit.algorithms.cipher.aes128 import Aes128
|
|
4
|
+
from ratchetkit.algorithms.cipher.aes256 import Aes256
|
|
5
|
+
from ratchetkit.algorithms.cipher.base import CipherAlgorithm
|
|
6
|
+
from ratchetkit.algorithms.cipher.camellia128 import Camellia128
|
|
7
|
+
from ratchetkit.algorithms.cipher.camellia256 import Camellia256
|
|
8
|
+
from ratchetkit.algorithms.cipher.chacha20 import ChaCha20
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Aes128",
|
|
12
|
+
"Aes256",
|
|
13
|
+
"Camellia128",
|
|
14
|
+
"Camellia256",
|
|
15
|
+
"ChaCha20",
|
|
16
|
+
"CipherAlgorithm",
|
|
17
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""AES-128 cipher algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.ciphers import algorithms
|
|
6
|
+
|
|
7
|
+
from ratchetkit.algorithms.cipher.base import CipherAlgorithm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Aes128(CipherAlgorithm):
|
|
11
|
+
name = "aes128"
|
|
12
|
+
key_size = 16
|
|
13
|
+
block_size = 16
|
|
14
|
+
nonce_size = 16
|
|
15
|
+
algorithm = algorithms.AES
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""AES-256 cipher algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.ciphers import algorithms
|
|
6
|
+
|
|
7
|
+
from ratchetkit.algorithms.cipher.base import CipherAlgorithm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Aes256(CipherAlgorithm):
|
|
11
|
+
name = "aes256"
|
|
12
|
+
key_size = 32
|
|
13
|
+
block_size = 16
|
|
14
|
+
nonce_size = 16
|
|
15
|
+
algorithm = algorithms.AES
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Base class for pluggable symmetric ciphers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, modes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CipherAlgorithm(ABC):
|
|
12
|
+
"""Symmetric cipher used when deriving and applying MessageKeys.
|
|
13
|
+
|
|
14
|
+
Block ciphers always run in CTR (single-use message keys). Stream
|
|
15
|
+
ciphers (ChaCha20) take the IV as the nonce directly.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
key_size: int
|
|
20
|
+
block_size: int
|
|
21
|
+
nonce_size: int
|
|
22
|
+
algorithm: Any # hazmat algorithms.* class
|
|
23
|
+
is_stream: bool = False
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def iv_size(cls) -> int:
|
|
27
|
+
if cls.is_stream:
|
|
28
|
+
# cryptography ChaCha20 uses 16-byte nonce (4-byte counter + 12-byte nonce)
|
|
29
|
+
return cls.nonce_size
|
|
30
|
+
# Only CTR is used for block ciphers; IV length is the block size.
|
|
31
|
+
return cls.block_size
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def encrypt(cls, key: bytes, iv: bytes, plaintext: bytes) -> bytes:
|
|
35
|
+
if len(key) != cls.key_size:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"{cls.name} key must be {cls.key_size} bytes, got {len(key)}"
|
|
38
|
+
)
|
|
39
|
+
if len(iv) != cls.iv_size():
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"{cls.name} iv must be {cls.iv_size()} bytes, got {len(iv)}"
|
|
42
|
+
)
|
|
43
|
+
if cls.is_stream:
|
|
44
|
+
cipher = Cipher(cls.algorithm(key, iv), mode=None)
|
|
45
|
+
else:
|
|
46
|
+
cipher = Cipher(cls.algorithm(key), modes.CTR(iv))
|
|
47
|
+
encryptor = cipher.encryptor()
|
|
48
|
+
return encryptor.update(plaintext) + encryptor.finalize()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def decrypt(cls, key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
|
|
52
|
+
if len(key) != cls.key_size:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"{cls.name} key must be {cls.key_size} bytes, got {len(key)}"
|
|
55
|
+
)
|
|
56
|
+
if len(iv) != cls.iv_size():
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"{cls.name} iv must be {cls.iv_size()} bytes, got {len(iv)}"
|
|
59
|
+
)
|
|
60
|
+
if cls.is_stream:
|
|
61
|
+
cipher = Cipher(cls.algorithm(key, iv), mode=None)
|
|
62
|
+
else:
|
|
63
|
+
cipher = Cipher(cls.algorithm(key), modes.CTR(iv))
|
|
64
|
+
decryptor = cipher.decryptor()
|
|
65
|
+
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Camellia-128 cipher algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from cryptography.hazmat.decrepit.ciphers.algorithms import Camellia
|
|
7
|
+
except ImportError: # pragma: no cover - older cryptography
|
|
8
|
+
from cryptography.hazmat.primitives.ciphers.algorithms import Camellia
|
|
9
|
+
|
|
10
|
+
from ratchetkit.algorithms.cipher.base import CipherAlgorithm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Camellia128(CipherAlgorithm):
|
|
14
|
+
name = "camellia128"
|
|
15
|
+
key_size = 16
|
|
16
|
+
block_size = 16
|
|
17
|
+
nonce_size = 16
|
|
18
|
+
algorithm = Camellia
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Camellia-256 cipher algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from cryptography.hazmat.decrepit.ciphers.algorithms import Camellia
|
|
7
|
+
except ImportError: # pragma: no cover - older cryptography
|
|
8
|
+
from cryptography.hazmat.primitives.ciphers.algorithms import Camellia
|
|
9
|
+
|
|
10
|
+
from ratchetkit.algorithms.cipher.base import CipherAlgorithm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Camellia256(CipherAlgorithm):
|
|
14
|
+
name = "camellia256"
|
|
15
|
+
key_size = 32
|
|
16
|
+
block_size = 16
|
|
17
|
+
nonce_size = 16
|
|
18
|
+
algorithm = Camellia
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""ChaCha20 stream cipher algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.ciphers import algorithms
|
|
6
|
+
|
|
7
|
+
from ratchetkit.algorithms.cipher.base import CipherAlgorithm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChaCha20(CipherAlgorithm):
|
|
11
|
+
name = "chacha20"
|
|
12
|
+
key_size = 32
|
|
13
|
+
block_size = 1
|
|
14
|
+
# cryptography ChaCha20 uses 16-byte nonce (4-byte counter + 12-byte nonce)
|
|
15
|
+
nonce_size = 16
|
|
16
|
+
algorithm = algorithms.ChaCha20
|
|
17
|
+
is_stream = True
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Hash algorithm classes."""
|
|
2
|
+
|
|
3
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
4
|
+
from ratchetkit.algorithms.hash.blake2b import Blake2b
|
|
5
|
+
from ratchetkit.algorithms.hash.sha256 import Sha256
|
|
6
|
+
from ratchetkit.algorithms.hash.sha384 import Sha384
|
|
7
|
+
from ratchetkit.algorithms.hash.sha3256 import Sha3256
|
|
8
|
+
from ratchetkit.algorithms.hash.sha512 import Sha512
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Blake2b",
|
|
12
|
+
"HashAlgorithm",
|
|
13
|
+
"Sha256",
|
|
14
|
+
"Sha384",
|
|
15
|
+
"Sha3256",
|
|
16
|
+
"Sha512",
|
|
17
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Base class for pluggable hash algorithms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cryptography.hazmat.primitives import hashes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HashAlgorithm(ABC):
|
|
12
|
+
"""Hashlib-compatible digest used for HKDF, HMAC, and signature digests."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
digest_size: int
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def new(cls, data: bytes = b"") -> Any:
|
|
20
|
+
"""Return a hashlib-style digest object."""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def constructor(cls):
|
|
25
|
+
"""Return the hashlib constructor (for hmac / HKDF)."""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def hazmat(cls) -> hashes.HashAlgorithm:
|
|
30
|
+
"""cryptography.hazmat hash for ECDSA / RSA-PSS."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""BLAKE2b hash algorithm (64-byte digest)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
|
|
9
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Blake2b(HashAlgorithm):
|
|
13
|
+
name = "blake2b"
|
|
14
|
+
digest_size = 64
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def new(cls, data: bytes = b""):
|
|
18
|
+
return hashlib.blake2b(data)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def constructor(cls):
|
|
22
|
+
return hashlib.blake2b
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def hazmat(cls) -> hashes.HashAlgorithm:
|
|
26
|
+
return hashes.BLAKE2b(64)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""SHA-256 hash algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
|
|
9
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Sha256(HashAlgorithm):
|
|
13
|
+
name = "sha256"
|
|
14
|
+
digest_size = 32
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def new(cls, data: bytes = b""):
|
|
18
|
+
return hashlib.sha256(data)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def constructor(cls):
|
|
22
|
+
return hashlib.sha256
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def hazmat(cls) -> hashes.HashAlgorithm:
|
|
26
|
+
return hashes.SHA256()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""SHA3-256 hash algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
|
|
9
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Sha3256(HashAlgorithm):
|
|
13
|
+
name = "sha3_256"
|
|
14
|
+
digest_size = 32
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def new(cls, data: bytes = b""):
|
|
18
|
+
return hashlib.sha3_256(data)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def constructor(cls):
|
|
22
|
+
return hashlib.sha3_256
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def hazmat(cls) -> hashes.HashAlgorithm:
|
|
26
|
+
return hashes.SHA3_256()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""SHA-384 hash algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
|
|
9
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Sha384(HashAlgorithm):
|
|
13
|
+
name = "sha384"
|
|
14
|
+
digest_size = 48
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def new(cls, data: bytes = b""):
|
|
18
|
+
return hashlib.sha384(data)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def constructor(cls):
|
|
22
|
+
return hashlib.sha384
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def hazmat(cls) -> hashes.HashAlgorithm:
|
|
26
|
+
return hashes.SHA384()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""SHA-512 hash algorithm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
|
|
9
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Sha512(HashAlgorithm):
|
|
13
|
+
name = "sha512"
|
|
14
|
+
digest_size = 64
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def new(cls, data: bytes = b""):
|
|
18
|
+
return hashlib.sha512(data)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def constructor(cls):
|
|
22
|
+
return hashlib.sha512
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def hazmat(cls) -> hashes.HashAlgorithm:
|
|
26
|
+
return hashes.SHA512()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""KEM / key-exchange algorithm classes.
|
|
2
|
+
|
|
3
|
+
Import concrete backends from their modules (``ratchetkit.algorithms.kem.x25519`` and
|
|
4
|
+
so on). This package does not re-export them: ``suite`` already imports the
|
|
5
|
+
classes, and a package-level import cycle otherwise forms through ``keys``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ratchetkit.algorithms.kem.base import KemBackend
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"KemBackend",
|
|
12
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""KEM backend interface for ExtendedTripleDiffieHellman and the Double Ratchet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
10
|
+
from ratchetkit.keys.keypair import (
|
|
11
|
+
KeyPair,
|
|
12
|
+
PrivateKey,
|
|
13
|
+
PublicKey,
|
|
14
|
+
SigningKeyPair,
|
|
15
|
+
SigningPrivateKey,
|
|
16
|
+
SigningPublicKey,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KemBackend(ABC):
|
|
21
|
+
"""Key-encapsulation strategy used by ExtendedTripleDiffieHellman and the Double Ratchet."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
supports_dh: bool = False
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def generate_keypair(self) -> KeyPair:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def encapsulate(self, their_public: PublicKey) -> tuple[bytes, bytes]:
|
|
32
|
+
"""Return ``(shared_secret, ciphertext)``."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def decapsulate(self, our_private: PrivateKey, ciphertext: bytes) -> bytes:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def generate_signing_keypair(self) -> SigningKeyPair:
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def sign(
|
|
44
|
+
self,
|
|
45
|
+
private: SigningPrivateKey,
|
|
46
|
+
message: bytes,
|
|
47
|
+
digest: type[HashAlgorithm],
|
|
48
|
+
) -> bytes:
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def verify(
|
|
53
|
+
self,
|
|
54
|
+
public: SigningPublicKey,
|
|
55
|
+
message: bytes,
|
|
56
|
+
signature: bytes,
|
|
57
|
+
digest: type[HashAlgorithm],
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Raise :class:`~ratchetkit.errors.InvalidSignature` on failure."""
|
|
60
|
+
|
|
61
|
+
def dh(self, our_private: PrivateKey, their_public: PublicKey) -> bytes:
|
|
62
|
+
"""Optional mutual DH. Only implemented by DH-capable backends."""
|
|
63
|
+
raise NotImplementedError(f"{self.name} does not support mutual DH")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Shared NIST elliptic-curve ECDH + ECDSA implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
8
|
+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
9
|
+
|
|
10
|
+
from ratchetkit.errors import InvalidKey, InvalidSignature
|
|
11
|
+
from ratchetkit.algorithms.kem.base import KemBackend
|
|
12
|
+
from ratchetkit.keys.keypair import (
|
|
13
|
+
KeyPair,
|
|
14
|
+
PrivateKey,
|
|
15
|
+
PublicKey,
|
|
16
|
+
SigningKeyPair,
|
|
17
|
+
SigningPrivateKey,
|
|
18
|
+
SigningPublicKey,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EcKem(KemBackend):
|
|
26
|
+
"""NIST elliptic-curve ECDH-as-KEM + ECDSA backend base."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
curve: ec.EllipticCurve
|
|
30
|
+
supports_dh = True
|
|
31
|
+
|
|
32
|
+
def generate_keypair(self) -> KeyPair:
|
|
33
|
+
private = ec.generate_private_key(self.curve)
|
|
34
|
+
return KeyPair(
|
|
35
|
+
public=PublicKey(self.encode_public(private.public_key()), self.name),
|
|
36
|
+
private=PrivateKey(self.encode_private(private), self.name),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def encapsulate(self, their_public: PublicKey) -> tuple[bytes, bytes]:
|
|
40
|
+
ephemeral = self.generate_keypair()
|
|
41
|
+
secret = self.dh(ephemeral.private, their_public)
|
|
42
|
+
return secret, ephemeral.public.data
|
|
43
|
+
|
|
44
|
+
def decapsulate(self, our_private: PrivateKey, ciphertext: bytes) -> bytes:
|
|
45
|
+
return self.dh(our_private, PublicKey(ciphertext, self.name))
|
|
46
|
+
|
|
47
|
+
def dh(self, our_private: PrivateKey, their_public: PublicKey) -> bytes:
|
|
48
|
+
try:
|
|
49
|
+
private = self.decode_private(our_private.data)
|
|
50
|
+
public = self.decode_public(their_public.data)
|
|
51
|
+
shared = private.exchange(ec.ECDH(), public)
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
raise InvalidKey(
|
|
54
|
+
f"{self.name} ECDH failed (invalid public key)"
|
|
55
|
+
) from exc
|
|
56
|
+
if not any(shared):
|
|
57
|
+
raise InvalidKey(f"{self.name} ECDH produced an all-zero shared secret")
|
|
58
|
+
return shared
|
|
59
|
+
|
|
60
|
+
def generate_signing_keypair(self) -> SigningKeyPair:
|
|
61
|
+
# Re-use the same curve for identity signing (ECDSA).
|
|
62
|
+
pair = self.generate_keypair()
|
|
63
|
+
return SigningKeyPair(
|
|
64
|
+
public=SigningPublicKey(pair.public.data, self.name),
|
|
65
|
+
private=SigningPrivateKey(pair.private.data, self.name),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def sign(
|
|
69
|
+
self,
|
|
70
|
+
private: SigningPrivateKey,
|
|
71
|
+
message: bytes,
|
|
72
|
+
digest: type[HashAlgorithm],
|
|
73
|
+
) -> bytes:
|
|
74
|
+
key = self.decode_private(private.data)
|
|
75
|
+
return key.sign(message, ec.ECDSA(digest.hazmat()))
|
|
76
|
+
|
|
77
|
+
def verify(
|
|
78
|
+
self,
|
|
79
|
+
public: SigningPublicKey,
|
|
80
|
+
message: bytes,
|
|
81
|
+
signature: bytes,
|
|
82
|
+
digest: type[HashAlgorithm],
|
|
83
|
+
) -> None:
|
|
84
|
+
key = self.decode_public(public.data)
|
|
85
|
+
try:
|
|
86
|
+
key.verify(signature, message, ec.ECDSA(digest.hazmat()))
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
raise InvalidSignature("ECDSA verification failed") from exc
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def encode_public(key: ec.EllipticCurvePublicKey) -> bytes:
|
|
92
|
+
return key.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
|
93
|
+
|
|
94
|
+
def encode_private(self, key: ec.EllipticCurvePrivateKey) -> bytes:
|
|
95
|
+
numbers = key.private_numbers()
|
|
96
|
+
byte_length = (self.curve.key_size + 7) // 8
|
|
97
|
+
return numbers.private_value.to_bytes(byte_length, "big")
|
|
98
|
+
|
|
99
|
+
def decode_private(self, data: bytes) -> ec.EllipticCurvePrivateKey:
|
|
100
|
+
value = int.from_bytes(data, "big")
|
|
101
|
+
return ec.derive_private_key(value, self.curve)
|
|
102
|
+
|
|
103
|
+
def decode_public(self, data: bytes) -> ec.EllipticCurvePublicKey:
|
|
104
|
+
return ec.EllipticCurvePublicKey.from_encoded_point(self.curve, data)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""ML-KEM-1024 + ML-DSA-87 (NIST category 5)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import mldsa, mlkem
|
|
6
|
+
|
|
7
|
+
from ratchetkit.algorithms.kem.mlkem_base import MlkemBackend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Mlkem1024(MlkemBackend):
|
|
11
|
+
name = "mlkem1024"
|
|
12
|
+
kem_private_cls = mlkem.MLKEM1024PrivateKey
|
|
13
|
+
kem_public_cls = mlkem.MLKEM1024PublicKey
|
|
14
|
+
dsa_private_cls = mldsa.MLDSA87PrivateKey
|
|
15
|
+
dsa_public_cls = mldsa.MLDSA87PublicKey
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""ML-KEM-768 + ML-DSA-65 (NIST category 3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import mldsa, mlkem
|
|
6
|
+
|
|
7
|
+
from ratchetkit.algorithms.kem.mlkem_base import MlkemBackend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Mlkem768(MlkemBackend):
|
|
11
|
+
name = "mlkem768"
|
|
12
|
+
kem_private_cls = mlkem.MLKEM768PrivateKey
|
|
13
|
+
kem_public_cls = mlkem.MLKEM768PublicKey
|
|
14
|
+
dsa_private_cls = mldsa.MLDSA65PrivateKey
|
|
15
|
+
dsa_public_cls = mldsa.MLDSA65PublicKey
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Shared ML-KEM encapsulation + ML-DSA signatures (FIPS 203 / 204)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from ratchetkit.errors import InvalidKey, InvalidSignature
|
|
8
|
+
from ratchetkit.algorithms.kem.base import KemBackend
|
|
9
|
+
from ratchetkit.keys.keypair import (
|
|
10
|
+
KeyPair,
|
|
11
|
+
PrivateKey,
|
|
12
|
+
PublicKey,
|
|
13
|
+
SigningKeyPair,
|
|
14
|
+
SigningPrivateKey,
|
|
15
|
+
SigningPublicKey,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ratchetkit.algorithms.hash.base import HashAlgorithm
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MlkemBackend(KemBackend):
|
|
23
|
+
"""Post-quantum KEM-only backend: ML-KEM for agreement, ML-DSA for identity.
|
|
24
|
+
|
|
25
|
+
Private keys are stored as the FIPS seed (``private_bytes_raw`` /
|
|
26
|
+
``from_seed_bytes``), not the expanded secret key.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
supports_dh = False
|
|
31
|
+
# cryptography class objects, set on each concrete subclass
|
|
32
|
+
kem_private_cls: Any
|
|
33
|
+
kem_public_cls: Any
|
|
34
|
+
dsa_private_cls: Any
|
|
35
|
+
dsa_public_cls: Any
|
|
36
|
+
|
|
37
|
+
def generate_keypair(self) -> KeyPair:
|
|
38
|
+
private = self.kem_private_cls.generate()
|
|
39
|
+
return KeyPair(
|
|
40
|
+
public=PublicKey(
|
|
41
|
+
private.public_key().public_bytes_raw(),
|
|
42
|
+
self.name,
|
|
43
|
+
),
|
|
44
|
+
private=PrivateKey(
|
|
45
|
+
private.private_bytes_raw(),
|
|
46
|
+
self.name,
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def encapsulate(self, their_public: PublicKey) -> tuple[bytes, bytes]:
|
|
51
|
+
public = self.kem_public_cls.from_public_bytes(their_public.data)
|
|
52
|
+
# cryptography returns (shared_secret, ciphertext)
|
|
53
|
+
secret, ciphertext = public.encapsulate()
|
|
54
|
+
return secret, ciphertext
|
|
55
|
+
|
|
56
|
+
def decapsulate(self, our_private: PrivateKey, ciphertext: bytes) -> bytes:
|
|
57
|
+
try:
|
|
58
|
+
private = self.kem_private_cls.from_seed_bytes(our_private.data)
|
|
59
|
+
return private.decapsulate(ciphertext)
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
raise InvalidKey(f"{self.name} decapsulation failed") from exc
|
|
62
|
+
|
|
63
|
+
def generate_signing_keypair(self) -> SigningKeyPair:
|
|
64
|
+
private = self.dsa_private_cls.generate()
|
|
65
|
+
return SigningKeyPair(
|
|
66
|
+
public=SigningPublicKey(
|
|
67
|
+
private.public_key().public_bytes_raw(),
|
|
68
|
+
self.name,
|
|
69
|
+
),
|
|
70
|
+
private=SigningPrivateKey(
|
|
71
|
+
private.private_bytes_raw(),
|
|
72
|
+
self.name,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def sign(
|
|
77
|
+
self,
|
|
78
|
+
private: SigningPrivateKey,
|
|
79
|
+
message: bytes,
|
|
80
|
+
digest: type[HashAlgorithm],
|
|
81
|
+
) -> bytes:
|
|
82
|
+
# ML-DSA is a pure signature scheme; digest is unused (suite still has one).
|
|
83
|
+
key = self.dsa_private_cls.from_seed_bytes(private.data)
|
|
84
|
+
return key.sign(message)
|
|
85
|
+
|
|
86
|
+
def verify(
|
|
87
|
+
self,
|
|
88
|
+
public: SigningPublicKey,
|
|
89
|
+
message: bytes,
|
|
90
|
+
signature: bytes,
|
|
91
|
+
digest: type[HashAlgorithm],
|
|
92
|
+
) -> None:
|
|
93
|
+
key = self.dsa_public_cls.from_public_bytes(public.data)
|
|
94
|
+
try:
|
|
95
|
+
key.verify(signature, message)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
raise InvalidSignature(f"{self.name} ML-DSA verification failed") from exc
|