Message prehashing (although not an official term in any right)
is the idea of first taking an input x, using a
cryptographically secure hash function H to calculate y =
H(x), and then generating a signature via Sign(secretKey,
y). The idea is that signing is often expensive, while hashing is
often extremely fast. As a result, signing the hash of a message
(which should be indistinguishable from a truly random function) is
often faster than simply signing the full message alone, and in
larger cases can save a significant amount of CPU cycles. However,
internally Ed25519 uses a hash function H already to hash the
input message for computing the signature. Thus, there is a
question - is it appropriate or desireable to hash the input
already if this is the case?
Generally speaking, it's OK to prehash messages before giving them
to Ed25519. However, there is a caveat. In the paper
"EdDSA for more curves",
the authors of the original EdDSA enhance the specification by
extending it with a message prehash function, H', along with an
internal hash H. Here, the prehash H' is simply applied to the
original message first before anything else. The original EdDSA
specification (and the implementation in this package) was a
trivial case of this enhancement: it was implicit that H' is
simply the identity function. We call the case where H' is the
identity function PureEdDSA, while the case where H' is a
cryptographic hash function is known as HashEdDSA. (Thus, the
interfaces sign and dsign implement PureEdDSA - while they can
be converted to HashEdDSA by simply hashing the ByteString
first with some other function.)
However, the authors note that HashEdDSA suffers from a weakness
that PureEdDSA does not - PureEdDSA is resiliant to collision
attacks in the underlying hash function H, while HashEdDSA is
vulnerable to collisions in H'. This is an important
distinction. Assume that the attacker finds a collision such that
H'(x) = H'(y), and then gets convinces a signer to HashEdDSA-sign
x - the attacker may then forge this signature and use it as the
same signature as for the message y. For a hash function of
N-bits of output, a collision attack takes roughly 2^(N/2)
operations.
Ed25519 internally sets H = SHA-512 anyway, which has no known
collision attacks or weaknesses in any meaningful sense. It is
however slower compared to other, more modern hash functions, and
is used on the input message in its entirety (and there are no
plans to switch the internal implementation of this package, or the
standard Ed25519 away from H = SHA-512).
But note: all other hash-then-sign constructions suffer from
this, in the sense they are all vulnerable to collision attacks
in H', should you prehash the message. In fact, PureEdDSA is
unique (as far as I am aware) in that it is immune to collision
attacks in H - should a collision be found, it would not suffer
from these forgeries. By this view, it's arguable that depending
on the HashEdDSA construction (for efficiency or size purposes)
when using EdDSA is somewhat less robust, even if SHA-512 or
whatever is not very fast. Despite that, just about any modern
hash you pick is going to be collision resistant to a fine degree
(say, 256 bits of output, therefore collisions 'at best' happen in
2^128 operations), so in practice this robustness issue may not
be that big of a deal.
However, the more pertinent issue is that due to the current design
of the API which requires the entire blob to sign up front, using
the HashEdDSA construction is often much more convenient, faster
and sometimes necessary too. For example, when signing very large
messages (such as creating a very large tar.gz file which you
wish to sign after creation), it is often convenient and possible
to use 'incremental' hashing APIs to incrementally consume data
blocks from the input in a constant amount of memory. At the end of
consumption, you can 'finalize' the data blocks and get back a
final N-bit hash, and sign this hash all in a constant amount of
memory. With the current API, using PureDSA would require you
loading the entire file up front to either sign, or verify it. This
is especially unoptimal for possibly smaller, low-memory systems
(where decompression, hashing or verification are all best done in
constant space if possible).
Beware however, that if you do this sort of incremental hashing for
large blobs, you are taking untrusted data and hashing it
before checking the signature - be exceptionally careful
with data from a possibly untrustworthy source until you can verify
the signature.
So, some basic guidelines are:
If you are simply not worried about efficiency very much, just
use PureEdDSA (i.e. just use sign and verify
directly).
If you have lots of small messages, use PureEdDSA (i.e.
just use sign and verify directly).
If you have to sign/verify large messages, possibly in
an incremental fashion, use HashEdDSA with a fast
hash (i.e. just hash a message before using sign or
verify on it).
As a result, you should be safe hashing your input before passing
it to sign or dsign in this library if you desire, and it may
save you CPU cycles for large inputs. It should be no different
than the typical hash-then-sign construction you see elsewhere,
with the same downfalls. Should you do this, an extremely
fast-yet-secure hash such as BLAKE2b is recommended, which is
even faster than MD5 or SHA-1 (and do not ever use MD5 or
SHA-1, on that note - they suffer from collision attacks).