diff --git a/CMakeLists.txt b/CMakeLists.txt index 638ca95c..e5be0441 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16) project( CBMPC - VERSION 0.1.0 + VERSION 0.3.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) @@ -122,7 +122,7 @@ if(NOT DEFINED CBMPC_OPENSSL_ROOT) if(DEFINED ENV{CBMPC_OPENSSL_ROOT}) set(CBMPC_OPENSSL_ROOT $ENV{CBMPC_OPENSSL_ROOT}) else() - set(CBMPC_OPENSSL_ROOT "/usr/local/opt/openssl@3.6.3") + set(CBMPC_OPENSSL_ROOT "/usr/local/opt/openssl@3.6.4") endif() endif() diff --git a/Dockerfile b/Dockerfile index 6fb094ae..745cdd78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,9 +37,9 @@ WORKDIR /build COPY scripts/openssl/build-static-openssl-linux.sh . RUN sh build-static-openssl-linux.sh \ && mkdir -p /usr/local/lib64 /usr/local/lib /usr/local/include \ - && ln -sf /usr/local/opt/openssl@3.6.1/lib64/libcrypto.a /usr/local/lib64/libcrypto.a \ - && ln -sf /usr/local/opt/openssl@3.6.1/lib64/libcrypto.a /usr/local/lib/libcrypto.a \ - && ln -sf /usr/local/opt/openssl@3.6.1/include/openssl /usr/local/include/openssl \ + && ln -sf /usr/local/opt/openssl@3.6.4/lib64/libcrypto.a /usr/local/lib64/libcrypto.a \ + && ln -sf /usr/local/opt/openssl@3.6.4/lib64/libcrypto.a /usr/local/lib/libcrypto.a \ + && ln -sf /usr/local/opt/openssl@3.6.4/include/openssl /usr/local/include/openssl \ && rm -rf /build WORKDIR /code diff --git a/README.md b/README.md index 58caea41..d12bac91 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ There are three build modes available: ### OpenSSL -The library depends on a **custom build of OpenSSL 3.6.3** with specific modifications (see [External Dependencies](#external-dependencies)). You must build this custom version before compiling the library. +The library depends on a **custom build of OpenSSL 3.6.4** with specific modifications (see [External Dependencies](#external-dependencies)). You must build this custom version before compiling the library. **Quick Start:** ```bash @@ -190,7 +190,7 @@ scripts/openssl/build-static-openssl-macos.sh # for x86_64 scripts/openssl/build-static-openssl-macos-m1.sh # for ARM64 ``` -**Note:** These scripts install OpenSSL to `/usr/local/opt/openssl@3.6.3` and may require `sudo` permission. +**Note:** These scripts install OpenSSL to `/usr/local/opt/openssl@3.6.4` and may require `sudo` permission. **Custom Install Location:** If you prefer a different installation path, you can set the `CBMPC_OPENSSL_ROOT` variable: diff --git a/cmake/openssl.cmake b/cmake/openssl.cmake index 86a8b33f..70d4497a 100644 --- a/cmake/openssl.cmake +++ b/cmake/openssl.cmake @@ -4,7 +4,7 @@ # The OpenSSL path can be customized via: # 1. CMake variable: -DCBMPC_OPENSSL_ROOT=/path/to/openssl # 2. Environment variable: export CBMPC_OPENSSL_ROOT=/path/to/openssl -# 3. Default: /usr/local/opt/openssl@3.6.3 +# 3. Default: /usr/local/opt/openssl@3.6.4 # # To build the custom OpenSSL, run the appropriate script: # - macOS (x86_64): scripts/openssl/build-static-openssl-macos.sh @@ -16,7 +16,7 @@ macro(link_openssl TARGET_NAME) if(DEFINED ENV{CBMPC_OPENSSL_ROOT}) set(CBMPC_OPENSSL_ROOT $ENV{CBMPC_OPENSSL_ROOT}) else() - set(CBMPC_OPENSSL_ROOT "/usr/local/opt/openssl@3.6.3") + set(CBMPC_OPENSSL_ROOT "/usr/local/opt/openssl@3.6.4") endif() endif() diff --git a/include-internal/cbmpc/internal/crypto/base_bn.h b/include-internal/cbmpc/internal/crypto/base_bn.h index cce596cc..ec1ffe97 100644 --- a/include-internal/cbmpc/internal/crypto/base_bn.h +++ b/include-internal/cbmpc/internal/crypto/base_bn.h @@ -7,8 +7,8 @@ #include -#if OPENSSL_VERSION_MAJOR != 3 || OPENSSL_VERSION_MINOR != 6 || OPENSSL_VERSION_PATCH != 3 -#error "cb-mpc copied OpenSSL BN internals require OpenSSL 3.6.3" +#if OPENSSL_VERSION_MAJOR != 3 || OPENSSL_VERSION_MINOR != 6 || OPENSSL_VERSION_PATCH != 4 +#error "cb-mpc copied OpenSSL BN internals require OpenSSL 3.6.4" #endif struct bignum_st { diff --git a/include-internal/cbmpc/internal/protocol/mpc_job.h b/include-internal/cbmpc/internal/protocol/mpc_job.h index 6536ef7a..5cbb61ad 100644 --- a/include-internal/cbmpc/internal/protocol/mpc_job.h +++ b/include-internal/cbmpc/internal/protocol/mpc_job.h @@ -144,14 +144,16 @@ class job_mp_t { return unpack_msgs(std::get<1>(msg_ctx), bufs, std::get<2>(msg_ctx)); } template - void unpack_multi_sets_msgs_helper(Tuple1& t1, Tuple2& t2, std::index_sequence) { + error_t unpack_multi_sets_msgs_helper(Tuple1& t1, Tuple2& t2, std::index_sequence) { + error_t rv = UNINITIALIZED_ERROR; // Use the index sequence to access elements from both tuples - (unpack_msg_ctx(std::get(t1), std::get(t2)), ...); + ((rv = unpack_msg_ctx(std::get(t1), std::get(t2))) || ...); + return rv; } template - void unpack_multi_sets_tupled_msgs(std::tuple& t1, std::tuple t2) { + error_t unpack_multi_sets_tupled_msgs(std::tuple& t1, std::tuple t2) { static_assert(sizeof...(Ts1) == sizeof...(Ts2), "Tuples must have the same length to unpack"); - unpack_multi_sets_msgs_helper(t1, t2, std::index_sequence_for{}); + return unpack_multi_sets_msgs_helper(t1, t2, std::index_sequence_for{}); } /* functions to send and received serialized multi-party messages */ @@ -291,6 +293,7 @@ class job_mp_t { // multiple times. template error_t group_message(const MSG_TUPLES&... msg_tuples) { + static_assert(sizeof...(MSG_TUPLES) > 0, "group_message requires at least one message tuple"); error_t rv = UNINITIALIZED_ERROR; auto packed_msgs_tuple = std::make_tuple(pack_multi_sets_msgs(msg_tuples)...); std::vector packed_msgs = combine_packed_msgs(packed_msgs_tuple); @@ -299,7 +302,7 @@ class job_mp_t { if (rv = receive_from_parties(party_set_t::all(), packed_msgs)) return rv; if (rv = split_packed_msgs(packed_msgs, packed_msgs_tuple)) return rv; - unpack_multi_sets_tupled_msgs(packed_msgs_tuple, std::tie(msg_tuples...)); + if (rv = unpack_multi_sets_tupled_msgs(packed_msgs_tuple, std::tie(msg_tuples...))) return rv; return SUCCESS; } diff --git a/include/cbmpc/api/tdh2.h b/include/cbmpc/api/tdh2.h index de6dbf84..034c1716 100644 --- a/include/cbmpc/api/tdh2.h +++ b/include/cbmpc/api/tdh2.h @@ -53,6 +53,10 @@ error_t verify(mem_t public_key, mem_t ciphertext, mem_t label); error_t partial_decrypt(mem_t private_share, mem_t ciphertext, mem_t label, buf_t& partial_decryption); // Combine additive shares + partial decryptions to decrypt. +// +// `public_shares` must be the complete, ordered public-share output from the +// same DKG operation as `public_key`. The function rejects a share set whose +// sum does not equal the public key. error_t combine_additive(mem_t public_key, const std::vector& public_shares, mem_t label, const std::vector& partial_decryptions, mem_t ciphertext, buf_t& plaintext); @@ -67,6 +71,9 @@ error_t combine_additive(mem_t public_key, const std::vector& public_shar // - `party_names.size() == public_shares.size()` // - `partial_decryption_party_names.size() == partial_decryptions.size()` // - The leaf set of `access_structure` must match `party_names` exactly. +// - `public_key`, `public_shares`, and `access_structure` must correspond to +// the same DKG operation. The public shares selected by the partial- +// decryption quorum must reconstruct to the public key. error_t combine_ac(const access_structure_t& access_structure, mem_t public_key, const std::vector& party_names, const std::vector& public_shares, mem_t label, const std::vector& partial_decryption_party_names, diff --git a/include/cbmpc/c_api/tdh2.h b/include/cbmpc/c_api/tdh2.h index 9f824331..60001053 100644 --- a/include/cbmpc/c_api/tdh2.h +++ b/include/cbmpc/c_api/tdh2.h @@ -61,6 +61,14 @@ cbmpc_error_t cbmpc_tdh2_partial_decrypt(cmem_t private_share, cmem_t ciphertext // Combine additive shares / partial decryptions to recover the plaintext. // +// Requirements: +// - `public_shares` must be the complete, ordered public-share output from the +// same `cbmpc_tdh2_dkg_additive` operation as `public_key`. +// - `partial_decryptions` must contain exactly one partial decryption for each +// role represented in `public_shares`. +// - The function returns an error if the public shares do not sum to the public +// key or if a partial-decryption role is missing or duplicated. +// // Ownership: // - On success, `out_plaintext->data` is allocated by the library and must be // freed with `cbmpc_cmem_free(*out_plaintext)`. @@ -78,6 +86,10 @@ cbmpc_error_t cbmpc_tdh2_combine_additive(cmem_t public_key, cmems_t public_shar // - `party_names_count == public_shares.count` // - `partial_decryption_party_names_count == partial_decryptions.count` // - The leaf set of `access_structure` must match `party_names` exactly. +// - `public_key`, `public_shares`, and `access_structure` must correspond to +// the same `cbmpc_tdh2_dkg_ac` operation. +// - The public shares selected by the partial-decryption quorum must +// reconstruct to the public key; otherwise, the function returns an error. // // Ownership: same as `cbmpc_tdh2_combine_additive`. cbmpc_error_t cbmpc_tdh2_combine_ac(const cbmpc_access_structure_t* access_structure, cmem_t public_key, diff --git a/scripts/openssl/build-static-openssl-linux.sh b/scripts/openssl/build-static-openssl-linux.sh index 8201edbf..bcc5fa2d 100755 --- a/scripts/openssl/build-static-openssl-linux.sh +++ b/scripts/openssl/build-static-openssl-linux.sh @@ -3,9 +3,9 @@ set -e cd /tmp -curl -L https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz --output openssl-3.6.3.tar.gz -expectedHash='243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1' -fileHash=$(sha256sum openssl-3.6.3.tar.gz | cut -d " " -f 1 ) +curl -L https://github.com/openssl/openssl/releases/download/openssl-3.6.4/openssl-3.6.4.tar.gz --output openssl-3.6.4.tar.gz +expectedHash='9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef' +fileHash=$(sha256sum openssl-3.6.4.tar.gz | cut -d " " -f 1 ) if [ $expectedHash != $fileHash ] then @@ -18,8 +18,8 @@ fi echo "LINUX Start" uname -r -tar -xzf openssl-3.6.3.tar.gz -cd openssl-3.6.3 +tar -xzf openssl-3.6.4.tar.gz +cd openssl-3.6.4 sed -i -e 's/^static//' crypto/ec/curve25519.c @@ -29,7 +29,7 @@ sed -i -e 's/^static//' crypto/ec/curve25519.c no-gost no-http no-idea no-mdc2 no-md2 no-md4 no-module no-nextprotoneg no-ocb no-ocsp no-psk no-padlockeng no-poly1305 \ no-quic no-rc2 no-rc4 no-rc5 no-rfc3779 no-scrypt no-sctp no-seed no-siphash no-sm2 no-sm3 no-sm4 no-sock no-srtp no-srp \ no-ssl-trace no-ssl3 no-stdio no-tests no-tls no-ts no-unit-test no-uplink no-whirlpool no-zlib \ - --prefix="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.3}" --libdir=lib64 + --prefix="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.4}" --libdir=lib64 make build_generated install_sw -j4 diff --git a/scripts/openssl/build-static-openssl-macos-m1.sh b/scripts/openssl/build-static-openssl-macos-m1.sh index 82a1f913..49c12765 100755 --- a/scripts/openssl/build-static-openssl-macos-m1.sh +++ b/scripts/openssl/build-static-openssl-macos-m1.sh @@ -20,9 +20,9 @@ fi export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-16.0}" cd /tmp -curl -L https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz --output openssl-3.6.3.tar.gz -expectedHash='243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1' -fileHash=$(sha256sum openssl-3.6.3.tar.gz | cut -d " " -f 1 ) +curl -L https://github.com/openssl/openssl/releases/download/openssl-3.6.4/openssl-3.6.4.tar.gz --output openssl-3.6.4.tar.gz +expectedHash='9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef' +fileHash=$(sha256sum openssl-3.6.4.tar.gz | cut -d " " -f 1 ) if [ $expectedHash != $fileHash ] @@ -34,8 +34,8 @@ then fi -tar -xzf openssl-3.6.3.tar.gz -cd openssl-3.6.3 +tar -xzf openssl-3.6.4.tar.gz +cd openssl-3.6.4 sed -i -e 's/^static//' crypto/ec/curve25519.c @@ -45,7 +45,7 @@ sed -i -e 's/^static//' crypto/ec/curve25519.c no-gost no-http no-idea no-mdc2 no-md2 no-md4 no-module no-nextprotoneg no-ocb no-ocsp no-psk no-padlockeng no-poly1305 \ no-quic no-rc2 no-rc4 no-rc5 no-rfc3779 no-scrypt no-sctp no-seed no-siphash no-sm2 no-sm3 no-sm4 no-sock no-srtp no-srp \ no-ssl-trace no-ssl3 no-stdio no-tests no-tls no-ts no-unit-test no-uplink no-whirlpool no-zlib \ - --prefix="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.3}" darwin64-arm64-cc + --prefix="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.4}" darwin64-arm64-cc make -j make install_sw diff --git a/scripts/openssl/build-static-openssl-macos.sh b/scripts/openssl/build-static-openssl-macos.sh index 2bf303e4..87eb9efd 100755 --- a/scripts/openssl/build-static-openssl-macos.sh +++ b/scripts/openssl/build-static-openssl-macos.sh @@ -20,9 +20,9 @@ fi export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-16.0}" cd /tmp -curl -L https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz --output openssl-3.6.3.tar.gz -expectedHash='243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1' -fileHash=$(sha256sum openssl-3.6.3.tar.gz | cut -d " " -f 1 ) +curl -L https://github.com/openssl/openssl/releases/download/openssl-3.6.4/openssl-3.6.4.tar.gz --output openssl-3.6.4.tar.gz +expectedHash='9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef' +fileHash=$(sha256sum openssl-3.6.4.tar.gz | cut -d " " -f 1 ) if [ $expectedHash != $fileHash ] then @@ -33,8 +33,8 @@ then fi -tar -xzf openssl-3.6.3.tar.gz -cd openssl-3.6.3 +tar -xzf openssl-3.6.4.tar.gz +cd openssl-3.6.4 sed -i -e 's/^static//' crypto/ec/curve25519.c @@ -44,7 +44,7 @@ sed -i -e 's/^static//' crypto/ec/curve25519.c no-gost no-http no-idea no-mdc2 no-md2 no-md4 no-module no-nextprotoneg no-ocb no-ocsp no-psk no-padlockeng no-poly1305 \ no-quic no-rc2 no-rc4 no-rc5 no-rfc3779 no-scrypt no-sctp no-seed no-siphash no-sm2 no-sm3 no-sm4 no-sock no-srtp no-srp \ no-ssl-trace no-ssl3 no-stdio no-tests no-tls no-ts no-unit-test no-uplink no-whirlpool no-zlib \ - --prefix="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.3}" darwin64-x86_64-cc + --prefix="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.4}" darwin64-x86_64-cc make -j make install_sw diff --git a/scripts/run-demos.sh b/scripts/run-demos.sh index 3152afa9..4ac2668a 100755 --- a/scripts/run-demos.sh +++ b/scripts/run-demos.sh @@ -17,7 +17,7 @@ CBMPC_PREFIX_PUBLIC="${CBMPC_PREFIX_PUBLIC:-${ROOT_PATH}/build/install/public}" CBMPC_PREFIX_FULL="${CBMPC_PREFIX_FULL:-${ROOT_PATH}/build/install/full}" # OpenSSL path is used by demos (C++ via CMake; Go via CGO_LDFLAGS). Keep it # configurable and consistent with `cmake/openssl.cmake`. -CBMPC_OPENSSL_ROOT="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.3}" +CBMPC_OPENSSL_ROOT="${CBMPC_OPENSSL_ROOT:-/usr/local/opt/openssl@3.6.4}" CPP_DEMOS=("basic_primitive" "zk" "parallel_transport") API_DEMOS=("pve" "hd_keyset_ecdsa_2p" "ecdsa_mp_pve_backup" "schnorr_2p_pve_batch_backup") diff --git a/src/cbmpc/crypto/tdh2.cpp b/src/cbmpc/crypto/tdh2.cpp index 25d41a85..ce13c1ca 100644 --- a/src/cbmpc/crypto/tdh2.cpp +++ b/src/cbmpc/crypto/tdh2.cpp @@ -130,19 +130,25 @@ error_t combine_additive(const public_key_t& pub_key, const pub_shares_t& Qi, me error_t rv = UNINITIALIZED_ERROR; const auto& curve = pub_key.Q.get_curve(); int n = int(Qi.size()); + ecc_point_t reconstructed_Q = curve.infinity(); for (const auto& _Qi : Qi) { if (rv = curve.check(_Qi)) return coinbase::error(rv, "combine_additive: check Qi failed"); + reconstructed_Q += _Qi; } + if (reconstructed_Q != pub_key.Q) return coinbase::error(E_CRYPTO, "combine_additive: public shares mismatch"); if ((int)partial_decryptions.size() != n) return coinbase::error(E_CRYPTO); if (rv = ciphertext.verify(pub_key, label)) return rv; ecc_point_t V = curve.infinity(); + std::vector seen_rids(n, false); for (int i = 0; i < n; i++) { const partial_decryption_t& partial_decryption = partial_decryptions[i]; const int rid = partial_decryption.rid; if (rid < 1 || rid > n) return coinbase::error(E_CRYPTO); + if (seen_rids[rid - 1]) return coinbase::error(E_CRYPTO, "combine_additive: duplicate rid"); + seen_rids[rid - 1] = true; if (rv = partial_decryption.check_partial_decryption_helper(Qi[rid - 1], ciphertext, curve)) return rv; V += partial_decryption.Xi; @@ -161,9 +167,21 @@ error_t combine(const ss::ac_t& ac, const public_key_t& pub_key, ss::ac_pub_shar if (rv = ciphertext.verify(pub_key, label)) return rv; + ss::ac_pub_shares_t selected_pub_shares; + for (const auto& [name, partial_decryption] : partial_decryptions) { + const auto it = pub_shares.find(name); + if (it == pub_shares.end()) return coinbase::error(E_CRYPTO, "combine: missing public share"); + selected_pub_shares[name] = it->second; + } + + ecc_point_t reconstructed_Q; + if (rv = ac.reconstruct_exponent(selected_pub_shares, reconstructed_Q)) return rv; + if (reconstructed_Q != pub_key.Q) return coinbase::error(E_CRYPTO, "combine: public shares mismatch"); + ss::ac_pub_shares_t Vs; for (const auto& [name, partial_decryption] : partial_decryptions) { - if (rv = partial_decryption.check_partial_decryption_helper(pub_shares[name], ciphertext, pub_key.Q.get_curve())) + if (rv = partial_decryption.check_partial_decryption_helper(selected_pub_shares.at(name), ciphertext, + pub_key.Q.get_curve())) return rv; if (rv = pub_key.Q.get_curve().check(partial_decryption.Xi)) return rv; diff --git a/src/cbmpc/protocol/ecdsa_mp.cpp b/src/cbmpc/protocol/ecdsa_mp.cpp index edbae884..e425f5b3 100644 --- a/src/cbmpc/protocol/ecdsa_mp.cpp +++ b/src/cbmpc/protocol/ecdsa_mp.cpp @@ -228,18 +228,18 @@ error_t sign(job_mp_t& job, key_t& key, mem_t msg, const party_idx_t sig_receive for (int t = 0; t < 4; t++) X[l][j][t] = bn_t::from_bin(X_bin[l * 4 + t]); } - // Initialize the view - crypto::sha256_t view; - view.update(E_i, eK_i._js, eRHO_i._js, pi_eK._js, pi_eRHO._js); - - // Proceed with message 4 of the signing protocol + // Validate received round-3 broadcast payloads before hashing them into the transcript. for (int j = 0; j < n; j++) { if (i == j) continue; - // The check for validating eK_i and eRHO_i is done in the verify function if (rv = pi_eK._j.verify(E, eK_i._j, sid, n_uc_elgamal_com_proofs * j + 0)) return rv; if (rv = pi_eRHO._j.verify(E, eRHO_i._j, sid, n_uc_elgamal_com_proofs * j + 1)) return rv; } + // Initialize the view + crypto::sha256_t view; + view.update(E_i, eK_i._js, eRHO_i._js, pi_eK._js, pi_eRHO._js); + + // Proceed with message 4 of the signing protocol auto seed = job.nonuniform_msg(); auto v_theta = job.nonuniform_msg>(); diff --git a/src/cbmpc/protocol/ot.cpp b/src/cbmpc/protocol/ot.cpp index c8031229..dffa993a 100644 --- a/src/cbmpc/protocol/ot.cpp +++ b/src/cbmpc/protocol/ot.cpp @@ -44,6 +44,11 @@ error_t base_ot_protocol_pvw_ctx_t::step2_S2R(const std::vector& x0, cons this->x0 = x0; this->x1 = x1; + for (int i = 0; i < m; i++) { + if (coinbase::bytes_to_bits(x0[i].size()) != l || coinbase::bytes_to_bits(x1[i].size()) != l) + return coinbase::error(E_BADARG, "base_ot_protocol_pvw_ctx_t::step2_S2R: x0/x1 size mismatch"); + } + const mod_t& q = curve.order(); ecc_point_t G0, G1, H0, H1; G0 = curve.generator(); @@ -86,6 +91,8 @@ error_t base_ot_protocol_pvw_ctx_t::output_R(std::vector& x) { x.resize(m); for (int i = 0; i < m; i++) { + if (coinbase::bytes_to_bits(V0[i].size()) != l || coinbase::bytes_to_bits(V1[i].size()) != l) + return coinbase::error(E_FORMAT, "base_ot_protocol_pvw_ctx_t::output_R: V0/V1 size mismatch"); if (rv = curve.check(U0[i])) return coinbase::error(rv, "base_ot_protocol_pvw_ctx_t::output_R: check U0[i] failed"); if (rv = curve.check(U1[i])) return coinbase::error(rv, "base_ot_protocol_pvw_ctx_t::output_R: check U1[i] failed"); // Optimization: if the curve backend supports constant-time point cmov, select U_b and V_b and do diff --git a/tests/unit/api/test_ecdsa_mp_ac.cpp b/tests/unit/api/test_ecdsa_mp_ac.cpp index 8e1c395d..b1f76a93 100644 --- a/tests/unit/api/test_ecdsa_mp_ac.cpp +++ b/tests/unit/api/test_ecdsa_mp_ac.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include "test_transport_harness.h" @@ -13,11 +15,13 @@ namespace { using coinbase::buf_t; using coinbase::error_t; +using coinbase::mem_t; using coinbase::api::curve_id; using coinbase::api::job_mp_t; using coinbase::api::party_idx_t; +using coinbase::api::data_transport_i; using coinbase::testutils::mpc_net_context_t; using coinbase::testutils::api_harness::failing_transport_t; using coinbase::testutils::api_harness::local_api_transport_t; @@ -698,6 +702,117 @@ TEST(ApiEcdsaMpAc, SignAcRejectsWrongAccessStructure) { EXPECT_TRUE(sig_der.empty()); } +namespace { + +static bool tamper_sign_round3_ek_l_curve_to_null(buf_t& msg) { + buf_t pairwise_msg; + buf_t broadcast_msg; + if (coinbase::deser(msg, pairwise_msg, broadcast_msg)) return false; + + elg_com_t eK; + elg_com_t eRHO; + coinbase::zk::uc_elgamal_com_t pi_eK; + coinbase::zk::uc_elgamal_com_t pi_eRHO; + if (coinbase::deser(broadcast_msg, eK, eRHO, pi_eK, pi_eRHO)) return false; + + eK.L = coinbase::crypto::ecc_point_t(); + broadcast_msg = coinbase::ser(eK, eRHO, pi_eK, pi_eRHO); + msg = coinbase::ser(pairwise_msg, broadcast_msg); + return true; +} + +class tamper_sign_round3_transport_t final : public data_transport_i { + public: + explicit tamper_sign_round3_transport_t(std::shared_ptr ctx) : ctx_(std::move(ctx)) {} + + error_t send(party_idx_t receiver, mem_t msg) override { + buf_t out(msg); + if (receiver == static_cast(0) && ++sends_to_victim_ == 4) { + if (!tamper_sign_round3_ek_l_curve_to_null(out)) return E_GENERAL; + tampered_ = true; + } + ctx_->send(receiver, out); + return SUCCESS; + } + + error_t receive(party_idx_t sender, buf_t& msg) override { return ctx_->receive(sender, msg); } + + error_t receive_all(const std::vector& senders, std::vector& msgs) override { + std::vector s; + s.reserve(senders.size()); + for (auto x : senders) s.push_back(static_cast(x)); + return ctx_->receive_all(s, msgs); + } + + bool tampered() const { return tampered_; } + + private: + std::shared_ptr ctx_; + int sends_to_victim_ = 0; + bool tampered_ = false; +}; + +} // namespace + +TEST(ApiEcdsaMpAc, SignRound3NullCurveElgComFromMaliciousPeerRejected) { + constexpr int n = 2; + + const std::vector> dkg_peers = make_peers(n); + const std::vector> dkg_transports = make_transports(dkg_peers); + + const std::vector names = {"p0", "p1"}; + std::vector name_views; + name_views.reserve(names.size()); + for (const auto& name : names) name_views.emplace_back(name); + + const coinbase::api::access_structure_t ac = + coinbase::api::access_structure_t::Threshold(2, { + coinbase::api::access_structure_t::leaf(names[0]), + coinbase::api::access_structure_t::leaf(names[1]), + }); + const std::vector quorum_party_names = {names[0], names[1]}; + + std::vector key_blobs(n); + std::vector sids(n); + std::vector rvs; + run_mp( + dkg_peers, + [&](int i) { + job_mp_t job{static_cast(i), name_views, *dkg_transports[static_cast(i)]}; + return coinbase::api::ecdsa_mp::dkg_ac(job, curve_id::secp256k1, sids[static_cast(i)], ac, + quorum_party_names, key_blobs[static_cast(i)]); + }, + rvs); + for (auto rv : rvs) ASSERT_EQ(rv, SUCCESS); + + const buf_t msg_hash = make_msg_hash32(); + + std::vector> sign_peers; + sign_peers.reserve(n); + for (int i = 0; i < n; i++) sign_peers.push_back(std::make_shared(i)); + for (const auto& p : sign_peers) p->init_with_peers(sign_peers); + + local_api_transport_t sign_t0(sign_peers[0]); + tamper_sign_round3_transport_t sign_t1(sign_peers[1]); + + std::vector sigs(n); + std::vector sign_rvs; + run_mp( + sign_peers, + [&](int i) { + job_mp_t job{static_cast(i), quorum_party_names, + i == 0 ? static_cast(sign_t0) : static_cast(sign_t1)}; + dylog_disable_scope_t no_log_err; + return coinbase::api::ecdsa_mp::sign_ac(job, key_blobs[static_cast(i)], ac, msg_hash, + /*sig_receiver=*/0, sigs[static_cast(i)]); + }, + sign_rvs); + + EXPECT_TRUE(sign_t1.tampered()); + EXPECT_EQ(sign_rvs[0], E_FORMAT); + EXPECT_EQ(sigs[0].size(), 0); +} + // ------------ Disclaimer: All the following tests have been generated by AI ------------ #include diff --git a/tests/unit/crypto/test_tdh2.cpp b/tests/unit/crypto/test_tdh2.cpp index da4591e7..32991188 100644 --- a/tests/unit/crypto/test_tdh2.cpp +++ b/tests/unit/crypto/test_tdh2.cpp @@ -14,6 +14,19 @@ namespace { class TDH2 : public testutils::TestAC {}; +static void rebind_ciphertext_to_public_key(ciphertext_t& ciphertext, const public_key_t& pub_key, const bn_t& r, + const bn_t& s, mem_t label) { + const auto& curve = pub_key.Q.get_curve(); + const auto& G = curve.generator(); + const mod_t& q = curve.order(); + + ciphertext.R2 = r * pub_key.Gamma; + const ecc_point_t W1 = s * G; + const ecc_point_t W2 = s * pub_key.Gamma; + ciphertext.e = ro::hash_number(ciphertext.c, label, ciphertext.R1, W1, ciphertext.R2, W2, ciphertext.iv).mod(q); + MODULO(q) ciphertext.f = s + r * ciphertext.e; +} + TEST_F(TDH2, AddCompleteness) { int n = 10; std::vector dec_shares; @@ -277,6 +290,80 @@ TEST_F(TDH2, PartialDecryptionSerializationAndTamperChecks) { EXPECT_ER(bad_proof.check_partial_decryption_helper(pub_shares[0], ciphertext, curve_p256)); } +TEST_F(TDH2, CombineAdditiveRejectsForgedPublicShareContext) { + constexpr int n = 3; + const bn_t victim_x = curve_p256.get_random_value(); + const public_key_t victim_key(victim_x * curve_p256.generator(), gen_random(32)); + + public_key_t fake_key; + crypto::tdh2::pub_shares_t fake_pub_shares; + std::vector fake_dec_shares; + testutils::generate_additive_shares(n, fake_key, fake_pub_shares, fake_dec_shares, curve_p256); + + const buf_t label = buf_t("tdh2-label"); + const buf_t plain = buf_t("forged-plaintext"); + const bn_t r = bn_t(17); + const bn_t s = bn_t(19); + const buf_t iv = bn_t(0xabcdef).to_bin(iv_size); + ciphertext_t forged_ciphertext = fake_key.encrypt(plain, label, r, s, iv); + + partial_decryptions_t fake_partials(n); + for (int i = 0; i < n; i++) { + ASSERT_OK(fake_dec_shares[i].decrypt(forged_ciphertext, label, fake_partials[i])); + } + + rebind_ciphertext_to_public_key(forged_ciphertext, victim_key, r, s, label); + ASSERT_OK(forged_ciphertext.verify(victim_key, label)); + + buf_t decrypted; + EXPECT_ER(combine_additive(victim_key, fake_pub_shares, label, fake_partials, forged_ciphertext, decrypted)); +} + +TEST_F(TDH2, CombineAdditiveRejectsDuplicateRoleInForgedPublicShareContext) { + const auto& G = curve_p256.generator(); + const mod_t& q = curve_p256.order(); + const bn_t victim_x = bn_t(101); + const bn_t x1 = bn_t(31); + const bn_t x2 = bn_t(37); + bn_t fake_x; + MODULO(q) fake_x = x1 + x2 + x2; + + const public_key_t victim_key(victim_x * G, gen_random(32)); + const public_key_t fake_key(fake_x * G, gen_random(32)); + const ecc_point_t Q1 = x1 * G; + const ecc_point_t Q2 = x2 * G; + crypto::tdh2::pub_shares_t forged_pub_shares = {Q1, Q2, victim_key.Q - Q1 - Q2}; + ASSERT_EQ(forged_pub_shares[0] + forged_pub_shares[1] + forged_pub_shares[2], victim_key.Q); + + const buf_t label = buf_t("tdh2-label"); + const buf_t plain = buf_t("forged-plaintext"); + const bn_t r = bn_t(41); + const bn_t s = bn_t(43); + const buf_t iv = bn_t(0x654321).to_bin(iv_size); + ciphertext_t forged_ciphertext = fake_key.encrypt(plain, label, r, s, iv); + + private_share_t share1; + share1.pub_key = fake_key; + share1.x = x1; + share1.rid = 1; + private_share_t share2; + share2.pub_key = fake_key; + share2.x = x2; + share2.rid = 2; + + partial_decryptions_t forged_partials(3); + ASSERT_OK(share1.decrypt(forged_ciphertext, label, forged_partials[0])); + ASSERT_OK(share2.decrypt(forged_ciphertext, label, forged_partials[1])); + forged_partials[2] = forged_partials[1]; + ASSERT_EQ(forged_partials[2].rid, 2); + + rebind_ciphertext_to_public_key(forged_ciphertext, victim_key, r, s, label); + ASSERT_OK(forged_ciphertext.verify(victim_key, label)); + + buf_t decrypted; + EXPECT_ER(combine_additive(victim_key, forged_pub_shares, label, forged_partials, forged_ciphertext, decrypted)); +} + TEST_F(TDH2, CombineAdditiveRejectsMalformedInputs) { const int n = 3; public_key_t enc_key; @@ -340,6 +427,37 @@ TEST_F(TDH2, CombineFailsWithInsufficientQuorum) { EXPECT_ER(combine(test_ac, enc_key, pub_shares, label, partial_decryptions, ciphertext, decrypted)); } +TEST_F(TDH2, CombineACRejectsForgedPublicShareContext) { + test_ac.curve = curve_p256; + const bn_t victim_x = curve_p256.get_random_value(); + const public_key_t victim_key(victim_x * curve_p256.generator(), gen_random(32)); + + public_key_t fake_key; + ss::ac_pub_shares_t fake_pub_shares; + ss::party_map_t fake_dec_shares; + testutils::generate_ac_shares(test_ac, fake_key, fake_pub_shares, fake_dec_shares, curve_p256); + + const buf_t label = buf_t("tdh2-label"); + const buf_t plain = buf_t("forged-plaintext"); + const bn_t r = bn_t(23); + const bn_t s = bn_t(29); + const buf_t iv = bn_t(0x123456).to_bin(iv_size); + ciphertext_t forged_ciphertext = fake_key.encrypt(plain, label, r, s, iv); + + ss::party_map_t fake_partials; + for (const auto& [name, share] : fake_dec_shares) { + partial_decryption_t partial; + ASSERT_OK(share.decrypt(forged_ciphertext, label, partial)); + fake_partials[name] = std::move(partial); + } + + rebind_ciphertext_to_public_key(forged_ciphertext, victim_key, r, s, label); + ASSERT_OK(forged_ciphertext.verify(victim_key, label)); + + buf_t decrypted; + EXPECT_ER(combine(test_ac, victim_key, fake_pub_shares, label, fake_partials, forged_ciphertext, decrypted)); +} + TEST_F(TDH2, CombineACRejectsTamperedInputs) { test_ac.curve = curve_p256; public_key_t enc_key; diff --git a/tests/unit/protocol/test_mpc_network.cpp b/tests/unit/protocol/test_mpc_network.cpp index 1e6e2322..ddce877c 100644 --- a/tests/unit/protocol/test_mpc_network.cpp +++ b/tests/unit/protocol/test_mpc_network.cpp @@ -1,9 +1,14 @@ +#include #include +#include +#include #include +#include #include #include "utils/local_network/mpc_tester.h" +#include "utils/local_network/network_context.h" namespace { @@ -11,6 +16,97 @@ using namespace coinbase; using namespace coinbase::mpc; using namespace coinbase::testutils; +static bool tamper_inner_broadcast_elg_com_l_curve_to_null(buf_t& msg) { + buf_t pairwise_msg; + buf_t broadcast_msg; + if (deser(msg, pairwise_msg, broadcast_msg)) return false; + + elg_com_t commitment; + if (deser(broadcast_msg, commitment)) return false; + + commitment.L = crypto::ecc_point_t(); + broadcast_msg = ser(commitment); + msg = ser(pairwise_msg, broadcast_msg); + return true; +} + +class local_internal_transport_t final : public coinbase::api::data_transport_i { + public: + explicit local_internal_transport_t(std::shared_ptr ctx) : ctx_(std::move(ctx)) {} + + error_t send(coinbase::api::party_idx_t receiver, mem_t msg) override { + ctx_->send(receiver, msg); + return SUCCESS; + } + + error_t receive(coinbase::api::party_idx_t sender, buf_t& msg) override { return ctx_->receive(sender, msg); } + + error_t receive_all(const std::vector& senders, std::vector& msgs) override { + std::vector internal_senders; + internal_senders.reserve(senders.size()); + for (auto sender : senders) internal_senders.push_back(static_cast(sender)); + return ctx_->receive_all(internal_senders, msgs); + } + + private: + std::shared_ptr ctx_; +}; + +class tamper_inner_elg_com_send_transport_t final : public coinbase::api::data_transport_i { + public: + tamper_inner_elg_com_send_transport_t(std::shared_ptr ctx, int tamper_send_index) + : ctx_(std::move(ctx)), tamper_send_index_(tamper_send_index) {} + + error_t send(coinbase::api::party_idx_t receiver, mem_t msg) override { + buf_t out(msg); + if (++send_count_ == tamper_send_index_) { + if (!tamper_inner_broadcast_elg_com_l_curve_to_null(out)) return E_GENERAL; + tampered_ = true; + } + ctx_->send(receiver, out); + return SUCCESS; + } + + error_t receive(coinbase::api::party_idx_t sender, buf_t& msg) override { return ctx_->receive(sender, msg); } + + error_t receive_all(const std::vector& senders, std::vector& msgs) override { + std::vector internal_senders; + internal_senders.reserve(senders.size()); + for (auto sender : senders) internal_senders.push_back(static_cast(sender)); + return ctx_->receive_all(internal_senders, msgs); + } + + bool tampered() const { return tampered_; } + + private: + std::shared_ptr ctx_; + int tamper_send_index_; + int send_count_ = 0; + bool tampered_ = false; +}; + +template +static void run_two_party_mp(const std::vector>& peers, + const std::vector>& transports, F&& f, + std::vector& out_rv) { + for (const auto& peer : peers) peer->reset(); + + out_rv.assign(peers.size(), UNINITIALIZED_ERROR); + std::atomic aborted{false}; + std::vector threads; + threads.reserve(peers.size()); + + for (size_t i = 0; i < peers.size(); i++) { + threads.emplace_back([&, i] { + out_rv[i] = f(static_cast(i)); + if (out_rv[i] && !aborted.exchange(true)) { + for (const auto& peer : peers) peer->abort(); + } + }); + } + for (auto& t : threads) t.join(); +} + class noop_transport_t final : public coinbase::api::data_transport_i { public: error_t send(coinbase::api::party_idx_t receiver, mem_t msg) override { @@ -212,4 +308,47 @@ TEST_P(NetworkMPC, PairwiseAndBroadcast) { } INSTANTIATE_TEST_SUITE_P(, NetworkMPC, testing::Values(2, 4, 5, 10, 32, 64)); +TEST(MPCJob, MultiSetGroupMessageRejectsMalformedInnerBroadcast) { + constexpr int n = 2; + constexpr int malicious = 1; + + std::vector> peers; + peers.reserve(n); + for (int i = 0; i < n; i++) peers.push_back(std::make_shared(i)); + for (const auto& peer : peers) peer->init_with_peers(peers); + + std::vector> transports; + transports.push_back(std::make_shared(peers[0])); + transports.push_back(std::make_shared(peers[1], /*tamper_send_index=*/1)); + + const std::vector names = {"alice", "bob"}; + const ecurve_t curve = crypto::curve_secp256k1; + const auto [E, _d] = crypto::ec_elgamal_commitment_t::local_keygen(curve); + (void)_d; + const elg_com_t valid_com = elg_com_t::commit(E, curve.get_random_value()).rand(curve.get_random_value()); + + std::vector results(n, UNINITIALIZED_ERROR); + run_two_party_mp( + peers, transports, + [&](int party_index) { + job_mp_t job(party_index, names, *transports[static_cast(party_index)]); + + auto pairwise = job.uniform_msg(buf_t("pairwise")); + auto broadcast = job.uniform_msg(valid_com); + + party_set_t receivers = party_set_t::of(1); + party_set_t senders = party_set_t::of(0); + party_set_t all_parties = party_set_t::all(); + + dylog_disable_scope_t no_log_err; + return job.group_message(std::tie(receivers, senders, pairwise), std::tie(all_parties, all_parties, broadcast)); + }, + results); + + auto* tamper_transport = dynamic_cast(transports[1].get()); + ASSERT_NE(tamper_transport, nullptr); + EXPECT_TRUE(tamper_transport->tampered()); + EXPECT_NE(results[0], SUCCESS); +} + } // namespace diff --git a/tests/unit/protocol/test_ot.cpp b/tests/unit/protocol/test_ot.cpp index 026cce07..e0d05f03 100644 --- a/tests/unit/protocol/test_ot.cpp +++ b/tests/unit/protocol/test_ot.cpp @@ -32,6 +32,42 @@ TEST(OT_Base, PVW) { } } +TEST(OT_Base, RejectsMalformedVLengths) { + const int u = 4; + base_ot_protocol_pvw_ctx_t ot; + bits_t b = crypto::gen_random_bits(u); + std::vector x0(u), x1(u), x_out; + for (int j = 0; j < u; ++j) { + x0[j] = crypto::gen_random(16); + x1[j] = crypto::gen_random(16); + } + ot.sid = crypto::gen_random(16); + EXPECT_OK(ot.step1_R2S(b)); + EXPECT_OK(ot.step2_S2R(x0, x1)); + + ot.V0[0] = buf_t(8); + EXPECT_NE(ot.output_R(x_out), SUCCESS); + + EXPECT_OK(ot.step2_S2R(x0, x1)); + ot.V1[0] = buf_t(8); + EXPECT_NE(ot.output_R(x_out), SUCCESS); + + EXPECT_OK(ot.step2_S2R(x0, x1)); + ot.V0[0] = buf_t(16); + ot.V1[0] = buf_t(8); + EXPECT_NE(ot.output_R(x_out), SUCCESS); +} + +TEST(OT_Base, RejectsMalformedSenderInputs) { + base_ot_protocol_pvw_ctx_t ot; + bits_t b = crypto::gen_random_bits(1); + std::vector x0 = {crypto::gen_random(16)}; + std::vector x1 = {buf_t(8)}; + ot.sid = crypto::gen_random(16); + EXPECT_OK(ot.step1_R2S(b)); + EXPECT_NE(ot.step2_S2R(x0, x1), SUCCESS); +} + TEST(OT_Helpers, MatrixAccessorsAndMessageTuples) { const int requested_cols = 17; h_matrix_256rows_t h_matrix;