Pitfall
Missing Domain Separation When a Hash Function Is Reused
What can go wrong. A single hash function is often reused across distinct purposes inside the same protocol: Fiat-Shamir challenges for different proofs, commitments, key derivation, session-ID generation, even signatures. When the same hash is invoked for these unrelated contexts without anything distinguishing them, it lets an adversary fraudulently pass off a hash output produced honestly in one context as valid in a different context.
Security implication. Without domain separation, a hash output has no unambiguous meaning: the verifier cannot tell which protocol, proof type, session, role, or statement it belongs to. This can enable replay across sessions, cross-context confusion between related protocol steps, or Fiat-Shamir challenges that bind less transcript data than the security proof assumes. In threshold-signature implementations, these failures can let adversarial transcripts verify in the wrong context.
How to avoid. Prepend a constant-length domain-separation tag, distinct per context, to every hash invocation. The tag should encode the protocol name, the specific proof or purpose inside the protocol, a session identifier, and typically a version number.
Example
bnb-chain/tss-lib shared SHA512_256i
(Verichains TSSHOCK disclosure, CVE-2022-47931, PR #256)
Fiat-Shamir hashes need to say which execution context they belong to, and they
need an injective encoding of the transcript values. Pre-fix tss-lib was
missing both: proof challenges had no caller-supplied session/context tag, and
individual inputs were concatenated without recording their lengths.
Before v2.0.0, bnb-chain/tss-lib used a shared SHA512_256i helper for proof
challenges across Schnorr, MtA, DLN, and commitment proofs. The helper included
a block-count prefix, but no caller-supplied session/context tag and no
per-input length tag (source).
The fix (PR #256) introduced
SHA512_256i_TAGGED. The tag is supplied by the caller and is typically a
session or party/session context, not a universal proof-type tag; separation
between proof types also depends on the different statement inputs each proof
hashes. The helper hashes the tag into the state and records each input length
before hashing the transcript (source):
1// FILE: common/hash.go - bnb-chain/tss-lib v2.0.0 (fixed excerpt)
2func SHA512_256i_TAGGED(tag []byte, in ...*big.Int) *big.Int {
3 tagBz := SHA512_256(tag)
4 var data []byte
5 state := crypto.SHA512_256.New()
6 state.Write(tagBz)
7 state.Write(tagBz)
8
9 inLen := len(in)
10 inLenBz := make([]byte, 64/8)
11 binary.LittleEndian.PutUint64(inLenBz, uint64(inLen))
12 ptrs := make([][]byte, inLen)
13 for i, n := range in {
14 ptrs[i] = n.Bytes()
15 }
16 data = append(data, inLenBz...)
17
18 for i := range in {
19 data = append(data, ptrs[i]...)
20 data = append(data, hashInputDelimiter)
21 dataLen := make([]byte, 8)
22 binary.LittleEndian.PutUint64(dataLen, uint64(len(ptrs[i])))
23 data = append(data, dataLen...)
24 }
25
26 state.Write(data)
27 return new(big.Int).SetBytes(state.Sum(nil))
28}