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}