Philbin: the safest (and fastest) AEGIS library
Philbin is a pure-Rust implementation of the AEGIS encryption algorithms with an API designed to be maximally misuse-resistant. It’s the only pure-Rust implementation that uses _runtime_CPU detection to dispatch to the best available SIMD implementation.
AEGIS is significantly faster than other popular AEADalgorithms (e.g. AES\-GCM and ChaCha20-Poly1305) on CPUs with AES hardware acceleration1 while being more secure. The algorithm was one of the winners of the CAESAR Competition for a reason. After a long road to standardization, RFC 10032 was published just a few days ago.2
The closest thing to an “official” AEGIS implementation is the raw C libaegis by the author of RFC 10032, Frank Denis. The aegisRust crate is a thin wrapper around `libaegis`3 and before Philbin, there were no pure-Rust implementations that used runtime CPU detection and thus provided reliably excellent performance. You’d have to use `RUSTFLAGS="-C target-cpu=native"`, but then the final binary is not portable; running it on a different CPU of the same architecture often leads to `SIGILL`errors.
Nothing of the kind is necessary with Philbin. A simple Rust `release` binary will give you all the performance on x86-64 and AArch64 with a software-only fallback for other architectures.
Memory Safety
Philbin’s raison d’être is safety plus a heavy emphasis on correctness and security.
There’s a grand total of two lines of code in unsafe blocks which are guarded by both compile-time and runtime checks. I’m using the “CPU feature tokens” approach for safe SIMD in Rust that has been pioneered by Sergey “Shnatsel” Davidoff and the rest of the Fearless SIMD team.4
Security
Philbin was entirely written by a human and reviewed for security issues by fancy AIs (which found effectively nothing5).
Great test coverage is table stakes for a sound cryptographic library. Philbin of course includes custom tests for interesting corner cases, but also all the IETF AEGIS test vectors and hundreds of Rooterberg test vectors.6
Beyond that, Philbin is thoroughly fuzzed under AddressSanitizer to find any crashes or panics. It’s also differentially fuzzed against libaegis to prove identical output.
Beyond that, I’ve also implemented a custom Test Vector Leakage Assessment(TVLA)7 harness that validates the implementation is constant-time regardless of secret data.
Critically, the TVLA harness runs a self-check on every startup: it _must_detect code that’s known to be variable-time for some inputs and must not fail on code that’s known to be correctly constant. Only after this is complete does the harness proceed to test Philbin. This is important because the harness relies on precise measurements and careful statistics to tease out nanosecond-level differences in execution times. This works extremely well on some CPUs and not at all on others. If the host environment is unsuitable, the harness aborts to avoid producing an unreliable report.
There’s also rigorous error handling leading to no panics on edge cases (and fuzzing to prove it). Everything is also painstakingly written to avoid _all_integer overflow and underflow.
Finally, `Key` objects are zeroized when they expire, error variants are carefully designed to avoid attacks similar to Padding Oracle and no dynamic content is ever included in error messages.
“Falling into the Pit of Success”
Too many cryptographic libraries make it easy to shoot yourself in the foot. I’ve put significant effort in designing Philbin’s APIs to be resistant to usage mistakes.
All Philbin APIs are exposed through two modules:
- An `easy` module with simple, high-level APIs,
- A `careful` module with low-level APIs that are harder to use.
- They handle nonces internally. You don’t even need to know what a nonce is.8
- They use your operating system’s CSRNG to get random numbers. Thus your application is safe even if it calls fork() or similar.
- They use sensible payload framing for the nonce, ciphertext and authentication tag.
- They make safe, conservative choices like 256-bit authentication tags and AEGIS-256X4 for the underlying algorithm so you don’t need to juggle the decision tree.
- Etc.
- They make it hard to reuse a nonce (which is a critical security issue) by taking them by-value.
- All “byte array” arguments (and there are many) use the New Type idiom to prevent accidental parameter swaps.
- Etc.
- `Intel Core Ultra 7 265K` as the “modern workstation.”
- `AMD Ryzen 5600` as the “older desktop.”
- `Intel N100` as the “slow x86-64 CPU;” it’s literally the second slowest CPU that Intel still sells (behind the `Intel N50`). It has a tiny 6 W TDP.
- `Intel Xeon Platinum 8581C` as the “modern server.” I rented this one from “teh cloudz.”
- `Apple M2` to get both AArch64 coverage and laptop coverage.
You’re better off sticking to just the `easy` module. You get simple `encrypt` and `decrypt` functions that make sensible decisions for you:
The low-level APIs in `careful` are still available if you have the cryptographic expertise necessary to use them correctly. Feeling lucky?
There are other protections that apply to all API surfaces. For instance, Philbin rejects all-zero keys and nonces since they are almost certainly catastrophic usage errors that will lead to exploitable vulnerabilities.
There are no valid production use-cases for an all-zero key. An all-zero nonce is sometimes used as the first value of a counter-based nonce; instead start your counter from 1 or a random number. Even better, use a randomly generated 256-bit nonce which AEGIS supports; Philbin’s `Nonce::generate()` will safely do this for you.
Further hardening of `careful` APIs:
Benchmarks
Benchmarking Notes
I’ve been running benchmarks and optimizing low-level code for decades. It’s extremely easy to produce data and conclusions that are, at best, misleading. This section covers what I’ve done to try to avoid that pitfall.9
I’ve run these benchmarks on multiple CPUs to provide a more hollistic view of performance; this is more important than usual because different CPUs support different SIMD instruction sets which naturally leads to widly different performance profiles.
The CPUs I ran benchmarks on include:
I then normalized benchmark execution time to the performance of either `ChaCha20-Poly1305` or the equivalent AEGIS variant in the `aegis` crate (depending on the chart) on the same CPU. A geometric mean then produces a cross-CPU speedup from many per-CPU speedups.
I used the ring crate for the non-AEGIS algorithm implementations because it’s probably the most respected cryptographic library in the Rust ecosystem. I gave aws-lc-rs a shot because it’s supposed to be faster, but I didn’t notice a performance difference on my workstation for these algorithms. My goal was to give AES and ChaCha the best possible showing.
The benchmarks use random 16 KiB messages; shorter messages don’t accurately portray performance because they are affected by static setup overhead. Longer messages exhibit similar performance to 16 KiB messages.
I’m compiling Philbin with `rustc` 1.98.1 which uses LLVM 23. Compiling the `aegis` crate also compiles the pure C `libaegis`, so I’ve downloaded and set up Clang from LLVM 23. Since my homelab runs Debian 13 which provides only LLVM 19, this avoids any unfair advantage to Philbin from using a more recent compiler backend.
There is one small wrinkle… the aegis crate passes \-mtune=native (N.B. not `-march=native`) to the compiler when compiling `libaegis`. This instructs the compiler to tune “everything applicable about the generated code, except for the ABI and the set of available instructions” to the CPU performing the compilation. This can yield a few percentage points of extra performance for that CPU model at the cost of losing a similar performance delta on other CPU models executing the same binary (especially CPUs from other vendors).
In other words, that flag shouldn’t be enabled by default. It makes benchmarks look better while usually hurting real-world performance. Since I used the `aegis` crate as-is, the benchmarks give `aegis`/`libaegis` a small (and unfair) advantage. Philbin uses no such flag.
As a big fan of transparency, raw benchmark data is available as is the Julia Pluto notebook I used to analyze the data and produce the charts. Also, here’s the exact rustc config; note that LTO is disabled.
Nothing is Ever Done
The benchmarks above use `aegis` `0.9.16`.
Literally as I was collecting the final benchmark data and writing this blog post, Frank Denis pushed out `aegis` `0.9.18` which came with new performance tweaks.10 I gave it a quick look and some rough benchmarking showed regressions on x86-64 which I reported and Frank fixed. `aegis`/`libaegis`seems to have gotten faster on AArch64 for some AEGIS variants as well.
I didn’t re-benchmark everything because it takes a good bit and I’m very tired, but Philbin appears to still hold a small lead (at least on my workstation).
This is a great reminder that benchmarks reflect a point in time. I have no doubt that both `libaegis` and Philbin will improve performance further.
Closing Thoughts
Building Philbin was a fun five months (on-and-off). I need it for a larger OSS application I’m working on; more to come!
I read SO. MUCH. ASSEMBLY. while working on this, my eyes are bleeding. If I never see another damn VMOVDQA, VAESENC or VPUNPCKLQDQ, it will be too fucking soon.
And the name? Rust + AEGIS => “raegis” => Regis Philbin \=> Philbin!
---
1. Note that effectively all desktop and mobile CPUs manufactured since \~2014 support AES hardware acceleration. ↩︎ 2. Huge thanks to Frank Denis for working on that RFC for years and finally getting it over the finish line! He’s made awesome contributions to OSS over the years, not least of which are libsodium and libaegis. Consider sponsoring him on GitHub. ↩︎ 3. The `aegis` crate includes a `pure-rust` Cargo feature (disabled by default), but warns that "\[enabling this feature\] will substantially degrade performance and some features may not be available". The `pure-rust` implementation in `aegis` does not use runtime CPU detection. ↩︎ 4. The Fearless SIMD crate has recently published version 1.0.0 so now is the perfect time to check it out! ↩︎ 5. The AIs spent many hours intensely fucking matrices to find some documentation typos. Totally worth the tokens; the future is now! They did praise me for “exceptional, ground-breaking work” so at least I have that going for me. ↩︎ 6. Philbin’s explicit design choices like rejection of all-zero keys and nonces lead to a handful of known differences. ↩︎ 7. See _“Test Vector Leakage Assessment (TVLA) methodology in practice”,_Becker, G. et al., 2013. Naturally, the paper isn’t available online. Academia gonna academia. ↩︎ 8. You should absolutely know what a nonce is. ↩︎ 9. I’ve produced garbage benchmark data enough times that I give myself 50-50 odds that someone reads this article and says “’tis all wrong, you’ve forgot to put the magic beans in the orange juice the night before the full moon!” and they’re right. ↩︎ 10. Thanks, Frank. 🤪 ↩︎
