aptos-ace-sdk 3.11.1__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.
Files changed (51) hide show
  1. aptos_ace_sdk-3.11.1/.gitignore +9 -0
  2. aptos_ace_sdk-3.11.1/PKG-INFO +111 -0
  3. aptos_ace_sdk-3.11.1/README.md +96 -0
  4. aptos_ace_sdk-3.11.1/benchmarks/bench_pairing.py +49 -0
  5. aptos_ace_sdk-3.11.1/pyproject.toml +26 -0
  6. aptos_ace_sdk-3.11.1/src/ace_sdk/__init__.py +46 -0
  7. aptos_ace_sdk-3.11.1/src/ace_sdk/_internal/__init__.py +3 -0
  8. aptos_ace_sdk-3.11.1/src/ace_sdk/_internal/aptos.py +296 -0
  9. aptos_ace_sdk-3.11.1/src/ace_sdk/_internal/common.py +298 -0
  10. aptos_ace_sdk-3.11.1/src/ace_sdk/_internal/deployment.py +45 -0
  11. aptos_ace_sdk-3.11.1/src/ace_sdk/_internal/discovery.py +208 -0
  12. aptos_ace_sdk-3.11.1/src/ace_sdk/_internal/http.py +22 -0
  13. aptos_ace_sdk-3.11.1/src/ace_sdk/admin_recovery.py +258 -0
  14. aptos_ace_sdk-3.11.1/src/ace_sdk/bcs.py +212 -0
  15. aptos_ace_sdk-3.11.1/src/ace_sdk/decryption.py +670 -0
  16. aptos_ace_sdk-3.11.1/src/ace_sdk/dkg/__init__.py +149 -0
  17. aptos_ace_sdk-3.11.1/src/ace_sdk/dkr/__init__.py +158 -0
  18. aptos_ace_sdk-3.11.1/src/ace_sdk/group/__init__.py +196 -0
  19. aptos_ace_sdk-3.11.1/src/ace_sdk/group/bls12381_pairing.py +639 -0
  20. aptos_ace_sdk-3.11.1/src/ace_sdk/group/bls12381fr.py +32 -0
  21. aptos_ace_sdk-3.11.1/src/ace_sdk/group/bls12381g1.py +453 -0
  22. aptos_ace_sdk-3.11.1/src/ace_sdk/group/bls12381g2.py +452 -0
  23. aptos_ace_sdk-3.11.1/src/ace_sdk/ibe_aptos.py +169 -0
  24. aptos_ace_sdk-3.11.1/src/ace_sdk/known_deployments.py +101 -0
  25. aptos_ace_sdk-3.11.1/src/ace_sdk/network.py +262 -0
  26. aptos_ace_sdk-3.11.1/src/ace_sdk/pedersen_polynomial_commitment/__init__.py +188 -0
  27. aptos_ace_sdk-3.11.1/src/ace_sdk/pke/__init__.py +275 -0
  28. aptos_ace_sdk-3.11.1/src/ace_sdk/pke/_ristretto255.py +162 -0
  29. aptos_ace_sdk-3.11.1/src/ace_sdk/pke/elgamal.py +88 -0
  30. aptos_ace_sdk-3.11.1/src/ace_sdk/pke/elgamal_otp_ristretto255.py +217 -0
  31. aptos_ace_sdk-3.11.1/src/ace_sdk/pke/group.py +131 -0
  32. aptos_ace_sdk-3.11.1/src/ace_sdk/pke/hpke_x25519_chacha20poly1305.py +242 -0
  33. aptos_ace_sdk-3.11.1/src/ace_sdk/py.typed +1 -0
  34. aptos_ace_sdk-3.11.1/src/ace_sdk/result.py +64 -0
  35. aptos_ace_sdk-3.11.1/src/ace_sdk/sig/__init__.py +217 -0
  36. aptos_ace_sdk-3.11.1/src/ace_sdk/sigma_dlog_linear/__init__.py +47 -0
  37. aptos_ace_sdk-3.11.1/src/ace_sdk/t_ibe/__init__.py +535 -0
  38. aptos_ace_sdk-3.11.1/src/ace_sdk/t_ibe/bfibe_bls12381_shortpk_otp_hmac.py +380 -0
  39. aptos_ace_sdk-3.11.1/src/ace_sdk/t_ibe/bfibe_bls12381_shortsig_aead.py +416 -0
  40. aptos_ace_sdk-3.11.1/src/ace_sdk/utils.py +78 -0
  41. aptos_ace_sdk-3.11.1/src/ace_sdk/vrf_aptos.py +328 -0
  42. aptos_ace_sdk-3.11.1/src/ace_sdk/vss/__init__.py +7 -0
  43. aptos_ace_sdk-3.11.1/src/ace_sdk/vss/_scheme_types.py +708 -0
  44. aptos_ace_sdk-3.11.1/src/ace_sdk/vss/dealing.py +109 -0
  45. aptos_ace_sdk-3.11.1/tests/__init__.py +2 -0
  46. aptos_ace_sdk-3.11.1/tests/helpers.py +72 -0
  47. aptos_ace_sdk-3.11.1/tests/test_admin_recovery.py +146 -0
  48. aptos_ace_sdk-3.11.1/tests/test_crypto.py +232 -0
  49. aptos_ace_sdk-3.11.1/tests/test_decryption.py +185 -0
  50. aptos_ace_sdk-3.11.1/tests/test_public_api.py +247 -0
  51. aptos_ace_sdk-3.11.1/tests/test_vrf_aptos.py +254 -0
@@ -0,0 +1,9 @@
1
+ .mypy_cache/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .venv/
5
+ __pycache__/
6
+ *.py[cod]
7
+ dist/
8
+ build/
9
+ *.egg-info/
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: aptos-ace-sdk
3
+ Version: 3.11.1
4
+ Summary: Python SDK for ACE (mirrors @aptos-labs/ace-sdk)
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: aptos-sdk>=0.11.0
8
+ Requires-Dist: cryptography>=46.0.0
9
+ Requires-Dist: py-ecc>=8.0.0
10
+ Requires-Dist: pyhpke>=0.6.0
11
+ Requires-Dist: pynacl<2,>=1.5.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # ACE Python SDK
17
+
18
+ Python SDK for ACE (Aptos Confidential Extension). This package mirrors the
19
+ core crypto and wire-format surface of `@aptos-labs/ace-sdk` for Python
20
+ callers.
21
+
22
+ The SDK currently includes:
23
+
24
+ - BCS helpers for the wire formats ACE uses.
25
+ - Scheme-tagged BLS12-381 G1/G2 group wrappers.
26
+ - Feldman/Shamir VSS helpers.
27
+ - Pedersen polynomial commitments, DKG and DKR state decoders.
28
+ - Public-key encryption schemes used by ACE.
29
+ - Scheme-tagged Ed25519 signing helpers for reconstructor/admin flows.
30
+ - Threshold IBE primitives.
31
+ - Known deployment metadata and network state view decoders.
32
+ - Discovery snapshot decoding for the keyless discovery service.
33
+ - Aptos IBE encryption helpers.
34
+ - Worker decryption request helpers for basic and custom flows.
35
+ - Disaster-recovery master-secret reconstruction helpers.
36
+ - Aptos threshold-VRF request, share verification, and output reconstruction helpers.
37
+
38
+ ## Install for local development
39
+
40
+ ```bash
41
+ python -m venv .venv
42
+ .venv/bin/python -m pip install -e '.[dev]'
43
+ ```
44
+
45
+ ## Quick check
46
+
47
+ ```python
48
+ from ace_sdk import pke
49
+
50
+ encryption_key, decryption_key = pke.keygen()
51
+ ciphertext = pke.encrypt(encryption_key, b"hello ace")
52
+ plaintext = pke.decrypt(decryption_key, ciphertext).unwrap_or_throw("decrypt failed")
53
+ assert plaintext == b"hello ace"
54
+ ```
55
+
56
+ The API follows Python naming conventions (`from_bytes`, `to_hex`,
57
+ `derive_encryption_key`) while preserving ACE's byte-level compatibility with
58
+ the TypeScript SDK.
59
+
60
+ The default PKE scheme is HPKE and does not require native system libraries.
61
+ The legacy `ElGamalOtpRistretto255` scheme requires libsodium >= 1.0.18 with
62
+ the `crypto_core_ristretto255_*` APIs. Install it with your system package
63
+ manager, or set `ACE_SDK_LIBSODIUM_PATH` to the library path.
64
+
65
+ ## Admin IBE validation
66
+
67
+ The reconstructed master secret printed by the ACE disaster-recovery flow is a
68
+ 32-byte little-endian Fr scalar. Use it to extract a full identity decryption
69
+ key and validate a ciphertext:
70
+
71
+ ```python
72
+ from ace_sdk import t_ibe
73
+
74
+ idk = t_ibe.extract(msk_scalar=msk_scalar, identity=label).unwrap_or_throw("extract")
75
+ plaintext = t_ibe.decrypt([idk], ciphertext).unwrap_or_throw("decrypt")
76
+ ```
77
+
78
+ For Aptos app encryption, `ibe_aptos.encrypt` builds the same full decryption
79
+ domain as the TypeScript SDK before t-IBE encryption.
80
+
81
+ ## Benchmarks
82
+
83
+ The BLS12-381 pairing implementation is pure Python and intentionally kept out
84
+ of the default test suite. Run its opt-in benchmark with:
85
+
86
+ ```bash
87
+ python benchmarks/bench_pairing.py --iterations 5
88
+ ```
89
+
90
+ ## Aptos custom flow
91
+
92
+ For custom access-control flows, callers provide the application proof payload
93
+ and the PKE keypair that workers should use to encrypt returned IDK shares:
94
+
95
+ ```python
96
+ from ace_sdk import ibe_aptos, pke
97
+
98
+ enc_pk, enc_sk = pke.keygen()
99
+ plaintext = ibe_aptos.decrypt_custom_flow(
100
+ ace_deployment=deployment,
101
+ keypair_id=keypair_id,
102
+ chain_id=chain_id,
103
+ module_addr=module_addr,
104
+ module_name="example",
105
+ label=b"object-id",
106
+ enc_pk=enc_pk,
107
+ enc_sk=enc_sk,
108
+ payload=custom_proof_payload,
109
+ ciphertext=ciphertext_bytes,
110
+ ).unwrap_or_throw("custom decrypt")
111
+ ```
@@ -0,0 +1,96 @@
1
+ # ACE Python SDK
2
+
3
+ Python SDK for ACE (Aptos Confidential Extension). This package mirrors the
4
+ core crypto and wire-format surface of `@aptos-labs/ace-sdk` for Python
5
+ callers.
6
+
7
+ The SDK currently includes:
8
+
9
+ - BCS helpers for the wire formats ACE uses.
10
+ - Scheme-tagged BLS12-381 G1/G2 group wrappers.
11
+ - Feldman/Shamir VSS helpers.
12
+ - Pedersen polynomial commitments, DKG and DKR state decoders.
13
+ - Public-key encryption schemes used by ACE.
14
+ - Scheme-tagged Ed25519 signing helpers for reconstructor/admin flows.
15
+ - Threshold IBE primitives.
16
+ - Known deployment metadata and network state view decoders.
17
+ - Discovery snapshot decoding for the keyless discovery service.
18
+ - Aptos IBE encryption helpers.
19
+ - Worker decryption request helpers for basic and custom flows.
20
+ - Disaster-recovery master-secret reconstruction helpers.
21
+ - Aptos threshold-VRF request, share verification, and output reconstruction helpers.
22
+
23
+ ## Install for local development
24
+
25
+ ```bash
26
+ python -m venv .venv
27
+ .venv/bin/python -m pip install -e '.[dev]'
28
+ ```
29
+
30
+ ## Quick check
31
+
32
+ ```python
33
+ from ace_sdk import pke
34
+
35
+ encryption_key, decryption_key = pke.keygen()
36
+ ciphertext = pke.encrypt(encryption_key, b"hello ace")
37
+ plaintext = pke.decrypt(decryption_key, ciphertext).unwrap_or_throw("decrypt failed")
38
+ assert plaintext == b"hello ace"
39
+ ```
40
+
41
+ The API follows Python naming conventions (`from_bytes`, `to_hex`,
42
+ `derive_encryption_key`) while preserving ACE's byte-level compatibility with
43
+ the TypeScript SDK.
44
+
45
+ The default PKE scheme is HPKE and does not require native system libraries.
46
+ The legacy `ElGamalOtpRistretto255` scheme requires libsodium >= 1.0.18 with
47
+ the `crypto_core_ristretto255_*` APIs. Install it with your system package
48
+ manager, or set `ACE_SDK_LIBSODIUM_PATH` to the library path.
49
+
50
+ ## Admin IBE validation
51
+
52
+ The reconstructed master secret printed by the ACE disaster-recovery flow is a
53
+ 32-byte little-endian Fr scalar. Use it to extract a full identity decryption
54
+ key and validate a ciphertext:
55
+
56
+ ```python
57
+ from ace_sdk import t_ibe
58
+
59
+ idk = t_ibe.extract(msk_scalar=msk_scalar, identity=label).unwrap_or_throw("extract")
60
+ plaintext = t_ibe.decrypt([idk], ciphertext).unwrap_or_throw("decrypt")
61
+ ```
62
+
63
+ For Aptos app encryption, `ibe_aptos.encrypt` builds the same full decryption
64
+ domain as the TypeScript SDK before t-IBE encryption.
65
+
66
+ ## Benchmarks
67
+
68
+ The BLS12-381 pairing implementation is pure Python and intentionally kept out
69
+ of the default test suite. Run its opt-in benchmark with:
70
+
71
+ ```bash
72
+ python benchmarks/bench_pairing.py --iterations 5
73
+ ```
74
+
75
+ ## Aptos custom flow
76
+
77
+ For custom access-control flows, callers provide the application proof payload
78
+ and the PKE keypair that workers should use to encrypt returned IDK shares:
79
+
80
+ ```python
81
+ from ace_sdk import ibe_aptos, pke
82
+
83
+ enc_pk, enc_sk = pke.keygen()
84
+ plaintext = ibe_aptos.decrypt_custom_flow(
85
+ ace_deployment=deployment,
86
+ keypair_id=keypair_id,
87
+ chain_id=chain_id,
88
+ module_addr=module_addr,
89
+ module_name="example",
90
+ label=b"object-id",
91
+ enc_pk=enc_pk,
92
+ enc_sk=enc_sk,
93
+ payload=custom_proof_payload,
94
+ ciphertext=ciphertext_bytes,
95
+ ).unwrap_or_throw("custom decrypt")
96
+ ```
@@ -0,0 +1,49 @@
1
+ # Copyright (c) Aptos Labs
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Small opt-in benchmark for the pure-Python BLS12-381 pairing."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import statistics
9
+ import time
10
+
11
+ from ace_sdk.group import bls12381g1, bls12381g2
12
+ from ace_sdk.group.bls12381_pairing import fp12_to_bytes, pairing_from_jacobian
13
+
14
+
15
+ def measure_pairing(iterations: int) -> list[float]:
16
+ g1 = bls12381g1.g1_generator().pt
17
+ g2 = bls12381g2.g2_generator().pt
18
+ timings_ms: list[float] = []
19
+ for _ in range(iterations):
20
+ start = time.perf_counter()
21
+ result = pairing_from_jacobian(g1, g2)
22
+ fp12_to_bytes(result)
23
+ timings_ms.append((time.perf_counter() - start) * 1000)
24
+ return timings_ms
25
+
26
+
27
+ def main() -> int:
28
+ parser = argparse.ArgumentParser()
29
+ parser.add_argument(
30
+ "-n",
31
+ "--iterations",
32
+ type=int,
33
+ default=5,
34
+ help="number of pairing operations to measure",
35
+ )
36
+ args = parser.parse_args()
37
+ if args.iterations < 1:
38
+ raise ValueError("iterations must be >= 1")
39
+
40
+ measure_pairing(1)
41
+ timings_ms = measure_pairing(args.iterations)
42
+ mean_ms = statistics.fmean(timings_ms)
43
+ print(f"pairing_from_jacobian+fp12_to_bytes: {mean_ms:.2f} ms/op")
44
+ print(f"iterations={args.iterations} min={min(timings_ms):.2f} max={max(timings_ms):.2f}")
45
+ return 0
46
+
47
+
48
+ if __name__ == "__main__":
49
+ raise SystemExit(main())
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aptos-ace-sdk"
7
+ version = "3.11.1"
8
+ description = "Python SDK for ACE (mirrors @aptos-labs/ace-sdk)"
9
+ readme = "README.md"
10
+ license = { text = "Apache-2.0" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "aptos-sdk>=0.11.0",
14
+ "cryptography>=46.0.0",
15
+ "PyNaCl>=1.5.0,<2",
16
+ "py_ecc>=8.0.0",
17
+ "pyhpke>=0.6.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=8.0.0",
23
+ ]
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/ace_sdk"]
@@ -0,0 +1,46 @@
1
+ # Copyright (c) Aptos Labs
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """ACE Python SDK public package entrypoint."""
4
+
5
+ from ace_sdk import (
6
+ dkg,
7
+ dkr,
8
+ group,
9
+ known_deployments,
10
+ network,
11
+ pedersen_polynomial_commitment,
12
+ pke,
13
+ sig,
14
+ sigma_dlog_linear,
15
+ t_ibe,
16
+ vrf_aptos,
17
+ vss,
18
+ )
19
+ from ace_sdk import admin_recovery, decryption, ibe_aptos
20
+ from ace_sdk._internal.deployment import AceDeployment
21
+ from ace_sdk._internal.discovery import DiscoveryViewV0
22
+ from ace_sdk._internal.common import ContractID, FullDecryptionDomain
23
+ from ace_sdk.result import Result
24
+
25
+ __all__ = [
26
+ "AceDeployment",
27
+ "ContractID",
28
+ "DiscoveryViewV0",
29
+ "FullDecryptionDomain",
30
+ "Result",
31
+ "admin_recovery",
32
+ "decryption",
33
+ "dkg",
34
+ "dkr",
35
+ "group",
36
+ "ibe_aptos",
37
+ "known_deployments",
38
+ "network",
39
+ "pedersen_polynomial_commitment",
40
+ "pke",
41
+ "sig",
42
+ "sigma_dlog_linear",
43
+ "t_ibe",
44
+ "vrf_aptos",
45
+ "vss",
46
+ ]
@@ -0,0 +1,3 @@
1
+ # Copyright (c) Aptos Labs
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Internal package mirroring src/_internal/*.ts (common, aptos, solana, deployment)."""
@@ -0,0 +1,296 @@
1
+ # Copyright (c) Aptos Labs
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Mirrors src/_internal/aptos.ts.
4
+
5
+ Only BCS wire-format (serialize/deserialize) is implemented for every Aptos
6
+ public-key/signature scheme: actual signature *verification* happens
7
+ server-side (Move contract / worker), never in this SDK, so we do not need
8
+ real crypto verification logic here -- just byte-exact encode/decode for
9
+ every variant a wallet might produce (Ed25519, AnyPublicKey/SingleKey
10
+ [Secp256k1, Secp256r1, Keyless, FederatedKeyless], MultiEd25519, MultiKey,
11
+ Keyless, FederatedKeyless).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from aptos_sdk.account_address import AccountAddress
17
+
18
+ from ace_sdk.bcs import (
19
+ Deserializer,
20
+ Serializer,
21
+ deserialize_account_address,
22
+ deserialize_option,
23
+ deserialize_option_str,
24
+ deserialize_vector,
25
+ serialize_account_address,
26
+ serialize_option,
27
+ serialize_vector,
28
+ )
29
+ from ace_sdk.result import Result
30
+
31
+ # == Ed25519 ===================================================================
32
+
33
+
34
+ class Ed25519PublicKey:
35
+ LENGTH = 32
36
+
37
+ def __init__(self, key: bytes) -> None:
38
+ if len(key) != self.LENGTH:
39
+ raise ValueError(f"Ed25519PublicKey length should be {self.LENGTH}")
40
+ self.key = key
41
+
42
+ def serialize(self, serializer: Serializer) -> None:
43
+ serializer.serialize_bytes(self.key)
44
+
45
+ @staticmethod
46
+ def deserialize(deserializer: Deserializer) -> "Ed25519PublicKey":
47
+ return Ed25519PublicKey(deserializer.deserialize_bytes())
48
+
49
+
50
+ class Ed25519Signature:
51
+ LENGTH = 64
52
+
53
+ def __init__(self, data: bytes) -> None:
54
+ if len(data) != self.LENGTH:
55
+ raise ValueError(f"Ed25519Signature length should be {self.LENGTH}")
56
+ self.data = data
57
+
58
+ def serialize(self, serializer: Serializer) -> None:
59
+ serializer.serialize_bytes(self.data)
60
+
61
+ @staticmethod
62
+ def deserialize(deserializer: Deserializer) -> "Ed25519Signature":
63
+ return Ed25519Signature(deserializer.deserialize_bytes())
64
+
65
+
66
+ # == Secp256k1 ==================================================================
67
+
68
+
69
+ class Secp256k1PublicKey:
70
+ LENGTH = 65
71
+ COMPRESSED_LENGTH = 33
72
+
73
+ def __init__(self, key: bytes) -> None:
74
+ if len(key) == self.LENGTH:
75
+ self.key = key
76
+ elif len(key) == self.COMPRESSED_LENGTH:
77
+ import ecdsa
78
+
79
+ point = ecdsa.VerifyingKey.from_string(key, curve=ecdsa.SECP256k1).pubkey.point
80
+ x = point.x().to_bytes(32, "big")
81
+ y = point.y().to_bytes(32, "big")
82
+ self.key = b"\x04" + x + y
83
+ else:
84
+ raise ValueError(
85
+ f"Secp256k1PublicKey length should be {self.LENGTH} or "
86
+ f"{self.COMPRESSED_LENGTH}, received {len(key)}"
87
+ )
88
+
89
+ def serialize(self, serializer: Serializer) -> None:
90
+ serializer.serialize_bytes(self.key)
91
+
92
+ @staticmethod
93
+ def deserialize(deserializer: Deserializer) -> "Secp256k1PublicKey":
94
+ return Secp256k1PublicKey(deserializer.deserialize_bytes())
95
+
96
+
97
+ class Secp256k1Signature:
98
+ LENGTH = 64
99
+
100
+ def __init__(self, data: bytes) -> None:
101
+ if len(data) != self.LENGTH:
102
+ raise ValueError(f"Secp256k1Signature length should be {self.LENGTH}")
103
+ self.data = data
104
+
105
+ def serialize(self, serializer: Serializer) -> None:
106
+ serializer.serialize_bytes(self.data)
107
+
108
+ @staticmethod
109
+ def deserialize(deserializer: Deserializer) -> "Secp256k1Signature":
110
+ return Secp256k1Signature(deserializer.deserialize_bytes())
111
+
112
+
113
+ # == Secp256r1 (WebAuthn / passkeys) ============================================
114
+
115
+
116
+ class Secp256r1PublicKey:
117
+ LENGTH = 65
118
+ COMPRESSED_LENGTH = 33
119
+
120
+ def __init__(self, key: bytes) -> None:
121
+ if len(key) == self.COMPRESSED_LENGTH:
122
+ import ecdsa
123
+
124
+ point = ecdsa.VerifyingKey.from_string(key, curve=ecdsa.NIST256p).pubkey.point
125
+ x = point.x().to_bytes(32, "big")
126
+ y = point.y().to_bytes(32, "big")
127
+ self.key = b"\x04" + x + y
128
+ elif len(key) == self.LENGTH:
129
+ self.key = key
130
+ else:
131
+ raise ValueError(
132
+ f"Secp256r1PublicKey length should be {self.LENGTH} or "
133
+ f"{self.COMPRESSED_LENGTH}, received {len(key)}"
134
+ )
135
+
136
+ def serialize(self, serializer: Serializer) -> None:
137
+ serializer.serialize_bytes(self.key)
138
+
139
+ @staticmethod
140
+ def deserialize(deserializer: Deserializer) -> "Secp256r1PublicKey":
141
+ return Secp256r1PublicKey(deserializer.deserialize_bytes())
142
+
143
+
144
+ class Secp256r1Signature:
145
+ LENGTH = 64
146
+
147
+ def __init__(self, data: bytes) -> None:
148
+ if len(data) != self.LENGTH:
149
+ raise ValueError(f"Secp256r1Signature length should be {self.LENGTH}")
150
+ self.data = data
151
+
152
+ def serialize(self, serializer: Serializer) -> None:
153
+ serializer.serialize_bytes(self.data)
154
+
155
+ @staticmethod
156
+ def deserialize(deserializer: Deserializer) -> "Secp256r1Signature":
157
+ return Secp256r1Signature(deserializer.deserialize_bytes())
158
+
159
+
160
+ class WebAuthnSignature:
161
+ def __init__(
162
+ self, signature: bytes, authenticator_data: bytes, client_data_json: bytes
163
+ ) -> None:
164
+ self.signature = signature
165
+ self.authenticator_data = authenticator_data
166
+ self.client_data_json = client_data_json
167
+
168
+ def serialize(self, serializer: Serializer) -> None:
169
+ serializer.serialize_u32_as_uleb128(0)
170
+ serializer.serialize_bytes(self.signature)
171
+ serializer.serialize_bytes(self.authenticator_data)
172
+ serializer.serialize_bytes(self.client_data_json)
173
+
174
+ @staticmethod
175
+ def deserialize(deserializer: Deserializer) -> "WebAuthnSignature":
176
+ variant_id = deserializer.deserialize_uleb128_as_u32()
177
+ if variant_id != 0:
178
+ raise ValueError(f"Invalid id for WebAuthnSignature: {variant_id}")
179
+ signature = deserializer.deserialize_bytes()
180
+ authenticator_data = deserializer.deserialize_bytes()
181
+ client_data_json = deserializer.deserialize_bytes()
182
+ return WebAuthnSignature(signature, authenticator_data, client_data_json)
183
+
184
+
185
+ # == MultiEd25519 ================================================================
186
+
187
+
188
+ class MultiEd25519PublicKey:
189
+ MAX_KEYS = 32
190
+ MIN_KEYS = 2
191
+ MIN_THRESHOLD = 1
192
+
193
+ def __init__(self, public_keys: list[Ed25519PublicKey], threshold: int) -> None:
194
+ if not (self.MIN_KEYS <= len(public_keys) <= self.MAX_KEYS):
195
+ raise ValueError(
196
+ f"Must have between {self.MIN_KEYS} and {self.MAX_KEYS} public keys"
197
+ )
198
+ if not (self.MIN_THRESHOLD <= threshold <= len(public_keys)):
199
+ raise ValueError(f"Threshold must be between {self.MIN_THRESHOLD} and {len(public_keys)}")
200
+ self.public_keys = public_keys
201
+ self.threshold = threshold
202
+
203
+ def to_bytes_inner(self) -> bytes:
204
+ out = bytearray()
205
+ for k in self.public_keys:
206
+ out += k.key
207
+ out.append(self.threshold)
208
+ return bytes(out)
209
+
210
+ def serialize(self, serializer: Serializer) -> None:
211
+ serializer.serialize_bytes(self.to_bytes_inner())
212
+
213
+ @staticmethod
214
+ def deserialize(deserializer: Deserializer) -> "MultiEd25519PublicKey":
215
+ data = deserializer.deserialize_bytes()
216
+ threshold = data[-1]
217
+ keys: list[Ed25519PublicKey] = []
218
+ i = 0
219
+ while i < len(data) - 1:
220
+ keys.append(Ed25519PublicKey(data[i : i + Ed25519PublicKey.LENGTH]))
221
+ i += Ed25519PublicKey.LENGTH
222
+ return MultiEd25519PublicKey(keys, threshold)
223
+
224
+
225
+ class MultiEd25519Signature:
226
+ def __init__(self, signatures: list[Ed25519Signature], bitmap: bytes) -> None:
227
+ self.signatures = signatures
228
+ self.bitmap = bitmap
229
+
230
+ def to_bytes_inner(self) -> bytes:
231
+ out = bytearray()
232
+ for s in self.signatures:
233
+ out += s.data
234
+ out += self.bitmap
235
+ return bytes(out)
236
+
237
+ def serialize(self, serializer: Serializer) -> None:
238
+ serializer.serialize_bytes(self.to_bytes_inner())
239
+
240
+ @staticmethod
241
+ def deserialize(deserializer: Deserializer) -> "MultiEd25519Signature":
242
+ data = deserializer.deserialize_bytes()
243
+ bitmap = data[-4:]
244
+ signatures: list[Ed25519Signature] = []
245
+ i = 0
246
+ end = len(data) - len(bitmap)
247
+ while i < end:
248
+ signatures.append(Ed25519Signature(data[i : i + Ed25519Signature.LENGTH]))
249
+ i += Ed25519Signature.LENGTH
250
+ return MultiEd25519Signature(signatures, bitmap)
251
+
252
+
253
+ # == Ephemeral (used inside Keyless) ============================================
254
+
255
+
256
+ class EphemeralPublicKey:
257
+ """Only Ed25519 variant exists today (EphemeralPublicKeyVariant.Ed25519 = 0)."""
258
+
259
+ VARIANT_ED25519 = 0
260
+
261
+ def __init__(self, public_key: Ed25519PublicKey) -> None:
262
+ if not isinstance(public_key, Ed25519PublicKey):
263
+ raise ValueError(f"Unsupported key for EphemeralPublicKey - {type(public_key)}")
264
+ self.public_key = public_key
265
+ self.variant = self.VARIANT_ED25519
266
+
267
+ def serialize(self, serializer: Serializer) -> None:
268
+ serializer.serialize_u32_as_uleb128(self.VARIANT_ED25519)
269
+ self.public_key.serialize(serializer)
270
+
271
+ @staticmethod
272
+ def deserialize(deserializer: Deserializer) -> "EphemeralPublicKey":
273
+ index = deserializer.deserialize_uleb128_as_u32()
274
+ if index == EphemeralPublicKey.VARIANT_ED25519:
275
+ return EphemeralPublicKey(Ed25519PublicKey.deserialize(deserializer))
276
+ raise ValueError(f"Unknown variant index for EphemeralPublicKey: {index}")
277
+
278
+
279
+ class EphemeralSignature:
280
+ VARIANT_ED25519 = 0
281
+
282
+ def __init__(self, signature: Ed25519Signature) -> None:
283
+ if not isinstance(signature, Ed25519Signature):
284
+ raise ValueError(f"Unsupported signature for EphemeralSignature - {type(signature)}")
285
+ self.signature = signature
286
+
287
+ def serialize(self, serializer: Serializer) -> None:
288
+ serializer.serialize_u32_as_uleb128(self.VARIANT_ED25519)
289
+ self.signature.serialize(serializer)
290
+
291
+ @staticmethod
292
+ def deserialize(deserializer: Deserializer) -> "EphemeralSignature":
293
+ index = deserializer.deserialize_uleb128_as_u32()
294
+ if index == EphemeralSignature.VARIANT_ED25519:
295
+ return EphemeralSignature(Ed25519Signature.deserialize(deserializer))
296
+ raise ValueError(f"Unknown variant index for EphemeralSignature: {index}")