Hi Viktor, Thank you -- I checked all three suggestions, and here is what I found. On constant time. You are right, and this is the first consideration in crypton too. The P-256 scalar multiplication is already constant-time: the scalar is taken four bits at a time, and the precomputed table is read by touching all sixteen entries and keeping one with a mask, so the address stream does not depend on the scalar. Field elements are a fixed-size felem[5], leading zeros included and computed over regardless. Nothing in this round of optimisation gave that up. If anything the traffic went the other way: - ECDSA and DSA now invert the nonce without a side channel. - Modular exponentiation no longer indexes a table with the bits of the exponent. That makes RSA signing 1.3-1.7x *slower* than in 1.1.5, which we accepted as the price of hiding the exponent, and said so in the README. - I measured what the constant-time table scan costs by removing it. It costs nothing measurable. There is no reason to drop it. On BoringSSL. The ISC claim is out of date, I am afraid. BoringSSL's LICENSE is Apache-2.0 today, and the header of crypto/fipsmodule/ec/asm/p256-x86_64-asm.pl is Apache-2.0 as well, copyright The OpenSSL Project, Intel and CloudFlare. It is the same file under the same licence as the one in OpenSSL, so it does not help. On fiat-crypto -- this one paid off. I had already tried fiat-crypto's C (fiat-c/src/p256_64.c), and it was disappointing: gcc cannot form adcx/adox chains from the saturated 4x64 representation. Disassembling the result shows 42 mulx and zero carry instructions. Under clang it wins, under gcc it loses on every machine I tried -- compiler-dependent in a way a library cannot build a default on. But your pointer sent me to fiat-amd64/, which I did not know about: the CryptOpt-generated, formally verified x86-64 assembly for the field. It is under fiat-crypto's own MIT / Apache-2.0 / BSD-1-Clause, so there is no licensing problem at all. Measured on an x86-64 with ADX, one operation feeding the next: crypton, 5x52 C mul 27.38 ns square 27.11 ns fiat, 4x64 C mul 41.66 ns square 41.74 ns fiat + CryptOpt asm mul 18.37 ns square 20.20 ns So the field itself is 1.49x and 1.34x faster. The whole curve, however, only came out 1.09x faster (variable-point 163.36 -> 150.25 us; the base point, which has a table, 1.18x). The saturated representation needs a conditional subtraction on every addition, and that eats about a fifth of a scalar multiplication. It is also why fiat's C alone is 0.72x: the assembly first pays that back and then adds nine percent. On top of that the assembly is System V only -- it uses the red zone, so Windows x86-64 is out -- needs ADX at runtime, and changes the representation, so the curve layer would have to be compiled twice and dispatched. I decided 1.09x is not worth carrying all of that. It is still the most promising lead so far, and if an AArch64 version appears, or if the additions get generated too, I will change my mind. RustCrypto is Rust with fiat-crypto underneath, so it is the same arithmetic as above and not callable from C. For completeness I also measured micro-ecc (what picotls uses) and BearSSL. Both target microcontrollers and are slower than what crypton has on a 64-bit machine -- micro-ecc is 101 us against 70 for a variable-point multiplication, and four times slower for the base point. In the end the thing that helped was neither assembly nor a better algorithm. A field multiplication ends in a carry chain the width of the number, and that chain is its latency: on an Apple M4, one product feeding the next costs 18.1 ns, while four independent ones cost 11.4 ns each. The curve arithmetic does have independent products to offer -- but the compiler was emitting the reduction once and *calling* it, and a call is a fence, so the two chains never overlapped. always_inline on three functions: Apple M4, Apple clang 1.23x Neoverse, clang 18 1.12x Neoverse, gcc 13 1.05x On x86-64 the same request makes it 0.82-0.95x: fifteen general-purpose registers are not enough to hold two of these chains, and gcc's object grows from 19 KB to 202 KB spilling. So it is gated on the architecture and enabled on AArch64 only. ECDH P-256 there is now 0.44 of OpenSSL, up from 0.36. If anyone knows of a permissively licensed P-256 assembly implementation -- or of CryptOpt output for AArch64 -- I would be glad to hear about it. That is the only thing left between crypton and OpenSSL on this curve. The current figures are in the benchmark tables in the README. --Kazu From: Viktor Dukhovni <ietf-dane@dukhovni.org> Subject: [Haskell-cafe] Re: P256 assembler implementation
On Wed, Sep 23, 2026 at 06:40:56PM +0900, Kazu Yamamoto (山本和彦) via Haskell-Cafe wrote:
I'm currently working on improving the performance of *crypton*, a Haskell cryptography library, and seeing excellent results; for some algorithms, performance is now on par with OpenSSL.
The current challenge lies with P256. I do not expect further performance gains from the existing C implementation, so I'm looking for an assembly implementation. While OpenSSL includes an assembly implementation, I cannot adopt it due to its Apache License.
If you have any good ideas, please share them via the *crypton* issue tracker. Even if the P256 issue remains unresolved, I plan to release version 2.0.0 in the near future.
For the latest status, please refer to the benchmark table in https://github.com/kazu-yamamoto/crypton
Note that performance is generally not the first consideration with an ECDSA (e.g. P-256) implementation. What's most needed is a constant-time implementation of the nonce generation and EC curve arithmetic (Montgomery ladder, ...).
The best-practice approach is to use a fixed-size representation of the inputs and intermediate results, with leading zero bytes retained and used in all computations regardless of their value.
The most likely to be a good fit is "fiat-crypto"
https://github.com/mit-plv/fiat-crypto
Gemini purports (Double-check of course):
1. BoringSSL (ISC License) Google’s BoringSSL contains highly optimized, constant-time assembly routines for P-256 curve operations (p256-x86_64.span, p256-armv8-aarch64.span, etc.): License: ISC License (a 2-clause BSD equivalent). Architectures: x86_64 (including ADX/BMI2 paths), aarch64 (ARM64), and 32-bit ARM. Property: Fully constant-time execution to prevent side-channel timing attacks.
2. fiat-crypto / Formally Verified Field Arithmetic (MIT / Apache-2.0 / BSD) Developed at MIT, fiat-crypto synthesizes correct-by-construction, constant-time C and assembly field arithmetic code for P-256: License: MIT License / Apache 2.0 / 2-Clause BSD. Architectures: x86_64, aarch64, riscv64 (and 32-bit targets). Property: Generated code avoids data-dependent branch/memory access patterns, making it constant-time. It is used inside BoringSSL, Go (crypto/internal/nistec), and OpenSSL.
3. RustCrypto p256 / elliptic-curves Project (Apache-2.0 / MIT) The RustCrypto ecosystem includes constant-time implementations of P-256: License: Dual-licensed Apache 2.0 / MIT (permissive BSD-style terms). Architectures: Platform-agnostic (via fiat-crypto backend C/inline assembly extensions for x86_64, aarch64, and riscv64).
-- Viktor. 🇺🇦 Слава Україні! _______________________________________________ Haskell-Cafe mailing list -- haskell-cafe@haskell.org To (un)subscribe, modify options or view archives go to: Only members subscribed via the mailman list are allowed to post.