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.
@@ -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.
@@ -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