* [1]Henry de Valence
* [2]Menu
*
* [3]Blog
*
* [4]Projects
*
* [5]Github
*
* [6]Twitter
*
* [7]Email
*
* [8]RSS
*
It’s 255:19AM. Do you know what your validation criteria are?
A basic property of a [9]digital signature scheme is that it should
specify which signatures are valid and which signatures are invalid, so
that all implementations can accept only valid signatures and reject
only invalid signatures. Unfortunately, Ed25519 signatures don’t
provide this property, making their use in distributed systems fragile
and risky.
Although the scheme was standardized in [10]RFC8032, the RFC does not
specify validation criteria, and does not require conformant
implementations to agree on whether a particular signature is valid. In
addition, because the specification changed validation criteria years
after deployment, it is incompatible with almost all existing
implementations. Worse still, some implementations added extra ad-hoc
criteria, making them further incompatible.
The result is an extremely wide variation in validation criteria across
implemententations. The diagram below plots verification results of a
\(14 \times 14\) grid of edge cases, with light squares representing
accepted signatures, and dark squares representing rejected ones. As
the diagram illustrates, verification results are generally
inconsistent not just between implementations, but also between
different versions and different modes. [ed25519-comparison.png]
Some protocols may tolerate this variation, but it is unacceptable for
any protocol that requires participants to reach consensus on signature
validity. A malicious participant can submit signatures that are
accepted by some implementations but rejected by others, causing a
network partitition or a consensus fork. Having only a single
implementation makes this problem less obvious, but it doesn’t go away,
since behavior can vary across versions of the ‘same’ implementation.
This occurred in practice with the widely used libsodium library, which
made breaking changes to validation criteria in a point release.
Finally, although all of these problems occur when verifying signatures
individually, they also prevent the use of batch verification, which
can provide a significant performance benefit.
This post describes:
1. The structure of Ed25519 signatures and the scope of potential
divergence in validation criteria [11](jump);
2. The results of a survey of Ed25519 validation behavior [12](jump),
which revealed:
+ that almost no implementations conform to RFC8032;
+ that there is a wide variation in behavior not just between
implementations, but also across different versions of the
same implementation;
+ a platform-specific bug in Go’s crypto/ed25519 that gave
different validation results on the IBM z/Architecture;
+ a crash denial-of-service bug in Tor triggered by validating
attacker-controlled signatures (though this bug was
coincidentally not exploitable because of the way the function
is currently used).
3. The ZIP215 rules [13](jump), a set of precisely defined validation
criteria for consensus-critical Ed25519 signatures which resolve
this problem. These rules are implemented in [14]ed25519-zebra in
Rust and and [15]ed25519consensus in Go, are backwards-compatible
with existing signatures, and will be deployed in Zcash as part of
the ‘Canopy’ network upgrade.
The scope of potential divergence
The Ed25519 signing process
Before explaining the validation criteria, it’s useful to briefly
review the signing process. An honest signer generates their signing
key, a random scalar \(a\), through a complicated procedure not
relevant here. Then, they multiply their signing key by the Curve25519
basepoint \(B\) to obtain their verification key, a curve point \(A =
[a]B\). The signing and verification keys are more commonly referred to
as the private and public keys, but (following Daira Hopwood), I prefer
to use the capability-based terminology, since it more precisely
captures the role of the key material.
To sign a message M, a signer first generates a secret random nonce
\(r\), through a procedure that is also not relevant here, and then
form a public commitment to this randomness by computing \(R = [r]B\).
Next, they use a hash function \(H\) to compute a challenge scalar as
\( k \gets H(R, A, M) \). Finally, they compute a response scalar as \(
s \gets r + ka \).
Divergence in Ed25519 signature validation criteria
To understand the scope of potential divergence in validation criteria,
let’s walk through the steps to verify a signature sig with
verification key \(A\) on the message M, noting each step with a
possibly divergent behavior choice. Then, in subsequent sections, we’ll
look at the scope and implications of each divergence.
Ed25519 signatures are 64 bytes long, structured as two 32-byte
components: sig = R_bytes || s_bytes. The first 32 bytes store an
encoding of \(R\), and the second 32 bytes store an encoding of \(s\).
Ed25519 verification keys are 32 bytes long, storing an encoding of
\(A\).
Next, the verifier parses s_bytes as \(s\). It’s always possible to
interpret a byte string as a little-endian encoded integer, so parsing
\(s\) can’t fail. However, \(s\) is supposed to represent an integer
\(\mod q\), where \(q\) is the order of the prime-order subgroup of
Curve25519. Honestly-generated signatures will have \(0 \leq s < q\),
and implementations can choose to reject \(s \ge q\). Call this check
canonical \(s\).
Next, the verifier attempts to parse A_bytes as \(A\). Not all byte
strings are valid point encodings, so parsing \(A\) can fail, causing
the signature to be rejected. Points can be encoded non-canonically,
although in contrast to the encoding of \(s\), the mechanism is
somewhat more complicated and subtle, as will be described [16]below.
Implementations can choose to reject non-canonically encoded curve
points. Call this check canonical \(A\).
The verifier then uses A_bytes, R_bytes, and the message M to recompute
the challenge value \(k \gets H(R, A, M)\). (Note that since \(H\) is a
hash function, it actually operates on the encodings A_bytes and
R_bytes, not their decoded internal representations).
Implementations then make a choice of verification equation, choosing
which of two verification equations to check. They can use either the
batched equation \[ [8]R = [8]([s]B - [k]A), \] or the unbatched
equation \[ R = [s]B - [k]A. \] The naming of batched equation versus
unbatched equation suggests that the difference is related to batched
versus individual verification, but in fact these give different
behavior even in the case of individual verification, as will be
explained [17]below.
Finally, to actually check whichever equation is used, an
implementation must choose an equality check. Recall that we didn’t
actually decode R_bytes to \(R\) yet. When using the batched equation,
an implementation must operate on \(R\), so it must decode R_bytes to
\(R\), and check equality of curve points. This also means it must make
a choice of whether to require canonical \(R\).
When using the unbatched equation, however, an implementation can
choose to either check equality of curve points, or to compute \(R’
\gets [s]B - [k]A\), encode \(R’\) to Rprime_bytes, and check equality
of byte strings. When \(R\) is canonically encoded, these equality
checks produce the same result. But because the encoding procedure
produces canonical encodings, if R_bytes contains a non-canonical
encoding of \(R\), then even if \(R’ = R\) (as curve points),
Rprime_bytes may differ from R_bytes (as byte strings).
Points of potential divergence
In summary, there are a number of points of potential divergence
between implementations:
1. Whether to require canonical \(s\) or to allow non-canonical
encodings.
2. Whether to require canonical \(A\) or to allow non-canonical
encodings.
3. The choice of verification equation, either batched or unbatched.
4. The choice of equality check, and the related choice of canonical
\(R\).
5. Any other ad-hoc checks added by a particular implementation.
Before comparing the choices made by RFC8032 and actually existing
implementations, let’s examine the exact mechanism and implications of
each of these points of potential divergence.
Canonical \(s\)
The values A_bytes, R_bytes, and M are all fed into the hash function,
so they cannot be modified without changing the challenge value.
However, a third party could replace \(s\) with \(s’ = s + nq\).
Because \(s’ \equiv s \pmod q\), the modified signature \((R, s’)\)
would still pass verification. Requiring that s_bytes encodes \(s < q\)
prevents this.
This check is simple, low-cost, prevents malleability, is required by
RFC8032, and is performed by most implementations. The only notable
exception is the original reference implementation of Ed25519, which
chose to write a paragraph arguing that signature malleability is never
a problem instead of performing the check.
Because this check is fairly common, well-known, and unobjectionable, I
didn’t focus on it, and didn’t comprehensively test implementation
behavior.
Canonical \(A\), \(R\)
While the mechanism for constructing non-canonically encoded scalar
values is fairly simple, the mechanism for constructing non-canonically
encoded point values is somewhat more complicated, as mentioned
[18]above. To explain it, we need to describe the “compressed Edwards
\(y\)” encoding used by Ed25519.
Curve25519 is defined over the finite field of order \(p = 2^{255} -
19\), so the coördinates of curve points \((x,y)\) are integers \(\mod
p\). The curve equation in (twisted) Edwards form is \[ - x^2 + y^2 = 1
+ d x^2 y^2, \] where \(d\) is a curve parameter. This means that \[
x^2 = \frac { y^2 - 1} {d y^2 + 1}. \] If the fraction on the
right-hand side is nonsquare, then there is no \(x\) so that the
right-hand side equals \(x^2\), and the \(y\) value is not the
\(y\)-coördinate of a curve point. If it is square, then there is a
square root \(x\), and the value of \(y\) is sufficient to recover
\(x\) up to a choice of sign.
Because \(p < 2^{255}\), the encodings of field elements fit in 255
bits. The compressed Edwards \(y\) format uses the first 255 bits to
store the \(y\) coördinate, and the 256th bit to indicate the sign of
the \(x\) coördinate. To decode this format, an implementation parses
\(y\) and the sign bit, and attempts to recover \(x\). If the square
root exists, it uses the sign bit to select the sign. Otherwise,
decoding fails.
The encoding has two components, and so there are two potential ways to
construct a non-canonical encoding. First, we could try to use a
non-canonical encoding of \(y\). Second, we could select \(y\) so that
both sign choices give the same \(x\) value.
In the first case, encoding a field element non-canonically requires
encoding \(y\) as \(y + p\). But because field elements are encoded in
255 bits and \(p = 2^{255} - 19\), this is only possible for the 19
values \( y = 0 \ldots 18 \) that can be encoded as \(y + p < 2^{255}
\). Not all of these values are \(y\)-coördinates of points, but
because there are only a few candidates, we can check every combination
of enc(y) || 0 and enc(y) || 1 to enumerate them.
In the second case, we need \(x = -x\). This forces \(x = 0\). The
curve equation is \[ - x^2 + y^2 = 1 + d x^2 y^2, \] so this requires
that \( y^2 = 1 \), giving \( y = 1 \) and \(y = -1\) as the two
candidate \(y\) values.
* When \(y = -1\), \(y\) can only be canonically encoded, so the
potential encodings are:
+ enc(-1) || 0 (canonical);
+ enc(-1) || 1 (non-canonical).
* When \(y = 1\), \(y\) can be encoded non-canonically, so the
potential encodings are:
+ enc(1) || 0 (canonical);
+ enc(1) || 1 (non-canonical);
+ enc(2^255 - 18) || 0 (non-canonical, included above);
+ enc(2^255 - 18) || 1 (non-canonical, included above).
This provides a mechanism for constructing non-canonically encoded
points, which I implemented in the [19]test suite for
[20]ed25519-zebra. In total, there are 25 such points. Of these, 6 are
of small order and are useful for constructing signature edge cases, as
described in the next section. (This taxonomy was created with pointers
from Sean Bowe and NCC Group).
Choice of verification equation
Recall that implementations can use one of two verification equations,
the batched equation \( [8]R = [8]([s]B - [k]A) \), or the unbatched
equation \( R = ([s]B - [k]A) \). The only difference between these two
equations is that the first one multiplies by \(8\). For honestly
generated signatures, \(s = r + ka\), \(A = [a]B\) and \(R = [r]B\), so
\[ \begin{aligned} [s]B - [k]A &= [r + ka]B - [k]A \\ &= [r]B +
[k]([a]B) - [k]A \\ &= R + [k]A - [k]A \\ &= R \end{aligned} \] and
both equations are satisfied. So, why are there two verification
equations, and what is their relationship to batch verification?
To explain this, we need to know some facts about the structure of the
Curve25519 group. Many abstract cryptographic protocols require an
implementation of a group of large prime order \( q \). This is usually
provided using an elliptic curve, such as Curve25519, where \(q \sim
2^{252}\). However, Curve25519 and other curves that can be written in
Edwards form don’t have prime order. Instead, they provide a group of
order \( hq \) for a small cofactor \(h\), such as \(h = 8\) in the
case of Curve25519. More information on [21]cofactors can be found on
the [22]Ristretto website.
This means that in addition to the prime-order subgroup, Curve25519
also contains a “torsion” subgroup of small-order points. The structure
of the full Curve25519 group is \(\mathbb Z / q \mathbb Z \times
\mathbb Z / 8 \mathbb Z \), meaning that every point \(P\) in the full
group can be written uniquely as the sum \(Q + T\) of a point in the
prime-order subgroup (call this the prime-order component) and a point
in the torsion subgroup (call this the torsion component).
When we multiply \(P\) by \( 8 \), the prime-order component will be
\([8]Q\). Since \(8\) and \(q\) are coprime, we could recover \(Q\)
from \([8]Q\) by multiplying by \(1/8 \pmod q\), so this “shifts” the
prime-order component around but doesn’t lose any structure. However,
because every point in the torsion subgroup has order dividing \(8\),
\([8]T = 0\), and the torsion component is killed by the
multiplication-by-\(8\) map.
What does this mean for the two verification equations? Because
multiplication by \(8\) kills the torsion component, the batched
equation can be thought of as checking validity only in the prime-order
subgroup, while the unbatched equation can be thought of as checking
validity in the full group. Honest Ed25519 signers only work in the
prime-order subgroup, so the two verification equations give the same
results for honestly-generated signatures.
But nothing stops a signer from deviating from the protocol, and
generating a verification key with nonzero torsion component. As one
concrete example, they can choose a torsion point \(T\) of order \(8\),
and publish \(A’ = [a]B + T\) as their verification key instead of \(A
= [a]B\). If they sign messages as normal, they’ll choose a random
\(r\), generate \( R \gets [r]B \) and \( s \gets r + ka \). Now,
consider what happens to \([s]B - [k]A’\): \[ \begin{aligned} [s]B -
[k]A’ &= [r + ka]B - [k](A + T) \\ &= [r]B + [k]A - [k]A - [k]T \\ &= R
- [k]T. \end{aligned} \]
A verifier using the batched equation will accept the signature if
\([8]R = [8]([s]B - [k]A’)\). Since \([s]B - [k]A’ = R - [k]T\) and
\([8]T = 0\), \[ [8]([s]B - [k]A’) = [8]R - [8][k]T = [8]R \] and the
batched equation is always satisfied.
However, a verifier using the unbatched equation will accept the
signature only if \([k]T = 0\), which happens whenever \(8\) divides
\(k\). Since \(k = H(R, A, M)\) is a uniformly distributed hash output,
the signer has a \(1/8\) chance of generating a signature that passes
unbatched verification.
Batch verification
We’ve now seen the difference between the verification equations. To
understand the connection to batch verification, we need to describe
how batch verification works.
Batch verification asks whether all items in some set are valid, rather
than asking whether each of them is valid. This can increase throughput
by allowing computation to be shared across items. In the case of
Ed25519, batch verification works as follows. First, rearrange the
verification equation as \[ 0 = [8]R - [8][s]B + [8][k]A, \] so that
we’re checking whether the verification term on the right-hand side is
zero.
In the batch setting, we have multiple (message, key, signature) pairs,
and we want to check whether all of their verification terms are zero.
To do this, we take a linear combination of the verification terms with
coefficients \(z_i\): \[ 0 = \sum_i z_i \left( [8]R_i - [8][s_i]B +
[8][k_i]A_i \right). \] This equation will hold for all values of
\(z_i\) if and only if all of the verification terms are zero. This
would be computationally infeasible to check directly, but the verifier
can instead select random, high-entropy values for \(z_i\). Because
we’re working in a prime-order (sub)group, if any verification terms
are nonzero, they’ll remain nonzero after multiplication by \(z_i\).
Why is this preferable? The reason is that it allows the verifier to
compute a single multiscalar multiplication of multiple points and
scalars. This can be performed much more efficiently than computing
multiple individual scalar multiplications and summing the results.
In practice, the verifier uses a slightly rewritten equation, \[ 0 =
[8] \left( \left[ - \textstyle\sum_i z_i s_i \right] B + \sum_i [z_i]
R_i + \sum_i [z_i k_i] A \right). \] This coalesces the coefficients of
the basepoint into a single term, reducing the size of the multiscalar
multiplication, and does a single cofactor multiplication (three
doublings) at the end. This can easily provide a 2x or more speedup on
verification. It’s possible to go further: [23]ed25519-zebra implements
an adaptive strategy that detects reused verification keys, giving an
[24]extra 2x speedup in the limiting case where all signatures are made
with the same verification key.
However, if we tried to do this without multiplication by the cofactor
(as in the original Ed25519 paper), we’d get totally inconsistent
behavior in the presence of one or more candidate signatures with
nonzero torsion components.
First, verification results would become probabilistic, because the
random coefficients might or might not kill the torsion components.
Second, verification results might depend on which items are or aren’t
included in the batch, if small-order components from one term cancel
with small-order components from another. Third, and most surprisingly,
it’s possible to construct signatures that pass single verification but
fail (cofactorless) batch verification, even in a single-element batch!
Why is this last case surprising? If a signature satisfies the
unbatched equation \(R = [s]B - [k]A\), the verification term \( R -
[s]B - [k]A \) is zero. But (cofactorless) batch verification computes
a random linear combination of this term, and the term is zero, so how
could the combination be nonzero?
The answer lies in a subtle implementation detail. What the verifier
actually checks is not \[ 0 = z \left( R - [s]B + [k]A \right) \] but
\[ 0 = [-zs]B + [z]R + [zk]A. \] Suppose that, e.g., \(A\) had a
torsion component of order \(8\), and that \(8\) divides \(k\). Then in
the first case, the verifier computes \([k]A\) and multiplication by
\(k\) kills the torsion component of \(A\). In the second case, the
verifier computes \([zk]A\). Since \(8\) divides \(k\), we might expect
that \(8\) divides \(zk\). But every existing implementation implements
scalar arithmetic \(\mod q\), the order of the prime-order group, not
\(\mod 8q\), the order of the full group. So the verifier first
computes \(zk \mod q\), and then uses the resulting integer
representative, which might not be divisible by \(8\), to perform
scalar multiplication.
The root cause of this problem is that the scalar arithmetic in Ed25519
implementations only produces well-defined results on the prime-order
subgroup, not the full group. This is bad because it means that basic
laws of arithmetic (e.g., that rearranging terms shouldn’t change the
value of an expression) don’t apply unless we restrict to the
prime-order subgroup.
For these reasons, the only way to make batch verification produce
internally consistent results is to use the batched verification
equation that includes multiplication by the cofactor.
A survey of implementation behaviors
Ed25519 is widely implemented and used in various consensus-critical
contexts. As we’ve seen [25]above, there are in principle several
points of potential divergence between implementations. Do these occur
in practice? To find out, I collected a [26]list of systems to look at,
and checked their behavior against the test vectors I created for
ZIP215, a set of validation rules described [27]below which fix this
problem in Zcash.
Constructing test vectors
Because the potential divergence happens on edge cases, not on
honestly-generated signatures, if we want to check for divergent
behavior, we need to construct specially crafted test vectors that will
produce different behavior when implementations make different choices.
How could we do this? Let’s start by examining the unbatched equation
\[ R = [s]B - [k]A. \] When we’re constructing test cases, we have
direct control over \(R\), \(s\), and \(A\). Since \(k\) is a hash
output, we can’t control its value, but it will vary depending on our
choices. We have no control over \(B\), which is a system parameter.
Now, we know from the preceding discussion that one big source of
divergent behavior is the presence of torsion components. So, we could
set \(A\) and \(R\) to be small-order points. Because the torsion
subgroup is so small, there’s a reasonable chance of cancellation
between the \(R\) and \([k]A\) term. The remaining term is the \([s]B\)
term, which we can eliminate by setting \(s = 0\). The resulting
signature will sometimes satisfy the unbatched equation, depending on
whether \(k = H(R,A,M)\) causes \(R = -[k]A\), and will always satisfy
the batched equation, because small-order terms are killed by cofactor
multiplication.
If we choose non-canonical encodings of small-order points, then in
addition to testing the choice of verification equation, we can also
test the choice to require canonical encodings of \(A\) or \(R\), and
the choice of equality check. There are 8 small-order points, each with
a canonical encoding. There are also 6 non-canonical encodings of
small-order points. Together, this gives \(14 \times 14 = 196\) test
cases.
Survey results
The image at the top of the post and repeated here shows the results of
verifying the 196 test cases as signatures on the message b"Zcash".
Each pixel corresponds to a verification result. Light pixels represent
accepted signatures, and dark pixels represent rejected ones. In the
case of RFC8032, the medium shade represents signatures which
implementations MAY accept, if they are so inclined. Each row
corresponds to a choice of R_bytes, and each column corresponds to a
choice of A_bytes. [28][ed25519-comparison.png]
This visualization shows multiple different, mutually incompatible
“families” of verification behavior. (Click the image to open
full-size).
RFC8032
First, we can see that RFC8032 is incompatible with every
implementation tested, with the exception of Bouncycastle. The RFC,
which was written several years after wide deployment, added a
requirement that implementations MUST reject non-canonical encodings.
But no then-existing code implemented this requirement.
However, because the RFC allows either the batched or unbatched
verification, even if there were two implementations conforming to the
RFC, there would still be no guarantee of compatible behavior.
“Classic” criteria
A second family of common behavior includes implementations matching
the behavior of the ref10 reference implementation, usually because
they are derived from it in some way. This includes Go’s
crypto/ed25519, OpenSSL, ed25519-donna (as used in Tor),
ed25519-dalek’s verify, and ed25519-elisabeth.
These implementations use the unbatched verification equation, and do
not check that \(A\) is canonically encoded. However, because they
recompute \(R’\), encode it to Rprime_bytes, and check that R_bytes =
Rprime_bytes, they do check that \(R\) is canonically encoded, but only
by accident. The visualization reflects this in the fact that there are
no valid signatures in the lower 6 rows corresponding to non-canonical
encodings of \(R\).
libsodium
Another family of common behavior includes libsodium and
implementations attempting to be compatible with it. When used without
the ED25519_COMPAT build flag, libsodium uses an additional ad-hoc
validation step, checking against a hard-coded list of excluded point
encodings. This additional validation step introduces incompatibility,
but it doesn’t solve the problems related to torsion components, since
it only excludes small-order points \(T\) and not points with a
small-order component \(Q + T\).
However, the validation behavior [29]changed in a point release from
1.0.15 to 1.0.16. In 1.0.15, R_bytes is checked against the list of
excluded point encodings, while A_bytes is checked to be nonzero. This
change was a problem for Zcash, because the initial implementation of
Zcash consensus in zcashd inherited Ed25519 validation criteria from
the version of libsodium it used. As a result, zcashd was stuck using
an old version of libsodium, because upgrading would be a breaking
change to the consensus rules.
Two implementations attempt to match libsodium behavior.
ed25519-zebra’s 1.x series provides exact bug-for-bug compatibility
with libsodium 1.0.15 as used in zcashd. ed25519-dalek’s verify_strict
method matches the libsodium 1.0.16-18 behavior.
Go on IBM s/390x
The IBM z/Architecture contains [30]many interesting instructions, one
of which is the KDSA “compute digital signature authentication”, which
provides access to hardware implementations of signing and verification
for Ed25519, Ed448, and ECDSA over the NIST P256, P384, and P521
curves.
Go’s crypto/ed25519 contained a platform-specific backend that called
this hardware implementation when available. However, running these
test vectors revealed an [31]incompatibility between the software
implementation and the hardware implementation.
Batch verification
Three of the tested implementations perform batch verification:
ed25519-zebra, ed25519-dalek, and ed25519-donna. Each test case was
verified in a batch of size 1.
None of these implementations require \(R\) or \(A\) to be canonically
encoded. In the case of ed25519-dalek and ed25519-donna, this behavior
differs from the single verification mode, which accidentally requires
\(R\) to be canonically encoded.
ed25519-zebra uses the batched verification equation, so all test cases
verify. ed25519-dalek and ed25519-donna use the unbatched equation for
batch verification, so verification depends on the values of the z_i.
This produces incompatible results from each other and from their
implementation of single verification.
The incompatibility between ed25519-donna’s batch verification and its
single verification created a [32]denial-of-service bug in Tor which
allows an adversary who can control the input to batch verification to
crash the Tor process. This happens because the batch verification
implementation falls back to single verification in case of a batch
failure and performs a defensive consistency check. However, passing
signatures that verify individually but not as part of a batch causes
the consistency check to fail and trigger an assertion. Luckily, this
is not exploitable, because batch verification is only used for batches
of size greater than 3, but no caller supplies such a batch, and so the
batch verification codepath is never used.
Fixing the problem
Ed25519 signatures are widely used in consensus-critical contexts
(e.g., blockchains), where different nodes must agree on whether or not
a given signature is valid. In these contexts, incompatible validation
criteria create the risk of consensus divergence, such as a chain fork.
Mitigating this risk can require an enormous amount of effort. For
instance, the initial implementation of Zcash consensus in zcashd
inherited validation criteria from the then-current version of
libsodium 1.0.15. Due to [33]a bug in libsodium, the actual behavior
was different from the intended criteria, which had been documented in
the Zcash protocol specification, so the specification had to be
corrected to match. Because libsodium never guaranteed stable
validation criteria, and changed behavior in a later point release,
zcashd was forced to use an older version before eventually patching a
newer version. This meant that [34]Zebra, the Zcash Foundation’s Zcash
implementation had to implement [35]ed25519-zebra, a library attempting
to match libsodium 1.0.15 exactly. And the initial implementation of
ed25519-zebra was also incompatible, because it precisely matched the
wrong compile-time configuration of libsodium.
Instead of attempting to replicate implementation-defined behavior, a
better solution would be to select a precise set of validation
criteria. There are two basic choices which determine the resulting
validation criteria.
Selecting precise criteria
Should the validation criteria allow batch verification, or not? In
order to allow batch verification, batch and single verification must
always agree, which means that single verification must use the batch
equation, rather than the unbatched equation.
The only downside of using the batch equation for single verification
is that it requires an additional 3 doublings to compute the
multiplication by \(8\). This costs a bit more than 1000 Skylake
cycles, or about 1-2% of the total cost of a single verification. The
upside is that batch verification can provide a [36]speedup of 2x or
more.
Should the validation criteria be backwards-compatible, or not? Most
existing implementations don’t require canonically-encoded points. If
every signature that was considered valid by an existing implementation
is considered valid under the new rules, an implementation of the new
rules can be used in place of an existing implementation. On the other
hand, if the new rules are more strict than the previous ones, it might
be possible for someone to insert a signature valid under the old rules
and not the new ones, which could cause unexpected breakage.
There’s no downside and no security impact to allowing non-canonical
point encodings, because this choice only affects signatures created
with specially crafted verification keys. Honest participants generate
their signing and verification keys according to the protocol, and are
unaffected either way.
The ZIP215 rules
To fix this problem in Zcash, we chose to support both batch
verification and backwards compatibility. The resulting rules are
specified in [37]ZIP215, scheduled to activate as part of the Zcash
Heartwood network upgrade, implemented in Rust in [38]ed25519-zebra’s
2.x series, and implemented in Go in a new [39]ed25519consensus
library.
Although this work was motivated by the problem in Zcash, the same risk
exists in other projects. For instance, Tendermint uses Go’s
crypto/ed25519, which means that any future consensus implementations,
such as [40]tendermint-rs need to carefully consider and mitigate the
risk of consensus incompatibilities, and interoperability mechanisms
like [41]IBC that are likely to have multiple implementations need to
specify precise validation criteria. In either case, upgrading to the
ZIP215 rules may be helpful.
Posted October 4, 2020 under [42]cryptography.
References
1.
https://hdevalence.ca/
2.
https://hdevalence.ca/blog/2020-10-04-its-25519am
3.
https://hdevalence.ca/blog/
4.
https://hdevalence.ca/projects/
5.
https://github.com/hdevalence
6.
https://twitter.com/hdevalence
7. mailto:
[email protected]
8.
https://hdevalence.ca/blog/rss.xml
9.
https://en.wikipedia.org/wiki/Digital_signature
10.
https://tools.ietf.org/html/rfc8032
11.
https://hdevalence.ca/blog/2020-10-04-its-25519am#points-of-potential-divergence
12.
https://hdevalence.ca/blog/2020-10-04-its-25519am#a-survey-of-implementation-behaviors
13.
https://hdevalence.ca/blog/2020-10-04-its-25519am#fixing-the-problem
14.
https://docs.rs/ed25519-zebra
15.
https://github.com/hdevalence/ed25519consensus
16.
https://hdevalence.ca/blog/2020-10-04-its-25519am#canonical-a-r
17.
https://hdevalence.ca/blog/2020-10-04-its-25519am#choice-of-verification-equation
18.
https://hdevalence.ca/blog/2020-10-04-its-25519am#divergence-in-ed25519-signature-validation-criteria
19.
https://github.com/ZcashFoundation/ed25519-zebra/blob/main/tests/util/mod.rs#L81-L155
20.
https://docs.rs/ed25519-zebra
21.
https://ristretto.group/why_ristretto.html
22.
https://ristretto.group/
23.
https://docs.rs/ed25519-zebra
24.
https://docs.rs/ed25519-zebra/2.2.0/ed25519_zebra/batch/index.html
25.
https://hdevalence.ca/blog/2020-10-04-its-25519am#points-of-potential-divergence
26.
https://twitter.com/hdevalence/status/1276747898781233152
27.
https://hdevalence.ca/blog/2020-10-04-its-25519am#fixing-the-problem
28.
https://hdevalence.ca/images/ed25519-comparison.png
29.
https://github.com/zcash/zcash/issues/2872#issuecomment-576911471
30.
https://publibfp.dhe.ibm.com/epubs/pdf/a227832c.pdf
31.
https://github.com/golang/go/issues/40475
32.
https://gitlab.torproject.org/tpo/core/tor/-/issues/40078
33.
https://github.com/zcash/zcash/issues/2872#issuecomment-576911471
34.
https://zebra.zfnd.org/
35.
https://docs.rs/ed25519-zebra
36.
https://docs.rs/ed25519-zebra/2.2.0/ed25519_zebra/batch/index.html
37.
https://zips.z.cash/zip-0215
38.
https://docs.rs/ed25519-zebra
39.
https://github.com/hdevalence/ed25519consensus
40.
https://github.com/informalsystems/tendermint-rs/issues/355
41.
https://tendermint.com/ibc/
42.
https://hdevalence.ca/blog/tagged/cryptography