Signing
August 25, 20265 min readbeginner
This is the heart of the scheme, and it is a loop. Everything in it is either the Schnorr skeleton from 02-Schnorr-and-Fiat-Shamir, the rejection rule from 04-Rejection-Sampling…
This is the heart of the scheme, and it is a loop. Everything in it is either the Schnorr skeleton from Schnorr and Fiat-Shamir, the rejection rule from Rejection Sampling, or Fiat-Shamir With Aborts, or machinery for keeping the signature small.
01.The rounding gadgets
Two small routines are needed first. Both are pure integer arithmetic.
With bucket width , decompose each as
and define and .
So says which bucket of width a value falls in, and says where inside that bucket. Knowing only the high bits locates a value to within , which is coarse but often enough.
Both must be branchless, because they are applied to secret-dependent values and a data-dependent branch would leak through timing or cache behaviour.
02.The loop
S1. Compute the message digest. , where was precomputed in key generation.
Then repeat the following until it succeeds.
S2. Commit. Sample with coefficients uniform on , and compute
S3. Take the high bits. .
Only the coarse part is committed to. Sending the whole of would be wasteful, and the verifier only needs to be able to recompute the same coarse value.
S4. Challenge. , and then , which places exactly coefficients of at pseudorandom positions and leaves the rest zero.
This is Fiat-Shamir: the challenge is a hash of the commitment and the message, so the signer cannot choose it.
S5. Respond.
S6. The rejection checks. Restart if any of these fails.
. This is the zero-knowledge check from Rejection Sampling, or Fiat-Shamir With Aborts. It is what makes the released uniform on a box that does not depend on .
. This one is not about secrecy at all. It is about the verifier being able to reproduce , and it keeps every coefficient a margin of away from a bucket boundary so that a small later perturbation cannot push it across.
. This bounds the discrepancy created by truncating the public key in Key Generation, guaranteeing it can move a coefficient by at most one bucket.
S7. Build the hint. Compute a one-bit flag per coefficient recording whether adding changes the bucket:
Restart if more than bits are set.
S8. Emit.
03.The three checks, separated
It is easy to read step S6 as one blob of conditions. They serve three distinct purposes and it is worth keeping them apart.
The first check exists so that the signature reveals nothing. Without it the scheme leaks the key, as note 3 demonstrated.
The second exists so that verification is possible at all. It guarantees the signer's can be reconstructed from what the verifier will have.
The third exists so that the hint stays well defined. It caps the damage from public-key truncation at one bucket, which is what makes a single bit per coefficient sufficient to describe it.
Only the first is about security. The other two are the price of the two size optimisations, committing to high bits only and truncating the public key.
04.The hint vector
The hint deserves its own explanation because it is the least obvious piece of the scheme.
The verifier will reconstruct something close to , but off by , because it holds only . It then takes high bits. Almost always the perturbation is too small to change which bucket a coefficient sits in, and the verifier gets the right answer. Occasionally a coefficient was near a boundary and the perturbation pushes it over.
The hint is a list of exactly which coefficients that happened to. One bit each, across coefficients.
Nearly all of those bits are zero, so serialising the hint as a bitstring would waste space. Instead it is stored as the positions of the set bits, of which there are at most , which keeps it under a hundred bytes.
The check "restart if more than bits are set" is what makes that encoding safe. Without it a signature could occasionally need more positions than the format allows.
The verifier applies , which returns the high bits as computed if the bit is zero, and the adjacent bucket if it is one.
05.What signing costs
Per attempt: one matrix-vector product over , which is ring multiplications, plus two smaller products and , plus a hash.
Expected attempts: four to six, from the rejection rate in Rejection Sampling, or Fiat-Shamir With Aborts.
So signing costs roughly five times a verification, and its running time varies from one invocation to the next. Both facts are unusual coming from ECDSA, and both matter for anybody building hardware: the signing datapath needs the same NTT engine as everything else, but it needs to be scheduled for a variable number of passes.
One property is worth stating explicitly because it looks alarming and is not. The number of restarts is public, observable in timing, and reveals nothing. Each attempt uses a fresh , and whether it passes depends on and on the resulting , not on any fixed property of . An observer counting restarts learns about the discarded randomness, which is discarded.