Pitfall

Challenge Hash Missing Prover's Party Identity and Session Identifier

What can go wrong. In the Fiat-Shamir transformation, the verifier’s challenge is replaced by a challenge hash that, in the single-prover, single-session case, depends only on the public statement and the prover’s commitment. In a multi-prover or multi-session setting this is not enough, and the hash must also bind to the prover’s party identifier (pid) and to the session identifier (ssid). If the pid is missing, nothing in the hash input identifies which prover computed it, so honest $P_i$ and malicious $P_m$ obtain the same challenge on the same statement and commitment within a single session. A proof $\pi_i$ produced by $P_i$ can then be replayed verbatim by $P_m$, who claims knowledge of the underlying witness without ever holding it. If the ssid is missing, the hash produces the same challenge value across every session running the same statement. Two invocations of the proof, one in key-generation session $A$ and another in signing session $B$, differ only in the surrounding protocol context, which the hash does not see. The proof bytes from session $A$ therefore remain structurally valid in session $B$, allowing replay across sessions.

Security implication. In a DKG (Distributed Key Generation) protocol, a malicious party $P_m$ can adaptively choose its public-key to match an honest party $P_i$’s ($X_m = X_i$). The malicious party then records $P_i$’s Schnorr proof and submits it as its own round contribution, passing the proof-of-knowledge check without holding any secret. The malicious party can also reuse it in later sessions.

How to avoid. Include the prover’s party identifier (pid, public key, or protocol-assigned role) in every FS challenge hash and derive a session identifier ssid from every public parameter of the current run. In practice many libraries fold the party identifier into the ssid derivation (the participant set is included in ssid).

Example Schnorr PoK in bnb-chain/tss-lib (CVE-2022-47930, PR #256)

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}