Copyright | (c) Austin Seipp 2013-2015 |
---|---|
License | MIT |
Maintainer | aseipp@pobox.com |
Stability | experimental |
Portability | portable |
Safe Haskell | Trustworthy |
Language | Haskell2010 |
This module provides bindings to the Ed25519 public-key signature system, including detached signatures. The documentation should be self explanatory with complete examples.
Below the basic documentation you'll find API, performance and
security notes, which you may want to read carefully before
continuing. (Nonetheless, Ed25519
is one of the easiest-to-use
signature systems around, and is simple to get started with for
building more complex protocols. But the below details are highly
educational and should help adjust your expectations properly.)
For more reading on the underlying implementation and theory (including how to get a copy of the Ed25519 software), visit http://ed25519.cr.yp.to. There are two papers that discuss the design of EdDSA/Ed25519 in detail:
- "High-speed high-security signatures" - The original specification by Bernstein, Duif, Lange, Schwabe, and Yang.
- "EdDSA for more curves" - An extension of the original EdDSA specification allowing it to be used with more curves (such as Ed41417, or Ed488), as well as defining the support for message prehashing. The original EdDSA is easily derived from the extended version through a few parameter defaults. (This package won't consider non-Ed25519 EdDSA systems any further.)
- newtype PublicKey = PublicKey {}
- newtype SecretKey = SecretKey {}
- createKeypair :: IO (PublicKey, SecretKey)
- createKeypairFromSeed_ :: ByteString -> Maybe (PublicKey, SecretKey)
- createKeypairFromSeed :: ByteString -> (PublicKey, SecretKey)
- toPublicKey :: SecretKey -> PublicKey
- sign :: SecretKey -> ByteString -> ByteString
- verify :: PublicKey -> ByteString -> Bool
- newtype Signature = Signature {}
- dsign :: SecretKey -> ByteString -> Signature
- dverify :: PublicKey -> ByteString -> Signature -> Bool
- sign' :: SecretKey -> ByteString -> Signature
- verify' :: PublicKey -> ByteString -> Signature -> Bool
A crash course introduction
The simplest use of this library is one where you probably need to
sign short messages, so they can be verified independently. That's
easily done by first creating a keypair with
, and
using createKeypair
to create a signed message. Then, you can distribute
your public key and the signed message, and any recipient can
verify that message:sign
>>>
(pk, sk) <- createKeypair
>>>
let msg = sign sk "Hello world"
>>>
verify pk msg
True
This interface is fine if your messages are small and simple binary
blobs you want to verify in an opaque manner, but internally it
creates a copy of the input message. Often, you'll want the
signature independently of the message, and that can be done with
and dsign
. Naturally, verification fails if the
message is incorrect:dverify
>>>
(pk, sk) <- createKeypair
>>>
let msg = "Hello world" :: ByteString
>>>
let sig = dsign sk msg
>>>
dverify pk msg sig
True>>>
dverify pk "Hello world" sig
True>>>
dverify pk "Goodbye world" sig
False
Finally, it's worth keeping in mind this package doesn't expose any
kind of incremental interface, and signing/verification can be
expensive. So, if you're dealing with large inputs, you can
hash the input with a robust, fast cryptographic hash, and then
sign that (for example, the hash
function below could be
SHA-512 or BLAKE2b):
>>>
(pk, sk) <- createKeypair
>>>
msg <- readBigFile "blob.tar.gz" :: IO ByteString
>>>
let sig = dsign sk (hash msg)
>>>
dverify pk (hash msg) sig
True
See the notes at the bottom of this module for more on message prehashing (as it acts slightly differently in an EdDSA system).
Keypair creation
Ed25519 signatures start off life by having a keypair created,
using
or createKeypair
, which gives
you back a createKeypairFromSeed_
you can use for signing messages, and a
SecretKey
your users can use to verify you in fact authored the
messages.PublicKey
Ed25519 is a deterministic signature system, meaning that you may
always recompute a
and a PublicKey
from an
initial, 32-byte input seed. Despite that, the default interface
almost all clients will wish to use is simply SecretKey
,
which uses an Operating System provided source of secure randomness
to seed key creation. (For more information, see the security notes
at the bottom of this page.)createKeypair
A
created by PublicKey
.createKeypair
Since: 0.0.1.0
PublicKey | |
|
A
created by SecretKey
. Be sure to keep this
safe!createKeypair
Since: 0.0.1.0
SecretKey | |
|
createKeypair :: IO (PublicKey, SecretKey) Source
Randomly generate a
and SecretKey
for doing
authenticated signing and verification. This essentically calls
PublicKey
with a randomly generated 32-byte seed,
the source of which is operating-system dependent (see security
notes below). However, internally it is implemented more
efficiently (with less allocations and copies).createKeypairFromSeed_
If you wish to use your own seed (for design purposes so you may
recreate keys, due to high paranoia, or because you have your own
source of randomness), please use
instead.createKeypairFromSeed_
Since: 0.0.1.0
:: ByteString | 32-byte seed |
-> Maybe (PublicKey, SecretKey) | Resulting keypair |
Generate a deterministic
and PublicKey
from a
given 32-byte seed, allowing you to recreate a keypair at any point
in time, providing you have the seed available.SecretKey
If the input seed is not 32 bytes in length,
returns createKeypairFromSeed_
. Otherwise, it
always returns Nothing
for the given seed.Just
(pk, sk)
NOTE: This function will replace
in
the future.createKeypairFromSeed
Since: 0.0.4.0
:: ByteString | 32-byte seed |
-> (PublicKey, SecretKey) | Resulting keypair |
Deprecated: This function is unsafe as it can
with an invalid input. Use fail
instead.createKeypairWithSeed_
Generate a deterministic
and PublicKey
from a
given 32-byte seed, allowing you to recreate a keypair at any point
in time, providing you have the seed available.SecretKey
Note that this will
if the given input is not 32 bytes in
length, so you must be careful with this input.error
Since: 0.0.3.0
Derive the
for a given PublicKey
. This is a
convenience which allows (for example) using SecretKey
and
only ever storing the returned createKeypair
for any future
operations.SecretKey
Since: 0.0.3.0
Signing and verifying messages
By default, the Ed25519 interface computes a signed message given
a
and an input message. A signed message consists
of an Ed25519 signature (of unspecified format), followed by the
input message. This means that given an input message of SecretKey
M
bytes, you get back a message of M+N
bytes where N
is a
constant (the size of the Ed25519 signature blob).
The default interface in this package reflects that. As a result,
any time you use
or sign
you will be given back the
full input, and then some.verify
:: SecretKey | Signers |
-> ByteString | Input message |
-> ByteString | Resulting signed message |
:: PublicKey | Signers |
-> ByteString | Signed message |
-> Bool | Verification result |
Detached signatures
This package also provides an alternative interface for detached
signatures, which is more in-line with what you might
traditionally expect from a signing API. In this mode, the
and dsign
interfaces simply return a constant-sized
blob, representing the Ed25519 signature of the input message.dverify
This allows users to independently download, verify or attach
signatures to messages in any way they see fit - for example, by
providing a tarball file to download, with a corresponding .sig
file containing the Ed25519 signature from the author.
A
which is detached from the message it signed.Signature
Since: 0.0.1.0
Signature | |
|
:: SecretKey | Signers |
-> ByteString | Input message |
-> Signature | Message |
Deprecated interface
The below interface is deprecated but functionally equivalent to the above; it simply has "worse" naming and will eventually be removed.
:: SecretKey | Signers |
-> ByteString | Input message |
-> Signature | Message |
Security, design and implementation notes
Included below are some notes on the security aspects of the Ed25519 signature system, its implementation and design, this package, and suggestions for how you might use it properly.
EdDSA background and properties
Ed25519 is a specific instantiation of the EdDSA digital signature scheme - a high performance, secure-by-design variant of Schnorr signatures based on "Twisted Edwards Curves" (hence the name EdDSA). The (extended) EdDSA system is defined by an elliptic curve:
ax^2 + y^2 = 1 + d*x^2*y^2
along with several other parameters, chosen by the implementation
in question. These parameters include a
, d
, and a field GF(p)
where p
is prime. Ed25519 specifically uses d = -121665/121666
,
a = -1
, and the finite field GF((2^155)-19)
, where (2^155)-19
is a prime number (which is also the namesake of the algorithm in
question, as Ed25519). This yields the equation:
-x^2 + y^2 = 1 - (121665/121666)*x^2*y^2
This curve is 'birationally equivalent' to the well-known Montgomery curve 'Curve25519', which means that EdDSA shares the same the difficult problem as Curve25519: that of the Elliptic Curve Discrete Logarithm Problem (ECDLP). Ed25519 is currently still the recommended EdDSA curve for most deployments.
As Ed25519 is an elliptic curve algorithm, the security level
(i.e. number of computations taken to find a solution to the ECDLP
with the fastest known attacks) is roughly half the key size in
bits, as it stands. As Ed25519 features 32-byte keys, the security
level of Ed25519 is thus 2^((32*8)/2) = 2^128
, far beyond any
attacker capability (modulo major breakthroughs for the ECDLP,
which would likely catastrophically be applicable to other systems
too).
Ed25519 designed to meet the standard notion of unforgeability for a public-key signature scheme under chosen-message attacks. This means that even should the attacker be able to request someone sign any arbitrary message of their choice (hence chosen-message), they are still not capable of any forgery what-so-ever, even the weakest kind of 'existential forgery'.
Generation of psuedo-random seeds
Seed generation as done by
uses Operating System
provided APIs for generating cryptographically secure psuedo-random
data to be used as an Ed25519 key seed. Your own deterministic keys
may be generated using createKeypair
, provided you have
your own cryptographically secure psuedo-random data from
somewhere.createKeypairFromSeed_
On Linux, OS X and other Unix machines, the
/dev/urandom
device is consulted internally in order to generate
random data. In the current implementation, a global file
descriptor is used through the lifetime of the program to
periodically get psuedo-random data.
On Windows, the CryptGenRandom
API is used internally. This
does not require file handles of any kind, and should work on all
versions of Windows. (Windows may instead use RtlGenRandom
in the
future for even less overhead.)
In the future, there are plans for this package to internally take
advantage of better APIs when they are available; for example, on
Linux 3.17 and above, getrandom(2)
provides psuedo-random data
directly through the internal pool provided by /dev/urandom
,
without a file descriptor. Similarly, OpenBSD provides the
arc4random(3)
family of functions, which internally uses a data
generator based on ChaCha20. These should offer somewhat better
efficiency, and also avoid file-descriptor exhaustion attacks which
could lead to denial of service in some scenarios.
Performance and implementation
Ed25519 is exceptionally fast, although the implementation provided
by this package is not the fastest possible implementation. Indeed,
it is rather slow, even by non-handwritten-assembly standards of
speed. That said, it should still be competitive with most other
signature schemes: the underlying implementation is ref10
from
SUPERCOP, authored by Daniel J. Bernstein,
which is within the
realm of competition
against some assembly implementations (only 2x slower), and much
faster than the slow reference implementation (25x slower). When up
against RSA
signatures (ronald3072) on a modern Intel machine, it is still 15x
faster at signing messages at the same 128-bit security level.
On the author's Sandy Bridge i5-2520M 2.50GHz CPU, the benchmarking code included with the library reports the following numbers for the Haskell interface:
benchmarking deterministic key generation time 250.0 μs (249.8 μs .. 250.3 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 250.0 μs (249.9 μs .. 250.2 μs) std dev 467.0 ns (331.7 ns .. 627.9 ns) benchmarking signing a 256 byte message time 273.2 μs (273.0 μs .. 273.4 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 273.3 μs (273.1 μs .. 273.5 μs) std dev 616.2 ns (374.1 ns .. 998.8 ns) benchmarking verifying a signature time 635.7 μs (634.6 μs .. 637.3 μs) 1.000 R² (1.000 R² .. 1.000 R²) mean 635.4 μs (635.0 μs .. 636.0 μs) std dev 1.687 μs (999.3 ns .. 2.487 μs) benchmarking roundtrip 256-byte sign/verify time 923.6 μs (910.0 μs .. 950.6 μs) 0.998 R² (0.996 R² .. 1.000 R²) mean 913.2 μs (910.6 μs .. 923.0 μs) std dev 15.93 μs (1.820 μs .. 33.72 μs)
In the future, this package will hopefully provide an opt-in (or possibly default) implementation of ed25519-donna, which should dramatically increase speed at no cost for many/all platforms.
Secure SecretKey
storage
SecretKey
By default, keys are not encrypted in any meaningful manner with any mechanism, and this package does not provide any means of doing so. As a result, your secret keys are only as secure as the computing environment housing them - a server alone out on the hostile internet, or a USB stick that's susceptable to theft.
If you wish to add some security to your keys, a very simple and
effective way is to add a password to your
with a
KDF and a hash. How does this work?SecretKey
- First, hash the secret key you have generated. Use this as a checksum of the original key. Truncating this hash to save space is acceptable; see below for more details and boring hemming and hawing.
- Given an input password, use a KDF to stretch it to the length
of a
.SecretKey
- XOR the
bytewise, directly with the output of your chosen KDF.SecretKey
- Attach the checksum you generated to the resulting encrypted key, and store it as you like.
In this mode, your key is XOR'd with the psuedo-random result of a KDF, which will stretch simple passwords like "I am the robot" into a suitable amount of psuedo-random data for a given secret key to be encrypted with. Decryption is simply the act of taking the password, generating the psuedo-random stream again, XORing the key bytewise, and validating the checksum. In this sense, you are simply using a KDF as a short stream cipher.
Recommendation: Encrypt keys by stretching a password with
scrypt (or yescrypt), using better-than-default parameters.
(These being N = 2^14
, r = 8
, p = 1
; the default results in
16mb of memory per invocation, and this is the recommended default
for 'interactive systems'; signing keys may be loaded on-startup
for some things however, so it may be profitable to increase
security as well as memory use in these cases. For example, at N =
2^18
, r = 10
and p = 2
, you'll get 320mb of memory per use,
which may be acceptable for dramatic security increases. See
elsewhere for exact memory use.) Checksums may be computed with an
exceptionally fast hash such as BLAKE2b.
Bonus points: Print that resulting checksum + key out on a piece of paper (~100 bytes, tops), and put that somewhere safe.
Q: What is the hash needed for? A: A simple file integrity check. Rather than invoke complicated methods of verifying if an ed25519 keypair is valid (as it is simply an opaque binary blob, for all intents and purposes), especially after 'streaming decryption', it's far easier to simply compute and compare against a checksum of the original to determine if decryption with your password worked.
Q: Wait, why is it OK to truncate the hash here? That sounds
scary. Won't that open up collisions or something like that if they
stole my encrypted key? A: No. The hash in this case is only
used as a checksum to see if the password is legitimate after
running the KDF and XORing with the result. Think about how the
'challenge' itself is chosen: if you know H(m)
, do you want to
find m
itself, or simply find m'
where H(m') = H(m)
? To
forge a signature, you want the original key, m
. Suppose given an
input of 256-bits, we hashed it and truncated to one bit. Finding
collisions would be easy: you would only need to try a few times to
find a collision or preimage. But you probably found m'
such that
H(m') = H(m)
- you didn't necessarily find m
itself. In this
sense, finding collisions or preimages of the hash is not useful to
the attacker, because you must find the unique m
.
Q: Okay, why use hashes at all? Why not CRC32? A: You could do that, it wouldn't change much. You can really use any kind of error detecting code you want. The thing is, some hashes such as BLAKE2 are very fast in things like software (not every CPU has CRC instructions, not all software uses CRC instructions), and you're likely to already have a fast, modern hash function sitting around anyway if you're signing stuff with Ed25519. Why not use it?
Prehashing and large input messages
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
and sign
implement PureEdDSA - while they can
be converted to HashEdDSA by simply hashing the dsign
first with some other function.)ByteString
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
andsign
directly).verify
- If you have lots of small messages, use PureEdDSA (i.e.
just use
andsign
directly).verify
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
orsign
on it).verify
- A hash like BLAKE2b is recommended. Fast and very secure.
- Remember: never touch input data in any form until you are done hashing it and verifying the signature.
As a result, you should be safe hashing your input before passing
it to
or sign
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).dsign