diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 5bc68987..b4d554db 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -20,8 +20,21 @@ on: pull_request_target: types: [opened, closed, synchronize] permissions: + contents: write + issues: write pull-requests: write jobs: CLAssistant: - uses: secretflow/.github/.github/workflows/cla.yml@main - secrets: inherit + runs-on: ubuntu-latest + steps: + - name: CLA Assistant + if: github.event_name == 'pull_request_target' || github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA' + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-document: https://github.com/secretflow/.github/blob/main/CLA.md + path-to-signatures: signatures/version1/cla.json + branch: main + allowlist: jjllzhang,test,renovate[bot],dependabot[bot],pre-commit-ci[bot],github-actions[bot],snyk-bot,Copilot + lock-pullrequest-aftermerge: true diff --git a/yacl/crypto/experimental/threshold_signatures/BUILD.bazel b/yacl/crypto/experimental/threshold_signatures/BUILD.bazel new file mode 100644 index 00000000..614dc80f --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/BUILD.bazel @@ -0,0 +1,98 @@ +# Copyright 2026 Ant Group Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("//bazel:yacl.bzl", "yacl_cc_binary", "yacl_cc_library") + +package(default_visibility = ["//visibility:public"]) + +TECDSA_COPTS = ["-std=gnu++20"] + +TSIG_CORE_SRCS = glob([ + "core/**/*.cc", +]) + +TSIG_CORE_HDRS = glob([ + "common/*.h", + "core/**/*.h", +]) + +TSIG_ECDSA_SRCS = glob([ + "ecdsa/**/*.cc", +]) + +TSIG_ECDSA_HDRS = glob([ + "ecdsa/**/*.h", +]) + +TSIG_COMMON_DEPS = [ + "//yacl/crypto/ecc/openssl:openssl", + # Big integer backend used across scalar/paillier/proof/protocol code paths. + "//yacl/math/mpint", +] + +yacl_cc_library( + name = "tsig_core", + srcs = TSIG_CORE_SRCS, + hdrs = TSIG_CORE_HDRS, + copts = TECDSA_COPTS, + deps = TSIG_COMMON_DEPS + [ + "//yacl/base:exception", + "//yacl/crypto/hash:ssl_hash", + "//yacl/crypto/rand", + ], +) + +yacl_cc_library( + name = "tsig_ecdsa", + srcs = TSIG_ECDSA_SRCS, + hdrs = TSIG_ECDSA_HDRS, + copts = TECDSA_COPTS, + deps = [ + ":tsig_core", + ], +) + +yacl_cc_library( + name = "tsig_sm2", + srcs = glob([ + "sm2/**/*.cc", + ]), + hdrs = glob([ + "sm2/**/*.h", + ]), + copts = TECDSA_COPTS, + deps = [ + ":tsig_core", + ], +) + +yacl_cc_binary( + name = "sign_flow_tests", + srcs = [ + "tests/ecdsa_flow_test.cc", + "tests/test_helpers.h", + ], + copts = TECDSA_COPTS, + deps = [":tsig_ecdsa"], +) + +yacl_cc_binary( + name = "sm2_sign_flow_tests", + srcs = [ + "tests/sm2_flow_test.cc", + "tests/test_helpers.h", + ], + copts = TECDSA_COPTS, + deps = [":tsig_sm2"], +) diff --git a/yacl/crypto/experimental/threshold_signatures/README.md b/yacl/crypto/experimental/threshold_signatures/README.md new file mode 100644 index 00000000..96216bf8 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/README.md @@ -0,0 +1,98 @@ +# Threshold Signatures + +## Introduction + +This directory contains experimental C++20 implementations of threshold ECDSA +and threshold SM2. The two schemes have independent protocol flows and share +common cryptographic building blocks through `core/`. + +The code is intended for protocol review and integration experiments. It does +not include transport, persistent storage, wallet integration, or production +hardening. + +## Implementation + +### Code Structure + +1. `common/` defines shared aliases, participant ids, peer maps, and error + helpers. +2. `core/` contains the shared primitives used by both schemes: + - `algebra/`: curve point and scalar wrappers over YACL ECC. + - `bigint/`: protocol-facing `MPInt` encoding and validation helpers. + - `commitment/`, `transcript/`, `proof/`, `vss/`: commitment, + Fiat-Shamir transcript, Schnorr proof, and Feldman VSS helpers. + - `mta/`: semantic MtA/MtAwc session API and the corresponding proof + primitives. + - `paillier/`: Paillier provider, auxiliary RSA setup, and paper proof + helpers. + - `protocol/`: peer-message checks and a simple linear round driver. + - `random/`, `suite/`: threshold-signature random helpers, suite + descriptors, group contexts, and hash dispatch. +3. `ecdsa/` implements threshold ECDSA key generation, signing, relation + proofs, and signature verification. +4. `sm2/` implements threshold SM2 key generation, offline presigning, online + signing, identifiable-abort data types, SM2-specific proofs, ZID binding, + and signature verification. +5. `tests/` contains executable ECDSA and SM2 flow tests. + +### Threshold ECDSA + +1. Key generation uses dealerless Feldman VSS, Paillier parameters, auxiliary + RSA parameters, square-free/auxiliary proofs, and public-share checks. +2. `VerifiedPublicKeygenData` validates and owns the public keygen data consumed + by signing. +3. Signing is exposed as explicit round methods on `SignParty`. The large + signing flow is split by round, and `SigningMtaExchange` hides pairwise + MtA/MtAwc message and proof details from the scheme-level round code. +4. Finalization verifies the produced ECDSA signature before returning it. + +### Threshold SM2 + +1. Key generation derives SM2 public-key material from distributed `z_i` shares, + pairwise MtA sigma shares, group relation proofs, square-free proofs, and + Paillier auxiliary proof artifacts. +2. Offline presigning computes nonce/product state with MtAwc and validates the + aggregate product relation. +3. Online signing binds the SM2 ZID digest, computes partial signatures, and + returns identifiable-abort evidence for invalid partials when possible. + +## Build / Test + +Build all threshold-signature targets: + +```bash +bazel build //yacl/crypto/experimental/threshold_signatures:all +``` + +Build individual libraries: + +```bash +bazel build //yacl/crypto/experimental/threshold_signatures:tsig_core +bazel build //yacl/crypto/experimental/threshold_signatures:tsig_ecdsa +bazel build //yacl/crypto/experimental/threshold_signatures:tsig_sm2 +``` + +Run the executable flow tests: + +```bash +bazel run //yacl/crypto/experimental/threshold_signatures:sign_flow_tests +bazel run //yacl/crypto/experimental/threshold_signatures:sm2_sign_flow_tests +``` + +## Dependencies + +- Elliptic-curve operations use YACL ECC support. +- Big integer operations use YACL `MPInt`. +- Random bytes are sourced through `yacl::crypto::SecureRandBytes`. +- Hashing is routed through YACL hash abstractions such as `SslHash` and + `HashAlgorithm`. + +## Notes + +- The module keeps ECDSA and SM2 protocol messages separate and shares only + primitives that are common to both schemes. +- Scheme-level code calls semantic MtA APIs such as `InitiatorInit`, + `ResponderMid`, and `InitiatorEnd`; low-level proof construction and + verification remain inside `core/mta`. +- `core/bigint` is a protocol adapter for encoding and validation. It should + not be expanded into a mirror of the full `MPInt` API. diff --git a/yacl/crypto/experimental/threshold_signatures/common/errors.h b/yacl/crypto/experimental/threshold_signatures/common/errors.h new file mode 100644 index 00000000..1f072c83 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/common/errors.h @@ -0,0 +1,24 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/base/exception.h" + +// Use YACL-style exceptions with a safe "{}" wrapper so dynamic messages are +// treated as plain text instead of runtime format strings. +#define TECDSA_THROW(msg) YACL_THROW("{}", (msg)) +#define TECDSA_THROW_ARGUMENT(msg) YACL_THROW_ARGUMENT_ERROR("{}", (msg)) +#define TECDSA_THROW_LOGIC(msg) YACL_THROW_LOGIC_ERROR("{}", (msg)) +#define TECDSA_ENFORCE(cond, msg) YACL_ENFORCE((cond), "{}", (msg)) diff --git a/yacl/crypto/experimental/threshold_signatures/common/ids.h b/yacl/crypto/experimental/threshold_signatures/common/ids.h new file mode 100644 index 00000000..9b151a77 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/common/ids.h @@ -0,0 +1,31 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +namespace tecdsa { + +using Bytes = std::vector; +using PartyIndex = uint32_t; + +struct PartyInfo { + PartyIndex id; + std::string endpoint; +}; + +} // namespace tecdsa diff --git a/yacl/crypto/experimental/threshold_signatures/common/types.h b/yacl/crypto/experimental/threshold_signatures/common/types.h new file mode 100644 index 00000000..37077d7a --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/common/types.h @@ -0,0 +1,30 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" + +namespace tecdsa { + +using BigInt = core::paillier::BigInt; +using ECPoint = core::Point; +using Scalar = core::Scalar; + +using PaillierProvider = core::paillier::PaillierProvider; +using PaillierPublicKey = core::paillier::PaillierPublicKey; + +} // namespace tecdsa diff --git a/yacl/crypto/experimental/threshold_signatures/core/algebra/point.cc b/yacl/crypto/experimental/threshold_signatures/core/algebra/point.cc new file mode 100644 index 00000000..2568b58a --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/algebra/point.cc @@ -0,0 +1,171 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::core { +namespace { + +using yacl::crypto::EcPoint; +using yacl::crypto::PointOctetFormat; + +std::shared_ptr RequireGroup( + std::shared_ptr group) { + return group == nullptr ? DefaultGroupContext() : std::move(group); +} + +const GroupContext& RequireSameGroup( + const std::shared_ptr& lhs, + const std::shared_ptr& rhs) { + if (lhs == nullptr || rhs == nullptr) { + TECDSA_THROW_ARGUMENT("Point group context must not be null"); + } + if (lhs->curve_id() != rhs->curve_id()) { + TECDSA_THROW_ARGUMENT("Point operands must use the same group context"); + } + return *lhs; +} + +std::string InvalidPointMessage(const GroupContext& group) { + return "Compressed point is not a valid " + std::string(group.curve_name()) + + " point"; +} + +EcPoint DeserializeCompressed(const GroupContext& group, + const std::array& compressed) { + try { + EcPoint point = group.ec_group().DeserializePoint( + compressed, PointOctetFormat::X962Compressed); + if (!group.ec_group().IsInCurveGroup(point) || + group.ec_group().IsInfinity(point)) { + TECDSA_THROW_ARGUMENT(InvalidPointMessage(group)); + } + return point; + } catch (const std::exception&) { + TECDSA_THROW_ARGUMENT(InvalidPointMessage(group)); + } +} + +std::array SerializeCompressed(const GroupContext& group, + const EcPoint& point) { + const auto encoded = + group.ec_group().SerializePoint(point, PointOctetFormat::X962Compressed); + + std::array out{}; + if (encoded.size() != static_cast(out.size())) { + TECDSA_THROW("Failed to serialize compressed curve point"); + } + std::memcpy(out.data(), encoded.data(), out.size()); + if (out[0] != 0x02 && out[0] != 0x03) { + TECDSA_THROW("Failed to serialize compressed curve point"); + } + return out; +} + +} // namespace + +Point::Point() : group_(DefaultGroupContext()) { + compressed_.fill(0); + compressed_[0] = 0x02; +} + +Point::Point(std::shared_ptr group, + const std::array& compressed) + : group_(RequireGroup(std::move(group))), compressed_(compressed) {} + +Point Point::FromCompressed(std::span compressed_bytes, + std::shared_ptr group) { + auto resolved_group = RequireGroup(std::move(group)); + if (compressed_bytes.size() != resolved_group->compressed_point_size_bytes()) { + TECDSA_THROW_ARGUMENT("Compressed point must be 33 bytes"); + } + if (compressed_bytes[0] != 0x02 && compressed_bytes[0] != 0x03) { + TECDSA_THROW_ARGUMENT(InvalidPointMessage(*resolved_group)); + } + + std::array compressed{}; + std::copy(compressed_bytes.begin(), compressed_bytes.end(), + compressed.begin()); + (void)DeserializeCompressed(*resolved_group, compressed); + return Point(std::move(resolved_group), compressed); +} + +Point Point::GeneratorMultiply(const Scalar& scalar) { + if (scalar.mp_value() == 0) { + TECDSA_THROW_ARGUMENT( + "Generator multiplication failed: scalar must be in [1, q-1]"); + } + + const GroupContext& group = *scalar.group(); + const EcPoint point = group.ec_group().MulBase(scalar.mp_value()); + if (group.ec_group().IsInfinity(point)) { + TECDSA_THROW_ARGUMENT( + "Generator multiplication failed: scalar must be in [1, q-1]"); + } + return Point(scalar.group(), SerializeCompressed(group, point)); +} + +Point Point::Add(const Point& other) const { + const GroupContext& group = RequireSameGroup(group_, other.group_); + const EcPoint lhs = DeserializeCompressed(group, compressed_); + const EcPoint rhs = DeserializeCompressed(group, other.compressed_); + const EcPoint combined = group.ec_group().Add(lhs, rhs); + if (group.ec_group().IsInfinity(combined)) { + TECDSA_THROW_ARGUMENT("Point addition failed (sum is point at infinity?)"); + } + + return Point(group_, SerializeCompressed(group, combined)); +} + +Point Point::Mul(const Scalar& scalar) const { + const GroupContext& group = RequireSameGroup(group_, scalar.group()); + if (scalar.mp_value() == 0) { + TECDSA_THROW_ARGUMENT("Point scalar multiplication failed"); + } + + const EcPoint point = DeserializeCompressed(group, compressed_); + const EcPoint multiplied = group.ec_group().Mul(point, scalar.mp_value()); + if (group.ec_group().IsInfinity(multiplied)) { + TECDSA_THROW_ARGUMENT("Point scalar multiplication failed"); + } + + return Point(group_, SerializeCompressed(group, multiplied)); +} + +Bytes Point::ToCompressedBytes() const { + return Bytes(compressed_.begin(), compressed_.end()); +} + +const std::shared_ptr& Point::group() const { + return group_; +} + +bool Point::operator==(const Point& other) const { + if (group_ == nullptr || other.group_ == nullptr) { + return group_ == other.group_ && compressed_ == other.compressed_; + } + return group_->curve_id() == other.group_->curve_id() && + compressed_ == other.compressed_; +} + +bool Point::operator!=(const Point& other) const { return !(*this == other); } + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/algebra/point.h b/yacl/crypto/experimental/threshold_signatures/core/algebra/point.h new file mode 100644 index 00000000..8e3a5110 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/algebra/point.h @@ -0,0 +1,55 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" + +namespace tecdsa::core { + +class Point { + public: + Point(); + + static Point FromCompressed( + std::span compressed_bytes, + std::shared_ptr group = DefaultGroupContext()); + static Point GeneratorMultiply(const Scalar& scalar); + + Point Add(const Point& other) const; + Point Mul(const Scalar& scalar) const; + + Bytes ToCompressedBytes() const; + const std::shared_ptr& group() const; + + bool operator==(const Point& other) const; + bool operator!=(const Point& other) const; + + private: + Point(std::shared_ptr group, + const std::array& compressed); + + std::shared_ptr group_; + std::array compressed_{}; +}; + +using ECPoint = Point; + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.cc b/yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.cc new file mode 100644 index 00000000..ebd3eee5 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.cc @@ -0,0 +1,150 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +namespace tecdsa::core { +namespace { + +using BigInt = Scalar::BigInt; + +std::shared_ptr RequireGroup( + std::shared_ptr group) { + return group == nullptr ? DefaultGroupContext() : std::move(group); +} + +const GroupContext& RequireSameGroup( + const std::shared_ptr& lhs, + const std::shared_ptr& rhs) { + if (lhs == nullptr || rhs == nullptr) { + TECDSA_THROW_ARGUMENT("Scalar group context must not be null"); + } + if (lhs->curve_id() != rhs->curve_id()) { + TECDSA_THROW_ARGUMENT("Scalar operands must use the same group context"); + } + return *lhs; +} + +BigInt NormalizeToGroupOrder(const GroupContext& group, const BigInt& input) { + return bigint::NormalizeMod(input, group.order()); +} + +BigInt ImportBigEndian(std::span bytes) { + if (bytes.empty()) { + TECDSA_THROW_ARGUMENT("Big-endian input must not be empty"); + } + return bigint::FromBigEndian(bytes); +} + +} // namespace + +Scalar::Scalar() : Scalar(BigInt(0), DefaultGroupContext()) {} + +Scalar::Scalar(const BigInt& value, std::shared_ptr group) + : group_(RequireGroup(std::move(group))), + value_(NormalizeToGroupOrder(*group_, value)) {} + +Scalar Scalar::FromUint64(uint64_t value, + std::shared_ptr group) { + return Scalar(BigInt(value), std::move(group)); +} + +Scalar Scalar::FromBigEndianModQ(std::span bytes, + std::shared_ptr group) { + return Scalar(ImportBigEndian(bytes), std::move(group)); +} + +Scalar Scalar::FromCanonicalBytes(std::span bytes, + std::shared_ptr group) { + auto resolved_group = RequireGroup(std::move(group)); + if (bytes.size() != resolved_group->scalar_size_bytes()) { + TECDSA_THROW_ARGUMENT("Canonical scalar must be exactly 32 bytes"); + } + + BigInt imported = ImportBigEndian(bytes); + if (imported >= resolved_group->order()) { + TECDSA_THROW_ARGUMENT("Canonical scalar is out of range"); + } + return Scalar(imported, std::move(resolved_group)); +} + +std::array Scalar::ToCanonicalBytes() const { + if (group_->scalar_size_bytes() != 32) { + TECDSA_THROW("Stage-1 scalar encoding only supports 32-byte groups"); + } + + std::array out{}; + const Bytes fixed = bigint::ToFixedWidth(value_, out.size()); + std::copy(fixed.begin(), fixed.end(), out.begin()); + return out; +} + +const Scalar::BigInt& Scalar::mp_value() const { return value_; } + +const Scalar::BigInt& Scalar::value() const { return value_; } + +const std::shared_ptr& Scalar::group() const { + return group_; +} + +Scalar Scalar::operator+(const Scalar& other) const { + const GroupContext& group = RequireSameGroup(group_, other.group_); + return Scalar(value_ + other.value_, GroupContext::Create(group.curve_id())); +} + +Scalar Scalar::operator-(const Scalar& other) const { + const GroupContext& group = RequireSameGroup(group_, other.group_); + return Scalar(value_ - other.value_, GroupContext::Create(group.curve_id())); +} + +Scalar Scalar::operator*(const Scalar& other) const { + const GroupContext& group = RequireSameGroup(group_, other.group_); + return Scalar(value_ * other.value_, GroupContext::Create(group.curve_id())); +} + +Scalar Scalar::InverseModQ() const { + if (value_ == 0) { + TECDSA_THROW_ARGUMENT("zero has no inverse modulo q"); + } + + const auto inv = bigint::TryInvertMod(value_, group_->order()); + if (!inv.has_value()) { + TECDSA_THROW_ARGUMENT("failed to invert scalar modulo q"); + } + return Scalar(*inv, group_); +} + +bool Scalar::operator==(const Scalar& other) const { + if (group_ == nullptr || other.group_ == nullptr) { + return group_ == other.group_ && value_ == other.value_; + } + return group_->curve_id() == other.group_->curve_id() && + value_ == other.value_; +} + +bool Scalar::operator!=(const Scalar& other) const { return !(*this == other); } + +const Scalar::BigInt& Scalar::ModulusQMpInt() { + return DefaultGroupContext()->order(); +} + +const Scalar::BigInt& Scalar::ModulusQ() { return DefaultGroupContext()->order(); } + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h b/yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h new file mode 100644 index 00000000..8691c16e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h @@ -0,0 +1,67 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" + +namespace tecdsa::core { + +class Scalar { + public: + using BigInt = yacl::math::MPInt; + + Scalar(); + explicit Scalar(const BigInt& value, + std::shared_ptr group = + DefaultGroupContext()); + + static Scalar FromUint64(uint64_t value, + std::shared_ptr group = + DefaultGroupContext()); + static Scalar FromBigEndianModQ( + std::span bytes, + std::shared_ptr group = DefaultGroupContext()); + static Scalar FromCanonicalBytes( + std::span bytes, + std::shared_ptr group = DefaultGroupContext()); + + std::array ToCanonicalBytes() const; + + const BigInt& mp_value() const; + const BigInt& value() const; + const std::shared_ptr& group() const; + + Scalar operator+(const Scalar& other) const; + Scalar operator-(const Scalar& other) const; + Scalar operator*(const Scalar& other) const; + Scalar InverseModQ() const; + + bool operator==(const Scalar& other) const; + bool operator!=(const Scalar& other) const; + + static const BigInt& ModulusQMpInt(); + static const BigInt& ModulusQ(); + + private: + std::shared_ptr group_; + BigInt value_; +}; + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.cc b/yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.cc new file mode 100644 index 00000000..9d536a94 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.cc @@ -0,0 +1,125 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::bigint { + +MPInt FromBigEndian(std::span bytes) { + MPInt out; + out.FromMagBytes(bytes, yacl::Endian::big); + return out; +} + +Bytes ToBigEndian(const MPInt& value) { + if (value < 0) { + TECDSA_THROW_ARGUMENT("bigint value must be non-negative"); + } + + if (value == 0) { + return Bytes{0x00}; + } + + const yacl::Buffer mag = value.ToMagBytes(yacl::Endian::big); + const auto* mag_ptr = mag.data(); + return Bytes(mag_ptr, mag_ptr + mag.size()); +} + +Bytes ToFixedWidth(const MPInt& value, size_t width) { + if (value < 0) { + TECDSA_THROW_ARGUMENT("cannot export negative integer to fixed width"); + } + if (width == 0) { + if (value == 0) { + return {}; + } + TECDSA_THROW_ARGUMENT("non-zero integer does not fit zero-width buffer"); + } + + Bytes out(width, 0); + if (value == 0) { + return out; + } + + const yacl::Buffer mag = value.ToMagBytes(yacl::Endian::big); + const size_t mag_size = mag.size(); + if (mag_size > width) { + TECDSA_THROW_ARGUMENT("integer does not fit fixed-width buffer"); + } + + const auto* mag_ptr = mag.data(); + std::copy(mag_ptr, mag_ptr + mag_size, + out.begin() + static_cast(width - mag_size)); + return out; +} + +MPInt NormalizeMod(const MPInt& value, const MPInt& modulus) { + if (modulus <= 0) { + TECDSA_THROW_ARGUMENT("modulus must be positive"); + } + return value.Mod(modulus); +} + +MPInt PowMod(const MPInt& base, const MPInt& exponent, const MPInt& modulus) { + if (modulus <= 0) { + TECDSA_THROW_ARGUMENT("modulus must be positive"); + } + if (exponent < 0) { + TECDSA_THROW_ARGUMENT("modular exponent must be non-negative"); + } + return base.PowMod(exponent, modulus); +} + +std::optional TryInvertMod(const MPInt& value, const MPInt& modulus) { + if (modulus <= 1) { + return std::nullopt; + } + + try { + return value.InvertMod(modulus); + } catch (const std::runtime_error&) { + return std::nullopt; + } +} + +MPInt RandomBelow(const MPInt& upper_exclusive) { + if (upper_exclusive <= 0) { + TECDSA_THROW_ARGUMENT("random upper bound must be positive"); + } + return MPInt::RandomLtN(upper_exclusive); +} + +MPInt RandomZnStar(const MPInt& modulus_n) { + if (modulus_n <= 2) { + TECDSA_THROW_ARGUMENT("modulus must be > 2"); + } + + MPInt candidate; + MPInt gcd; + do { + candidate = RandomBelow(modulus_n); + gcd = MPInt::Gcd(candidate, modulus_n); + } while (candidate == 0 || gcd != 1); + + return candidate; +} + +} // namespace tecdsa::bigint diff --git a/yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h b/yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h new file mode 100644 index 00000000..0999c2ff --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h @@ -0,0 +1,43 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/math/mpint/mp_int.h" + +namespace tecdsa::bigint { + +using MPInt = yacl::math::MPInt; + +// Keep this file as a protocol-facing adapter, not a mirror of MPInt. Prefer +// direct MPInt calls unless the helper adds encoding, validation, or +// protocol-domain semantics needed by threshold signing. +MPInt FromBigEndian(std::span bytes); +Bytes ToBigEndian(const MPInt& value); +Bytes ToFixedWidth(const MPInt& value, size_t width); + +MPInt NormalizeMod(const MPInt& value, const MPInt& modulus); +MPInt PowMod(const MPInt& base, const MPInt& exponent, const MPInt& modulus); +std::optional TryInvertMod(const MPInt& value, const MPInt& modulus); + +MPInt RandomBelow(const MPInt& upper_exclusive); +MPInt RandomZnStar(const MPInt& modulus_n); + +} // namespace tecdsa::bigint diff --git a/yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.cc b/yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.cc new file mode 100644 index 00000000..309703ea --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.cc @@ -0,0 +1,102 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +namespace tecdsa::core::commitment { +namespace { + +constexpr char kCommitPrefix[] = "GG2019/commit/v1"; + +void AppendField(std::span field, Bytes* out) { + encoding::AppendSizedField(field, out, + "Commitment field exceeds uint32 length"); +} + +Bytes BuildCommitPreimage(const std::string& domain, + std::span message, + std::span randomness) { + Bytes preimage; + preimage.reserve(sizeof(kCommitPrefix) - 1 + domain.size() + message.size() + + randomness.size() + 12); + + const std::span prefix_bytes( + reinterpret_cast(kCommitPrefix), + sizeof(kCommitPrefix) - 1); + const std::span domain_bytes( + reinterpret_cast(domain.data()), domain.size()); + + AppendField(prefix_bytes, &preimage); + AppendField(domain_bytes, &preimage); + AppendField(message, &preimage); + AppendField(randomness, &preimage); + return preimage; +} + +} // namespace + +CommitmentResult CommitMessage(HashId hash_id, const std::string& domain, + std::span message, + size_t randomness_len) { + CommitmentResult out; + out.randomness = Csprng::RandomBytes(randomness_len); + out.commitment = + ComputeCommitment(hash_id, domain, message, out.randomness); + return out; +} + +CommitmentResult CommitMessage(const ThresholdSuite& suite, + const std::string& domain, + std::span message, + size_t randomness_len) { + return CommitMessage(suite.commitment_hash, domain, message, randomness_len); +} + +Bytes ComputeCommitment(HashId hash_id, const std::string& domain, + std::span message, + std::span randomness) { + const Bytes preimage = BuildCommitPreimage(domain, message, randomness); + return Hash(hash_id, preimage); +} + +Bytes ComputeCommitment(const ThresholdSuite& suite, const std::string& domain, + std::span message, + std::span randomness) { + return ComputeCommitment(suite.commitment_hash, domain, message, randomness); +} + +bool VerifyCommitment(HashId hash_id, const std::string& domain, + std::span message, + std::span randomness, + std::span commitment) { + const Bytes expected = ComputeCommitment(hash_id, domain, message, randomness); + return std::equal(expected.begin(), expected.end(), commitment.begin(), + commitment.end()); +} + +bool VerifyCommitment(const ThresholdSuite& suite, const std::string& domain, + std::span message, + std::span randomness, + std::span commitment) { + return VerifyCommitment(suite.commitment_hash, domain, message, randomness, + commitment); +} + +} // namespace tecdsa::core::commitment diff --git a/yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h b/yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h new file mode 100644 index 00000000..40775a12 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h @@ -0,0 +1,57 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::commitment { + +struct CommitmentResult { + Bytes commitment; + Bytes randomness; +}; + +CommitmentResult CommitMessage(HashId hash_id, const std::string& domain, + std::span message, + size_t randomness_len = 32); + +CommitmentResult CommitMessage(const ThresholdSuite& suite, + const std::string& domain, + std::span message, + size_t randomness_len = 32); + +Bytes ComputeCommitment(HashId hash_id, const std::string& domain, + std::span message, + std::span randomness); + +Bytes ComputeCommitment(const ThresholdSuite& suite, const std::string& domain, + std::span message, + std::span randomness); + +bool VerifyCommitment(HashId hash_id, const std::string& domain, + std::span message, + std::span randomness, + std::span commitment); + +bool VerifyCommitment(const ThresholdSuite& suite, const std::string& domain, + std::span message, + std::span randomness, + std::span commitment); + +} // namespace tecdsa::core::commitment diff --git a/yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.cc b/yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.cc new file mode 100644 index 00000000..d32e1c38 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.cc @@ -0,0 +1,59 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +namespace tecdsa::core::encoding { + +Bytes EncodeMpInt(const BigInt& value) { + const Bytes payload = bigint::ToBigEndian(value); + + if (payload.size() > UINT32_MAX) { + TECDSA_THROW_ARGUMENT("mpz byte length exceeds uint32"); + } + + Bytes out; + out.reserve(4 + payload.size()); + AppendU32Be(static_cast(payload.size()), &out); + out.insert(out.end(), payload.begin(), payload.end()); + return out; +} + +BigInt DecodeMpInt(std::span encoded, size_t max_len) { + if (encoded.size() < 4) { + TECDSA_THROW_ARGUMENT("Encoded mpz is too short"); + } + + const uint32_t payload_len = ReadU32Be(encoded, size_t{0}); + if (payload_len == 0) { + TECDSA_THROW_ARGUMENT("Encoded mpz payload length must be >= 1"); + } + if (payload_len > max_len) { + TECDSA_THROW_ARGUMENT("Encoded mpz payload exceeds max_len"); + } + if (encoded.size() != 4 + payload_len) { + TECDSA_THROW_ARGUMENT("Encoded mpz has inconsistent payload length"); + } + + return bigint::FromBigEndian(encoded.subspan(4, payload_len)); +} + +Bytes EncodePoint(const Point& point) { return point.ToCompressedBytes(); } + +} // namespace tecdsa::core::encoding diff --git a/yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h b/yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h new file mode 100644 index 00000000..ee75d61e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h @@ -0,0 +1,86 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/math/mpint/mp_int.h" + +namespace tecdsa::core::encoding { + +using BigInt = yacl::math::MPInt; + +inline void AppendU32Be(uint32_t value, Bytes* out) { + out->push_back(static_cast((value >> 24) & 0xFF)); + out->push_back(static_cast((value >> 16) & 0xFF)); + out->push_back(static_cast((value >> 8) & 0xFF)); + out->push_back(static_cast(value & 0xFF)); +} + +inline uint32_t ReadU32Be(std::span input, size_t offset) { + if (offset + 4 > input.size()) { + TECDSA_THROW_ARGUMENT("Not enough bytes to read u32"); + } + + return (static_cast(input[offset]) << 24) | + (static_cast(input[offset + 1]) << 16) | + (static_cast(input[offset + 2]) << 8) | + static_cast(input[offset + 3]); +} + +inline uint32_t ReadU32Be(std::span input, size_t* offset) { + const uint32_t value = ReadU32Be(input, *offset); + *offset += 4; + return value; +} + +inline void AppendSizedField( + std::span field, Bytes* out, + const char* oversize_error = "sized field exceeds uint32 length") { + if (field.size() > UINT32_MAX) { + TECDSA_THROW_ARGUMENT(oversize_error); + } + AppendU32Be(static_cast(field.size()), out); + out->insert(out->end(), field.begin(), field.end()); +} + +inline Bytes ReadSizedField(std::span input, size_t* offset, + size_t max_len, const char* field_name) { + const uint32_t len = ReadU32Be(input, offset); + if (len > max_len) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + " exceeds maximum length"); + } + if (*offset + len > input.size()) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + " has inconsistent length"); + } + + Bytes out(input.begin() + static_cast(*offset), + input.begin() + static_cast(*offset + len)); + *offset += len; + return out; +} + +Bytes EncodeMpInt(const BigInt& value); +BigInt DecodeMpInt(std::span encoded, size_t max_len = 8192); + +Bytes EncodePoint(const Point& point); + +} // namespace tecdsa::core::encoding diff --git a/yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.cc b/yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.cc new file mode 100644 index 00000000..c05b19f5 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.cc @@ -0,0 +1,150 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.h" + +namespace tecdsa::core::keygen { + +LocalRound1Bundle PrepareLocalRound1Bundle( + const std::vector& participants, uint32_t threshold, + const ThresholdSuite& suite, std::string_view commitment_domain, + const std::function& sample_non_zero_scalar, + const EvaluatePolynomialFn& evaluate_polynomial_at, + const BuildCommitmentsFn& build_commitments) { + const std::string domain(commitment_domain); + while (true) { + std::vector candidate_coefficients; + candidate_coefficients.reserve(threshold + 1); + for (uint32_t i = 0; i <= threshold; ++i) { + candidate_coefficients.push_back(sample_non_zero_scalar()); + } + + std::unordered_map candidate_shares; + candidate_shares.reserve(participants.size()); + bool has_zero_share = false; + for (PartyIndex party : participants) { + const Scalar share = evaluate_polynomial_at(candidate_coefficients, party); + if (share.value() == 0) { + has_zero_share = true; + break; + } + candidate_shares.emplace(party, share); + } + if (has_zero_share) { + continue; + } + + std::vector commitments = build_commitments(candidate_coefficients); + const ECPoint public_point = commitments.front(); + const auto commit = + commitment::CommitMessage(suite, domain, encoding::EncodePoint(public_point)); + return LocalRound1Bundle{ + .coefficients = std::move(candidate_coefficients), + .shares = std::move(candidate_shares), + .commitments = std::move(commitments), + .public_point = public_point, + .commitment = commit.commitment, + .randomness = commit.randomness, + }; + } +} + +LocalPaillierAuxBundle PrepareLocalPaillierAuxBundle( + uint32_t paillier_modulus_bits, uint32_t aux_rsa_modulus_bits, + size_t max_paillier_keygen_attempts, const Bytes& session_id, + PartyIndex self_id, const ThresholdSuite& suite, + const std::shared_ptr& challenge_group) { + LocalPaillierAuxBundle out; + for (size_t attempt = 0; attempt < max_paillier_keygen_attempts; ++attempt) { + auto candidate = std::make_shared( + paillier_modulus_bits); + const auto candidate_n = candidate->modulus_n_bigint(); + if (candidate_n > paillier::MinPaillierModulusQ8(challenge_group)) { + out.paillier = std::move(candidate); + out.paillier_public = paillier::PaillierPublicKey{.n = candidate_n}; + break; + } + } + + if (out.paillier == nullptr) { + TECDSA_THROW("failed to generate Paillier modulus N > q^8"); + } + + const auto proof_context = paillier::BuildProofContext( + session_id, self_id, suite, challenge_group); + const auto aux_setup = paillier::GeneratePaperAuxSetup(aux_rsa_modulus_bits); + out.aux_rsa_params = aux_setup.params; + out.aux_rsa_witness = aux_setup.witness; + if (!paillier::ValidatePaperAuxSetup(out.aux_rsa_params, + out.aux_rsa_witness)) { + TECDSA_THROW("failed to validate local paper auxiliary setup"); + } + out.aux_param_proof = paillier::BuildAuxCorrectFormProof( + out.aux_rsa_params, out.aux_rsa_witness, proof_context); + return out; +} + +void ValidatePeerRound1Common( + const Bytes& session_id, PartyIndex peer_id, const ThresholdSuite& suite, + const std::shared_ptr& challenge_group, + const paillier::PaillierPublicKey& paillier_public, + const paillier::AuxRsaParams& aux_rsa_params, + const paillier::AuxCorrectFormProof& aux_param_proof) { + paillier::ValidatePaillierPublicKeyOrThrow(paillier_public, challenge_group); + if (!paillier::ValidateAuxRsaParams(aux_rsa_params)) { + TECDSA_THROW_ARGUMENT("peer aux RSA parameters are invalid"); + } + + const auto proof_context = paillier::BuildProofContext( + session_id, peer_id, suite, challenge_group); + if (!paillier::VerifyAuxCorrectFormProof(aux_rsa_params, aux_param_proof, + proof_context)) { + TECDSA_THROW_ARGUMENT("peer aux parameter proof verification failed"); + } +} + +void ValidatePeerRound2ShareCommon( + uint32_t threshold, const ThresholdSuite& suite, + std::string_view commitment_domain, std::string_view public_point_name, + const Bytes& phase1_commitment, const ECPoint& public_point, + const Bytes& randomness, const std::vector& commitments, + const Scalar& share, const VerifyShareFn& verify_share_for_self) { + const std::string domain(commitment_domain); + if (commitments.size() != threshold + 1) { + TECDSA_THROW_ARGUMENT("peer commitment count does not match threshold"); + } + if (commitments.front() != public_point) { + TECDSA_THROW_ARGUMENT("peer Feldman commitments do not open to " + + std::string(public_point_name)); + } + if (!commitment::VerifyCommitment(suite, domain, + encoding::EncodePoint(public_point), + randomness, phase1_commitment)) { + TECDSA_THROW_ARGUMENT("peer phase1 commitment verification failed"); + } + if (!verify_share_for_self(commitments, share)) { + TECDSA_THROW_ARGUMENT("peer Feldman share verification failed"); + } +} + +} // namespace tecdsa::core::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.h b/yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.h new file mode 100644 index 00000000..da35b585 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.h @@ -0,0 +1,84 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::keygen { + +using EvaluatePolynomialFn = + std::function&, PartyIndex)>; +using BuildCommitmentsFn = + std::function(const std::vector&)>; +using VerifyShareFn = + std::function&, const Scalar&)>; + +struct LocalRound1Bundle { + std::vector coefficients; + std::unordered_map shares; + std::vector commitments; + ECPoint public_point; + Bytes commitment; + Bytes randomness; +}; + +LocalRound1Bundle PrepareLocalRound1Bundle( + const std::vector& participants, uint32_t threshold, + const ThresholdSuite& suite, std::string_view commitment_domain, + const std::function& sample_non_zero_scalar, + const EvaluatePolynomialFn& evaluate_polynomial_at, + const BuildCommitmentsFn& build_commitments); + +struct LocalPaillierAuxBundle { + std::shared_ptr paillier; + paillier::PaillierPublicKey paillier_public; + paillier::AuxRsaParams aux_rsa_params; + paillier::PaperAuxSetupWitness aux_rsa_witness; + paillier::AuxCorrectFormProof aux_param_proof; +}; + +LocalPaillierAuxBundle PrepareLocalPaillierAuxBundle( + uint32_t paillier_modulus_bits, uint32_t aux_rsa_modulus_bits, + size_t max_paillier_keygen_attempts, const Bytes& session_id, + PartyIndex self_id, const ThresholdSuite& suite, + const std::shared_ptr& challenge_group); + +void ValidatePeerRound1Common( + const Bytes& session_id, PartyIndex peer_id, const ThresholdSuite& suite, + const std::shared_ptr& challenge_group, + const paillier::PaillierPublicKey& paillier_public, + const paillier::AuxRsaParams& aux_rsa_params, + const paillier::AuxCorrectFormProof& aux_param_proof); + +void ValidatePeerRound2ShareCommon( + uint32_t threshold, const ThresholdSuite& suite, + std::string_view commitment_domain, std::string_view public_point_name, + const Bytes& phase1_commitment, const ECPoint& public_point, + const Bytes& randomness, const std::vector& commitments, + const Scalar& share, const VerifyShareFn& verify_share_for_self); + +} // namespace tecdsa::core::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/core/mta/messages.h b/yacl/crypto/experimental/threshold_signatures/core/mta/messages.h new file mode 100644 index 00000000..bb86ad12 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/mta/messages.h @@ -0,0 +1,56 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h" + +namespace tecdsa::core::mta { + +enum class MtaType : uint8_t { + kMta = 1, + kMtAwc = 2, +}; + +inline bool RequiresPublicPoint(MtaType type) { + return type == MtaType::kMtAwc; +} + +// Internal transport object used by PairwiseProductSession. Scheme-owned public +// message types live outside core and adapt to this structure at the boundary. +struct PairwiseProductRequest { + PartyIndex from = 0; + PartyIndex to = 0; + MtaType type = MtaType::kMta; + Bytes instance_id; + BigInt c1 = BigInt(0); + A1RangeProof a1_proof; +}; + +// Internal transport object used by PairwiseProductSession. Scheme-owned public +// message types live outside core and adapt to this structure at the boundary. +struct PairwiseProductResponse { + PartyIndex from = 0; + PartyIndex to = 0; + MtaType type = MtaType::kMta; + Bytes instance_id; + BigInt c2 = BigInt(0); + std::optional a2_proof; + std::optional a3_proof; +}; + +} // namespace tecdsa::core::mta diff --git a/yacl/crypto/experimental/threshold_signatures/core/mta/proofs.cc b/yacl/crypto/experimental/threshold_signatures/core/mta/proofs.cc new file mode 100644 index 00000000..f9b68e5f --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/mta/proofs.cc @@ -0,0 +1,581 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h" + +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +namespace tecdsa::core::mta { +namespace { + +constexpr char kA1RangeProofName[] = "A1Range"; +constexpr char kA2MtAwcProofName[] = "A2MtAwc"; +constexpr char kA3MtAProofName[] = "A3MtA"; + +BigInt NormalizeMod(const BigInt& value, const BigInt& modulus) { + return bigint::NormalizeMod(value, modulus); +} + +bool IsZnStarElement(const BigInt& value, const BigInt& modulus) { + if (value <= 0 || value >= modulus) { + return false; + } + return BigInt::Gcd(value, modulus) == 1; +} + +bool IsInRange(const BigInt& value, const BigInt& modulus) { + return value >= 0 && value < modulus; +} + +BigInt QPow(const BigInt& q, size_t exponent) { + BigInt out(1); + for (size_t i = 0; i < exponent; ++i) { + out *= q; + } + return out; +} + +BigInt QPow3(const MtaProofContext& ctx) { return QPow(ctx.group->order(), 3); } + +BigInt QPow7(const MtaProofContext& ctx) { return QPow(ctx.group->order(), 7); } + +std::string BuildProofId(const MtaProofContext& ctx, + std::string_view proof_name) { + return ctx.proof_domain_prefix + "/" + std::string(proof_name) + "/v1"; +} + +Bytes CurveNameBytes(const MtaProofContext& ctx) { + const auto curve_name = ctx.group->curve_name(); + return Bytes(reinterpret_cast(curve_name.data()), + reinterpret_cast(curve_name.data()) + + curve_name.size()); +} + +Bytes ModulusQBytes(const MtaProofContext& ctx) { + return bigint::ToFixedWidth(ctx.group->order(), ctx.group->scalar_size_bytes()); +} + +void AppendCommonMtaTranscriptFields(core::transcript::Transcript* transcript, + std::string_view proof_name, + const MtaProofContext& ctx) { + const std::string proof_id = BuildProofId(ctx, proof_name); + transcript->append_proof_id(proof_id); + transcript->append_session_id(ctx.session_id); + transcript->append_u32_be("initiator", ctx.initiator_id); + transcript->append_u32_be("responder", ctx.responder_id); + transcript->append_fields({ + core::transcript::TranscriptFieldRef{ + .label = "mta_id", .data = ctx.instance_id}, + core::transcript::TranscriptFieldRef{ + .label = "curve", .data = CurveNameBytes(ctx)}, + core::transcript::TranscriptFieldRef{ + .label = "q", .data = ModulusQBytes(ctx)}, + }); +} + +Scalar BuildA1RangeChallenge(const MtaProofContext& ctx, const BigInt& n, + const BigInt& gamma, const AuxRsaParams& aux, + const BigInt& c, const BigInt& z, const BigInt& u, + const BigInt& w) { + core::transcript::Transcript transcript(ctx.transcript_hash); + AppendCommonMtaTranscriptFields(&transcript, kA1RangeProofName, ctx); + const Bytes n_bytes = core::encoding::EncodeMpInt(n); + const Bytes gamma_bytes = core::encoding::EncodeMpInt(gamma); + const Bytes n_tilde_bytes = core::encoding::EncodeMpInt(aux.n_tilde); + const Bytes h1_bytes = core::encoding::EncodeMpInt(aux.h1); + const Bytes h2_bytes = core::encoding::EncodeMpInt(aux.h2); + const Bytes c_bytes = core::encoding::EncodeMpInt(c); + const Bytes z_bytes = core::encoding::EncodeMpInt(z); + const Bytes u_bytes = core::encoding::EncodeMpInt(u); + const Bytes w_bytes = core::encoding::EncodeMpInt(w); + transcript.append_fields({ + core::transcript::TranscriptFieldRef{.label = "N", .data = n_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "Gamma", .data = gamma_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "Ntilde", .data = n_tilde_bytes}, + core::transcript::TranscriptFieldRef{.label = "h1", .data = h1_bytes}, + core::transcript::TranscriptFieldRef{.label = "h2", .data = h2_bytes}, + core::transcript::TranscriptFieldRef{.label = "c", .data = c_bytes}, + core::transcript::TranscriptFieldRef{.label = "z", .data = z_bytes}, + core::transcript::TranscriptFieldRef{.label = "u", .data = u_bytes}, + core::transcript::TranscriptFieldRef{.label = "w", .data = w_bytes}, + }); + return transcript.challenge_scalar(ctx.group); +} + +Scalar BuildA2MtAwcChallenge(const MtaProofContext& ctx, const BigInt& n, + const BigInt& gamma, const AuxRsaParams& aux, + const BigInt& c1, const BigInt& c2, + const ECPoint& statement_x, + const A2MtAwcProof& proof) { + core::transcript::Transcript transcript(ctx.transcript_hash); + AppendCommonMtaTranscriptFields(&transcript, kA2MtAwcProofName, ctx); + const Bytes n_bytes = core::encoding::EncodeMpInt(n); + const Bytes gamma_bytes = core::encoding::EncodeMpInt(gamma); + const Bytes n_tilde_bytes = core::encoding::EncodeMpInt(aux.n_tilde); + const Bytes h1_bytes = core::encoding::EncodeMpInt(aux.h1); + const Bytes h2_bytes = core::encoding::EncodeMpInt(aux.h2); + const Bytes c1_bytes = core::encoding::EncodeMpInt(c1); + const Bytes c2_bytes = core::encoding::EncodeMpInt(c2); + const Bytes x_bytes = core::encoding::EncodePoint(statement_x); + const Bytes u_bytes = core::encoding::EncodePoint(proof.u); + const Bytes z_bytes = core::encoding::EncodeMpInt(proof.z); + const Bytes z2_bytes = core::encoding::EncodeMpInt(proof.z2); + const Bytes t_bytes = core::encoding::EncodeMpInt(proof.t); + const Bytes v_bytes = core::encoding::EncodeMpInt(proof.v); + const Bytes w_bytes = core::encoding::EncodeMpInt(proof.w); + transcript.append_fields({ + core::transcript::TranscriptFieldRef{.label = "N", .data = n_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "Gamma", .data = gamma_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "Ntilde", .data = n_tilde_bytes}, + core::transcript::TranscriptFieldRef{.label = "h1", .data = h1_bytes}, + core::transcript::TranscriptFieldRef{.label = "h2", .data = h2_bytes}, + core::transcript::TranscriptFieldRef{.label = "c1", .data = c1_bytes}, + core::transcript::TranscriptFieldRef{.label = "c2", .data = c2_bytes}, + core::transcript::TranscriptFieldRef{.label = "X", .data = x_bytes}, + core::transcript::TranscriptFieldRef{.label = "u", .data = u_bytes}, + core::transcript::TranscriptFieldRef{.label = "z", .data = z_bytes}, + core::transcript::TranscriptFieldRef{.label = "z2", .data = z2_bytes}, + core::transcript::TranscriptFieldRef{.label = "t", .data = t_bytes}, + core::transcript::TranscriptFieldRef{.label = "v", .data = v_bytes}, + core::transcript::TranscriptFieldRef{.label = "w", .data = w_bytes}, + }); + return transcript.challenge_scalar(ctx.group); +} + +Scalar BuildA3MtAChallenge(const MtaProofContext& ctx, const BigInt& n, + const BigInt& gamma, const AuxRsaParams& aux, + const BigInt& c1, const BigInt& c2, + const A3MtAProof& proof) { + core::transcript::Transcript transcript(ctx.transcript_hash); + AppendCommonMtaTranscriptFields(&transcript, kA3MtAProofName, ctx); + const Bytes n_bytes = core::encoding::EncodeMpInt(n); + const Bytes gamma_bytes = core::encoding::EncodeMpInt(gamma); + const Bytes n_tilde_bytes = core::encoding::EncodeMpInt(aux.n_tilde); + const Bytes h1_bytes = core::encoding::EncodeMpInt(aux.h1); + const Bytes h2_bytes = core::encoding::EncodeMpInt(aux.h2); + const Bytes c1_bytes = core::encoding::EncodeMpInt(c1); + const Bytes c2_bytes = core::encoding::EncodeMpInt(c2); + const Bytes z_bytes = core::encoding::EncodeMpInt(proof.z); + const Bytes z2_bytes = core::encoding::EncodeMpInt(proof.z2); + const Bytes t_bytes = core::encoding::EncodeMpInt(proof.t); + const Bytes v_bytes = core::encoding::EncodeMpInt(proof.v); + const Bytes w_bytes = core::encoding::EncodeMpInt(proof.w); + transcript.append_fields({ + core::transcript::TranscriptFieldRef{.label = "N", .data = n_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "Gamma", .data = gamma_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "Ntilde", .data = n_tilde_bytes}, + core::transcript::TranscriptFieldRef{.label = "h1", .data = h1_bytes}, + core::transcript::TranscriptFieldRef{.label = "h2", .data = h2_bytes}, + core::transcript::TranscriptFieldRef{.label = "c1", .data = c1_bytes}, + core::transcript::TranscriptFieldRef{.label = "c2", .data = c2_bytes}, + core::transcript::TranscriptFieldRef{.label = "z", .data = z_bytes}, + core::transcript::TranscriptFieldRef{.label = "z2", .data = z2_bytes}, + core::transcript::TranscriptFieldRef{.label = "t", .data = t_bytes}, + core::transcript::TranscriptFieldRef{.label = "v", .data = v_bytes}, + core::transcript::TranscriptFieldRef{.label = "w", .data = w_bytes}, + }); + return transcript.challenge_scalar(ctx.group); +} + +} // namespace + +MtaProofContext BuildProofContext(const Bytes& session_id, + PartyIndex initiator_id, + PartyIndex responder_id, + const Bytes& instance_id, + const ThresholdSuite& suite, + std::shared_ptr group) { + if (group == nullptr) { + group = GroupContext::Create(suite.curve); + } + return MtaProofContext{ + .session_id = session_id, + .initiator_id = initiator_id, + .responder_id = responder_id, + .instance_id = instance_id, + .transcript_hash = suite.transcript_hash, + .group = std::move(group), + .proof_domain_prefix = suite.proof_domain_prefix, + }; +} + +BigInt RandomBelow(const BigInt& upper_exclusive) { + if (upper_exclusive <= 0) { + TECDSA_THROW_ARGUMENT("random upper bound must be positive"); + } + return bigint::RandomBelow(upper_exclusive); +} + +BigInt SampleZnStar(const BigInt& modulus_n) { + if (modulus_n <= 2) { + TECDSA_THROW_ARGUMENT("Paillier modulus must be > 2"); + } + return bigint::RandomZnStar(modulus_n); +} + +BigInt QPow5(const std::shared_ptr& group) { + return QPow(group->order(), 5); +} + +BigInt MulMod(const BigInt& lhs, const BigInt& rhs, const BigInt& modulus) { + return NormalizeMod(lhs * rhs, modulus); +} + +BigInt PowMod(const BigInt& base, const BigInt& exp, const BigInt& modulus) { + if (exp < 0) { + TECDSA_THROW_ARGUMENT("modular exponent must be non-negative"); + } + return bigint::PowMod(base, exp, modulus); +} + +A1RangeProof ProveA1Range(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c, + const BigInt& witness_m, const BigInt& witness_r) { + const BigInt n2 = n * n; + const BigInt gamma = n + BigInt(1); + const BigInt n_tilde = verifier_aux.n_tilde; + const BigInt h1 = verifier_aux.h1; + const BigInt h2 = verifier_aux.h2; + const BigInt q_mul_n_tilde = ctx.group->order() * n_tilde; + const BigInt q3_mul_n_tilde = QPow3(ctx) * n_tilde; + + while (true) { + const BigInt alpha = RandomBelow(QPow3(ctx)); + const BigInt beta = SampleZnStar(n); + const BigInt gamma_rand = RandomBelow(q3_mul_n_tilde); + const BigInt rho = RandomBelow(q_mul_n_tilde); + + const BigInt z = MulMod(PowMod(h1, witness_m, n_tilde), + PowMod(h2, rho, n_tilde), n_tilde); + const BigInt u = MulMod(PowMod(gamma, alpha, n2), PowMod(beta, n, n2), n2); + const BigInt w = MulMod(PowMod(h1, alpha, n_tilde), + PowMod(h2, gamma_rand, n_tilde), n_tilde); + + const Scalar e_scalar = + BuildA1RangeChallenge(ctx, n, gamma, verifier_aux, c, z, u, w); + const BigInt e = e_scalar.mp_value(); + const BigInt s = MulMod(PowMod(witness_r, e, n), beta, n); + const BigInt s1 = (e * witness_m) + alpha; + const BigInt s2 = (e * rho) + gamma_rand; + if (s1 > QPow3(ctx)) { + continue; + } + + return A1RangeProof{ + .z = z, + .u = u, + .w = w, + .s = s, + .s1 = s1, + .s2 = s2, + }; + } +} + +bool VerifyA1Range(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c, + const A1RangeProof& proof) { + const BigInt n2 = n * n; + const BigInt gamma = n + BigInt(1); + const BigInt n_tilde = verifier_aux.n_tilde; + const BigInt h1 = verifier_aux.h1; + const BigInt h2 = verifier_aux.h2; + + if (!IsInRange(c, n2) || !IsInRange(proof.u, n2) || + !IsInRange(proof.z, n_tilde) || !IsInRange(proof.w, n_tilde) || + !IsZnStarElement(proof.s, n)) { + return false; + } + if (proof.s1 < 0 || proof.s1 > QPow3(ctx) || proof.s2 < 0) { + return false; + } + + const Scalar e_scalar = BuildA1RangeChallenge(ctx, n, gamma, verifier_aux, c, + proof.z, proof.u, proof.w); + const BigInt e = e_scalar.mp_value(); + + const BigInt c_pow_e = PowMod(c, e, n2); + const std::optional c_pow_e_inv = bigint::TryInvertMod(c_pow_e, n2); + if (!c_pow_e_inv.has_value()) { + return false; + } + + BigInt rhs_u = + MulMod(PowMod(gamma, proof.s1, n2), PowMod(proof.s, n, n2), n2); + rhs_u = MulMod(rhs_u, *c_pow_e_inv, n2); + if (NormalizeMod(proof.u, n2) != rhs_u) { + return false; + } + + const BigInt lhs_nt = MulMod(PowMod(h1, proof.s1, n_tilde), + PowMod(h2, proof.s2, n_tilde), n_tilde); + const BigInt rhs_nt = MulMod(proof.w, PowMod(proof.z, e, n_tilde), n_tilde); + return lhs_nt == rhs_nt; +} + +A2MtAwcProof ProveA2MtAwc(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const ECPoint& statement_x, + const BigInt& witness_x, const BigInt& witness_y, + const BigInt& witness_r) { + const BigInt n2 = n * n; + const BigInt gamma = n + BigInt(1); + const BigInt n_tilde = verifier_aux.n_tilde; + const BigInt h1 = verifier_aux.h1; + const BigInt h2 = verifier_aux.h2; + const BigInt q_mul_n_tilde = ctx.group->order() * n_tilde; + const BigInt q3_mul_n_tilde = QPow3(ctx) * n_tilde; + + while (true) { + const BigInt alpha = RandomBelow(QPow3(ctx)); + const Scalar alpha_scalar(alpha, ctx.group); + if (alpha_scalar.value() == 0) { + continue; + } + + const BigInt rho = RandomBelow(q_mul_n_tilde); + const BigInt rho2 = RandomBelow(q3_mul_n_tilde); + const BigInt sigma = RandomBelow(q_mul_n_tilde); + const BigInt beta = SampleZnStar(n); + const BigInt gamma_rand = RandomBelow(QPow7(ctx)); + const BigInt tau = RandomBelow(q3_mul_n_tilde); + + const ECPoint u = ECPoint::GeneratorMultiply(alpha_scalar); + const BigInt z = MulMod(PowMod(h1, witness_x, n_tilde), + PowMod(h2, rho, n_tilde), n_tilde); + const BigInt z2 = + MulMod(PowMod(h1, alpha, n_tilde), PowMod(h2, rho2, n_tilde), n_tilde); + const BigInt t = MulMod(PowMod(h1, witness_y, n_tilde), + PowMod(h2, sigma, n_tilde), n_tilde); + + BigInt v = MulMod(PowMod(c1, alpha, n2), PowMod(gamma, gamma_rand, n2), n2); + v = MulMod(v, PowMod(beta, n, n2), n2); + const BigInt w = MulMod(PowMod(h1, gamma_rand, n_tilde), + PowMod(h2, tau, n_tilde), n_tilde); + + A2MtAwcProof proof{ + .u = u, + .z = z, + .z2 = z2, + .t = t, + .v = v, + .w = w, + }; + const Scalar e_scalar = BuildA2MtAwcChallenge(ctx, n, gamma, verifier_aux, + c1, c2, statement_x, proof); + const BigInt e = e_scalar.mp_value(); + + const BigInt s = MulMod(PowMod(witness_r, e, n), beta, n); + const BigInt s1 = (e * witness_x) + alpha; + const BigInt s2 = (e * rho) + rho2; + const BigInt t1 = (e * witness_y) + gamma_rand; + const BigInt t2 = (e * sigma) + tau; + if (s1 > QPow3(ctx) || t1 > QPow7(ctx)) { + continue; + } + proof.s = s; + proof.s1 = s1; + proof.s2 = s2; + proof.t1 = t1; + proof.t2 = t2; + return proof; + } +} + +bool VerifyA2MtAwc(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const ECPoint& statement_x, + const A2MtAwcProof& proof) { + const BigInt n2 = n * n; + const BigInt gamma = n + BigInt(1); + const BigInt n_tilde = verifier_aux.n_tilde; + const BigInt h1 = verifier_aux.h1; + const BigInt h2 = verifier_aux.h2; + + if (!IsInRange(c1, n2) || !IsInRange(c2, n2) || !IsInRange(proof.v, n2) || + !IsInRange(proof.z, n_tilde) || !IsInRange(proof.z2, n_tilde) || + !IsInRange(proof.t, n_tilde) || !IsInRange(proof.w, n_tilde) || + !IsZnStarElement(proof.s, n)) { + return false; + } + if (proof.s1 < 0 || proof.s1 > QPow3(ctx) || proof.t1 < 0 || + proof.t1 > QPow7(ctx) || proof.s2 < 0 || proof.t2 < 0) { + return false; + } + + const Scalar e_scalar = BuildA2MtAwcChallenge(ctx, n, gamma, verifier_aux, c1, + c2, statement_x, proof); + + try { + const Scalar s1_mod_q(proof.s1, ctx.group); + if (s1_mod_q.value() == 0) { + return false; + } + const ECPoint lhs_curve = ECPoint::GeneratorMultiply(s1_mod_q); + ECPoint rhs_curve = proof.u; + if (e_scalar.value() != 0) { + rhs_curve = rhs_curve.Add(statement_x.Mul(e_scalar)); + } + if (lhs_curve != rhs_curve) { + return false; + } + } catch (const std::exception&) { + return false; + } + + const BigInt e = e_scalar.mp_value(); + const BigInt lhs_nt_1 = MulMod(PowMod(h1, proof.s1, n_tilde), + PowMod(h2, proof.s2, n_tilde), n_tilde); + const BigInt rhs_nt_1 = + MulMod(PowMod(proof.z, e, n_tilde), proof.z2, n_tilde); + if (lhs_nt_1 != rhs_nt_1) { + return false; + } + + const BigInt lhs_nt_2 = MulMod(PowMod(h1, proof.t1, n_tilde), + PowMod(h2, proof.t2, n_tilde), n_tilde); + const BigInt rhs_nt_2 = MulMod(PowMod(proof.t, e, n_tilde), proof.w, n_tilde); + if (lhs_nt_2 != rhs_nt_2) { + return false; + } + + BigInt lhs_paillier = + MulMod(PowMod(c1, proof.s1, n2), PowMod(proof.s, n, n2), n2); + lhs_paillier = MulMod(lhs_paillier, PowMod(gamma, proof.t1, n2), n2); + const BigInt rhs_paillier = MulMod(PowMod(c2, e, n2), proof.v, n2); + return lhs_paillier == rhs_paillier; +} + +A3MtAProof ProveA3MtA(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const BigInt& witness_x, + const BigInt& witness_y, const BigInt& witness_r) { + const BigInt n2 = n * n; + const BigInt gamma = n + BigInt(1); + const BigInt n_tilde = verifier_aux.n_tilde; + const BigInt h1 = verifier_aux.h1; + const BigInt h2 = verifier_aux.h2; + const BigInt q_mul_n_tilde = ctx.group->order() * n_tilde; + const BigInt q3_mul_n_tilde = QPow3(ctx) * n_tilde; + + while (true) { + const BigInt alpha = RandomBelow(QPow3(ctx)); + const BigInt rho = RandomBelow(q_mul_n_tilde); + const BigInt rho2 = RandomBelow(q3_mul_n_tilde); + const BigInt sigma = RandomBelow(q_mul_n_tilde); + const BigInt beta = SampleZnStar(n); + const BigInt gamma_rand = RandomBelow(QPow7(ctx)); + const BigInt tau = RandomBelow(q3_mul_n_tilde); + + const BigInt z = MulMod(PowMod(h1, witness_x, n_tilde), + PowMod(h2, rho, n_tilde), n_tilde); + const BigInt z2 = + MulMod(PowMod(h1, alpha, n_tilde), PowMod(h2, rho2, n_tilde), n_tilde); + const BigInt t = MulMod(PowMod(h1, witness_y, n_tilde), + PowMod(h2, sigma, n_tilde), n_tilde); + BigInt v = MulMod(PowMod(c1, alpha, n2), PowMod(gamma, gamma_rand, n2), n2); + v = MulMod(v, PowMod(beta, n, n2), n2); + const BigInt w = MulMod(PowMod(h1, gamma_rand, n_tilde), + PowMod(h2, tau, n_tilde), n_tilde); + + A3MtAProof proof{ + .z = z, + .z2 = z2, + .t = t, + .v = v, + .w = w, + }; + const Scalar e_scalar = + BuildA3MtAChallenge(ctx, n, gamma, verifier_aux, c1, c2, proof); + const BigInt e = e_scalar.mp_value(); + + const BigInt s = MulMod(PowMod(witness_r, e, n), beta, n); + const BigInt s1 = (e * witness_x) + alpha; + const BigInt s2 = (e * rho) + rho2; + const BigInt t1 = (e * witness_y) + gamma_rand; + const BigInt t2 = (e * sigma) + tau; + if (s1 > QPow3(ctx) || t1 > QPow7(ctx)) { + continue; + } + proof.s = s; + proof.s1 = s1; + proof.s2 = s2; + proof.t1 = t1; + proof.t2 = t2; + return proof; + } +} + +bool VerifyA3MtA(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const A3MtAProof& proof) { + const BigInt n2 = n * n; + const BigInt gamma = n + BigInt(1); + const BigInt n_tilde = verifier_aux.n_tilde; + const BigInt h1 = verifier_aux.h1; + const BigInt h2 = verifier_aux.h2; + + if (!IsInRange(c1, n2) || !IsInRange(c2, n2) || !IsInRange(proof.v, n2) || + !IsInRange(proof.z, n_tilde) || !IsInRange(proof.z2, n_tilde) || + !IsInRange(proof.t, n_tilde) || !IsInRange(proof.w, n_tilde) || + !IsZnStarElement(proof.s, n)) { + return false; + } + if (proof.s1 < 0 || proof.s1 > QPow3(ctx) || proof.t1 < 0 || + proof.t1 > QPow7(ctx) || proof.s2 < 0 || proof.t2 < 0) { + return false; + } + + const Scalar e_scalar = + BuildA3MtAChallenge(ctx, n, gamma, verifier_aux, c1, c2, proof); + const BigInt e = e_scalar.mp_value(); + + const BigInt lhs_nt_1 = MulMod(PowMod(h1, proof.s1, n_tilde), + PowMod(h2, proof.s2, n_tilde), n_tilde); + const BigInt rhs_nt_1 = + MulMod(PowMod(proof.z, e, n_tilde), proof.z2, n_tilde); + if (lhs_nt_1 != rhs_nt_1) { + return false; + } + + const BigInt lhs_nt_2 = MulMod(PowMod(h1, proof.t1, n_tilde), + PowMod(h2, proof.t2, n_tilde), n_tilde); + const BigInt rhs_nt_2 = MulMod(PowMod(proof.t, e, n_tilde), proof.w, n_tilde); + if (lhs_nt_2 != rhs_nt_2) { + return false; + } + + BigInt lhs_paillier = + MulMod(PowMod(c1, proof.s1, n2), PowMod(proof.s, n, n2), n2); + lhs_paillier = MulMod(lhs_paillier, PowMod(gamma, proof.t1, n2), n2); + const BigInt rhs_paillier = MulMod(PowMod(c2, e, n2), proof.v, n2); + return lhs_paillier == rhs_paillier; +} + +} // namespace tecdsa::core::mta diff --git a/yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h b/yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h new file mode 100644 index 00000000..980723be --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h @@ -0,0 +1,124 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::mta { + +using BigInt = core::paillier::BigInt; +using AuxRsaParams = core::paillier::AuxRsaParams; + +inline constexpr size_t kMtaInstanceIdLen = 16; + +struct MtaProofContext { + Bytes session_id; + PartyIndex initiator_id = 0; + PartyIndex responder_id = 0; + Bytes instance_id; + HashId transcript_hash = HashId::kSha256; + std::shared_ptr group; + std::string proof_domain_prefix = "GG2019"; +}; + +MtaProofContext BuildProofContext(const Bytes& session_id, + PartyIndex initiator_id, + PartyIndex responder_id, + const Bytes& instance_id, + const ThresholdSuite& suite, + std::shared_ptr group); + +// Internal transport proof carried by PairwiseProductSession. Canonical +// scheme-owned proofs live under ecdsa/proofs and sm2/proofs. +struct A1RangeProof { + BigInt z = BigInt(0); + BigInt u = BigInt(0); + BigInt w = BigInt(0); + BigInt s = BigInt(0); + BigInt s1 = BigInt(0); + BigInt s2 = BigInt(0); +}; + +// Internal transport proof carried by PairwiseProductSession. Canonical +// scheme-owned proofs live under ecdsa/proofs and sm2/proofs. +struct A2MtAwcProof { + ECPoint u; + BigInt z = BigInt(0); + BigInt z2 = BigInt(0); + BigInt t = BigInt(0); + BigInt v = BigInt(0); + BigInt w = BigInt(0); + BigInt s = BigInt(0); + BigInt s1 = BigInt(0); + BigInt s2 = BigInt(0); + BigInt t1 = BigInt(0); + BigInt t2 = BigInt(0); +}; + +// Internal transport proof carried by PairwiseProductSession. Canonical +// scheme-owned proofs live under ecdsa/proofs and sm2/proofs. +struct A3MtAProof { + BigInt z = BigInt(0); + BigInt z2 = BigInt(0); + BigInt t = BigInt(0); + BigInt v = BigInt(0); + BigInt w = BigInt(0); + BigInt s = BigInt(0); + BigInt s1 = BigInt(0); + BigInt s2 = BigInt(0); + BigInt t1 = BigInt(0); + BigInt t2 = BigInt(0); +}; + +BigInt RandomBelow(const BigInt& upper_exclusive); +BigInt SampleZnStar(const BigInt& modulus_n); +BigInt QPow5(const std::shared_ptr& group); +BigInt MulMod(const BigInt& lhs, const BigInt& rhs, const BigInt& modulus); +BigInt PowMod(const BigInt& base, const BigInt& exp, const BigInt& modulus); + +A1RangeProof ProveA1Range(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c, + const BigInt& witness_m, const BigInt& witness_r); +bool VerifyA1Range(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c, + const A1RangeProof& proof); + +A2MtAwcProof ProveA2MtAwc(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const ECPoint& statement_x, + const BigInt& witness_x, const BigInt& witness_y, + const BigInt& witness_r); +bool VerifyA2MtAwc(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const ECPoint& statement_x, + const A2MtAwcProof& proof); + +A3MtAProof ProveA3MtA(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const BigInt& witness_x, + const BigInt& witness_y, const BigInt& witness_r); +bool VerifyA3MtA(const MtaProofContext& ctx, const BigInt& n, + const AuxRsaParams& verifier_aux, const BigInt& c1, + const BigInt& c2, const A3MtAProof& proof); + +} // namespace tecdsa::core::mta diff --git a/yacl/crypto/experimental/threshold_signatures/core/mta/session.cc b/yacl/crypto/experimental/threshold_signatures/core/mta/session.cc new file mode 100644 index 00000000..ee43e03b --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/mta/session.cc @@ -0,0 +1,393 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/mta/session.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +namespace tecdsa::core::mta { +namespace { + +std::shared_ptr ResolveGroup( + const ThresholdSuite& suite, std::shared_ptr group) { + if (group == nullptr) { + group = GroupContext::Create(suite.curve); + } + if (group->curve_id() != suite.curve) { + TECDSA_THROW_ARGUMENT("PairwiseProductSession group must match suite curve"); + } + return group; +} + +void ValidateMtAwcSecretPointOrThrow( + const Scalar& responder_secret, const ECPoint& public_witness_point) { + try { + if (ECPoint::GeneratorMultiply(responder_secret) != public_witness_point) { + TECDSA_THROW_ARGUMENT( + "MtAwc public witness point does not match responder secret"); + } + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("failed to validate MtAwc point: ") + + ex.what()); + } +} + +void ValidatePublicWitnessPointPresenceOrThrow( + MtaType type, const ECPoint* public_witness_point, + const char* context_name) { + if (RequiresPublicPoint(type)) { + if (public_witness_point == nullptr) { + TECDSA_THROW_ARGUMENT(std::string(context_name) + + " requires a public witness point"); + } + return; + } + if (public_witness_point != nullptr) { + TECDSA_THROW_ARGUMENT(std::string(context_name) + + " does not use a public witness point"); + } +} + +void ValidateExpectedMtaTypeOrThrow(MtaType actual, MtaType expected, + const char* context_name) { + if (actual != expected) { + TECDSA_THROW_ARGUMENT(std::string(context_name) + " type mismatch"); + } +} + +} // namespace + +Bytes RandomMtaInstanceId() { return Csprng::RandomBytes(kMtaInstanceIdLen); } + +std::string BytesToKey(const Bytes& bytes) { + return std::string(reinterpret_cast(bytes.data()), bytes.size()); +} + +std::string MtaTypeKey(MtaType type) { + switch (type) { + case MtaType::kMta: + return "mta"; + case MtaType::kMtAwc: + return "mtawc"; + } + TECDSA_THROW_ARGUMENT("unknown MtaType"); +} + +PairwiseProductSession::PairwiseProductSession(Config cfg) + : cfg_(std::move(cfg)) { + if (!cfg_.suite.has_value()) { + TECDSA_THROW_ARGUMENT("PairwiseProductSession suite must be explicit"); + } + cfg_.group = ResolveGroup(*cfg_.suite, std::move(cfg_.group)); +} + +void PairwiseProductSession::RegisterInitiatorInstance( + PairwiseProductInitiatorInstance instance) { + if (instance.instance_id.size() != kMtaInstanceIdLen) { + TECDSA_THROW_ARGUMENT("initiator instance id has invalid length"); + } + + const std::string instance_key = BytesToKey(instance.instance_id); + if (consumed_request_keys_.contains(instance_key)) { + TECDSA_THROW_ARGUMENT("initiator instance id conflicts with a consumed request"); + } + generated_instance_keys_.insert(instance_key); + if (!pending_initiator_instances_ + .emplace(instance_key, std::move(instance)) + .second) { + TECDSA_THROW_ARGUMENT("duplicate initiator instance id"); + } +} + +const PairwiseProductInitiatorInstance& +PairwiseProductSession::GetInitiatorInstance(const Bytes& instance_id) const { + const std::string instance_key = BytesToKey(instance_id); + const auto it = pending_initiator_instances_.find(instance_key); + if (it == pending_initiator_instances_.end()) { + TECDSA_THROW_ARGUMENT("unknown initiator instance id"); + } + return it->second; +} + +PairwiseProductRequest PairwiseProductSession::InitiatorInit( + const InitiatorInitArgs& args) { + return CreateRequestImpl(MtaType::kMta, args); +} + +PairwiseProductRequest PairwiseProductSession::InitiatorInitWithCheck( + const InitiatorInitArgs& args) { + return CreateRequestImpl(MtaType::kMtAwc, args); +} + +PairwiseProductRequest PairwiseProductSession::CreateRequestImpl( + MtaType type, const InitiatorInitArgs& args) { + if (args.initiator_paillier == nullptr) { + TECDSA_THROW_ARGUMENT("initiator Paillier provider must be present"); + } + if (args.responder_aux == nullptr) { + TECDSA_THROW_ARGUMENT("responder auxiliary parameters must be present"); + } + if (args.responder_id == cfg_.self_id) { + TECDSA_THROW_ARGUMENT("responder id must be a peer"); + } + + const Bytes instance_id = ReserveFreshInstanceId(); + const BigInt n = args.initiator_paillier->modulus_n_bigint(); + const PaillierCiphertextWithRandomBigInt encrypted = + args.initiator_paillier->EncryptWithRandomBigInt( + args.initiator_secret.mp_value()); + + const A1RangeProof a1_proof = ProveA1Range( + BuildProofContext(cfg_.session_id, cfg_.self_id, args.responder_id, + instance_id, *cfg_.suite, cfg_.group), + n, *args.responder_aux, encrypted.ciphertext, + args.initiator_secret.mp_value(), encrypted.randomness); + + RegisterInitiatorInstance(PairwiseProductInitiatorInstance{ + .responder = args.responder_id, + .type = type, + .instance_id = instance_id, + .c1 = encrypted.ciphertext, + }); + return PairwiseProductRequest{ + .from = cfg_.self_id, + .to = args.responder_id, + .type = type, + .instance_id = instance_id, + .c1 = encrypted.ciphertext, + .a1_proof = a1_proof, + }; +} + +PairwiseProductSession::ConsumeRequestResult +PairwiseProductSession::ResponderMid(const PairwiseProductRequest& request, + const ResponderMidArgs& args) { + return ConsumeRequestImpl(request, MtaType::kMta, args.initiator_modulus_n, + args.responder_aux, args.initiator_aux, + args.responder_secret, nullptr); +} + +PairwiseProductSession::ConsumeRequestResult +PairwiseProductSession::ResponderMidWithCheck( + const PairwiseProductRequest& request, + const ResponderMidWithCheckArgs& args) { + return ConsumeRequestImpl(request, MtaType::kMtAwc, + args.initiator_modulus_n, args.responder_aux, + args.initiator_aux, args.responder_secret, + &args.public_witness_point); +} + +PairwiseProductSession::ConsumeRequestResult +PairwiseProductSession::ConsumeRequestImpl( + const PairwiseProductRequest& request, MtaType expected_type, + const BigInt& initiator_modulus_n, const AuxRsaParams* responder_aux, + const AuxRsaParams* initiator_aux, const Scalar& responder_secret, + const ECPoint* public_witness_point) { + ValidateExpectedMtaTypeOrThrow(request.type, expected_type, + "pairwise product request"); + if (responder_aux == nullptr) { + TECDSA_THROW_ARGUMENT("responder auxiliary parameters must be present"); + } + if (initiator_aux == nullptr) { + TECDSA_THROW_ARGUMENT("initiator auxiliary parameters must be present"); + } + if (request.to != cfg_.self_id) { + TECDSA_THROW_ARGUMENT("pairwise product request must target self"); + } + if (request.from == cfg_.self_id) { + TECDSA_THROW_ARGUMENT("pairwise product request sender must be a peer"); + } + if (request.instance_id.size() != kMtaInstanceIdLen) { + TECDSA_THROW_ARGUMENT("pairwise product request instance id has invalid length"); + } + + const std::string instance_key = BytesToKey(request.instance_id); + if (generated_instance_keys_.contains(instance_key) || + consumed_request_keys_.contains(instance_key)) { + TECDSA_THROW_ARGUMENT("duplicate pairwise product request instance id"); + } + + const BigInt n = initiator_modulus_n; + const BigInt n2 = n * n; + if (request.c1 < 0 || request.c1 >= n2) { + TECDSA_THROW_ARGUMENT("pairwise product request ciphertext c1 is out of range"); + } + + if (!VerifyA1Range( + BuildProofContext(cfg_.session_id, request.from, cfg_.self_id, + request.instance_id, *cfg_.suite, cfg_.group), + n, *responder_aux, request.c1, request.a1_proof)) { + TECDSA_THROW_ARGUMENT("pairwise product A1 proof verification failed"); + } + + ValidatePublicWitnessPointPresenceOrThrow(request.type, + public_witness_point, + "pairwise product responder input"); + if (request.type == MtaType::kMtAwc) { + ValidateMtAwcSecretPointOrThrow(responder_secret, + *public_witness_point); + } + + const BigInt y = RandomBelow(QPow5(cfg_.group)); + const BigInt r_b = SampleZnStar(n); + const BigInt gamma = n + BigInt(1); + const BigInt c1_pow_x = + PowMod(request.c1, responder_secret.mp_value(), n2); + const BigInt gamma_pow_y = PowMod(gamma, y, n2); + const BigInt r_pow_n = PowMod(r_b, n, n2); + const BigInt c2 = + MulMod(MulMod(c1_pow_x, gamma_pow_y, n2), r_pow_n, n2); + + PairwiseProductResponse response{ + .from = cfg_.self_id, + .to = request.from, + .type = request.type, + .instance_id = request.instance_id, + .c2 = c2, + .a2_proof = std::nullopt, + .a3_proof = std::nullopt, + }; + + if (request.type == MtaType::kMta) { + response.a3_proof = ProveA3MtA( + BuildProofContext(cfg_.session_id, request.from, cfg_.self_id, + request.instance_id, *cfg_.suite, cfg_.group), + n, *initiator_aux, request.c1, c2, + responder_secret.mp_value(), y, r_b); + } else { + response.a2_proof = ProveA2MtAwc( + BuildProofContext(cfg_.session_id, request.from, cfg_.self_id, + request.instance_id, *cfg_.suite, cfg_.group), + n, *initiator_aux, request.c1, c2, *public_witness_point, + responder_secret.mp_value(), y, r_b); + } + + consumed_request_keys_.insert(instance_key); + return ConsumeRequestResult{ + .response = std::move(response), + .responder_share = Scalar(-y, cfg_.group), + }; +} + +PairwiseProductSession::ConsumeResponseResult +PairwiseProductSession::InitiatorEnd(const PairwiseProductResponse& response, + const InitiatorEndArgs& args) { + return ConsumeResponseImpl(response, MtaType::kMta, args.initiator_paillier, + args.initiator_aux, nullptr); +} + +PairwiseProductSession::ConsumeResponseResult +PairwiseProductSession::InitiatorEndWithCheck( + const PairwiseProductResponse& response, + const InitiatorEndWithCheckArgs& args) { + return ConsumeResponseImpl(response, MtaType::kMtAwc, + args.initiator_paillier, args.initiator_aux, + &args.public_witness_point); +} + +PairwiseProductSession::ConsumeResponseResult +PairwiseProductSession::ConsumeResponseImpl( + const PairwiseProductResponse& response, MtaType expected_type, + const PaillierProvider* initiator_paillier, + const AuxRsaParams* initiator_aux, const ECPoint* public_witness_point) { + ValidateExpectedMtaTypeOrThrow(response.type, expected_type, + "pairwise product response"); + if (initiator_paillier == nullptr) { + TECDSA_THROW_ARGUMENT("initiator Paillier provider must be present"); + } + if (initiator_aux == nullptr) { + TECDSA_THROW_ARGUMENT("initiator auxiliary parameters must be present"); + } + if (response.to != cfg_.self_id) { + TECDSA_THROW_ARGUMENT("pairwise product response must target self"); + } + if (response.from == cfg_.self_id) { + TECDSA_THROW_ARGUMENT("pairwise product response sender must be a peer"); + } + if (response.instance_id.size() != kMtaInstanceIdLen) { + TECDSA_THROW_ARGUMENT("pairwise product response instance id has invalid length"); + } + + const std::string instance_key = BytesToKey(response.instance_id); + const auto instance_it = pending_initiator_instances_.find(instance_key); + if (instance_it == pending_initiator_instances_.end()) { + TECDSA_THROW_ARGUMENT("unknown pairwise product response instance id"); + } + const PairwiseProductInitiatorInstance& instance = instance_it->second; + if (instance.responder != response.from) { + TECDSA_THROW_ARGUMENT("pairwise product response sender mismatch"); + } + if (instance.type != response.type) { + TECDSA_THROW_ARGUMENT("pairwise product response type mismatch"); + } + + const BigInt n = initiator_paillier->modulus_n_bigint(); + const BigInt n2 = n * n; + if (response.c2 < 0 || response.c2 >= n2) { + TECDSA_THROW_ARGUMENT("pairwise product response ciphertext c2 is out of range"); + } + + ValidatePublicWitnessPointPresenceOrThrow(response.type, + public_witness_point, + "pairwise product response input"); + + if (response.type == MtaType::kMta) { + if (!response.a3_proof.has_value() || response.a2_proof.has_value()) { + TECDSA_THROW_ARGUMENT("MtA response must carry only an A3 proof"); + } + if (!VerifyA3MtA( + BuildProofContext(cfg_.session_id, cfg_.self_id, response.from, + response.instance_id, *cfg_.suite, cfg_.group), + n, *initiator_aux, instance.c1, response.c2, + *response.a3_proof)) { + TECDSA_THROW_ARGUMENT("pairwise product A3 proof verification failed"); + } + } else { + if (!response.a2_proof.has_value() || response.a3_proof.has_value()) { + TECDSA_THROW_ARGUMENT("MtAwc response must carry only an A2 proof"); + } + if (!VerifyA2MtAwc( + BuildProofContext(cfg_.session_id, cfg_.self_id, response.from, + response.instance_id, *cfg_.suite, cfg_.group), + n, *initiator_aux, instance.c1, response.c2, + *public_witness_point, *response.a2_proof)) { + TECDSA_THROW_ARGUMENT("pairwise product A2 proof verification failed"); + } + } + + const Scalar initiator_share( + initiator_paillier->DecryptBigInt(response.c2), cfg_.group); + pending_initiator_instances_.erase(instance_it); + return ConsumeResponseResult{.initiator_share = initiator_share}; +} + +Bytes PairwiseProductSession::ReserveFreshInstanceId() { + while (true) { + Bytes instance_id = RandomMtaInstanceId(); + const std::string instance_key = BytesToKey(instance_id); + if (generated_instance_keys_.contains(instance_key) || + consumed_request_keys_.contains(instance_key)) { + continue; + } + generated_instance_keys_.insert(instance_key); + return instance_id; + } +} + +} // namespace tecdsa::core::mta diff --git a/yacl/crypto/experimental/threshold_signatures/core/mta/session.h b/yacl/crypto/experimental/threshold_signatures/core/mta/session.h new file mode 100644 index 00000000..e83d8d07 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/mta/session.h @@ -0,0 +1,237 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/mta/messages.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::mta { + +using PaillierProvider = core::paillier::PaillierProvider; +using PaillierCiphertextWithRandomBigInt = + core::paillier::PaillierCiphertextWithRandomBigInt; + +Bytes RandomMtaInstanceId(); +std::string BytesToKey(const Bytes& bytes); +std::string MtaTypeKey(MtaType type); + +template +void RequireExactlyOneRequestPerPeerAndType( + const std::vector& requests, std::span peers, + PartyIndex self_id, std::span expected_types, + ToCoreRequest to_core_request, const std::string& message_name) { + std::vector expected_type_storage; + std::vector expected_type_keys; + expected_type_storage.reserve(expected_types.size()); + expected_type_keys.reserve(expected_types.size()); + for (MtaType type : expected_types) { + expected_type_storage.push_back(MtaTypeKey(type)); + expected_type_keys.push_back(expected_type_storage.back()); + } + tecdsa::core::protocol::PeerTypeMessageStore peer_type_messages( + peers, self_id, expected_type_keys, message_name); + peer_type_messages.RequireExpectedCount(requests.size()); + + std::unordered_set seen_instance_keys; + seen_instance_keys.reserve(requests.size()); + for (const auto& request : requests) { + const PairwiseProductRequest core_request = to_core_request(request); + peer_type_messages.Add(core_request.from, core_request.to, + MtaTypeKey(core_request.type)); + if (core_request.instance_id.size() != kMtaInstanceIdLen) { + TECDSA_THROW_ARGUMENT(message_name + " instance id has invalid length"); + } + + const std::string instance_key = BytesToKey(core_request.instance_id); + if (!seen_instance_keys.insert(instance_key).second) { + TECDSA_THROW_ARGUMENT("duplicate " + message_name + " instance id"); + } + } +} + +template +void RequireExactlyOneRequestPerPeer( + const std::vector& requests, std::span peers, + PartyIndex self_id, MtaType expected_type, ToCoreRequest to_core_request, + const std::string& message_name) { + const MtaType expected_types[] = {expected_type}; + RequireExactlyOneRequestPerPeerAndType( + requests, peers, self_id, std::span(expected_types), + to_core_request, message_name); +} + +struct PairwiseProductInitiatorInstance { + PartyIndex responder = 0; + MtaType type = MtaType::kMta; + Bytes instance_id; + BigInt c1 = BigInt(0); +}; + +class PairwiseProductSession { + public: + struct Config { + Bytes session_id; + PartyIndex self_id = 0; + std::optional suite; + std::shared_ptr group; + }; + + struct InitiatorInitArgs { + PartyIndex responder_id = 0; + const PaillierProvider* initiator_paillier = nullptr; + const AuxRsaParams* responder_aux = nullptr; + Scalar initiator_secret; + }; + + struct ResponderMidArgs { + BigInt initiator_modulus_n = BigInt(0); + const AuxRsaParams* responder_aux = nullptr; + const AuxRsaParams* initiator_aux = nullptr; + Scalar responder_secret; + }; + + struct ResponderMidWithCheckArgs { + BigInt initiator_modulus_n = BigInt(0); + const AuxRsaParams* responder_aux = nullptr; + const AuxRsaParams* initiator_aux = nullptr; + Scalar responder_secret; + const ECPoint& public_witness_point; + }; + + struct ConsumeRequestResult { + PairwiseProductResponse response; + Scalar responder_share; + }; + + struct InitiatorEndArgs { + const PaillierProvider* initiator_paillier = nullptr; + const AuxRsaParams* initiator_aux = nullptr; + }; + + struct InitiatorEndWithCheckArgs { + const PaillierProvider* initiator_paillier = nullptr; + const AuxRsaParams* initiator_aux = nullptr; + const ECPoint& public_witness_point; + }; + + struct ConsumeResponseResult { + Scalar initiator_share; + }; + + explicit PairwiseProductSession(Config cfg); + + const PairwiseProductInitiatorInstance& GetInitiatorInstance( + const Bytes& instance_id) const; + + PairwiseProductRequest InitiatorInit(const InitiatorInitArgs& args); + PairwiseProductRequest InitiatorInitWithCheck( + const InitiatorInitArgs& args); + ConsumeRequestResult ResponderMid(const PairwiseProductRequest& request, + const ResponderMidArgs& args); + ConsumeRequestResult ResponderMidWithCheck( + const PairwiseProductRequest& request, + const ResponderMidWithCheckArgs& args); + ConsumeResponseResult InitiatorEnd(const PairwiseProductResponse& response, + const InitiatorEndArgs& args); + ConsumeResponseResult InitiatorEndWithCheck( + const PairwiseProductResponse& response, + const InitiatorEndWithCheckArgs& args); + + private: + PairwiseProductRequest CreateRequestImpl(MtaType type, + const InitiatorInitArgs& args); + ConsumeRequestResult ConsumeRequestImpl( + const PairwiseProductRequest& request, MtaType expected_type, + const BigInt& initiator_modulus_n, const AuxRsaParams* responder_aux, + const AuxRsaParams* initiator_aux, const Scalar& responder_secret, + const ECPoint* public_witness_point); + ConsumeResponseResult ConsumeResponseImpl( + const PairwiseProductResponse& response, MtaType expected_type, + const PaillierProvider* initiator_paillier, + const AuxRsaParams* initiator_aux, const ECPoint* public_witness_point); + Bytes ReserveFreshInstanceId(); + void RegisterInitiatorInstance(PairwiseProductInitiatorInstance instance); + + Config cfg_; + std::unordered_map + pending_initiator_instances_; + std::unordered_set generated_instance_keys_; + std::unordered_set consumed_request_keys_; +}; + +template +void RequireExactlyOneResponsePerInitiatorInstance( + const std::vector& responses, std::span peers, + size_t expected_response_count, PartyIndex self_id, + const PairwiseProductSession& session, ToCoreResponse to_core_response, + const std::string& message_name) { + if (responses.size() != expected_response_count) { + TECDSA_THROW_ARGUMENT(message_name + + " must contain exactly one response per request"); + } + + std::unordered_set seen_request_keys; + std::unordered_set seen_instance_keys; + seen_request_keys.reserve(responses.size()); + seen_instance_keys.reserve(responses.size()); + for (const auto& response : responses) { + const PairwiseProductResponse core_response = to_core_response(response); + if (!tecdsa::core::protocol::IsPeer(peers, core_response.from)) { + TECDSA_THROW_ARGUMENT(message_name + " sender is not a peer"); + } + if (core_response.to != self_id) { + TECDSA_THROW_ARGUMENT(message_name + " must target self"); + } + if (core_response.instance_id.size() != kMtaInstanceIdLen) { + TECDSA_THROW_ARGUMENT(message_name + " instance id has invalid length"); + } + + const std::string instance_key = BytesToKey(core_response.instance_id); + if (!seen_instance_keys.insert(instance_key).second) { + TECDSA_THROW_ARGUMENT("duplicate " + message_name + " instance id"); + } + + const auto& instance = + session.GetInitiatorInstance(core_response.instance_id); + if (instance.responder != core_response.from) { + TECDSA_THROW_ARGUMENT(message_name + " sender mismatch"); + } + if (instance.type != core_response.type) { + TECDSA_THROW_ARGUMENT(message_name + " type mismatch"); + } + const std::string request_key = + tecdsa::core::protocol::MakePeerTypeKey( + core_response.from, MtaTypeKey(core_response.type)); + if (!seen_request_keys.insert(request_key).second) { + TECDSA_THROW_ARGUMENT("duplicate " + message_name + " for sender/type"); + } + } +} + +} // namespace tecdsa::core::mta diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.cc b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.cc new file mode 100644 index 00000000..b275db0c --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.cc @@ -0,0 +1,154 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +namespace tecdsa::core::paillier { +namespace spi = paillier_internal; + +bool IsZnStarElement(const BigInt& value, const BigInt& modulus) { + if (modulus <= 2 || value <= 0 || value >= modulus) { + return false; + } + return BigInt::Gcd(value, modulus) == 1; +} + +bool ValidateAuxRsaParams(const AuxRsaParams& params) { + if (params.n_tilde <= 2 || params.h1 == params.h2) { + return false; + } + return IsZnStarElement(params.h1, params.n_tilde) && + IsZnStarElement(params.h2, params.n_tilde); +} + +bool IsLikelySquareFreeModulus(const BigInt& modulus_n) { + if (modulus_n <= 2 || modulus_n.IsEven() || spi::IsPerfectSquare(modulus_n)) { + return false; + } + + static constexpr std::array kSmallPrimes = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, + 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, + 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, + 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, + 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, + 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, + 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, + 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, + 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, + 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, + 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997, + }; + + for (unsigned long prime : kSmallPrimes) { + const unsigned long prime_square = prime * prime; + if (modulus_n.Mod(BigInt(prime_square)) == 0) { + return false; + } + } + + return true; +} + +SquareFreeProof BuildSquareFreeProofGmr98( + const BigInt& modulus_n, const BigInt& lambda_n, + const StrictProofVerifierContext& context) { + if (modulus_n <= 3) { + TECDSA_THROW_ARGUMENT("square-free GMR98 proof requires modulus N > 3"); + } + if (lambda_n <= 1) { + TECDSA_THROW_ARGUMENT("square-free GMR98 proof requires lambda(N) > 1"); + } + if (!IsLikelySquareFreeModulus(modulus_n)) { + TECDSA_THROW_ARGUMENT( + "square-free GMR98 proof requires likely square-free modulus"); + } + + const auto d_opt = + spi::InvertMod(spi::NormalizeMod(modulus_n, lambda_n), lambda_n); + if (!d_opt.has_value()) { + TECDSA_THROW_ARGUMENT( + "square-free GMR98 proof requires gcd(N, lambda(N)) = 1"); + } + const BigInt d = *d_opt; + const Bytes nonce = Csprng::RandomBytes(spi::kStrictNonceLen); + + spi::SquareFreeGmr98Payload payload; + payload.nonce = nonce; + payload.rounds = static_cast(spi::kSquareFreeGmr98Rounds); + payload.roots.reserve(payload.rounds); + + for (uint32_t round = 0; round < payload.rounds; ++round) { + const BigInt challenge = + spi::DeriveSquareFreeGmr98Challenge(modulus_n, context, nonce, round); + const BigInt root = spi::PowMod(challenge, d, modulus_n); + if (!spi::IsZnStarResidue(root, modulus_n)) { + TECDSA_THROW("square-free GMR98 proof generated invalid root"); + } + if (spi::PowMod(root, modulus_n, modulus_n) != challenge) { + TECDSA_THROW( + "square-free GMR98 proof generated inconsistent root equation"); + } + payload.roots.push_back(root); + } + + return SquareFreeProof{.blob = spi::EncodeSquareFreeGmr98Payload(payload)}; +} + +bool VerifySquareFreeProofGmr98(const BigInt& modulus_n, + const SquareFreeProof& proof, + const StrictProofVerifierContext& context) { + if (!IsLikelySquareFreeModulus(modulus_n) || proof.blob.empty()) { + return false; + } + + spi::SquareFreeGmr98Payload payload; + try { + payload = spi::DecodeSquareFreeGmr98Payload(proof.blob); + } catch (const std::exception&) { + return false; + } + if (payload.nonce.size() != spi::kStrictNonceLen || + payload.rounds != spi::kSquareFreeGmr98Rounds || + payload.roots.size() != payload.rounds) { + return false; + } + + for (uint32_t round = 0; round < payload.rounds; ++round) { + const BigInt& root = payload.roots[round]; + if (!spi::IsZnStarResidue(root, modulus_n)) { + return false; + } + const BigInt challenge = + spi::DeriveSquareFreeGmr98Challenge(modulus_n, context, payload.nonce, + round); + if (spi::PowMod(root, modulus_n, modulus_n) != challenge) { + return false; + } + } + return true; +} + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h new file mode 100644 index 00000000..e07d8b47 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h @@ -0,0 +1,44 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" + +namespace tecdsa::core::paillier { + +struct AuxRsaParams { + BigInt n_tilde = BigInt(0); + BigInt h1 = BigInt(0); + BigInt h2 = BigInt(0); +}; + +struct SquareFreeProof { + Bytes blob; +}; + +bool IsZnStarElement(const BigInt& value, const BigInt& modulus); +bool ValidateAuxRsaParams(const AuxRsaParams& params); +bool IsLikelySquareFreeModulus(const BigInt& modulus_n); + +SquareFreeProof BuildSquareFreeProofGmr98( + const BigInt& modulus_n, const BigInt& lambda_n, + const StrictProofVerifierContext& context); +bool VerifySquareFreeProofGmr98(const BigInt& modulus_n, + const SquareFreeProof& proof, + const StrictProofVerifierContext& context); + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_codec.cc b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_codec.cc new file mode 100644 index 00000000..752421e5 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_codec.cc @@ -0,0 +1,79 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h" + +namespace tecdsa::core::paillier_internal { +namespace { + +void AppendMpIntField(const BigInt& value, Bytes* out) { + const Bytes encoded = encoding::EncodeMpInt(value); + encoding::AppendSizedField(encoded, out); +} + +BigInt ReadMpIntField(std::span input, size_t* offset, + const char* field_name) { + const Bytes encoded = + encoding::ReadSizedField(input, offset, kMaxStrictFieldLen, field_name); + return encoding::DecodeMpInt(encoded, kMaxStrictFieldLen); +} + +} // namespace + +Bytes EncodeSquareFreeGmr98Payload(const SquareFreeGmr98Payload& payload) { + if (payload.rounds == 0 || payload.rounds > kMaxSquareFreeGmr98Rounds) { + TECDSA_THROW_ARGUMENT("square-free GMR98 rounds out of range"); + } + if (payload.roots.size() != payload.rounds) { + TECDSA_THROW_ARGUMENT("square-free GMR98 roots count mismatch"); + } + + Bytes out; + encoding::AppendSizedField(payload.nonce, &out); + encoding::AppendU32Be(payload.rounds, &out); + for (const BigInt& root : payload.roots) { + AppendMpIntField(root, &out); + } + return out; +} + +SquareFreeGmr98Payload DecodeSquareFreeGmr98Payload( + std::span blob) { + size_t offset = 0; + SquareFreeGmr98Payload payload; + payload.nonce = encoding::ReadSizedField(blob, &offset, kMaxStrictNonceLen, + "square-free GMR98 nonce"); + payload.rounds = encoding::ReadU32Be(blob, &offset); + if (payload.rounds == 0 || payload.rounds > kMaxSquareFreeGmr98Rounds) { + TECDSA_THROW_ARGUMENT("square-free GMR98 rounds out of range"); + } + payload.roots.reserve(payload.rounds); + for (uint32_t i = 0; i < payload.rounds; ++i) { + payload.roots.push_back( + ReadMpIntField(blob, &offset, "square-free GMR98 root")); + } + if (offset != blob.size()) { + TECDSA_THROW_ARGUMENT("square-free GMR98 payload has trailing bytes"); + } + return payload; +} + +} // namespace tecdsa::core::paillier_internal diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h new file mode 100644 index 00000000..e1121268 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h @@ -0,0 +1,73 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" + +namespace tecdsa::core::paillier_internal { + +using BigInt = paillier::BigInt; + +inline constexpr char kSquareFreeProofNameGmr98[] = "SquareFreeGMR98"; +inline constexpr size_t kStrictNonceLen = 32; +inline constexpr size_t kMaxStrictNonceLen = 256; +inline constexpr size_t kMaxStrictFieldLen = 8192; +inline constexpr size_t kSquareFreeGmr98Rounds = 24; +inline constexpr size_t kMaxSquareFreeGmr98Rounds = 128; +inline constexpr size_t kMaxSquareFreeGmr98ChallengeAttempts = 64; +inline constexpr size_t kMaxAuxParamGenerationAttempts = 128; + +struct SquareFreeGmr98Payload { + Bytes nonce; + uint32_t rounds = 0; + std::vector roots; +}; + +struct AuxRsaParamsBigInt { + BigInt n_tilde; + BigInt h1; + BigInt h2; +}; + +BigInt RandomBelow(const BigInt& upper_exclusive); +BigInt RandomZnStar(const BigInt& modulus_n); +bool IsZnStarResidue(const BigInt& value, const BigInt& modulus); +BigInt NormalizeMod(const BigInt& value, const BigInt& modulus); +BigInt MulMod(const BigInt& lhs, const BigInt& rhs, const BigInt& modulus); +BigInt PowMod(const BigInt& base, const BigInt& exp, const BigInt& modulus); +std::optional InvertMod(const BigInt& value, const BigInt& modulus); +bool IsPerfectSquare(const BigInt& value); + +AuxRsaParamsBigInt ToBigIntParams(const paillier::AuxRsaParams& params); + +BigInt DeriveSquareFreeGmr98Challenge( + const BigInt& modulus_n, const paillier::StrictProofVerifierContext& context, + std::span nonce, uint32_t round_idx); + +Bytes EncodeSquareFreeGmr98Payload(const SquareFreeGmr98Payload& payload); +SquareFreeGmr98Payload DecodeSquareFreeGmr98Payload( + std::span blob); + +BigInt PickCoprimeDeterministic(const BigInt& modulus, const BigInt& seed); + +} // namespace tecdsa::core::paillier_internal diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_math.cc b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_math.cc new file mode 100644 index 00000000..5c5e5de9 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_math.cc @@ -0,0 +1,212 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h" +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::paillier_internal { +namespace { + +void AppendVerifierContext(transcript::Transcript* transcript, + const paillier::StrictProofVerifierContext& context) { + if (!context.session_id.empty()) { + transcript->append_session_id(context.session_id); + } + if (context.prover_id.has_value()) { + transcript->append_u32_be("prover_id", *context.prover_id); + } + if (context.verifier_id.has_value()) { + transcript->append_u32_be("verifier_id", *context.verifier_id); + } +} + +void AppendU32Be(uint32_t value, Bytes* out) { + out->push_back(static_cast((value >> 24) & 0xFF)); + out->push_back(static_cast((value >> 16) & 0xFF)); + out->push_back(static_cast((value >> 8) & 0xFF)); + out->push_back(static_cast(value & 0xFF)); +} + +bool IsInRange(const BigInt& value, const BigInt& modulus) { + return value >= 0 && value < modulus; +} + +Bytes ExpandHashStream(std::span seed, size_t out_len) { + if (out_len == 0) { + return {}; + } + + Bytes out; + out.reserve(out_len); + uint32_t block = 0; + while (out.size() < out_len) { + Bytes block_input(seed.begin(), seed.end()); + AppendU32Be(block, &block_input); + const Bytes digest = Hash(HashId::kSha256, block_input); + const size_t remaining = out_len - out.size(); + const size_t take = std::min(remaining, digest.size()); + out.insert(out.end(), digest.begin(), + digest.begin() + static_cast(take)); + ++block; + } + return out; +} + +std::string BuildProofId(const paillier::StrictProofVerifierContext& context, + const char* proof_name) { + return context.proof_domain_prefix + "/" + proof_name + "/v1"; +} + +} // namespace + +BigInt RandomBelow(const BigInt& upper_exclusive) { + if (upper_exclusive <= 0) { + TECDSA_THROW_ARGUMENT("random upper bound must be positive"); + } + return bigint::RandomBelow(upper_exclusive); +} + +BigInt RandomZnStar(const BigInt& modulus_n) { + if (modulus_n <= 2) { + TECDSA_THROW_ARGUMENT("modulus must be > 2"); + } + return bigint::RandomZnStar(modulus_n); +} + +bool IsZnStarResidue(const BigInt& value, const BigInt& modulus) { + if (!IsInRange(value, modulus) || value == 0) { + return false; + } + const BigInt gcd = BigInt::Gcd(value, modulus); + return gcd == 1; +} + +BigInt NormalizeMod(const BigInt& value, const BigInt& modulus) { + return bigint::NormalizeMod(value, modulus); +} + +BigInt MulMod(const BigInt& lhs, const BigInt& rhs, const BigInt& modulus) { + return NormalizeMod(lhs * rhs, modulus); +} + +BigInt PowMod(const BigInt& base, const BigInt& exp, const BigInt& modulus) { + if (exp < 0) { + TECDSA_THROW_ARGUMENT("modular exponent must be non-negative"); + } + return bigint::PowMod(base, exp, modulus); +} + +std::optional InvertMod(const BigInt& value, const BigInt& modulus) { + return bigint::TryInvertMod(value, modulus); +} + +bool IsPerfectSquare(const BigInt& value) { + if (value < 0) { + return false; + } + if (value <= 1) { + return true; + } + + BigInt low(1); + BigInt high = BigInt(1) << (((value.BitCount() + 1) / 2) + 1); + while (low <= high) { + const BigInt mid = (low + high) >> 1; + const BigInt sq = mid * mid; + if (sq == value) { + return true; + } + if (sq < value) { + low = mid + BigInt(1); + } else { + high = mid - BigInt(1); + } + } + return false; +} + +AuxRsaParamsBigInt ToBigIntParams(const paillier::AuxRsaParams& params) { + return AuxRsaParamsBigInt{ + .n_tilde = params.n_tilde, + .h1 = params.h1, + .h2 = params.h2, + }; +} + +BigInt DeriveSquareFreeGmr98Challenge( + const BigInt& modulus_n, const paillier::StrictProofVerifierContext& context, + std::span nonce, uint32_t round_idx) { + if (modulus_n <= 3) { + TECDSA_THROW_ARGUMENT("square-free GMR98 challenge requires modulus N > 3"); + } + + const Bytes n_bytes = encoding::EncodeMpInt(modulus_n); + const size_t byte_len = std::max(1, (modulus_n.BitCount() + 7) / 8); + + for (uint32_t attempt = 0; attempt < kMaxSquareFreeGmr98ChallengeAttempts; + ++attempt) { + transcript::Transcript transcript(context.transcript_hash); + const std::string proof_id = + BuildProofId(context, kSquareFreeProofNameGmr98); + transcript.append_proof_id(proof_id); + AppendVerifierContext(&transcript, context); + transcript.append_fields({ + transcript::TranscriptFieldRef{.label = "N", .data = n_bytes}, + transcript::TranscriptFieldRef{.label = "nonce", .data = nonce}, + }); + transcript.append_u32_be("round", round_idx); + transcript.append_u32_be("attempt", attempt); + + const Bytes seed = Hash(context.transcript_hash, transcript.bytes()); + const Bytes expanded = ExpandHashStream(seed, byte_len); + BigInt candidate = bigint::FromBigEndian(expanded); + candidate = NormalizeMod(candidate, modulus_n); + if (IsZnStarResidue(candidate, modulus_n)) { + return candidate; + } + } + + TECDSA_THROW("failed to derive square-free GMR98 challenge in Z*_N"); +} + +BigInt PickCoprimeDeterministic(const BigInt& modulus, const BigInt& seed) { + BigInt value = NormalizeMod(seed, modulus); + if (value < 2) { + value = BigInt(2); + } + + while (true) { + if (value >= modulus) { + value = BigInt(2); + } + const BigInt gcd = BigInt::Gcd(value, modulus); + if (gcd == 1) { + return value; + } + value += BigInt(1); + } +} + +} // namespace tecdsa::core::paillier_internal diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.cc b/yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.cc new file mode 100644 index 00000000..ae58a70b --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.cc @@ -0,0 +1,278 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +namespace tecdsa::core::paillier { +namespace { + +constexpr size_t kMaxKeygenAttempts = 128; + +} // namespace + +BigInt MinPaillierModulusQ8( + const std::shared_ptr& challenge_group) { + BigInt out(1); + for (size_t i = 0; i < 8; ++i) { + out *= challenge_group->order(); + } + return out; +} + +void ValidatePaillierPublicKeyOrThrow( + const PaillierPublicKey& pub, + const std::shared_ptr& challenge_group) { + if (pub.n <= MinPaillierModulusQ8(challenge_group)) { + TECDSA_THROW_ARGUMENT("Paillier modulus must satisfy N > q^8"); + } +} + +StrictProofVerifierContext BuildProofContext( + const Bytes& session_id, PartyIndex prover_id, const ThresholdSuite& suite, + std::shared_ptr challenge_group, + std::optional verifier_id) { + if (challenge_group == nullptr) { + challenge_group = GroupContext::Create(suite.curve); + } + StrictProofVerifierContext context; + context.session_id = session_id; + context.prover_id = prover_id; + context.verifier_id = verifier_id; + context.transcript_hash = suite.transcript_hash; + context.challenge_group = std::move(challenge_group); + context.proof_domain_prefix = suite.proof_domain_prefix; + return context; +} + +PaillierProvider::PaillierProvider(unsigned long modulus_bits) { + if (modulus_bits < 256) { + TECDSA_THROW_ARGUMENT("Paillier modulus_bits must be >= 256"); + } + GenerateKeyPair(modulus_bits); + if (!VerifyKeyPair()) { + TECDSA_THROW("native Paillier generated invalid key pair"); + } +} + +PaillierCiphertextWithRandomBigInt PaillierProvider::EncryptWithRandomBigInt( + const BigInt& plaintext) const { + PaillierCiphertextWithRandomBigInt out; + out.randomness = SampleZnStar(); + out.ciphertext = EncryptWithProvidedRandomBigInt(plaintext, out.randomness); + return out; +} + +BigInt PaillierProvider::EncryptWithProvidedRandomBigInt( + const BigInt& plaintext, const BigInt& randomness) const { + if (!initialized_) { + TECDSA_THROW_LOGIC("Paillier key pair is not initialized"); + } + if (!IsInZnStar(randomness)) { + TECDSA_THROW_ARGUMENT("Paillier randomness must be in Z*_N"); + } + + const BigInt plain = NormalizeMod(plaintext, n_); + const BigInt g_pow_m = NormalizeMod((plain * n_) + BigInt(1), n2_); + const BigInt r_pow_n = randomness.PowMod(n_, n2_); + return NormalizeMod(g_pow_m * r_pow_n, n2_); +} + +BigInt PaillierProvider::DecryptBigInt(const BigInt& ciphertext) const { + if (!initialized_) { + TECDSA_THROW_LOGIC("Paillier key pair is not initialized"); + } + + const BigInt cipher = NormalizeMod(ciphertext, n2_); + const BigInt u = cipher.PowMod(lambda_, n2_); + const BigInt l_of_u = LFunction(u); + return NormalizeMod(l_of_u * mu_, n_); +} + +bool PaillierProvider::VerifyKeyPair() const { + if (!initialized_) { + return false; + } + if (p_ <= 1 || q_ <= 1 || p_ == q_) { + return false; + } + if (n_ != p_ * q_) { + return false; + } + if (n2_ != n_ * n_) { + return false; + } + if (g_ != n_ + BigInt(1)) { + return false; + } + if (lambda_ <= 1) { + return false; + } + + const BigInt p_minus_1 = p_ - BigInt(1); + const BigInt q_minus_1 = q_ - BigInt(1); + const BigInt lambda_check = BigInt::Lcm(p_minus_1, q_minus_1); + if (lambda_check != lambda_) { + return false; + } + + const BigInt gcd = BigInt::Gcd(lambda_, n_); + if (gcd != 1) { + return false; + } + + const auto mu_check = bigint::TryInvertMod(lambda_, n_); + if (!mu_check.has_value() || *mu_check != mu_) { + return false; + } + + const BigInt n_minus_1 = n_ - BigInt(1); + const std::array plain_cases = { + BigInt(0), + BigInt(1), + BigInt(2), + n_minus_1, + }; + for (const auto& plain : plain_cases) { + const BigInt cipher = EncryptWithProvidedRandomBigInt(plain, BigInt(1)); + const BigInt decrypted = DecryptBigInt(cipher); + if (decrypted != NormalizeMod(plain, n_)) { + return false; + } + } + + return true; +} + +BigInt PaillierProvider::modulus_n_bigint() const { + if (!initialized_) { + TECDSA_THROW_LOGIC("Paillier key pair is not initialized"); + } + return n_; +} + +BigInt PaillierProvider::private_lambda_bigint() const { + if (!initialized_) { + TECDSA_THROW_LOGIC("Paillier key pair is not initialized"); + } + return lambda_; +} + +void PaillierProvider::GenerateKeyPair(unsigned long modulus_bits) { + const size_t p_bits = + std::max(2, (static_cast(modulus_bits) + 1) / 2 + 1); + const size_t q_bits = p_bits; + + for (size_t attempt = 0; attempt < kMaxKeygenAttempts; ++attempt) { + do { + p_ = RandomOddWithBitSize(p_bits); + } while (!IsProbablePrime(p_)); + + do { + do { + q_ = RandomOddWithBitSize(q_bits); + } while (!IsProbablePrime(q_)); + } while (q_ == p_); + + n_ = p_ * q_; + n2_ = n_ * n_; + g_ = n_ + BigInt(1); + + const BigInt p_minus_1 = p_ - BigInt(1); + const BigInt q_minus_1 = q_ - BigInt(1); + lambda_ = BigInt::Lcm(p_minus_1, q_minus_1); + if (lambda_ <= 1) { + continue; + } + + const auto mu_opt = bigint::TryInvertMod(lambda_, n_); + if (!mu_opt.has_value()) { + continue; + } + mu_ = *mu_opt; + + initialized_ = true; + return; + } + + TECDSA_THROW("failed to generate valid native Paillier key pair"); +} + +BigInt PaillierProvider::NormalizeMod(const BigInt& value, + const BigInt& modulus) { + return bigint::NormalizeMod(value, modulus); +} + +bool PaillierProvider::IsProbablePrime(const BigInt& candidate) { + if (candidate <= 1) { + return false; + } + return candidate.IsPrime(); +} + +BigInt PaillierProvider::RandomBelow(const BigInt& upper_exclusive) { + return bigint::RandomBelow(upper_exclusive); +} + +BigInt PaillierProvider::RandomOddWithBitSize(size_t bits) { + if (bits < 2) { + TECDSA_THROW_ARGUMENT("prime bit size must be >= 2"); + } + + BigInt candidate; + BigInt::RandomMonicExactBits(bits, &candidate); + candidate.SetBit(0, 1); + if (candidate.BitCount() != bits) { + TECDSA_THROW("failed to sample odd integer with exact bit size"); + } + return candidate; +} + +BigInt PaillierProvider::SampleZnStar() const { + if (!initialized_) { + TECDSA_THROW_LOGIC("Paillier key pair is not initialized"); + } + return bigint::RandomZnStar(n_); +} + +bool PaillierProvider::IsInZnStar(const BigInt& value) const { + if (!initialized_) { + return false; + } + if (value <= 0 || value >= n_) { + return false; + } + + const BigInt gcd = BigInt::Gcd(value, n_); + return gcd == 1; +} + +BigInt PaillierProvider::LFunction(const BigInt& value) const { + const BigInt normalized = NormalizeMod(value, n2_); + const BigInt numer = normalized - BigInt(1); + if (numer.Mod(n_) != 0) { + TECDSA_THROW_ARGUMENT("invalid Paillier L(x) input"); + } + return numer / n_; +} + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h b/yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h new file mode 100644 index 00000000..7f855f70 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h @@ -0,0 +1,105 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/math/mpint/mp_int.h" + +namespace tecdsa::core::paillier { + +using BigInt = yacl::math::MPInt; + +struct PaillierCiphertextWithRandom { + BigInt ciphertext; + BigInt randomness; +}; + +using PaillierCiphertextWithRandomBigInt = PaillierCiphertextWithRandom; + +struct PaillierPublicKey { + BigInt n = BigInt(0); +}; + +struct StrictProofVerifierContext { + Bytes session_id; + std::optional prover_id; + std::optional verifier_id; + HashId transcript_hash = HashId::kSha256; + std::shared_ptr challenge_group; + std::string proof_domain_prefix = "GG2019"; +}; + +StrictProofVerifierContext BuildProofContext( + const Bytes& session_id, PartyIndex prover_id, + const ThresholdSuite& suite, + std::shared_ptr challenge_group, + std::optional verifier_id = std::nullopt); +BigInt MinPaillierModulusQ8( + const std::shared_ptr& challenge_group); +void ValidatePaillierPublicKeyOrThrow( + const PaillierPublicKey& pub, + const std::shared_ptr& challenge_group); + +class PaillierProvider { + public: + explicit PaillierProvider(unsigned long modulus_bits); + ~PaillierProvider() = default; + + PaillierProvider(const PaillierProvider&) = delete; + PaillierProvider& operator=(const PaillierProvider&) = delete; + + PaillierProvider(PaillierProvider&& other) noexcept = default; + PaillierProvider& operator=(PaillierProvider&& other) noexcept = default; + + PaillierCiphertextWithRandomBigInt EncryptWithRandomBigInt( + const BigInt& plaintext) const; + BigInt EncryptWithProvidedRandomBigInt(const BigInt& plaintext, + const BigInt& randomness) const; + BigInt DecryptBigInt(const BigInt& ciphertext) const; + + BigInt modulus_n_bigint() const; + BigInt private_lambda_bigint() const; + + bool VerifyKeyPair() const; + + private: + void GenerateKeyPair(unsigned long modulus_bits); + static BigInt NormalizeMod(const BigInt& value, const BigInt& modulus); + static bool IsProbablePrime(const BigInt& candidate); + static BigInt RandomBelow(const BigInt& upper_exclusive); + static BigInt RandomOddWithBitSize(size_t bits); + + BigInt SampleZnStar() const; + bool IsInZnStar(const BigInt& value) const; + BigInt LFunction(const BigInt& value) const; + + BigInt p_; + BigInt q_; + BigInt n_; + BigInt n2_; + BigInt g_; + BigInt lambda_; + BigInt mu_; + bool initialized_ = false; +}; + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.cc b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.cc new file mode 100644 index 00000000..2e13ffcc --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.cc @@ -0,0 +1,530 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs_internal.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.h" +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::paillier { +namespace { + +constexpr char kPiModProofName[] = "PiMod"; +constexpr char kPiPrmProofName[] = "PiPrm"; +constexpr uint32_t kPiModRounds = 128; +constexpr uint32_t kPiPrmRounds = 128; + +struct PiModRoundPayload { + BigInt x; + uint8_t a = 0; + uint8_t b = 0; + BigInt z; +}; + +struct PiModPayload { + BigInt w; + std::vector rounds; +}; + +struct PiPrmPayload { + std::vector commitments; + std::vector responses; +}; + +void AppendVerifierContext(transcript::Transcript* transcript, + const StrictProofVerifierContext& context) { + if (!context.session_id.empty()) { + transcript->append_session_id(context.session_id); + } + if (context.prover_id.has_value()) { + transcript->append_u32_be("prover_id", *context.prover_id); + } + if (context.verifier_id.has_value()) { + transcript->append_u32_be("verifier_id", *context.verifier_id); + } +} + +std::string BuildProofId(const StrictProofVerifierContext& context, + const char* proof_name) { + return context.proof_domain_prefix + "/" + proof_name + "/v1"; +} + +void AppendMpIntField(const BigInt& value, Bytes* out) { + const Bytes encoded = encoding::EncodeMpInt(value); + encoding::AppendSizedField(encoded, out); +} + +BigInt ReadMpIntField(std::span input, size_t* offset, + const char* field_name) { + const Bytes encoded = + encoding::ReadSizedField(input, offset, + paillier_internal::kMaxStrictFieldLen, + field_name); + return encoding::DecodeMpInt(encoded, + paillier_internal::kMaxStrictFieldLen); +} + +Bytes ExpandHashStream(HashId hash_id, std::span seed, + size_t out_len) { + if (out_len == 0) { + return {}; + } + + Bytes out; + out.reserve(out_len); + uint32_t block = 0; + while (out.size() < out_len) { + Bytes block_input(seed.begin(), seed.end()); + encoding::AppendU32Be(block, &block_input); + const Bytes digest = Hash(hash_id, block_input); + const size_t remaining = out_len - out.size(); + const size_t take = std::min(remaining, digest.size()); + out.insert(out.end(), digest.begin(), + digest.begin() + static_cast(take)); + ++block; + } + return out; +} + +BigInt NormalizeMod(const BigInt& value, const BigInt& modulus) { + return paillier_internal::NormalizeMod(value, modulus); +} + +BigInt PowMod(const BigInt& base, const BigInt& exp, const BigInt& modulus) { + return paillier_internal::PowMod(base, exp, modulus); +} + +BigInt MulMod(const BigInt& lhs, const BigInt& rhs, const BigInt& modulus) { + return paillier_internal::MulMod(lhs, rhs, modulus); +} + +std::optional TryInvertMod(const BigInt& value, const BigInt& modulus) { + return paillier_internal::InvertMod(value, modulus); +} + +bool IsQuadraticResidueModPrime(const BigInt& value, const BigInt& prime, + const BigInt& prime_subgroup_order) { + const BigInt reduced = NormalizeMod(value, prime); + if (reduced == 0) { + return false; + } + return PowMod(reduced, prime_subgroup_order, prime) == 1; +} + +BigInt CombineCrt(const BigInt& mod_p_value, const BigInt& mod_q_value, + const BigInt& prime_p, const BigInt& prime_q) { + const auto p_inv_mod_q = TryInvertMod(prime_p, prime_q); + if (!p_inv_mod_q.has_value()) { + TECDSA_THROW("CRT combination requires coprime moduli"); + } + const BigInt delta = + NormalizeMod((mod_q_value - mod_p_value) * *p_inv_mod_q, prime_q); + return NormalizeMod(mod_p_value + (prime_p * delta), prime_p * prime_q); +} + +BigInt MakeJacobiMinusOneWitnessElement(const PaperAuxSetupWitness& witness) { + return CombineCrt(witness.P_tilde - 1, BigInt(1), witness.P_tilde, + witness.Q_tilde); +} + +int JacobiSymbol(BigInt value, BigInt modulus) { + if (modulus <= 0 || modulus.IsEven()) { + TECDSA_THROW_ARGUMENT("Jacobi symbol requires a positive odd modulus"); + } + + value = NormalizeMod(value, modulus); + int sign = 1; + while (value != 0) { + while (value.IsEven()) { + value /= 2; + const auto modulus_mod_8 = modulus % 8; + if (modulus_mod_8 == 3 || modulus_mod_8 == 5) { + sign = -sign; + } + } + std::swap(value, modulus); + if ((value % 4) == 3 && (modulus % 4) == 3) { + sign = -sign; + } + value = NormalizeMod(value, modulus); + } + return modulus == 1 ? sign : 0; +} + +bool HasJacobiSymbolMinusOne(const BigInt& value, const BigInt& modulus) { + return IsZnStarElement(value, modulus) && JacobiSymbol(value, modulus) == -1; +} + +std::vector DerivePiModChallenges( + const AuxRsaParams& params, const StrictProofVerifierContext& context, + const BigInt& w) { + transcript::Transcript base(context.transcript_hash); + base.append_proof_id(BuildProofId(context, kPiModProofName)); + AppendVerifierContext(&base, context); + const Bytes n_bytes = encoding::EncodeMpInt(params.n_tilde); + const Bytes w_bytes = encoding::EncodeMpInt(w); + base.append_fields({ + transcript::TranscriptFieldRef{.label = "Ntilde", .data = n_bytes}, + transcript::TranscriptFieldRef{.label = "w", .data = w_bytes}, + }); + base.append_u32_be("rounds", kPiModRounds); + + const size_t byte_len = + std::max(1, (params.n_tilde.BitCount() + 7) / 8); + std::vector challenges; + challenges.reserve(kPiModRounds); + for (uint32_t round = 0; round < kPiModRounds; ++round) { + for (uint32_t attempt = 0; + attempt < paillier_internal::kMaxSquareFreeGmr98ChallengeAttempts; + ++attempt) { + transcript::Transcript round_transcript(context.transcript_hash); + round_transcript.append_fields({ + transcript::TranscriptFieldRef{ + .label = "base", + .data = base.bytes(), + }, + }); + round_transcript.append_u32_be("round", round); + round_transcript.append_u32_be("attempt", attempt); + const Bytes seed = Hash(context.transcript_hash, round_transcript.bytes()); + const Bytes expanded = ExpandHashStream(context.transcript_hash, seed, + byte_len); + BigInt candidate = bigint::FromBigEndian(expanded); + candidate = NormalizeMod(candidate, params.n_tilde); + if (IsZnStarElement(candidate, params.n_tilde)) { + challenges.push_back(candidate); + break; + } + } + if (challenges.size() != round + 1) { + TECDSA_THROW("failed to derive PiMod challenge in Z*_N"); + } + } + return challenges; +} + +std::vector DerivePiPrmChallengeBits( + const AuxRsaParams& params, const StrictProofVerifierContext& context, + const std::vector& commitments) { + transcript::Transcript transcript(context.transcript_hash); + transcript.append_proof_id(BuildProofId(context, kPiPrmProofName)); + AppendVerifierContext(&transcript, context); + const Bytes n_bytes = encoding::EncodeMpInt(params.n_tilde); + const Bytes h1_bytes = encoding::EncodeMpInt(params.h1); + const Bytes h2_bytes = encoding::EncodeMpInt(params.h2); + transcript.append_fields({ + transcript::TranscriptFieldRef{.label = "Ntilde", .data = n_bytes}, + transcript::TranscriptFieldRef{.label = "h1", .data = h1_bytes}, + transcript::TranscriptFieldRef{.label = "h2", .data = h2_bytes}, + }); + for (size_t i = 0; i < commitments.size(); ++i) { + transcript.append("A" + std::to_string(i), encoding::EncodeMpInt(commitments[i])); + } + + const Bytes seed = Hash(context.transcript_hash, transcript.bytes()); + const Bytes expanded = ExpandHashStream(context.transcript_hash, seed, + (kPiPrmRounds + 7) / 8); + std::vector bits; + bits.reserve(kPiPrmRounds); + for (uint32_t i = 0; i < kPiPrmRounds; ++i) { + const uint8_t byte = expanded[i / 8]; + bits.push_back(static_cast((byte >> (i % 8)) & 0x01)); + } + return bits; +} + +std::optional> SelectPiModMasks( + const BigInt& y_i, const BigInt& w, const PaperAuxSetupWitness& witness, + BigInt* adjusted) { + for (uint8_t a = 0; a <= 1; ++a) { + for (uint8_t b = 0; b <= 1; ++b) { + BigInt candidate = y_i; + if (b == 1) { + candidate = MulMod(candidate, w, witness.P_tilde * witness.Q_tilde); + } + if (a == 1) { + candidate = NormalizeMod(BigInt(0) - candidate, + witness.P_tilde * witness.Q_tilde); + } + if (IsQuadraticResidueModPrime(candidate, witness.P_tilde, + witness.p_tilde) && + IsQuadraticResidueModPrime(candidate, witness.Q_tilde, + witness.q_tilde)) { + *adjusted = candidate; + return std::make_pair(a, b); + } + } + } + return std::nullopt; +} + +PiModRoundPayload BuildPiModRound(const BigInt& y_i, const BigInt& w, + const PaperAuxSetupWitness& witness) { + BigInt adjusted; + const auto masks = SelectPiModMasks(y_i, w, witness, &adjusted); + if (!masks.has_value()) { + TECDSA_THROW("failed to select PiMod adjustment bits"); + } + + const auto inv4_p = TryInvertMod(BigInt(4), witness.p_tilde); + const auto inv4_q = TryInvertMod(BigInt(4), witness.q_tilde); + if (!inv4_p.has_value() || !inv4_q.has_value()) { + TECDSA_THROW("failed to invert 4 modulo Blum safe-prime sub-orders"); + } + + const BigInt x_p = + PowMod(NormalizeMod(adjusted, witness.P_tilde), *inv4_p, witness.P_tilde); + const BigInt x_q = + PowMod(NormalizeMod(adjusted, witness.Q_tilde), *inv4_q, witness.Q_tilde); + const BigInt x = CombineCrt(x_p, x_q, witness.P_tilde, witness.Q_tilde); + + const BigInt phi_n = + (witness.P_tilde - 1) * (witness.Q_tilde - 1); + const auto n_inv = TryInvertMod(witness.P_tilde * witness.Q_tilde, phi_n); + if (!n_inv.has_value()) { + TECDSA_THROW("PiMod requires gcd(N, phi(N)) = 1"); + } + const BigInt z = PowMod(y_i, *n_inv, witness.P_tilde * witness.Q_tilde); + + if (PowMod(x, BigInt(4), witness.P_tilde * witness.Q_tilde) != adjusted) { + TECDSA_THROW("PiMod prover failed to build a valid quartic root"); + } + if (PowMod(z, witness.P_tilde * witness.Q_tilde, + witness.P_tilde * witness.Q_tilde) != y_i) { + TECDSA_THROW("PiMod prover failed to build a valid N-th root"); + } + + return PiModRoundPayload{ + .x = x, + .a = masks->first, + .b = masks->second, + .z = z, + }; +} + +Bytes EncodePiModPayload(const PiModPayload& payload) { + Bytes out; + AppendMpIntField(payload.w, &out); + encoding::AppendU32Be(payload.rounds.size(), &out); + for (const auto& round : payload.rounds) { + AppendMpIntField(round.x, &out); + out.push_back(round.a); + out.push_back(round.b); + AppendMpIntField(round.z, &out); + } + return out; +} + +PiModPayload DecodePiModPayload(std::span blob) { + size_t offset = 0; + PiModPayload payload; + payload.w = ReadMpIntField(blob, &offset, "PiMod w"); + const uint32_t rounds = encoding::ReadU32Be(blob, &offset); + payload.rounds.reserve(rounds); + for (uint32_t i = 0; i < rounds; ++i) { + PiModRoundPayload round; + round.x = ReadMpIntField(blob, &offset, "PiMod x"); + if (offset + 2 > blob.size()) { + TECDSA_THROW_ARGUMENT("PiMod payload truncated on bit flags"); + } + round.a = blob[offset++]; + round.b = blob[offset++]; + round.z = ReadMpIntField(blob, &offset, "PiMod z"); + payload.rounds.push_back(std::move(round)); + } + if (offset != blob.size()) { + TECDSA_THROW_ARGUMENT("PiMod payload has trailing bytes"); + } + return payload; +} + +Bytes EncodePiPrmPayload(const PiPrmPayload& payload) { + Bytes out; + encoding::AppendU32Be(payload.commitments.size(), &out); + for (const BigInt& commitment : payload.commitments) { + AppendMpIntField(commitment, &out); + } + encoding::AppendU32Be(payload.responses.size(), &out); + for (const BigInt& response : payload.responses) { + AppendMpIntField(response, &out); + } + return out; +} + +PiPrmPayload DecodePiPrmPayload(std::span blob) { + size_t offset = 0; + PiPrmPayload payload; + const uint32_t commitments_count = encoding::ReadU32Be(blob, &offset); + payload.commitments.reserve(commitments_count); + for (uint32_t i = 0; i < commitments_count; ++i) { + payload.commitments.push_back( + ReadMpIntField(blob, &offset, "PiPrm commitment")); + } + const uint32_t responses_count = encoding::ReadU32Be(blob, &offset); + payload.responses.reserve(responses_count); + for (uint32_t i = 0; i < responses_count; ++i) { + payload.responses.push_back(ReadMpIntField(blob, &offset, "PiPrm response")); + } + if (offset != blob.size()) { + TECDSA_THROW_ARGUMENT("PiPrm payload has trailing bytes"); + } + return payload; +} + +PiPrmPayload BuildPiPrmPayload(const AuxRsaParams& params, + const PaperAuxSetupWitness& witness, + const StrictProofVerifierContext& context) { + const BigInt phi_n = + (witness.P_tilde - 1) * (witness.Q_tilde - 1); + PiPrmPayload payload; + payload.commitments.reserve(kPiPrmRounds); + payload.responses.reserve(kPiPrmRounds); + + std::vector randomness; + randomness.reserve(kPiPrmRounds); + for (uint32_t i = 0; i < kPiPrmRounds; ++i) { + const BigInt a_i = BigInt::RandomLtN(phi_n); + randomness.push_back(a_i); + payload.commitments.push_back(PowMod(params.h2, a_i, params.n_tilde)); + } + + const std::vector challenge_bits = + DerivePiPrmChallengeBits(params, context, payload.commitments); + for (uint32_t i = 0; i < kPiPrmRounds; ++i) { + const BigInt response = NormalizeMod( + randomness[i] + (challenge_bits[i] == 0 ? BigInt(0) : witness.lambda), + phi_n); + payload.responses.push_back(response); + } + return payload; +} + +} // namespace + +AuxCorrectFormProof BuildAuxCorrectFormProof( + const AuxRsaParams& params, const PaperAuxSetupWitness& witness, + const StrictProofVerifierContext& context) { + if (!ValidatePaperAuxSetup(params, witness)) { + TECDSA_THROW_ARGUMENT( + "cannot build paper aux proof from invalid parameters or witness"); + } + + const BigInt w = MakeJacobiMinusOneWitnessElement(witness); + PiModPayload pi_mod; + pi_mod.w = w; + const std::vector y_values = DerivePiModChallenges(params, context, w); + pi_mod.rounds.reserve(y_values.size()); + for (const BigInt& y_i : y_values) { + pi_mod.rounds.push_back(BuildPiModRound(y_i, w, witness)); + } + + const PiPrmPayload pi_prm = BuildPiPrmPayload(params, witness, context); + return AuxCorrectFormProof{ + .pi_mod = + PiModProof{ + .blob = EncodePiModPayload(pi_mod), + }, + .pi_prm = + PiPrmProof{ + .blob = EncodePiPrmPayload(pi_prm), + }, + }; +} + +bool VerifyAuxCorrectFormProof(const AuxRsaParams& params, + const AuxCorrectFormProof& proof, + const StrictProofVerifierContext& context) { + if (!ValidateAuxRsaParams(params) || proof.pi_mod.blob.empty() || + proof.pi_prm.blob.empty()) { + return false; + } + + PiModPayload pi_mod; + PiPrmPayload pi_prm; + try { + pi_mod = DecodePiModPayload(proof.pi_mod.blob); + pi_prm = DecodePiPrmPayload(proof.pi_prm.blob); + } catch (const std::exception&) { + return false; + } + + if (params.n_tilde <= 2 || params.n_tilde.IsEven() || params.n_tilde.IsPrime()) { + return false; + } + if (!HasJacobiSymbolMinusOne(pi_mod.w, params.n_tilde) || + pi_mod.rounds.size() != kPiModRounds || + pi_prm.commitments.size() != kPiPrmRounds || + pi_prm.responses.size() != kPiPrmRounds) { + return false; + } + + const std::vector y_values = + DerivePiModChallenges(params, context, pi_mod.w); + for (size_t i = 0; i < pi_mod.rounds.size(); ++i) { + const auto& round = pi_mod.rounds[i]; + if (round.a > 1 || round.b > 1 || !IsZnStarElement(round.x, params.n_tilde) || + !IsZnStarElement(round.z, params.n_tilde)) { + return false; + } + BigInt expected = y_values[i]; + if (round.b == 1) { + expected = MulMod(expected, pi_mod.w, params.n_tilde); + } + if (round.a == 1) { + expected = NormalizeMod(BigInt(0) - expected, params.n_tilde); + } + if (PowMod(round.z, params.n_tilde, params.n_tilde) != y_values[i]) { + return false; + } + if (PowMod(round.x, BigInt(4), params.n_tilde) != expected) { + return false; + } + } + + if (!IsZnStarElement(params.h2, params.n_tilde)) { + return false; + } + const std::vector challenge_bits = + DerivePiPrmChallengeBits(params, context, pi_prm.commitments); + for (size_t i = 0; i < pi_prm.commitments.size(); ++i) { + if (!IsZnStarElement(pi_prm.commitments[i], params.n_tilde) || + pi_prm.responses[i] < 0) { + return false; + } + BigInt rhs = pi_prm.commitments[i]; + if (challenge_bits[i] != 0) { + rhs = MulMod(rhs, params.h1, params.n_tilde); + } + if (PowMod(params.h2, pi_prm.responses[i], params.n_tilde) != rhs) { + return false; + } + } + return true; +} + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h new file mode 100644 index 00000000..d9ad1471 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h @@ -0,0 +1,28 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h" + +namespace tecdsa::core::paillier { + +AuxCorrectFormProof BuildAuxCorrectFormProof( + const AuxRsaParams& params, const PaperAuxSetupWitness& witness, + const StrictProofVerifierContext& context); +bool VerifyAuxCorrectFormProof(const AuxRsaParams& params, + const AuxCorrectFormProof& proof, + const StrictProofVerifierContext& context); + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.cc b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.cc new file mode 100644 index 00000000..c43d49b8 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.cc @@ -0,0 +1,155 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/math/common.h" + +namespace tecdsa::core::paillier { +namespace { + +constexpr size_t kMaxPaperAuxSetupAttempts = 128; + +BigInt RandomNonZeroBelow(const BigInt& upper_exclusive) { + if (upper_exclusive <= 1) { + TECDSA_THROW_ARGUMENT( + "paper auxiliary setup requires an upper bound greater than one"); + } + BigInt candidate; + do { + candidate = BigInt::RandomLtN(upper_exclusive); + } while (candidate == 0); + return candidate; +} + +BigInt SampleZnStarElement(const BigInt& modulus) { + if (modulus <= 2) { + TECDSA_THROW_ARGUMENT( + "paper auxiliary setup requires modulus greater than two"); + } + BigInt candidate; + do { + candidate = BigInt::RandomLtN(modulus); + } while (!IsZnStarElement(candidate, modulus)); + return candidate; +} + +} // namespace + +bool ValidatePaperAuxSetup(const AuxRsaParams& params, + const PaperAuxSetupWitness& witness) { + if (!ValidateAuxRsaParams(params)) { + return false; + } + if (witness.p_tilde <= 1 || witness.q_tilde <= 1 || witness.P_tilde <= 3 || + witness.Q_tilde <= 3 || witness.tau <= 0 || witness.lambda <= 0) { + return false; + } + if (!witness.p_tilde.IsPrime() || !witness.q_tilde.IsPrime() || + !witness.P_tilde.IsPrime() || !witness.Q_tilde.IsPrime()) { + return false; + } + + if (witness.P_tilde != (witness.p_tilde * 2) + 1 || + witness.Q_tilde != (witness.q_tilde * 2) + 1) { + return false; + } + if ((witness.P_tilde % 4) != 3 || (witness.Q_tilde % 4) != 3) { + return false; + } + + const BigInt subgroup_order = witness.p_tilde * witness.q_tilde; + if (subgroup_order <= 1 || witness.lambda >= subgroup_order) { + return false; + } + if (params.n_tilde != witness.P_tilde * witness.Q_tilde) { + return false; + } + if (!IsZnStarElement(witness.tau, params.n_tilde)) { + return false; + } + + const BigInt expected_h2 = witness.tau.PowMod(BigInt(2), params.n_tilde); + const BigInt expected_h1 = + expected_h2.PowMod(witness.lambda, params.n_tilde); + if (expected_h2 != params.h2 || expected_h1 != params.h1) { + return false; + } + return params.h1 > 1 && params.h2 > 1 && params.h1 != params.h2; +} + +PaperAuxSetupBundle GeneratePaperAuxSetup(uint32_t modulus_bits) { + if (modulus_bits < 164) { + TECDSA_THROW_ARGUMENT("paper auxiliary RSA modulus bits must be >= 164"); + } + + const size_t p_bits = static_cast(modulus_bits) / 2; + const size_t q_bits = static_cast(modulus_bits) - p_bits; + if (p_bits <= 81 || q_bits <= 81) { + TECDSA_THROW_ARGUMENT( + "paper auxiliary RSA modulus halves must each be greater than 81 bits"); + } + + for (size_t attempt = 0; attempt < kMaxPaperAuxSetupAttempts; ++attempt) { + BigInt P_tilde; + BigInt Q_tilde; + BigInt::RandPrimeOver(p_bits, &P_tilde, yacl::math::PrimeType::FastSafe); + BigInt::RandPrimeOver(q_bits, &Q_tilde, yacl::math::PrimeType::FastSafe); + if (P_tilde == Q_tilde) { + continue; + } + + const BigInt p_tilde = (P_tilde - 1) / 2; + const BigInt q_tilde = (Q_tilde - 1) / 2; + const BigInt n_tilde = P_tilde * Q_tilde; + const BigInt subgroup_order = p_tilde * q_tilde; + + const BigInt tau = SampleZnStarElement(n_tilde); + const BigInt h2 = tau.PowMod(BigInt(2), n_tilde); + if (!IsZnStarElement(h2, n_tilde) || h2 <= 1) { + continue; + } + + const BigInt lambda = RandomNonZeroBelow(subgroup_order); + const BigInt h1 = h2.PowMod(lambda, n_tilde); + + PaperAuxSetupBundle bundle{ + .params = + AuxRsaParams{ + .n_tilde = n_tilde, + .h1 = h1, + .h2 = h2, + }, + .witness = + PaperAuxSetupWitness{ + .p_tilde = p_tilde, + .q_tilde = q_tilde, + .P_tilde = P_tilde, + .Q_tilde = Q_tilde, + .tau = tau, + .lambda = lambda, + }, + }; + if (ValidatePaperAuxSetup(bundle.params, bundle.witness)) { + return bundle; + } + } + + TECDSA_THROW("failed to generate paper auxiliary setup"); +} + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.h b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.h new file mode 100644 index 00000000..e2b11838 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_setup.h @@ -0,0 +1,27 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h" + +namespace tecdsa::core::paillier { + +PaperAuxSetupBundle GeneratePaperAuxSetup(uint32_t modulus_bits); +bool ValidatePaperAuxSetup(const AuxRsaParams& params, + const PaperAuxSetupWitness& witness); + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h new file mode 100644 index 00000000..eebb6860 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h @@ -0,0 +1,48 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" + +namespace tecdsa::core::paillier { + +struct PiModProof { + Bytes blob; +}; + +struct PiPrmProof { + Bytes blob; +}; + +struct AuxCorrectFormProof { + PiModProof pi_mod; + PiPrmProof pi_prm; +}; + +struct PaperAuxSetupWitness { + BigInt p_tilde = BigInt(0); + BigInt q_tilde = BigInt(0); + BigInt P_tilde = BigInt(0); + BigInt Q_tilde = BigInt(0); + BigInt tau = BigInt(0); + BigInt lambda = BigInt(0); +}; + +struct PaperAuxSetupBundle { + AuxRsaParams params; + PaperAuxSetupWitness witness; +}; + +} // namespace tecdsa::core::paillier diff --git a/yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.cc b/yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.cc new file mode 100644 index 00000000..7ada425d --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.cc @@ -0,0 +1,71 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h" + +#include +#include + +namespace tecdsa::core::participant { + +void ValidateParticipantsOrThrow(const std::vector& participants, + PartyIndex self_id, + const char* context_name) { + if (participants.size() < 2) { + TECDSA_THROW_ARGUMENT(std::string(context_name) + + " requires at least 2 participants"); + } + + std::unordered_set dedup; + bool self_present = false; + for (PartyIndex id : participants) { + if (id == 0) { + TECDSA_THROW_ARGUMENT("participants must not contain 0"); + } + if (!dedup.insert(id).second) { + TECDSA_THROW_ARGUMENT("participants must be unique"); + } + if (id == self_id) { + self_present = true; + } + } + if (!self_present) { + TECDSA_THROW_ARGUMENT("self_id must be in participants"); + } +} + +std::vector BuildPeers(const std::vector& participants, + PartyIndex self_id) { + std::vector peers; + peers.reserve(participants.size()); + for (PartyIndex party : participants) { + if (party != self_id) { + peers.push_back(party); + } + } + return peers; +} + +ParticipantSet BuildParticipantSet(const std::vector& participants, + PartyIndex self_id, + const char* context_name) { + ValidateParticipantsOrThrow(participants, self_id, context_name); + return ParticipantSet{ + .participants = participants, + .peers = BuildPeers(participants, self_id), + .self_id = self_id, + }; +} + +} // namespace tecdsa::core::participant diff --git a/yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h b/yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h new file mode 100644 index 00000000..fb48b647 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h @@ -0,0 +1,67 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" + +namespace tecdsa::core::participant { + +struct ParticipantSet { + std::vector participants; + std::vector peers; + PartyIndex self_id = 0; +}; + +void ValidateParticipantsOrThrow(const std::vector& participants, + PartyIndex self_id, + const char* context_name); + +std::vector BuildPeers(const std::vector& participants, + PartyIndex self_id); + +ParticipantSet BuildParticipantSet(const std::vector& participants, + PartyIndex self_id, + const char* context_name); + +template +void RequireExactlyPeers(const MapType& messages, + const std::vector& participants, + PartyIndex self_id, const char* field_name) { + size_t expected = 0; + for (PartyIndex party : participants) { + if (party != self_id) { + ++expected; + } + } + if (messages.size() != expected) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + + " must contain exactly one entry per peer"); + } + for (PartyIndex party : participants) { + if (party == self_id) { + continue; + } + if (!messages.contains(party)) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + + " is missing a peer message"); + } + } +} + +} // namespace tecdsa::core::participant diff --git a/yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.cc b/yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.cc new file mode 100644 index 00000000..579bf78a --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.cc @@ -0,0 +1,93 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h" + +namespace tecdsa::core::proof { +namespace { + +Scalar BuildSchnorrChallenge(const ThresholdSuite& suite, + const Bytes& session_id, PartyIndex party_id, + const ECPoint& statement, const ECPoint& a) { + transcript::Transcript transcript(suite.transcript_hash); + const Bytes statement_bytes = encoding::EncodePoint(statement); + const Bytes a_bytes = encoding::EncodePoint(a); + const std::string proof_id = + suite.scheme == SchemeId::kSm2 + ? "SM2/Schnorr/v1" + : suite.proof_domain_prefix + "/Schnorr/v1"; + transcript.append_proof_id(proof_id); + transcript.append_session_id(session_id); + transcript.append_u32_be("party_id", party_id); + transcript.append_fields({ + transcript::TranscriptFieldRef{.label = "X", .data = statement_bytes}, + transcript::TranscriptFieldRef{.label = "A", .data = a_bytes}, + }); + return transcript.challenge_scalar(statement.group()); +} + +} // namespace + +SchnorrProof BuildSchnorrProof(const ThresholdSuite& suite, + const Bytes& session_id, PartyIndex prover_id, + const ECPoint& statement, + const Scalar& witness) { + if (witness.value() == 0) { + TECDSA_THROW_ARGUMENT("schnorr witness must be non-zero"); + } + + while (true) { + const Scalar r = vss::RandomNonZeroScalar(witness.group()); + const ECPoint a = ECPoint::GeneratorMultiply(r); + const Scalar e = + BuildSchnorrChallenge(suite, session_id, prover_id, statement, a); + const Scalar z = r + (e * witness); + if (z.value() == 0) { + continue; + } + return SchnorrProof{a, z}; + } +} + +bool VerifySchnorrProof(const ThresholdSuite& suite, const Bytes& session_id, + PartyIndex prover_id, + const ECPoint& statement, const SchnorrProof& proof) { + if (proof.z.value() == 0) { + return false; + } + + try { + const Scalar e = + BuildSchnorrChallenge(suite, session_id, prover_id, statement, proof.a); + const ECPoint lhs = ECPoint::GeneratorMultiply(proof.z); + + ECPoint rhs = proof.a; + if (e.value() != 0) { + rhs = rhs.Add(statement.Mul(e)); + } + return lhs == rhs; + } catch (const std::exception&) { + return false; + } +} + +} // namespace tecdsa::core::proof diff --git a/yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h b/yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h new file mode 100644 index 00000000..c1e526eb --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h @@ -0,0 +1,42 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/types.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::proof { + +SchnorrProof BuildSchnorrProof(const ThresholdSuite& suite, + const Bytes& session_id, PartyIndex prover_id, + const ECPoint& statement, + const Scalar& witness); + +bool VerifySchnorrProof(const ThresholdSuite& suite, const Bytes& session_id, + PartyIndex prover_id, + const ECPoint& statement, const SchnorrProof& proof); + +template + requires(SchnorrProofLike && + !std::same_as, SchnorrProof>) +bool VerifySchnorrProof(const ThresholdSuite& suite, const Bytes& session_id, + PartyIndex prover_id, + const ECPoint& statement, const Proof& proof) { + return VerifySchnorrProof(suite, session_id, prover_id, statement, + SchnorrProof{proof.a, proof.z}); +} + +} // namespace tecdsa::core::proof diff --git a/yacl/crypto/experimental/threshold_signatures/core/proof/types.h b/yacl/crypto/experimental/threshold_signatures/core/proof/types.h new file mode 100644 index 00000000..8a1ef055 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/proof/types.h @@ -0,0 +1,49 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" + +namespace tecdsa::core::proof { + +template +concept SchnorrProofLike = + requires(const T& proof, const ECPoint& point, const Scalar& scalar) { + { proof.a } -> std::convertible_to; + { proof.z } -> std::convertible_to; + T{point, scalar}; + }; + +// Generic substrate proof shared by multiple schemes. Canonical public proof +// owners should live in the scheme layer, not in core::proof. +struct SchnorrProof { + ECPoint a; + Scalar z; + + template + requires(!std::same_as, SchnorrProof> && + requires(const ECPoint& point, const Scalar& scalar) { + T{point, scalar}; + }) + operator T() const { + return T{a, z}; + } +}; + +} // namespace tecdsa::core::proof diff --git a/yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.cc b/yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.cc new file mode 100644 index 00000000..41d200fd --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.cc @@ -0,0 +1,78 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" + +#include + +namespace tecdsa::core::protocol { + +bool IsPeer(std::span peers, PartyIndex party) { + for (PartyIndex peer : peers) { + if (peer == party) { + return true; + } + } + return false; +} + +std::string MakePeerTypeKey(PartyIndex peer, std::string_view type_key) { + std::string out; + out.reserve(sizeof(PartyIndex) + type_key.size()); + out.push_back(static_cast((peer >> 24) & 0xFF)); + out.push_back(static_cast((peer >> 16) & 0xFF)); + out.push_back(static_cast((peer >> 8) & 0xFF)); + out.push_back(static_cast(peer & 0xFF)); + out.append(type_key.data(), type_key.size()); + return out; +} + +PeerTypeMessageStore::PeerTypeMessageStore( + std::span peers, PartyIndex self_id, + std::span expected_type_keys, + std::string message_name) + : peers_(peers.begin(), peers.end()), + self_id_(self_id), + message_name_(std::move(message_name)) { + expected_type_keys_.reserve(expected_type_keys.size()); + for (std::string_view type_key : expected_type_keys) { + expected_type_keys_.emplace(type_key); + } + seen_keys_.reserve(peers_.size() * expected_type_keys_.size()); +} + +void PeerTypeMessageStore::RequireExpectedCount(size_t count) const { + if (count != peers_.size() * expected_type_keys_.size()) { + TECDSA_THROW_ARGUMENT(message_name_ + + " must contain exactly one message per peer/type"); + } +} + +void PeerTypeMessageStore::Add(PartyIndex sender, PartyIndex receiver, + std::string_view type_key) { + if (!IsPeer(peers_, sender)) { + TECDSA_THROW_ARGUMENT(message_name_ + " sender is not a peer"); + } + if (receiver != self_id_) { + TECDSA_THROW_ARGUMENT(message_name_ + " must target self"); + } + if (!expected_type_keys_.contains(std::string(type_key))) { + TECDSA_THROW_ARGUMENT(message_name_ + " has unexpected type"); + } + if (!seen_keys_.insert(MakePeerTypeKey(sender, type_key)).second) { + TECDSA_THROW_ARGUMENT("duplicate " + message_name_ + " for sender/type"); + } +} + +} // namespace tecdsa::core::protocol diff --git a/yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h b/yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h new file mode 100644 index 00000000..d7e823c8 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h @@ -0,0 +1,79 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" + +namespace tecdsa::core::protocol { + +bool IsPeer(std::span peers, PartyIndex party); + +std::string MakePeerTypeKey(PartyIndex peer, std::string_view type_key); + +class PeerTypeMessageStore { + public: + PeerTypeMessageStore(std::span peers, PartyIndex self_id, + std::span expected_type_keys, + std::string message_name); + + void RequireExpectedCount(size_t count) const; + void Add(PartyIndex sender, PartyIndex receiver, std::string_view type_key); + + private: + std::vector peers_; + PartyIndex self_id_ = 0; + std::unordered_set expected_type_keys_; + std::unordered_set seen_keys_; + std::string message_name_; +}; + +template +void RequireExactlyPeerMessages(const MapType& messages, + std::span peers, + const char* field_name) { + if (messages.size() != peers.size()) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + + " must contain exactly one entry per peer"); + } + for (PartyIndex peer : peers) { + if (!messages.contains(peer)) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + + " is missing a peer message"); + } + } +} + +template +std::vector ValuesInPeerOrder(const std::unordered_map& map, + std::span peers, + const char* field_name) { + RequireExactlyPeerMessages(map, peers, field_name); + std::vector out; + out.reserve(peers.size()); + for (PartyIndex peer : peers) { + out.push_back(map.at(peer)); + } + return out; +} + +} // namespace tecdsa::core::protocol diff --git a/yacl/crypto/experimental/threshold_signatures/core/protocol/round_driver.h b/yacl/crypto/experimental/threshold_signatures/core/protocol/round_driver.h new file mode 100644 index 00000000..d0f5418d --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/protocol/round_driver.h @@ -0,0 +1,70 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::core::protocol { + +template +class LinearRoundDriver { + public: + static_assert(std::is_enum_v, + "LinearRoundDriver requires an enum step type"); + + constexpr explicit LinearRoundDriver(Step initial) : current_(initial) {} + + constexpr Step current() const { return current_; } + + constexpr bool CanProceed(Step required_current) const { + return Value(current_) == Value(required_current); + } + + constexpr bool HasReached(Step step) const { + return Value(current_) >= Value(step); + } + + std::string ExpectedMessages(std::string_view message_name) const { + return "expected " + std::string(message_name); + } + + void RequireReached(Step required, std::string_view error_message) const { + if (!HasReached(required)) { + TECDSA_THROW_LOGIC(std::string(error_message)); + } + } + + void Advance(Step required_current, Step next, + std::string_view error_message) { + if (!CanProceed(required_current)) { + TECDSA_THROW_LOGIC(std::string(error_message)); + } + current_ = next; + } + + private: + static constexpr auto Value(Step step) { + return static_cast>(step); + } + + Step current_; +}; + +} // namespace tecdsa::core::protocol diff --git a/yacl/crypto/experimental/threshold_signatures/core/random/csprng.cc b/yacl/crypto/experimental/threshold_signatures/core/random/csprng.cc new file mode 100644 index 00000000..cccca1c9 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/random/csprng.cc @@ -0,0 +1,43 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +#include +#include + +#include "yacl/crypto/rand/rand.h" + +namespace tecdsa { + +Bytes Csprng::RandomBytes(size_t size) { + if (size == 0) { + return {}; + } + + return yacl::crypto::SecureRandBytes(static_cast(size)); +} + +core::Scalar Csprng::RandomScalar() { + while (true) { + const Bytes bytes = RandomBytes(32); + try { + return core::Scalar::FromCanonicalBytes(bytes); + } catch (const std::invalid_argument&) { + continue; + } + } +} + +} // namespace tecdsa diff --git a/yacl/crypto/experimental/threshold_signatures/core/random/csprng.h b/yacl/crypto/experimental/threshold_signatures/core/random/csprng.h new file mode 100644 index 00000000..29a5e9d1 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/random/csprng.h @@ -0,0 +1,30 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" + +namespace tecdsa { + +class Csprng { + public: + static Bytes RandomBytes(size_t size); + static core::Scalar RandomScalar(); +}; + +} // namespace tecdsa diff --git a/yacl/crypto/experimental/threshold_signatures/core/suite/group_context.cc b/yacl/crypto/experimental/threshold_signatures/core/suite/group_context.cc new file mode 100644 index 00000000..0cf282fb --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/suite/group_context.cc @@ -0,0 +1,105 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::core { +namespace { + +using yacl::crypto::EcGroupFactory; +using yacl::crypto::PointOctetFormat; + +std::shared_ptr CreateContext(CurveId curve_id, + const char* curve_name) { + auto group = + EcGroupFactory::Instance().Create(curve_name, yacl::ArgLib = "openssl"); + if (group == nullptr) { + TECDSA_THROW(std::string("Failed to create ") + curve_name + + " curve via yacl openssl backend"); + } + + GroupContext::BigInt order = group->GetOrder(); + const size_t scalar_size_bytes = (order.BitCount() + 7) / 8; + const size_t compressed_point_size_bytes = + group->GetSerializeLength(PointOctetFormat::X962Compressed); + + return std::shared_ptr(new GroupContext( + curve_id, curve_name, std::move(order), scalar_size_bytes, + compressed_point_size_bytes, std::move(group))); +} + +std::shared_ptr CreateSecp256k1Context() { + return CreateContext(CurveId::kSecp256k1, "secp256k1"); +} + +std::shared_ptr CreateSm2P256V1Context() { + return CreateContext(CurveId::kSm2P256V1, "sm2"); +} + +} // namespace + +GroupContext::GroupContext(CurveId curve_id, std::string curve_name, + BigInt order, size_t scalar_size_bytes, + size_t compressed_point_size_bytes, + std::unique_ptr ec_group) + : curve_id_(curve_id), + curve_name_(std::move(curve_name)), + order_(std::move(order)), + scalar_size_bytes_(scalar_size_bytes), + compressed_point_size_bytes_(compressed_point_size_bytes), + ec_group_(std::move(ec_group)) {} + +std::shared_ptr GroupContext::Create(CurveId curve_id) { + switch (curve_id) { + case CurveId::kSecp256k1: { + static const std::shared_ptr kSecp256k1 = + CreateSecp256k1Context(); + return kSecp256k1; + } + case CurveId::kSm2P256V1: { + static const std::shared_ptr kSm2P256V1 = + CreateSm2P256V1Context(); + return kSm2P256V1; + } + } + + TECDSA_THROW_ARGUMENT("Unsupported curve id"); +} + +CurveId GroupContext::curve_id() const { return curve_id_; } + +std::string_view GroupContext::curve_name() const { return curve_name_; } + +const GroupContext::BigInt& GroupContext::order() const { return order_; } + +size_t GroupContext::scalar_size_bytes() const { return scalar_size_bytes_; } + +size_t GroupContext::compressed_point_size_bytes() const { + return compressed_point_size_bytes_; +} + +yacl::crypto::EcGroup& GroupContext::ec_group() const { return *ec_group_; } + +const std::shared_ptr& DefaultGroupContext() { + static const std::shared_ptr kDefault = + GroupContext::Create(DefaultEcdsaSuite().curve); + return kDefault; +} + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h b/yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h new file mode 100644 index 00000000..b6c7c270 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h @@ -0,0 +1,61 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/ecc/ecc_spi.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/math/mpint/mp_int.h" + +namespace tecdsa::core { + +class Scalar; +class Point; + +class GroupContext { + public: + using BigInt = yacl::math::MPInt; + + static std::shared_ptr Create(CurveId curve_id); + + GroupContext(CurveId curve_id, std::string curve_name, BigInt order, + size_t scalar_size_bytes, size_t compressed_point_size_bytes, + std::unique_ptr ec_group); + + CurveId curve_id() const; + std::string_view curve_name() const; + const BigInt& order() const; + size_t scalar_size_bytes() const; + size_t compressed_point_size_bytes() const; + yacl::crypto::EcGroup& ec_group() const; + + private: + CurveId curve_id_; + std::string curve_name_; + BigInt order_; + size_t scalar_size_bytes_; + size_t compressed_point_size_bytes_; + std::unique_ptr ec_group_; + + friend class Scalar; + friend class Point; +}; + +const std::shared_ptr& DefaultGroupContext(); + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/suite/suite.cc b/yacl/crypto/experimental/threshold_signatures/core/suite/suite.cc new file mode 100644 index 00000000..583bbe02 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/suite/suite.cc @@ -0,0 +1,75 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/hash/ssl_hash.h" + +namespace tecdsa::core { + +namespace { + +yacl::crypto::HashAlgorithm ToYaclHashAlgorithm(HashId hash_id) { + switch (hash_id) { + case HashId::kSha256: + return yacl::crypto::HashAlgorithm::SHA256; + case HashId::kSha512: + return yacl::crypto::HashAlgorithm::SHA512; + case HashId::kSm3: + return yacl::crypto::HashAlgorithm::SM3; + } + + TECDSA_THROW_ARGUMENT("Unsupported hash id"); +} + +} // namespace + +const ThresholdSuite& DefaultEcdsaSuite() { + static const ThresholdSuite kDefault = []() { + ThresholdSuite suite; + suite.scheme = SchemeId::kEcdsa; + suite.curve = CurveId::kSecp256k1; + suite.transcript_hash = HashId::kSha256; + suite.commitment_hash = HashId::kSha256; + suite.message_hash = HashId::kSha256; + suite.proof_domain_prefix = "GG2019"; + suite.normalize_low_s = true; + return suite; + }(); + return kDefault; +} + +const ThresholdSuite& DefaultSm2Suite() { + static const ThresholdSuite kDefault = []() { + ThresholdSuite suite; + suite.scheme = SchemeId::kSm2; + suite.curve = CurveId::kSm2P256V1; + suite.transcript_hash = HashId::kSm3; + suite.commitment_hash = HashId::kSm3; + suite.message_hash = HashId::kSm3; + suite.proof_domain_prefix = "GG2019"; + suite.normalize_low_s = false; + return suite; + }(); + return kDefault; +} + +Bytes Hash(HashId hash_id, std::span data) { + yacl::crypto::SslHash hash(ToYaclHashAlgorithm(hash_id)); + return hash.Update(yacl::ByteContainerView(data.data(), data.size())) + .CumulativeHash(); +} + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/suite/suite.h b/yacl/crypto/experimental/threshold_signatures/core/suite/suite.h new file mode 100644 index 00000000..325d2abb --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/suite/suite.h @@ -0,0 +1,55 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" + +namespace tecdsa::core { + +enum class SchemeId { + kEcdsa, + kSm2, +}; + +enum class CurveId { + kSecp256k1, + kSm2P256V1, +}; + +enum class HashId { + kSha256, + kSha512, + kSm3, +}; + +struct ThresholdSuite { + SchemeId scheme = SchemeId::kEcdsa; + CurveId curve = CurveId::kSecp256k1; + HashId transcript_hash = HashId::kSha256; + HashId commitment_hash = HashId::kSha256; + HashId message_hash = HashId::kSha256; + std::string proof_domain_prefix = "GG2019"; + bool normalize_low_s = true; +}; + +const ThresholdSuite& DefaultEcdsaSuite(); +const ThresholdSuite& DefaultSm2Suite(); + +Bytes Hash(HashId hash_id, std::span data); + +} // namespace tecdsa::core diff --git a/yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.cc b/yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.cc new file mode 100644 index 00000000..363951e3 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.cc @@ -0,0 +1,93 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +namespace tecdsa::core::transcript { +namespace { + +std::span AsByteSpan(std::string_view value) { + return std::span( + reinterpret_cast(value.data()), value.size()); +} + +} // namespace + +Transcript::Transcript(HashId challenge_hash) : challenge_hash_(challenge_hash) {} + +void Transcript::append(std::string_view label, std::span data) { + if (label.size() > UINT32_MAX || data.size() > UINT32_MAX) { + TECDSA_THROW_ARGUMENT("Transcript field exceeds uint32 length"); + } + + encoding::AppendU32Be(static_cast(label.size()), &transcript_); + transcript_.insert(transcript_.end(), label.begin(), label.end()); + + encoding::AppendU32Be(static_cast(data.size()), &transcript_); + transcript_.insert(transcript_.end(), data.begin(), data.end()); +} + +void Transcript::append_ascii(std::string_view label, std::string_view ascii) { + append(label, AsByteSpan(ascii)); +} + +void Transcript::append_proof_id(std::string_view proof_id) { + append_ascii("proof_id", proof_id); +} + +void Transcript::append_session_id(std::span session_id) { + append("session_id", session_id); +} + +void Transcript::append_u32_be(std::string_view label, uint32_t value) { + std::array encoded = { + static_cast((value >> 24) & 0xFF), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast(value & 0xFF), + }; + append(label, encoded); +} + +void Transcript::append_fields( + std::initializer_list fields) { + for (const TranscriptFieldRef& field : fields) { + append(field.label, field.data); + } +} + +Transcript::BigInt Transcript::challenge_bigint_mod( + const BigInt& modulus) const { + if (modulus <= BigInt(0)) { + TECDSA_THROW_ARGUMENT("Transcript challenge modulus must be positive"); + } + return bigint::NormalizeMod(bigint::FromBigEndian(Hash(challenge_hash_, transcript_)), + modulus); +} + +Scalar Transcript::challenge_scalar( + const std::shared_ptr& group) const { + return Scalar::FromBigEndianModQ(Hash(challenge_hash_, transcript_), group); +} + +const Bytes& Transcript::bytes() const { return transcript_; } + +} // namespace tecdsa::core::transcript diff --git a/yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h b/yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h new file mode 100644 index 00000000..7fbe5ff8 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h @@ -0,0 +1,56 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" + +namespace tecdsa::core::transcript { + +struct TranscriptFieldRef { + std::string_view label; + std::span data; +}; + +class Transcript { + public: + using BigInt = Scalar::BigInt; + + explicit Transcript(HashId challenge_hash); + + void append(std::string_view label, std::span data); + void append_ascii(std::string_view label, std::string_view ascii); + void append_proof_id(std::string_view proof_id); + void append_session_id(std::span session_id); + void append_u32_be(std::string_view label, uint32_t value); + void append_fields(std::initializer_list fields); + BigInt challenge_bigint_mod(const BigInt& modulus) const; + Scalar challenge_scalar( + const std::shared_ptr& group) const; + + const Bytes& bytes() const; + + private: + HashId challenge_hash_; + Bytes transcript_; +}; + +} // namespace tecdsa::core::transcript diff --git a/yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.cc b/yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.cc new file mode 100644 index 00000000..b16d1399 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.cc @@ -0,0 +1,76 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h" + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" + +namespace tecdsa::core::vss { +namespace { + +using BigInt = Scalar::BigInt; + +BigInt NormalizeModQ(const BigInt& value, + const std::shared_ptr& group) { + return bigint::NormalizeMod(value, group->order()); +} + +} // namespace + +std::unordered_map ComputeLagrangeAtZero( + const std::vector& participants, + const std::shared_ptr& group) { + std::unordered_map out; + out.reserve(participants.size()); + + for (PartyIndex i : participants) { + BigInt numerator(1); + BigInt denominator(1); + + for (PartyIndex j : participants) { + if (j == i) { + continue; + } + + const BigInt neg_j = NormalizeModQ(BigInt(0) - BigInt(j), group); + numerator = NormalizeModQ(numerator * neg_j, group); + + const BigInt diff = NormalizeModQ(BigInt(i) - BigInt(j), group); + if (diff == 0) { + TECDSA_THROW_ARGUMENT("duplicate participant id in lagrange set"); + } + denominator = NormalizeModQ(denominator * diff, group); + } + + Scalar lambda = + Scalar(numerator, group) * Scalar(denominator, group).InverseModQ(); + out.emplace(i, lambda); + } + + return out; +} + +ECPoint SumPointsOrThrow(const std::vector& points) { + if (points.empty()) { + TECDSA_THROW_ARGUMENT("cannot sum an empty point vector"); + } + ECPoint sum = points.front(); + for (size_t i = 1; i < points.size(); ++i) { + sum = sum.Add(points[i]); + } + return sum; +} + +} // namespace tecdsa::core::vss diff --git a/yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h b/yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h new file mode 100644 index 00000000..75bbbc41 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h @@ -0,0 +1,33 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" + +namespace tecdsa::core::vss { + +std::unordered_map ComputeLagrangeAtZero( + const std::vector& participants, + const std::shared_ptr& group); + +ECPoint SumPointsOrThrow(const std::vector& points); + +} // namespace tecdsa::core::vss diff --git a/yacl/crypto/experimental/threshold_signatures/core/vss/feldman.cc b/yacl/crypto/experimental/threshold_signatures/core/vss/feldman.cc new file mode 100644 index 00000000..5ddb5791 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/vss/feldman.cc @@ -0,0 +1,96 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +namespace tecdsa::core::vss { + +using BigInt = Scalar::BigInt; + +Scalar RandomNonZeroScalar(const std::shared_ptr& group) { + while (true) { + const Scalar candidate = Scalar::FromBigEndianModQ(Csprng::RandomBytes(32), group); + if (candidate.value() != 0) { + return candidate; + } + } +} + +Scalar EvaluatePolynomialAt(const std::vector& coefficients, + PartyIndex party_id) { + if (coefficients.empty()) { + TECDSA_THROW_ARGUMENT("polynomial coefficients must not be empty"); + } + + const auto& group = coefficients.front().group(); + const BigInt& q = group->order(); + const BigInt x = BigInt(party_id).Mod(q); + + BigInt acc(0); + BigInt power(1); + for (const Scalar& coefficient : coefficients) { + acc = bigint::NormalizeMod(acc + coefficient.mp_value() * power, q); + power = bigint::NormalizeMod(power * x, q); + } + return Scalar(acc, group); +} + +std::vector BuildCommitments( + const std::vector& coefficients) { + if (coefficients.empty()) { + TECDSA_THROW_ARGUMENT("polynomial coefficients must not be empty"); + } + + std::vector commitments; + commitments.reserve(coefficients.size()); + for (const Scalar& coefficient : coefficients) { + commitments.push_back(ECPoint::GeneratorMultiply(coefficient)); + } + return commitments; +} + +bool VerifyShareForReceiver(PartyIndex receiver_id, size_t threshold, + const std::vector& commitments, + const Scalar& share) { + if (share.value() == 0) { + return false; + } + if (commitments.size() != threshold + 1 || commitments.empty()) { + return false; + } + + try { + ECPoint rhs = commitments.front(); + const auto& group = share.group(); + const BigInt& q = group->order(); + const BigInt receiver = BigInt(receiver_id); + BigInt power = receiver.Mod(q); + for (size_t k = 1; k < commitments.size(); ++k) { + rhs = rhs.Add(commitments[k].Mul(Scalar(power, group))); + power = bigint::NormalizeMod(power * receiver, q); + } + const ECPoint lhs = ECPoint::GeneratorMultiply(share); + return lhs == rhs; + } catch (const std::exception&) { + return false; + } +} + +} // namespace tecdsa::core::vss diff --git a/yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h b/yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h new file mode 100644 index 00000000..3431d49d --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h @@ -0,0 +1,40 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/point.h" +#include "yacl/crypto/experimental/threshold_signatures/core/algebra/scalar.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" + +namespace tecdsa::core::vss { + +Scalar RandomNonZeroScalar(const std::shared_ptr& group); + +Scalar EvaluatePolynomialAt(const std::vector& coefficients, + PartyIndex party_id); + +std::vector BuildCommitments( + const std::vector& coefficients); + +bool VerifyShareForReceiver(PartyIndex receiver_id, size_t threshold, + const std::vector& commitments, + const Scalar& share); + +} // namespace tecdsa::core::vss diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.cc new file mode 100644 index 00000000..18ccc565 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.cc @@ -0,0 +1,285 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.h" + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" +#include "yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h" + +namespace tecdsa::ecdsa::keygen { +namespace { + +namespace paillier = tecdsa::core::paillier; + +constexpr uint32_t kMinPaillierKeygenBits = 2048; +constexpr uint32_t kMinAuxRsaKeygenBits = 164; +constexpr size_t kMaxPaillierKeygenAttempts = 32; +constexpr char kKeygenPhase1CommitDomain[] = "GG2019/keygen/phase1"; + +} // namespace + +KeygenParty::KeygenParty(KeygenConfig cfg) : cfg_(std::move(cfg)) { + const auto participant_set = core::participant::BuildParticipantSet( + cfg_.participants, cfg_.self_id, "ecdsa::keygen::KeygenParty"); + peers_ = participant_set.peers; + if (cfg_.threshold >= cfg_.participants.size()) { + TECDSA_THROW_ARGUMENT("threshold must be less than participant count"); + } + if (cfg_.paillier_modulus_bits < kMinPaillierKeygenBits) { + TECDSA_THROW_ARGUMENT("paillier_modulus_bits must be >= 2048"); + } + if (cfg_.aux_rsa_modulus_bits < kMinAuxRsaKeygenBits) { + TECDSA_THROW_ARGUMENT("aux_rsa_modulus_bits must be >= 164"); + } +} + +void KeygenParty::EnsureLocalPolynomialPrepared() { + if (!local_poly_coefficients_.empty()) { + return; + } + + const auto ecdsa_group = core::DefaultGroupContext(); + auto prepared = core::keygen::PrepareLocalRound1Bundle( + cfg_.participants, cfg_.threshold, core::DefaultEcdsaSuite(), + kKeygenPhase1CommitDomain, + [ecdsa_group]() { return core::vss::RandomNonZeroScalar(ecdsa_group); }, + [](const std::vector& coefficients, PartyIndex party) { + return core::vss::EvaluatePolynomialAt(coefficients, party); + }, + [](const std::vector& coefficients) { + return core::vss::BuildCommitments(coefficients); + }); + local_poly_coefficients_ = std::move(prepared.coefficients); + local_shares_ = std::move(prepared.shares); + local_vss_commitments_ = std::move(prepared.commitments); + local_y_i_ = prepared.public_point; + local_commitment_ = std::move(prepared.commitment); + local_open_randomness_ = std::move(prepared.randomness); +} + +void KeygenParty::EnsureLocalPaillierAndAuxPrepared() { + if (local_aux_rsa_params_.n_tilde > 0) { + return; + } + + auto prepared = core::keygen::PrepareLocalPaillierAuxBundle( + cfg_.paillier_modulus_bits, cfg_.aux_rsa_modulus_bits, + kMaxPaillierKeygenAttempts, cfg_.session_id, cfg_.self_id, + core::DefaultEcdsaSuite(), local_y_i_.group()); + local_paillier_ = std::move(prepared.paillier); + local_paillier_public_ = prepared.paillier_public; + local_aux_rsa_params_ = prepared.aux_rsa_params; + local_aux_rsa_witness_ = prepared.aux_rsa_witness; + local_aux_param_proof_ = prepared.aux_param_proof; +} + +void KeygenParty::EnsureLocalProofsPrepared() { + if (!local_square_free_proof_.blob.empty()) { + return; + } + + EnsureLocalPaillierAndAuxPrepared(); + const auto context = + paillier::BuildProofContext(cfg_.session_id, cfg_.self_id, + core::DefaultEcdsaSuite(), local_y_i_.group()); + local_square_free_proof_ = BuildSquareFreeProofGmr98( + local_paillier_public_.n, local_paillier_->private_lambda_bigint(), + context); +} + +KeygenRound1Msg KeygenParty::MakeRound1() { + if (round1_done_) { + TECDSA_THROW_LOGIC("MakeRound1 must not be called twice"); + } + + EnsureLocalPolynomialPrepared(); + EnsureLocalProofsPrepared(); + + all_phase1_commitments_[cfg_.self_id] = local_commitment_; + all_paillier_public_[cfg_.self_id] = local_paillier_public_; + all_aux_rsa_params_[cfg_.self_id] = local_aux_rsa_params_; + all_aux_param_proofs_[cfg_.self_id] = local_aux_param_proof_; + + round1_done_ = true; + return KeygenRound1Msg{ + .commitment = local_commitment_, + .paillier_public = local_paillier_public_, + .aux_rsa_params = local_aux_rsa_params_, + .aux_param_proof = local_aux_param_proof_, + }; +} + +KeygenRound2Out KeygenParty::MakeRound2( + const PeerMap& peer_round1) { + if (round2_done_) { + TECDSA_THROW_LOGIC("MakeRound2 must not be called twice"); + } + if (!round1_done_) { + TECDSA_THROW_LOGIC("MakeRound1 must be completed before MakeRound2"); + } + core::participant::RequireExactlyPeers(peer_round1, cfg_.participants, + cfg_.self_id, "peer_round1"); + + for (PartyIndex peer : peers_) { + const auto it = peer_round1.find(peer); + const KeygenRound1Msg& msg = it->second; + core::keygen::ValidatePeerRound1Common( + cfg_.session_id, peer, core::DefaultEcdsaSuite(), local_y_i_.group(), + msg.paillier_public, msg.aux_rsa_params, msg.aux_param_proof); + + all_phase1_commitments_[peer] = msg.commitment; + all_paillier_public_[peer] = msg.paillier_public; + all_aux_rsa_params_[peer] = msg.aux_rsa_params; + all_aux_param_proofs_[peer] = msg.aux_param_proof; + } + + KeygenRound2Out out; + out.broadcast = KeygenRound2Broadcast{ + .y_i = local_y_i_, + .randomness = local_open_randomness_, + .commitments = local_vss_commitments_, + }; + for (PartyIndex peer : peers_) { + out.shares_for_peers.emplace(peer, local_shares_.at(peer)); + } + round2_done_ = true; + return out; +} + +KeygenRound3Msg KeygenParty::MakeRound3( + const PeerMap& peer_round2, + const PeerMap& shares_for_self) { + if (round3_done_) { + TECDSA_THROW_LOGIC("MakeRound3 must not be called twice"); + } + if (!round2_done_) { + TECDSA_THROW_LOGIC("MakeRound2 must be completed before MakeRound3"); + } + core::participant::RequireExactlyPeers(peer_round2, cfg_.participants, + cfg_.self_id, "peer_round2"); + core::participant::RequireExactlyPeers(shares_for_self, cfg_.participants, + cfg_.self_id, "shares_for_self"); + + Scalar x_sum = local_shares_.at(cfg_.self_id); + std::vector y_points; + y_points.reserve(cfg_.participants.size()); + y_points.push_back(local_y_i_); + + for (PartyIndex peer : peers_) { + const KeygenRound2Broadcast& msg = peer_round2.at(peer); + const auto commitment_it = all_phase1_commitments_.find(peer); + if (commitment_it == all_phase1_commitments_.end()) { + TECDSA_THROW_LOGIC("missing stored round1 commitment for peer"); + } + + const Scalar share = shares_for_self.at(peer); + core::keygen::ValidatePeerRound2ShareCommon( + cfg_.threshold, core::DefaultEcdsaSuite(), kKeygenPhase1CommitDomain, + "y_i", commitment_it->second, msg.y_i, msg.randomness, msg.commitments, + share, [this](const std::vector& commitments, + const Scalar& candidate_share) { + return core::vss::VerifyShareForReceiver( + cfg_.self_id, cfg_.threshold, commitments, candidate_share); + }); + + x_sum = x_sum + share; + y_points.push_back(msg.y_i); + } + + if (x_sum.value() == 0) { + TECDSA_THROW("aggregated local share is zero"); + } + + local_x_i_ = x_sum; + try { + aggregated_y_ = core::vss::SumPointsOrThrow(y_points); + } catch (const std::exception& ex) { + TECDSA_THROW(std::string("failed to aggregate public key points: ") + + ex.what()); + } + + local_X_i_ = ECPoint::GeneratorMultiply(local_x_i_); + round3_done_ = true; + return KeygenRound3Msg{ + .X_i = local_X_i_, + .proof = core::proof::BuildSchnorrProof(core::DefaultEcdsaSuite(), + cfg_.session_id, cfg_.self_id, + local_X_i_, local_x_i_), + .square_free_proof = local_square_free_proof_, + }; +} + +KeygenOutput KeygenParty::Finalize(const PeerMap& peer_round3) { + if (!round3_done_) { + TECDSA_THROW_LOGIC("MakeRound3 must be completed before Finalize"); + } + + core::participant::RequireExactlyPeers(peer_round3, cfg_.participants, + cfg_.self_id, "peer_round3"); + + PublicKeygenData public_data; + public_data.y = aggregated_y_; + public_data.threshold = cfg_.threshold; + public_data.all_paillier_public = all_paillier_public_; + public_data.all_aux_rsa_params = all_aux_rsa_params_; + public_data.all_aux_param_proofs = all_aux_param_proofs_; + public_data.all_square_free_proofs[cfg_.self_id] = local_square_free_proof_; + public_data.all_X_i[cfg_.self_id] = local_X_i_; + + for (PartyIndex peer : peers_) { + const KeygenRound3Msg& msg = peer_round3.at(peer); + if (!core::proof::VerifySchnorrProof(core::DefaultEcdsaSuite(), + cfg_.session_id, peer, msg.X_i, + msg.proof)) { + TECDSA_THROW_ARGUMENT("peer Schnorr proof verification failed"); + } + const auto pk_it = all_paillier_public_.find(peer); + if (pk_it == all_paillier_public_.end()) { + TECDSA_THROW_LOGIC("missing stored Paillier public key for peer"); + } + const auto context = paillier::BuildProofContext( + cfg_.session_id, peer, core::DefaultEcdsaSuite(), local_y_i_.group()); + if (!VerifySquareFreeProofGmr98(pk_it->second.n, msg.square_free_proof, + context)) { + TECDSA_THROW_ARGUMENT("peer square-free proof verification failed"); + } + + public_data.all_X_i[peer] = msg.X_i; + public_data.all_square_free_proofs[peer] = msg.square_free_proof; + } + + return KeygenOutput{ + .local_key_share = + LocalKeyShare{ + .x_i = local_x_i_, + .X_i = local_X_i_, + .paillier = local_paillier_, + }, + .public_keygen_data = std::move(public_data), + }; +} + +} // namespace tecdsa::ecdsa::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.h new file mode 100644 index 00000000..ebd39b1e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.h @@ -0,0 +1,82 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/messages.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h" + +namespace tecdsa::ecdsa::keygen { + +struct KeygenConfig { + Bytes session_id; + PartyIndex self_id = 0; + std::vector participants; + uint32_t threshold = 1; + uint32_t paillier_modulus_bits = 2048; + uint32_t aux_rsa_modulus_bits = 2048; +}; + +class KeygenParty { + public: + explicit KeygenParty(KeygenConfig cfg); + + KeygenRound1Msg MakeRound1(); + KeygenRound2Out MakeRound2(const PeerMap& peer_round1); + KeygenRound3Msg MakeRound3( + const PeerMap& peer_round2, + const PeerMap& shares_for_self); + KeygenOutput Finalize(const PeerMap& peer_round3); + + private: + void EnsureLocalPolynomialPrepared(); + void EnsureLocalPaillierAndAuxPrepared(); + void EnsureLocalProofsPrepared(); + + KeygenConfig cfg_; + std::vector peers_; + + std::vector local_poly_coefficients_; + PeerMap local_shares_; + + std::shared_ptr local_paillier_; + PaillierPublicKey local_paillier_public_; + AuxRsaParams local_aux_rsa_params_; + core::paillier::PaperAuxSetupWitness local_aux_rsa_witness_; + SquareFreeProof local_square_free_proof_; + AuxCorrectFormProof local_aux_param_proof_; + + ECPoint local_y_i_; + Bytes local_commitment_; + Bytes local_open_randomness_; + std::vector local_vss_commitments_; + + PeerMap all_phase1_commitments_; + PeerMap all_paillier_public_; + PeerMap all_aux_rsa_params_; + PeerMap all_aux_param_proofs_; + + Scalar local_x_i_; + ECPoint local_X_i_; + ECPoint aggregated_y_; + + bool round1_done_ = false; + bool round2_done_ = false; + bool round3_done_ = false; +}; + +} // namespace tecdsa::ecdsa::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/messages.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/messages.h new file mode 100644 index 00000000..e60b54d0 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/messages.h @@ -0,0 +1,81 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/proofs/types.h" + +namespace tecdsa::ecdsa::keygen { + +template +using PeerMap = std::unordered_map; + +using AuxCorrectFormProof = core::paillier::AuxCorrectFormProof; +using AuxRsaParams = core::paillier::AuxRsaParams; +using SquareFreeProof = core::paillier::SquareFreeProof; + +struct KeygenRound1Msg { + Bytes commitment; + PaillierPublicKey paillier_public; + AuxRsaParams aux_rsa_params; + AuxCorrectFormProof aux_param_proof; +}; + +struct KeygenRound2Broadcast { + ECPoint y_i; + Bytes randomness; + std::vector commitments; +}; + +struct KeygenRound2Out { + KeygenRound2Broadcast broadcast; + PeerMap shares_for_peers; +}; + +struct KeygenRound3Msg { + ECPoint X_i; + tecdsa::ecdsa::proofs::SchnorrProof proof; + SquareFreeProof square_free_proof; +}; + +struct LocalKeyShare { + Scalar x_i; + ECPoint X_i; + std::shared_ptr paillier; +}; + +struct PublicKeygenData { + ECPoint y; + uint32_t threshold = 0; + PeerMap all_X_i; + PeerMap all_paillier_public; + PeerMap all_aux_rsa_params; + PeerMap all_square_free_proofs; + PeerMap all_aux_param_proofs; +}; + +struct KeygenOutput { + LocalKeyShare local_key_share; + PublicKeygenData public_keygen_data; +}; + +} // namespace tecdsa::ecdsa::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.cc new file mode 100644 index 00000000..3a01eafd --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.cc @@ -0,0 +1,137 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" + +namespace tecdsa::ecdsa::keygen { +namespace { + +namespace paillier = tecdsa::core::paillier; + +template +void RequirePartyEntry(const MapT& map, PartyIndex party, + const char* field_name) { + if (!map.contains(party)) { + TECDSA_THROW_ARGUMENT(std::string("public keygen data missing ") + + field_name); + } +} + +} // namespace + +VerifiedPublicKeygenData VerifiedPublicKeygenData::Create( + const PublicKeygenData& public_data, const Bytes& keygen_session_id, + const core::ThresholdSuite& suite, + const std::shared_ptr& group, + std::span participants, PartyIndex self_id, + const LocalKeyShare& local_key_share) { + for (PartyIndex party : participants) { + RequirePartyEntry(public_data.all_X_i, party, "X_i"); + RequirePartyEntry(public_data.all_paillier_public, party, + "Paillier public key"); + RequirePartyEntry(public_data.all_aux_rsa_params, party, + "auxiliary RSA parameters"); + RequirePartyEntry(public_data.all_square_free_proofs, party, + "square-free proof"); + RequirePartyEntry(public_data.all_aux_param_proofs, party, + "aux parameter proof"); + + const auto& paillier_public = public_data.all_paillier_public.at(party); + const auto& aux_params = public_data.all_aux_rsa_params.at(party); + const auto& square_free_proof = + public_data.all_square_free_proofs.at(party); + const auto& aux_param_proof = public_data.all_aux_param_proofs.at(party); + + paillier::ValidatePaillierPublicKeyOrThrow(paillier_public, group); + if (!paillier::ValidateAuxRsaParams(aux_params)) { + TECDSA_THROW_ARGUMENT("public aux RSA parameters are invalid"); + } + + const paillier::StrictProofVerifierContext proof_context = + paillier::BuildProofContext(keygen_session_id, party, suite, group); + if (!paillier::VerifySquareFreeProofGmr98(paillier_public.n, + square_free_proof, + proof_context)) { + TECDSA_THROW_ARGUMENT("square-free proof verification failed"); + } + if (!paillier::VerifyAuxCorrectFormProof(aux_params, aux_param_proof, + proof_context)) { + TECDSA_THROW_ARGUMENT("aux parameter proof verification failed"); + } + } + + if (local_key_share.paillier == nullptr) { + TECDSA_THROW_ARGUMENT("local Paillier provider must be present"); + } + const auto self_pk_it = public_data.all_paillier_public.find(self_id); + if (self_pk_it == public_data.all_paillier_public.end()) { + TECDSA_THROW_ARGUMENT("missing self Paillier public key"); + } + if (self_pk_it->second.n != local_key_share.paillier->modulus_n_bigint()) { + TECDSA_THROW_ARGUMENT( + "self Paillier public key does not match local provider"); + } + + const auto self_x_it = public_data.all_X_i.find(self_id); + if (self_x_it == public_data.all_X_i.end()) { + TECDSA_THROW_ARGUMENT("missing self X_i in public keygen data"); + } + if (self_x_it->second != local_key_share.X_i) { + TECDSA_THROW_ARGUMENT("self X_i does not match local key share"); + } + + return VerifiedPublicKeygenData(public_data); +} + +const PublicKeygenData& VerifiedPublicKeygenData::raw() const { + if (!initialized_) { + TECDSA_THROW_LOGIC("verified public keygen data is not initialized"); + } + return public_data_; +} + +const PaillierPublicKey& VerifiedPublicKeygenData::PaillierOf( + PartyIndex party) const { + const auto it = raw().all_paillier_public.find(party); + if (it == raw().all_paillier_public.end()) { + TECDSA_THROW_LOGIC("missing verified Paillier public key"); + } + return it->second; +} + +const AuxRsaParams& VerifiedPublicKeygenData::AuxOf(PartyIndex party) const { + const auto it = raw().all_aux_rsa_params.find(party); + if (it == raw().all_aux_rsa_params.end()) { + TECDSA_THROW_LOGIC("missing verified auxiliary parameters"); + } + return it->second; +} + +const ECPoint& VerifiedPublicKeygenData::XOf(PartyIndex party) const { + const auto it = raw().all_X_i.find(party); + if (it == raw().all_X_i.end()) { + TECDSA_THROW_LOGIC("missing verified X_i"); + } + return it->second; +} + +} // namespace tecdsa::ecdsa::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.h new file mode 100644 index 00000000..97f63b3c --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.h @@ -0,0 +1,52 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/messages.h" + +namespace tecdsa::ecdsa::keygen { + +class VerifiedPublicKeygenData { + public: + VerifiedPublicKeygenData() = default; + + static VerifiedPublicKeygenData Create( + const PublicKeygenData& public_data, const Bytes& keygen_session_id, + const core::ThresholdSuite& suite, + const std::shared_ptr& group, + std::span participants, PartyIndex self_id, + const LocalKeyShare& local_key_share); + + const PublicKeygenData& raw() const; + const PaillierPublicKey& PaillierOf(PartyIndex party) const; + const AuxRsaParams& AuxOf(PartyIndex party) const; + const ECPoint& XOf(PartyIndex party) const; + + private: + explicit VerifiedPublicKeygenData(PublicKeygenData public_data) + : public_data_(std::move(public_data)), initialized_(true) {} + + PublicKeygenData public_data_; + bool initialized_ = false; +}; + +} // namespace tecdsa::ecdsa::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/proofs/types.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/proofs/types.h new file mode 100644 index 00000000..c4e5ab0d --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/proofs/types.h @@ -0,0 +1,29 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/types.h" + +namespace tecdsa::ecdsa::proofs { + +using BigInt = core::paillier::BigInt; +using A1RangeProof = core::mta::A1RangeProof; +using A2MtAwcProof = core::mta::A2MtAwcProof; +using A3MtAProof = core::mta::A3MtAProof; +using SchnorrProof = core::proof::SchnorrProof; + +} // namespace tecdsa::ecdsa::proofs diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/finalize.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/finalize.cc new file mode 100644 index 00000000..c2c8c80a --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/finalize.cc @@ -0,0 +1,54 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h" + +namespace tecdsa::ecdsa::sign { +namespace { + +namespace verify = tecdsa::ecdsa::verify; + +} // namespace + +Signature SignParty::Finalize(const PeerMap& peer_round5e) { + state_.rounds.RequireReached(SignStep::kRound5EDone, + "RevealRound5E must be completed before Finalize"); + + core::protocol::RequireExactlyPeerMessages(peer_round5e, peers_, + "peer_round5e"); + Scalar s = state_.final_share.s_i; + for (PartyIndex peer : peers_) { + s = s + peer_round5e.at(peer); + } + if (s.value() == 0) { + TECDSA_THROW_ARGUMENT("aggregated signature scalar s is zero"); + } + + if (!verify::VerifyEcdsaSignatureMath(verified_keygen_data_.raw().y, + cfg_.msg32, state_.delta.r, s)) { + TECDSA_THROW_ARGUMENT("final ECDSA signature verification failed"); + } + + return Signature{ + .r = state_.delta.r, + .s = s, + .R = state_.delta.R, + }; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h new file mode 100644 index 00000000..55f8f80e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h @@ -0,0 +1,62 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +namespace tecdsa::ecdsa::sign::internal { + +inline constexpr size_t kCommitmentLen = 32; +inline constexpr char kPhase1CommitDomain[] = "GG2019/sign/phase1"; +inline constexpr char kPhase5ACommitDomain[] = "GG2019/sign/phase5A"; +inline constexpr char kPhase5CCommitDomain[] = "GG2019/sign/phase5C"; + +inline void ValidateCommitmentOrThrow(const Bytes& commitment, + const char* field_name) { + if (commitment.size() != kCommitmentLen) { + TECDSA_THROW_ARGUMENT(std::string(field_name) + + " must be exactly 32 bytes"); + } +} + +inline std::optional InvertScalar(const Scalar& scalar) { + if (scalar.value() == 0) { + return std::nullopt; + } + try { + return scalar.InverseModQ(); + } catch (const std::exception&) { + return std::nullopt; + } +} + +template +void StoreCommitments(const PeerMap& messages, + std::span peers, + PeerMap* commitments, const char* field_name) { + for (PartyIndex peer : peers) { + const Bytes& commitment = messages.at(peer).commitment; + ValidateCommitmentOrThrow(commitment, field_name); + (*commitments)[peer] = commitment; + } +} + +} // namespace tecdsa::ecdsa::sign::internal diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/messages.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/messages.h new file mode 100644 index 00000000..49733185 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/messages.h @@ -0,0 +1,97 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/proofs/types.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h" + +namespace tecdsa::ecdsa::sign { + +template +using PeerMap = std::unordered_map; + +enum class MtaType : uint8_t { + kTimesGamma = 1, + kTimesW = 2, +}; + +struct SignRound1Msg { + Bytes commitment; +}; + +struct SignRound2Request { + PartyIndex from = 0; + PartyIndex to = 0; + MtaType type = MtaType::kTimesGamma; + Bytes instance_id; + BigInt c1 = BigInt(0); + proofs::A1RangeProof a1_proof; +}; + +struct SignRound2Response { + PartyIndex from = 0; + PartyIndex to = 0; + MtaType type = MtaType::kTimesGamma; + Bytes instance_id; + BigInt c2 = BigInt(0); + std::optional a2_proof; + std::optional a3_proof; +}; + +struct SignRound3Msg { + Scalar delta_i; +}; + +struct SignRound4Msg { + ECPoint gamma_i; + Bytes randomness; + proofs::SchnorrProof gamma_proof; +}; + +struct SignRound5AMsg { + Bytes commitment; +}; + +struct SignRound5BMsg { + ECPoint V_i; + ECPoint A_i; + Bytes randomness; + proofs::SchnorrProof a_schnorr_proof; + VRelationProof v_relation_proof; +}; + +struct SignRound5CMsg { + Bytes commitment; +}; + +struct SignRound5DMsg { + ECPoint U_i; + ECPoint T_i; + Bytes randomness; +}; + +struct Signature { + Scalar r; + Scalar s; + ECPoint R; +}; + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.cc new file mode 100644 index 00000000..a5ddf48c --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.cc @@ -0,0 +1,217 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::ecdsa::sign { +namespace { + +namespace mta = tecdsa::core::mta; + +mta::MtaType ToCoreMtaType(MtaType type) { + switch (type) { + case MtaType::kTimesGamma: + return mta::MtaType::kMta; + case MtaType::kTimesW: + return mta::MtaType::kMtAwc; + } + TECDSA_THROW_ARGUMENT("unknown protocol MtaType"); +} + +MtaType FromCoreMtaType(mta::MtaType type) { + switch (type) { + case mta::MtaType::kMta: + return MtaType::kTimesGamma; + case mta::MtaType::kMtAwc: + return MtaType::kTimesW; + } + TECDSA_THROW_ARGUMENT("unknown core MtaType"); +} + +SignRound2Request ToProtocolRequest( + const mta::PairwiseProductRequest& request) { + return SignRound2Request{ + .from = request.from, + .to = request.to, + .type = FromCoreMtaType(request.type), + .instance_id = request.instance_id, + .c1 = request.c1, + .a1_proof = request.a1_proof, + }; +} + +mta::PairwiseProductRequest ToCoreRequest(const SignRound2Request& request) { + return mta::PairwiseProductRequest{ + .from = request.from, + .to = request.to, + .type = ToCoreMtaType(request.type), + .instance_id = request.instance_id, + .c1 = request.c1, + .a1_proof = request.a1_proof, + }; +} + +SignRound2Response ToProtocolResponse( + const mta::PairwiseProductResponse& response) { + return SignRound2Response{ + .from = response.from, + .to = response.to, + .type = FromCoreMtaType(response.type), + .instance_id = response.instance_id, + .c2 = response.c2, + .a2_proof = response.a2_proof, + .a3_proof = response.a3_proof, + }; +} + +mta::PairwiseProductResponse ToCoreResponse( + const SignRound2Response& response) { + return mta::PairwiseProductResponse{ + .from = response.from, + .to = response.to, + .type = ToCoreMtaType(response.type), + .instance_id = response.instance_id, + .c2 = response.c2, + .a2_proof = response.a2_proof, + .a3_proof = response.a3_proof, + }; +} + +void AddShare(Round2MtaSums* sums, MtaType type, bool initiator, + const Scalar& share) { + if (type == MtaType::kTimesGamma) { + if (initiator) { + sums->delta_initiator = sums->delta_initiator + share; + } else { + sums->delta_responder = sums->delta_responder + share; + } + return; + } + if (initiator) { + sums->sigma_initiator = sums->sigma_initiator + share; + } else { + sums->sigma_responder = sums->sigma_responder + share; + } +} + +} // namespace + +SigningMtaExchange::SigningMtaExchange(Config cfg) + : session_({.session_id = std::move(cfg.session_id), + .self_id = cfg.self_id, + .suite = std::move(cfg.suite), + .group = std::move(cfg.group)}), + self_id_(cfg.self_id) {} + +std::vector SigningMtaExchange::CreateRequests( + std::span peers, + const keygen::LocalKeyShare& local_key_share, + const keygen::VerifiedPublicKeygenData& public_data, + const Scalar& local_k_i) { + std::vector out; + out.reserve(peers.size() * 2); + for (PartyIndex peer : peers) { + const auto& peer_aux = public_data.AuxOf(peer); + out.push_back(ToProtocolRequest(session_.InitiatorInit({ + .responder_id = peer, + .initiator_paillier = local_key_share.paillier.get(), + .responder_aux = &peer_aux, + .initiator_secret = local_k_i, + }))); + out.push_back(ToProtocolRequest(session_.InitiatorInitWithCheck({ + .responder_id = peer, + .initiator_paillier = local_key_share.paillier.get(), + .responder_aux = &peer_aux, + .initiator_secret = local_k_i, + }))); + } + return out; +} + +Round2ResponseBatch SigningMtaExchange::ConsumeRequests( + const std::vector& requests_for_self, + std::span peers, + const keygen::VerifiedPublicKeygenData& public_data, + const Scalar& local_gamma_i, const Scalar& local_w_i, + const PeerMap& w_points) { + const std::array expected_types = { + mta::MtaType::kMta, mta::MtaType::kMtAwc}; + mta::RequireExactlyOneRequestPerPeerAndType( + requests_for_self, peers, self_id_, expected_types, ToCoreRequest, + "round2 request"); + + Round2ResponseBatch out; + out.messages.reserve(requests_for_self.size()); + const auto& self_aux = public_data.AuxOf(self_id_); + for (const SignRound2Request& request : requests_for_self) { + const Scalar witness = + (request.type == MtaType::kTimesGamma) ? local_gamma_i : local_w_i; + auto consume_result = + request.type == MtaType::kTimesGamma + ? session_.ResponderMid( + ToCoreRequest(request), + {.initiator_modulus_n = + public_data.PaillierOf(request.from).n, + .responder_aux = &self_aux, + .initiator_aux = &public_data.AuxOf(request.from), + .responder_secret = witness}) + : session_.ResponderMidWithCheck( + ToCoreRequest(request), + {.initiator_modulus_n = + public_data.PaillierOf(request.from).n, + .responder_aux = &self_aux, + .initiator_aux = &public_data.AuxOf(request.from), + .responder_secret = witness, + .public_witness_point = w_points.at(self_id_)}); + AddShare(&out.sums, request.type, false, consume_result.responder_share); + out.messages.push_back(ToProtocolResponse(consume_result.response)); + } + return out; +} + +Round2MtaSums SigningMtaExchange::ConsumeResponses( + const std::vector& responses_for_self, + std::span peers, + const keygen::LocalKeyShare& local_key_share, + const keygen::VerifiedPublicKeygenData& public_data, + const PeerMap& w_points) { + mta::RequireExactlyOneResponsePerInitiatorInstance( + responses_for_self, peers, peers.size() * 2, self_id_, session_, + ToCoreResponse, "round2 response"); + + Round2MtaSums sums; + const auto& self_aux = public_data.AuxOf(self_id_); + for (const SignRound2Response& response : responses_for_self) { + auto consume_result = + response.type == MtaType::kTimesGamma + ? session_.InitiatorEnd( + ToCoreResponse(response), + {.initiator_paillier = local_key_share.paillier.get(), + .initiator_aux = &self_aux}) + : session_.InitiatorEndWithCheck( + ToCoreResponse(response), + {.initiator_paillier = local_key_share.paillier.get(), + .initiator_aux = &self_aux, + .public_witness_point = w_points.at(response.from)}); + AddShare(&sums, response.type, true, consume_result.initiator_share); + } + return sums; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.h new file mode 100644 index 00000000..e332a8ac --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.h @@ -0,0 +1,77 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/mta/session.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/messages.h" + +namespace tecdsa::ecdsa::sign { + +struct Round2MtaSums { + Scalar delta_initiator; + Scalar delta_responder; + Scalar sigma_initiator; + Scalar sigma_responder; +}; + +struct Round2ResponseBatch { + std::vector messages; + Round2MtaSums sums; +}; + +class SigningMtaExchange { + public: + struct Config { + Bytes session_id; + PartyIndex self_id = 0; + core::ThresholdSuite suite; + std::shared_ptr group; + }; + + explicit SigningMtaExchange(Config cfg); + + std::vector CreateRequests( + std::span peers, + const keygen::LocalKeyShare& local_key_share, + const keygen::VerifiedPublicKeygenData& public_data, + const Scalar& local_k_i); + + Round2ResponseBatch ConsumeRequests( + const std::vector& requests_for_self, + std::span peers, + const keygen::VerifiedPublicKeygenData& public_data, + const Scalar& local_gamma_i, const Scalar& local_w_i, + const PeerMap& w_points); + + Round2MtaSums ConsumeResponses( + const std::vector& responses_for_self, + std::span peers, + const keygen::LocalKeyShare& local_key_share, + const keygen::VerifiedPublicKeygenData& public_data, + const PeerMap& w_points); + + private: + core::mta::PairwiseProductSession session_; + PartyIndex self_id_ = 0; +}; + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.cc new file mode 100644 index 00000000..a4995c2f --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.cc @@ -0,0 +1,149 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h" + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +namespace tecdsa::ecdsa::sign { +namespace { + +constexpr char kVRelationProofId[] = "GG2019/VRel/v1"; + +} // namespace + +Bytes SerializePointPair(const ECPoint& first, const ECPoint& second) { + Bytes out; + const Bytes first_bytes = first.ToCompressedBytes(); + const Bytes second_bytes = second.ToCompressedBytes(); + out.reserve(first_bytes.size() + second_bytes.size()); + out.insert(out.end(), first_bytes.begin(), first_bytes.end()); + out.insert(out.end(), second_bytes.begin(), second_bytes.end()); + return out; +} + +Scalar BuildVRelationChallenge(const Bytes& session_id, PartyIndex party_id, + const ECPoint& r_statement, + const ECPoint& v_statement, + const ECPoint& alpha) { + core::transcript::Transcript transcript(core::DefaultEcdsaSuite().transcript_hash); + const Bytes r_bytes = core::encoding::EncodePoint(r_statement); + const Bytes v_bytes = core::encoding::EncodePoint(v_statement); + const Bytes alpha_bytes = core::encoding::EncodePoint(alpha); + transcript.append_proof_id(kVRelationProofId); + transcript.append_session_id(session_id); + transcript.append_u32_be("party_id", party_id); + transcript.append_fields({ + core::transcript::TranscriptFieldRef{.label = "R", .data = r_bytes}, + core::transcript::TranscriptFieldRef{.label = "V", .data = v_bytes}, + core::transcript::TranscriptFieldRef{.label = "alpha", + .data = alpha_bytes}, + }); + return transcript.challenge_scalar(r_statement.group()); +} + +ECPoint BuildRGeneratorLinearCombination(const ECPoint& r_base, + const Scalar& r_multiplier, + const Scalar& g_multiplier) { + std::optional out; + if (r_multiplier.value() != 0) { + out = r_base.Mul(r_multiplier); + } + if (g_multiplier.value() != 0) { + const ECPoint g_term = ECPoint::GeneratorMultiply(g_multiplier); + out = out.has_value() ? out->Add(g_term) : g_term; + } + if (!out.has_value()) { + TECDSA_THROW_ARGUMENT("linear combination is point at infinity"); + } + return *out; +} + +VRelationProof BuildVRelationProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& r_statement, + const ECPoint& v_statement, + const Scalar& s_witness, + const Scalar& l_witness) { + try { + if (BuildRGeneratorLinearCombination(r_statement, s_witness, l_witness) != + v_statement) { + TECDSA_THROW_ARGUMENT("v relation witness does not match statement"); + } + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("invalid v relation witness: ") + + ex.what()); + } + + while (true) { + const Scalar a = Csprng::RandomScalar(); + const Scalar b = Csprng::RandomScalar(); + if (a.value() == 0 && b.value() == 0) { + continue; + } + + ECPoint alpha; + try { + alpha = BuildRGeneratorLinearCombination(r_statement, a, b); + } catch (const std::exception&) { + continue; + } + + const Scalar c = BuildVRelationChallenge(session_id, prover_id, r_statement, + v_statement, alpha); + const Scalar t = a + (c * s_witness); + const Scalar u = b + (c * l_witness); + if (t.value() == 0 && u.value() == 0) { + continue; + } + + return VRelationProof{ + .alpha = alpha, + .t = t, + .u = u, + }; + } +} + +bool VerifyVRelationProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& r_statement, + const ECPoint& v_statement, + const VRelationProof& proof) { + if (proof.t.value() == 0 && proof.u.value() == 0) { + return false; + } + + try { + const Scalar c = BuildVRelationChallenge(session_id, prover_id, r_statement, + v_statement, proof.alpha); + const ECPoint lhs = + BuildRGeneratorLinearCombination(r_statement, proof.t, proof.u); + + ECPoint rhs = proof.alpha; + if (c.value() != 0) { + rhs = rhs.Add(v_statement.Mul(c)); + } + return lhs == rhs; + } catch (const std::exception&) { + return false; + } +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h new file mode 100644 index 00000000..0da3f796 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h @@ -0,0 +1,50 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" + +namespace tecdsa::ecdsa::sign { + +struct VRelationProof { + ECPoint alpha; + Scalar t; + Scalar u; +}; + +Bytes SerializePointPair(const ECPoint& first, const ECPoint& second); + +Scalar BuildVRelationChallenge(const Bytes& session_id, PartyIndex party_id, + const ECPoint& r_statement, + const ECPoint& v_statement, + const ECPoint& alpha); + +ECPoint BuildRGeneratorLinearCombination(const ECPoint& r_base, + const Scalar& r_multiplier, + const Scalar& g_multiplier); + +VRelationProof BuildVRelationProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& r_statement, + const ECPoint& v_statement, + const Scalar& s_witness, + const Scalar& l_witness); + +bool VerifyVRelationProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& r_statement, + const ECPoint& v_statement, + const VRelationProof& proof); + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round1.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round1.cc new file mode 100644 index 00000000..d79202ef --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round1.cc @@ -0,0 +1,30 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::ecdsa::sign { + +SignRound1Msg SignParty::MakeRound1() { + if (state_.rounds.HasReached(SignStep::kRound1Done)) { + TECDSA_THROW_LOGIC("MakeRound1 must not be called twice"); + } + EnsurePhase1Prepared(); + return SignRound1Msg{ + .commitment = state_.round1.commitments.at(cfg_.self_id)}; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round2_mta.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round2_mta.cc new file mode 100644 index 00000000..5f5bec19 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round2_mta.cc @@ -0,0 +1,66 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h" + +namespace tecdsa::ecdsa::sign { + +std::vector SignParty::MakeRound2Requests( + const PeerMap& peer_round1) { + if (state_.rounds.HasReached(SignStep::kRound2RequestsDone)) { + TECDSA_THROW_LOGIC("MakeRound2Requests must not be called twice"); + } + + EnsurePhase1Prepared(); + core::protocol::RequireExactlyPeerMessages(peer_round1, peers_, + "peer_round1"); + internal::StoreCommitments(peer_round1, peers_, &state_.round1.commitments, + "sign round1 commitment"); + + std::vector out = phase2_mta_.CreateRequests( + peers_, cfg_.local_key_share, verified_keygen_data_, + state_.round1.k_i); + state_.rounds.Advance(SignStep::kRound1Done, SignStep::kRound2RequestsDone, + "MakeRound2Requests must not be called twice"); + return out; +} + +std::vector SignParty::MakeRound2Responses( + const std::vector& requests_for_self) { + if (state_.rounds.HasReached(SignStep::kRound2ResponsesDone)) { + TECDSA_THROW_LOGIC("MakeRound2Responses must not be called twice"); + } + state_.rounds.RequireReached( + SignStep::kRound2RequestsDone, + "MakeRound2Requests must be completed before MakeRound2Responses"); + + Round2ResponseBatch batch = phase2_mta_.ConsumeRequests( + requests_for_self, peers_, verified_keygen_data_, state_.round1.gamma_i, + local_w_i_, w_points_); + state_.round2.sums.delta_responder = batch.sums.delta_responder; + state_.round2.sums.sigma_responder = batch.sums.sigma_responder; + state_.rounds.Advance(SignStep::kRound2RequestsDone, + SignStep::kRound2ResponsesDone, + "MakeRound2Responses must not be called twice"); + return std::move(batch.messages); +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round3_delta.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round3_delta.cc new file mode 100644 index 00000000..e7ff32da --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round3_delta.cc @@ -0,0 +1,51 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::ecdsa::sign { + +SignRound3Msg SignParty::MakeRound3( + const std::vector& responses_for_self) { + if (state_.rounds.HasReached(SignStep::kRound3Done)) { + TECDSA_THROW_LOGIC("MakeRound3 must not be called twice"); + } + state_.rounds.RequireReached( + SignStep::kRound2ResponsesDone, + "MakeRound2Responses must be completed before MakeRound3"); + + const Round2MtaSums initiator_sums = phase2_mta_.ConsumeResponses( + responses_for_self, peers_, cfg_.local_key_share, verified_keygen_data_, + w_points_); + state_.round2.sums.delta_initiator = initiator_sums.delta_initiator; + state_.round2.sums.sigma_initiator = initiator_sums.sigma_initiator; + + state_.round2.delta_i = + (state_.round1.k_i * state_.round1.gamma_i) + + state_.round2.sums.delta_initiator + + state_.round2.sums.delta_responder; + state_.round2.sigma_i = + (state_.round1.k_i * local_w_i_) + + state_.round2.sums.sigma_initiator + + state_.round2.sums.sigma_responder; + state_.rounds.Advance(SignStep::kRound2ResponsesDone, SignStep::kRound3Done, + "MakeRound3 must not be called twice"); + return SignRound3Msg{.delta_i = state_.round2.delta_i}; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round4_gamma.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round4_gamma.cc new file mode 100644 index 00000000..58501969 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round4_gamma.cc @@ -0,0 +1,61 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h" +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h" + +namespace tecdsa::ecdsa::sign { + +SignRound4Msg SignParty::MakeRound4( + const PeerMap& peer_round3) { + if (state_.rounds.HasReached(SignStep::kRound4Done)) { + TECDSA_THROW_LOGIC("MakeRound4 must not be called twice"); + } + state_.rounds.RequireReached(SignStep::kRound3Done, + "MakeRound3 must be completed before MakeRound4"); + + core::protocol::RequireExactlyPeerMessages(peer_round3, peers_, + "peer_round3"); + Scalar delta = state_.round2.delta_i; + for (PartyIndex peer : peers_) { + delta = delta + peer_round3.at(peer).delta_i; + } + if (delta.value() == 0) { + TECDSA_THROW_ARGUMENT("aggregated delta is zero"); + } + + const std::optional delta_inv = internal::InvertScalar(delta); + if (!delta_inv.has_value()) { + TECDSA_THROW_ARGUMENT("failed to invert aggregated delta"); + } + state_.delta.delta_inv = *delta_inv; + + state_.rounds.Advance(SignStep::kRound3Done, SignStep::kRound4Done, + "MakeRound4 must not be called twice"); + return SignRound4Msg{ + .gamma_i = state_.round1.Gamma_i, + .randomness = state_.round1.randomness, + .gamma_proof = core::proof::BuildSchnorrProof( + core::DefaultEcdsaSuite(), cfg_.session_id, cfg_.self_id, + state_.round1.Gamma_i, state_.round1.gamma_i), + }; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round5_consistency.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round5_consistency.cc new file mode 100644 index 00000000..d9daa897 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round5_consistency.cc @@ -0,0 +1,222 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h" +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h" + +namespace tecdsa::ecdsa::sign { +namespace { + +namespace relation = tecdsa::ecdsa::sign; +namespace verify = tecdsa::ecdsa::verify; + +} // namespace + +SignRound5AMsg SignParty::MakeRound5A( + const PeerMap& peer_round4) { + if (state_.rounds.HasReached(SignStep::kRound5ADone)) { + TECDSA_THROW_LOGIC("MakeRound5A must not be called twice"); + } + state_.rounds.RequireReached(SignStep::kRound4Done, + "MakeRound4 must be completed before MakeRound5A"); + + core::protocol::RequireExactlyPeerMessages(peer_round4, peers_, + "peer_round4"); + std::vector gamma_points; + gamma_points.reserve(cfg_.participants.size()); + gamma_points.push_back(state_.round1.Gamma_i); + + for (PartyIndex peer : peers_) { + const SignRound4Msg& msg = peer_round4.at(peer); + const auto commitment_it = state_.round1.commitments.find(peer); + if (commitment_it == state_.round1.commitments.end()) { + TECDSA_THROW_LOGIC("missing stored round1 commitment for peer"); + } + if (!core::commitment::VerifyCommitment( + core::DefaultEcdsaSuite(), internal::kPhase1CommitDomain, + msg.gamma_i.ToCompressedBytes(), msg.randomness, + commitment_it->second)) { + TECDSA_THROW_ARGUMENT( + "round4 gamma opening does not match round1 commitment"); + } + if (!core::proof::VerifySchnorrProof(core::DefaultEcdsaSuite(), + cfg_.session_id, peer, msg.gamma_i, + msg.gamma_proof)) { + TECDSA_THROW_ARGUMENT("round4 gamma Schnorr proof verification failed"); + } + gamma_points.push_back(msg.gamma_i); + } + + try { + state_.delta.gamma = core::vss::SumPointsOrThrow(gamma_points); + state_.delta.R = state_.delta.gamma.Mul(state_.delta.delta_inv); + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("failed to compute R in round5A: ") + + ex.what()); + } + state_.delta.r = verify::XCoordinateModQ(state_.delta.R); + if (state_.delta.r.value() == 0) { + TECDSA_THROW_ARGUMENT("computed r is zero"); + } + + EnsureRound5ASharePrepared(); + return SignRound5AMsg{ + .commitment = + state_.final_share.round5a_commitments.at(cfg_.self_id)}; +} + +SignRound5BMsg SignParty::MakeRound5B( + const PeerMap& peer_round5a) { + if (state_.rounds.HasReached(SignStep::kRound5BDone)) { + TECDSA_THROW_LOGIC("MakeRound5B must not be called twice"); + } + state_.rounds.RequireReached(SignStep::kRound5ADone, + "MakeRound5A must be completed before MakeRound5B"); + + core::protocol::RequireExactlyPeerMessages(peer_round5a, peers_, + "peer_round5a"); + internal::StoreCommitments(peer_round5a, peers_, + &state_.final_share.round5a_commitments, + "sign round5A commitment"); + + state_.rounds.Advance(SignStep::kRound5ADone, SignStep::kRound5BDone, + "MakeRound5B must not be called twice"); + return SignRound5BMsg{ + .V_i = state_.final_share.V_i, + .A_i = state_.final_share.A_i, + .randomness = state_.final_share.round5a_randomness, + .a_schnorr_proof = core::proof::BuildSchnorrProof( + core::DefaultEcdsaSuite(), cfg_.session_id, cfg_.self_id, + state_.final_share.A_i, state_.final_share.rho_i), + .v_relation_proof = relation::BuildVRelationProof( + cfg_.session_id, cfg_.self_id, state_.delta.R, + state_.final_share.V_i, state_.final_share.s_i, + state_.final_share.l_i), + }; +} + +SignRound5CMsg SignParty::MakeRound5C( + const PeerMap& peer_round5b) { + if (state_.rounds.HasReached(SignStep::kRound5CDone)) { + TECDSA_THROW_LOGIC("MakeRound5C must not be called twice"); + } + state_.rounds.RequireReached(SignStep::kRound5BDone, + "MakeRound5B must be completed before MakeRound5C"); + + core::protocol::RequireExactlyPeerMessages(peer_round5b, peers_, + "peer_round5b"); + std::vector v_points; + std::vector a_points; + v_points.reserve(cfg_.participants.size()); + a_points.reserve(cfg_.participants.size()); + v_points.push_back(state_.final_share.V_i); + a_points.push_back(state_.final_share.A_i); + + for (PartyIndex peer : peers_) { + const SignRound5BMsg& msg = peer_round5b.at(peer); + const auto commitment_it = + state_.final_share.round5a_commitments.find(peer); + if (commitment_it == state_.final_share.round5a_commitments.end()) { + TECDSA_THROW_LOGIC("missing stored round5A commitment for peer"); + } + + if (!core::commitment::VerifyCommitment( + core::DefaultEcdsaSuite(), internal::kPhase5ACommitDomain, + relation::SerializePointPair(msg.V_i, msg.A_i), msg.randomness, + commitment_it->second)) { + TECDSA_THROW_ARGUMENT( + "round5B opening does not match round5A commitment"); + } + if (!core::proof::VerifySchnorrProof(core::DefaultEcdsaSuite(), + cfg_.session_id, peer, msg.A_i, + msg.a_schnorr_proof)) { + TECDSA_THROW_ARGUMENT("round5B A_i Schnorr proof verification failed"); + } + if (!relation::VerifyVRelationProof(cfg_.session_id, peer, state_.delta.R, + msg.V_i, msg.v_relation_proof)) { + TECDSA_THROW_ARGUMENT("round5B V relation proof verification failed"); + } + + v_points.push_back(msg.V_i); + a_points.push_back(msg.A_i); + } + + try { + state_.consistency.V = core::vss::SumPointsOrThrow(v_points); + state_.consistency.A = core::vss::SumPointsOrThrow(a_points); + if (message_scalar_.value() != 0) { + state_.consistency.V = state_.consistency.V.Add( + ECPoint::GeneratorMultiply(Scalar() - message_scalar_)); + } + state_.consistency.V = state_.consistency.V.Add( + verified_keygen_data_.raw().y.Mul(Scalar() - state_.delta.r)); + state_.consistency.U_i = + state_.consistency.V.Mul(state_.final_share.rho_i); + state_.consistency.T_i = + state_.consistency.A.Mul(state_.final_share.l_i); + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("failed to compute round5C values: ") + + ex.what()); + } + + const core::commitment::CommitmentResult commit = + core::commitment::CommitMessage( + core::DefaultEcdsaSuite(), internal::kPhase5CCommitDomain, + relation::SerializePointPair(state_.consistency.U_i, + state_.consistency.T_i)); + state_.consistency.round5c_randomness = commit.randomness; + state_.consistency.round5c_commitments[cfg_.self_id] = commit.commitment; + state_.rounds.Advance(SignStep::kRound5BDone, SignStep::kRound5CDone, + "MakeRound5C must not be called twice"); + return SignRound5CMsg{ + .commitment = state_.consistency.round5c_commitments.at(cfg_.self_id)}; +} + +SignRound5DMsg SignParty::MakeRound5D( + const PeerMap& peer_round5c) { + if (state_.rounds.HasReached(SignStep::kRound5DDone)) { + TECDSA_THROW_LOGIC("MakeRound5D must not be called twice"); + } + state_.rounds.RequireReached(SignStep::kRound5CDone, + "MakeRound5C must be completed before MakeRound5D"); + + core::protocol::RequireExactlyPeerMessages(peer_round5c, peers_, + "peer_round5c"); + internal::StoreCommitments(peer_round5c, peers_, + &state_.consistency.round5c_commitments, + "sign round5C commitment"); + + state_.rounds.Advance(SignStep::kRound5CDone, SignStep::kRound5DDone, + "MakeRound5D must not be called twice"); + return SignRound5DMsg{ + .U_i = state_.consistency.U_i, + .T_i = state_.consistency.T_i, + .randomness = state_.consistency.round5c_randomness, + }; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round5_reveal.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round5_reveal.cc new file mode 100644 index 00000000..a2a5015e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/round5_reveal.cc @@ -0,0 +1,87 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h" +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/message_store.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h" + +namespace tecdsa::ecdsa::sign { +namespace { + +namespace relation = tecdsa::ecdsa::sign; + +} // namespace + +Scalar SignParty::RevealRound5E( + const PeerMap& peer_round5d) { + if (state_.rounds.HasReached(SignStep::kRound5EDone)) { + TECDSA_THROW_LOGIC("RevealRound5E must not be called twice"); + } + state_.rounds.RequireReached( + SignStep::kRound5DDone, + "MakeRound5D must be completed before RevealRound5E"); + + core::protocol::RequireExactlyPeerMessages(peer_round5d, peers_, + "peer_round5d"); + std::vector u_points; + std::vector t_points; + u_points.reserve(cfg_.participants.size()); + t_points.reserve(cfg_.participants.size()); + u_points.push_back(state_.consistency.U_i); + t_points.push_back(state_.consistency.T_i); + + for (PartyIndex peer : peers_) { + const SignRound5DMsg& msg = peer_round5d.at(peer); + const auto commitment_it = + state_.consistency.round5c_commitments.find(peer); + if (commitment_it == state_.consistency.round5c_commitments.end()) { + TECDSA_THROW_LOGIC("missing stored round5C commitment for peer"); + } + if (!core::commitment::VerifyCommitment( + core::DefaultEcdsaSuite(), internal::kPhase5CCommitDomain, + relation::SerializePointPair(msg.U_i, msg.T_i), msg.randomness, + commitment_it->second)) { + TECDSA_THROW_ARGUMENT( + "round5D opening does not match round5C commitment"); + } + u_points.push_back(msg.U_i); + t_points.push_back(msg.T_i); + } + + try { + const ECPoint sum_u = core::vss::SumPointsOrThrow(u_points); + const ECPoint sum_t = core::vss::SumPointsOrThrow(t_points); + if (sum_u != sum_t) { + TECDSA_THROW_ARGUMENT("round5D consistency check failed"); + } + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("failed to validate round5D: ") + + ex.what()); + } + + state_.rounds.Advance(SignStep::kRound5DDone, SignStep::kRound5EDone, + "RevealRound5E must not be called twice"); + return state_.final_share.s_i; +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.cc new file mode 100644 index 00000000..2b4a41d3 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.cc @@ -0,0 +1,162 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" + +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h" +#include "yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/internal.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/relation_proofs.h" + +namespace tecdsa::ecdsa::sign { +namespace { + +namespace keygen = tecdsa::ecdsa::keygen; +namespace relation = tecdsa::ecdsa::sign; + +} // namespace + +SignParty::SignParty(SignConfig cfg) + : cfg_(std::move(cfg)), + phase2_mta_({.session_id = cfg_.session_id, + .self_id = cfg_.self_id, + .suite = core::DefaultEcdsaSuite(), + .group = nullptr}), + message_scalar_(Scalar::FromBigEndianModQ(cfg_.msg32)) { + const auto participant_set = core::participant::BuildParticipantSet( + cfg_.participants, cfg_.self_id, "ecdsa::sign::SignParty"); + peers_ = participant_set.peers; + if (cfg_.participants.size() != + static_cast(cfg_.public_keygen_data.threshold) + 1) { + TECDSA_THROW_ARGUMENT("signer set size must equal threshold + 1"); + } + if (cfg_.msg32.size() != 32) { + TECDSA_THROW_ARGUMENT("msg32 must be exactly 32 bytes for SignParty"); + } + if (cfg_.local_key_share.x_i.value() == 0) { + TECDSA_THROW_ARGUMENT("local x_i share must be non-zero"); + } + if (cfg_.local_key_share.paillier == nullptr) { + TECDSA_THROW_ARGUMENT("local Paillier provider must be present"); + } + if (ECPoint::GeneratorMultiply(cfg_.local_key_share.x_i) != + cfg_.local_key_share.X_i) { + TECDSA_THROW_ARGUMENT("local key share X_i does not match x_i"); + } + + verified_keygen_data_ = keygen::VerifiedPublicKeygenData::Create( + cfg_.public_keygen_data, cfg_.keygen_session_id, + core::DefaultEcdsaSuite(), cfg_.local_key_share.x_i.group(), + cfg_.participants, cfg_.self_id, cfg_.local_key_share); + + PrepareResharedSigningShares(); +} + +void SignParty::PrepareResharedSigningShares() { + lagrange_coefficients_ = core::vss::ComputeLagrangeAtZero( + cfg_.participants, cfg_.local_key_share.x_i.group()); + + const auto lambda_self_it = lagrange_coefficients_.find(cfg_.self_id); + if (lambda_self_it == lagrange_coefficients_.end()) { + TECDSA_THROW_ARGUMENT("missing lagrange coefficient for self"); + } + + local_w_i_ = lambda_self_it->second * cfg_.local_key_share.x_i; + + std::vector w_points; + w_points.reserve(cfg_.participants.size()); + for (PartyIndex party : cfg_.participants) { + const auto lambda_it = lagrange_coefficients_.find(party); + if (lambda_it == lagrange_coefficients_.end()) { + TECDSA_THROW_ARGUMENT("missing lagrange coefficient for participant"); + } + + try { + w_points_[party] = verified_keygen_data_.XOf(party).Mul(lambda_it->second); + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("failed to compute W_i: ") + ex.what()); + } + w_points.push_back(w_points_.at(party)); + } + + try { + const ECPoint reconstructed_y = core::vss::SumPointsOrThrow(w_points); + if (reconstructed_y != verified_keygen_data_.raw().y) { + TECDSA_THROW_ARGUMENT("W_i aggregation does not reconstruct y"); + } + } catch (const std::exception& ex) { + TECDSA_THROW_ARGUMENT(std::string("failed to validate W_i aggregation: ") + + ex.what()); + } +} + +void SignParty::EnsurePhase1Prepared() { + if (state_.rounds.HasReached(SignStep::kRound1Done)) { + return; + } + + const auto& group = cfg_.local_key_share.x_i.group(); + state_.round1.k_i = core::vss::RandomNonZeroScalar(group); + state_.round1.gamma_i = core::vss::RandomNonZeroScalar(group); + state_.round1.Gamma_i = ECPoint::GeneratorMultiply(state_.round1.gamma_i); + + const core::commitment::CommitmentResult commit = + core::commitment::CommitMessage( + core::DefaultEcdsaSuite(), internal::kPhase1CommitDomain, + state_.round1.Gamma_i.ToCompressedBytes()); + state_.round1.randomness = commit.randomness; + state_.round1.commitments[cfg_.self_id] = commit.commitment; + state_.rounds.Advance(SignStep::kInit, SignStep::kRound1Done, + "MakeRound1 must not be called twice"); +} + +void SignParty::EnsureRound5ASharePrepared() { + if (state_.rounds.HasReached(SignStep::kRound5ADone)) { + return; + } + + state_.final_share.s_i = + (message_scalar_ * state_.round1.k_i) + + (state_.delta.r * state_.round2.sigma_i); + const auto& group = cfg_.local_key_share.x_i.group(); + state_.final_share.l_i = core::vss::RandomNonZeroScalar(group); + state_.final_share.rho_i = core::vss::RandomNonZeroScalar(group); + + state_.final_share.V_i = ECPoint::GeneratorMultiply(state_.final_share.l_i); + if (state_.final_share.s_i.value() != 0) { + state_.final_share.V_i = + state_.final_share.V_i.Add(state_.delta.R.Mul(state_.final_share.s_i)); + } + state_.final_share.A_i = ECPoint::GeneratorMultiply(state_.final_share.rho_i); + + const core::commitment::CommitmentResult commit = + core::commitment::CommitMessage( + core::DefaultEcdsaSuite(), internal::kPhase5ACommitDomain, + relation::SerializePointPair(state_.final_share.V_i, + state_.final_share.A_i)); + state_.final_share.round5a_randomness = commit.randomness; + state_.final_share.round5a_commitments[cfg_.self_id] = commit.commitment; + state_.rounds.Advance(SignStep::kRound4Done, SignStep::kRound5ADone, + "MakeRound5A must not be called twice"); +} + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h new file mode 100644 index 00000000..eeb1ee71 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h @@ -0,0 +1,78 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/messages.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/verified_data.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/messages.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/state.h" + +namespace tecdsa::ecdsa::sign { + +using LocalKeyShare = tecdsa::ecdsa::keygen::LocalKeyShare; +using PublicKeygenData = tecdsa::ecdsa::keygen::PublicKeygenData; + +struct SignConfig { + Bytes session_id; + Bytes keygen_session_id; + PartyIndex self_id = 0; + std::vector participants; + LocalKeyShare local_key_share; + PublicKeygenData public_keygen_data; + Bytes msg32; +}; + +class SignParty { + public: + explicit SignParty(SignConfig cfg); + + SignRound1Msg MakeRound1(); + std::vector MakeRound2Requests( + const PeerMap& peer_round1); + std::vector MakeRound2Responses( + const std::vector& requests_for_self); + SignRound3Msg MakeRound3( + const std::vector& responses_for_self); + SignRound4Msg MakeRound4(const PeerMap& peer_round3); + SignRound5AMsg MakeRound5A(const PeerMap& peer_round4); + SignRound5BMsg MakeRound5B(const PeerMap& peer_round5a); + SignRound5CMsg MakeRound5C(const PeerMap& peer_round5b); + SignRound5DMsg MakeRound5D(const PeerMap& peer_round5c); + Scalar RevealRound5E(const PeerMap& peer_round5d); + Signature Finalize(const PeerMap& peer_round5e); + + private: + void PrepareResharedSigningShares(); + void EnsurePhase1Prepared(); + void EnsureRound5ASharePrepared(); + + SignConfig cfg_; + keygen::VerifiedPublicKeygenData verified_keygen_data_; + std::vector peers_; + SigningMtaExchange phase2_mta_; + SigningState state_; + + Scalar message_scalar_; + PeerMap lagrange_coefficients_; + PeerMap w_points_; + Scalar local_w_i_; +}; + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/state.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/state.h new file mode 100644 index 00000000..fd33a6d7 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/sign/state.h @@ -0,0 +1,88 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "yacl/crypto/experimental/threshold_signatures/core/protocol/round_driver.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/messages.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/mta_exchange.h" + +namespace tecdsa::ecdsa::sign { + +enum class SignStep : uint8_t { + kInit = 0, + kRound1Done = 1, + kRound2RequestsDone = 2, + kRound2ResponsesDone = 3, + kRound3Done = 4, + kRound4Done = 5, + kRound5ADone = 6, + kRound5BDone = 7, + kRound5CDone = 8, + kRound5DDone = 9, + kRound5EDone = 10, +}; + +struct Round1State { + Scalar k_i; + Scalar gamma_i; + ECPoint Gamma_i; + Bytes randomness; + PeerMap commitments; +}; + +struct Round2MtaState { + Round2MtaSums sums; + Scalar delta_i; + Scalar sigma_i; +}; + +struct DeltaState { + Scalar delta_inv; + ECPoint gamma; + ECPoint R; + Scalar r; +}; + +struct FinalShareState { + Scalar s_i; + Scalar l_i; + Scalar rho_i; + ECPoint V_i; + ECPoint A_i; + Bytes round5a_randomness; + PeerMap round5a_commitments; +}; + +struct ConsistencyState { + ECPoint V; + ECPoint A; + ECPoint U_i; + ECPoint T_i; + Bytes round5c_randomness; + PeerMap round5c_commitments; +}; + +struct SigningState { + core::protocol::LinearRoundDriver rounds{SignStep::kInit}; + Round1State round1; + Round2MtaState round2; + DeltaState delta; + FinalShareState final_share; + ConsistencyState consistency; +}; + +} // namespace tecdsa::ecdsa::sign diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.cc b/yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.cc new file mode 100644 index 00000000..cb1b2cb4 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.cc @@ -0,0 +1,81 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h" + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" + +namespace tecdsa::ecdsa::verify { + +Scalar XCoordinateModQ(const ECPoint& point) { + const Bytes compressed = point.ToCompressedBytes(); + if (compressed.size() != 33) { + TECDSA_THROW_ARGUMENT("invalid compressed point length"); + } + const std::span x_bytes(compressed.data() + 1, 32); + return Scalar::FromBigEndianModQ(x_bytes); +} + +bool IsHighScalar(const Scalar& scalar) { + static const auto kHalfOrder = Scalar::ModulusQMpInt() >> 1; + return scalar.value() > kHalfOrder; +} + +bool VerifyEcdsaSignatureMath(const ECPoint& public_key, + std::span msg32, const Scalar& r, + const Scalar& s) { + // Align with GG2019's final signature check: standard ECDSA verification on + // secp256k1. + if (msg32.size() != 32 || r.mp_value() == 0 || s.mp_value() == 0) { + return false; + } + + try { + const Scalar z = Scalar::FromBigEndianModQ(msg32); + const Scalar w = s.InverseModQ(); + const Scalar u1 = z * w; + const Scalar u2 = r * w; + + std::optional left; + std::optional right; + if (u1.mp_value() != 0) { + left = ECPoint::GeneratorMultiply(u1); + } + if (u2.mp_value() != 0) { + right = public_key.Mul(u2); + } + + ECPoint reconstructed; + if (left.has_value() && right.has_value()) { + reconstructed = left->Add(*right); + } else if (left.has_value()) { + reconstructed = *left; + } else if (right.has_value()) { + reconstructed = *right; + } else { + return false; + } + + const Scalar v = XCoordinateModQ(reconstructed); + return v == r; + } catch (const std::exception&) { + return false; + } +} + +} // namespace tecdsa::ecdsa::verify diff --git a/yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h b/yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h new file mode 100644 index 00000000..fce3f712 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h @@ -0,0 +1,33 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" + +namespace tecdsa::ecdsa::verify { + +Scalar XCoordinateModQ(const ECPoint& point); +bool IsHighScalar(const Scalar& scalar); + +// Verify secp256k1 ECDSA by the standard equation: +// R' = (m * s^-1)G + (r * s^-1)Y, accept iff r == x(R') mod q. +bool VerifyEcdsaSignatureMath(const ECPoint& public_key, + std::span msg32, const Scalar& r, + const Scalar& s); + +} // namespace tecdsa::ecdsa::verify diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/common.h b/yacl/crypto/experimental/threshold_signatures/sm2/common.h new file mode 100644 index 00000000..50ad3e5e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/common.h @@ -0,0 +1,113 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include + +#include "yacl/crypto/ecc/ecc_spi.h" +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/group_context.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/dealerless_dkg.h" +#include "yacl/crypto/experimental/threshold_signatures/core/vss/feldman.h" +#include "yacl/crypto/experimental/threshold_signatures/core/bigint/bigint_utils.h" +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/core/random/csprng.h" + +namespace tecdsa::sm2::internal { + +using BigInt = Scalar::BigInt; + +inline const std::shared_ptr& Sm2Group() { + static const std::shared_ptr kGroup = + core::GroupContext::Create(core::CurveId::kSm2P256V1); + return kGroup; +} + +inline Scalar Sm2Zero() { return Scalar(BigInt(0), Sm2Group()); } + +inline Scalar Sm2One() { return Scalar::FromUint64(1, Sm2Group()); } + +inline Scalar Sm2Negate(const Scalar& value) { return Sm2Zero() - value; } + +inline Scalar RandomSm2Scalar() { + while (true) { + try { + return Scalar::FromCanonicalBytes(Csprng::RandomBytes(32), Sm2Group()); + } catch (const std::exception&) { + continue; + } + } +} + +inline Scalar RandomNonZeroSm2Scalar() { + while (true) { + const Scalar candidate = RandomSm2Scalar(); + if (candidate.value() != 0) { + return candidate; + } + } +} + +inline Scalar XCoordinateModN(const ECPoint& point) { + const Bytes compressed = point.ToCompressedBytes(); + if (compressed.size() != Sm2Group()->compressed_point_size_bytes()) { + TECDSA_THROW_ARGUMENT("invalid SM2 compressed point length"); + } + return Scalar::FromBigEndianModQ( + std::span(compressed.data() + 1, compressed.size() - 1), + Sm2Group()); +} + +inline ECPoint SumPointsOrThrow(const std::vector& points) { + return core::vss::SumPointsOrThrow(points); +} + +inline Scalar EvaluatePolynomialAt(const std::vector& coefficients, + PartyIndex party_id) { + return core::vss::EvaluatePolynomialAt(coefficients, party_id); +} + +inline std::vector BuildCommitments( + const std::vector& coefficients) { + return core::vss::BuildCommitments(coefficients); +} + +inline bool VerifyShareForReceiver(PartyIndex receiver_id, size_t threshold, + const std::vector& commitments, + const Scalar& share) { + return core::vss::VerifyShareForReceiver(receiver_id, threshold, commitments, + share); +} + +inline std::unordered_map ComputeLagrangeAtZero( + const std::vector& participants) { + return core::vss::ComputeLagrangeAtZero(participants, Sm2Group()); +} + +inline Bytes SerializeUncompressed(const ECPoint& point) { + const auto& group = *Sm2Group(); + const auto ec_point = group.ec_group().DeserializePoint( + point.ToCompressedBytes(), yacl::crypto::PointOctetFormat::X962Compressed); + const yacl::Buffer encoded = group.ec_group().SerializePoint( + ec_point, yacl::crypto::PointOctetFormat::X962Uncompressed); + return Bytes(encoded.data(), encoded.data() + encoded.size()); +} + +} // namespace tecdsa::sm2::internal diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h b/yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h new file mode 100644 index 00000000..83b0def5 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h @@ -0,0 +1,40 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h" + +namespace tecdsa::sm2::detection { + +inline AbortEvidence MakeAbort( + AbortStage stage, EvidenceKind evidence_kind, const Bytes& session_id, + PartyIndex reporter, std::string reason, + std::optional culprit = std::nullopt, + std::optional instance_id = std::nullopt) { + return AbortEvidence{ + .session_id = session_id, + .stage = stage, + .evidence_kind = evidence_kind, + .reporter = reporter, + .culprit = culprit, + .instance_id = std::move(instance_id), + .reason = std::move(reason), + }; +} + +} // namespace tecdsa::sm2::detection diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h b/yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h new file mode 100644 index 00000000..f25b86d2 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h @@ -0,0 +1,57 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" + +namespace tecdsa::sm2::detection { + +enum class AbortStage { + kKeygen = 1, + kOffline = 2, + kOnline = 3, +}; + +enum class EvidenceKind { + kMtaProof = 1, + kNonceProof = 2, + kPartialSignature = 3, + kGammaProof = 4, + kSquareFreeProof = 5, + kCommitment = 6, +}; + +struct AbortEvidence { + Bytes session_id; + AbortStage stage = AbortStage::kKeygen; + EvidenceKind evidence_kind = EvidenceKind::kMtaProof; + PartyIndex reporter = 0; + std::optional culprit; + std::optional instance_id; + std::string reason; +}; + +template +struct DetectionResult { + std::optional value; + std::optional abort; + + bool ok() const { return value.has_value(); } +}; + +} // namespace tecdsa::sm2::detection diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.cc b/yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.cc new file mode 100644 index 00000000..415ebdaa --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.cc @@ -0,0 +1,483 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h" + +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/keygen/round12_helpers.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paillier.h" +#include "yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h" + +namespace tecdsa::sm2::keygen { +namespace { + +namespace mta = tecdsa::core::mta; +namespace paillier = tecdsa::core::paillier; + +constexpr uint32_t kMinPaillierKeygenBits = 2048; +constexpr uint32_t kMinAuxRsaKeygenBits = 164; +constexpr size_t kMaxPaillierKeygenAttempts = 32; +constexpr char kKeygenPhase1CommitDomain[] = "SM2/keygen/phase1"; +constexpr char kDefaultSignerId[] = "1234567812345678"; + +} // namespace + +KeygenParty::KeygenParty(KeygenConfig cfg) + : cfg_(std::move(cfg)), + sigma_session_({.session_id = cfg_.session_id, + .self_id = cfg_.self_id, + .suite = core::DefaultSm2Suite(), + .group = internal::Sm2Group()}) { + const auto participant_set = core::participant::BuildParticipantSet( + cfg_.participants, cfg_.self_id, "sm2::keygen::KeygenParty"); + peers_ = participant_set.peers; + if (cfg_.threshold >= cfg_.participants.size()) { + TECDSA_THROW_ARGUMENT("threshold must be less than participant count"); + } + if (cfg_.paillier_modulus_bits < kMinPaillierKeygenBits) { + TECDSA_THROW_ARGUMENT("paillier_modulus_bits must be >= 2048"); + } + if (cfg_.aux_rsa_modulus_bits < kMinAuxRsaKeygenBits) { + TECDSA_THROW_ARGUMENT("aux_rsa_modulus_bits must be >= 164"); + } + if (cfg_.signer_id.empty()) { + cfg_.signer_id.assign(kDefaultSignerId, + kDefaultSignerId + sizeof(kDefaultSignerId) - 1); + } + local_z_i_ = internal::Sm2Zero(); + local_secret_z_i_ = internal::Sm2Zero(); + local_gamma_i_ = internal::Sm2Zero(); + sigma_initiator_sum_ = internal::Sm2Zero(); + sigma_responder_sum_ = internal::Sm2Zero(); + local_sigma_i_ = internal::Sm2Zero(); +} + +void KeygenParty::EnsureLocalPolynomialPrepared() { + if (!local_poly_coefficients_.empty()) { + return; + } + + auto prepared = core::keygen::PrepareLocalRound1Bundle( + cfg_.participants, cfg_.threshold, core::DefaultSm2Suite(), + kKeygenPhase1CommitDomain, []() { return internal::RandomNonZeroSm2Scalar(); }, + [](const std::vector& coefficients, PartyIndex party) { + return internal::EvaluatePolynomialAt(coefficients, party); + }, + [](const std::vector& coefficients) { + return internal::BuildCommitments(coefficients); + }); + local_poly_coefficients_ = std::move(prepared.coefficients); + local_shares_ = std::move(prepared.shares); + local_vss_commitments_ = std::move(prepared.commitments); + local_Z_i_ = prepared.public_point; + local_secret_z_i_ = local_poly_coefficients_.front(); + local_commitment_ = std::move(prepared.commitment); + local_open_randomness_ = std::move(prepared.randomness); +} + +void KeygenParty::EnsureLocalPaillierAndAuxPrepared() { + if (local_aux_rsa_params_.n_tilde > 0) { + return; + } + + auto prepared = core::keygen::PrepareLocalPaillierAuxBundle( + cfg_.paillier_modulus_bits, cfg_.aux_rsa_modulus_bits, + kMaxPaillierKeygenAttempts, cfg_.session_id, cfg_.self_id, + core::DefaultSm2Suite(), internal::Sm2Group()); + local_paillier_ = std::move(prepared.paillier); + local_paillier_public_ = prepared.paillier_public; + local_aux_rsa_params_ = prepared.aux_rsa_params; + local_aux_rsa_witness_ = prepared.aux_rsa_witness; + local_aux_param_proof_ = prepared.aux_param_proof; +} + +void KeygenParty::EnsureLocalProofsPrepared() { + if (!local_square_free_proof_.blob.empty()) { + return; + } + + EnsureLocalPaillierAndAuxPrepared(); + const auto context = + paillier::BuildProofContext(cfg_.session_id, cfg_.self_id, + core::DefaultSm2Suite(), internal::Sm2Group()); + local_square_free_proof_ = paillier::BuildSquareFreeProofGmr98( + local_paillier_public_.n, local_paillier_->private_lambda_bigint(), + context); +} + +KeygenRound1Msg KeygenParty::MakeRound1() { + EnsureLocalPolynomialPrepared(); + EnsureLocalProofsPrepared(); + + all_phase1_commitments_[cfg_.self_id] = local_commitment_; + all_paillier_public_[cfg_.self_id] = local_paillier_public_; + all_aux_rsa_params_[cfg_.self_id] = local_aux_rsa_params_; + all_aux_param_proofs_[cfg_.self_id] = local_aux_param_proof_; + + return KeygenRound1Msg{ + .commitment = local_commitment_, + .paillier_public = local_paillier_public_, + .aux_rsa_params = local_aux_rsa_params_, + .aux_param_proof = local_aux_param_proof_, + }; +} + +KeygenRound2Out KeygenParty::MakeRound2( + const PeerMap& peer_round1) { + (void)MakeRound1(); + core::participant::RequireExactlyPeers(peer_round1, cfg_.participants, + cfg_.self_id, "peer_round1"); + + for (PartyIndex peer : peers_) { + const auto& msg = peer_round1.at(peer); + core::keygen::ValidatePeerRound1Common( + cfg_.session_id, peer, core::DefaultSm2Suite(), internal::Sm2Group(), + msg.paillier_public, msg.aux_rsa_params, msg.aux_param_proof); + + all_phase1_commitments_[peer] = msg.commitment; + all_paillier_public_[peer] = msg.paillier_public; + all_aux_rsa_params_[peer] = msg.aux_rsa_params; + all_aux_param_proofs_[peer] = msg.aux_param_proof; + } + + KeygenRound2Out out; + out.broadcast = KeygenRound2Broadcast{ + .Z_i = local_Z_i_, + .randomness = local_open_randomness_, + .commitments = local_vss_commitments_, + }; + for (PartyIndex peer : peers_) { + out.shares_for_peers.emplace(peer, local_shares_.at(peer)); + } + round2_done_ = true; + return out; +} + +std::vector KeygenParty::MakeRound3Requests( + const PeerMap& peer_round2, + const PeerMap& shares_for_self) { + if (round3_requests_done_) { + TECDSA_THROW_LOGIC("MakeRound3Requests must not be called twice"); + } + if (!round2_done_) { + TECDSA_THROW_LOGIC("MakeRound2 must be completed before MakeRound3Requests"); + } + + core::participant::RequireExactlyPeers(peer_round2, cfg_.participants, + cfg_.self_id, "peer_round2"); + core::participant::RequireExactlyPeers(shares_for_self, cfg_.participants, + cfg_.self_id, "shares_for_self"); + + Scalar z_sum = local_shares_.at(cfg_.self_id); + for (PartyIndex peer : peers_) { + const auto& msg = peer_round2.at(peer); + const auto commitment_it = all_phase1_commitments_.find(peer); + if (commitment_it == all_phase1_commitments_.end()) { + TECDSA_THROW_LOGIC("missing stored round1 commitment for peer"); + } + const Scalar share = shares_for_self.at(peer); + core::keygen::ValidatePeerRound2ShareCommon( + cfg_.threshold, core::DefaultSm2Suite(), kKeygenPhase1CommitDomain, + "Z_i", commitment_it->second, msg.Z_i, msg.randomness, msg.commitments, + share, [this](const std::vector& commitments, + const Scalar& candidate_share) { + return internal::VerifyShareForReceiver( + cfg_.self_id, cfg_.threshold, commitments, candidate_share); + }); + z_sum = z_sum + share; + } + + if (z_sum.value() == 0) { + TECDSA_THROW_ARGUMENT("aggregated local z share is zero"); + } + + local_z_i_ = z_sum; + local_Y_i_ = ECPoint::GeneratorMultiply(local_z_i_); + local_gamma_i_ = internal::RandomNonZeroSm2Scalar(); + local_Gamma_i_ = ECPoint::GeneratorMultiply(local_gamma_i_); + std::vector z_points; + z_points.reserve(cfg_.participants.size()); + z_points.push_back(local_Z_i_); + for (PartyIndex peer : peers_) { + z_points.push_back(peer_round2.at(peer).Z_i); + } + global_Z_ = internal::SumPointsOrThrow(z_points); + local_ZGamma_i_ = global_Z_.Mul(local_gamma_i_); + + std::vector out; + out.reserve(peers_.size()); + for (PartyIndex peer : peers_) { + out.push_back(sigma_session_.InitiatorInit({ + .responder_id = peer, + .initiator_paillier = local_paillier_.get(), + .responder_aux = &all_aux_rsa_params_.at(peer), + .initiator_secret = local_gamma_i_, + })); + } + round3_requests_done_ = true; + return out; +} + +detection::DetectionResult> +KeygenParty::TryMakeRound3Responses( + const std::vector& requests_for_self) { + if (round3_responses_done_) { + TECDSA_THROW_LOGIC("MakeRound3Responses must not be called twice"); + } + if (!round3_requests_done_) { + TECDSA_THROW_LOGIC( + "MakeRound3Requests must be completed before MakeRound3Responses"); + } + + try { + mta::RequireExactlyOneRequestPerPeer( + requests_for_self, peers_, cfg_.self_id, mta::MtaType::kMta, + std::identity{}, "SM2 keygen request"); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kMtaProof, cfg_.session_id, + cfg_.self_id, ex.what())}; + } + + std::vector out; + out.reserve(requests_for_self.size()); + Scalar responder_sum = sigma_responder_sum_; + for (const auto& request : requests_for_self) { + try { + const auto consume = sigma_session_.ResponderMid( + request, + {.initiator_modulus_n = all_paillier_public_.at(request.from).n, + .responder_aux = &all_aux_rsa_params_.at(cfg_.self_id), + .initiator_aux = &all_aux_rsa_params_.at(request.from), + .responder_secret = local_secret_z_i_}); + responder_sum = responder_sum + consume.responder_share; + out.push_back(consume.response); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kMtaProof, cfg_.session_id, + cfg_.self_id, ex.what(), request.from, request.instance_id)}; + } + } + + sigma_responder_sum_ = responder_sum; + round3_responses_done_ = true; + return {.value = std::move(out), .abort = std::nullopt}; +} + +KeygenRound4Msg KeygenParty::MakeRound4( + const std::vector& responses_for_self) { + if (round4_done_) { + TECDSA_THROW_LOGIC("MakeRound4 must not be called twice"); + } + if (!round3_responses_done_) { + TECDSA_THROW_LOGIC("MakeRound3Responses must be completed before MakeRound4"); + } + + mta::RequireExactlyOneResponsePerInitiatorInstance( + responses_for_self, peers_, peers_.size(), cfg_.self_id, sigma_session_, + std::identity{}, "SM2 keygen response"); + + for (const auto& response : responses_for_self) { + const auto consume = sigma_session_.InitiatorEnd( + response, + {.initiator_paillier = local_paillier_.get(), + .initiator_aux = &all_aux_rsa_params_.at(cfg_.self_id)}); + sigma_initiator_sum_ = sigma_initiator_sum_ + consume.initiator_share; + } + + local_sigma_i_ = (local_gamma_i_ * local_secret_z_i_) + + sigma_initiator_sum_ + sigma_responder_sum_; + round4_done_ = true; + return KeygenRound4Msg{ + .sigma_i = local_sigma_i_, + .Y_i = local_Y_i_, + .y_proof = + proofs::BuildPiGroupProof(cfg_.session_id, cfg_.self_id, local_Y_i_, + local_z_i_), + .Gamma_i = local_Gamma_i_, + .ZGamma_i = local_ZGamma_i_, + .gamma_proof = proofs::BuildPiGroupRelationProof( + cfg_.session_id, cfg_.self_id, global_Z_, local_Gamma_i_, + local_ZGamma_i_, local_gamma_i_), + .square_free_proof = local_square_free_proof_, + }; +} + +detection::DetectionResult KeygenParty::TryFinalize( + const PeerMap& peer_round4) { + if (!round4_done_) { + TECDSA_THROW_LOGIC("MakeRound4 must be completed before Finalize"); + } + + try { + core::participant::RequireExactlyPeers(peer_round4, cfg_.participants, + cfg_.self_id, "peer_round4"); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, ex.what())}; + } + + Scalar sigma = local_sigma_i_; + std::vector gamma_points; + gamma_points.reserve(cfg_.participants.size()); + gamma_points.push_back(local_Gamma_i_); + + PublicKeygenData public_data; + public_data.threshold = cfg_.threshold; + public_data.all_Y_i[cfg_.self_id] = local_Y_i_; + public_data.all_paillier_public = all_paillier_public_; + public_data.all_aux_rsa_params = all_aux_rsa_params_; + public_data.all_aux_param_proofs = all_aux_param_proofs_; + public_data.all_square_free_proofs[cfg_.self_id] = local_square_free_proof_; + + for (PartyIndex peer : peers_) { + const auto& msg = peer_round4.at(peer); + try { + if (!proofs::VerifyPiGroupProof(cfg_.session_id, peer, msg.Y_i, + msg.y_proof)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, "peer y share proof verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + try { + if (!proofs::VerifyPiGroupRelationProof(cfg_.session_id, peer, global_Z_, + msg.Gamma_i, msg.ZGamma_i, + msg.gamma_proof)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, "peer gamma proof verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + const auto context = + paillier::BuildProofContext(cfg_.session_id, peer, + core::DefaultSm2Suite(), + internal::Sm2Group()); + try { + if (!paillier::VerifySquareFreeProofGmr98( + all_paillier_public_.at(peer).n, msg.square_free_proof, + context)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kSquareFreeProof, cfg_.session_id, + cfg_.self_id, "peer pi_sqr proof verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kSquareFreeProof, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + + sigma = sigma + msg.sigma_i; + public_data.all_Y_i[peer] = msg.Y_i; + gamma_points.push_back(msg.Gamma_i); + public_data.all_square_free_proofs[peer] = msg.square_free_proof; + } + + if (sigma.value() == 0) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, "aggregated sigma is zero")}; + } + const Scalar sigma_inverse = sigma.InverseModQ(); + public_data.sigma_inverse = sigma_inverse; + + ECPoint plus_one_public; + try { + for (PartyIndex party : cfg_.participants) { + const ECPoint gamma_point = + (party == cfg_.self_id) ? local_Gamma_i_ : peer_round4.at(party).Gamma_i; + public_data.all_plus_one_public_shares[party] = gamma_point.Mul(sigma_inverse); + } + std::vector plus_one_points; + plus_one_points.reserve(cfg_.participants.size()); + for (PartyIndex party : cfg_.participants) { + plus_one_points.push_back(public_data.all_plus_one_public_shares.at(party)); + } + plus_one_public = internal::SumPointsOrThrow(plus_one_points); + public_data.public_key = + plus_one_public.Add(ECPoint::GeneratorMultiply(internal::Sm2Negate( + internal::Sm2One()))); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, + std::string("failed to derive SM2 public key: ") + ex.what())}; + } + + if (public_data.public_key == plus_one_public) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kKeygen, + detection::EvidenceKind::kGammaProof, cfg_.session_id, + cfg_.self_id, "invalid SM2 public key derivation")}; + } + + zid::IdentityBinding binding = + zid::BindIdentity(cfg_.signer_id, public_data.public_key); + + KeygenOutput output{ + .local_key_share = + LocalKeyShare{ + .z_i = local_z_i_, + .paillier = local_paillier_, + .binding = std::move(binding), + }, + .public_keygen_data = std::move(public_data), + }; + return {.value = std::move(output), .abort = std::nullopt}; +} + +} // namespace tecdsa::sm2::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h b/yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h new file mode 100644 index 00000000..6a095703 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h @@ -0,0 +1,166 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/mta/session.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/paper_aux_types.h" +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/messages/mta_messages.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/types.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.h" + +namespace tecdsa::sm2::keygen { + +template +using PeerMap = std::unordered_map; + +using AuxCorrectFormProof = core::paillier::AuxCorrectFormProof; +using AuxRsaParams = core::paillier::AuxRsaParams; + +struct KeygenConfig { + Bytes session_id; + PartyIndex self_id = 0; + std::vector participants; + uint32_t threshold = 1; + uint32_t paillier_modulus_bits = 2048; + uint32_t aux_rsa_modulus_bits = 2048; + Bytes signer_id; +}; + +struct KeygenRound1Msg { + Bytes commitment; + PaillierPublicKey paillier_public; + AuxRsaParams aux_rsa_params; + AuxCorrectFormProof aux_param_proof; +}; + +struct KeygenRound2Broadcast { + ECPoint Z_i; + Bytes randomness; + std::vector commitments; +}; + +struct KeygenRound2Out { + KeygenRound2Broadcast broadcast; + PeerMap shares_for_peers; +}; + +using KeygenRound3Request = tecdsa::sm2::messages::PairwiseProductRequest; +using KeygenRound3Response = tecdsa::sm2::messages::PairwiseProductResponse; + +struct KeygenRound4Msg { + Scalar sigma_i; + ECPoint Y_i; + proofs::PiGroupProof y_proof; + ECPoint Gamma_i; + ECPoint ZGamma_i; + proofs::PiGroupRelationProof gamma_proof; + proofs::PiSqrProof square_free_proof; +}; + +struct LocalKeyShare { + Scalar z_i; + std::shared_ptr paillier; + zid::IdentityBinding binding; +}; + +struct PublicKeygenData { + ECPoint public_key; + Scalar sigma_inverse; + uint32_t threshold = 0; + PeerMap all_Y_i; + PeerMap all_plus_one_public_shares; + PeerMap all_paillier_public; + PeerMap all_aux_rsa_params; + PeerMap all_square_free_proofs; + PeerMap all_aux_param_proofs; +}; + +struct KeygenOutput { + LocalKeyShare local_key_share; + PublicKeygenData public_keygen_data; +}; + +class KeygenParty { + public: + explicit KeygenParty(KeygenConfig cfg); + + KeygenRound1Msg MakeRound1(); + KeygenRound2Out MakeRound2(const PeerMap& peer_round1); + std::vector MakeRound3Requests( + const PeerMap& peer_round2, + const PeerMap& shares_for_self); + detection::DetectionResult> + TryMakeRound3Responses(const std::vector& requests_for_self); + KeygenRound4Msg MakeRound4( + const std::vector& responses_for_self); + detection::DetectionResult TryFinalize( + const PeerMap& peer_round4); + + private: + void EnsureLocalPolynomialPrepared(); + void EnsureLocalPaillierAndAuxPrepared(); + void EnsureLocalProofsPrepared(); + + KeygenConfig cfg_; + std::vector peers_; + + std::vector local_poly_coefficients_; + PeerMap local_shares_; + std::shared_ptr local_paillier_; + PaillierPublicKey local_paillier_public_; + AuxRsaParams local_aux_rsa_params_; + core::paillier::PaperAuxSetupWitness local_aux_rsa_witness_; + proofs::PiSqrProof local_square_free_proof_; + AuxCorrectFormProof local_aux_param_proof_; + + ECPoint local_Z_i_; + Bytes local_commitment_; + Bytes local_open_randomness_; + std::vector local_vss_commitments_; + ECPoint local_Y_i_; + ECPoint global_Z_; + ECPoint local_ZGamma_i_; + + PeerMap all_phase1_commitments_; + PeerMap all_paillier_public_; + PeerMap all_aux_rsa_params_; + PeerMap all_aux_param_proofs_; + + Scalar local_secret_z_i_; + Scalar local_z_i_; + Scalar local_gamma_i_; + ECPoint local_Gamma_i_; + tecdsa::core::mta::PairwiseProductSession sigma_session_; + Scalar sigma_initiator_sum_; + Scalar sigma_responder_sum_; + Scalar local_sigma_i_; + + bool round2_done_ = false; + bool round3_requests_done_ = false; + bool round3_responses_done_ = false; + bool round4_done_ = false; +}; + +} // namespace tecdsa::sm2::keygen diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/messages/mta_messages.h b/yacl/crypto/experimental/threshold_signatures/sm2/messages/mta_messages.h new file mode 100644 index 00000000..0d313636 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/messages/mta_messages.h @@ -0,0 +1,25 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/core/mta/messages.h" + +namespace tecdsa::sm2::messages { + +using MtaType = core::mta::MtaType; +using PairwiseProductRequest = core::mta::PairwiseProductRequest; +using PairwiseProductResponse = core::mta::PairwiseProductResponse; + +} // namespace tecdsa::sm2::messages diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.cc b/yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.cc new file mode 100644 index 00000000..2fa481f2 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.cc @@ -0,0 +1,385 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.h" + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/commitment/commitment.h" +#include "yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h" + +namespace tecdsa::sm2::presign { +namespace { + +namespace mta = tecdsa::core::mta; + +constexpr char kPhase1CommitDomain[] = "SM2/offline/phase1"; + +} // namespace + +OfflineParty::OfflineParty(OfflineConfig cfg) + : cfg_(std::move(cfg)), + delta_session_({.session_id = cfg_.session_id, + .self_id = cfg_.self_id, + .suite = core::DefaultSm2Suite(), + .group = internal::Sm2Group()}) { + const auto participant_set = core::participant::BuildParticipantSet( + cfg_.participants, cfg_.self_id, "sm2::presign::OfflineParty"); + peers_ = participant_set.peers; + if (cfg_.participants.size() != + static_cast(cfg_.public_keygen_data.threshold) + 1) { + TECDSA_THROW_ARGUMENT("offline signer set size must equal threshold + 1"); + } + if (cfg_.local_key_share.z_i.value() == 0) { + TECDSA_THROW_ARGUMENT("local z share must be non-zero"); + } + if (cfg_.local_key_share.paillier == nullptr) { + TECDSA_THROW_ARGUMENT("local Paillier provider must be present"); + } + const auto lagrange = internal::ComputeLagrangeAtZero(cfg_.participants); + const auto lambda_it = lagrange.find(cfg_.self_id); + if (lambda_it == lagrange.end()) { + TECDSA_THROW_ARGUMENT("missing Lagrange coefficient for signer"); + } + for (PartyIndex party : cfg_.participants) { + if (!cfg_.public_keygen_data.all_Y_i.contains(party)) { + TECDSA_THROW_ARGUMENT("public SM2 Y_i shares must be present"); + } + } + local_w_i_ = lambda_it->second * cfg_.local_key_share.z_i; + if (local_w_i_.value() == 0) { + TECDSA_THROW_ARGUMENT("weighted SM2 signing share must be non-zero"); + } + std::vector w_points; + w_points.reserve(cfg_.participants.size()); + for (PartyIndex party : cfg_.participants) { + const auto party_lambda_it = lagrange.find(party); + if (party_lambda_it == lagrange.end()) { + TECDSA_THROW_ARGUMENT("missing Lagrange coefficient for signer subset"); + } + w_points_[party] = + cfg_.public_keygen_data.all_Y_i.at(party).Mul(party_lambda_it->second); + w_points.push_back(w_points_.at(party)); + } + local_W_i_ = w_points_.at(cfg_.self_id); + if (ECPoint::GeneratorMultiply(local_w_i_) != local_W_i_) { + TECDSA_THROW_ARGUMENT("local SM2 W_i does not match weighted signing share"); + } + W_ = internal::SumPointsOrThrow(w_points); + local_k_i_ = internal::Sm2Zero(); + delta_initiator_sum_ = internal::Sm2Zero(); + delta_responder_sum_ = internal::Sm2Zero(); + local_delta_i_ = internal::Sm2Zero(); +} + +void OfflineParty::EnsureRound1Prepared() { + if (round1_done_) { + return; + } + + local_k_i_ = internal::RandomNonZeroSm2Scalar(); + local_K_i_ = ECPoint::GeneratorMultiply(local_k_i_); + const auto commit = core::commitment::CommitMessage( + core::DefaultSm2Suite(), kPhase1CommitDomain, + local_K_i_.ToCompressedBytes()); + local_randomness_ = commit.randomness; + phase1_commitments_[cfg_.self_id] = commit.commitment; + round1_done_ = true; +} + +Round1Msg OfflineParty::MakeRound1() { + EnsureRound1Prepared(); + return Round1Msg{.commitment = phase1_commitments_.at(cfg_.self_id)}; +} + +std::vector OfflineParty::MakeRound2Requests( + const PeerMap& peer_round1) { + if (round2_requests_done_) { + TECDSA_THROW_LOGIC("MakeRound2Requests must not be called twice"); + } + + EnsureRound1Prepared(); + core::participant::RequireExactlyPeers(peer_round1, cfg_.participants, + cfg_.self_id, "peer_round1"); + std::vector out; + out.reserve(peers_.size()); + for (PartyIndex peer : peers_) { + phase1_commitments_[peer] = peer_round1.at(peer).commitment; + out.push_back(delta_session_.InitiatorInitWithCheck({ + .responder_id = peer, + .initiator_paillier = cfg_.local_key_share.paillier.get(), + .responder_aux = &cfg_.public_keygen_data.all_aux_rsa_params.at(peer), + .initiator_secret = local_k_i_, + })); + } + round2_requests_done_ = true; + return out; +} + +detection::DetectionResult> +OfflineParty::TryMakeRound2Responses( + const std::vector& requests_for_self) { + if (round2_responses_done_) { + TECDSA_THROW_LOGIC("MakeRound2Responses must not be called twice"); + } + if (!round2_requests_done_) { + TECDSA_THROW_LOGIC( + "MakeRound2Requests must be completed before MakeRound2Responses"); + } + + try { + mta::RequireExactlyOneRequestPerPeer( + requests_for_self, peers_, cfg_.self_id, mta::MtaType::kMtAwc, + std::identity{}, "SM2 offline request"); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kMtaProof, cfg_.session_id, + cfg_.self_id, ex.what())}; + } + + std::vector out; + out.reserve(requests_for_self.size()); + Scalar responder_sum = delta_responder_sum_; + for (const auto& request : requests_for_self) { + try { + const auto consume = delta_session_.ResponderMidWithCheck( + request, + {.initiator_modulus_n = + cfg_.public_keygen_data.all_paillier_public.at(request.from).n, + .responder_aux = + &cfg_.public_keygen_data.all_aux_rsa_params.at(cfg_.self_id), + .initiator_aux = + &cfg_.public_keygen_data.all_aux_rsa_params.at(request.from), + .responder_secret = local_w_i_, + .public_witness_point = local_W_i_}); + responder_sum = responder_sum + consume.responder_share; + out.push_back(consume.response); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kMtaProof, cfg_.session_id, + cfg_.self_id, ex.what(), request.from, request.instance_id)}; + } + } + delta_responder_sum_ = responder_sum; + round2_responses_done_ = true; + return {.value = std::move(out), .abort = std::nullopt}; +} + +Round3Msg OfflineParty::MakeRound3( + const std::vector& responses_for_self) { + if (round3_done_) { + TECDSA_THROW_LOGIC("MakeRound3 must not be called twice"); + } + if (!round2_responses_done_) { + TECDSA_THROW_LOGIC("MakeRound2Responses must be completed before MakeRound3"); + } + + mta::RequireExactlyOneResponsePerInitiatorInstance( + responses_for_self, peers_, peers_.size(), cfg_.self_id, delta_session_, + std::identity{}, "SM2 offline response"); + for (const auto& response : responses_for_self) { + const auto consume = delta_session_.InitiatorEndWithCheck( + response, + {.initiator_paillier = cfg_.local_key_share.paillier.get(), + .initiator_aux = &cfg_.public_keygen_data.all_aux_rsa_params.at(cfg_.self_id), + .public_witness_point = w_points_.at(response.from)}); + delta_initiator_sum_ = delta_initiator_sum_ + consume.initiator_share; + } + + local_delta_i_ = (local_k_i_ * local_w_i_) + delta_initiator_sum_ + + delta_responder_sum_; + local_T_i_ = ECPoint::GeneratorMultiply(local_delta_i_); + local_WK_i_ = W_.Mul(local_k_i_); + round3_done_ = true; + return Round3Msg{ + .K_i = local_K_i_, + .randomness = local_randomness_, + .k_proof = + proofs::BuildPiGroupProof(cfg_.session_id, cfg_.self_id, local_K_i_, + local_k_i_), + .T_i = local_T_i_, + .t_proof = + proofs::BuildPiGroupProof(cfg_.session_id, cfg_.self_id, local_T_i_, + local_delta_i_), + .WK_i = local_WK_i_, + .wk_proof = proofs::BuildPiGroupRelationProof( + cfg_.session_id, cfg_.self_id, W_, local_K_i_, local_WK_i_, + local_k_i_), + }; +} + +detection::DetectionResult OfflineParty::TryFinalize( + const PeerMap& peer_round3) { + if (!round3_done_) { + TECDSA_THROW_LOGIC("MakeRound3 must be completed before Finalize"); + } + + try { + core::participant::RequireExactlyPeers(peer_round3, cfg_.participants, + cfg_.self_id, "peer_round3"); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, ex.what())}; + } + std::vector k_points; + k_points.reserve(cfg_.participants.size()); + k_points.push_back(local_K_i_); + PeerMap all_t_points; + all_t_points[cfg_.self_id] = local_T_i_; + PeerMap all_wk_points; + all_wk_points[cfg_.self_id] = local_WK_i_; + std::vector t_points; + t_points.reserve(cfg_.participants.size()); + t_points.push_back(local_T_i_); + std::vector wk_points; + wk_points.reserve(cfg_.participants.size()); + wk_points.push_back(local_WK_i_); + for (PartyIndex peer : peers_) { + const auto& msg = peer_round3.at(peer); + const auto commitment_it = phase1_commitments_.find(peer); + if (commitment_it == phase1_commitments_.end()) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kCommitment, cfg_.session_id, + cfg_.self_id, "missing phase1 commitment for peer")}; + } + try { + if (!core::commitment::VerifyCommitment( + core::DefaultSm2Suite(), kPhase1CommitDomain, + msg.K_i.ToCompressedBytes(), msg.randomness, + commitment_it->second)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kCommitment, cfg_.session_id, + cfg_.self_id, "SM2 offline nonce opening verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kCommitment, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + try { + if (!proofs::VerifyPiGroupProof(cfg_.session_id, peer, msg.K_i, + msg.k_proof)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, "SM2 offline nonce pi_group verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + try { + if (!proofs::VerifyPiGroupProof(cfg_.session_id, peer, msg.T_i, + msg.t_proof)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, "SM2 offline delta proof verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + try { + if (!proofs::VerifyPiGroupRelationProof(cfg_.session_id, peer, W_, + msg.K_i, msg.WK_i, + msg.wk_proof)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, "SM2 offline W^k relation proof verification failed", peer)}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, ex.what(), peer)}; + } + k_points.push_back(msg.K_i); + all_t_points[peer] = msg.T_i; + all_wk_points[peer] = msg.WK_i; + t_points.push_back(msg.T_i); + wk_points.push_back(msg.WK_i); + } + + try { + if (internal::SumPointsOrThrow(t_points) != internal::SumPointsOrThrow(wk_points)) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, "SM2 offline product relation check failed")}; + } + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, + std::string("failed to aggregate SM2 offline relation points: ") + + ex.what())}; + } + + OfflineState offline; + try { + offline = OfflineState{ + .delta_i = local_delta_i_, + .R = internal::SumPointsOrThrow(k_points), + .W = W_, + .all_W_i = w_points_, + .all_T_i = std::move(all_t_points), + .all_WK_i = std::move(all_wk_points), + }; + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOffline, + detection::EvidenceKind::kNonceProof, cfg_.session_id, + cfg_.self_id, + std::string("failed to finalize SM2 offline presign: ") + + ex.what())}; + } + return {.value = std::move(offline), .abort = std::nullopt}; +} + +} // namespace tecdsa::sm2::presign diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.h b/yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.h new file mode 100644 index 00000000..768e9e7e --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.h @@ -0,0 +1,104 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/core/mta/session.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/messages/mta_messages.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h" + +namespace tecdsa::sm2::presign { + +template +using PeerMap = std::unordered_map; + +struct OfflineConfig { + Bytes session_id; + PartyIndex self_id = 0; + std::vector participants; + keygen::LocalKeyShare local_key_share; + keygen::PublicKeygenData public_keygen_data; +}; + +struct Round1Msg { + Bytes commitment; +}; + +using Round2Request = tecdsa::sm2::messages::PairwiseProductRequest; +using Round2Response = tecdsa::sm2::messages::PairwiseProductResponse; + +struct Round3Msg { + ECPoint K_i; + Bytes randomness; + proofs::PiGroupProof k_proof; + ECPoint T_i; + proofs::PiGroupProof t_proof; + ECPoint WK_i; + proofs::PiGroupRelationProof wk_proof; +}; + +struct OfflineState { + Scalar delta_i; + ECPoint R; + ECPoint W; + PeerMap all_W_i; + PeerMap all_T_i; + PeerMap all_WK_i; +}; + +class OfflineParty { + public: + explicit OfflineParty(OfflineConfig cfg); + + Round1Msg MakeRound1(); + std::vector MakeRound2Requests( + const PeerMap& peer_round1); + detection::DetectionResult> + TryMakeRound2Responses(const std::vector& requests_for_self); + Round3Msg MakeRound3(const std::vector& responses_for_self); + detection::DetectionResult TryFinalize( + const PeerMap& peer_round3); + + private: + void EnsureRound1Prepared(); + + OfflineConfig cfg_; + std::vector peers_; + Scalar local_k_i_; + Scalar local_w_i_; + ECPoint local_K_i_; + ECPoint local_W_i_; + PeerMap w_points_; + ECPoint W_; + Bytes local_randomness_; + PeerMap phase1_commitments_; + tecdsa::core::mta::PairwiseProductSession delta_session_; + Scalar delta_initiator_sum_; + Scalar delta_responder_sum_; + Scalar local_delta_i_; + ECPoint local_T_i_; + ECPoint local_WK_i_; + bool round1_done_ = false; + bool round2_requests_done_ = false; + bool round2_responses_done_ = false; + bool round3_done_ = false; +}; + +} // namespace tecdsa::sm2::presign diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.cc b/yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.cc new file mode 100644 index 00000000..836e1ed6 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.cc @@ -0,0 +1,109 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h" + +#include "yacl/crypto/experimental/threshold_signatures/core/encoding/encoding.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/schnorr.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/crypto/experimental/threshold_signatures/core/transcript/transcript.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" + +namespace tecdsa::sm2::proofs { +namespace { + +constexpr char kPiGroupRelationProofId[] = "SM2/PiGroupRelation/v1"; + +Scalar BuildPiGroupRelationChallenge(const Bytes& session_id, + PartyIndex prover_id, const ECPoint& base_h, + const ECPoint& statement_g, + const ECPoint& statement_h, + const PiGroupRelationProof& proof) { + core::transcript::Transcript transcript(core::DefaultSm2Suite().transcript_hash); + transcript.append_proof_id(kPiGroupRelationProofId); + transcript.append_session_id(session_id); + transcript.append_u32_be("prover", prover_id); + const Bytes base_h_bytes = core::encoding::EncodePoint(base_h); + const Bytes statement_g_bytes = core::encoding::EncodePoint(statement_g); + const Bytes statement_h_bytes = core::encoding::EncodePoint(statement_h); + const Bytes a_g_bytes = core::encoding::EncodePoint(proof.a_g); + const Bytes a_h_bytes = core::encoding::EncodePoint(proof.a_h); + transcript.append_fields({ + core::transcript::TranscriptFieldRef{ + .label = "base_h", .data = base_h_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "statement_g", .data = statement_g_bytes}, + core::transcript::TranscriptFieldRef{ + .label = "statement_h", .data = statement_h_bytes}, + core::transcript::TranscriptFieldRef{.label = "a_g", .data = a_g_bytes}, + core::transcript::TranscriptFieldRef{.label = "a_h", .data = a_h_bytes}, + }); + return transcript.challenge_scalar(tecdsa::sm2::internal::Sm2Group()); +} + +} // namespace + +PiGroupProof BuildPiGroupProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& statement, + const Scalar& witness) { + return core::proof::BuildSchnorrProof(core::DefaultSm2Suite(), session_id, + prover_id, statement, witness); +} + +bool VerifyPiGroupProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& statement, const PiGroupProof& proof) { + return core::proof::VerifySchnorrProof(core::DefaultSm2Suite(), session_id, + prover_id, statement, proof); +} + +PiGroupRelationProof BuildPiGroupRelationProof(const Bytes& session_id, + PartyIndex prover_id, + const ECPoint& base_h, + const ECPoint& statement_g, + const ECPoint& statement_h, + const Scalar& witness) { + const Scalar alpha = tecdsa::sm2::internal::RandomNonZeroSm2Scalar(); + PiGroupRelationProof proof{ + .a_g = ECPoint::GeneratorMultiply(alpha), + .a_h = base_h.Mul(alpha), + .z = alpha, + }; + const Scalar challenge = BuildPiGroupRelationChallenge( + session_id, prover_id, base_h, statement_g, statement_h, proof); + proof.z = alpha + (challenge * witness); + return proof; +} + +bool VerifyPiGroupRelationProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& base_h, + const ECPoint& statement_g, + const ECPoint& statement_h, + const PiGroupRelationProof& proof) { + try { + const Scalar challenge = BuildPiGroupRelationChallenge( + session_id, prover_id, base_h, statement_g, statement_h, proof); + const ECPoint lhs_g = ECPoint::GeneratorMultiply(proof.z); + const ECPoint rhs_g = proof.a_g.Add(statement_g.Mul(challenge)); + if (lhs_g != rhs_g) { + return false; + } + const ECPoint lhs_h = base_h.Mul(proof.z); + const ECPoint rhs_h = proof.a_h.Add(statement_h.Mul(challenge)); + return lhs_h == rhs_h; + } catch (const std::exception&) { + return false; + } +} + +} // namespace tecdsa::sm2::proofs diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h b/yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h new file mode 100644 index 00000000..ef09cd06 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/proofs/pi_group.h @@ -0,0 +1,40 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/proofs/types.h" + +namespace tecdsa::sm2::proofs { + +PiGroupProof BuildPiGroupProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& statement, + const Scalar& witness); +bool VerifyPiGroupProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& statement, const PiGroupProof& proof); + +PiGroupRelationProof BuildPiGroupRelationProof(const Bytes& session_id, + PartyIndex prover_id, + const ECPoint& base_h, + const ECPoint& statement_g, + const ECPoint& statement_h, + const Scalar& witness); +bool VerifyPiGroupRelationProof(const Bytes& session_id, PartyIndex prover_id, + const ECPoint& base_h, + const ECPoint& statement_g, + const ECPoint& statement_h, + const PiGroupRelationProof& proof); + +} // namespace tecdsa::sm2::proofs diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/proofs/types.h b/yacl/crypto/experimental/threshold_signatures/sm2/proofs/types.h new file mode 100644 index 00000000..f4e1f37c --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/proofs/types.h @@ -0,0 +1,37 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/core/mta/proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/paillier/aux_proofs.h" +#include "yacl/crypto/experimental/threshold_signatures/core/proof/types.h" + +namespace tecdsa::sm2::proofs { + +using BigInt = core::paillier::BigInt; +using PiRangeProof = core::mta::A1RangeProof; +using PiLinearGroupProof = core::mta::A2MtAwcProof; +using PiLinearProof = core::mta::A3MtAProof; +using PiGroupProof = core::proof::SchnorrProof; +using PiSqrProof = core::paillier::SquareFreeProof; + +struct PiGroupRelationProof { + ECPoint a_g; + ECPoint a_h; + Scalar z; +}; + +} // namespace tecdsa::sm2::proofs diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/sign/online.cc b/yacl/crypto/experimental/threshold_signatures/sm2/sign/online.cc new file mode 100644 index 00000000..590ff3e6 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/sign/online.cc @@ -0,0 +1,148 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/sm2/sign/online.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/participant/participant_set.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h" + +namespace tecdsa::sm2::sign { + +OnlineParty::OnlineParty(OnlineConfig cfg) : cfg_(std::move(cfg)) { + const auto participant_set = core::participant::BuildParticipantSet( + cfg_.participants, cfg_.self_id, "sm2::sign::OnlineParty"); + peers_ = participant_set.peers; + if (cfg_.participants.size() != + static_cast(cfg_.public_keygen_data.threshold) + 1) { + TECDSA_THROW_ARGUMENT("online signer set size must equal threshold + 1"); + } + if (cfg_.local_key_share.z_i.value() == 0) { + TECDSA_THROW_ARGUMENT("local z share must be non-zero"); + } + const auto lagrange = internal::ComputeLagrangeAtZero(cfg_.participants); + const auto lambda_it = lagrange.find(cfg_.self_id); + if (lambda_it == lagrange.end()) { + TECDSA_THROW_ARGUMENT("missing Lagrange coefficient for signer"); + } + local_w_i_ = lambda_it->second * cfg_.local_key_share.z_i; + if (local_w_i_.value() == 0) { + TECDSA_THROW_ARGUMENT("weighted SM2 signing share must be non-zero"); + } + for (PartyIndex party : cfg_.participants) { + if (!cfg_.offline.all_W_i.contains(party) || !cfg_.offline.all_T_i.contains(party)) { + TECDSA_THROW_ARGUMENT("offline state is missing W_i/T_i data for signers"); + } + } + if (ECPoint::GeneratorMultiply(local_w_i_) != cfg_.offline.all_W_i.at(cfg_.self_id)) { + TECDSA_THROW_ARGUMENT("offline W_i does not match local SM2 signing share"); + } + + const Bytes digest = zid::PreprocessMessageDigest(cfg_.local_key_share.binding, + cfg_.message); + message_hash_ = Scalar::FromBigEndianModQ(digest, internal::Sm2Group()); + r_ = message_hash_ + internal::XCoordinateModN(cfg_.offline.R); + if (r_.value() == 0) { + TECDSA_THROW_ARGUMENT("computed SM2 signature r is zero"); + } +} + +Scalar OnlineParty::MakePartialSignature() { + if (partial_done_) { + TECDSA_THROW_LOGIC("MakePartialSignature must not be called twice"); + } + + partial_s_prime_ = (r_ * local_w_i_) + cfg_.offline.delta_i; + partial_done_ = true; + return partial_s_prime_; +} + +detection::DetectionResult OnlineParty::TryFinalize( + const PeerMap& peer_partials) { + try { + core::participant::RequireExactlyPeers(peer_partials, cfg_.participants, + cfg_.self_id, "peer_partials"); + } catch (const std::exception& ex) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOnline, + detection::EvidenceKind::kPartialSignature, cfg_.session_id, + cfg_.self_id, ex.what())}; + } + + if (!partial_done_) { + MakePartialSignature(); + } + Scalar s_prime = partial_s_prime_; + for (PartyIndex peer : peers_) { + s_prime = s_prime + peer_partials.at(peer); + } + const Scalar s = s_prime - r_; + if (s.value() == 0) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOnline, + detection::EvidenceKind::kPartialSignature, cfg_.session_id, + cfg_.self_id, "aggregated SM2 signature scalar s is zero")}; + } + + verify::Signature signature{ + .r = r_, + .s = s, + .R = cfg_.offline.R, + }; + if (!verify::VerifySm2SignatureMath(cfg_.public_keygen_data.public_key, + cfg_.local_key_share.binding, + cfg_.message, signature)) { + std::optional culprit; + size_t mismatch_count = 0; + auto record_mismatch = [&](PartyIndex party, const Scalar& partial) { + const ECPoint lhs = ECPoint::GeneratorMultiply(partial); + const ECPoint rhs = + cfg_.offline.all_T_i.at(party).Add(cfg_.offline.all_W_i.at(party).Mul(r_)); + if (lhs != rhs) { + culprit = party; + ++mismatch_count; + } + }; + try { + record_mismatch(cfg_.self_id, partial_s_prime_); + for (PartyIndex peer : peers_) { + record_mismatch(peer, peer_partials.at(peer)); + } + } catch (const std::exception&) { + mismatch_count = 0; + culprit = std::nullopt; + } + if (mismatch_count == 1 && culprit.has_value()) { + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOnline, + detection::EvidenceKind::kPartialSignature, cfg_.session_id, + cfg_.self_id, "SM2 partial signature relation check failed", *culprit)}; + } + return {.value = std::nullopt, + .abort = detection::MakeAbort( + detection::AbortStage::kOnline, + detection::EvidenceKind::kPartialSignature, cfg_.session_id, + cfg_.self_id, "final SM2 signature verification failed")}; + } + + return {.value = std::move(signature), .abort = std::nullopt}; +} + +} // namespace tecdsa::sm2::sign diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/sign/online.h b/yacl/crypto/experimental/threshold_signatures/sm2/sign/online.h new file mode 100644 index 00000000..11af40dc --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/sign/online.h @@ -0,0 +1,60 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/types.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.h" + +namespace tecdsa::sm2::sign { + +template +using PeerMap = std::unordered_map; + +struct OnlineConfig { + Bytes session_id; + PartyIndex self_id = 0; + std::vector participants; + keygen::LocalKeyShare local_key_share; + keygen::PublicKeygenData public_keygen_data; + presign::OfflineState offline; + Bytes message; +}; + +class OnlineParty { + public: + explicit OnlineParty(OnlineConfig cfg); + + Scalar MakePartialSignature(); + detection::DetectionResult TryFinalize( + const PeerMap& peer_partials); + + private: + OnlineConfig cfg_; + std::vector peers_; + Scalar local_w_i_; + Scalar message_hash_; + Scalar r_; + Scalar partial_s_prime_; + bool partial_done_ = false; +}; + +} // namespace tecdsa::sm2::sign diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.cc b/yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.cc new file mode 100644 index 00000000..ae7c86bc --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.cc @@ -0,0 +1,48 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" + +namespace tecdsa::sm2::verify { + +bool VerifySm2SignatureMath(const ECPoint& public_key, + const zid::IdentityBinding& binding, + std::span message, + const Signature& signature) { + if (signature.r.value() == 0 || signature.s.value() == 0) { + return false; + } + + try { + const Bytes digest = zid::PreprocessMessageDigest(binding, message); + const Scalar e = Scalar::FromBigEndianModQ(digest, internal::Sm2Group()); + const Scalar t = signature.r + signature.s; + if (t.value() == 0) { + return false; + } + + ECPoint reconstructed = ECPoint::GeneratorMultiply(signature.s); + reconstructed = reconstructed.Add(public_key.Mul(t)); + const Scalar expected_r = e + internal::XCoordinateModN(reconstructed); + return expected_r == signature.r; + } catch (const std::exception&) { + return false; + } +} + +} // namespace tecdsa::sm2::verify diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.h b/yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.h new file mode 100644 index 00000000..f336bd24 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.h @@ -0,0 +1,35 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.h" + +namespace tecdsa::sm2::verify { + +struct Signature { + Scalar r; + Scalar s; + ECPoint R; +}; + +bool VerifySm2SignatureMath(const ECPoint& public_key, + const zid::IdentityBinding& binding, + std::span message, + const Signature& signature); + +} // namespace tecdsa::sm2::verify diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.cc b/yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.cc new file mode 100644 index 00000000..7f4320b7 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.cc @@ -0,0 +1,97 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.h" + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/errors.h" +#include "yacl/crypto/experimental/threshold_signatures/core/suite/suite.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" + +namespace tecdsa::sm2::zid { +namespace { + +constexpr std::array kSm2A = { + 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, +}; + +constexpr std::array kSm2B = { + 0x28, 0xE9, 0xFA, 0x9E, 0x9D, 0x9F, 0x5E, 0x34, 0x4D, 0x5A, 0x9E, + 0x4B, 0xCF, 0x65, 0x09, 0xA7, 0xF3, 0x97, 0x89, 0xF5, 0x15, 0xAB, + 0x8F, 0x92, 0xDD, 0xBC, 0xBD, 0x41, 0x4D, 0x94, 0x0E, 0x93, +}; + +constexpr std::array kSm2Gx = { + 0x32, 0xC4, 0xAE, 0x2C, 0x1F, 0x19, 0x81, 0x19, 0x5F, 0x99, 0x04, + 0x46, 0x6A, 0x39, 0xC9, 0x94, 0x8F, 0xE3, 0x0B, 0xBF, 0xF2, 0x66, + 0x0B, 0xE1, 0x71, 0x5A, 0x45, 0x89, 0x33, 0x4C, 0x74, 0xC7, +}; + +constexpr std::array kSm2Gy = { + 0xBC, 0x37, 0x36, 0xA2, 0xF4, 0xF6, 0x77, 0x9C, 0x59, 0xBD, 0xCE, + 0xE3, 0x6B, 0x69, 0x21, 0x53, 0xD0, 0xA9, 0x87, 0x7C, 0xC6, 0x2A, + 0x47, 0x40, 0x02, 0xDF, 0x32, 0xE5, 0x21, 0x39, 0xF0, 0xA0, +}; + +void AppendU16Be(uint16_t value, Bytes* out) { + out->push_back(static_cast((value >> 8) & 0xFF)); + out->push_back(static_cast(value & 0xFF)); +} + +} // namespace + +Bytes ComputeZid(std::span signer_id, const ECPoint& public_key) { + if (signer_id.size() > (UINT16_MAX / 8)) { + TECDSA_THROW_ARGUMENT("SM2 signer id is too long"); + } + + const Bytes public_key_uncompressed = internal::SerializeUncompressed(public_key); + if (public_key_uncompressed.size() != 65 || public_key_uncompressed[0] != 0x04) { + TECDSA_THROW_ARGUMENT("SM2 public key must serialize to uncompressed form"); + } + + Bytes preimage; + preimage.reserve(2 + signer_id.size() + 32 * 6); + AppendU16Be(static_cast(signer_id.size() * 8), &preimage); + preimage.insert(preimage.end(), signer_id.begin(), signer_id.end()); + preimage.insert(preimage.end(), kSm2A.begin(), kSm2A.end()); + preimage.insert(preimage.end(), kSm2B.begin(), kSm2B.end()); + preimage.insert(preimage.end(), kSm2Gx.begin(), kSm2Gx.end()); + preimage.insert(preimage.end(), kSm2Gy.begin(), kSm2Gy.end()); + preimage.insert(preimage.end(), public_key_uncompressed.begin() + 1, + public_key_uncompressed.begin() + 33); + preimage.insert(preimage.end(), public_key_uncompressed.begin() + 33, + public_key_uncompressed.end()); + return core::Hash(core::HashId::kSm3, preimage); +} + +IdentityBinding BindIdentity(std::span signer_id, + const ECPoint& public_key) { + IdentityBinding binding; + binding.signer_id.assign(signer_id.begin(), signer_id.end()); + binding.zid = ComputeZid(signer_id, public_key); + return binding; +} + +Bytes PreprocessMessageDigest(const IdentityBinding& binding, + std::span message) { + Bytes preimage = binding.zid; + preimage.insert(preimage.end(), message.begin(), message.end()); + return core::Hash(core::HashId::kSm3, preimage); +} + +} // namespace tecdsa::sm2::zid diff --git a/yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.h b/yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.h new file mode 100644 index 00000000..6319851b --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/sm2/zid/zid.h @@ -0,0 +1,34 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/types.h" + +namespace tecdsa::sm2::zid { + +struct IdentityBinding { + Bytes signer_id; + Bytes zid; +}; + +Bytes ComputeZid(std::span signer_id, const ECPoint& public_key); +IdentityBinding BindIdentity(std::span signer_id, + const ECPoint& public_key); +Bytes PreprocessMessageDigest(const IdentityBinding& binding, + std::span message); + +} // namespace tecdsa::sm2::zid diff --git a/yacl/crypto/experimental/threshold_signatures/tests/ecdsa_flow_test.cc b/yacl/crypto/experimental/threshold_signatures/tests/ecdsa_flow_test.cc new file mode 100644 index 00000000..86caf4c2 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/tests/ecdsa_flow_test.cc @@ -0,0 +1,442 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/keygen/keygen.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/sign/sign.h" +#include "yacl/crypto/experimental/threshold_signatures/ecdsa/verify/verify.h" +#include "yacl/crypto/experimental/threshold_signatures/tests/test_helpers.h" + +namespace tecdsa::ecdsa_flow_test { +namespace { + +using ::tecdsa::Bytes; +using ::tecdsa::PartyIndex; +using ::tecdsa::Scalar; +using ::tecdsa::ecdsa::keygen::KeygenOutput; +using ::tecdsa::ecdsa::keygen::KeygenParty; +using ::tecdsa::ecdsa::keygen::KeygenRound1Msg; +using ::tecdsa::ecdsa::keygen::KeygenRound2Broadcast; +using ::tecdsa::ecdsa::keygen::KeygenRound3Msg; +using ::tecdsa::ecdsa::sign::Signature; +using ::tecdsa::ecdsa::sign::SignConfig; +using ::tecdsa::ecdsa::sign::SignParty; +using ::tecdsa::ecdsa::sign::SignRound1Msg; +using ::tecdsa::ecdsa::sign::SignRound2Request; +using ::tecdsa::ecdsa::sign::SignRound2Response; +using ::tecdsa::ecdsa::sign::SignRound3Msg; +using ::tecdsa::ecdsa::sign::SignRound4Msg; +using ::tecdsa::ecdsa::sign::SignRound5AMsg; +using ::tecdsa::ecdsa::sign::SignRound5BMsg; +using ::tecdsa::ecdsa::sign::SignRound5CMsg; +using ::tecdsa::ecdsa::sign::SignRound5DMsg; +using ::tecdsa::test_helpers::BuildPeerMapFor; +using ::tecdsa::test_helpers::Expect; +using ::tecdsa::test_helpers::ExpectThrow; + +constexpr uint32_t kTestAuxRsaBits = 192; + +template +using PeerMap = std::unordered_map; + +using KeygenOutputs = std::unordered_map; +using KeygenPartyMap = std::unordered_map; +using KeygenRound2Shares = std::unordered_map>; +using SignPartyMap = std::unordered_map; + +struct SignFixture { + std::vector signers; + Bytes msg32; +}; + +struct SignRoundState { + PeerMap round1; + std::vector round2_requests; + std::vector round2_responses; + PeerMap round3; + PeerMap round4; + PeerMap round5a; + PeerMap round5b; + PeerMap round5c; + PeerMap round5d; + PeerMap round5e; +}; + +std::vector BuildParticipants(uint32_t n) { + std::vector out; + out.reserve(n); + for (PartyIndex id = 1; id <= n; ++id) { + out.push_back(id); + } + return out; +} + +std::unordered_map> +GroupRound2RequestsByRecipient(const std::vector& requests) { + std::unordered_map> grouped; + for (const SignRound2Request& request : requests) { + grouped[request.to].push_back(request); + } + return grouped; +} + +std::unordered_map> +GroupRound2ResponsesByRecipient( + const std::vector& responses) { + std::unordered_map> grouped; + for (const SignRound2Response& response : responses) { + grouped[response.to].push_back(response); + } + return grouped; +} + +KeygenOutputs RunKeygenAndCollectResults(uint32_t n, uint32_t t, + const Bytes& session_id) { + const std::vector participants = BuildParticipants(n); + KeygenPartyMap parties; + for (PartyIndex party : participants) { + ecdsa::keygen::KeygenConfig cfg; + cfg.session_id = session_id; + cfg.self_id = party; + cfg.participants = participants; + cfg.threshold = t; + cfg.aux_rsa_modulus_bits = kTestAuxRsaBits; + parties.emplace(party, KeygenParty(std::move(cfg))); + } + + PeerMap round1; + for (PartyIndex party : participants) { + round1.emplace(party, parties.at(party).MakeRound1()); + } + + PeerMap round2_broadcasts; + KeygenRound2Shares round2_shares; + for (PartyIndex party : participants) { + const auto peer_round1 = BuildPeerMapFor(participants, party, round1); + const auto round2 = parties.at(party).MakeRound2(peer_round1); + round2_broadcasts.emplace(party, round2.broadcast); + round2_shares.emplace(party, round2.shares_for_peers); + } + + PeerMap round3; + for (PartyIndex party : participants) { + const auto peer_round2 = + BuildPeerMapFor(participants, party, round2_broadcasts); + PeerMap shares_for_self; + for (PartyIndex peer : participants) { + if (peer != party) { + shares_for_self.emplace(peer, round2_shares.at(peer).at(party)); + } + } + round3.emplace(party, + parties.at(party).MakeRound3(peer_round2, shares_for_self)); + } + + KeygenOutputs outputs; + for (PartyIndex party : participants) { + outputs.emplace( + party, + parties.at(party).Finalize(BuildPeerMapFor(participants, party, round3))); + } + return outputs; +} + +SignFixture BuildSignFixture(const std::vector& signers) { + return SignFixture{.signers = signers, + .msg32 = Bytes(32, static_cast(0x5A))}; +} + +SignPartyMap BuildSignParties(const SignFixture& fixture, + const KeygenOutputs& keygen_results, + const Bytes& sign_session_id, + const Bytes& keygen_session_id) { + SignPartyMap parties; + for (PartyIndex signer : fixture.signers) { + const auto result_it = keygen_results.find(signer); + if (result_it == keygen_results.end()) { + throw std::runtime_error("missing keygen result for signer"); + } + SignConfig cfg; + cfg.session_id = sign_session_id; + cfg.keygen_session_id = keygen_session_id; + cfg.self_id = signer; + cfg.participants = fixture.signers; + cfg.local_key_share = result_it->second.local_key_share; + cfg.public_keygen_data = result_it->second.public_keygen_data; + cfg.msg32 = fixture.msg32; + parties.emplace(signer, SignParty(std::move(cfg))); + } + return parties; +} + +PeerMap CollectRound1Messages( + SignPartyMap* parties, const std::vector& signers) { + PeerMap round1; + for (PartyIndex signer : signers) { + round1.emplace(signer, parties->at(signer).MakeRound1()); + } + return round1; +} + +std::vector CollectRound2Requests( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round1) { + std::vector out; + for (PartyIndex signer : signers) { + const auto requests = parties->at(signer).MakeRound2Requests( + BuildPeerMapFor(signers, signer, round1)); + out.insert(out.end(), requests.begin(), requests.end()); + } + return out; +} + +std::vector CollectRound2Responses( + SignPartyMap* parties, const std::vector& signers, + const std::vector& round2_requests) { + const auto grouped = GroupRound2RequestsByRecipient(round2_requests); + std::vector out; + for (PartyIndex signer : signers) { + const auto it = grouped.find(signer); + if (it == grouped.end()) { + throw std::runtime_error("missing round2 requests for signer"); + } + const auto responses = parties->at(signer).MakeRound2Responses(it->second); + out.insert(out.end(), responses.begin(), responses.end()); + } + return out; +} + +PeerMap CollectRound3Messages( + SignPartyMap* parties, const std::vector& signers, + const std::vector& round2_responses) { + const auto grouped = GroupRound2ResponsesByRecipient(round2_responses); + PeerMap round3; + for (PartyIndex signer : signers) { + const auto it = grouped.find(signer); + if (it == grouped.end()) { + throw std::runtime_error("missing round2 responses for signer"); + } + round3.emplace(signer, parties->at(signer).MakeRound3(it->second)); + } + return round3; +} + +PeerMap CollectRound4Messages( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round3) { + PeerMap round4; + for (PartyIndex signer : signers) { + round4.emplace(signer, parties->at(signer).MakeRound4( + BuildPeerMapFor(signers, signer, round3))); + } + return round4; +} + +PeerMap CollectRound5AMessages( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round4) { + PeerMap round5a; + for (PartyIndex signer : signers) { + round5a.emplace(signer, parties->at(signer).MakeRound5A( + BuildPeerMapFor(signers, signer, round4))); + } + return round5a; +} + +PeerMap CollectRound5BMessages( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round5a) { + PeerMap round5b; + for (PartyIndex signer : signers) { + round5b.emplace(signer, parties->at(signer).MakeRound5B( + BuildPeerMapFor(signers, signer, round5a))); + } + return round5b; +} + +PeerMap CollectRound5CMessages( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round5b) { + PeerMap round5c; + for (PartyIndex signer : signers) { + round5c.emplace(signer, parties->at(signer).MakeRound5C( + BuildPeerMapFor(signers, signer, round5b))); + } + return round5c; +} + +PeerMap CollectRound5DMessages( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round5c) { + PeerMap round5d; + for (PartyIndex signer : signers) { + round5d.emplace(signer, parties->at(signer).MakeRound5D( + BuildPeerMapFor(signers, signer, round5c))); + } + return round5d; +} + +PeerMap CollectRound5EReveals( + SignPartyMap* parties, const std::vector& signers, + const PeerMap& round5d) { + PeerMap round5e; + for (PartyIndex signer : signers) { + round5e.emplace(signer, parties->at(signer).RevealRound5E( + BuildPeerMapFor(signers, signer, round5d))); + } + return round5e; +} + +PeerMap FinalizeSignatures(SignPartyMap* parties, + const std::vector& signers, + const PeerMap& round5e) { + PeerMap signatures; + for (PartyIndex signer : signers) { + signatures.emplace(signer, parties->at(signer).Finalize( + BuildPeerMapFor(signers, signer, round5e))); + } + return signatures; +} + +SignPartyMap BuildDefaultSignParties(const KeygenOutputs& keygen_results, + const Bytes& keygen_session_id, + const Bytes& sign_session_id) { + const std::vector signers = {1, 2}; + return BuildSignParties(BuildSignFixture(signers), keygen_results, + sign_session_id, keygen_session_id); +} + +void RunToRound5B(SignPartyMap* parties, const std::vector& signers, + SignRoundState* state) { + state->round1 = CollectRound1Messages(parties, signers); + state->round2_requests = + CollectRound2Requests(parties, signers, state->round1); + state->round2_responses = + CollectRound2Responses(parties, signers, state->round2_requests); + state->round3 = + CollectRound3Messages(parties, signers, state->round2_responses); + state->round4 = CollectRound4Messages(parties, signers, state->round3); + state->round5a = CollectRound5AMessages(parties, signers, state->round4); + state->round5b = CollectRound5BMessages(parties, signers, state->round5a); +} + +void TestSignEndToEndProducesVerifiableSignature() { + const Bytes keygen_session = {0xD1, 0x03, 0x01}; + const Bytes sign_session = {0xE1, 0x02, 0x01}; + const auto keygen_results = + RunKeygenAndCollectResults(/*n=*/3, /*t=*/1, keygen_session); + const std::vector signers = {1, 2}; + const SignFixture fixture = BuildSignFixture(signers); + auto parties = + BuildSignParties(fixture, keygen_results, sign_session, keygen_session); + + SignRoundState state; + RunToRound5B(&parties, signers, &state); + state.round5c = CollectRound5CMessages(&parties, signers, state.round5b); + state.round5d = CollectRound5DMessages(&parties, signers, state.round5c); + state.round5e = CollectRound5EReveals(&parties, signers, state.round5d); + const auto signatures = FinalizeSignatures(&parties, signers, state.round5e); + + const Signature& baseline = signatures.at(signers.front()); + Expect(baseline.r.value() != 0, "final signature r must be non-zero"); + Expect(baseline.s.value() != 0, "final signature s must be non-zero"); + Expect(ecdsa::verify::VerifyEcdsaSignatureMath( + keygen_results.at(1).public_keygen_data.y, fixture.msg32, + baseline.r, baseline.s), + "final signature must verify"); + + for (PartyIndex signer : signers) { + const Signature& signature = signatures.at(signer); + Expect(signature.r == baseline.r, "all signers must derive same r"); + Expect(signature.s == baseline.s, "all signers must derive same s"); + Expect(signature.R == baseline.R, "all signers must derive same R"); + } +} + +void TestTamperedMtaProofAbortsResponder() { + const Bytes session = {0xD7, 0x03, 0x01}; + const auto keygen_results = RunKeygenAndCollectResults(/*n=*/3, /*t=*/1, + session); + const std::vector signers = {1, 2}; + auto parties = + BuildDefaultSignParties(keygen_results, session, Bytes{0xE7, 0x02, 0x01}); + + const auto round1 = CollectRound1Messages(&parties, signers); + std::vector round2_requests = + CollectRound2Requests(&parties, signers, round1); + for (SignRound2Request& request : round2_requests) { + if (request.from == 1 && request.to == 2) { + request.a1_proof.s2 += BigInt(1); + ExpectThrow( + [&]() { + (void)CollectRound2Responses(&parties, signers, round2_requests); + }, + "responder must reject tampered A1 proof"); + return; + } + } + throw std::runtime_error("Test failed: missing round2 request to tamper"); +} + +void TestTamperedCommitmentAbortsReceiver() { + const Bytes session = {0xDB, 0x03, 0x01}; + const auto keygen_results = RunKeygenAndCollectResults(/*n=*/3, /*t=*/1, + session); + const std::vector signers = {1, 2}; + auto parties = + BuildDefaultSignParties(keygen_results, session, Bytes{0xEB, 0x02, 0x01}); + + SignRoundState state; + state.round1 = CollectRound1Messages(&parties, signers); + state.round2_requests = + CollectRound2Requests(&parties, signers, state.round1); + state.round2_responses = + CollectRound2Responses(&parties, signers, state.round2_requests); + state.round3 = + CollectRound3Messages(&parties, signers, state.round2_responses); + state.round4 = CollectRound4Messages(&parties, signers, state.round3); + state.round5a = CollectRound5AMessages(&parties, signers, state.round4); + state.round5a.at(1).commitment[0] ^= 0x01; + state.round5b = CollectRound5BMessages(&parties, signers, state.round5a); + + ExpectThrow( + [&]() { (void)CollectRound5CMessages(&parties, signers, state.round5b); }, + "receiver must reject tampered round5A commitment"); +} + +} // namespace +} // namespace tecdsa::ecdsa_flow_test + +int main() { + using namespace tecdsa::ecdsa_flow_test; + + try { + TestSignEndToEndProducesVerifiableSignature(); + TestTamperedMtaProofAbortsResponder(); + TestTamperedCommitmentAbortsReceiver(); + } catch (const std::exception& ex) { + std::cerr << ex.what() << '\n'; + return 1; + } + + std::cout << "sign_flow_tests passed" << '\n'; + return 0; +} diff --git a/yacl/crypto/experimental/threshold_signatures/tests/sm2_flow_test.cc b/yacl/crypto/experimental/threshold_signatures/tests/sm2_flow_test.cc new file mode 100644 index 00000000..4d363cf4 --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/tests/sm2_flow_test.cc @@ -0,0 +1,479 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/sm2/common.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/detection/evidence.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/keygen/keygen.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/presign/offline.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/sign/online.h" +#include "yacl/crypto/experimental/threshold_signatures/sm2/verify/verify.h" +#include "yacl/crypto/experimental/threshold_signatures/tests/test_helpers.h" + +namespace tecdsa::sm2_flow_test { +namespace { + +using ::tecdsa::Bytes; +using ::tecdsa::ECPoint; +using ::tecdsa::PartyIndex; +using ::tecdsa::Scalar; +using ::tecdsa::sm2::detection::AbortStage; +using ::tecdsa::sm2::detection::EvidenceKind; +using ::tecdsa::sm2::keygen::KeygenOutput; +using ::tecdsa::sm2::keygen::KeygenParty; +using ::tecdsa::sm2::keygen::KeygenRound1Msg; +using ::tecdsa::sm2::keygen::KeygenRound2Broadcast; +using ::tecdsa::sm2::keygen::KeygenRound3Request; +using ::tecdsa::sm2::keygen::KeygenRound3Response; +using ::tecdsa::sm2::keygen::KeygenRound4Msg; +using ::tecdsa::sm2::keygen::PeerMap; +using ::tecdsa::sm2::presign::OfflineParty; +using ::tecdsa::sm2::presign::OfflineState; +using ::tecdsa::sm2::presign::Round1Msg; +using ::tecdsa::sm2::presign::Round2Request; +using ::tecdsa::sm2::presign::Round2Response; +using ::tecdsa::sm2::presign::Round3Msg; +using ::tecdsa::sm2::sign::OnlineParty; +using ::tecdsa::sm2::verify::Signature; +using ::tecdsa::test_helpers::BuildPeerMapFor; +using ::tecdsa::test_helpers::Expect; +using ::tecdsa::test_helpers::ExpectThrow; + +constexpr uint32_t kTestAuxRsaBits = 192; + +using KeygenOutputs = std::unordered_map; +using KeygenPartyMap = std::unordered_map; +using OfflinePartyMap = std::unordered_map; +using OfflineStates = std::unordered_map; +using OnlinePartyMap = std::unordered_map; +using KeygenRound2Shares = std::unordered_map>; + +std::vector BuildParticipants(uint32_t n) { + std::vector out; + out.reserve(n); + for (PartyIndex party = 1; party <= n; ++party) { + out.push_back(party); + } + return out; +} + +template +std::unordered_map> GroupByRecipient( + const std::vector& messages) { + std::unordered_map> grouped; + for (const auto& message : messages) { + grouped[message.to].push_back(message); + } + return grouped; +} + +template +T RequireDetectionValue(const sm2::detection::DetectionResult& result, + const std::string& message) { + if (!result.ok()) { + throw std::runtime_error("Test failed: " + message + ": " + + result.abort->reason); + } + return *result.value; +} + +KeygenOutputs RunKeygen(uint32_t n, uint32_t t, const Bytes& session_id, + const Bytes& signer_id) { + const auto participants = BuildParticipants(n); + KeygenPartyMap parties; + for (PartyIndex party : participants) { + sm2::keygen::KeygenConfig cfg; + cfg.session_id = session_id; + cfg.self_id = party; + cfg.participants = participants; + cfg.threshold = t; + cfg.aux_rsa_modulus_bits = kTestAuxRsaBits; + cfg.signer_id = signer_id; + parties.emplace(party, KeygenParty(std::move(cfg))); + } + + PeerMap round1; + for (PartyIndex party : participants) { + round1.emplace(party, parties.at(party).MakeRound1()); + } + + PeerMap round2_broadcasts; + KeygenRound2Shares round2_shares; + for (PartyIndex party : participants) { + const auto round2 = parties.at(party).MakeRound2( + BuildPeerMapFor(participants, party, round1)); + round2_broadcasts.emplace(party, round2.broadcast); + round2_shares.emplace(party, round2.shares_for_peers); + } + + std::vector all_requests; + for (PartyIndex party : participants) { + PeerMap shares_for_self; + for (PartyIndex peer : participants) { + if (peer != party) { + shares_for_self.emplace(peer, round2_shares.at(peer).at(party)); + } + } + const auto requests = parties.at(party).MakeRound3Requests( + BuildPeerMapFor(participants, party, round2_broadcasts), + shares_for_self); + all_requests.insert(all_requests.end(), requests.begin(), requests.end()); + } + + const auto grouped_requests = GroupByRecipient(all_requests); + std::vector all_responses; + for (PartyIndex party : participants) { + const auto responses = RequireDetectionValue( + parties.at(party).TryMakeRound3Responses(grouped_requests.at(party)), + "SM2 keygen round3 responses"); + all_responses.insert(all_responses.end(), responses.begin(), responses.end()); + } + + const auto grouped_responses = GroupByRecipient(all_responses); + PeerMap round4; + for (PartyIndex party : participants) { + round4.emplace(party, + parties.at(party).MakeRound4(grouped_responses.at(party))); + } + + KeygenOutputs outputs; + for (PartyIndex party : participants) { + outputs.emplace( + party, RequireDetectionValue( + parties.at(party).TryFinalize( + BuildPeerMapFor(participants, party, round4)), + "SM2 keygen finalize")); + } + return outputs; +} + +OfflinePartyMap BuildOfflineParties(const std::vector& signers, + const KeygenOutputs& keygen_outputs, + const Bytes& session_id) { + OfflinePartyMap parties; + for (PartyIndex signer : signers) { + sm2::presign::OfflineConfig cfg; + cfg.session_id = session_id; + cfg.self_id = signer; + cfg.participants = signers; + cfg.local_key_share = keygen_outputs.at(signer).local_key_share; + cfg.public_keygen_data = keygen_outputs.at(signer).public_keygen_data; + parties.emplace(signer, OfflineParty(std::move(cfg))); + } + return parties; +} + +OfflineStates RunOffline(const std::vector& signers, + const KeygenOutputs& keygen_outputs, + const Bytes& session_id) { + auto parties = BuildOfflineParties(signers, keygen_outputs, session_id); + PeerMap round1; + for (PartyIndex signer : signers) { + round1.emplace(signer, parties.at(signer).MakeRound1()); + } + + std::vector all_requests; + for (PartyIndex signer : signers) { + const auto requests = parties.at(signer).MakeRound2Requests( + BuildPeerMapFor(signers, signer, round1)); + all_requests.insert(all_requests.end(), requests.begin(), requests.end()); + } + + const auto grouped_requests = GroupByRecipient(all_requests); + std::vector all_responses; + for (PartyIndex signer : signers) { + const auto responses = RequireDetectionValue( + parties.at(signer).TryMakeRound2Responses(grouped_requests.at(signer)), + "SM2 offline round2 responses"); + all_responses.insert(all_responses.end(), responses.begin(), responses.end()); + } + + const auto grouped_responses = GroupByRecipient(all_responses); + PeerMap round3; + for (PartyIndex signer : signers) { + round3.emplace(signer, + parties.at(signer).MakeRound3(grouped_responses.at(signer))); + } + + OfflineStates states; + for (PartyIndex signer : signers) { + states.emplace( + signer, RequireDetectionValue( + parties.at(signer).TryFinalize( + BuildPeerMapFor(signers, signer, round3)), + "SM2 offline finalize")); + } + return states; +} + +OnlinePartyMap BuildOnlineParties(const std::vector& signers, + const KeygenOutputs& keygen_outputs, + const OfflineStates& offline_states, + const Bytes& session_id, + const Bytes& message) { + OnlinePartyMap parties; + for (PartyIndex signer : signers) { + sm2::sign::OnlineConfig cfg; + cfg.session_id = session_id; + cfg.self_id = signer; + cfg.participants = signers; + cfg.local_key_share = keygen_outputs.at(signer).local_key_share; + cfg.public_keygen_data = keygen_outputs.at(signer).public_keygen_data; + cfg.offline = offline_states.at(signer); + cfg.message = message; + parties.emplace(signer, OnlineParty(std::move(cfg))); + } + return parties; +} + +void ExpectAbort(const std::optional& abort, + AbortStage stage, EvidenceKind evidence_kind, + PartyIndex culprit, const std::optional& instance_id, + const std::string& message) { + Expect(abort.has_value(), message + ": missing abort report"); + Expect(abort->stage == stage, message + ": wrong stage"); + Expect(abort->evidence_kind == evidence_kind, + message + ": wrong evidence kind"); + Expect(abort->culprit == culprit, message + ": wrong culprit"); + Expect(abort->instance_id == instance_id, message + ": wrong instance id"); + Expect(!abort->reason.empty(), message + ": missing reason"); +} + +void TestOfflinePresignFlow() { + const Bytes signer_id = {'a', 'l', 'i', 'c', 'e'}; + const auto participants = BuildParticipants(3); + const std::vector signers = {1, 2}; + const auto keygen_outputs = + RunKeygen(/*n=*/3, /*t=*/1, Bytes{0x82, 0x01}, signer_id); + ExpectThrow( + [&]() { + (void)BuildOfflineParties(participants, keygen_outputs, + Bytes{0x82, 0x02}); + }, + "SM2 offline must reject signer sets larger than threshold + 1"); + ExpectThrow( + [&]() { + (void)BuildOfflineParties(std::vector{1}, keygen_outputs, + Bytes{0x82, 0x03}); + }, + "SM2 offline must reject signer sets smaller than threshold + 1"); + + const auto offline_states = + RunOffline(signers, keygen_outputs, Bytes{0x82, 0x04}); + const auto& baseline = offline_states.at(signers.front()); + + for (PartyIndex party : signers) { + const auto& state = offline_states.at(party); + Expect(state.R == baseline.R, "all SM2 parties must derive same offline R"); + Expect(state.W == baseline.W, "all SM2 parties must derive same offline W"); + Expect(state.delta_i.value() != 0, + "SM2 offline delta share should be non-zero in honest flow"); + Expect(state.all_W_i.size() == signers.size(), + "SM2 offline state must include all W_i"); + Expect(state.all_T_i.size() == signers.size(), + "SM2 offline state must include all T_i"); + Expect(state.all_WK_i.size() == signers.size(), + "SM2 offline state must include all WK_i"); + Expect(ECPoint::GeneratorMultiply(state.delta_i) == state.all_T_i.at(party), + "SM2 offline T_i must match local delta_i"); + } + Expect(sm2::internal::SumPointsOrThrow( + {baseline.all_W_i.at(1), baseline.all_W_i.at(2)}) == baseline.W, + "SM2 offline W must aggregate signer W_i"); + Expect(sm2::internal::SumPointsOrThrow( + {baseline.all_T_i.at(1), baseline.all_T_i.at(2)}) == + sm2::internal::SumPointsOrThrow( + {baseline.all_WK_i.at(1), baseline.all_WK_i.at(2)}), + "SM2 offline aggregate T_i must match aggregate WK_i"); +} + +void TestOnlineSignFlow() { + const Bytes signer_id = {'a', 'l', 'i', 'c', 'e'}; + const Bytes message = {'h', 'e', 'l', 'l', 'o', ' ', 's', 'm', '2'}; + const auto participants = BuildParticipants(3); + const std::vector signers = {1, 2}; + const auto keygen_outputs = + RunKeygen(/*n=*/3, /*t=*/1, Bytes{0x83, 0x01}, signer_id); + const auto offline_states = + RunOffline(signers, keygen_outputs, Bytes{0x83, 0x02}); + auto oversized_offline_states = offline_states; + oversized_offline_states.emplace(3, offline_states.at(signers.front())); + ExpectThrow( + [&]() { + (void)BuildOnlineParties(participants, keygen_outputs, + oversized_offline_states, Bytes{0x83, 0x03}, + message); + }, + "SM2 online must reject signer sets larger than threshold + 1"); + ExpectThrow( + [&]() { + (void)BuildOnlineParties(std::vector{1}, keygen_outputs, + offline_states, Bytes{0x83, 0x04}, message); + }, + "SM2 online must reject signer sets smaller than threshold + 1"); + + const auto lagrange = sm2::internal::ComputeLagrangeAtZero(signers); + Scalar subset_z = sm2::internal::Sm2Zero(); + Scalar delta = sm2::internal::Sm2Zero(); + for (PartyIndex party : signers) { + subset_z = subset_z + + (lagrange.at(party) * keygen_outputs.at(party).local_key_share.z_i); + delta = delta + offline_states.at(party).delta_i; + } + Expect(subset_z.value() != 0, "strict SM2 subset z must be non-zero"); + Expect(ECPoint::GeneratorMultiply(delta) == + offline_states.at(signers.front()).R.Mul(subset_z), + "strict SM2 offline delta must satisfy delta = k * z"); + + const Bytes digest = sm2::zid::PreprocessMessageDigest( + keygen_outputs.at(signers.front()).local_key_share.binding, message); + const Scalar e = Scalar::FromBigEndianModQ(digest, sm2::internal::Sm2Group()); + const Scalar r = + e + sm2::internal::XCoordinateModN(offline_states.at(signers.front()).R); + const Signature manual_signature{ + .r = r, + .s = delta + (r * subset_z) - r, + .R = offline_states.at(signers.front()).R, + }; + Expect(sm2::verify::VerifySm2SignatureMath( + keygen_outputs.at(signers.front()).public_keygen_data.public_key, + keygen_outputs.at(signers.front()).local_key_share.binding, + message, manual_signature), + "strict SM2 subset signature formula must verify"); + + auto online_parties = BuildOnlineParties(signers, keygen_outputs, + offline_states, Bytes{0x83, 0x03}, + message); + sm2::sign::PeerMap partials; + for (PartyIndex party : signers) { + partials.emplace(party, online_parties.at(party).MakePartialSignature()); + Expect(ECPoint::GeneratorMultiply(partials.at(party)) == + offline_states.at(party).all_T_i.at(party).Add( + offline_states.at(party).all_W_i.at(party).Mul(r)), + "strict SM2 online partial must satisfy g^s_i = T_i * W_i^r"); + } + + std::unordered_map signatures; + for (PartyIndex party : signers) { + const auto result = online_parties.at(party).TryFinalize( + BuildPeerMapFor(signers, party, partials)); + Expect(result.ok(), "strict SM2 online finalize must succeed"); + signatures.emplace(party, *result.value); + } + const auto& baseline = signatures.at(signers.front()); + + for (PartyIndex party : signers) { + const auto& signature = signatures.at(party); + Expect(signature.r == baseline.r && signature.s == baseline.s && + signature.R == baseline.R, + "all SM2 parties must finalize the same signature"); + Expect(sm2::verify::VerifySm2SignatureMath( + keygen_outputs.at(party).public_keygen_data.public_key, + keygen_outputs.at(party).local_key_share.binding, message, + signature), + "final SM2 signature must verify"); + } +} + +void TestTamperedOfflineA1ProofIdentifiesCulprit() { + const Bytes signer_id = {'a', 'l', 'i', 'c', 'e'}; + const std::vector signers = {1, 2}; + const auto keygen_outputs = + RunKeygen(/*n=*/3, /*t=*/1, Bytes{0x84, 0x07}, signer_id); + auto parties = BuildOfflineParties(signers, keygen_outputs, Bytes{0x84, 0x08}); + + PeerMap round1; + for (PartyIndex party : signers) { + round1.emplace(party, parties.at(party).MakeRound1()); + } + + std::vector all_requests; + for (PartyIndex party : signers) { + const auto requests = parties.at(party).MakeRound2Requests( + BuildPeerMapFor(signers, party, round1)); + all_requests.insert(all_requests.end(), requests.begin(), requests.end()); + } + + std::optional tampered_instance_id; + for (auto& request : all_requests) { + if (request.from == 1 && request.to == 2) { + request.a1_proof.s2 = request.a1_proof.s2 + decltype(request.a1_proof.s2)(1); + tampered_instance_id = request.instance_id; + break; + } + } + Expect(tampered_instance_id.has_value(), + "SM2 offline A1 tamper case must locate request from 1 to 2"); + + const auto grouped_requests = GroupByRecipient(all_requests); + const auto result = parties.at(2).TryMakeRound2Responses(grouped_requests.at(2)); + Expect(!result.ok(), "SM2 offline MtA must reject tampered A1 proof"); + ExpectAbort(result.abort, AbortStage::kOffline, EvidenceKind::kMtaProof, + /*culprit=*/1, tampered_instance_id, "SM2 offline MtA blame"); +} + +void TestTamperedOnlinePartialIdentifiesCulprit() { + const Bytes signer_id = {'a', 'l', 'i', 'c', 'e'}; + const Bytes message = {'t', 'a', 'm', 'p', 'e', 'r'}; + const std::vector signers = {1, 2}; + const auto keygen_outputs = + RunKeygen(/*n=*/3, /*t=*/1, Bytes{0x84, 0x04}, signer_id); + const auto offline_states = + RunOffline(signers, keygen_outputs, Bytes{0x84, 0x05}); + auto parties = BuildOnlineParties(signers, keygen_outputs, offline_states, + Bytes{0x84, 0x06}, message); + + sm2::sign::PeerMap partials; + for (PartyIndex party : signers) { + partials.emplace(party, parties.at(party).MakePartialSignature()); + } + partials.at(1) = + partials.at(1) + Scalar::FromUint64(1, partials.at(1).group()); + + const auto result = + parties.at(2).TryFinalize(BuildPeerMapFor(signers, 2, partials)); + Expect(!result.ok(), + "SM2 online finalize must reject a tampered partial signature"); + ExpectAbort(result.abort, AbortStage::kOnline, + EvidenceKind::kPartialSignature, /*culprit=*/1, std::nullopt, + "SM2 online partial blame"); +} + +} // namespace +} // namespace tecdsa::sm2_flow_test + +int main() { + using namespace tecdsa::sm2_flow_test; + + try { + TestOfflinePresignFlow(); + TestOnlineSignFlow(); + TestTamperedOfflineA1ProofIdentifiesCulprit(); + TestTamperedOnlinePartialIdentifiesCulprit(); + } catch (const std::exception& ex) { + std::cerr << ex.what() << '\n'; + return 1; + } + + std::cout << "sm2_sign_flow_tests passed" << '\n'; + return 0; +} diff --git a/yacl/crypto/experimental/threshold_signatures/tests/test_helpers.h b/yacl/crypto/experimental/threshold_signatures/tests/test_helpers.h new file mode 100644 index 00000000..e1aebcfd --- /dev/null +++ b/yacl/crypto/experimental/threshold_signatures/tests/test_helpers.h @@ -0,0 +1,59 @@ +// Copyright 2026 Ant Group Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include + +#include "yacl/crypto/experimental/threshold_signatures/common/ids.h" + +namespace tecdsa::test_helpers { + +inline void Expect(bool condition, const std::string& message) { + if (!condition) { + throw std::runtime_error("Test failed: " + message); + } +} + +inline void ExpectThrow(const std::function& fn, + const std::string& message) { + bool threw = false; + try { + fn(); + } catch (const std::exception&) { + threw = true; + } + if (!threw) { + throw std::runtime_error("Test failed: " + message); + } +} + +template +std::unordered_map BuildPeerMapFor( + const std::vector& parties, PartyIndex self_id, + const std::unordered_map& all_msgs) { + std::unordered_map out; + for (PartyIndex peer : parties) { + if (peer != self_id) { + out.emplace(peer, all_msgs.at(peer)); + } + } + return out; +} + +} // namespace tecdsa::test_helpers diff --git a/yacl/link/factory_test.cc b/yacl/link/factory_test.cc index d273e92b..08a153d9 100644 --- a/yacl/link/factory_test.cc +++ b/yacl/link/factory_test.cc @@ -14,6 +14,7 @@ #include "yacl/link/factory.h" +#include #include #include #include @@ -39,6 +40,8 @@ enum class SslMode { SM2_SM3, // mode = 2 }; +constexpr int kFactoryTestBasePort = 45000; + inline std::pair GenCertFiles( const std::string& prefix, const SslMode mode) { auto pk_path = fmt::format("{}_pk.pem", prefix); @@ -87,15 +90,21 @@ inline std::pair GenCertFiles( inline ContextDesc MakeDesc(int count, const SslMode mode) { ContextDesc desc; desc.id = fmt::format("world_{}", count); - desc.parties.push_back(ContextDesc::Party("alice", "127.0.0.1:63927")); - desc.parties.push_back(ContextDesc::Party("bob", "127.0.0.1:63921")); + const int alice_port = kFactoryTestBasePort + count * 2; + const int bob_port = alice_port + 1; + desc.parties.push_back( + ContextDesc::Party("alice", fmt::format("127.0.0.1:{}", alice_port))); + desc.parties.push_back( + ContextDesc::Party("bob", fmt::format("127.0.0.1:{}", bob_port))); if (mode != SslMode::NONE) { desc.enable_ssl = true; desc.server_ssl_opts.ciphers = ""; // auto detect // export rsa keys to files - auto [server_sk_path, server_cer_path] = GenCertFiles("server", mode); - auto [client_sk_path, client_cer_path] = GenCertFiles("client", mode); + auto [server_sk_path, server_cer_path] = + GenCertFiles(fmt::format("server_{}", count), mode); + auto [client_sk_path, client_cer_path] = + GenCertFiles(fmt::format("client_{}", count), mode); desc.server_ssl_opts.cert.certificate_path = server_cer_path; desc.server_ssl_opts.cert.private_key_path = server_sk_path; @@ -110,9 +119,11 @@ template class FactoryTest : public ::testing::Test { public: void SetUp() override { - static int desc_count = 0; + static std::atomic desc_count{0}; contexts_.resize(2); - auto desc = MakeDesc(desc_count++, SslMode(M::get_mode())); + auto desc = + MakeDesc(desc_count.fetch_add(1, std::memory_order_relaxed), + SslMode(M::get_mode())); auto create_brpc = [&](int self_rank) { contexts_[self_rank] = M::get_t_instance().CreateContext(desc, self_rank); @@ -139,6 +150,8 @@ class FactoryTest : public ::testing::Test { for (auto& f : waits) { f.get(); } + + contexts_.clear(); } std::vector> contexts_;