$\newcommand{\coinheads}{\mathsf{HEADS}}$ $\newcommand{\cointails}{\mathsf{TAILS}}$ $\newcommand{\varalice}{\class{var var_Alice}{\text{Alice}}}$ $\newcommand{\varbob}{\class{var var_Bob}{\text{Bob}}}$ $\newcommand{\alicebob}[3]{#1 & \ra{#2} & #3\\[-5pt]}$ $\newcommand{\bobalice}[3]{#1 & \la{#2} & #3\\[-5pt]}$ $\newcommand{\alicework}[1]{#1 & &\\[-5pt]}$ $\newcommand{\bobwork}[1]{ & & #1\\[-5pt]}$ $\newcommand{\work}[2]{#1 & & #2\\}$ $\newcommand{\allwork}[1]{ & #1 & \\}$ $\newcommand{\dupwork}[1]{#1 & & #1\\}$ $\newcommand{\aliceseparator}{-------&&\\}$ $\newcommand{\bobseparator}{&&-------\\}$ $\newcommand{\foo}{\phantom{\text{bigarrowfitsallthis}}}$ $\newcommand{\ra}[1]{% \vphantom{\xrightarrow{asd}}% \smash{\xrightarrow[\foo]{#1}}% }$ $\newcommand{\la}[1]{% \vphantom{\xleftarrow{asd}}% \smash{\xleftarrow[\foo]{#1}}% }$ $\newcommand{\z}[1]{\mathbb{Z}_{#1}}$ $\newcommand{\zq}{\mathbb{Z}_\varq}$ $\newcommand{\zqs}{\mathbb{Z}_q^\ast}$ $\newcommand{\zps}{\mathbb{Z}_p^\ast}$ $\newcommand{\zns}[1]{\mathbb{Z}_{#1}^\ast}$ $\require{action} \newcommand{\sampleSymb}{ {\overset{\$}{\leftarrow}} }$ $\newcommand{\field}[1]{\mathbb{F}_{#1}}$ $\newcommand{\sample}[1]{#1\sampleSymb\zq}$ $\newcommand{\sampleGeneric}[2]{#1\sampleSymb#2}$ $\newcommand{\sampleInterval}[2]{#1\sampleSymb\interval{#2}}$ $\newcommand{\sampleRange}[2]{#1\sampleSymb\range{#2}}$ $\newcommand{\sampleCgroup}[1]{#1\sampleSymb\cgroup}$ $\newcommand{\samplezqs}[1]{\class{hover}{#1\sampleSymb\zqs}}$ $\newcommand{\sampleN}[2]{\class{hover}{#1\sampleSymb\z{#2}}}$ $\newcommand{\sampleNs}[2]{\class{hover}{#1\sampleSymb\z{#2}^\ast}}$ $\newcommand{\equalQ}{\overset{?}{=}}$ $\newcommand{\gQ}{\overset{?}{>}}$ $\newcommand{\inQ}{\overset{?}{\in}}$ $\newcommand{\cgroup}{\mathbb{G}}$ $\newcommand{\Hash}{\mathsf{Hash}}$ $\newcommand{\hash}[1]{\Hash({#1})}$ $\newcommand{\HashToField}{\mathsf{HashToField}}$ $\newcommand{\hashtofield}[1]{\HashToField({#1})}$ $\newcommand{\HashToGroup}{\mathsf{HashToGroup}}$ $\newcommand{\hashtogroup}[1]{\HashToGroup({#1})}$ $\newcommand{\hashbit}[2]{\mathsf{Hash}({#1})\verb+[0:#2]+}$ $\newcommand{\hmac}[2]{\mathsf{HMAC}_{#1}\left(#2\right)}$ $\newcommand{\naturals}{\mathbb{N}}$ $\newcommand{\sqfree}{L_\mathsf{square-free}}$ $\newcommand{\ceil}[1]{\lceil #1 \rceil}$ $\newcommand{\sampleSet}[2]{\class{hover}{#1\sampleSymb#2}}$ $\newcommand{\bunch}[1]{\{ #1_i\}_{i=1}^m}$ $\newcommand{\bunchi}[1]{\{ #1\}_{i=1}^m}$ $\newcommand{\forb}{\text{ for }i=1,\ldots,m}$ $\newcommand{\interval}[1]{[0, #1[}$ $\newcommand{\range}[1]{[#1]}$ $\newcommand{\rangeone}[1]{\{1, \dots,#1 -1 \}}$ $\newcommand{\vara}{\class{var var_a}{a}}$ $\newcommand{\varb}{\class{var var_b}{b}}$ $\newcommand{\varc}{\class{var var_c}{c}}$ $\newcommand{\vard}{\class{var var_d}{d}}$ $\newcommand{\varh}{\class{var var_h}{h}}$ $\newcommand{\varH}{\class{var var_H}{H}}$ $\newcommand{\varg}{\class{var var_g}{g}}$ $\newcommand{\varG}{\class{var var_G}{G}}$ $\newcommand{\vari}{\class{var var_i}{i}}$ $\newcommand{\varj}{\class{var var_j}{j}}$ $\newcommand{\vars}{\class{var var_s}{s}}$ $\newcommand{\vart}{\class{var var_t}{t}}$ $\newcommand{\varu}{\class{var var_u}{u}}$ $\newcommand{\varU}{\class{var var_U}{U}}$ $\newcommand{\varl}{\class{var var_l}{l}}$ $\newcommand{\varm}{\class{var var_m}{m}}$ $\newcommand{\varn}{\class{var var_n}{n}}$ $\newcommand{\varx}{\class{var var_x}{x}}$ $\newcommand{\varX}{\class{var var_X}{X}}$ $\newcommand{\varz}{\class{var var_z}{z}}$ $\newcommand{\varr}{\class{var var_r}{r}}$ $\newcommand{\varq}{\class{var var_q}{q}}$ $\newcommand{\varp}{\class{var var_p}{p}}$ $\newcommand{\vare}{\class{var var_e}{e}}$ $\newcommand{\vary}{\class{var var_y}{y}}$ $\newcommand{\varv}{\class{var var_v}{v}}$ $\newcommand{\varw}{\class{var var_w}{w}}$ $\newcommand{\varC}{\class{var var_C}{C}}$ $\newcommand{\varf}{\class{var var_f}{f}}$ $\newcommand{\varA}{\class{var var_A}{A}}$ $\newcommand{\varB}{\class{var var_B}{B}}$ $\newcommand{\varC}{\class{var var_C}{C}}$ $\newcommand{\varL}{\class{var var_L}{L}}$ $\newcommand{\varP}{\class{var var_P}{P}}$ $\newcommand{\varR}{\class{var var_R}{R}}$ $\newcommand{\varT}{\class{var var_T}{T}}$ $\newcommand{\varX}{\class{var var_X}{X}}$ $\newcommand{\varalpha}{\class{var var_alpha}{\alpha}}$ $\newcommand{\varprover}{\class{var var_Prover}{\text{Prover}}}$ $\newcommand{\varprover}{\class{var var_Prover}{\text{Prover}}}$ $\newcommand{\varverifier}{\class{var var_Verifier}{\text{Verifier}}}$ $\newcommand{\varN}{\class{var var_N}{N}}$ $\newcommand{\rhovar}{\class{var var_ρ}{\rho}}$ $\newcommand{\sigmavar}{\class{var var_σ}{\sigma}}$ $\newcommand{\thetavar}{\class{var var_θ}{\theta}}$ $\newcommand{\muvar}{\class{var var_μ}{\mu}}$ $\renewcommand{\vec}[1]{\mathbf{#1}}$ $\newcommand{\veca}{\vec{\class{var var_vec_a}{a}}}$ $\newcommand{\vecb}{\vec{\class{var var_vec_b}{b}}}$ $\newcommand{\vecc}{\vec{\class{var var_vec_c}{c}}}$ $\newcommand{\vecs}{\vec{\class{var var_vec_s}{s}}}$ $\newcommand{\vecG}{\vec{\class{var var_vec_G}{G}}}$ $\newcommand{\vecH}{\vec{\class{var var_vec_H}{H}}}$ $\newcommand{\vecg}{\vec{\class{var var_vec_g}{g}}}$ $\newcommand{\vech}{\vec{\class{var var_vec_h}{h}}}$ $\newcommand{\true}{\mathsf{true}}$ $\newcommand{\false}{\mathsf{false}}$ $\newcommand{\ctx}{\mathsf{ctx}}$ $\newcommand{\coloneqq}{≔}$ $\newcommand{\ip}[2]{\left\langle #1, #2 \right\rangle}$ $\newcommand{\uwork}[2]{\underline{#1} & & \underline{#2}\\}$ $\newcommand{\aliceworks}[1]{#1 & &\\[-2pt]}$ $\newcommand{\bobworks}[1]{ & & #1\\[-2pt]}$ $\newcommand{\Halving}{\text{Halving}}$ $\newcommand{\HalveProof}{\text{HalveProof}}$ $\newcommand{\HalveVerify}{\text{HalveVerify}}$ $\newcommand{\indent}{\qquad}$ $\newcommand{\append}{\mathrm{append}}$ $\newcommand{\schnorrvalidate}{\mathsf{schnorr}\_\mathsf{validate}}$
Fiat-Shamir transformation

Fiat-Shamir transformation #

Overview #

It turns out that in practice, most zero-knowledge proofs tend to have the same three-step structure:

  1. The prover first generates some random value, the commitment, and sends it to the verifier.
  2. The verifier responds with a challenge value generated uniformly at random.
  3. The prover computes the final proof based on both the commitment and challenge.

As you can tell, this structure is interactive, meaning that the prover requires a response from the verifier before they can complete their proof, which is not ideal for most applications. Fortunately, provers can avoid this by using the Fiat-Shamir heuristic (sometimes referred to as the Fiat-Shamir transformation), developed by Amos Fiat and Adi Shamir.

The idea behind the Fiat-Shamir transformation is that instead of having the verifier send a random challenge value to the prover, the prover can compute this value themselves by using a random function, such as a cryptographic hash function.

Example: In the Schnorr protocol, we have an interactive and non-interactive version. In the interactive version, the prover sends their random commitment, $u = g^r$, and the verifier responds with a challenge value, $c$, that they generate uniformly at random. In the non-interactive version, the prover generates $c$ themselves by computing a hash over all of the public values, $c = \hash{g,q,h,u}$. As we will discuss, the hash must include all of these values.

If you’d like to read more about this, we’ve written a blog post describing how they work in more detail.

What can go wrong? #

This transformation may seem straightforward, but unfortunately, it tends to be very tricky in practice. In particular, the prover generates the random challenge value using a cryptographic hash function- but what are the inputs? It turns out that if you choose the wrong inputs, it usually means your proof system is broken. To see this, let’s look at Schnorr’s protocol as an example.

Bad example, NEVER DO THIS: Recall that in the Schnorr protocol, the prover generates their random commitment, $u = g^r$, computes the challenge value, $c$, and then computes the final proof $z = r + x\cdot c$, where $x$ is the secret value corresponding to their public key, $h = g^x$. Let’s say the implementation is incorrect, and the challenge value, $c$, is not computed using the public key, $h$, and instead it’s computed as $c = \hash{g,q,u}$. It turns out that malicious provers can now forge proofs by doing the following attack:

  • Set the commitment value, $u$, to be some public key that you do not know the secret key for.
  • Compute the challenge value $c = \hash{g,q,u}$.
  • Set the proof $z$ to be a random value, and then compute your public key to be $h = (\frac{g^z}{u})^{\frac{1}{c}}$
  • Send $u,c,z$ to the verifier.

The verifier then performs their two checks: $c \equalQ \hash{g,q,u}$ and $g^z \equalQ u\cdot h^c$. The first check will pass as they are both the same, and the second check will pass because of how we constructed $z$, $u$, and $h$.

Since we set $u$ to be some value we do not know the secret key for, we also will not know the secret key for $h$, which means we have forged our proof of knowledge.

Note: This attack does not allow you to forge proofs for any public key, this only works for random public keys. This tends not to be problematic when Schnorr proofs as a proof of knowledge in the typical public-key model. However, this can be very problematic when these proofs are used in other scenarios, such as a cryptographic voting system.

Recommendations #

Rule of thumb #

The exact inputs required for the hash function will be different for every zero-knowledge proof system. The general rule of thumb to follow is to include all of the public information and all elements in the proof transcript up until that point inside the hash function.

In the Schnorr protocol, the public information is the public key, $h$, and the parameters related to the group being used, $g$ and $q$, and the proof transcript also includes the commitment value, $u$, and so we include all of those values in our hash computation.

For short factoring proof protocol, it’s a bit more complicated. The public information is the number the prover can factor, $N$, so this must be included. What about transcript values? In this scheme, the prover does a bit more work before computing the challenge compared to the Schnorr protocol. Specifically, they generate a random value ($r$), a series of values ($z_i$) using the nothing-up-my-sleeve construction, and then they compute $X = \hash{\bunchi{z_i^r \mod N}}$ (this is not the Fiat-Shamir challenge). Since both the $X$ value and the set of $z_i$ values will be known to the verifier (i.e., they are part of the transcript), these must also be included in the hash computation. So, the correct challenge computation is $e = \hash{N, \bunchi{z_i}, X}$.

Note: In the short factoring proof and in Girault’s scheme, the Fiat-Shamir challenge needs to additionally be of a specific bit-size. So, the correct computation is actually $e = \hashbit{N, \bunchi{z_i}, X}{k}$.

Remember: Always include all public information and all transcript values. When in doubt, consult ZKDocs!

Preventing replay attacks #

Just like digital signatures, zero-knowledge proofs can also be susceptible to replay attacks. As is the case for other replay attacks, the severity of these replay attacks will be highly application and context-specific.

If you believe replay attacks could be severe for your application, you might be able to use the Fiat-Shamir transformation to protect yourself. Specifically, suppose your application has some notion of identity tied to each party (a unique party ID, for example). In that case, you should include the ID of the prover and the verifier inside of the Fiat-Shamir hash computation. Then, when the verifier verifies the proof, they should also check that the IDs used in the hash function match the ID of themself and the prover. This will prevent other malicious parties from replaying any proof that they did not produce themselves.

References #