crypton is a fork from cryptonite with the original author's permission.
crypton is a low-level cryptography library. To achieve high
performance, it utilizes C and assembly language to define FFI
bindings, structuring them in a way that makes them easy to use.
AES is where this matters most, and which implementation runs is decided at runtime from what the processor has.
On x86-64 with AES-NI and carry-less multiply, on AArch64 and on 32-bit ARM with the ARMv8 cryptographic extension, and on ppc64le with the vector instructions PowerISA 2.07 brought to POWER8, AES and GHASH are instructions rather than software. crypton's AES and AES-GCM then make no branch and no memory access that depends on the key or on the data: the secrets stay in vector registers and never reach one a branch can test. Every x86-64 part since about 2010 and every AArch64 part in ordinary use has these, and the ppc64le path is little-endian only.
Where none of them is there, crypton computes AES and GHASH without a table.
The portable AES is bitsliced -- the S-box is boolean algebra over four
blocks held across eight 64-bit words -- and the GF(2^128) multiply is built
from shifts, masks and integer multiplies. Neither looks anything up, so
neither derives an address from a secret, and the portable path is constant
time as well. The code is BearSSL's, under MIT, in cbits/bearssl.
The constant-time harness in cbits/tests/ct runs the same driver against
the portable implementation and against each architecture's instructions, and
every one of them is required to report nothing at all.
So the answer does not depend on the machine any more, and what is left to
ask is whether AES is fast on it. hasAESAcceleration and
hasGHASHAcceleration answer that without naming an architecture.
processorOptions says what was found, in the processor's own names --
AESNI and PCLMUL on x86, ARMAES and ARMPMULL on AArch64 and 32-bit
ARM, PPCAES and PPCVPMSUM on ppc64le -- along with everything else it was
asked about, which is unrelated to this.
ghci> import Crypto.System.CPU
ghci> hasAESAcceleration
True
ghci> processorOptions -- on an x86-64 machine
[AESNI,PCLMUL,SSSE3,AVX,AVX2,SHANI,MOVBE,ADX,VAES]
ghci> processorOptions -- and on an AArch64 one
[NEON,ARMAES,ARMPMULL,ARMSHA1,ARMSHA2,ARMSHA512]
RSA is the other place to know about, and there the choice is the caller's.
The private key operations in Crypto.PubKey.RSA.PKCS15, .OAEP and .PSS
take a Maybe Blinder, and Nothing is no harder to write than the safe
form:
decrypt :: Maybe Blinder -> PrivateKey -> ByteString -> ...
decryptSafer :: MonadRandom m => PrivateKey -> ByteString -> m ...
The exponent itself is not what is at risk. expSafe keeps the value of an
exponent out of the work it does, so the private exponent does not leak
through the exponentiation. What a blinder covers is the other side: without
one, the operation runs on the ciphertext the caller was handed, so how long
it takes depends on a number an attacker may have chosen and can vary. That
is what a remote timing attack on RSA needs. With a blinder the input is
multiplied by a random value first and the result divided out afterwards, so
the timing carries nothing an attacker can steer.
decryptSafer and signSafer generate the blinder themselves and are the
ones to reach for. Pass Nothing only where the input is not attacker
controlled and you have decided that it is not.
The RSA rows in the tables below are the unblinded path. A fresh blinder
costs one more exponentiation, by the public exponent, which is the cheap
direction: measured on the M4, signing goes from about 316 to about 335
microseconds, about six per cent. signSafer and decryptSafer pay less
than that from 2.2.4: they keep the blinder of a key they have seen and square
it for the next use, as OpenSSL does, drawing a fresh one every 32 uses.
Crypto.PubKey.RSA.Blinded gives a caller the same thing to hold, a
BlindedKey passed along from one operation to the next.
From 2.2.5 every private-key operation also raises its result back to the public exponent before letting it out, as OpenSSL does. A fault in one half of a CRT computation would otherwise give an answer that reveals a prime of the key; that answer is now computed again without CRT instead. It costs a few per cent of a signature, and the tables below include it.
The algorithms a TLS connection uses, measured against the last release before the rewrite and against OpenSSL on the same machine. Throughput is over 16 KiB messages; the public key operations are one operation each; every figure is the best of several runs, and crypton and OpenSSL are run alternately so that neither gets the quieter machine.
Bulk encryption and hashing are measured through crypton's C layer, as
openssl speed measures OpenSSL's. The public key operations are measured
through crypton's Haskell API, since that is where ECDSA and RSA live and it
is what a program actually calls; the Haskell layer adds well under a
microsecond, which the X25519 and ECDH P-256 rows confirm by agreeing with a
C-level measurement to within a percent. Both releases of crypton are built
the same way -- -optc-O3, which is what each asks for -- and by
cabal build, since a copy of the sources compiled by hand does not measure
what a program linking the library gets, and leaves out whole implementations
without saying so. Each column of a table comes from one run on the machine
named above it.
An AMD EPYC 7763, which has AES-NI, PCLMULQDQ, AVX2, ADX, VAES, VPCLMULQDQ and the SHA extensions, against OpenSSL 4.0.3.
Throughput in MB/s, higher is better:
| crypton 1.1.5 | crypton 2.2.5 | OpenSSL | 2.2.5 / OpenSSL | |
|---|---|---|---|---|
| AES-128-GCM | 1335 | 6044 | 4046 | 1.49 |
| AES-256-GCM | 1093 | 5494 | 3764 | 1.46 |
| ChaCha20-Poly1305 | 399 | 2197 | 2212 | 0.99 |
| SHA-1 | 729 | 1663 | 1673 | 0.99 |
| SHA-256 | 289 | 1585 | 1571 | 1.01 |
| SHA-512 | 450 | 770 | 749 | 1.03 |
| SHA3-256 | 109 | 421 | 425 | 0.99 |
Time per operation in microseconds, lower is better:
| crypton 1.1.5 | crypton 2.2.5 | OpenSSL | OpenSSL / 2.2.5 | |
|---|---|---|---|---|
| X25519 | 44.31 | 28.56 | 36.51 | 1.28 |
| ECDH P-256 | 164.3 | 50.63 | 51.53 | 1.02 |
| ECDH P-384 | 2241 | 164.4 | 834.7 | 5.08 |
| Ed25519 sign | 29.33 | 18.57 | 33.68 | 1.81 |
| Ed25519 verify | 48.48 | 47.59 | 110.7 | 2.33 |
| ECDSA P-256 sign | 81.70 | 18.08 | 21.88 | 1.21 |
| ECDSA P-256 verify | 231.8 | 69.63 | 67.60 | 0.97 |
| ECDSA P-384 sign | 2273 | 303.9 | 877.6 | 2.89 |
| ECDSA P-384 verify | 2673 | 474.3 | 722.8 | 1.52 |
| RSA-2048 sign/decrypt | 759.5 | 633.9 | 659.4 | 1.04 |
| RSA-2048 verify/encrypt | 33.24 | 22.70 | 18.80 | 0.83 |
| RSA-3072 sign/decrypt | 2500 | 2232 | 2010 | 0.90 |
| RSA-3072 verify/encrypt | 65.79 | 61.73 | 40.09 | 0.65 |
An Apple M4, which has the AES, PMULL, SHA-1, SHA-2, SHA-512 and SHA-3 instructions, against OpenSSL 4.0.3.
Throughput in MB/s, higher is better:
| crypton 1.1.5 | crypton 2.2.5 | OpenSSL | 2.2.5 / OpenSSL | |
|---|---|---|---|---|
| AES-128-GCM | 134 | 12850 | 11215 | 1.15 |
| AES-256-GCM | 100 | 10356 | 9405 | 1.10 |
| ChaCha20-Poly1305 | 804 | 2425 | 2302 | 1.05 |
| SHA-1 | 1286 | 3389 | 3363 | 1.01 |
| SHA-256 | 500 | 3397 | 3363 | 1.01 |
| SHA-512 | 762 | 1881 | 1888 | 1.00 |
| SHA3-256 | 564 | 1125 | 1104 | 1.02 |
Time per operation in microseconds, lower is better:
| crypton 1.1.5 | crypton 2.2.5 | OpenSSL | OpenSSL / 2.2.5 | |
|---|---|---|---|---|
| X25519 | 17.43 | 11.10 | 15.29 | 1.38 |
| ECDH P-256 | 63.98 | 19.70 | 24.14 | 1.23 |
| ECDH P-384 | 3181 | 68.57 | 371.6 | 5.42 |
| Ed25519 sign | 12.57 | 7.217 | 13.24 | 1.83 |
| Ed25519 verify | 17.62 | 17.75 | 34.72 | 1.96 |
| ECDSA P-256 sign | 30.53 | 6.333 | 10.46 | 1.65 |
| ECDSA P-256 verify | 91.95 | 26.52 | 31.97 | 1.21 |
| ECDSA P-384 sign | 3158 | 123.5 | 393.2 | 3.18 |
| ECDSA P-384 verify | 3790 | 200.5 | 327.0 | 1.63 |
| RSA-2048 sign/decrypt | 448.6 | 323.6 | 320.5 | 0.99 |
| RSA-2048 verify/encrypt | 18.12 | 9.683 | 8.366 | 0.86 |
| RSA-3072 sign/decrypt | 1468 | 952.9 | 925.6 | 0.97 |
| RSA-3072 verify/encrypt | 48.07 | 20.47 | 17.79 | 0.87 |
There are two changes behind the 1.1.5 column and the 2.2.5 one, not a single steady improvement.
The first, in 2.0.0, was a rewrite: the bulk algorithms moved into C, the
curves other than P-256 moved out of Haskell Integer arithmetic, and
everything that touches a secret was made to take the same time whatever the
secret is. 1.1.5 had no AArch64 code of its own at all, which is why AES-GCM
there is close to a hundred times what it was, and on x86-64 it had AES-NI
and nothing else.
The second, from 2.1.0 onwards, is assembly, for the operations where C cannot reach. Which of the two a row owes its gain to is not the same everywhere: ECDSA P-384 signing took nineteenfold from the rewrite and a further fifth from the assembly, while X25519 waited for the assembly entirely and ECDH P-384 is almost all of it.
Most of that assembly is not crypton's. The prime curves, the inverse modulo
a group order, X25519, and RSA's Montgomery multiplication on x86-64 go
through s2n-bignum, vendored in
cbits/s2n. Every routine in it carries a machine-checked proof in
HOL-Light that it computes what it says, and is written in a constant-time
style. It is Apache-2.0 OR ISC OR MIT-0, and crypton takes it under ISC.
That licence is why any of this was possible. The obvious assembly to reach
for is OpenSSL's and BoringSSL's ecp_nistz256, and it cannot be used here:
it is Apache-2.0 only, and Intel and CloudFlare hold copyright in it besides
OpenSSL, so nobody is in a position to relicense it.
crypton is behind in a few rows now: RSA on both architectures, except RSA-2048 signing on x86-64, and ECDSA P-256 verification on x86-64. The AES-GCM rows were the other half of this section until 2.1.5; they are ahead on both machines now, and what the instructions do is still worth setting out.
RSA. 2.0.0 made signing slower than 1.1.5 on purpose: its modular
exponentiation stopped indexing a table with the bits of the exponent, and
hiding the exponent is what the difference bought. 2.2.5 has repaid that cost
on both machines, by different means. On x86-64 s2n-bignum's Montgomery
multiplication is twice the C's, because the C cannot form the two carry
chains ADCX and ADOX give, and 2.2.5 signs in less than 1.1.5 took, at
3072 bits as well as 2048. On AArch64 s2n-bignum's routines measure no better
than the C, so the gain there is the C's own: the multiplication and squaring
for each key size are generated and unrolled, with the reduction folded into
each column as in Koç, Acar and Kaliski's FIPS method, and signing is within a
few per cent of OpenSSL, the check of the result included. The measurements
are in #275.
On processors with AVX-512 IFMA, Intel Xeons from Ice Lake and AMD from Zen 4, crypton computes the two halves of the CRT together in 52-bit digits, Gueron and Krasnov's method and the one OpenSSL takes there. Neither machine above has IFMA, so neither table shows it.
Verification's exponent is 65537, seventeen bits, so there is little exponentiation to speed up and the work around it counts. crypton keeps R^2, the Montgomery constant of a modulus, for the moduli it has seen, and from 2.2.5 hands the signature to the C as bytes and builds the padding without a list. On macOS it verifies in its own C at every RSA size: the GMP that GHC builds into itself there is compiled without optimisation (ghc#28030), which is most of what the RSA-3072 verification row on AArch64 gained. Elsewhere GMP keeps the sizes other than 2048 bits, and on x86-64 it is the reason the RSA-3072 verification row is still where it was.
The wide AES instructions. VAES and VPCLMULQDQ do two blocks where
AES-NI and PCLMULQDQ do one, and four in their 512-bit form. crypton uses
the 256-bit form where the processor has it, which is Zen 3 and Ice Lake
onwards, and the 512-bit form where that is worth having, which is Ice Lake and
Zen 5 onwards. There was nothing to borrow: the wide AES-GCM in OpenSSL,
BoringSSL and AWS-LC is Apache-2.0 and s2n-bignum has no GCM, so both files are
crypton's own.
Having the 256-bit one is where the 1.49 in the x86-64 table comes from, and
it is narrower than it sounds. The EPYC 7763 is Zen 3: VAES and VPCLMULQDQ,
no AVX-512. OpenSSL's x86-64 AES-GCM is aesni-gcm-x86_64.pl, which is
128-bit -- its vaesencs are the VEX encoding of AESENC on xmm, and
there is not one ymm in the file -- or aes-gcm-avx512.pl, which wants
AVX512VAES. There is no rung between them, so on this processor OpenSSL
takes a block at a time where crypton takes two. The same idea as theirs,
one step further down the feature ladder; not a better one.
The 512-bit path arrived after 2.1.2, so it is in the 2.2.5 column -- but neither machine in the tables above has AVX-512, so neither column shows it. On the runners that do, measured over 16 KiB in MB/s: an EPYC 9V45 (Zen 5) goes from 9616 to 14268 with it, a Xeon 6973P-C from 8095 to 9848, a Xeon 8573C from 6983 to 8447. OpenSSL on those machines is ahead still -- 25760 on the first of them -- because it interleaves the GHASH with the AES where crypton does them in turn. Zen 4 keeps the 256-bit path: its 512-bit instructions are two passes through a 256-bit datapath, so the wider encoding buys nothing there and costs a little.
AArch64 has no counterpart to any of these: one AES block and one GHASH multiplication at a time is all the instruction set offers. Its AES-GCM rows were 0.85 and 0.87 until 2.1.5, for that reason. What closed it was not width but the GHASH's representation -- H is twisted once at key setup so that GCM's bit reflection is already undone, which turns a reduction of some twenty-five shifts and XORs into two PMULL and six EOR and makes Karatsuba worth taking. The scheme is ARM's, from the BSD-3-Clause part of AArch64cryptolib, written out in crypton's own intrinsics. The AES there is ahead of OpenSSL's and always was; it was the GHASH beside it that was behind.
One row wants a word of its own: crypton's Ed25519.sign derives the public
key from the secret key every time it signs, so that a caller who passes a
public key that does not match cannot be made to leak the private one. That
costs a second scalar multiplication, which OpenSSL's signing does not pay --
and the row is still 1.83 on AArch64 and 1.81 on x86-64, so the safety is had
for nothing here rather than paid for.
Ed25519 verification wanted one too, for an unhappier reason, and 2.2.1
settles it. On x86-64 the row was about eight per cent slower from 2.1.8
onwards: 2.1.5, 2.1.6 and 2.1.7 all measure near 48 microseconds there,
2.1.8, 2.1.10 and 2.2.0 all measure near 52, and 2.2.1 is near 48 again.
Nothing in Ed25519 ever changed -- no commit touches cbits/ed25519 across
any of it, and AArch64 never moved, the compiled ed25519.o there being
byte for byte the same object either side of 2.1.8. What moved was a line
in this package's cabal file. The ML-KEM and ML-DSA backends that 2.1.8
vendored want -mavx2 -mbmi2, and they were given as cc-options inside an
if arch(x86_64) block -- but cc-options in a conditional belong to the
component and not to the block, so every C file in the library was compiled
with AVX2 allowed, cbits/ed25519 among them, and what the compiler made of
it that way is slower on this machine than what it replaced. Signing, which
is the same file and a different routine, is where it always was. 2.2.1
narrowed the flags to the assembly that asked for them, which is also what
stopped a binary built on one x86-64 machine from dying on another, and the
row came back.
SHA-1 is in the tables because a number of protocols and file formats still ask for it, not because it is a good choice for anything new. The algorithms that nothing should ask for any more -- MD5, 3DES, RC4, CBC mode -- are left out.
ML-KEM and ML-DSA are left out too, though 2.1.8 brought both and they are
fast -- mlkem-native and mldsa-native, from the PQ Code Package. No
comparison is reported here: the implementations to measure against are
still developing.