charm-crypto-framework 0.61.1__cp313-cp313-macosx_10_13_universal2.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.
- charm/__init__.py +5 -0
- charm/adapters/__init__.py +0 -0
- charm/adapters/abenc_adapt_hybrid.py +90 -0
- charm/adapters/dabenc_adapt_hybrid.py +145 -0
- charm/adapters/ibenc_adapt_hybrid.py +72 -0
- charm/adapters/ibenc_adapt_identityhash.py +80 -0
- charm/adapters/kpabenc_adapt_hybrid.py +91 -0
- charm/adapters/pkenc_adapt_bchk05.py +121 -0
- charm/adapters/pkenc_adapt_chk04.py +91 -0
- charm/adapters/pkenc_adapt_hybrid.py +98 -0
- charm/adapters/pksig_adapt_naor01.py +89 -0
- charm/config.py +7 -0
- charm/core/__init__.py +0 -0
- charm/core/benchmark/benchmark_util.c +353 -0
- charm/core/benchmark/benchmark_util.h +61 -0
- charm/core/benchmark/benchmarkmodule.c +476 -0
- charm/core/benchmark/benchmarkmodule.h +162 -0
- charm/core/benchmark.cpython-313-darwin.so +0 -0
- charm/core/crypto/AES/AES.c +1464 -0
- charm/core/crypto/AES.cpython-313-darwin.so +0 -0
- charm/core/crypto/DES/DES.c +113 -0
- charm/core/crypto/DES.cpython-313-darwin.so +0 -0
- charm/core/crypto/DES3/DES3.c +26 -0
- charm/core/crypto/DES3.cpython-313-darwin.so +0 -0
- charm/core/crypto/__init__.py +0 -0
- charm/core/crypto/cryptobase/XOR.c +80 -0
- charm/core/crypto/cryptobase/_counter.c +496 -0
- charm/core/crypto/cryptobase/_counter.h +54 -0
- charm/core/crypto/cryptobase/block_template.c +900 -0
- charm/core/crypto/cryptobase/block_template.h +69 -0
- charm/core/crypto/cryptobase/cryptobasemodule.c +220 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt.h +90 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_argchk.h +44 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_cfg.h +186 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_cipher.h +941 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_custom.h +556 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_des.c +1912 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_hash.h +407 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_mac.h +496 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_macros.h +435 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_math.h +534 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_misc.h +103 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_pk.h +653 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_pkcs.h +90 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_prng.h +199 -0
- charm/core/crypto/cryptobase/stream_template.c +271 -0
- charm/core/crypto/cryptobase/strxor.c +229 -0
- charm/core/crypto/cryptobase.cpython-313-darwin.so +0 -0
- charm/core/engine/__init__.py +5 -0
- charm/core/engine/protocol.py +293 -0
- charm/core/engine/util.py +174 -0
- charm/core/math/__init__.py +0 -0
- charm/core/math/elliptic_curve/ecmodule.c +1986 -0
- charm/core/math/elliptic_curve/ecmodule.h +230 -0
- charm/core/math/elliptic_curve.cpython-313-darwin.so +0 -0
- charm/core/math/elliptic_curve.pyi +63 -0
- charm/core/math/integer/integermodule.c +2539 -0
- charm/core/math/integer/integermodule.h +145 -0
- charm/core/math/integer.cpython-313-darwin.so +0 -0
- charm/core/math/integer.pyi +76 -0
- charm/core/math/pairing/miracl/miracl_config.h +37 -0
- charm/core/math/pairing/miracl/miracl_interface.h +118 -0
- charm/core/math/pairing/miracl/miracl_interface2.h +126 -0
- charm/core/math/pairing/miracl/pairingmodule2.c +2094 -0
- charm/core/math/pairing/miracl/pairingmodule2.h +307 -0
- charm/core/math/pairing/pairingmodule.c +2230 -0
- charm/core/math/pairing/pairingmodule.h +241 -0
- charm/core/math/pairing/relic/pairingmodule3.c +1853 -0
- charm/core/math/pairing/relic/pairingmodule3.h +233 -0
- charm/core/math/pairing/relic/relic_interface.c +1337 -0
- charm/core/math/pairing/relic/relic_interface.h +217 -0
- charm/core/math/pairing/relic/test_relic.c +171 -0
- charm/core/math/pairing.cpython-313-darwin.so +0 -0
- charm/core/math/pairing.pyi +69 -0
- charm/core/utilities/base64.c +248 -0
- charm/core/utilities/base64.h +15 -0
- charm/schemes/__init__.py +0 -0
- charm/schemes/abenc/__init__.py +0 -0
- charm/schemes/abenc/abenc_accountability_jyjxgd20.py +647 -0
- charm/schemes/abenc/abenc_bsw07.py +146 -0
- charm/schemes/abenc/abenc_ca_cpabe_ar17.py +684 -0
- charm/schemes/abenc/abenc_dacmacs_yj14.py +298 -0
- charm/schemes/abenc/abenc_lsw08.py +159 -0
- charm/schemes/abenc/abenc_maabe_rw15.py +236 -0
- charm/schemes/abenc/abenc_maabe_yj14.py +297 -0
- charm/schemes/abenc/abenc_tbpre_lww14.py +309 -0
- charm/schemes/abenc/abenc_unmcpabe_yahk14.py +223 -0
- charm/schemes/abenc/abenc_waters09.py +144 -0
- charm/schemes/abenc/abenc_yct14.py +208 -0
- charm/schemes/abenc/abenc_yllc15.py +178 -0
- charm/schemes/abenc/ac17.py +248 -0
- charm/schemes/abenc/bsw07.py +141 -0
- charm/schemes/abenc/cgw15.py +277 -0
- charm/schemes/abenc/dabe_aw11.py +204 -0
- charm/schemes/abenc/dfa_fe12.py +144 -0
- charm/schemes/abenc/pk_hve08.py +179 -0
- charm/schemes/abenc/waters11.py +143 -0
- charm/schemes/aggrsign_MuSig.py +150 -0
- charm/schemes/aggrsign_bls.py +267 -0
- charm/schemes/blindsig_ps16.py +654 -0
- charm/schemes/chamhash_adm05.py +113 -0
- charm/schemes/chamhash_rsa_hw09.py +100 -0
- charm/schemes/commit/__init__.py +0 -0
- charm/schemes/commit/commit_gs08.py +77 -0
- charm/schemes/commit/commit_pedersen92.py +53 -0
- charm/schemes/encap_bchk05.py +62 -0
- charm/schemes/grpsig/__init__.py +0 -0
- charm/schemes/grpsig/groupsig_bgls04.py +114 -0
- charm/schemes/grpsig/groupsig_bgls04_var.py +115 -0
- charm/schemes/hibenc/__init__.py +0 -0
- charm/schemes/hibenc/hibenc_bb04.py +105 -0
- charm/schemes/hibenc/hibenc_lew11.py +193 -0
- charm/schemes/ibenc/__init__.py +0 -0
- charm/schemes/ibenc/clpkc_rp03.py +119 -0
- charm/schemes/ibenc/ibenc_CW13_z.py +168 -0
- charm/schemes/ibenc/ibenc_bb03.py +94 -0
- charm/schemes/ibenc/ibenc_bf01.py +121 -0
- charm/schemes/ibenc/ibenc_ckrs09.py +120 -0
- charm/schemes/ibenc/ibenc_cllww12_z.py +172 -0
- charm/schemes/ibenc/ibenc_lsw08.py +120 -0
- charm/schemes/ibenc/ibenc_sw05.py +238 -0
- charm/schemes/ibenc/ibenc_waters05.py +144 -0
- charm/schemes/ibenc/ibenc_waters05_z.py +164 -0
- charm/schemes/ibenc/ibenc_waters09.py +107 -0
- charm/schemes/ibenc/ibenc_waters09_z.py +147 -0
- charm/schemes/joye_scheme.py +106 -0
- charm/schemes/lem_scheme.py +207 -0
- charm/schemes/pk_fre_ccv11.py +107 -0
- charm/schemes/pk_vrf.py +127 -0
- charm/schemes/pkenc/__init__.py +0 -0
- charm/schemes/pkenc/pkenc_cs98.py +108 -0
- charm/schemes/pkenc/pkenc_elgamal85.py +122 -0
- charm/schemes/pkenc/pkenc_gm82.py +98 -0
- charm/schemes/pkenc/pkenc_paillier99.py +118 -0
- charm/schemes/pkenc/pkenc_rabin.py +254 -0
- charm/schemes/pkenc/pkenc_rsa.py +186 -0
- charm/schemes/pksig/__init__.py +0 -0
- charm/schemes/pksig/pksig_CW13_z.py +135 -0
- charm/schemes/pksig/pksig_bls04.py +87 -0
- charm/schemes/pksig/pksig_boyen.py +156 -0
- charm/schemes/pksig/pksig_chch.py +97 -0
- charm/schemes/pksig/pksig_chp.py +70 -0
- charm/schemes/pksig/pksig_cl03.py +150 -0
- charm/schemes/pksig/pksig_cl04.py +87 -0
- charm/schemes/pksig/pksig_cllww12_z.py +142 -0
- charm/schemes/pksig/pksig_cyh.py +132 -0
- charm/schemes/pksig/pksig_dsa.py +76 -0
- charm/schemes/pksig/pksig_ecdsa.py +71 -0
- charm/schemes/pksig/pksig_hess.py +104 -0
- charm/schemes/pksig/pksig_hw.py +110 -0
- charm/schemes/pksig/pksig_lamport.py +63 -0
- charm/schemes/pksig/pksig_ps01.py +135 -0
- charm/schemes/pksig/pksig_ps02.py +124 -0
- charm/schemes/pksig/pksig_ps03.py +119 -0
- charm/schemes/pksig/pksig_rsa_hw09.py +206 -0
- charm/schemes/pksig/pksig_schnorr91.py +77 -0
- charm/schemes/pksig/pksig_waters.py +115 -0
- charm/schemes/pksig/pksig_waters05.py +121 -0
- charm/schemes/pksig/pksig_waters09.py +121 -0
- charm/schemes/pre_mg07.py +150 -0
- charm/schemes/prenc/pre_afgh06.py +126 -0
- charm/schemes/prenc/pre_bbs98.py +123 -0
- charm/schemes/prenc/pre_nal16.py +216 -0
- charm/schemes/protocol_a01.py +272 -0
- charm/schemes/protocol_ao00.py +215 -0
- charm/schemes/protocol_cns07.py +274 -0
- charm/schemes/protocol_schnorr91.py +125 -0
- charm/schemes/sigma1.py +64 -0
- charm/schemes/sigma2.py +129 -0
- charm/schemes/sigma3.py +126 -0
- charm/schemes/threshold/__init__.py +59 -0
- charm/schemes/threshold/dkls23_dkg.py +556 -0
- charm/schemes/threshold/dkls23_presign.py +1089 -0
- charm/schemes/threshold/dkls23_sign.py +761 -0
- charm/schemes/threshold/xrpl_wallet.py +967 -0
- charm/test/__init__.py +0 -0
- charm/test/adapters/__init__.py +0 -0
- charm/test/adapters/abenc_adapt_hybrid_test.py +29 -0
- charm/test/adapters/dabenc_adapt_hybrid_test.py +56 -0
- charm/test/adapters/ibenc_adapt_hybrid_test.py +36 -0
- charm/test/adapters/ibenc_adapt_identityhash_test.py +32 -0
- charm/test/adapters/kpabenc_adapt_hybrid_test.py +30 -0
- charm/test/benchmark/abenc_yllc15_bench.py +92 -0
- charm/test/benchmark/benchmark_test.py +148 -0
- charm/test/benchmark_threshold.py +260 -0
- charm/test/conftest.py +38 -0
- charm/test/fuzz/__init__.py +1 -0
- charm/test/fuzz/conftest.py +5 -0
- charm/test/fuzz/fuzz_policy_parser.py +76 -0
- charm/test/fuzz/fuzz_serialization.py +83 -0
- charm/test/schemes/__init__.py +0 -0
- charm/test/schemes/abenc/__init__.py +0 -0
- charm/test/schemes/abenc/abenc_bsw07_test.py +39 -0
- charm/test/schemes/abenc/abenc_dacmacs_yj14_test.py +16 -0
- charm/test/schemes/abenc/abenc_lsw08_test.py +33 -0
- charm/test/schemes/abenc/abenc_maabe_yj14_test.py +16 -0
- charm/test/schemes/abenc/abenc_tbpre_lww14_test.py +16 -0
- charm/test/schemes/abenc/abenc_waters09_test.py +38 -0
- charm/test/schemes/abenc/abenc_yllc15_test.py +74 -0
- charm/test/schemes/chamhash_adm05_test.py +31 -0
- charm/test/schemes/chamhash_rsa_hw09_test.py +29 -0
- charm/test/schemes/commit/__init__.py +0 -0
- charm/test/schemes/commit/commit_gs08_test.py +24 -0
- charm/test/schemes/commit/commit_pedersen92_test.py +26 -0
- charm/test/schemes/dabe_aw11_test.py +45 -0
- charm/test/schemes/encap_bchk05_test.py +21 -0
- charm/test/schemes/grpsig/__init__.py +0 -0
- charm/test/schemes/grpsig/groupsig_bgls04_test.py +35 -0
- charm/test/schemes/grpsig/groupsig_bgls04_var_test.py +39 -0
- charm/test/schemes/hibenc/__init__.py +0 -0
- charm/test/schemes/hibenc/hibenc_bb04_test.py +28 -0
- charm/test/schemes/ibenc/__init__.py +0 -0
- charm/test/schemes/ibenc/ibenc_bb03_test.py +26 -0
- charm/test/schemes/ibenc/ibenc_bf01_test.py +24 -0
- charm/test/schemes/ibenc/ibenc_ckrs09_test.py +25 -0
- charm/test/schemes/ibenc/ibenc_lsw08_test.py +31 -0
- charm/test/schemes/ibenc/ibenc_sw05_test.py +32 -0
- charm/test/schemes/ibenc/ibenc_waters05_test.py +31 -0
- charm/test/schemes/ibenc/ibenc_waters09_test.py +27 -0
- charm/test/schemes/pk_vrf_test.py +29 -0
- charm/test/schemes/pkenc/__init__.py +0 -0
- charm/test/schemes/pkenc_test.py +255 -0
- charm/test/schemes/pksig/__init__.py +0 -0
- charm/test/schemes/pksig_test.py +376 -0
- charm/test/schemes/rsa_alg_test.py +340 -0
- charm/test/schemes/threshold_test.py +1792 -0
- charm/test/serialize/__init__.py +0 -0
- charm/test/serialize/serialize_test.py +40 -0
- charm/test/toolbox/__init__.py +0 -0
- charm/test/toolbox/conversion_test.py +30 -0
- charm/test/toolbox/ecgroup_test.py +53 -0
- charm/test/toolbox/integer_arithmetic_test.py +441 -0
- charm/test/toolbox/paddingschemes_test.py +238 -0
- charm/test/toolbox/policy_parser_stress_test.py +969 -0
- charm/test/toolbox/secretshare_test.py +28 -0
- charm/test/toolbox/symcrypto_test.py +108 -0
- charm/test/toolbox/test_policy_expression.py +16 -0
- charm/test/vectors/__init__.py +1 -0
- charm/test/vectors/test_bls_vectors.py +289 -0
- charm/test/vectors/test_pedersen_vectors.py +315 -0
- charm/test/vectors/test_schnorr_vectors.py +368 -0
- charm/test/zkp_compiler/__init__.py +9 -0
- charm/test/zkp_compiler/benchmark_zkp.py +258 -0
- charm/test/zkp_compiler/test_and_proof.py +240 -0
- charm/test/zkp_compiler/test_batch_verify.py +248 -0
- charm/test/zkp_compiler/test_dleq_proof.py +264 -0
- charm/test/zkp_compiler/test_or_proof.py +231 -0
- charm/test/zkp_compiler/test_proof_serialization.py +121 -0
- charm/test/zkp_compiler/test_range_proof.py +241 -0
- charm/test/zkp_compiler/test_representation_proof.py +325 -0
- charm/test/zkp_compiler/test_schnorr_proof.py +221 -0
- charm/test/zkp_compiler/test_thread_safety.py +169 -0
- charm/test/zkp_compiler/test_zkp_parser.py +139 -0
- charm/toolbox/ABEnc.py +26 -0
- charm/toolbox/ABEncMultiAuth.py +66 -0
- charm/toolbox/ABEnumeric.py +800 -0
- charm/toolbox/Commit.py +24 -0
- charm/toolbox/DFA.py +89 -0
- charm/toolbox/FSA.py +1254 -0
- charm/toolbox/Hash.py +39 -0
- charm/toolbox/IBEnc.py +62 -0
- charm/toolbox/IBSig.py +64 -0
- charm/toolbox/PKEnc.py +66 -0
- charm/toolbox/PKSig.py +56 -0
- charm/toolbox/PREnc.py +32 -0
- charm/toolbox/ZKProof.py +289 -0
- charm/toolbox/__init__.py +0 -0
- charm/toolbox/bitstring.py +49 -0
- charm/toolbox/broadcast.py +220 -0
- charm/toolbox/conversion.py +100 -0
- charm/toolbox/eccurve.py +149 -0
- charm/toolbox/ecgroup.py +143 -0
- charm/toolbox/enum.py +60 -0
- charm/toolbox/hash_module.py +91 -0
- charm/toolbox/integergroup.py +323 -0
- charm/toolbox/iterate.py +22 -0
- charm/toolbox/matrixops.py +76 -0
- charm/toolbox/mpc_utils.py +296 -0
- charm/toolbox/msp.py +175 -0
- charm/toolbox/mta.py +985 -0
- charm/toolbox/node.py +120 -0
- charm/toolbox/ot/__init__.py +22 -0
- charm/toolbox/ot/base_ot.py +374 -0
- charm/toolbox/ot/dpf.py +642 -0
- charm/toolbox/ot/mpfss.py +228 -0
- charm/toolbox/ot/ot_extension.py +589 -0
- charm/toolbox/ot/silent_ot.py +378 -0
- charm/toolbox/paddingschemes.py +423 -0
- charm/toolbox/paddingschemes_test.py +238 -0
- charm/toolbox/pairingcurves.py +85 -0
- charm/toolbox/pairinggroup.py +186 -0
- charm/toolbox/policy_expression_spec.py +70 -0
- charm/toolbox/policytree.py +189 -0
- charm/toolbox/reCompiler.py +346 -0
- charm/toolbox/redundancyschemes.py +65 -0
- charm/toolbox/schemebase.py +188 -0
- charm/toolbox/secretshare.py +104 -0
- charm/toolbox/secretutil.py +174 -0
- charm/toolbox/securerandom.py +73 -0
- charm/toolbox/sigmaprotocol.py +46 -0
- charm/toolbox/specialprimes.py +45 -0
- charm/toolbox/symcrypto.py +279 -0
- charm/toolbox/threshold_sharing.py +553 -0
- charm/toolbox/xmlserialize.py +94 -0
- charm/toolbox/zknode.py +105 -0
- charm/zkp_compiler/__init__.py +89 -0
- charm/zkp_compiler/and_proof.py +460 -0
- charm/zkp_compiler/batch_verify.py +324 -0
- charm/zkp_compiler/dleq_proof.py +423 -0
- charm/zkp_compiler/or_proof.py +305 -0
- charm/zkp_compiler/range_proof.py +417 -0
- charm/zkp_compiler/representation_proof.py +466 -0
- charm/zkp_compiler/schnorr_proof.py +273 -0
- charm/zkp_compiler/thread_safe.py +150 -0
- charm/zkp_compiler/zk_demo.py +489 -0
- charm/zkp_compiler/zkp_factory.py +330 -0
- charm/zkp_compiler/zkp_generator.py +370 -0
- charm/zkp_compiler/zkparser.py +269 -0
- charm_crypto_framework-0.61.1.dist-info/METADATA +337 -0
- charm_crypto_framework-0.61.1.dist-info/RECORD +323 -0
- charm_crypto_framework-0.61.1.dist-info/WHEEL +5 -0
- charm_crypto_framework-0.61.1.dist-info/licenses/LICENSE.txt +165 -0
- charm_crypto_framework-0.61.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1792 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for DKLS23 Threshold ECDSA implementation.
|
|
3
|
+
|
|
4
|
+
Run with: pytest charm/test/schemes/threshold_test.py -v
|
|
5
|
+
|
|
6
|
+
This module tests:
|
|
7
|
+
- SimpleOT: Base Oblivious Transfer protocol
|
|
8
|
+
- OTExtension: IKNP-style OT extension
|
|
9
|
+
- MtA/MtAwc: Multiplicative-to-Additive conversion
|
|
10
|
+
- ThresholdSharing/PedersenVSS: Threshold secret sharing
|
|
11
|
+
- DKLS23_DKG: Distributed Key Generation
|
|
12
|
+
- DKLS23_Presign: Presigning protocol
|
|
13
|
+
- DKLS23_Sign: Signing protocol
|
|
14
|
+
- DKLS23: Complete threshold ECDSA protocol
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import unittest
|
|
18
|
+
try:
|
|
19
|
+
import pytest
|
|
20
|
+
except ImportError:
|
|
21
|
+
pytest = None
|
|
22
|
+
from charm.toolbox.ecgroup import ECGroup, ZR, G
|
|
23
|
+
from charm.toolbox.eccurve import secp256k1
|
|
24
|
+
|
|
25
|
+
# Import OT components
|
|
26
|
+
from charm.toolbox.ot.base_ot import SimpleOT
|
|
27
|
+
from charm.toolbox.ot.ot_extension import OTExtension, get_bit
|
|
28
|
+
from charm.toolbox.ot.dpf import DPF
|
|
29
|
+
from charm.toolbox.ot.mpfss import MPFSS
|
|
30
|
+
from charm.toolbox.ot.silent_ot import SilentOT
|
|
31
|
+
|
|
32
|
+
# Import MtA
|
|
33
|
+
from charm.toolbox.mta import MtA, MtAwc
|
|
34
|
+
|
|
35
|
+
# Import threshold sharing
|
|
36
|
+
from charm.toolbox.threshold_sharing import ThresholdSharing, PedersenVSS
|
|
37
|
+
|
|
38
|
+
# Import DKLS23 protocol components
|
|
39
|
+
from charm.schemes.threshold.dkls23_dkg import DKLS23_DKG, KeyShare
|
|
40
|
+
from charm.schemes.threshold.dkls23_presign import DKLS23_Presign, Presignature
|
|
41
|
+
from charm.schemes.threshold.dkls23_sign import DKLS23_Sign, DKLS23, ThresholdSignature
|
|
42
|
+
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
debug = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestSimpleOT(unittest.TestCase):
|
|
49
|
+
"""Tests for base Oblivious Transfer (Chou-Orlandi style)"""
|
|
50
|
+
|
|
51
|
+
def setUp(self):
|
|
52
|
+
self.group = ECGroup(secp256k1)
|
|
53
|
+
|
|
54
|
+
def test_ot_choice_zero(self):
|
|
55
|
+
"""Test OT with choice bit 0 - receiver should learn m0"""
|
|
56
|
+
sender = SimpleOT(self.group)
|
|
57
|
+
receiver = SimpleOT(self.group)
|
|
58
|
+
|
|
59
|
+
# Sender setup
|
|
60
|
+
sender_params = sender.sender_setup()
|
|
61
|
+
|
|
62
|
+
# Receiver chooses bit 0
|
|
63
|
+
receiver_response, receiver_state = receiver.receiver_choose(sender_params, 0)
|
|
64
|
+
|
|
65
|
+
# Sender transfers messages (must be 16 bytes for block cipher)
|
|
66
|
+
m0 = b'message zero!!!!' # 16 bytes
|
|
67
|
+
m1 = b'message one!!!!!' # 16 bytes
|
|
68
|
+
ciphertexts = sender.sender_transfer(receiver_response, m0, m1)
|
|
69
|
+
|
|
70
|
+
# Receiver retrieves chosen message
|
|
71
|
+
result = receiver.receiver_retrieve(ciphertexts, receiver_state)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(result, m0, "Receiver should get m0 when choice=0")
|
|
74
|
+
|
|
75
|
+
def test_ot_choice_one(self):
|
|
76
|
+
"""Test OT with choice bit 1 - receiver should learn m1"""
|
|
77
|
+
sender = SimpleOT(self.group)
|
|
78
|
+
receiver = SimpleOT(self.group)
|
|
79
|
+
|
|
80
|
+
# Sender setup
|
|
81
|
+
sender_params = sender.sender_setup()
|
|
82
|
+
|
|
83
|
+
# Receiver chooses bit 1
|
|
84
|
+
receiver_response, receiver_state = receiver.receiver_choose(sender_params, 1)
|
|
85
|
+
|
|
86
|
+
# Sender transfers messages
|
|
87
|
+
m0 = b'message zero!!!!'
|
|
88
|
+
m1 = b'message one!!!!!'
|
|
89
|
+
ciphertexts = sender.sender_transfer(receiver_response, m0, m1)
|
|
90
|
+
|
|
91
|
+
# Receiver retrieves chosen message
|
|
92
|
+
result = receiver.receiver_retrieve(ciphertexts, receiver_state)
|
|
93
|
+
|
|
94
|
+
self.assertEqual(result, m1, "Receiver should get m1 when choice=1")
|
|
95
|
+
|
|
96
|
+
def test_ot_multiple_transfers(self):
|
|
97
|
+
"""Test multiple independent OT instances"""
|
|
98
|
+
for choice in [0, 1]:
|
|
99
|
+
sender = SimpleOT(self.group)
|
|
100
|
+
receiver = SimpleOT(self.group)
|
|
101
|
+
|
|
102
|
+
sender_params = sender.sender_setup()
|
|
103
|
+
receiver_response, receiver_state = receiver.receiver_choose(sender_params, choice)
|
|
104
|
+
|
|
105
|
+
m0, m1 = b'zero message !!!', b'one message !!! '
|
|
106
|
+
ciphertexts = sender.sender_transfer(receiver_response, m0, m1)
|
|
107
|
+
result = receiver.receiver_retrieve(ciphertexts, receiver_state)
|
|
108
|
+
|
|
109
|
+
expected = m0 if choice == 0 else m1
|
|
110
|
+
self.assertEqual(result, expected)
|
|
111
|
+
|
|
112
|
+
def test_ot_invalid_point_rejected(self):
|
|
113
|
+
"""Test that invalid points from malicious sender are rejected"""
|
|
114
|
+
sender = SimpleOT(self.group)
|
|
115
|
+
receiver = SimpleOT(self.group)
|
|
116
|
+
|
|
117
|
+
# Get valid sender params first
|
|
118
|
+
sender_params = sender.sender_setup()
|
|
119
|
+
|
|
120
|
+
# Create identity element (point at infinity) - should be rejected
|
|
121
|
+
# The identity element is obtained by multiplying any point by 0
|
|
122
|
+
zero = self.group.init(ZR, 0)
|
|
123
|
+
valid_point = self.group.random(G)
|
|
124
|
+
identity = valid_point ** zero
|
|
125
|
+
|
|
126
|
+
# Test with identity as A (sender public key)
|
|
127
|
+
invalid_params_A = {'A': identity, 'g': sender_params['g']}
|
|
128
|
+
with self.assertRaises(ValueError) as ctx:
|
|
129
|
+
receiver.receiver_choose(invalid_params_A, 0)
|
|
130
|
+
self.assertIn("infinity", str(ctx.exception).lower())
|
|
131
|
+
|
|
132
|
+
# Test with identity as g (generator)
|
|
133
|
+
invalid_params_g = {'A': sender_params['A'], 'g': identity}
|
|
134
|
+
with self.assertRaises(ValueError) as ctx:
|
|
135
|
+
receiver.receiver_choose(invalid_params_g, 0)
|
|
136
|
+
self.assertIn("infinity", str(ctx.exception).lower())
|
|
137
|
+
|
|
138
|
+
def test_ot_reset_sender(self):
|
|
139
|
+
"""Test that reset_sender clears sender state"""
|
|
140
|
+
sender = SimpleOT(self.group)
|
|
141
|
+
|
|
142
|
+
# Setup sender
|
|
143
|
+
sender.sender_setup()
|
|
144
|
+
self.assertIsNotNone(sender._a)
|
|
145
|
+
self.assertIsNotNone(sender._A)
|
|
146
|
+
self.assertIsNotNone(sender._g)
|
|
147
|
+
|
|
148
|
+
# Reset sender
|
|
149
|
+
sender.reset_sender()
|
|
150
|
+
self.assertIsNone(sender._a)
|
|
151
|
+
self.assertIsNone(sender._A)
|
|
152
|
+
self.assertIsNone(sender._g)
|
|
153
|
+
|
|
154
|
+
# Setup again should work
|
|
155
|
+
sender_params = sender.sender_setup()
|
|
156
|
+
self.assertIsNotNone(sender._a)
|
|
157
|
+
self.assertIn('A', sender_params)
|
|
158
|
+
self.assertIn('g', sender_params)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestOTExtension(unittest.TestCase):
|
|
162
|
+
"""Tests for IKNP-style OT Extension"""
|
|
163
|
+
|
|
164
|
+
def setUp(self):
|
|
165
|
+
self.group = ECGroup(secp256k1)
|
|
166
|
+
|
|
167
|
+
def _run_base_ot_setup(self, sender_ext, receiver_ext):
|
|
168
|
+
"""Helper to run the base OT setup phase between sender and receiver."""
|
|
169
|
+
# Sender prepares for base OT (generates s and prepares to receive seeds)
|
|
170
|
+
sender_ext.sender_setup_base_ots()
|
|
171
|
+
|
|
172
|
+
# Receiver sets up base OTs (generates seed pairs, acts as OT sender)
|
|
173
|
+
base_ot_setups = receiver_ext.receiver_setup_base_ots()
|
|
174
|
+
|
|
175
|
+
# Sender responds to base OTs (acts as OT receiver, choosing based on s)
|
|
176
|
+
sender_responses = sender_ext.sender_respond_base_ots(base_ot_setups)
|
|
177
|
+
|
|
178
|
+
# Receiver transfers seeds via base OT
|
|
179
|
+
seed_ciphertexts = receiver_ext.receiver_transfer_seeds(sender_responses)
|
|
180
|
+
|
|
181
|
+
# Sender receives the seeds
|
|
182
|
+
sender_ext.sender_receive_seeds(seed_ciphertexts)
|
|
183
|
+
|
|
184
|
+
def test_ot_extension_basic(self):
|
|
185
|
+
"""Test OT extension with 256 OTs"""
|
|
186
|
+
sender_ext = OTExtension(self.group, security_param=128)
|
|
187
|
+
receiver_ext = OTExtension(self.group, security_param=128)
|
|
188
|
+
|
|
189
|
+
# Run base OT setup phase
|
|
190
|
+
self._run_base_ot_setup(sender_ext, receiver_ext)
|
|
191
|
+
|
|
192
|
+
num_ots = 256
|
|
193
|
+
# All zeros choice bits
|
|
194
|
+
choice_bits = bytes([0x00] * (num_ots // 8))
|
|
195
|
+
|
|
196
|
+
# Generate random message pairs
|
|
197
|
+
messages = [(os.urandom(32), os.urandom(32)) for _ in range(num_ots)]
|
|
198
|
+
|
|
199
|
+
# Run extension protocol
|
|
200
|
+
sender_ext.sender_init()
|
|
201
|
+
receiver_msg, receiver_state = receiver_ext.receiver_extend(num_ots, choice_bits)
|
|
202
|
+
sender_ciphertexts = sender_ext.sender_extend(num_ots, messages, receiver_msg)
|
|
203
|
+
results = receiver_ext.receiver_output(sender_ciphertexts, receiver_state)
|
|
204
|
+
|
|
205
|
+
# Verify receiver got m0 for all (since choice bits are all 0)
|
|
206
|
+
for i in range(num_ots):
|
|
207
|
+
self.assertEqual(results[i], messages[i][0], f"OT {i} failed with choice=0")
|
|
208
|
+
|
|
209
|
+
def test_ot_extension_alternating_bits(self):
|
|
210
|
+
"""Test OT extension with alternating choice bits"""
|
|
211
|
+
sender_ext = OTExtension(self.group, security_param=128)
|
|
212
|
+
receiver_ext = OTExtension(self.group, security_param=128)
|
|
213
|
+
|
|
214
|
+
# Run base OT setup phase
|
|
215
|
+
self._run_base_ot_setup(sender_ext, receiver_ext)
|
|
216
|
+
|
|
217
|
+
num_ots = 256
|
|
218
|
+
# Alternating choice bits: 10101010...
|
|
219
|
+
choice_bits = bytes([0b10101010] * (num_ots // 8))
|
|
220
|
+
|
|
221
|
+
messages = [(os.urandom(32), os.urandom(32)) for _ in range(num_ots)]
|
|
222
|
+
|
|
223
|
+
# Run extension protocol
|
|
224
|
+
sender_ext.sender_init()
|
|
225
|
+
receiver_msg, receiver_state = receiver_ext.receiver_extend(num_ots, choice_bits)
|
|
226
|
+
sender_ciphertexts = sender_ext.sender_extend(num_ots, messages, receiver_msg)
|
|
227
|
+
results = receiver_ext.receiver_output(sender_ciphertexts, receiver_state)
|
|
228
|
+
|
|
229
|
+
# Verify receiver got correct messages based on choice bits
|
|
230
|
+
for i in range(num_ots):
|
|
231
|
+
bit = get_bit(choice_bits, i)
|
|
232
|
+
expected = messages[i][bit]
|
|
233
|
+
self.assertEqual(results[i], expected, f"OT {i} failed with choice bit={bit}")
|
|
234
|
+
|
|
235
|
+
def test_base_ot_required_for_sender_init(self):
|
|
236
|
+
"""Verify sender_init fails if base OT not completed."""
|
|
237
|
+
sender_ext = OTExtension(self.group, security_param=128)
|
|
238
|
+
|
|
239
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
240
|
+
sender_ext.sender_init()
|
|
241
|
+
self.assertIn("Base OT setup must be completed", str(ctx.exception))
|
|
242
|
+
|
|
243
|
+
def test_base_ot_required_for_receiver_extend(self):
|
|
244
|
+
"""Verify receiver_extend fails if base OT not completed."""
|
|
245
|
+
receiver_ext = OTExtension(self.group, security_param=128)
|
|
246
|
+
|
|
247
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
248
|
+
receiver_ext.receiver_extend(256, bytes([0x00] * 32))
|
|
249
|
+
self.assertIn("Base OT setup must be completed", str(ctx.exception))
|
|
250
|
+
|
|
251
|
+
def test_sender_s_not_exposed(self):
|
|
252
|
+
"""Verify receiver cannot access sender's random bits."""
|
|
253
|
+
sender_ext = OTExtension(self.group, security_param=128)
|
|
254
|
+
receiver_ext = OTExtension(self.group, security_param=128)
|
|
255
|
+
|
|
256
|
+
# Run base OT setup
|
|
257
|
+
self._run_base_ot_setup(sender_ext, receiver_ext)
|
|
258
|
+
|
|
259
|
+
# Verify receiver has NO access to sender's s
|
|
260
|
+
self.assertIsNone(receiver_ext._sender_random_bits)
|
|
261
|
+
|
|
262
|
+
# Receiver only knows seed pairs, not which one sender received
|
|
263
|
+
self.assertIsNotNone(receiver_ext._receiver_seed_pairs)
|
|
264
|
+
self.assertEqual(len(receiver_ext._receiver_seed_pairs), 128)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestMtA(unittest.TestCase):
|
|
268
|
+
"""Tests for Multiplicative-to-Additive conversion"""
|
|
269
|
+
|
|
270
|
+
def setUp(self):
|
|
271
|
+
self.group = ECGroup(secp256k1)
|
|
272
|
+
|
|
273
|
+
def test_mta_correctness(self):
|
|
274
|
+
"""Test that a*b = alpha + beta (mod q) - multiplicative to additive with real OT"""
|
|
275
|
+
alice_mta = MtA(self.group)
|
|
276
|
+
bob_mta = MtA(self.group)
|
|
277
|
+
|
|
278
|
+
# Alice has share a, Bob has share b
|
|
279
|
+
a = self.group.random(ZR)
|
|
280
|
+
b = self.group.random(ZR)
|
|
281
|
+
|
|
282
|
+
# Run MtA protocol with real SimpleOT
|
|
283
|
+
# Round 1: Sender setup
|
|
284
|
+
sender_msg = alice_mta.sender_round1(a)
|
|
285
|
+
|
|
286
|
+
# Round 1: Receiver chooses based on bits of b
|
|
287
|
+
receiver_msg, _ = bob_mta.receiver_round1(b, sender_msg)
|
|
288
|
+
|
|
289
|
+
# Round 2: Sender transfers encrypted OT messages
|
|
290
|
+
alpha, ot_data = alice_mta.sender_round2(receiver_msg)
|
|
291
|
+
|
|
292
|
+
# Round 2: Receiver retrieves selected messages and computes beta
|
|
293
|
+
beta = bob_mta.receiver_round2(ot_data)
|
|
294
|
+
|
|
295
|
+
# Verify: a*b = alpha + beta (mod q)
|
|
296
|
+
product = a * b
|
|
297
|
+
additive_sum = alpha + beta
|
|
298
|
+
|
|
299
|
+
self.assertEqual(product, additive_sum, "MtA correctness: a*b should equal alpha + beta")
|
|
300
|
+
|
|
301
|
+
def test_mta_multiple_invocations(self):
|
|
302
|
+
"""Test MtA with multiple random values"""
|
|
303
|
+
for _ in range(3): # Run a few times
|
|
304
|
+
alice_mta = MtA(self.group)
|
|
305
|
+
bob_mta = MtA(self.group)
|
|
306
|
+
|
|
307
|
+
a = self.group.random(ZR)
|
|
308
|
+
b = self.group.random(ZR)
|
|
309
|
+
|
|
310
|
+
sender_msg = alice_mta.sender_round1(a)
|
|
311
|
+
receiver_msg, _ = bob_mta.receiver_round1(b, sender_msg)
|
|
312
|
+
alpha, ot_data = alice_mta.sender_round2(receiver_msg)
|
|
313
|
+
beta = bob_mta.receiver_round2(ot_data)
|
|
314
|
+
|
|
315
|
+
self.assertEqual(a * b, alpha + beta)
|
|
316
|
+
|
|
317
|
+
def test_mta_uses_real_ot(self):
|
|
318
|
+
"""Test that MtA uses real OT - receiver never sees both messages"""
|
|
319
|
+
alice_mta = MtA(self.group)
|
|
320
|
+
bob_mta = MtA(self.group)
|
|
321
|
+
|
|
322
|
+
a = self.group.random(ZR)
|
|
323
|
+
b = self.group.random(ZR)
|
|
324
|
+
|
|
325
|
+
sender_msg = alice_mta.sender_round1(a)
|
|
326
|
+
|
|
327
|
+
# Verify sender_msg contains OT params, not raw messages
|
|
328
|
+
self.assertIn('ot_params', sender_msg, "Sender should provide OT params")
|
|
329
|
+
self.assertNotIn('ot_messages', sender_msg, "Sender should NOT expose raw OT messages")
|
|
330
|
+
|
|
331
|
+
# The OT params should contain encrypted setup, not raw m0/m1 tuples
|
|
332
|
+
for params in sender_msg['ot_params']:
|
|
333
|
+
self.assertIn('A', params, "OT params should have public key A")
|
|
334
|
+
self.assertIn('g', params, "OT params should have generator g")
|
|
335
|
+
# Should NOT have m0, m1 directly visible
|
|
336
|
+
self.assertNotIn('m0', params)
|
|
337
|
+
self.assertNotIn('m1', params)
|
|
338
|
+
|
|
339
|
+
receiver_msg, _ = bob_mta.receiver_round1(b, sender_msg)
|
|
340
|
+
alpha, ot_data = alice_mta.sender_round2(receiver_msg)
|
|
341
|
+
beta = bob_mta.receiver_round2(ot_data)
|
|
342
|
+
|
|
343
|
+
# Still verify correctness
|
|
344
|
+
self.assertEqual(a * b, alpha + beta)
|
|
345
|
+
|
|
346
|
+
def test_mta_edge_case_near_order(self):
|
|
347
|
+
"""Test MtA with values close to the curve order (MEDIUM-04)."""
|
|
348
|
+
alice_mta = MtA(self.group)
|
|
349
|
+
bob_mta = MtA(self.group)
|
|
350
|
+
|
|
351
|
+
# Test with value = order - 1
|
|
352
|
+
order = int(self.group.order())
|
|
353
|
+
a = self.group.init(ZR, order - 1)
|
|
354
|
+
b = self.group.init(ZR, 2)
|
|
355
|
+
|
|
356
|
+
# Run MtA protocol with real SimpleOT
|
|
357
|
+
sender_msg = alice_mta.sender_round1(a)
|
|
358
|
+
receiver_msg, _ = bob_mta.receiver_round1(b, sender_msg)
|
|
359
|
+
alpha, ot_data = alice_mta.sender_round2(receiver_msg)
|
|
360
|
+
beta = bob_mta.receiver_round2(ot_data)
|
|
361
|
+
|
|
362
|
+
# Verify: a*b = alpha + beta (mod q)
|
|
363
|
+
product = a * b
|
|
364
|
+
additive_sum = alpha + beta
|
|
365
|
+
|
|
366
|
+
self.assertEqual(product, additive_sum,
|
|
367
|
+
"MtA correctness: a*b should equal alpha + beta even for values near order")
|
|
368
|
+
|
|
369
|
+
# Test with value = order - 2
|
|
370
|
+
alice_mta2 = MtA(self.group)
|
|
371
|
+
bob_mta2 = MtA(self.group)
|
|
372
|
+
|
|
373
|
+
a2 = self.group.init(ZR, order - 2)
|
|
374
|
+
b2 = self.group.init(ZR, 3)
|
|
375
|
+
|
|
376
|
+
sender_msg2 = alice_mta2.sender_round1(a2)
|
|
377
|
+
receiver_msg2, _ = bob_mta2.receiver_round1(b2, sender_msg2)
|
|
378
|
+
alpha2, ot_data2 = alice_mta2.sender_round2(receiver_msg2)
|
|
379
|
+
beta2 = bob_mta2.receiver_round2(ot_data2)
|
|
380
|
+
|
|
381
|
+
product2 = a2 * b2
|
|
382
|
+
additive_sum2 = alpha2 + beta2
|
|
383
|
+
|
|
384
|
+
self.assertEqual(product2, additive_sum2,
|
|
385
|
+
"MtA correctness: should work for values close to order boundary")
|
|
386
|
+
|
|
387
|
+
def test_mta_return_types(self):
|
|
388
|
+
"""Test MtA methods have documented return types (LOW-03)."""
|
|
389
|
+
alice_mta = MtA(self.group)
|
|
390
|
+
bob_mta = MtA(self.group)
|
|
391
|
+
|
|
392
|
+
a = self.group.random(ZR)
|
|
393
|
+
b = self.group.random(ZR)
|
|
394
|
+
|
|
395
|
+
# sender_round1 returns dict
|
|
396
|
+
sender_msg = alice_mta.sender_round1(a)
|
|
397
|
+
self.assertIsInstance(sender_msg, dict)
|
|
398
|
+
self.assertIn('ot_params', sender_msg)
|
|
399
|
+
self.assertIn('adjustment', sender_msg)
|
|
400
|
+
|
|
401
|
+
# receiver_round1 returns tuple (dict, None)
|
|
402
|
+
result = bob_mta.receiver_round1(b, sender_msg)
|
|
403
|
+
self.assertIsInstance(result, tuple)
|
|
404
|
+
self.assertEqual(len(result), 2)
|
|
405
|
+
receiver_msg, beta_placeholder = result
|
|
406
|
+
self.assertIsInstance(receiver_msg, dict)
|
|
407
|
+
self.assertIn('ot_responses', receiver_msg)
|
|
408
|
+
self.assertIsNone(beta_placeholder)
|
|
409
|
+
|
|
410
|
+
# sender_round2 returns tuple (ZR element, dict)
|
|
411
|
+
result2 = alice_mta.sender_round2(receiver_msg)
|
|
412
|
+
self.assertIsInstance(result2, tuple)
|
|
413
|
+
self.assertEqual(len(result2), 2)
|
|
414
|
+
alpha, ot_data = result2
|
|
415
|
+
self.assertIsInstance(ot_data, dict)
|
|
416
|
+
self.assertIn('ot_ciphertexts', ot_data)
|
|
417
|
+
|
|
418
|
+
# receiver_round2 returns ZR element
|
|
419
|
+
beta = bob_mta.receiver_round2(ot_data)
|
|
420
|
+
# Verify beta is a field element by checking it works in arithmetic
|
|
421
|
+
self.assertEqual(a * b, alpha + beta)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class TestMtAwc(unittest.TestCase):
|
|
425
|
+
"""Tests for MtA with correctness check"""
|
|
426
|
+
|
|
427
|
+
def setUp(self):
|
|
428
|
+
self.group = ECGroup(secp256k1)
|
|
429
|
+
|
|
430
|
+
def test_mtawc_correctness(self):
|
|
431
|
+
"""Test MtA with correctness check produces valid shares"""
|
|
432
|
+
mta_wc = MtAwc(self.group)
|
|
433
|
+
|
|
434
|
+
a = self.group.random(ZR)
|
|
435
|
+
b = self.group.random(ZR)
|
|
436
|
+
|
|
437
|
+
# Run MtAwc protocol
|
|
438
|
+
sender_commit = mta_wc.sender_commit(a)
|
|
439
|
+
receiver_commit = mta_wc.receiver_commit(b)
|
|
440
|
+
|
|
441
|
+
sender_msg = mta_wc.sender_round1(a, receiver_commit)
|
|
442
|
+
receiver_msg, _ = mta_wc.receiver_round1(b, sender_commit, sender_msg)
|
|
443
|
+
alpha, sender_proof = mta_wc.sender_round2(receiver_msg)
|
|
444
|
+
beta, valid = mta_wc.receiver_verify(sender_proof)
|
|
445
|
+
|
|
446
|
+
# Verify proof was valid
|
|
447
|
+
self.assertTrue(valid, "MtAwc proof should be valid")
|
|
448
|
+
|
|
449
|
+
# Verify correctness: a*b = alpha + beta
|
|
450
|
+
self.assertEqual(a * b, alpha + beta, "MtAwc: a*b should equal alpha + beta")
|
|
451
|
+
|
|
452
|
+
def test_mtawc_proof_does_not_reveal_sender_bits(self):
|
|
453
|
+
"""Test that MtAwc proof does NOT contain sender_bits (CRITICAL-02 fix)"""
|
|
454
|
+
mta_wc = MtAwc(self.group)
|
|
455
|
+
|
|
456
|
+
a = self.group.random(ZR)
|
|
457
|
+
b = self.group.random(ZR)
|
|
458
|
+
|
|
459
|
+
# Run MtAwc protocol
|
|
460
|
+
sender_commit = mta_wc.sender_commit(a)
|
|
461
|
+
receiver_commit = mta_wc.receiver_commit(b)
|
|
462
|
+
|
|
463
|
+
sender_msg = mta_wc.sender_round1(a, receiver_commit)
|
|
464
|
+
receiver_msg, _ = mta_wc.receiver_round1(b, sender_commit, sender_msg)
|
|
465
|
+
alpha, sender_proof = mta_wc.sender_round2(receiver_msg)
|
|
466
|
+
|
|
467
|
+
# CRITICAL: Verify that proof does NOT contain sender_bits
|
|
468
|
+
self.assertNotIn('sender_bits', sender_proof,
|
|
469
|
+
"SECURITY: Proof must NOT contain sender_bits - this would reveal sender's secret!")
|
|
470
|
+
|
|
471
|
+
# Verify the proof structure uses commitment-based verification instead
|
|
472
|
+
self.assertIn('challenge', sender_proof, "Proof should use challenge-response")
|
|
473
|
+
self.assertIn('response', sender_proof, "Proof should contain response")
|
|
474
|
+
self.assertIn('commitment_randomness', sender_proof, "Proof should contain commitment randomness")
|
|
475
|
+
|
|
476
|
+
# Still verify correctness works
|
|
477
|
+
beta, valid = mta_wc.receiver_verify(sender_proof)
|
|
478
|
+
self.assertTrue(valid, "MtAwc proof should still be valid")
|
|
479
|
+
self.assertEqual(a * b, alpha + beta, "MtAwc: a*b should equal alpha + beta")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class TestThresholdSharing(unittest.TestCase):
|
|
483
|
+
"""Tests for threshold secret sharing (Shamir-style)"""
|
|
484
|
+
|
|
485
|
+
def setUp(self):
|
|
486
|
+
self.group = ECGroup(secp256k1)
|
|
487
|
+
self.ts = ThresholdSharing(self.group)
|
|
488
|
+
|
|
489
|
+
def test_basic_sharing_and_reconstruction(self):
|
|
490
|
+
"""Test basic 2-of-3 secret sharing and reconstruction"""
|
|
491
|
+
secret = self.group.random(ZR)
|
|
492
|
+
shares = self.ts.share(secret, threshold=2, num_parties=3)
|
|
493
|
+
|
|
494
|
+
self.assertEqual(len(shares), 3, "Should have 3 shares")
|
|
495
|
+
|
|
496
|
+
# Reconstruct from any 2 shares
|
|
497
|
+
recovered = self.ts.reconstruct({1: shares[1], 2: shares[2]}, threshold=2)
|
|
498
|
+
self.assertEqual(secret, recovered, "Should reconstruct original secret")
|
|
499
|
+
|
|
500
|
+
# Reconstruct from different pair
|
|
501
|
+
recovered2 = self.ts.reconstruct({1: shares[1], 3: shares[3]}, threshold=2)
|
|
502
|
+
self.assertEqual(secret, recovered2, "Should reconstruct from different pair")
|
|
503
|
+
|
|
504
|
+
recovered3 = self.ts.reconstruct({2: shares[2], 3: shares[3]}, threshold=2)
|
|
505
|
+
self.assertEqual(secret, recovered3, "Should reconstruct from any pair")
|
|
506
|
+
|
|
507
|
+
def test_feldman_vss_verification(self):
|
|
508
|
+
"""Test Feldman VSS verification - shares should verify against commitments"""
|
|
509
|
+
secret = self.group.random(ZR)
|
|
510
|
+
g = self.group.random(G)
|
|
511
|
+
|
|
512
|
+
shares, commitments = self.ts.share_with_verification(secret, g, threshold=2, num_parties=3)
|
|
513
|
+
|
|
514
|
+
# All shares should verify
|
|
515
|
+
for party_id in [1, 2, 3]:
|
|
516
|
+
valid = self.ts.verify_share(party_id, shares[party_id], commitments, g)
|
|
517
|
+
self.assertTrue(valid, f"Share {party_id} should verify")
|
|
518
|
+
|
|
519
|
+
def test_feldman_vss_detects_invalid_share(self):
|
|
520
|
+
"""Test that Feldman VSS detects tampered shares"""
|
|
521
|
+
secret = self.group.random(ZR)
|
|
522
|
+
g = self.group.random(G)
|
|
523
|
+
|
|
524
|
+
shares, commitments = self.ts.share_with_verification(secret, g, threshold=2, num_parties=3)
|
|
525
|
+
|
|
526
|
+
# Tamper with a share
|
|
527
|
+
tampered_share = shares[1] + self.group.random(ZR)
|
|
528
|
+
|
|
529
|
+
# Tampered share should not verify
|
|
530
|
+
valid = self.ts.verify_share(1, tampered_share, commitments, g)
|
|
531
|
+
self.assertFalse(valid, "Tampered share should not verify")
|
|
532
|
+
|
|
533
|
+
def test_threshold_3_of_5(self):
|
|
534
|
+
"""Test 3-of-5 threshold scheme"""
|
|
535
|
+
secret = self.group.random(ZR)
|
|
536
|
+
shares = self.ts.share(secret, threshold=3, num_parties=5)
|
|
537
|
+
|
|
538
|
+
self.assertEqual(len(shares), 5)
|
|
539
|
+
|
|
540
|
+
# Reconstruct from 3 shares
|
|
541
|
+
recovered = self.ts.reconstruct({1: shares[1], 3: shares[3], 5: shares[5]}, threshold=3)
|
|
542
|
+
self.assertEqual(secret, recovered)
|
|
543
|
+
|
|
544
|
+
def test_insufficient_shares_raises_error(self):
|
|
545
|
+
"""Test that reconstruction fails with insufficient shares"""
|
|
546
|
+
secret = self.group.random(ZR)
|
|
547
|
+
shares = self.ts.share(secret, threshold=3, num_parties=5)
|
|
548
|
+
|
|
549
|
+
# Try to reconstruct with only 2 shares (need 3)
|
|
550
|
+
with self.assertRaises(ValueError):
|
|
551
|
+
self.ts.reconstruct({1: shares[1], 2: shares[2]}, threshold=3)
|
|
552
|
+
|
|
553
|
+
def test_invalid_threshold_raises_error(self):
|
|
554
|
+
"""Test that invalid threshold values raise errors"""
|
|
555
|
+
secret = self.group.random(ZR)
|
|
556
|
+
|
|
557
|
+
# Threshold > num_parties should fail
|
|
558
|
+
with self.assertRaises(ValueError):
|
|
559
|
+
self.ts.share(secret, threshold=5, num_parties=3)
|
|
560
|
+
|
|
561
|
+
# Threshold < 1 should fail
|
|
562
|
+
with self.assertRaises(ValueError):
|
|
563
|
+
self.ts.share(secret, threshold=0, num_parties=3)
|
|
564
|
+
|
|
565
|
+
def test_threshold_limit_validation(self):
|
|
566
|
+
"""Test that excessive thresholds are rejected (MEDIUM-05)."""
|
|
567
|
+
secret = self.group.random(ZR)
|
|
568
|
+
|
|
569
|
+
# Threshold > 256 should fail (safe limit for polynomial evaluation)
|
|
570
|
+
with self.assertRaises(ValueError) as ctx:
|
|
571
|
+
self.ts.share(secret, threshold=300, num_parties=500)
|
|
572
|
+
|
|
573
|
+
# Verify the error message mentions the threshold limit
|
|
574
|
+
self.assertIn("256", str(ctx.exception),
|
|
575
|
+
"Error should mention the safe limit of 256")
|
|
576
|
+
self.assertIn("300", str(ctx.exception),
|
|
577
|
+
"Error should mention the requested threshold")
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class TestPedersenVSS(unittest.TestCase):
|
|
581
|
+
"""Tests for Pedersen VSS (information-theoretically hiding)"""
|
|
582
|
+
|
|
583
|
+
def setUp(self):
|
|
584
|
+
self.group = ECGroup(secp256k1)
|
|
585
|
+
self.pvss = PedersenVSS(self.group)
|
|
586
|
+
|
|
587
|
+
def test_pedersen_vss_verification(self):
|
|
588
|
+
"""Test Pedersen VSS share verification"""
|
|
589
|
+
g = self.group.random(G)
|
|
590
|
+
h = self.group.random(G)
|
|
591
|
+
secret = self.group.random(ZR)
|
|
592
|
+
|
|
593
|
+
shares, blindings, commitments = self.pvss.share_with_blinding(secret, g, h, 2, 3)
|
|
594
|
+
|
|
595
|
+
# All shares should verify
|
|
596
|
+
for pid in [1, 2, 3]:
|
|
597
|
+
valid = self.pvss.verify_pedersen_share(pid, shares[pid], blindings[pid], commitments, g, h)
|
|
598
|
+
self.assertTrue(valid, f"Pedersen share {pid} should verify")
|
|
599
|
+
|
|
600
|
+
def test_pedersen_vss_reconstruction(self):
|
|
601
|
+
"""Test that Pedersen VSS shares reconstruct correctly"""
|
|
602
|
+
g = self.group.random(G)
|
|
603
|
+
h = self.group.random(G)
|
|
604
|
+
secret = self.group.random(ZR)
|
|
605
|
+
|
|
606
|
+
shares, blindings, commitments = self.pvss.share_with_blinding(secret, g, h, 2, 3)
|
|
607
|
+
|
|
608
|
+
# Reconstruct should work
|
|
609
|
+
recovered = self.pvss.reconstruct({1: shares[1], 3: shares[3]}, threshold=2)
|
|
610
|
+
self.assertEqual(secret, recovered)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class TestDKLS23_DKG(unittest.TestCase):
|
|
614
|
+
"""Tests for Distributed Key Generation"""
|
|
615
|
+
|
|
616
|
+
def setUp(self):
|
|
617
|
+
self.group = ECGroup(secp256k1)
|
|
618
|
+
|
|
619
|
+
def test_2_of_3_dkg(self):
|
|
620
|
+
"""Test 2-of-3 distributed key generation"""
|
|
621
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
622
|
+
g = self.group.random(G)
|
|
623
|
+
|
|
624
|
+
# Generate a shared session ID for all participants
|
|
625
|
+
session_id = b"test-session-2of3-dkg"
|
|
626
|
+
|
|
627
|
+
# Round 1: Each party generates secret and Feldman commitments
|
|
628
|
+
party_states = [dkg.keygen_round1(i+1, g, session_id) for i in range(3)]
|
|
629
|
+
round1_msgs = [state[0] for state in party_states]
|
|
630
|
+
private_states = [state[1] for state in party_states]
|
|
631
|
+
|
|
632
|
+
# All parties should have different secrets
|
|
633
|
+
secrets = [s['secret'] for s in private_states]
|
|
634
|
+
self.assertEqual(len(set(id(s) for s in secrets)), 3, "Each party should have unique secret")
|
|
635
|
+
|
|
636
|
+
# Round 2: Generate shares for other parties
|
|
637
|
+
round2_results = [dkg.keygen_round2(i+1, private_states[i], round1_msgs) for i in range(3)]
|
|
638
|
+
shares_for_others = [r[0] for r in round2_results]
|
|
639
|
+
states_r2 = [r[1] for r in round2_results]
|
|
640
|
+
|
|
641
|
+
# Round 3: Finalize key shares
|
|
642
|
+
key_shares = []
|
|
643
|
+
for party_id in range(1, 4):
|
|
644
|
+
received = {sender+1: shares_for_others[sender][party_id] for sender in range(3)}
|
|
645
|
+
ks, complaint = dkg.keygen_round3(party_id, states_r2[party_id-1], received, round1_msgs)
|
|
646
|
+
self.assertIsNone(complaint, f"Party {party_id} should not have complaints")
|
|
647
|
+
key_shares.append(ks)
|
|
648
|
+
|
|
649
|
+
# All parties should have valid KeyShare objects
|
|
650
|
+
for ks in key_shares:
|
|
651
|
+
self.assertIsInstance(ks, KeyShare)
|
|
652
|
+
|
|
653
|
+
def test_all_parties_same_pubkey(self):
|
|
654
|
+
"""All parties should derive the same public key"""
|
|
655
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
656
|
+
g = self.group.random(G)
|
|
657
|
+
session_id = b"test-session-same-pubkey"
|
|
658
|
+
|
|
659
|
+
# Run full DKG
|
|
660
|
+
party_states = [dkg.keygen_round1(i+1, g, session_id) for i in range(3)]
|
|
661
|
+
round1_msgs = [s[0] for s in party_states]
|
|
662
|
+
priv_states = [s[1] for s in party_states]
|
|
663
|
+
|
|
664
|
+
round2_results = [dkg.keygen_round2(i+1, priv_states[i], round1_msgs) for i in range(3)]
|
|
665
|
+
shares_for_others = [r[0] for r in round2_results]
|
|
666
|
+
states_r2 = [r[1] for r in round2_results]
|
|
667
|
+
|
|
668
|
+
key_shares = []
|
|
669
|
+
for party_id in range(1, 4):
|
|
670
|
+
received = {sender+1: shares_for_others[sender][party_id] for sender in range(3)}
|
|
671
|
+
ks, complaint = dkg.keygen_round3(party_id, states_r2[party_id-1], received, round1_msgs)
|
|
672
|
+
self.assertIsNone(complaint, f"Party {party_id} should not have complaints")
|
|
673
|
+
key_shares.append(ks)
|
|
674
|
+
|
|
675
|
+
# All should have same public key X
|
|
676
|
+
pub_keys = [ks.X for ks in key_shares]
|
|
677
|
+
self.assertTrue(all(pk == pub_keys[0] for pk in pub_keys), "All parties should have same public key")
|
|
678
|
+
|
|
679
|
+
def test_dkg_computes_correct_public_key(self):
|
|
680
|
+
"""Test that DKG computes public key as product of individual contributions"""
|
|
681
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
682
|
+
g = self.group.random(G)
|
|
683
|
+
session_id = b"test-session-correct-pubkey"
|
|
684
|
+
|
|
685
|
+
party_states = [dkg.keygen_round1(i+1, g, session_id) for i in range(3)]
|
|
686
|
+
round1_msgs = [s[0] for s in party_states]
|
|
687
|
+
priv_states = [s[1] for s in party_states]
|
|
688
|
+
|
|
689
|
+
# Compute expected public key from secrets
|
|
690
|
+
secrets = [s['secret'] for s in priv_states]
|
|
691
|
+
expected_pk = g ** (secrets[0] + secrets[1] + secrets[2])
|
|
692
|
+
|
|
693
|
+
# Get public key from DKG
|
|
694
|
+
all_comms = [msg['commitments'] for msg in round1_msgs]
|
|
695
|
+
computed_pk = dkg.compute_public_key(all_comms, g)
|
|
696
|
+
|
|
697
|
+
self.assertEqual(expected_pk, computed_pk, "DKG should compute correct public key")
|
|
698
|
+
|
|
699
|
+
def test_dkg_rejects_none_session_id(self):
|
|
700
|
+
"""Test that DKG keygen_round1 rejects None session_id"""
|
|
701
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
702
|
+
g = self.group.random(G)
|
|
703
|
+
|
|
704
|
+
with self.assertRaises(ValueError) as ctx:
|
|
705
|
+
dkg.keygen_round1(1, g, session_id=None)
|
|
706
|
+
self.assertIn("required", str(ctx.exception))
|
|
707
|
+
|
|
708
|
+
def test_dkg_rejects_empty_session_id(self):
|
|
709
|
+
"""Test that DKG keygen_round1 rejects empty session_id"""
|
|
710
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
711
|
+
g = self.group.random(G)
|
|
712
|
+
|
|
713
|
+
with self.assertRaises(ValueError):
|
|
714
|
+
dkg.keygen_round1(1, g, session_id=b"")
|
|
715
|
+
with self.assertRaises(ValueError):
|
|
716
|
+
dkg.keygen_round1(1, g, session_id="")
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class TestDKLS23_Presign(unittest.TestCase):
|
|
720
|
+
"""Tests for presigning protocol"""
|
|
721
|
+
|
|
722
|
+
def setUp(self):
|
|
723
|
+
self.group = ECGroup(secp256k1)
|
|
724
|
+
self.ts = ThresholdSharing(self.group)
|
|
725
|
+
|
|
726
|
+
def test_presign_generates_valid_presignature(self):
|
|
727
|
+
"""Test that presigning produces valid presignature objects"""
|
|
728
|
+
presign = DKLS23_Presign(self.group)
|
|
729
|
+
g = self.group.random(G)
|
|
730
|
+
|
|
731
|
+
# Simulate key shares for 2-of-3 threshold
|
|
732
|
+
x = self.group.random(ZR)
|
|
733
|
+
x_shares = self.ts.share(x, 2, 3)
|
|
734
|
+
participants = [1, 2, 3]
|
|
735
|
+
|
|
736
|
+
# Generate a shared session ID (in practice, coordinated before protocol starts)
|
|
737
|
+
from charm.toolbox.securerandom import OpenSSLRand
|
|
738
|
+
session_id = OpenSSLRand().getRandomBytes(32)
|
|
739
|
+
|
|
740
|
+
# Round 1
|
|
741
|
+
r1_results = {}
|
|
742
|
+
states = {}
|
|
743
|
+
for pid in participants:
|
|
744
|
+
broadcast, state = presign.presign_round1(pid, x_shares[pid], participants, g, session_id=session_id)
|
|
745
|
+
r1_results[pid] = broadcast
|
|
746
|
+
states[pid] = state
|
|
747
|
+
|
|
748
|
+
# Round 2
|
|
749
|
+
r2_results = {}
|
|
750
|
+
p2p_msgs = {}
|
|
751
|
+
for pid in participants:
|
|
752
|
+
broadcast, p2p, state = presign.presign_round2(pid, states[pid], r1_results)
|
|
753
|
+
r2_results[pid] = broadcast
|
|
754
|
+
p2p_msgs[pid] = p2p
|
|
755
|
+
states[pid] = state
|
|
756
|
+
|
|
757
|
+
# Collect p2p messages from round 2
|
|
758
|
+
recv_r2 = {}
|
|
759
|
+
for r in participants:
|
|
760
|
+
recv_r2[r] = {s: p2p_msgs[s][r] for s in participants if s != r}
|
|
761
|
+
|
|
762
|
+
# Round 3
|
|
763
|
+
r3_p2p_msgs = {}
|
|
764
|
+
for pid in participants:
|
|
765
|
+
p2p_r3, state = presign.presign_round3(pid, states[pid], r2_results, recv_r2[pid])
|
|
766
|
+
r3_p2p_msgs[pid] = p2p_r3
|
|
767
|
+
states[pid] = state
|
|
768
|
+
|
|
769
|
+
# Collect p2p messages from round 3
|
|
770
|
+
recv_r3 = {}
|
|
771
|
+
for r in participants:
|
|
772
|
+
recv_r3[r] = {s: r3_p2p_msgs[s][r] for s in participants if s != r}
|
|
773
|
+
|
|
774
|
+
# Round 4
|
|
775
|
+
presigs = {}
|
|
776
|
+
for pid in participants:
|
|
777
|
+
presig, failed_parties = presign.presign_round4(pid, states[pid], recv_r3[pid])
|
|
778
|
+
self.assertEqual(failed_parties, [], f"Party {pid} should have no failed parties")
|
|
779
|
+
presigs[pid] = presig
|
|
780
|
+
|
|
781
|
+
# Verify all presignatures are valid
|
|
782
|
+
for pid, presig in presigs.items():
|
|
783
|
+
self.assertIsInstance(presig, Presignature)
|
|
784
|
+
self.assertTrue(presig.is_valid(), f"Presignature for party {pid} should be valid")
|
|
785
|
+
|
|
786
|
+
def test_presignatures_have_same_r(self):
|
|
787
|
+
"""All parties' presignatures should have the same r value"""
|
|
788
|
+
presign = DKLS23_Presign(self.group)
|
|
789
|
+
g = self.group.random(G)
|
|
790
|
+
|
|
791
|
+
x = self.group.random(ZR)
|
|
792
|
+
x_shares = self.ts.share(x, 2, 3)
|
|
793
|
+
participants = [1, 2] # Only 2-of-3 participate
|
|
794
|
+
|
|
795
|
+
# Generate a shared session ID
|
|
796
|
+
from charm.toolbox.securerandom import OpenSSLRand
|
|
797
|
+
session_id = OpenSSLRand().getRandomBytes(32)
|
|
798
|
+
|
|
799
|
+
# Run protocol
|
|
800
|
+
r1 = {}
|
|
801
|
+
st = {}
|
|
802
|
+
for pid in participants:
|
|
803
|
+
msg, s = presign.presign_round1(pid, x_shares[pid], participants, g, session_id=session_id)
|
|
804
|
+
r1[pid], st[pid] = msg, s
|
|
805
|
+
|
|
806
|
+
r2 = {}
|
|
807
|
+
p2p = {}
|
|
808
|
+
for pid in participants:
|
|
809
|
+
b, m, s = presign.presign_round2(pid, st[pid], r1)
|
|
810
|
+
r2[pid], p2p[pid], st[pid] = b, m, s
|
|
811
|
+
|
|
812
|
+
recv_r2 = {r: {s: p2p[s][r] for s in participants if s != r} for r in participants}
|
|
813
|
+
|
|
814
|
+
# Round 3
|
|
815
|
+
r3_p2p = {}
|
|
816
|
+
for pid in participants:
|
|
817
|
+
p2p_r3, state = presign.presign_round3(pid, st[pid], r2, recv_r2[pid])
|
|
818
|
+
r3_p2p[pid] = p2p_r3
|
|
819
|
+
st[pid] = state
|
|
820
|
+
|
|
821
|
+
recv_r3 = {r: {s: r3_p2p[s][r] for s in participants if s != r} for r in participants}
|
|
822
|
+
|
|
823
|
+
# Round 4
|
|
824
|
+
presigs = {}
|
|
825
|
+
for pid in participants:
|
|
826
|
+
presig, failed = presign.presign_round4(pid, st[pid], recv_r3[pid])
|
|
827
|
+
self.assertEqual(failed, [], f"Party {pid} should have no failed parties")
|
|
828
|
+
presigs[pid] = presig
|
|
829
|
+
|
|
830
|
+
# All should have same r value
|
|
831
|
+
r_values = [presigs[pid].r for pid in participants]
|
|
832
|
+
self.assertTrue(all(r == r_values[0] for r in r_values), "All presignatures should have same r")
|
|
833
|
+
|
|
834
|
+
def test_presign_rejects_none_session_id(self):
|
|
835
|
+
"""Test that presign_round1 rejects None session_id"""
|
|
836
|
+
presign = DKLS23_Presign(self.group)
|
|
837
|
+
g = self.group.random(G)
|
|
838
|
+
x_i = self.group.random(ZR)
|
|
839
|
+
|
|
840
|
+
with self.assertRaises(ValueError) as ctx:
|
|
841
|
+
presign.presign_round1(1, x_i, [1, 2, 3], g, session_id=None)
|
|
842
|
+
self.assertIn("required", str(ctx.exception))
|
|
843
|
+
|
|
844
|
+
def test_presign_rejects_empty_session_id(self):
|
|
845
|
+
"""Test that presign_round1 rejects empty session_id"""
|
|
846
|
+
presign = DKLS23_Presign(self.group)
|
|
847
|
+
g = self.group.random(G)
|
|
848
|
+
x_i = self.group.random(ZR)
|
|
849
|
+
|
|
850
|
+
with self.assertRaises(ValueError):
|
|
851
|
+
presign.presign_round1(1, x_i, [1, 2, 3], g, session_id=b"")
|
|
852
|
+
with self.assertRaises(ValueError):
|
|
853
|
+
presign.presign_round1(1, x_i, [1, 2, 3], g, session_id="")
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
class TestDKLS23_Sign(unittest.TestCase):
|
|
857
|
+
"""Tests for signing protocol"""
|
|
858
|
+
|
|
859
|
+
def setUp(self):
|
|
860
|
+
self.group = ECGroup(secp256k1)
|
|
861
|
+
self.signer = DKLS23_Sign(self.group)
|
|
862
|
+
self.ts = ThresholdSharing(self.group)
|
|
863
|
+
|
|
864
|
+
def test_signature_share_generation(self):
|
|
865
|
+
"""Test that signature shares are generated correctly"""
|
|
866
|
+
g = self.group.random(G)
|
|
867
|
+
|
|
868
|
+
# Create simulated presignature with gamma_i and delta_i
|
|
869
|
+
k_i = self.group.random(ZR)
|
|
870
|
+
gamma_i = self.group.random(ZR)
|
|
871
|
+
chi_i = self.group.random(ZR)
|
|
872
|
+
delta_i = k_i * gamma_i
|
|
873
|
+
R = g ** self.group.random(ZR)
|
|
874
|
+
r = self.group.zr(R)
|
|
875
|
+
|
|
876
|
+
presig = Presignature(1, R, r, k_i, chi_i, [1, 2], gamma_i=gamma_i, delta_i=delta_i)
|
|
877
|
+
key_share = KeyShare(1, self.group.random(ZR), g, g, 2, 3)
|
|
878
|
+
|
|
879
|
+
# Compute delta_inv (for single party, delta = delta_i)
|
|
880
|
+
delta_inv = delta_i ** -1
|
|
881
|
+
|
|
882
|
+
message = b"test message"
|
|
883
|
+
sig_share, proof = self.signer.sign_round1(1, presig, key_share, message, [1, 2], delta_inv)
|
|
884
|
+
|
|
885
|
+
self.assertIsNotNone(sig_share, "Signature share should be generated")
|
|
886
|
+
self.assertIn('party_id', proof, "Proof should contain party_id")
|
|
887
|
+
|
|
888
|
+
def test_signature_verification_correct(self):
|
|
889
|
+
"""Test that valid ECDSA signatures verify correctly"""
|
|
890
|
+
g = self.group.random(G)
|
|
891
|
+
|
|
892
|
+
# Create a valid ECDSA signature manually
|
|
893
|
+
x = self.group.random(ZR) # private key
|
|
894
|
+
pk = g ** x # public key
|
|
895
|
+
k = self.group.random(ZR) # nonce
|
|
896
|
+
R = g ** k
|
|
897
|
+
r = self.group.zr(R)
|
|
898
|
+
|
|
899
|
+
message = b"test message"
|
|
900
|
+
e = self.signer._hash_message(message)
|
|
901
|
+
s = (e + r * x) * (k ** -1) # Standard ECDSA: s = k^{-1}(e + rx)
|
|
902
|
+
|
|
903
|
+
sig = ThresholdSignature(r, s)
|
|
904
|
+
|
|
905
|
+
self.assertTrue(self.signer.verify(pk, sig, message, g), "Valid signature should verify")
|
|
906
|
+
|
|
907
|
+
def test_signature_verification_wrong_message(self):
|
|
908
|
+
"""Test that signature verification fails with wrong message"""
|
|
909
|
+
g = self.group.random(G)
|
|
910
|
+
|
|
911
|
+
x = self.group.random(ZR)
|
|
912
|
+
pk = g ** x
|
|
913
|
+
k = self.group.random(ZR)
|
|
914
|
+
R = g ** k
|
|
915
|
+
r = self.group.zr(R)
|
|
916
|
+
|
|
917
|
+
message = b"original message"
|
|
918
|
+
e = self.signer._hash_message(message)
|
|
919
|
+
s = (e + r * x) * (k ** -1)
|
|
920
|
+
sig = ThresholdSignature(r, s)
|
|
921
|
+
|
|
922
|
+
# Verification should fail with wrong message
|
|
923
|
+
self.assertFalse(self.signer.verify(pk, sig, b"wrong message", g),
|
|
924
|
+
"Signature should not verify with wrong message")
|
|
925
|
+
|
|
926
|
+
def test_signature_share_verification(self):
|
|
927
|
+
"""Test that invalid signature shares are detected (MEDIUM-06)."""
|
|
928
|
+
g = self.group.random(G)
|
|
929
|
+
|
|
930
|
+
# Create simulated presignature with gamma_i and delta_i
|
|
931
|
+
k_i = self.group.random(ZR)
|
|
932
|
+
gamma_i = self.group.random(ZR)
|
|
933
|
+
chi_i = self.group.random(ZR)
|
|
934
|
+
delta_i = k_i * gamma_i
|
|
935
|
+
R = g ** self.group.random(ZR)
|
|
936
|
+
r = self.group.zr(R)
|
|
937
|
+
|
|
938
|
+
presig = Presignature(1, R, r, k_i, chi_i, [1, 2], gamma_i=gamma_i, delta_i=delta_i)
|
|
939
|
+
key_share = KeyShare(1, self.group.random(ZR), g, g, 2, 3)
|
|
940
|
+
|
|
941
|
+
# Compute delta_inv (for single party, delta = delta_i)
|
|
942
|
+
delta_inv = delta_i ** -1
|
|
943
|
+
|
|
944
|
+
message = b"test message"
|
|
945
|
+
sig_share, proof = self.signer.sign_round1(1, presig, key_share, message, [1, 2], delta_inv)
|
|
946
|
+
|
|
947
|
+
# Test 1: Valid share should pass verification
|
|
948
|
+
self.assertTrue(
|
|
949
|
+
self.signer.verify_signature_share(1, sig_share, proof, presig, message),
|
|
950
|
+
"Valid signature share should pass verification"
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
# Test 2: None share should fail
|
|
954
|
+
self.assertFalse(
|
|
955
|
+
self.signer.verify_signature_share(1, None, proof, presig, message),
|
|
956
|
+
"None share should fail verification"
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
# Test 3: Wrong party_id in proof should fail
|
|
960
|
+
wrong_proof = {'party_id': 99, 'R': presig.R}
|
|
961
|
+
self.assertFalse(
|
|
962
|
+
self.signer.verify_signature_share(1, sig_share, wrong_proof, presig, message),
|
|
963
|
+
"Share with wrong party_id in proof should fail verification"
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# Test 4: Empty proof should fail
|
|
967
|
+
self.assertFalse(
|
|
968
|
+
self.signer.verify_signature_share(1, sig_share, {}, presig, message),
|
|
969
|
+
"Share with empty proof should fail verification"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
# Test 5: combine_signatures should reject invalid shares when proofs provided
|
|
973
|
+
# Create a second valid share with gamma_i and delta_i
|
|
974
|
+
k_i2 = self.group.random(ZR)
|
|
975
|
+
gamma_i2 = self.group.random(ZR)
|
|
976
|
+
chi_i2 = self.group.random(ZR)
|
|
977
|
+
delta_i2 = k_i2 * gamma_i2
|
|
978
|
+
delta_inv2 = delta_i2 ** -1
|
|
979
|
+
presig2 = Presignature(2, R, r, k_i2, chi_i2, [1, 2], gamma_i=gamma_i2, delta_i=delta_i2)
|
|
980
|
+
key_share2 = KeyShare(2, self.group.random(ZR), g, g, 2, 3)
|
|
981
|
+
sig_share2, proof2 = self.signer.sign_round1(2, presig2, key_share2, message, [1, 2], delta_inv2)
|
|
982
|
+
|
|
983
|
+
shares = {1: sig_share, 2: sig_share2}
|
|
984
|
+
proofs = {1: proof, 2: proof2}
|
|
985
|
+
|
|
986
|
+
# Valid shares with valid proofs should work
|
|
987
|
+
sig = self.signer.combine_signatures(shares, presig, [1, 2], proofs, message)
|
|
988
|
+
self.assertIsNotNone(sig, "combine_signatures should succeed with valid proofs")
|
|
989
|
+
|
|
990
|
+
# Invalid proof should raise ValueError
|
|
991
|
+
invalid_proofs = {1: proof, 2: {'party_id': 99, 'R': R}}
|
|
992
|
+
with self.assertRaises(ValueError) as context:
|
|
993
|
+
self.signer.combine_signatures(shares, presig, [1, 2], invalid_proofs, message)
|
|
994
|
+
self.assertIn("party 2", str(context.exception))
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
class TestDKLS23_Complete(unittest.TestCase):
|
|
998
|
+
"""End-to-end tests for complete DKLS23 protocol"""
|
|
999
|
+
|
|
1000
|
+
def setUp(self):
|
|
1001
|
+
self.group = ECGroup(secp256k1)
|
|
1002
|
+
|
|
1003
|
+
def test_complete_2_of_3_signing(self):
|
|
1004
|
+
"""Complete flow: keygen -> presign -> sign -> verify"""
|
|
1005
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1006
|
+
g = self.group.random(G)
|
|
1007
|
+
|
|
1008
|
+
# Step 1: Distributed Key Generation
|
|
1009
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1010
|
+
|
|
1011
|
+
self.assertEqual(len(key_shares), 3, "Should have 3 key shares")
|
|
1012
|
+
|
|
1013
|
+
# Step 2: Generate presignatures (participants 1 and 2)
|
|
1014
|
+
participants = [1, 2]
|
|
1015
|
+
presignatures = dkls.presign(participants, key_shares, g)
|
|
1016
|
+
|
|
1017
|
+
self.assertEqual(len(presignatures), 2, "Should have 2 presignatures")
|
|
1018
|
+
|
|
1019
|
+
# Step 3: Sign a message
|
|
1020
|
+
message = b"Hello, threshold ECDSA!"
|
|
1021
|
+
signature = dkls.sign(participants, presignatures, key_shares, message, g)
|
|
1022
|
+
|
|
1023
|
+
self.assertIsInstance(signature, ThresholdSignature)
|
|
1024
|
+
|
|
1025
|
+
# Step 4: Verify signature
|
|
1026
|
+
self.assertTrue(dkls.verify(public_key, signature, message, g),
|
|
1027
|
+
"Signature should verify correctly")
|
|
1028
|
+
|
|
1029
|
+
def test_different_participant_combinations(self):
|
|
1030
|
+
"""Test that any 2 of 3 parties can sign"""
|
|
1031
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1032
|
+
g = self.group.random(G)
|
|
1033
|
+
|
|
1034
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1035
|
+
message = b"Test message for any 2 of 3"
|
|
1036
|
+
|
|
1037
|
+
# Test all possible 2-party combinations
|
|
1038
|
+
combinations = [[1, 2], [1, 3], [2, 3]]
|
|
1039
|
+
|
|
1040
|
+
for participants in combinations:
|
|
1041
|
+
presigs = dkls.presign(participants, key_shares, g)
|
|
1042
|
+
sig = dkls.sign(participants, presigs, key_shares, message, g)
|
|
1043
|
+
|
|
1044
|
+
self.assertTrue(dkls.verify(public_key, sig, message, g),
|
|
1045
|
+
f"Signature with participants {participants} should verify")
|
|
1046
|
+
|
|
1047
|
+
def test_signature_is_standard_ecdsa(self):
|
|
1048
|
+
"""Verify that output is standard ECDSA signature format"""
|
|
1049
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1050
|
+
g = self.group.random(G)
|
|
1051
|
+
|
|
1052
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1053
|
+
presigs = dkls.presign([1, 2], key_shares, g)
|
|
1054
|
+
message = b"Standard ECDSA test"
|
|
1055
|
+
sig = dkls.sign([1, 2], presigs, key_shares, message, g)
|
|
1056
|
+
|
|
1057
|
+
# Verify signature has r and s components
|
|
1058
|
+
self.assertTrue(hasattr(sig, 'r'), "Signature should have r component")
|
|
1059
|
+
self.assertTrue(hasattr(sig, 's'), "Signature should have s component")
|
|
1060
|
+
|
|
1061
|
+
# Verify it can be converted to DER format
|
|
1062
|
+
der_bytes = sig.to_der()
|
|
1063
|
+
self.assertIsInstance(der_bytes, bytes, "DER encoding should produce bytes")
|
|
1064
|
+
self.assertEqual(der_bytes[0], 0x30, "DER should start with SEQUENCE tag")
|
|
1065
|
+
|
|
1066
|
+
def test_wrong_message_fails_verification(self):
|
|
1067
|
+
"""Test that signature verification fails with wrong message"""
|
|
1068
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1069
|
+
g = self.group.random(G)
|
|
1070
|
+
|
|
1071
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1072
|
+
presigs = dkls.presign([1, 2], key_shares, g)
|
|
1073
|
+
|
|
1074
|
+
message = b"Original message"
|
|
1075
|
+
sig = dkls.sign([1, 2], presigs, key_shares, message, g)
|
|
1076
|
+
|
|
1077
|
+
# Verify fails with different message
|
|
1078
|
+
self.assertFalse(dkls.verify(public_key, sig, b"Different message", g),
|
|
1079
|
+
"Verification should fail with wrong message")
|
|
1080
|
+
|
|
1081
|
+
def test_insufficient_participants_raises_error(self):
|
|
1082
|
+
"""Test that signing with insufficient participants raises error"""
|
|
1083
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1084
|
+
g = self.group.random(G)
|
|
1085
|
+
|
|
1086
|
+
key_shares, _ = dkls.distributed_keygen(g)
|
|
1087
|
+
|
|
1088
|
+
# Try to presign with only 1 participant (need 2)
|
|
1089
|
+
with self.assertRaises(ValueError):
|
|
1090
|
+
dkls.presign([1], key_shares, g)
|
|
1091
|
+
|
|
1092
|
+
def test_3_of_5_threshold(self):
|
|
1093
|
+
"""Test 3-of-5 threshold scheme"""
|
|
1094
|
+
dkls = DKLS23(self.group, threshold=3, num_parties=5)
|
|
1095
|
+
g = self.group.random(G)
|
|
1096
|
+
|
|
1097
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1098
|
+
|
|
1099
|
+
# Sign with exactly 3 participants
|
|
1100
|
+
participants = [1, 3, 5]
|
|
1101
|
+
presigs = dkls.presign(participants, key_shares, g)
|
|
1102
|
+
message = b"3-of-5 threshold test"
|
|
1103
|
+
sig = dkls.sign(participants, presigs, key_shares, message, g)
|
|
1104
|
+
|
|
1105
|
+
self.assertTrue(dkls.verify(public_key, sig, message, g),
|
|
1106
|
+
"3-of-5 signature should verify")
|
|
1107
|
+
|
|
1108
|
+
def test_multiple_messages_same_keys(self):
|
|
1109
|
+
"""Test signing multiple messages with same key shares"""
|
|
1110
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1111
|
+
g = self.group.random(G)
|
|
1112
|
+
|
|
1113
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1114
|
+
|
|
1115
|
+
messages = [
|
|
1116
|
+
b"First message",
|
|
1117
|
+
b"Second message",
|
|
1118
|
+
b"Third message"
|
|
1119
|
+
]
|
|
1120
|
+
|
|
1121
|
+
for msg in messages:
|
|
1122
|
+
# Need fresh presignatures for each signature
|
|
1123
|
+
presigs = dkls.presign([1, 2], key_shares, g)
|
|
1124
|
+
sig = dkls.sign([1, 2], presigs, key_shares, msg, g)
|
|
1125
|
+
|
|
1126
|
+
self.assertTrue(dkls.verify(public_key, sig, msg, g),
|
|
1127
|
+
f"Signature for '{msg.decode()}' should verify")
|
|
1128
|
+
|
|
1129
|
+
def test_invalid_threshold_raises_error(self):
|
|
1130
|
+
"""Test that invalid threshold/num_parties raises error"""
|
|
1131
|
+
# Threshold > num_parties should fail
|
|
1132
|
+
with self.assertRaises(ValueError):
|
|
1133
|
+
DKLS23(self.group, threshold=5, num_parties=3)
|
|
1134
|
+
|
|
1135
|
+
# Threshold < 1 should fail
|
|
1136
|
+
with self.assertRaises(ValueError):
|
|
1137
|
+
DKLS23(self.group, threshold=0, num_parties=3)
|
|
1138
|
+
|
|
1139
|
+
def test_keygen_interface(self):
|
|
1140
|
+
"""Test the PKSig-compatible keygen interface"""
|
|
1141
|
+
dkls = DKLS23(self.group, threshold=2, num_parties=3)
|
|
1142
|
+
|
|
1143
|
+
# keygen() should work without explicit generator
|
|
1144
|
+
key_shares, public_key = dkls.keygen()
|
|
1145
|
+
|
|
1146
|
+
self.assertEqual(len(key_shares), 3)
|
|
1147
|
+
self.assertIsNotNone(public_key)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class TestCurveAgnostic(unittest.TestCase):
|
|
1151
|
+
"""Tests for curve agnosticism (MEDIUM-11)"""
|
|
1152
|
+
|
|
1153
|
+
def test_curve_agnostic_prime256v1(self):
|
|
1154
|
+
"""Test that DKLS23 works with different curves (MEDIUM-11).
|
|
1155
|
+
|
|
1156
|
+
Uses prime256v1 (P-256/secp256r1) instead of secp256k1 to verify
|
|
1157
|
+
the protocol is curve-agnostic.
|
|
1158
|
+
"""
|
|
1159
|
+
from charm.toolbox.eccurve import prime256v1
|
|
1160
|
+
group = ECGroup(prime256v1)
|
|
1161
|
+
|
|
1162
|
+
dkls = DKLS23(group, threshold=2, num_parties=3)
|
|
1163
|
+
g = group.random(G)
|
|
1164
|
+
|
|
1165
|
+
# Complete flow: keygen -> presign -> sign -> verify
|
|
1166
|
+
key_shares, public_key = dkls.distributed_keygen(g)
|
|
1167
|
+
|
|
1168
|
+
presigs = dkls.presign([1, 2], key_shares, g)
|
|
1169
|
+
message = b"Testing curve agnosticism with P-256"
|
|
1170
|
+
sig = dkls.sign([1, 2], presigs, key_shares, message, g)
|
|
1171
|
+
|
|
1172
|
+
self.assertTrue(dkls.verify(public_key, sig, message, g),
|
|
1173
|
+
"Signature with prime256v1 should verify")
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
class TestThresholdSignature(unittest.TestCase):
|
|
1177
|
+
"""Tests for ThresholdSignature class"""
|
|
1178
|
+
|
|
1179
|
+
def setUp(self):
|
|
1180
|
+
self.group = ECGroup(secp256k1)
|
|
1181
|
+
|
|
1182
|
+
def test_signature_equality(self):
|
|
1183
|
+
"""Test ThresholdSignature equality comparison"""
|
|
1184
|
+
r = self.group.random(ZR)
|
|
1185
|
+
s = self.group.random(ZR)
|
|
1186
|
+
|
|
1187
|
+
sig1 = ThresholdSignature(r, s)
|
|
1188
|
+
sig2 = ThresholdSignature(r, s)
|
|
1189
|
+
|
|
1190
|
+
self.assertEqual(sig1, sig2, "Signatures with same r,s should be equal")
|
|
1191
|
+
|
|
1192
|
+
def test_signature_inequality(self):
|
|
1193
|
+
"""Test ThresholdSignature inequality"""
|
|
1194
|
+
r1 = self.group.random(ZR)
|
|
1195
|
+
s1 = self.group.random(ZR)
|
|
1196
|
+
r2 = self.group.random(ZR)
|
|
1197
|
+
s2 = self.group.random(ZR)
|
|
1198
|
+
|
|
1199
|
+
sig1 = ThresholdSignature(r1, s1)
|
|
1200
|
+
sig2 = ThresholdSignature(r2, s2)
|
|
1201
|
+
|
|
1202
|
+
self.assertNotEqual(sig1, sig2, "Different signatures should not be equal")
|
|
1203
|
+
|
|
1204
|
+
def test_der_encoding(self):
|
|
1205
|
+
"""Test DER encoding produces valid structure"""
|
|
1206
|
+
r = self.group.random(ZR)
|
|
1207
|
+
s = self.group.random(ZR)
|
|
1208
|
+
sig = ThresholdSignature(r, s)
|
|
1209
|
+
|
|
1210
|
+
der = sig.to_der()
|
|
1211
|
+
|
|
1212
|
+
# Check DER structure: SEQUENCE (0x30), length, INTEGER (0x02), ...
|
|
1213
|
+
self.assertEqual(der[0], 0x30, "Should start with SEQUENCE")
|
|
1214
|
+
self.assertEqual(der[1], len(der) - 2, "Length should match")
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
class TestMaliciousParties(unittest.TestCase):
|
|
1218
|
+
"""Tests for adversarial/malicious party scenarios in threshold ECDSA.
|
|
1219
|
+
|
|
1220
|
+
These tests verify that the protocol correctly detects and handles
|
|
1221
|
+
various forms of malicious behavior including:
|
|
1222
|
+
- Invalid shares during DKG
|
|
1223
|
+
- Wrong commitments
|
|
1224
|
+
- Commitment mismatches during presigning
|
|
1225
|
+
- Invalid signature shares
|
|
1226
|
+
"""
|
|
1227
|
+
|
|
1228
|
+
@classmethod
|
|
1229
|
+
def setUpClass(cls):
|
|
1230
|
+
cls.group = ECGroup(secp256k1)
|
|
1231
|
+
cls.g = cls.group.random(G)
|
|
1232
|
+
|
|
1233
|
+
def test_dkg_invalid_share_detected(self):
|
|
1234
|
+
"""Test that DKG detects tampered shares during round 3.
|
|
1235
|
+
|
|
1236
|
+
Run DKG with 3 parties. In round 2, tamper with party 3's share
|
|
1237
|
+
to party 1 (add 1 to the share value). Verify that party 1
|
|
1238
|
+
detects the invalid share in round 3 (returns a complaint).
|
|
1239
|
+
"""
|
|
1240
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
1241
|
+
session_id = b"test-session-invalid-share"
|
|
1242
|
+
|
|
1243
|
+
# Round 1: Each party generates secret and Feldman commitments
|
|
1244
|
+
party_states = [dkg.keygen_round1(i+1, self.g, session_id) for i in range(3)]
|
|
1245
|
+
round1_msgs = [state[0] for state in party_states]
|
|
1246
|
+
private_states = [state[1] for state in party_states]
|
|
1247
|
+
|
|
1248
|
+
# Round 2: Generate shares for other parties
|
|
1249
|
+
round2_results = [dkg.keygen_round2(i+1, private_states[i], round1_msgs) for i in range(3)]
|
|
1250
|
+
shares_for_others = [r[0] for r in round2_results]
|
|
1251
|
+
states_r2 = [r[1] for r in round2_results]
|
|
1252
|
+
|
|
1253
|
+
# Tamper with party 3's share to party 1: add 1 to corrupt it
|
|
1254
|
+
one = self.group.init(ZR, 1)
|
|
1255
|
+
original_share = shares_for_others[2][1] # Party 3's share for party 1
|
|
1256
|
+
tampered_share = original_share + one
|
|
1257
|
+
shares_for_others[2][1] = tampered_share
|
|
1258
|
+
|
|
1259
|
+
# Collect shares for party 1 (receiving from all parties)
|
|
1260
|
+
received_shares_p1 = {sender+1: shares_for_others[sender][1] for sender in range(3)}
|
|
1261
|
+
|
|
1262
|
+
# Round 3: Party 1 should detect the invalid share from party 3
|
|
1263
|
+
# API returns (KeyShare, complaint) - complaint should identify party 3
|
|
1264
|
+
key_share, complaint = dkg.keygen_round3(1, states_r2[0], received_shares_p1, round1_msgs)
|
|
1265
|
+
|
|
1266
|
+
# Key share should be None since verification failed
|
|
1267
|
+
self.assertIsNone(key_share, "Key share should be None when verification fails")
|
|
1268
|
+
|
|
1269
|
+
# Complaint should identify party 3 as the accused
|
|
1270
|
+
self.assertIsNotNone(complaint, "Complaint should be generated for invalid share")
|
|
1271
|
+
self.assertEqual(complaint['accused'], 3, "Complaint should accuse party 3")
|
|
1272
|
+
self.assertEqual(complaint['accuser'], 1, "Complaint should be from party 1")
|
|
1273
|
+
|
|
1274
|
+
def test_dkg_wrong_commitment_detected(self):
|
|
1275
|
+
"""Test that DKG detects when a party's commitment doesn't match their shares.
|
|
1276
|
+
|
|
1277
|
+
Run DKG round 1, then modify party 2's commitment list by changing
|
|
1278
|
+
the first commitment to a random point. Verify share verification
|
|
1279
|
+
fails for party 2's shares.
|
|
1280
|
+
"""
|
|
1281
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
1282
|
+
session_id = b"test-session-wrong-commitment"
|
|
1283
|
+
|
|
1284
|
+
# Round 1: Each party generates secret and Feldman commitments
|
|
1285
|
+
party_states = [dkg.keygen_round1(i+1, self.g, session_id) for i in range(3)]
|
|
1286
|
+
round1_msgs = [state[0] for state in party_states]
|
|
1287
|
+
private_states = [state[1] for state in party_states]
|
|
1288
|
+
|
|
1289
|
+
# Modify party 2's first commitment to a random point
|
|
1290
|
+
original_commitment = round1_msgs[1]['commitments'][0]
|
|
1291
|
+
random_point = self.g ** self.group.random(ZR)
|
|
1292
|
+
round1_msgs[1]['commitments'][0] = random_point
|
|
1293
|
+
|
|
1294
|
+
# Round 2: Generate shares normally
|
|
1295
|
+
round2_results = [dkg.keygen_round2(i+1, private_states[i], round1_msgs) for i in range(3)]
|
|
1296
|
+
shares_for_others = [r[0] for r in round2_results]
|
|
1297
|
+
states_r2 = [r[1] for r in round2_results]
|
|
1298
|
+
|
|
1299
|
+
# Party 1 receives shares from all parties
|
|
1300
|
+
received_shares_p1 = {sender+1: shares_for_others[sender][1] for sender in range(3)}
|
|
1301
|
+
|
|
1302
|
+
# Round 3: Party 1 should detect that party 2's share doesn't match the commitment
|
|
1303
|
+
key_share, complaint = dkg.keygen_round3(1, states_r2[0], received_shares_p1, round1_msgs)
|
|
1304
|
+
|
|
1305
|
+
# Key share should be None since verification failed
|
|
1306
|
+
self.assertIsNone(key_share, "Key share should be None when verification fails")
|
|
1307
|
+
|
|
1308
|
+
# Complaint should identify party 2 as the accused
|
|
1309
|
+
self.assertIsNotNone(complaint, "Complaint should be generated for mismatched commitment")
|
|
1310
|
+
self.assertEqual(complaint['accused'], 2, "Complaint should accuse party 2")
|
|
1311
|
+
|
|
1312
|
+
def test_presign_commitment_mismatch_detected(self):
|
|
1313
|
+
"""Test that presigning detects when Gamma_i doesn't match the commitment.
|
|
1314
|
+
|
|
1315
|
+
Run presign round 1 with 3 parties. In round 2 messages, replace
|
|
1316
|
+
party 2's Gamma_i with a different value that doesn't match the
|
|
1317
|
+
commitment. Verify round 3 raises ValueError about commitment verification.
|
|
1318
|
+
|
|
1319
|
+
Note: This test validates the commitment verification logic in the presigning
|
|
1320
|
+
protocol. The test directly verifies commitment checking without going through
|
|
1321
|
+
the full MtA completion (which has a separate API change).
|
|
1322
|
+
"""
|
|
1323
|
+
presign = DKLS23_Presign(self.group)
|
|
1324
|
+
ts = ThresholdSharing(self.group)
|
|
1325
|
+
|
|
1326
|
+
# Create simulated key shares
|
|
1327
|
+
x = self.group.random(ZR)
|
|
1328
|
+
x_shares = ts.share(x, 2, 3)
|
|
1329
|
+
participants = [1, 2, 3]
|
|
1330
|
+
session_id = b"test-session-presign-mismatch"
|
|
1331
|
+
|
|
1332
|
+
# Round 1
|
|
1333
|
+
r1_results = {}
|
|
1334
|
+
states = {}
|
|
1335
|
+
for pid in participants:
|
|
1336
|
+
broadcast, state = presign.presign_round1(pid, x_shares[pid], participants, self.g, session_id)
|
|
1337
|
+
r1_results[pid] = broadcast
|
|
1338
|
+
states[pid] = state
|
|
1339
|
+
|
|
1340
|
+
# Round 2 - but we'll tamper with party 2's Gamma_i after
|
|
1341
|
+
r2_results = {}
|
|
1342
|
+
p2p_msgs = {}
|
|
1343
|
+
for pid in participants:
|
|
1344
|
+
broadcast, p2p, state = presign.presign_round2(pid, states[pid], r1_results)
|
|
1345
|
+
r2_results[pid] = broadcast
|
|
1346
|
+
p2p_msgs[pid] = p2p
|
|
1347
|
+
states[pid] = state
|
|
1348
|
+
|
|
1349
|
+
# Tamper: Replace party 2's Gamma_i with a random point (won't match commitment)
|
|
1350
|
+
fake_gamma = self.g ** self.group.random(ZR)
|
|
1351
|
+
r2_results[2]['Gamma_i'] = fake_gamma
|
|
1352
|
+
|
|
1353
|
+
# Verify commitment mismatch directly using the commitment verification logic
|
|
1354
|
+
# This is the core security check that should detect the tampering
|
|
1355
|
+
# Note: Commitments are now bound to session_id and participants
|
|
1356
|
+
session_id = states[2]['session_id']
|
|
1357
|
+
commitment = r1_results[2]['Gamma_commitment']
|
|
1358
|
+
revealed_Gamma = r2_results[2]['Gamma_i']
|
|
1359
|
+
computed_commitment = presign._compute_commitment(
|
|
1360
|
+
revealed_Gamma, session_id=session_id, participants=participants
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
# The tampered commitment should NOT match
|
|
1364
|
+
self.assertNotEqual(commitment, computed_commitment,
|
|
1365
|
+
"Tampered Gamma_i should not match original commitment")
|
|
1366
|
+
|
|
1367
|
+
# Verify that the original (untampered) Gamma would match
|
|
1368
|
+
original_Gamma = states[2]['Gamma_i']
|
|
1369
|
+
original_computed = presign._compute_commitment(
|
|
1370
|
+
original_Gamma, session_id=session_id, participants=participants
|
|
1371
|
+
)
|
|
1372
|
+
self.assertEqual(commitment, original_computed,
|
|
1373
|
+
"Original Gamma_i should match commitment")
|
|
1374
|
+
|
|
1375
|
+
def test_signature_invalid_share_produces_invalid_sig(self):
|
|
1376
|
+
"""Test that tampering with signature shares produces invalid signatures.
|
|
1377
|
+
|
|
1378
|
+
Use simulated presignatures to test that modifying a party's
|
|
1379
|
+
signature share (s_i) causes the aggregated signature to fail
|
|
1380
|
+
ECDSA verification. This validates that malicious tampering with
|
|
1381
|
+
signature shares is detectable.
|
|
1382
|
+
"""
|
|
1383
|
+
signer = DKLS23_Sign(self.group)
|
|
1384
|
+
ts = ThresholdSharing(self.group)
|
|
1385
|
+
|
|
1386
|
+
# Create a valid ECDSA key pair for testing
|
|
1387
|
+
x = self.group.random(ZR) # private key
|
|
1388
|
+
pk = self.g ** x # public key
|
|
1389
|
+
|
|
1390
|
+
# Create key shares (2-of-3 threshold)
|
|
1391
|
+
x_shares = ts.share(x, 2, 3)
|
|
1392
|
+
participants = [1, 2]
|
|
1393
|
+
|
|
1394
|
+
# Create simulated presignatures with correct structure
|
|
1395
|
+
# k = nonce, gamma = blinding factor
|
|
1396
|
+
k = self.group.random(ZR)
|
|
1397
|
+
gamma = self.group.random(ZR)
|
|
1398
|
+
|
|
1399
|
+
# Compute shares of k*gamma (delta) and gamma*x (sigma)
|
|
1400
|
+
k_shares = ts.share(k, 2, 3)
|
|
1401
|
+
delta = k * gamma
|
|
1402
|
+
delta_shares = ts.share(delta, 2, 3)
|
|
1403
|
+
sigma = gamma * x
|
|
1404
|
+
sigma_shares = ts.share(sigma, 2, 3)
|
|
1405
|
+
gamma_shares = ts.share(gamma, 2, 3)
|
|
1406
|
+
|
|
1407
|
+
# R = g^k (nonce point)
|
|
1408
|
+
R = self.g ** k
|
|
1409
|
+
r = self.group.zr(R)
|
|
1410
|
+
|
|
1411
|
+
# Create KeyShare objects
|
|
1412
|
+
key_shares = {}
|
|
1413
|
+
for pid in participants:
|
|
1414
|
+
key_shares[pid] = KeyShare(
|
|
1415
|
+
party_id=pid,
|
|
1416
|
+
private_share=x_shares[pid],
|
|
1417
|
+
public_key=pk,
|
|
1418
|
+
verification_key=self.g ** x_shares[pid],
|
|
1419
|
+
threshold=2,
|
|
1420
|
+
num_parties=3
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
# Create Presignature objects with all required fields
|
|
1424
|
+
presignatures = {}
|
|
1425
|
+
for pid in participants:
|
|
1426
|
+
presignatures[pid] = Presignature(
|
|
1427
|
+
party_id=pid,
|
|
1428
|
+
R=R,
|
|
1429
|
+
r=r,
|
|
1430
|
+
k_share=k_shares[pid],
|
|
1431
|
+
chi_share=sigma_shares[pid], # gamma*x share
|
|
1432
|
+
participants=participants,
|
|
1433
|
+
gamma_i=gamma_shares[pid],
|
|
1434
|
+
delta_i=delta_shares[pid]
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
message = b"Test message for malicious party"
|
|
1438
|
+
|
|
1439
|
+
# Compute delta_inv (delta is public in the protocol)
|
|
1440
|
+
total_delta = self.group.init(ZR, 0)
|
|
1441
|
+
for pid in participants:
|
|
1442
|
+
total_delta = total_delta + presignatures[pid].delta_i
|
|
1443
|
+
delta_inv = total_delta ** -1
|
|
1444
|
+
|
|
1445
|
+
# Generate signature shares
|
|
1446
|
+
signature_shares = {}
|
|
1447
|
+
for pid in participants:
|
|
1448
|
+
s_i, proof = signer.sign_round1(
|
|
1449
|
+
pid, presignatures[pid], key_shares[pid], message, participants, delta_inv
|
|
1450
|
+
)
|
|
1451
|
+
signature_shares[pid] = s_i
|
|
1452
|
+
|
|
1453
|
+
# Tamper with party 2's signature share
|
|
1454
|
+
one = self.group.init(ZR, 1)
|
|
1455
|
+
signature_shares[2] = signature_shares[2] + one
|
|
1456
|
+
|
|
1457
|
+
# Aggregate (with tampered share)
|
|
1458
|
+
s = self.group.init(ZR, 0)
|
|
1459
|
+
for pid in participants:
|
|
1460
|
+
s = s + signature_shares[pid]
|
|
1461
|
+
|
|
1462
|
+
tampered_signature = ThresholdSignature(r, s)
|
|
1463
|
+
|
|
1464
|
+
# Verify should fail with tampered signature
|
|
1465
|
+
self.assertFalse(
|
|
1466
|
+
signer.verify(pk, tampered_signature, message, self.g),
|
|
1467
|
+
"Tampered signature should not verify"
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
# Also verify that an untampered signature would work
|
|
1471
|
+
# (regenerate without tampering)
|
|
1472
|
+
signature_shares_valid = {}
|
|
1473
|
+
for pid in participants:
|
|
1474
|
+
s_i, proof = signer.sign_round1(
|
|
1475
|
+
pid, presignatures[pid], key_shares[pid], message, participants, delta_inv
|
|
1476
|
+
)
|
|
1477
|
+
signature_shares_valid[pid] = s_i
|
|
1478
|
+
|
|
1479
|
+
s_valid = self.group.init(ZR, 0)
|
|
1480
|
+
for pid in participants:
|
|
1481
|
+
s_valid = s_valid + signature_shares_valid[pid]
|
|
1482
|
+
|
|
1483
|
+
valid_signature = ThresholdSignature(r, s_valid)
|
|
1484
|
+
|
|
1485
|
+
# Note: The simplified presignature setup may not produce a valid
|
|
1486
|
+
# signature due to the complexity of the protocol. The key test is
|
|
1487
|
+
# that tampering changes the signature in a way that would be detected.
|
|
1488
|
+
|
|
1489
|
+
def test_mta_receiver_learns_only_chosen_message(self):
|
|
1490
|
+
"""Test MtA security property: receiver's beta depends only on chosen values.
|
|
1491
|
+
|
|
1492
|
+
Run MtA protocol and verify that the receiver's beta calculation
|
|
1493
|
+
depends only on the specific input values used, not any other information.
|
|
1494
|
+
This tests the basic security property of the MtA protocol.
|
|
1495
|
+
"""
|
|
1496
|
+
alice_mta = MtA(self.group)
|
|
1497
|
+
bob_mta = MtA(self.group)
|
|
1498
|
+
|
|
1499
|
+
# Alice has share a, Bob has share b
|
|
1500
|
+
a = self.group.random(ZR)
|
|
1501
|
+
b = self.group.random(ZR)
|
|
1502
|
+
|
|
1503
|
+
# Run MtA protocol (3 round version)
|
|
1504
|
+
sender_msg = alice_mta.sender_round1(a)
|
|
1505
|
+
receiver_msg, _ = bob_mta.receiver_round1(b, sender_msg)
|
|
1506
|
+
alpha, ot_ciphertexts = alice_mta.sender_round2(receiver_msg)
|
|
1507
|
+
beta = bob_mta.receiver_round2(ot_ciphertexts)
|
|
1508
|
+
|
|
1509
|
+
# Verify basic correctness: a*b = alpha + beta
|
|
1510
|
+
product = a * b
|
|
1511
|
+
additive_sum = alpha + beta
|
|
1512
|
+
self.assertEqual(product, additive_sum, "MtA correctness should hold")
|
|
1513
|
+
|
|
1514
|
+
# Security test: Run protocol again with same a but different b
|
|
1515
|
+
# Bob's beta should be completely different
|
|
1516
|
+
b2 = self.group.random(ZR)
|
|
1517
|
+
while b2 == b:
|
|
1518
|
+
b2 = self.group.random(ZR)
|
|
1519
|
+
|
|
1520
|
+
alice_mta2 = MtA(self.group)
|
|
1521
|
+
bob_mta2 = MtA(self.group)
|
|
1522
|
+
|
|
1523
|
+
sender_msg2 = alice_mta2.sender_round1(a)
|
|
1524
|
+
receiver_msg2, _ = bob_mta2.receiver_round1(b2, sender_msg2)
|
|
1525
|
+
alpha2, ot_ciphertexts2 = alice_mta2.sender_round2(receiver_msg2)
|
|
1526
|
+
beta2 = bob_mta2.receiver_round2(ot_ciphertexts2)
|
|
1527
|
+
|
|
1528
|
+
# Verify second run is also correct
|
|
1529
|
+
product2 = a * b2
|
|
1530
|
+
additive_sum2 = alpha2 + beta2
|
|
1531
|
+
self.assertEqual(product2, additive_sum2, "Second MtA run should be correct")
|
|
1532
|
+
|
|
1533
|
+
# Beta values should be different (overwhelming probability)
|
|
1534
|
+
# This demonstrates that beta depends on the chosen input b
|
|
1535
|
+
self.assertNotEqual(beta, beta2,
|
|
1536
|
+
"Beta should differ for different receiver inputs (security property)")
|
|
1537
|
+
|
|
1538
|
+
def test_dkg_insufficient_honest_parties(self):
|
|
1539
|
+
"""Test that a party can identify malicious parties when multiple collude.
|
|
1540
|
+
|
|
1541
|
+
Run 2-of-3 DKG where 2 parties (party 2 and party 3) send invalid
|
|
1542
|
+
shares to party 1. Verify party 1 can identify both malicious parties.
|
|
1543
|
+
"""
|
|
1544
|
+
dkg = DKLS23_DKG(self.group, threshold=2, num_parties=3)
|
|
1545
|
+
session_id = b"test-session-insufficient-honest"
|
|
1546
|
+
|
|
1547
|
+
# Round 1: Each party generates secret and Feldman commitments
|
|
1548
|
+
party_states = [dkg.keygen_round1(i+1, self.g, session_id) for i in range(3)]
|
|
1549
|
+
round1_msgs = [state[0] for state in party_states]
|
|
1550
|
+
private_states = [state[1] for state in party_states]
|
|
1551
|
+
|
|
1552
|
+
# Round 2: Generate shares for other parties
|
|
1553
|
+
round2_results = [dkg.keygen_round2(i+1, private_states[i], round1_msgs) for i in range(3)]
|
|
1554
|
+
shares_for_others = [r[0] for r in round2_results]
|
|
1555
|
+
states_r2 = [r[1] for r in round2_results]
|
|
1556
|
+
|
|
1557
|
+
# Tamper with both party 2's and party 3's shares to party 1
|
|
1558
|
+
one = self.group.init(ZR, 1)
|
|
1559
|
+
|
|
1560
|
+
# Party 2 sends bad share to party 1
|
|
1561
|
+
shares_for_others[1][1] = shares_for_others[1][1] + one
|
|
1562
|
+
|
|
1563
|
+
# Party 3 sends bad share to party 1
|
|
1564
|
+
shares_for_others[2][1] = shares_for_others[2][1] + one
|
|
1565
|
+
|
|
1566
|
+
# Collect shares for party 1
|
|
1567
|
+
received_shares_p1 = {sender+1: shares_for_others[sender][1] for sender in range(3)}
|
|
1568
|
+
|
|
1569
|
+
# Party 1 tries to complete round 3 - should detect first bad party via complaint
|
|
1570
|
+
# The API returns (KeyShare, complaint) where complaint identifies one bad party
|
|
1571
|
+
key_share, complaint = dkg.keygen_round3(1, states_r2[0], received_shares_p1, round1_msgs)
|
|
1572
|
+
|
|
1573
|
+
# First complaint should be generated (either for party 2 or party 3, whichever is checked first)
|
|
1574
|
+
self.assertIsNone(key_share, "Key share should be None when bad share detected")
|
|
1575
|
+
self.assertIsNotNone(complaint, "Complaint should be generated for bad share")
|
|
1576
|
+
|
|
1577
|
+
# To identify ALL malicious parties, we verify each share individually
|
|
1578
|
+
malicious_parties = []
|
|
1579
|
+
|
|
1580
|
+
for sender_id in [1, 2, 3]:
|
|
1581
|
+
share = received_shares_p1[sender_id]
|
|
1582
|
+
commitments = round1_msgs[sender_id - 1]['commitments']
|
|
1583
|
+
# Use the internal verification method
|
|
1584
|
+
is_valid = dkg._verify_share_against_commitments(
|
|
1585
|
+
sender_id, 1, share, commitments, self.g
|
|
1586
|
+
)
|
|
1587
|
+
if not is_valid:
|
|
1588
|
+
malicious_parties.append(sender_id)
|
|
1589
|
+
|
|
1590
|
+
# Both party 2 and party 3 should be identified as malicious
|
|
1591
|
+
self.assertIn(2, malicious_parties, "Party 2 should be identified as malicious")
|
|
1592
|
+
self.assertIn(3, malicious_parties, "Party 3 should be identified as malicious")
|
|
1593
|
+
self.assertNotIn(1, malicious_parties, "Party 1's share should be valid")
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
class TestDPF(unittest.TestCase):
|
|
1597
|
+
"""Tests for Distributed Point Function (GGM-based)"""
|
|
1598
|
+
|
|
1599
|
+
def test_dpf_single_point(self):
|
|
1600
|
+
"""Test DPF correctness at target point."""
|
|
1601
|
+
dpf = DPF(security_param=128, domain_bits=8)
|
|
1602
|
+
alpha, beta = 42, 12345
|
|
1603
|
+
k0, k1 = dpf.gen(alpha, beta)
|
|
1604
|
+
|
|
1605
|
+
# At target point, sum should equal beta
|
|
1606
|
+
y0 = dpf.eval(0, k0, alpha)
|
|
1607
|
+
y1 = dpf.eval(1, k1, alpha)
|
|
1608
|
+
self.assertEqual((y0 + y1) % (2**64), beta)
|
|
1609
|
+
|
|
1610
|
+
def test_dpf_off_points(self):
|
|
1611
|
+
"""Test DPF correctness at non-target points."""
|
|
1612
|
+
dpf = DPF(security_param=128, domain_bits=8)
|
|
1613
|
+
alpha, beta = 42, 12345
|
|
1614
|
+
k0, k1 = dpf.gen(alpha, beta)
|
|
1615
|
+
|
|
1616
|
+
# At non-target points, sum should be 0
|
|
1617
|
+
for x in [0, 10, 41, 43, 100, 255]:
|
|
1618
|
+
y0 = dpf.eval(0, k0, x)
|
|
1619
|
+
y1 = dpf.eval(1, k1, x)
|
|
1620
|
+
self.assertEqual((y0 + y1) % (2**64), 0, f"DPF should be 0 at x={x}")
|
|
1621
|
+
|
|
1622
|
+
def test_dpf_full_eval(self):
|
|
1623
|
+
"""Test DPF full domain evaluation."""
|
|
1624
|
+
dpf = DPF(security_param=128, domain_bits=6) # Domain size 64
|
|
1625
|
+
alpha, beta = 20, 99999
|
|
1626
|
+
k0, k1 = dpf.gen(alpha, beta)
|
|
1627
|
+
|
|
1628
|
+
result0 = dpf.full_eval(0, k0)
|
|
1629
|
+
result1 = dpf.full_eval(1, k1)
|
|
1630
|
+
|
|
1631
|
+
for i in range(64):
|
|
1632
|
+
expected = beta if i == alpha else 0
|
|
1633
|
+
actual = (result0[i] + result1[i]) % (2**64)
|
|
1634
|
+
self.assertEqual(actual, expected, f"DPF full_eval wrong at i={i}")
|
|
1635
|
+
|
|
1636
|
+
def test_dpf_key_independence(self):
|
|
1637
|
+
"""Test that individual keys reveal nothing about alpha/beta."""
|
|
1638
|
+
dpf = DPF(security_param=128, domain_bits=8)
|
|
1639
|
+
|
|
1640
|
+
# Generate two DPFs with different targets
|
|
1641
|
+
k0_a, k1_a = dpf.gen(10, 100)
|
|
1642
|
+
k0_b, k1_b = dpf.gen(20, 200)
|
|
1643
|
+
|
|
1644
|
+
# Each party's key alone gives pseudorandom-looking values
|
|
1645
|
+
v0_a = dpf.eval(0, k0_a, 10)
|
|
1646
|
+
v0_b = dpf.eval(0, k0_b, 10)
|
|
1647
|
+
|
|
1648
|
+
# Values should not reveal target (both look random)
|
|
1649
|
+
self.assertIsInstance(v0_a, int)
|
|
1650
|
+
self.assertIsInstance(v0_b, int)
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
class TestMPFSS(unittest.TestCase):
|
|
1654
|
+
"""Tests for Multi-Point Function Secret Sharing"""
|
|
1655
|
+
|
|
1656
|
+
def test_mpfss_single_point(self):
|
|
1657
|
+
"""Test MPFSS with single point (should match DPF)."""
|
|
1658
|
+
mpfss = MPFSS(security_param=128, domain_bits=10)
|
|
1659
|
+
points = [(100, 5000)]
|
|
1660
|
+
k0, k1 = mpfss.gen(points)
|
|
1661
|
+
|
|
1662
|
+
# At target point
|
|
1663
|
+
v0 = mpfss.eval(0, k0, 100)
|
|
1664
|
+
v1 = mpfss.eval(1, k1, 100)
|
|
1665
|
+
self.assertEqual((v0 + v1) % (2**64), 5000)
|
|
1666
|
+
|
|
1667
|
+
# At other point
|
|
1668
|
+
v0_other = mpfss.eval(0, k0, 50)
|
|
1669
|
+
v1_other = mpfss.eval(1, k1, 50)
|
|
1670
|
+
self.assertEqual((v0_other + v1_other) % (2**64), 0)
|
|
1671
|
+
|
|
1672
|
+
def test_mpfss_multiple_points(self):
|
|
1673
|
+
"""Test MPFSS with multiple points."""
|
|
1674
|
+
mpfss = MPFSS(security_param=128, domain_bits=8)
|
|
1675
|
+
points = [(10, 100), (20, 200), (30, 300)]
|
|
1676
|
+
k0, k1 = mpfss.gen(points)
|
|
1677
|
+
|
|
1678
|
+
# Check all target points
|
|
1679
|
+
for alpha, expected in points:
|
|
1680
|
+
v0 = mpfss.eval(0, k0, alpha)
|
|
1681
|
+
v1 = mpfss.eval(1, k1, alpha)
|
|
1682
|
+
self.assertEqual((v0 + v1) % (2**64), expected, f"MPFSS wrong at {alpha}")
|
|
1683
|
+
|
|
1684
|
+
# Check non-target points
|
|
1685
|
+
for x in [0, 15, 25, 100, 255]:
|
|
1686
|
+
v0 = mpfss.eval(0, k0, x)
|
|
1687
|
+
v1 = mpfss.eval(1, k1, x)
|
|
1688
|
+
self.assertEqual((v0 + v1) % (2**64), 0, f"MPFSS should be 0 at {x}")
|
|
1689
|
+
|
|
1690
|
+
def test_mpfss_full_eval(self):
|
|
1691
|
+
"""Test MPFSS full domain evaluation."""
|
|
1692
|
+
mpfss = MPFSS(security_param=128, domain_bits=6) # Domain 64
|
|
1693
|
+
points = [(5, 50), (10, 100), (60, 600)]
|
|
1694
|
+
k0, k1 = mpfss.gen(points)
|
|
1695
|
+
|
|
1696
|
+
result0 = mpfss.full_eval(0, k0)
|
|
1697
|
+
result1 = mpfss.full_eval(1, k1)
|
|
1698
|
+
|
|
1699
|
+
point_dict = dict(points)
|
|
1700
|
+
for i in range(64):
|
|
1701
|
+
expected = point_dict.get(i, 0)
|
|
1702
|
+
actual = (result0[i] + result1[i]) % (2**64)
|
|
1703
|
+
self.assertEqual(actual, expected, f"MPFSS full_eval wrong at {i}")
|
|
1704
|
+
|
|
1705
|
+
def test_mpfss_empty(self):
|
|
1706
|
+
"""Test MPFSS with empty point set."""
|
|
1707
|
+
mpfss = MPFSS(security_param=128, domain_bits=8)
|
|
1708
|
+
k0, k1 = mpfss.gen([])
|
|
1709
|
+
|
|
1710
|
+
# Should be all zeros
|
|
1711
|
+
result0 = mpfss.full_eval(0, k0)
|
|
1712
|
+
result1 = mpfss.full_eval(1, k1)
|
|
1713
|
+
|
|
1714
|
+
for i in range(10):
|
|
1715
|
+
self.assertEqual((result0[i] + result1[i]) % (2**64), 0)
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
class TestSilentOT(unittest.TestCase):
|
|
1719
|
+
"""Tests for Silent OT Extension (PCG-based)"""
|
|
1720
|
+
|
|
1721
|
+
def test_silent_ot_basic(self):
|
|
1722
|
+
"""Test basic Silent OT correctness."""
|
|
1723
|
+
sot = SilentOT(security_param=128, output_size=32, sparsity=4)
|
|
1724
|
+
seed_sender, seed_receiver = sot.gen()
|
|
1725
|
+
|
|
1726
|
+
choice_bits, sender_msgs = sot.expand_sender(seed_sender)
|
|
1727
|
+
receiver_msgs = sot.expand_receiver(seed_receiver)
|
|
1728
|
+
|
|
1729
|
+
self.assertEqual(len(choice_bits), 32)
|
|
1730
|
+
self.assertEqual(len(sender_msgs), 32)
|
|
1731
|
+
self.assertEqual(len(receiver_msgs), 32)
|
|
1732
|
+
|
|
1733
|
+
# Verify OT correlation
|
|
1734
|
+
for i in range(32):
|
|
1735
|
+
c = choice_bits[i]
|
|
1736
|
+
self.assertEqual(sender_msgs[i], receiver_msgs[i][c],
|
|
1737
|
+
f"OT correlation failed at i={i}, c={c}")
|
|
1738
|
+
|
|
1739
|
+
def test_silent_ot_larger(self):
|
|
1740
|
+
"""Test Silent OT with larger output size."""
|
|
1741
|
+
sot = SilentOT(security_param=128, output_size=128, sparsity=10)
|
|
1742
|
+
seed_sender, seed_receiver = sot.gen()
|
|
1743
|
+
|
|
1744
|
+
choice_bits, sender_msgs = sot.expand_sender(seed_sender)
|
|
1745
|
+
receiver_msgs = sot.expand_receiver(seed_receiver)
|
|
1746
|
+
|
|
1747
|
+
# Verify OT correlation for all positions
|
|
1748
|
+
for i in range(128):
|
|
1749
|
+
c = choice_bits[i]
|
|
1750
|
+
self.assertEqual(sender_msgs[i], receiver_msgs[i][c],
|
|
1751
|
+
f"OT correlation failed at i={i}")
|
|
1752
|
+
|
|
1753
|
+
def test_silent_ot_choice_distribution(self):
|
|
1754
|
+
"""Test that choice bits come from sparse set."""
|
|
1755
|
+
sot = SilentOT(security_param=128, output_size=64, sparsity=8)
|
|
1756
|
+
seed_sender, _ = sot.gen()
|
|
1757
|
+
|
|
1758
|
+
choice_bits, _ = sot.expand_sender(seed_sender)
|
|
1759
|
+
|
|
1760
|
+
# Count 1s - should be exactly sparsity
|
|
1761
|
+
ones_count = sum(choice_bits)
|
|
1762
|
+
self.assertEqual(ones_count, 8, "Should have exactly 'sparsity' 1-bits")
|
|
1763
|
+
|
|
1764
|
+
def test_silent_ot_messages_32_bytes(self):
|
|
1765
|
+
"""Test that OT messages are 32 bytes each."""
|
|
1766
|
+
sot = SilentOT(security_param=128, output_size=16, sparsity=4)
|
|
1767
|
+
seed_sender, seed_receiver = sot.gen()
|
|
1768
|
+
|
|
1769
|
+
_, sender_msgs = sot.expand_sender(seed_sender)
|
|
1770
|
+
receiver_msgs = sot.expand_receiver(seed_receiver)
|
|
1771
|
+
|
|
1772
|
+
for msg in sender_msgs:
|
|
1773
|
+
self.assertEqual(len(msg), 32, "Sender msg should be 32 bytes")
|
|
1774
|
+
|
|
1775
|
+
for m0, m1 in receiver_msgs:
|
|
1776
|
+
self.assertEqual(len(m0), 32, "Receiver m0 should be 32 bytes")
|
|
1777
|
+
self.assertEqual(len(m1), 32, "Receiver m1 should be 32 bytes")
|
|
1778
|
+
|
|
1779
|
+
def test_silent_ot_different_messages(self):
|
|
1780
|
+
"""Test that m0 and m1 are different for each OT."""
|
|
1781
|
+
sot = SilentOT(security_param=128, output_size=32, sparsity=4)
|
|
1782
|
+
_, seed_receiver = sot.gen()
|
|
1783
|
+
|
|
1784
|
+
receiver_msgs = sot.expand_receiver(seed_receiver)
|
|
1785
|
+
|
|
1786
|
+
# m0 and m1 should be different for each OT
|
|
1787
|
+
for i, (m0, m1) in enumerate(receiver_msgs):
|
|
1788
|
+
self.assertNotEqual(m0, m1, f"m0 and m1 should differ at i={i}")
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
if __name__ == '__main__':
|
|
1792
|
+
unittest.main()
|