Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion common/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,37 @@ package common

import "math/big"

const primalityRounds = 30
const (
primalityRounds = 30
// MinUnknownOrderModulusBitLen is the width floor of every unknown-order
// modulus this library's exported verifiers will accept. The wire path
// pins a peer's modulus to exactly 2048 bits before any proof is
// verified; below 2048 bits the secp256k1-scale challenge space (2^256)
// no longer dominates the modulus, and no supported key can have a
// narrower unknown-order modulus.
MinUnknownOrderModulusBitLen = 2048

// MaxUnknownOrderModulusBitLen is a resource ceiling on the width of an
// unknown-order modulus the exported verifiers will accept. Everything a
// verifier does with a caller-supplied modulus -- the ProbablyPrime call
// below, any modular reduction, GCD, sampler and per-candidate
// exponentiation -- is O(bitLen), so without a ceiling a direct API call
// allocates and hashes proportional to a caller-controlled width. This
// ceiling is 32x the 2048-bit wire-pinned width, so it excludes nothing
// this library's wire path can produce; it only bounds work an
// untrusted caller can force through an exported verify entry point.
MaxUnknownOrderModulusBitLen = 65536
)

// ExceedsUnknownOrderModulusCeiling reports whether N is wider than
// MaxUnknownOrderModulusBitLen. It is a standalone O(bitLen) predicate (no
// primality work, no modulus-sized allocation) so exported unknown-order
// verifiers can reject oversized caller-supplied moduli before
// IsUsableUnknownOrderModulus's ProbablyPrime call or any other
// modulus-sized work runs against them.
func ExceedsUnknownOrderModulusCeiling(N *big.Int) bool {
return N != nil && N.BitLen() > MaxUnknownOrderModulusBitLen
}

func IsUsableUnknownOrderModulus(N *big.Int, minBitLen int) bool {
return N != nil &&
Expand Down
13 changes: 9 additions & 4 deletions crypto/dlnproof/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ import (
)

const (
Iterations = 128
verifyMinModulusBitLen = 2048
fsDomainTagDLNProof = "tss-lib.threshold.dlnproof"
Iterations = 128
fsDomainTagDLNProof = "tss-lib.threshold.dlnproof"
)

func fsSessionDLNProof(session []byte) []byte {
Expand Down Expand Up @@ -87,7 +86,13 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool {
if p == nil {
return false
}
if !common.IsUsableUnknownOrderModulus(N, verifyMinModulusBitLen) {
// Width policy: reject a caller-supplied modulus wider than the shared
// ceiling before any ProbablyPrime call or per-candidate modular
// exponentiation is run against it.
if common.ExceedsUnknownOrderModulusCeiling(N) {
return false
}
if !common.IsUsableUnknownOrderModulus(N, common.MinUnknownOrderModulusBitLen) {
return false
}
modN := common.ModInt(N)
Expand Down
6 changes: 3 additions & 3 deletions crypto/mta/constant_time_equiv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func TestBobProofUnequalWidthsCTEquivalence(t *testing.T) {
//
// The protocol's internal Verify calls must complete, so the keygen fixture's ~2048-bit
// auxiliary moduli are used instead of #10's tiny 11*23 modulus, which the current
// hardened verifiers reject (verifyMinModulusBitLen floor). The witness betaPrm is a
// hardened verifiers reject (MinUnknownOrderModulusBitLen floor). The witness betaPrm is a
// q5-width random that can approach the public plaintext domain width; it is a valid
// Paillier plaintext and the homomorphic result does not wrap the domain.
func TestShareProtocolUnequalWidthsCTEquivalence(t *testing.T) {
Expand All @@ -177,8 +177,8 @@ func TestShareProtocolUnequalWidthsCTEquivalence(t *testing.T) {
require.NoError(t, err)
// The fixture auxiliary moduli must clear the verifier floor so the full share
// protocol's internal Verify calls complete.
require.True(t, common.IsUsableUnknownOrderModulus(NTildei, verifyMinModulusBitLen), "fixture auxiliary modulus must clear the verifier floor")
require.True(t, common.IsUsableUnknownOrderModulus(NTildej, verifyMinModulusBitLen), "fixture auxiliary modulus must clear the verifier floor")
require.True(t, common.IsUsableUnknownOrderModulus(NTildei, common.MinUnknownOrderModulusBitLen), "fixture auxiliary modulus must clear the verifier floor")
require.True(t, common.IsUsableUnknownOrderModulus(NTildej, common.MinUnknownOrderModulusBitLen), "fixture auxiliary modulus must clear the verifier floor")

a, b := big.NewInt(1<<24+3), big.NewInt(1<<32+5)
B := crypto.ScalarBaseMult(tss.EC(), b)
Expand Down
21 changes: 9 additions & 12 deletions crypto/mta/legacy_bob_historical_witness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,10 @@ func TestLegacyBobWidenedVerifierAcceptsHistoricalWitnessRange(t *testing.T) {
q5 := new(big.Int).Exp(q, big.NewInt(5), nil)
q6 := new(big.Int).Mul(q5, q)
tightMaxT1 := new(big.Int).Add(pk.N, q6)
widenedMaxT1 := legacyT1Max(ec, pk, true)
widenedMaxT1 := new(big.Int).Mul(new(big.Int).Add(q, big.NewInt(1)), pk.N)
// The widened bound must be exactly the historical witness-range bound:
// T1 = e*y + gamma with e < q, y < N, gamma < N gives T1 < q*N + N.
if widenedMaxT1.Cmp(new(big.Int).Mul(new(big.Int).Add(q, big.NewInt(1)), pk.N)) != 0 {
t.Fatal("widened legacy T1 bound is not (q+1)*N")
}
if legacyT1Max(ec, pk, false).Cmp(tightMaxT1) != 0 {
t.Fatal("tight legacy T1 bound is not N + q^6")
}
// (The verifier computes it inline in VerifyLegacy when compat is on.)

X := crypto.ScalarBaseMult(ec, x)
cases := []struct {
Expand Down Expand Up @@ -168,9 +163,11 @@ func TestLegacyHistoricalWitnessRangeRejectsWiderT1(t *testing.T) {
c1 := fixedPaillierEncryption(pk, big.NewInt(5), firstSmallUnit(pk.N, 3))
y := new(big.Int).Sub(pk.N, big.NewInt(1))
bobProof, c2 := historicalBobProofForWitnessY(t, ec, pk, aux.NTildei, aux.H1i, aux.H2i, c1, x, y, r, nil)

if bobProof.T1.Cmp(legacyT1Max(ec, pk, false)) < 0 {
t.Fatal("y = N - 1 witness did not place T1 above the tight bound; the y = N-T wrap family is not exercised")
// The shared verify core owns the default tight legacy bound: N + q^6.
q6 := new(big.Int).Exp(ec.Params().N, big.NewInt(6), nil)
tightT1Max := new(big.Int).Add(pk.N, q6)
if bobProof.T1.Cmp(tightT1Max) < 0 {
t.Fatal("y = N - 1 witness did not place T1 above the tight N + q^6 bound; the y = N-T wrap family is not exercised")
}
if _, err := AliceEnd(ec, pk, bobProof.ProofBob, aux.H1i, aux.H2i, c1, c2, aux.NTildei, owner.PaillierSK); err == nil {
t.Fatal("default AliceEnd accepted a proof with T1 above the tight bound")
Expand All @@ -195,8 +192,8 @@ func TestLegacyHistoricalWitnessRangeRejectsWiderT1(t *testing.T) {

X := crypto.ScalarBaseMult(ec, x)
wcProof, _ := historicalBobProofForWitnessY(t, ec, pk, aux.NTildei, aux.H1i, aux.H2i, c1, x, y, r, X)
if wcProof.T1.Cmp(legacyT1Max(ec, pk, false)) < 0 {
t.Fatal("y = N - 1 WC witness did not place T1 above the tight bound; the y = N-T wrap family is not exercised")
if wcProof.T1.Cmp(tightT1Max) < 0 {
t.Fatal("y = N - 1 WC witness did not place T1 above the tight N + q^6 bound; the y = N-T wrap family is not exercised")
}
if _, err := AliceEndWC(ec, pk, wcProof, X, c1, c2, aux.NTildei, aux.H1i, aux.H2i, owner.PaillierSK); err == nil {
t.Fatal("default AliceEndWC accepted a proof with T1 above the tight bound")
Expand Down
53 changes: 25 additions & 28 deletions crypto/mta/proofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,40 +230,30 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde,
return pf.verify(ec, pk, NTilde, h1, h2, c1, c2, X, optionalProofSession(session), nil)
}

// legacyT1Max returns the exclusive T1 upper bound for the session-less
// legacy Bob/BobWC verifier.
//
// The tight bound N + q^6 admits this implementation's own legacy prover:
// T1 = e*y + gamma with e < q, y < q^5 and gamma < N, so an honest legacy
// response is below q^6 + N.
//
// The widened bound (q+1)*N admits the historical (2e712689) BobMid/BobMidWC
// witness range, which samples y below the Paillier modulus: T1 = e*y + gamma
// with e < q, y < N, gamma < N, hence T1 < (q-1)*N + N < (q+1)*N.
func legacyT1Max(ec elliptic.Curve, pk *paillier.PublicKey, historicalBobCompat bool) *big.Int {
if historicalBobCompat {
return new(big.Int).Mul(new(big.Int).Add(ec.Params().N, one), pk.N)
}
q := ec.Params().N
q3 := new(big.Int).Mul(q, q)
q3 = new(big.Int).Mul(q, q3)
q6 := new(big.Int).Mul(q3, q3)
return new(big.Int).Add(pk.N, q6)
}

// VerifyLegacy verifies a session-less legacy Bob/BobWC proof (the exact
// 2e712689 untagged challenge). It is the explicit compatibility-aware entry
// point used by signing round 3 for legacy parties: the default bound is the
// tight N + q^6 cap, and historicalBobCompat widens it to the historical
// witness-range bound (q+1)*N described on legacyT1Max.
// point used by signing round 3 for legacy parties. The shared verify core
// owns the default tight legacy bound, N + q^6: the session-less prover
// samples y below q^5 with T1 = e*y + gamma, e < q, gamma < N, so an honest
// legacy response stays below q^6 + N. historicalBobCompat passes the
// widened historical witness-range bound (q+1)*N as an explicit override,
// computed and passed only in that case; every other entry point gets the
// core's tight bound.
func (pf *ProofBobWC) VerifyLegacy(
ec elliptic.Curve,
pk *paillier.PublicKey,
NTilde, h1, h2, c1, c2 *big.Int,
X *crypto.ECPoint,
historicalBobCompat bool,
) bool {
return pf.verify(ec, pk, NTilde, h1, h2, c1, c2, X, nil, legacyT1Max(ec, pk, historicalBobCompat))
var maxT1Override *big.Int
if historicalBobCompat {
// The historical (2e712689) prover samples y below the Paillier
// modulus: T1 = e*y + gamma with e < q, y < N, gamma < N, hence
// T1 < (q-1)*N + N < (q+1)*N.
maxT1Override = new(big.Int).Mul(new(big.Int).Add(ec.Params().N, one), pk.N)
}
return pf.verify(ec, pk, NTilde, h1, h2, c1, c2, X, nil, maxT1Override)
}

// verify is the shared Bob/BobWC verification core. maxT1Override, when
Expand All @@ -290,8 +280,15 @@ func (pf *ProofBobWC) verify(
} else if !pf.ProofBob.ValidateBasic() {
return false
}
if !common.IsUsableUnknownOrderModulus(pk.N, verifyMinModulusBitLen) ||
!common.IsUsableUnknownOrderModulus(NTilde, verifyMinModulusBitLen) {
// Width policy: reject caller-supplied moduli wider than the shared
// ceiling before IsUsableUnknownOrderModulus's ProbablyPrime call or any
// modulus-sized work is run against them.
if common.ExceedsUnknownOrderModulusCeiling(pk.N) ||
common.ExceedsUnknownOrderModulusCeiling(NTilde) {
return false
}
if !common.IsUsableUnknownOrderModulus(pk.N, common.MinUnknownOrderModulusBitLen) ||
!common.IsUsableUnknownOrderModulus(NTilde, common.MinUnknownOrderModulusBitLen) {
return false
}
if !common.IsCanonicalGenerator(NTilde, h1) || !common.IsCanonicalGenerator(NTilde, h2) || h1.Cmp(h2) == 0 {
Expand Down Expand Up @@ -578,7 +575,7 @@ func (pf *ProofBob) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1

// ProveBob.VerifyLegacy is the explicit compatibility-aware session-less
// counterpart of ProveBob.Verify: historicalBobCompat widens the T1 bound to
// the historical witness range per legacyT1Max; the prover and every other
// the historical witness range via VerifyLegacy's (q+1)*N override; the prover and every other
// check are unchanged.
func (pf *ProofBob) VerifyLegacy(
ec elliptic.Curve,
Expand Down
12 changes: 9 additions & 3 deletions crypto/mta/range_proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (

const (
RangeProofAliceBytesParts = 6
verifyMinModulusBitLen = 2048
fsDomainTagRangeAlice = "tss-lib.threshold.mta.range-alice"
fsDomainTagBob = "tss-lib.threshold.mta.bob"
fsDomainTagBobWC = "tss-lib.threshold.mta.bob-wc"
Expand Down Expand Up @@ -136,8 +135,15 @@ func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTi
NTilde == nil || h1 == nil || h2 == nil || c == nil {
return false
}
if !common.IsUsableUnknownOrderModulus(pk.N, verifyMinModulusBitLen) ||
!common.IsUsableUnknownOrderModulus(NTilde, verifyMinModulusBitLen) {
// Width policy: reject caller-supplied moduli wider than the shared
// ceiling before any ProbablyPrime call or modulus-sized work is run
// against them.
if common.ExceedsUnknownOrderModulusCeiling(pk.N) ||
common.ExceedsUnknownOrderModulusCeiling(NTilde) {
return false
}
if !common.IsUsableUnknownOrderModulus(pk.N, common.MinUnknownOrderModulusBitLen) ||
!common.IsUsableUnknownOrderModulus(NTilde, common.MinUnknownOrderModulusBitLen) {
return false
}
if !common.IsCanonicalGenerator(NTilde, h1) || !common.IsCanonicalGenerator(NTilde, h2) || h1.Cmp(h2) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion crypto/mta/share_protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func AliceEndWC(

// AliceEndLegacy verifies a session-less legacy Bob proof using the legacy
// T1 bound, then decrypts cB. historicalBobCompat widens the T1 bound to the
// historical (2e712689) witness range per legacyT1Max; the default (false)
// historical (2e712689) witness range via VerifyLegacy's (q+1)*N override;
// keeps the tight N + q^6 bound. This is the signing round-3 entry point for
// legacy parties; AliceEnd is unchanged for standalone callers.
func AliceEndLegacy(
Expand Down
43 changes: 43 additions & 0 deletions crypto/paillier/constant_time_equiv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,46 @@ func TestModProofHelpersCTEquivalence(t *testing.T) {
assert.Equal(t, qrCompOff, qrCompOn, "isQuadResidueModComposite must agree")
assert.Zero(t, rootOff.Cmp(rootOn), "quadResidueModComposite must be byte-identical")
}

// TestModProofCTEquivalence: ModProof is randomised only through w; replaying
// the entropy stream makes every derived challenge and 4th root deterministic,
// so CT-on and CT-off proofs must be byte-identical and both must verify.
func TestModProofCTEquivalence(t *testing.T) {
facSetUp(t)

var proofOff, proofOn *ModProof
for _, enabled := range []bool{false, true} {
t.Run(fmt.Sprintf("CT=%t", enabled), func(t *testing.T) {
previousReader := rand.Reader
t.Cleanup(func() {
rand.Reader = previousReader
})
rand.Reader = mathrand.New(mathrand.NewSource(42))
setPaillierCTTestMode(t, enabled)
proof := privateKey.ModProof()
if enabled {
proofOn = proof
} else {
proofOff = proof
}
})
}

require.NotNil(t, proofOff)
require.NotNil(t, proofOn)
require.Zero(t, proofOff.W.Cmp(proofOn.W), "w must be identical")
for i := range PARAM_M {
require.Zero(t, proofOff.X[i].Cmp(proofOn.X[i]), "X[%d] must be byte-identical", i)
require.Zero(t, proofOff.Z[i].Cmp(proofOn.Z[i]), "Z[%d] must be byte-identical", i)
require.Equal(t, proofOff.A[i], proofOn.A[i], "A[%d] must match", i)
require.Equal(t, proofOff.B[i], proofOn.B[i], "B[%d] must match", i)
}

ok, err := proofOff.ModVerify(publicKey.N)
assert.NoError(t, err)
assert.True(t, ok, "proofOff must verify")

ok, err = proofOn.ModVerify(publicKey.N)
assert.NoError(t, err)
assert.True(t, ok, "proofOn must verify")
}
14 changes: 12 additions & 2 deletions crypto/paillier/factor_proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,20 @@ func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int, session ...[]byte) (bo
if common.AnyIsNil(pf.P, pf.Q, pf.A, pf.B, pf.T, pf.Sigma, pf.Z1, pf.Z2, pf.W1, pf.W2, pf.V) {
return false, fmt.Errorf("fac proof verify: nil bigint present in proof")
}
if !common.IsUsableUnknownOrderModulus(pkN, verifyMinModulusBitLen) {
// Width policy: reject moduli wider than the shared ceiling before any
// ProbablyPrime call or modulus-sized work is run against them.
if common.ExceedsUnknownOrderModulusCeiling(pkN) {
return false, fmt.Errorf("fac proof verify: Paillier modulus bit length %d exceeds maximum %d",
pkN.BitLen(), common.MaxUnknownOrderModulusBitLen)
}
if common.ExceedsUnknownOrderModulusCeiling(N) {
return false, fmt.Errorf("fac proof verify: auxiliary modulus bit length %d exceeds maximum %d",
N.BitLen(), common.MaxUnknownOrderModulusBitLen)
}
if !common.IsUsableUnknownOrderModulus(pkN, common.MinUnknownOrderModulusBitLen) {
return false, fmt.Errorf("fac proof verify: invalid Paillier modulus %x", pkN)
}
if !common.IsUsableUnknownOrderModulus(N, verifyMinModulusBitLen) {
if !common.IsUsableUnknownOrderModulus(N, common.MinUnknownOrderModulusBitLen) {
return false, fmt.Errorf("fac proof verify: invalid auxiliary modulus %x", N)
}
for name, base := range map[string]*big.Int{
Expand Down
Loading
Loading