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.
Files changed (71) hide show
  1. ratchetkit/__init__.py +3 -0
  2. ratchetkit/algorithms/__init__.py +1 -0
  3. ratchetkit/algorithms/cipher/__init__.py +17 -0
  4. ratchetkit/algorithms/cipher/aes128.py +15 -0
  5. ratchetkit/algorithms/cipher/aes256.py +15 -0
  6. ratchetkit/algorithms/cipher/base.py +65 -0
  7. ratchetkit/algorithms/cipher/camellia128.py +18 -0
  8. ratchetkit/algorithms/cipher/camellia256.py +18 -0
  9. ratchetkit/algorithms/cipher/chacha20.py +17 -0
  10. ratchetkit/algorithms/hash/__init__.py +17 -0
  11. ratchetkit/algorithms/hash/base.py +30 -0
  12. ratchetkit/algorithms/hash/blake2b.py +26 -0
  13. ratchetkit/algorithms/hash/sha256.py +26 -0
  14. ratchetkit/algorithms/hash/sha3256.py +26 -0
  15. ratchetkit/algorithms/hash/sha384.py +26 -0
  16. ratchetkit/algorithms/hash/sha512.py +26 -0
  17. ratchetkit/algorithms/kem/__init__.py +12 -0
  18. ratchetkit/algorithms/kem/base.py +63 -0
  19. ratchetkit/algorithms/kem/ec_base.py +104 -0
  20. ratchetkit/algorithms/kem/mlkem1024.py +15 -0
  21. ratchetkit/algorithms/kem/mlkem768.py +15 -0
  22. ratchetkit/algorithms/kem/mlkem_base.py +97 -0
  23. ratchetkit/algorithms/kem/rsa2048.py +10 -0
  24. ratchetkit/algorithms/kem/rsa4096.py +10 -0
  25. ratchetkit/algorithms/kem/rsa_base.py +151 -0
  26. ratchetkit/algorithms/kem/secp256r1.py +12 -0
  27. ratchetkit/algorithms/kem/secp384r1.py +12 -0
  28. ratchetkit/algorithms/kem/secp521r1.py +12 -0
  29. ratchetkit/algorithms/kem/x25519.py +107 -0
  30. ratchetkit/algorithms/kem/x448.py +107 -0
  31. ratchetkit/errors.py +61 -0
  32. ratchetkit/handshake/__init__.py +15 -0
  33. ratchetkit/handshake/message.py +177 -0
  34. ratchetkit/handshake/pqxdh.py +251 -0
  35. ratchetkit/handshake/x3dh.py +301 -0
  36. ratchetkit/kdf.py +70 -0
  37. ratchetkit/keys/__init__.py +5 -0
  38. ratchetkit/keys/chain.py +57 -0
  39. ratchetkit/keys/identity.py +36 -0
  40. ratchetkit/keys/keypair.py +98 -0
  41. ratchetkit/keys/message.py +87 -0
  42. ratchetkit/keys/prekey.py +372 -0
  43. ratchetkit/keys/root.py +102 -0
  44. ratchetkit/packing.py +154 -0
  45. ratchetkit/py.typed +0 -0
  46. ratchetkit/ratchet/__init__.py +22 -0
  47. ratchetkit/ratchet/chains.py +68 -0
  48. ratchetkit/ratchet/header.py +51 -0
  49. ratchetkit/ratchet/limits.py +35 -0
  50. ratchetkit/ratchet/message/__init__.py +11 -0
  51. ratchetkit/ratchet/message/base.py +44 -0
  52. ratchetkit/ratchet/message/double.py +65 -0
  53. ratchetkit/ratchet/message/triple.py +91 -0
  54. ratchetkit/ratchet/protocol/__init__.py +11 -0
  55. ratchetkit/ratchet/protocol/base.py +49 -0
  56. ratchetkit/ratchet/protocol/double.py +504 -0
  57. ratchetkit/ratchet/protocol/triple.py +258 -0
  58. ratchetkit/ratchet/skipped.py +123 -0
  59. ratchetkit/ratchet/spqr.py +555 -0
  60. ratchetkit/scka/__init__.py +18 -0
  61. ratchetkit/scka/chunk.py +124 -0
  62. ratchetkit/scka/kem_scka.py +410 -0
  63. ratchetkit/session.py +235 -0
  64. ratchetkit/suite/__init__.py +33 -0
  65. ratchetkit/suite/algs.py +94 -0
  66. ratchetkit/suite/double.py +135 -0
  67. ratchetkit/suite/triple.py +118 -0
  68. ratchetkit-1.2.0.dist-info/METADATA +268 -0
  69. ratchetkit-1.2.0.dist-info/RECORD +71 -0
  70. ratchetkit-1.2.0.dist-info/WHEEL +4 -0
  71. ratchetkit-1.2.0.dist-info/licenses/LICENSE +661 -0
ratchetkit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ratchetkit - X3DH/PQXDH + Double/Triple Ratchet with pluggable crypto."""
2
+
3
+ __version__ = "1.2.0"
@@ -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