Common MPC Pitfalls

Schnorr PoK in bnb-chain/tss-lib

The Schnorr PoK in bnb-chain/tss-lib lets party $P_i$ prove knowledge of its secret key share $x_i$ by sending $(R = g^k, s = k + c \cdot x_i)$ where $c$ is a Fiat-Shamir challenge. In v1.x the challenge was derived solely from the public key and the commitment (source):

 1// FILE: crypto/schnorr/schnorr_proof.go — bnb-chain/tss-lib v1.3.5 (vulnerable)
 2
 3// NewZKProof constructs a new Schnorr ZK proof of knowledge of the discrete logarithm (GG18Spec Fig. 16)
 4func NewZKProof(x *big.Int, X *crypto.ECPoint) (*ZKProof, error) {
 5    if x == nil || X == nil || !X.ValidateBasic() {
 6        return nil, errors.New("ZKProof constructor received nil or invalid value(s)")
 7    }
 8    ec := X.Curve()
 9    ecParams := ec.Params()
10    q := ecParams.N
11    g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) // already on the curve.
12
13    a := common.GetRandomPositiveInt(q)
14    alpha := crypto.ScalarBaseMult(ec, a)
15
16    var c *big.Int
17    {
18        // Challenge includes only public key X and commitment alpha — no session ID,
19        // no party identity, no protocol context.
20        cHash := common.SHA512_256i(X.X(), X.Y(), g.X(), g.Y(), alpha.X(), alpha.Y())
21        c = common.RejectionSample(q, cHash)
22    }
23    t := new(big.Int).Mul(c, x)
24    t = common.ModInt(q).Add(a, t)
25
26    return &ZKProof{Alpha: alpha, T: t}, nil
27}

As described in CVE-2022-47930, the Schnorr proof of knowledge does not utilize a session id, context, or random nonce in the generation of the challenge. This allows a malicious party to replay a proof generated by an honest party. (The CVE record names the IoFinnet fork as the affected product, but the same flaw and PR #256 fix apply to the bnb-chain upstream shown here.) The fix (PR #256) added a Session []byte parameter prepended to every proof challenge via the domain-separating SHA512_256i_TAGGED (source):

 1// FILE: crypto/schnorr/schnorr_proof.go — bnb-chain/tss-lib v2.0.0 (fixed)
 2
 3// NewZKProof constructs a new Schnorr ZK proof of knowledge of the discrete logarithm (GG18Spec Fig. 16)
 4func NewZKProof(Session []byte, x *big.Int, X *crypto.ECPoint) (*ZKProof, error) {
 5    if x == nil || X == nil || !X.ValidateBasic() {
 6        return nil, errors.New("ZKProof constructor received nil or invalid value(s)")
 7    }
 8    ec := X.Curve()
 9    ecParams := ec.Params()
10    q := ecParams.N
11    g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) // already on the curve.
12
13    a := common.GetRandomPositiveInt(q)
14    alpha := crypto.ScalarBaseMult(ec, a)
15
16    var c *big.Int
17    {
18        // Session is prepended via the domain-separating tagged hash, binding the
19        // challenge to the protocol session (and, by convention, the participant set).
20        cHash := common.SHA512_256i_TAGGED(Session, X.X(), X.Y(), g.X(), g.Y(), alpha.X(), alpha.Y())
21        c = common.RejectionSample(q, cHash)
22    }
23    t := new(big.Int).Mul(c, x)
24    t = common.ModInt(q).Add(a, t)
25
26    return &ZKProof{Alpha: alpha, T: t}, nil
27}