modularot 0.1.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.
- modularot-0.1.0/LICENSE +21 -0
- modularot-0.1.0/PKG-INFO +63 -0
- modularot-0.1.0/README.md +43 -0
- modularot-0.1.0/modularot/__init__.py +49 -0
- modularot-0.1.0/modularot/cli.py +381 -0
- modularot-0.1.0/modularot/custom_kems/__init__.py +0 -0
- modularot-0.1.0/modularot/custom_kems/ggh_fo/__init__.py +0 -0
- modularot-0.1.0/modularot/custom_kems/ggh_fo/ggh_fo.py +280 -0
- modularot-0.1.0/modularot/custom_kems/ntru_fo/__init__.py +0 -0
- modularot-0.1.0/modularot/custom_kems/ntru_fo/ntru_fo.py +347 -0
- modularot-0.1.0/modularot/kem_adapter/__init__.py +0 -0
- modularot-0.1.0/modularot/kem_adapter/adapter.py +58 -0
- modularot-0.1.0/modularot/oblivious_sampling.py +207 -0
- modularot-0.1.0/modularot/ot_protocol.py +332 -0
- modularot-0.1.0/modularot.egg-info/PKG-INFO +63 -0
- modularot-0.1.0/modularot.egg-info/SOURCES.txt +20 -0
- modularot-0.1.0/modularot.egg-info/dependency_links.txt +1 -0
- modularot-0.1.0/modularot.egg-info/entry_points.txt +2 -0
- modularot-0.1.0/modularot.egg-info/requires.txt +7 -0
- modularot-0.1.0/modularot.egg-info/top_level.txt +1 -0
- modularot-0.1.0/pyproject.toml +38 -0
- modularot-0.1.0/setup.cfg +4 -0
modularot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriele Bottani
|
|
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.
|
modularot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modularot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modular KEM-based 1-out-of-n Oblivious Transfer (semi-honest), agnostic to the underlying post-quantum KEM
|
|
5
|
+
Author: Gabriele Bottani
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TheGaBr0/modularot
|
|
8
|
+
Project-URL: Source, https://github.com/TheGaBr0/modularot/tree/release
|
|
9
|
+
Project-URL: Issues, https://github.com/TheGaBr0/modularot/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: cryptography>=41.0
|
|
14
|
+
Requires-Dist: liboqs-python
|
|
15
|
+
Provides-Extra: custom-kems
|
|
16
|
+
Requires-Dist: python-flint>=0.5; extra == "custom-kems"
|
|
17
|
+
Requires-Dist: GGH_crypto>=1.0.5; extra == "custom-kems"
|
|
18
|
+
Requires-Dist: numpy; extra == "custom-kems"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# modularot
|
|
22
|
+
Modular KEM-based 1-out-of-*n* Oblivious Transfer (semi-honest). The protocol never names a specific KEM: the only KEM-specific step, sampling an *oblivious* public key (a well-formed key nobody holds the secret for), is passed in as a callable.
|
|
23
|
+
|
|
24
|
+
The receiver sends one real public key for its choice and oblivious keys for the other branches. The sender encapsulates under every key and seals each message with ChaCha20-Poly1305. The receiver can only open the chosen one.
|
|
25
|
+
|
|
26
|
+
Samplers are included for ML-KEM / Kyber, FrodoKEM, NTRU-HPS / HRSS and sntrup761 (via liboqs), plus two research KEMs, `NTRU_FO` and `GGH_FO`, built with the Fujisaki-Okamoto transform.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
```
|
|
30
|
+
pip install modularot
|
|
31
|
+
```
|
|
32
|
+
Requires the [liboqs](https://github.com/open-quantum-safe/liboqs) C library, installed before `liboqs-python`. The custom KEMs need an extra (`python-flint`):
|
|
33
|
+
```
|
|
34
|
+
pip install "modularot[custom-kems]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
```python
|
|
39
|
+
from modularot import run_ot, oqs_kem_factory, resolve_oqs
|
|
40
|
+
|
|
41
|
+
result = run_ot(
|
|
42
|
+
make_kem=oqs_kem_factory("ML-KEM-768"),
|
|
43
|
+
sampler=resolve_oqs("ML-KEM-768"),
|
|
44
|
+
label="ML-KEM-768",
|
|
45
|
+
choice=1, # branch the receiver wants
|
|
46
|
+
n=4, # 1-out-of-4
|
|
47
|
+
)
|
|
48
|
+
assert result["ok"] # receiver got the message it asked for
|
|
49
|
+
assert result["secure"] # receiver could not open any other branch
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Command line:
|
|
53
|
+
```
|
|
54
|
+
modularot run --kem ML-KEM-768 --n 3 --choice 1
|
|
55
|
+
modularot list-kems
|
|
56
|
+
modularot bench --ns 2 4 8 --csv out.csv
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Other KEMs
|
|
60
|
+
Wrap the KEM in a `KEMAdapter(gen, encap, decap)` (all on `bytes`; `encap` returns `(shared_secret, ciphertext)`) and write a sampler `real_pk -> oblivious_pk` that returns a key in the same encoding as a real one, without running key generation. Then pass both to `run_ot`.
|
|
61
|
+
|
|
62
|
+
## Limits
|
|
63
|
+
Semi-honest security only. `NTRU_FO` and `GGH_FO` are research code, not quantum-safe or production-ready.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# modularot
|
|
2
|
+
Modular KEM-based 1-out-of-*n* Oblivious Transfer (semi-honest). The protocol never names a specific KEM: the only KEM-specific step, sampling an *oblivious* public key (a well-formed key nobody holds the secret for), is passed in as a callable.
|
|
3
|
+
|
|
4
|
+
The receiver sends one real public key for its choice and oblivious keys for the other branches. The sender encapsulates under every key and seals each message with ChaCha20-Poly1305. The receiver can only open the chosen one.
|
|
5
|
+
|
|
6
|
+
Samplers are included for ML-KEM / Kyber, FrodoKEM, NTRU-HPS / HRSS and sntrup761 (via liboqs), plus two research KEMs, `NTRU_FO` and `GGH_FO`, built with the Fujisaki-Okamoto transform.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
```
|
|
10
|
+
pip install modularot
|
|
11
|
+
```
|
|
12
|
+
Requires the [liboqs](https://github.com/open-quantum-safe/liboqs) C library, installed before `liboqs-python`. The custom KEMs need an extra (`python-flint`):
|
|
13
|
+
```
|
|
14
|
+
pip install "modularot[custom-kems]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
```python
|
|
19
|
+
from modularot import run_ot, oqs_kem_factory, resolve_oqs
|
|
20
|
+
|
|
21
|
+
result = run_ot(
|
|
22
|
+
make_kem=oqs_kem_factory("ML-KEM-768"),
|
|
23
|
+
sampler=resolve_oqs("ML-KEM-768"),
|
|
24
|
+
label="ML-KEM-768",
|
|
25
|
+
choice=1, # branch the receiver wants
|
|
26
|
+
n=4, # 1-out-of-4
|
|
27
|
+
)
|
|
28
|
+
assert result["ok"] # receiver got the message it asked for
|
|
29
|
+
assert result["secure"] # receiver could not open any other branch
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Command line:
|
|
33
|
+
```
|
|
34
|
+
modularot run --kem ML-KEM-768 --n 3 --choice 1
|
|
35
|
+
modularot list-kems
|
|
36
|
+
modularot bench --ns 2 4 8 --csv out.csv
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Other KEMs
|
|
40
|
+
Wrap the KEM in a `KEMAdapter(gen, encap, decap)` (all on `bytes`; `encap` returns `(shared_secret, ciphertext)`) and write a sampler `real_pk -> oblivious_pk` that returns a key in the same encoding as a real one, without running key generation. Then pass both to `run_ot`.
|
|
41
|
+
|
|
42
|
+
## Limits
|
|
43
|
+
Semi-honest security only. `NTRU_FO` and `GGH_FO` are research code, not quantum-safe or production-ready.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
modularot -- modular KEM-based 1-out-of-n Oblivious Transfer (semi-honest).
|
|
3
|
+
|
|
4
|
+
The protocol is KEM-agnostic; the only KEM-specific operation (sampling an
|
|
5
|
+
oblivious public key) is injected as a callable. See `ot_protocol` for the
|
|
6
|
+
construction and `oblivious_sampling` for the samplers.
|
|
7
|
+
|
|
8
|
+
Quick start (any liboqs lattice KEM)::
|
|
9
|
+
|
|
10
|
+
from modularot import run_ot, oqs_kem_factory, resolve_oqs
|
|
11
|
+
|
|
12
|
+
result = run_ot(
|
|
13
|
+
make_kem=oqs_kem_factory("ML-KEM-768"),
|
|
14
|
+
sampler=resolve_oqs("ML-KEM-768"),
|
|
15
|
+
label="ML-KEM-768",
|
|
16
|
+
choice=1, # branch the receiver wants
|
|
17
|
+
n=4, # 1-out-of-4
|
|
18
|
+
)
|
|
19
|
+
assert result["ok"] and result["secure"]
|
|
20
|
+
|
|
21
|
+
`OTSender`, `OTReceiver` and `KEMAdapter` are exported for custom KEMs; see
|
|
22
|
+
`modularot.custom_kems` (NTRU_FO, GGH_FO) for examples.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .ot_protocol import (
|
|
26
|
+
OTReceiver,
|
|
27
|
+
OTSender,
|
|
28
|
+
run_ot,
|
|
29
|
+
receiver_cannot_read_other,
|
|
30
|
+
)
|
|
31
|
+
from .ot_protocol import _oqs_kem_factory as oqs_kem_factory
|
|
32
|
+
from .kem_adapter.adapter import KEM, KEMAdapter, KEMInfo
|
|
33
|
+
from .oblivious_sampling import resolve_oqs, is_supported_oqs
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"OTReceiver",
|
|
39
|
+
"OTSender",
|
|
40
|
+
"run_ot",
|
|
41
|
+
"receiver_cannot_read_other",
|
|
42
|
+
"oqs_kem_factory",
|
|
43
|
+
"KEM",
|
|
44
|
+
"KEMAdapter",
|
|
45
|
+
"KEMInfo",
|
|
46
|
+
"resolve_oqs",
|
|
47
|
+
"is_supported_oqs",
|
|
48
|
+
"__version__",
|
|
49
|
+
]
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for modularot: the modular KEM-based 1-out-of-n OT.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
run -- run one 1-out-of-n OT transfer (KEM, n, choice, messages).
|
|
6
|
+
bench -- per KEM and per n: request/transfer/finalize timing, bandwidth
|
|
7
|
+
and KeyGen-vs-oblivious-sample cost. Console tables + CSV.
|
|
8
|
+
list-kems -- list the names usable with `run --kem` / `bench --kems`.
|
|
9
|
+
|
|
10
|
+
Usage (`modularot` entry point or `python -m modularot.cli`):
|
|
11
|
+
modularot run # ML-KEM-768, 1-out-of-2, placeholder messages
|
|
12
|
+
modularot run --kem ML-KEM-768 --n 4 --choice 2
|
|
13
|
+
modularot run --n 3 --message "a" --message "b" --message "c" --choice 1
|
|
14
|
+
modularot run --kem GGH_FO --ggh-dim 200 --quiet --output secret.bin
|
|
15
|
+
modularot list-kems
|
|
16
|
+
modularot bench # default KEM subset + NTRU_FO + GGH_FO
|
|
17
|
+
modularot bench --all # every supported liboqs mechanism, + custom
|
|
18
|
+
modularot bench --kems ML-KEM-768 sntrup761 --no-custom
|
|
19
|
+
modularot bench --ns 2 4 8 16 32 --reps 5 --csv out.csv
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import csv
|
|
24
|
+
import statistics
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
import oqs
|
|
29
|
+
|
|
30
|
+
from .ot_protocol import run_ot, _oqs_kem_factory, _fmt_ns
|
|
31
|
+
from .oblivious_sampling import resolve_oqs, is_supported_oqs, sample_oblivious_ntru_fo
|
|
32
|
+
from .kem_adapter.adapter import KEMAdapter
|
|
33
|
+
|
|
34
|
+
# One per (family, security level): short run covering every encoding family.
|
|
35
|
+
DEFAULT_KEMS = [
|
|
36
|
+
"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", # structured, q=3329
|
|
37
|
+
"FrodoKEM-640-AES", "FrodoKEM-976-AES", "FrodoKEM-1344-AES", # unstructured LWE
|
|
38
|
+
"NTRU-HPS-2048-509", "NTRU-HRSS-701", # NTRU, bit-packed
|
|
39
|
+
"sntrup761", # NTRU Prime, mixed-radix
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
DEFAULT_NS = [2, 4, 8, 16, 32]
|
|
43
|
+
|
|
44
|
+
CUSTOM_KEM_NAMES = {"NTRU_FO", "GGH_FO"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ntrufo_entry(N: int, q: int = 8192, p: int = 3):
|
|
48
|
+
"""(label, kem_factory, sampler) for NTRU_FO; each factory call builds an
|
|
49
|
+
independent instance."""
|
|
50
|
+
from .custom_kems.ntru_fo.ntru_fo import NTRU_FO
|
|
51
|
+
|
|
52
|
+
def factory():
|
|
53
|
+
impl = NTRU_FO(N=N, p=p, q=q)
|
|
54
|
+
return KEMAdapter(gen=impl.generate_keys, encap=impl.encapsulate,
|
|
55
|
+
decap=impl.decapsulate)
|
|
56
|
+
|
|
57
|
+
return (f"NTRU_FO-{N}", factory, lambda pk: sample_oblivious_ntru_fo(pk, q=q))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _gghfo_entry(dimension: int):
|
|
61
|
+
"""(label, kem_factory, sampler) for GGH-HNF FO (GGH_private=True). Needs
|
|
62
|
+
`pip install modularot[custom-kems]`; dimension=200 is teaching-scale.
|
|
63
|
+
Classic GGH has no sound oblivious sampler."""
|
|
64
|
+
from .custom_kems.ggh_fo import ggh_fo
|
|
65
|
+
from .oblivious_sampling import sample_oblivious_ggh_hnf
|
|
66
|
+
|
|
67
|
+
def factory():
|
|
68
|
+
return KEMAdapter(
|
|
69
|
+
gen=lambda: ggh_fo.gen(dimension=dimension),
|
|
70
|
+
encap=ggh_fo.encaps,
|
|
71
|
+
decap=ggh_fo.decaps,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return (f"GGH_FO-{dimension}", factory, sample_oblivious_ggh_hnf)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_entry(kem: str, ntrufo_n: int, ggh_dim: int):
|
|
78
|
+
"""(label, kem_factory, sampler) for one KEM name -- custom or liboqs."""
|
|
79
|
+
if kem == "NTRU_FO":
|
|
80
|
+
try:
|
|
81
|
+
return _ntrufo_entry(ntrufo_n)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise SystemExit(f"cannot build NTRU_FO: {e}")
|
|
84
|
+
if kem == "GGH_FO":
|
|
85
|
+
try:
|
|
86
|
+
return _gghfo_entry(ggh_dim)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
raise SystemExit(
|
|
89
|
+
f"cannot build GGH_FO (needs the GGH_crypto package): {e}")
|
|
90
|
+
|
|
91
|
+
enabled = set(oqs.get_enabled_kem_mechanisms())
|
|
92
|
+
if kem not in enabled:
|
|
93
|
+
raise SystemExit(f"KEM '{kem}' not enabled in this liboqs build "
|
|
94
|
+
f"(see `modularot list-kems`)")
|
|
95
|
+
if not is_supported_oqs(kem):
|
|
96
|
+
raise SystemExit(f"KEM '{kem}' has no oblivious sampler, refused "
|
|
97
|
+
f"rather than weaken security")
|
|
98
|
+
return (kem, _oqs_kem_factory(kem), resolve_oqs(kem))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _median(xs):
|
|
102
|
+
return int(statistics.median(xs))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def warmup(kem_factory) -> None:
|
|
106
|
+
"""Absorb lazy-init overhead outside the timed region."""
|
|
107
|
+
kem = kem_factory()
|
|
108
|
+
pk, sk = kem.Gen()
|
|
109
|
+
ss, ct = kem.encaps(pk)
|
|
110
|
+
kem.decaps(sk, ct)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def bench_sampler_vs_keygen(kem_factory, sampler, reps: int) -> dict:
|
|
114
|
+
"""Median cost of one real KeyGen vs one oblivious sample."""
|
|
115
|
+
keygen_ns, sample_ns = [], []
|
|
116
|
+
for _ in range(reps):
|
|
117
|
+
kem = kem_factory()
|
|
118
|
+
t0 = time.perf_counter_ns()
|
|
119
|
+
pk, _sk = kem.Gen()
|
|
120
|
+
keygen_ns.append(time.perf_counter_ns() - t0)
|
|
121
|
+
|
|
122
|
+
t0 = time.perf_counter_ns()
|
|
123
|
+
sampler(pk)
|
|
124
|
+
sample_ns.append(time.perf_counter_ns() - t0)
|
|
125
|
+
return {"keygen": _median(keygen_ns), "sample": _median(sample_ns)}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def bench_cell(label: str, kem_factory, sampler, n: int, reps: int) -> dict:
|
|
129
|
+
"""Median timings over `reps` runs of a full 1-out-of-n OT session."""
|
|
130
|
+
runs = []
|
|
131
|
+
for r in range(reps):
|
|
132
|
+
# security check on the first rep only
|
|
133
|
+
runs.append(run_ot(kem_factory, sampler, label,
|
|
134
|
+
choice=0, n=n, check_security=(r == 0),
|
|
135
|
+
verbose=False))
|
|
136
|
+
first = runs[0]
|
|
137
|
+
return {
|
|
138
|
+
"KEM": label, "n": n,
|
|
139
|
+
"request": _median([r["request"] for r in runs]),
|
|
140
|
+
"transfer": _median([r["transfer"] for r in runs]),
|
|
141
|
+
"finalize": _median([r["finalize"] for r in runs]),
|
|
142
|
+
"TOTAL": _median([r["TOTAL"] for r in runs]),
|
|
143
|
+
"pk_bw": first["pk_bw"], "ct_bw": first["ct_bw"],
|
|
144
|
+
"ok": all(r["ok"] for r in runs), "secure": first["secure"],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── `run`: use the library from the command line ────────────────────────────
|
|
149
|
+
|
|
150
|
+
def cmd_run(args) -> None:
|
|
151
|
+
label, factory, sampler = _build_entry(args.kem, args.ntrufo_n, args.ggh_dim)
|
|
152
|
+
|
|
153
|
+
n = args.n
|
|
154
|
+
if not 0 <= args.choice < n:
|
|
155
|
+
raise SystemExit(f"--choice must be in [0, {n - 1}]")
|
|
156
|
+
|
|
157
|
+
if args.message and args.message_file:
|
|
158
|
+
raise SystemExit("use either --message or --message-file, not both")
|
|
159
|
+
if args.message:
|
|
160
|
+
if len(args.message) != n:
|
|
161
|
+
raise SystemExit(
|
|
162
|
+
f"need exactly {n} --message values (one per branch, in "
|
|
163
|
+
f"order), got {len(args.message)}")
|
|
164
|
+
messages = [m.encode() for m in args.message]
|
|
165
|
+
elif args.message_file:
|
|
166
|
+
if len(args.message_file) != n:
|
|
167
|
+
raise SystemExit(
|
|
168
|
+
f"need exactly {n} --message-file values (one per branch, "
|
|
169
|
+
f"in order), got {len(args.message_file)}")
|
|
170
|
+
messages = [open(p, "rb").read() for p in args.message_file]
|
|
171
|
+
else:
|
|
172
|
+
messages = None # run_ot falls back to placeholder messages
|
|
173
|
+
|
|
174
|
+
result = run_ot(factory, sampler, label, choice=args.choice, n=n,
|
|
175
|
+
messages=messages, check_security=not args.no_security_check,
|
|
176
|
+
verbose=not args.quiet)
|
|
177
|
+
|
|
178
|
+
if args.quiet:
|
|
179
|
+
sys.stdout.buffer.write(result["message"] or b"")
|
|
180
|
+
|
|
181
|
+
if args.output:
|
|
182
|
+
with open(args.output, "wb") as f:
|
|
183
|
+
f.write(result["message"] or b"")
|
|
184
|
+
if not args.quiet:
|
|
185
|
+
print(f"Decrypted message written to {args.output}")
|
|
186
|
+
|
|
187
|
+
if not result["ok"]:
|
|
188
|
+
raise SystemExit(1)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ── `list-kems` ───────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
def cmd_list_kems(args) -> None:
|
|
194
|
+
enabled = sorted(a for a in oqs.get_enabled_kem_mechanisms() if is_supported_oqs(a))
|
|
195
|
+
print(f"liboqs KEMs with an oblivious sampler ({len(enabled)}, usable with --kem/--kems):")
|
|
196
|
+
for k in enabled:
|
|
197
|
+
print(f" {k}")
|
|
198
|
+
print("\nCustom KEMs (need `pip install modularot[custom-kems]`):")
|
|
199
|
+
print(" NTRU_FO ring degree via --ntrufo-n (default 701)")
|
|
200
|
+
print(" GGH_FO dimension via --ggh-dim (default 200)")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ── `bench`: scalability sweep ───────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
def cmd_bench(args) -> None:
|
|
206
|
+
enabled = set(oqs.get_enabled_kem_mechanisms())
|
|
207
|
+
if args.all:
|
|
208
|
+
kems = sorted(a for a in enabled if is_supported_oqs(a))
|
|
209
|
+
else:
|
|
210
|
+
kems = args.kems or DEFAULT_KEMS
|
|
211
|
+
missing = [a for a in kems if a not in enabled]
|
|
212
|
+
unsupported = [a for a in kems if a in enabled and not is_supported_oqs(a)]
|
|
213
|
+
if missing:
|
|
214
|
+
print(f"skipping (not enabled in this liboqs build): {', '.join(missing)}")
|
|
215
|
+
if unsupported:
|
|
216
|
+
print(f"skipping (no oblivious sampler): {', '.join(unsupported)}")
|
|
217
|
+
kems = [a for a in kems if a in enabled and is_supported_oqs(a)]
|
|
218
|
+
|
|
219
|
+
# (label, kem_factory, sampler) — oqs mechanisms plus the custom KEMs
|
|
220
|
+
entries = [(alg, _oqs_kem_factory(alg), resolve_oqs(alg)) for alg in kems]
|
|
221
|
+
|
|
222
|
+
if not args.no_custom:
|
|
223
|
+
try:
|
|
224
|
+
entries.append(_ntrufo_entry(args.ntrufo_n))
|
|
225
|
+
except Exception as e:
|
|
226
|
+
print(f"skipping NTRU_FO: {e}")
|
|
227
|
+
try:
|
|
228
|
+
entries.append(_gghfo_entry(args.ggh_dim))
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(f"skipping GGH_FO (needs the GGH_crypto package): {e}")
|
|
231
|
+
|
|
232
|
+
if not entries:
|
|
233
|
+
raise SystemExit("no benchmarkable KEMs selected")
|
|
234
|
+
|
|
235
|
+
print(f"KEMs: {', '.join(label for label, _, _ in entries)}")
|
|
236
|
+
print(f"n: {args.ns} reps/cell: {args.reps}\n")
|
|
237
|
+
|
|
238
|
+
# ── table 1: real KeyGen vs oblivious sample ──────────────────────────────
|
|
239
|
+
print("═" * 78)
|
|
240
|
+
print("KeyGen vs oblivious sample (median, one call)")
|
|
241
|
+
print("═" * 78)
|
|
242
|
+
print(f"{'KEM':<22} {'KeyGen':>14} {'ObliviousSample':>16} {'ratio':>9}")
|
|
243
|
+
print("─" * 78)
|
|
244
|
+
ratios = {}
|
|
245
|
+
for label, factory, sampler in entries:
|
|
246
|
+
warmup(factory)
|
|
247
|
+
r = bench_sampler_vs_keygen(factory, sampler, args.reps)
|
|
248
|
+
ratio = r["keygen"] / max(r["sample"], 1)
|
|
249
|
+
ratios[label] = r
|
|
250
|
+
print(f"{label:<22} {_fmt_ns(r['keygen']):>14} {_fmt_ns(r['sample']):>16} "
|
|
251
|
+
f"{ratio:>8.1f}x")
|
|
252
|
+
print("─" * 78)
|
|
253
|
+
|
|
254
|
+
# ── table 2: full-session scaling over n ─────────────────────────────────
|
|
255
|
+
rows = []
|
|
256
|
+
for label, factory, sampler in entries:
|
|
257
|
+
print(f"\n{'═' * 98}")
|
|
258
|
+
print(f"{label} — 1-out-of-n scaling (medians of {args.reps} runs)")
|
|
259
|
+
print(f"{'═' * 98}")
|
|
260
|
+
print(f"{'n':>4} {'Request':>12} {'Transfer':>12} {'Finalize':>12} "
|
|
261
|
+
f"{'TOTAL':>12} {'BW R->S':>12} {'BW S->R':>12} OK SEC")
|
|
262
|
+
print("─" * 98)
|
|
263
|
+
for n in args.ns:
|
|
264
|
+
cell = bench_cell(label, factory, sampler, n, args.reps)
|
|
265
|
+
rows.append(cell)
|
|
266
|
+
print(f"{n:>4} {_fmt_ns(cell['request']):>12} {_fmt_ns(cell['transfer']):>12} "
|
|
267
|
+
f"{_fmt_ns(cell['finalize']):>12} {_fmt_ns(cell['TOTAL']):>12} "
|
|
268
|
+
f"{cell['pk_bw']:>10} B {cell['ct_bw']:>10} B "
|
|
269
|
+
f"{'✓' if cell['ok'] else '✗'} {'✓' if cell['secure'] else '✗'}")
|
|
270
|
+
print("─" * 98)
|
|
271
|
+
|
|
272
|
+
# ── CSV for plots ─────────────────────────────────────────────────────────
|
|
273
|
+
# units in the header, fixed-precision milliseconds
|
|
274
|
+
fields = ["kem", "n",
|
|
275
|
+
"request_ms", "transfer_ms", "finalize_ms", "total_ms",
|
|
276
|
+
"pk_bytes", "ct_bytes", "total_bytes",
|
|
277
|
+
"keygen_ms", "obliv_sample_ms", "keygen_over_sample",
|
|
278
|
+
"ok", "secure"]
|
|
279
|
+
|
|
280
|
+
def _ms(ns: int) -> float:
|
|
281
|
+
return round(ns / 1e6, 4)
|
|
282
|
+
|
|
283
|
+
with open(args.csv, "w", newline="") as f:
|
|
284
|
+
w = csv.DictWriter(f, fieldnames=fields)
|
|
285
|
+
w.writeheader()
|
|
286
|
+
for row in rows:
|
|
287
|
+
r = ratios[row["KEM"]]
|
|
288
|
+
w.writerow({
|
|
289
|
+
"kem": row["KEM"], "n": row["n"],
|
|
290
|
+
"request_ms": _ms(row["request"]),
|
|
291
|
+
"transfer_ms": _ms(row["transfer"]),
|
|
292
|
+
"finalize_ms": _ms(row["finalize"]),
|
|
293
|
+
"total_ms": _ms(row["TOTAL"]),
|
|
294
|
+
"pk_bytes": row["pk_bw"],
|
|
295
|
+
"ct_bytes": row["ct_bw"],
|
|
296
|
+
"total_bytes": row["pk_bw"] + row["ct_bw"],
|
|
297
|
+
"keygen_ms": _ms(r["keygen"]),
|
|
298
|
+
"obliv_sample_ms": _ms(r["sample"]),
|
|
299
|
+
"keygen_over_sample": round(r["keygen"] / max(r["sample"], 1), 2),
|
|
300
|
+
"ok": row["ok"], "secure": row["secure"],
|
|
301
|
+
})
|
|
302
|
+
print(f"\nCSV written to {args.csv} ({len(rows)} rows)")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ── argument parsing / dispatch ──────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
def _add_run_args(p: argparse.ArgumentParser) -> None:
|
|
308
|
+
p.add_argument("--kem", default="ML-KEM-768",
|
|
309
|
+
help="KEM mechanism name, liboqs or custom (NTRU_FO, "
|
|
310
|
+
"GGH_FO); see `modularot list-kems` (default: ML-KEM-768)")
|
|
311
|
+
p.add_argument("--n", type=int, default=2,
|
|
312
|
+
help="number of branches, 1-out-of-n (default: 2)")
|
|
313
|
+
p.add_argument("--choice", type=int, default=0,
|
|
314
|
+
help="branch the receiver wants to learn (default: 0)")
|
|
315
|
+
p.add_argument("--message", action="append", metavar="MSG",
|
|
316
|
+
help="sender's message for one branch (utf-8 text); pass "
|
|
317
|
+
"once per branch, in branch order (default: "
|
|
318
|
+
"placeholder demo messages)")
|
|
319
|
+
p.add_argument("--message-file", action="append", metavar="PATH",
|
|
320
|
+
help="like --message but read raw bytes from a file; "
|
|
321
|
+
"pass once per branch, in branch order")
|
|
322
|
+
p.add_argument("--output", metavar="PATH",
|
|
323
|
+
help="write the receiver's decrypted message to this "
|
|
324
|
+
"file (default: printed to stdout)")
|
|
325
|
+
p.add_argument("--ggh-dim", type=int, metavar="DIM", default=200,
|
|
326
|
+
help="dimension for --kem GGH_FO (default: 200)")
|
|
327
|
+
p.add_argument("--ntrufo-n", type=int, metavar="N", default=701,
|
|
328
|
+
help="ring degree for --kem NTRU_FO (default: 701)")
|
|
329
|
+
p.add_argument("--no-security-check", action="store_true",
|
|
330
|
+
help="skip the adversarial curious-receiver check")
|
|
331
|
+
p.add_argument("--quiet", action="store_true",
|
|
332
|
+
help="print nothing but the raw decrypted message bytes")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _add_bench_args(p: argparse.ArgumentParser) -> None:
|
|
336
|
+
p.add_argument("--kems", nargs="+", default=None,
|
|
337
|
+
help="mechanisms to benchmark (default: representative subset)")
|
|
338
|
+
p.add_argument("--all", action="store_true",
|
|
339
|
+
help="benchmark every supported enabled mechanism")
|
|
340
|
+
p.add_argument("--ns", nargs="+", type=int, default=DEFAULT_NS,
|
|
341
|
+
help=f"branch counts (default: {DEFAULT_NS})")
|
|
342
|
+
p.add_argument("--reps", type=int, default=5,
|
|
343
|
+
help="repetitions per cell, median reported (default: 5)")
|
|
344
|
+
p.add_argument("--no-custom", action="store_true",
|
|
345
|
+
help="skip the custom NTRU_FO and GGH_FO KEMs (on by default)")
|
|
346
|
+
p.add_argument("--ggh-dim", type=int, metavar="DIM", default=200,
|
|
347
|
+
help="dimension for the custom GGH_FO KEM (default: 200; "
|
|
348
|
+
"teaching-scale, needs the GGH_crypto package)")
|
|
349
|
+
p.add_argument("--ntrufo-n", type=int, metavar="N", default=701,
|
|
350
|
+
help="ring degree for the custom NTRU_FO KEM (default: 701)")
|
|
351
|
+
p.add_argument("--csv", default="ot_scaling.csv",
|
|
352
|
+
help="CSV output path (default: ot_scaling.csv)")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
356
|
+
ap = argparse.ArgumentParser(
|
|
357
|
+
prog="modularot",
|
|
358
|
+
description="Modular KEM-based 1-out-of-n Oblivious Transfer (semi-honest)")
|
|
359
|
+
sub = ap.add_subparsers(dest="command", required=True)
|
|
360
|
+
|
|
361
|
+
rp = sub.add_parser("run", help="run one 1-out-of-n OT transfer")
|
|
362
|
+
_add_run_args(rp)
|
|
363
|
+
rp.set_defaults(func=cmd_run)
|
|
364
|
+
|
|
365
|
+
bp = sub.add_parser("bench", help="scaling benchmark across KEMs and branch counts")
|
|
366
|
+
_add_bench_args(bp)
|
|
367
|
+
bp.set_defaults(func=cmd_bench)
|
|
368
|
+
|
|
369
|
+
lp = sub.add_parser("list-kems", help="list KEM names usable with run/bench")
|
|
370
|
+
lp.set_defaults(func=cmd_list_kems)
|
|
371
|
+
|
|
372
|
+
return ap
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def main() -> None:
|
|
376
|
+
args = build_parser().parse_args()
|
|
377
|
+
args.func(args)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
main()
|
|
File without changes
|
|
File without changes
|