aws-lc — Engineering Performance
14 engineers all time · Jan 2025 – Sep 2026 · built 2026-09-10 · GitHub
Performance snapshot
Today's rolling 90-day reading for aws-lc, compared with the start of the series. Pick a window to move that comparison point.
Avg. perf / dev / mo
−1.2%
0.42 → 0.42 ETV
Active engineers
−10.0%
10.0 → 9.0
Features
+3.9pp
43.5% → 47.4%
vs. AWS
0.50x
1.3x → 0.50x · −50% below
aws-lc vs. AWS
Per-engineer ETV for aws-lc against AWS as a whole. Both lines are 90-day rolling averages scaled to a 30-day month, so they share one axis and can be read against each other at any point. Pick a window to zoom the chart to it.
Performance Composition
Each month's output split by type of work: Features (new value), Maintenance (sustaining systems), Tests, Docs, and Fixes (rework). The yellow line is output per engineer, so when it rises each engineer is delivering more, whatever the team size did. Unit: Engineering Throughput Value (ETV).
Engineering capacity
Effective engineers behind aws-lc, against its pre-AI baseline. Each subject has its own: aws-lc's is 0.42 ETV / dev / mo, its first reading in Q1 2025. Per-engineer ETV divided by that gives a capacity multiple, and that multiple applied to the engineers active in the trailing 90 days turns it into engineer-equivalents. The line is the real headcount, so the gap between line and area is what the leverage is worth. Because each baseline is its own, every subject opens at 1.0x on its first day: multiples measure improvement and are not comparable between subjects.
Knowledge concentration
How dependent is this repo on a small number of engineers? Higher top-1 share = higher key-person risk.
Sean McGrail owns 21.6 % of commits.
Behind the numbers
Written summary of the work completed each month.
No monthly reports available yet.
Most impactful commits
Top 10 by ETV in the all-time window.
- 6.6ETVImport s2n-bignum 2025-09-05-04 (#2667) Notably, this imports the newly added CFI directives.Dusan Kostic · 3bf53ced · 2025-09-05
- 5.1ETVImplement ragdoll (#2615) This commit moves the ragdoll implementation from the feature branch to main branch. Most (but not all) additions are from the new import method for Jitter Entropy. After this PR, we will import from v3.6.3 tag. There are a few additions compared to the feature branch: build_and_test_ssl_runner_valgrind currently overrides the defined GO timeout value. It shouldn't. So, this PR also removes that to allow the SSL Valgrind runner to finish with the slower Jitter Entropy. Concurrently execute ssl runner tests under Valgrind to improve CI speed Initialize an un-initialized variable in urandom.c.torben-hansen · fc028742 · 2025-09-02
- 3.4ETVopenssl-ca command implementation for self-sign certificates (#2937) ### Description of changes: Adds a fairly limited `openssl ca` command that is targeted towards supporting the specific [efs-utils](https://github.com/aws/efs-utils) use case for generating and self-signing a certificate. ### Call-outs: * This only supports the self-signed certificate use case * Some code paths are retained to make it easier to add support for non-self-signed use cases in the future * A limited set of command-line options were added to support efs-utils use-case, there are more that could be added in the future. There are analogous options in the configuration file that can be provided in lieu of the missing command line arguments. * `openssl ca` has a series of "database" files that it record and tracks metadata in. Some of the behaviors like revocation checking is retained even though generation of revocations is not supported. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.Sean McGrail · b27c81ff · 2026-01-24
- 3.3ETVClean up CLI code (#2927) ### Description of changes: This PR addresses the remaining comments from PRs #2877 and #2898. I also took the liberty to clean up `tool-openssl` to make our code easier to read. ### Testing: Unit tests By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license. --------- Co-authored-by: Samuel Chiang <sachiang@amazon.com>Nghi Ho · 2f1bbe4a · 2026-03-10
- 3.3ETVImport mldsa-native (#2902) ### Issues: Related PRs: - Import-mldsa-native-NTT stress test on delocator [#2903](https://github.com/aws/aws-lc/pull/2903) - AES-GCM: Add function pointer trampolines to avoid delocator issue [#2919](https://github.com/aws/aws-lc/pull/2919) - Service Indicator: Add error call trampoline to avoid delocator issue [#2920](https://github.com/aws/aws-lc/pull/2920) ### Import mldsa-native This imports mldsa-native (https://github.com/pq-code-package/mldsa-native) into AWS-LC. This PR focuses on the minimal configuration of mldsa-native: No assembly and no FIPS-202 code are imported. mldsa-native is a high-performance, high-assurance C90 implementation of ML-DSA developed under the Post-Quantum Cryptography Alliance (PQCA) and the Linux Foundation. It is a fork of the Dilithium reference implementation. ### Import Mechanism The mldsa-native source code is unmodified and imported using the importer script `crypto/fipsmodule/ml_dsa/importer.sh;` the details of the import are in META.yml. A custom config is provided for mldsa-native which in particular includes a small 'compatibility layer' between AWS-LC/OpenSSL and mldsa-native -- see below. ### Future imports (C-only) Future updates of the C-only mldsa-native source tree should happen through a re-import of mldsa-native: That is, (a) delete `crypto/fipsmodule/ml_dsa/mldsa` and (b) re-run import.sh. This will re-import `mldsa-native/main`, though you can set the `GITHUB_SHA` and `GITHUB_REPOSITORY` environment variables to point to any other mldsa-native repository/fork. ### Future imports (native code) Once we have verified meaningful parts of the mldsa-native assembly backends, PRs will be filed to integrate those. The details for this integration are TBD and not necessary to finalize for this PR. The options are (a) extending import.sh to import larger parts of the mldsa-native upstream source tree, including native backends, (b) writing custom backends, backed by sources living in the s2n-bignum source tree. Both is possible and compatible with this PR. ### Import Scope mldsa-native has a C-only version as well as native 'backends' in AVX2 and Neon for high performance. This commit only imports the C-only version. Integration of native backends will be done separately. mldsa-native offers its own FIPS-202 implementation, including fast versions of batched FIPS-202. However, this commit does not import those, but instead provides glue-code around AWS-LC's own FIPS-202 implementation. The path to leveraging the FIPS-202 performance improvements in mldsa-native would be to integrate them directly into `crypto/fipsmodule/sha`. ### Impact on build None. No build-files are modified. The multilevel build process remains unchanged. ### Internal API changes 3 Removed functions: ``` [D] 'function void ml_dsa_44_params_init(ml_dsa_params*)' {ml_dsa_44_params_init} [D] 'function void ml_dsa_65_params_init(ml_dsa_params*)' {ml_dsa_65_params_init} [D] 'function void ml_dsa_87_params_init(ml_dsa_params*)' {ml_dsa_87_params_init} ``` ### Compatibility layer The configuration file `mldsa_native_config.h` includes a compatibility layer between AWS-LC/OpenSSL and mldsa-native, covering: * FIPS/PCT: If AWSLC_FIPS is set, `MLD_CONFIG_KEYGEN_PCT` is enabled to include a PCT. * FIPS/PCT: If `BORINGSSL_FIPS_BREAK_TESTS` is set, `MLD_CONFIG_KEYGEN_PCT_BREAKAGE_TEST` is set and `mld_break_pct` defined via `boringssl_fips_break_test("MLDSA_PWCT")`, to include runtime-breakage of the PCT for testing purposes. * CT: If `BORINGSSL_CONSTANT_TIME_VALIDATION` is set, then `MLD_CONFIG_CT_TESTING_ENABLED` is set to enable valgrind testing. * Zeroization: `MLD_CONFIG_CUSTOM_ZEROIZE` is set and `mld_zeroize` mapped to `OPENSSL_cleanse` to use OpenSSL's zeroization function. * Randombytes: `MLD_CONFIG_CUSTOM_RANDOMBYTES` is set and `mld_randombytes` mapped to `RAND_bytes` to use AWS-LC's randombytes function. ### Side-channels mldsa-native's CI uses a patched version of valgrind to check for various compilers and compile flags that there are no secret-dependent memory accesses, branches, or divisions. The relevant assertions have been kept but are unused unless `MLD_CONFIG_CT_TESTING_ENABLED` is set, which is the case if and only if `BORINGSSL_CONSTANT_TIME_VALIDATION` is set. mldsa-native uses value barriers to block potentially harmful compiler reasoning and optimization. Where standard gcc/clang inline assembly is not available, mldsa-native falls back to a slower 'opt blocker' based on a volatile global -- both are described in ct.h. ### Formal Verification All C-code imported in this commit is formally verified using the C Bounded Model Checker (CBMC) to be free of various classes of undefined behaviour, including out-of-bounds memory accesses and arithmetic overflow; the latter is of particular interest for ML-DSA because of the use of lazy modular reduction for improved performance. The heart of the CBMC proofs are function contract and loop annotations to the C-code. Function contracts are denoted `__contract__(...)` clauses and occur at the time of declaration, while loop contracts are denoted `__loop__` and follow the for statement. The function contract and loop statements are kept in the source, but removed by the preprocessor so long as the CBMC macro is undefined. Keeping them simplifies the import, and care has been taken to make them readable to the non-expert, and thereby serve as precise documentation of assumptions and guarantees upheld by the code. ### FIPS Compliance mldsa-native unconditionally includes stack zeroization. mldsa-native's default secure memset is replaced by `OPENSSL_cleanse`. mldsa-native conditionally includes a PCT, guarded by `MLD_CONFIG_KEYGEN_PCT`. This is set in the config if and only if `AWSLC_FIPS` is set. While not part of the FIPS standard, the `pk_from_sk` function includes validation of both t0 (low-order bits) and tr (hash of public key) using constant-time comparison functions (`mld_ct_memcmp`), providing strong assurance of key consistency. ### Testing We KAT ML-DSA with test vectors obtained from https://github.com/post-quantum-cryptography/KAT within `PQDSAParameterTest.KAT`. We select the KATs for the signing mode `hedged`, which derives the signing private random seed (rho) pseudorandomly from the signer's private key, the message to be signed, and a 256-bit string `rnd` which is generated at random. The `pure` variant of these KATs were used, as they provide test vector inputs for "pure" i.e., non-pre-hashed messages. We also run the ACVP test vectors obtained from https://github.com/usnistgov/ACVP-Server within the three functions `PerMLDSATest.ACVPKeyGen`, `PerMLDSATest.ACVPSigGen` and `PerMLDSATest.ACVPSigVer`. These correspond to the tests found at ML-DSA-keyGen-FIPS204, ML-DSA-sigGen-FIPS204, and ML-DSA-sigVer-FIPS204. To test ML-DSA pure, non-deterministic mode, we use `tgId = 19, 21, 23` of sigGen and `tgId = 7, 9, 11` of sigVer. To test ML-DSA ExternalMu, non-deterministic mode, we use `tgId = 20, 22, 24` of sigGen and `tgId = 8, 10, 12` of sigVer. **Test Results**: - ML-DSA Tests: 100% passing (61/61 tests) ### Formatting Code in `crypto/fipsmodule/ml_dsa/mldsa` is directly imported from mldsa-native and comes with its own `crypto/fipsmodule/ml_dsa/mldsa/.clang-format`. ### Prefix build The prefix build should not be affected by the import, since no definitions of external linkage are imported (everything is tagged either static directly, or `MLD_EXTERNAL_API` or `MLD_INTERNAL_API`, both of which are set to static in the context of the import, too). ### Performance Performance should be comparable to the previous integration as both are based on C-only code with AWS-LC's FIPS-202 implementation. The fast mldsa-native backends are not yet imported. ### Multilevel build At the core, mldsa-native is currently a 'single-level' implementation of ML-DSA: A build of the main source tree provides an implementation of exactly one of ML-DSA-44/65/87, depending on the `MLD_CONFIG_PARAMETER_SET` parameter. To build all security levels, level-specific sources are built 3 times, once per security level, and linked with a single build of the level-independent code. The single-compilation-unit approach pursued by AWS-LC makes this process fairly simple since one merely needs to include the single-compilation-unit file provided by mldsa-native three times, and configure it so that the level-independent code is included only once. The final include moreover #undef'ines all macros defined by mldsa-native, reducing the risk of name clashes with other parts of `crypto/fipsmodule/bcm.c`. Note that this process is entirely internal to ml_dsa.c, and does not affect the AWS-LC build. HashML-DSA: mldsa-native includes lots of HashML-DSA functionality that we dont need in aws-lc. Perhaps we should add config upstream to mldsa-native to choose which of pure/externalmu/hash modes are imported to reduce unused code. ### Main differences from reference implementation mldsa-native is a fork of the ML-DSA reference implementation (Dilithium). The following gives an overview of the major changes: - CBMC and debug annotations, and minor code restructurings or signature changes to facilitate the CBMC proofs. For example, functions are structured to make loop bounds and memory access patterns explicit for formal verification. - Introduction of 4x-batched versions of some functions from the reference implementation. This is to leverage 4x-batched Keccak-f1600 implementations if present. The batching happens at the C level even if no native backend for FIPS 202 is present. - FIPS 204 compliance: Introduced optional PCT (FIPS 204, Section 4.4, Pairwise Consistency) and zeroization of stack buffers as required by (FIPS 204, Section 3.6.3, Destruction of intermediate values). - Introduction of native backend implementations for AVX2. Those are drop-in replacements for the corresponding C functions and dispatched at compile-time. (Not in this PR, but the C code prep is in place). - Restructuring of files to separate level-specific from level-generic functionality. This is needed to enable a multi-level build of mldsa-native where level-generic code is shared between levels. - More pervasive use of value barriers to harden constant-time primitives, even when Link-Time-Optimization (LTO) is enabled. The use of LTO can lead to insecure compilation in case of the reference implementation. ### License mldsa-native (everything under `crypto/fipsmodule/ml_dsa/mldsa/**`) is imported under the Apache 2.0 license and the ISC license. The LICENSE file remains unchanged. Integration-specific code (everything with direct parent `crypto/fipsmodule/ml_dsa/*`) is made under the terms of the Apache 2.0 license and the ISC license. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.Jake Massimo · cb37cb0a · 2026-01-20
- 3.1ETVML-DSA: import and enable x86_64 assembly backend from mldsa-native (#3195) ## Summary - Imports the x86_64 native arithmetic backend from mldsa-native into ML-DSA, providing AVX2-accelerated assembly for core polynomial operations - Only pure assembly (`.S`) files with completed HOL-Light formal verification proofs are imported; C intrinsics and unverified assembly are excluded - Follows the same integration pattern as ML-KEM's x86_64 backend (PR #2631 / commit 3b1e95e4), using s2n-bignum macros for symbol visibility ## Benchmark Measured on Intel Xeon Platinum 8175M @ 2.50GHz (EC2 c5.2xlarge), average of 4 runs (ops/sec, higher is better). "Before" is `bssl speed -filter MLDSA` built from `main`; "After" is the same with this PR applied: ``` ┌──────────────────────┬──────────┬──────────┬─────────────┐ │ Operation │ Before │ After │ Speedup │ ├──────────────────────┼──────────┼──────────┼─────────────┤ │ MLDSA44 keygen │ 14,472 │ 18,151 │ 1.25x │ │ MLDSA44 signing │ 2,343 │ 4,794 │ 2.05x │ │ MLDSA44 verify │ 11,537 │ 19,064 │ 1.65x │ │ MLDSA65 keygen │ 6,607 │ 8,599 │ 1.30x │ │ MLDSA65 signing │ 1,489 │ 3,265 │ 2.19x │ │ MLDSA65 verify │ 8,302 │ 10,894 │ 1.31x │ │ MLDSA87 keygen │ 5,599 │ 6,932 │ 1.24x │ │ MLDSA87 signing │ 1,310 │ 2,862 │ 2.18x │ │ MLDSA87 verify │ 5,705 │ 7,723 │ 1.35x │ └──────────────────────┴──────────┴──────────┴─────────────┘ ``` ## Changes **Commit 1 — Manual/preparatory changes:** - `CMakeLists.txt` — adds ML-DSA x86_64 assembly sources to BCM build via `file(GLOB ... CONFIGURE_DEPENDS "*.S")` - `mldsa_native_config.h` — enables native backend with `MLD_CONFIG_USE_NATIVE_BACKEND_ARITH` - `mldsa_native_backend.h` — platform dispatcher (x86_64 + AVX2 only for now) - `mldsa_x86_64_meta.h` — hand-maintained backend header outside the imported `mldsa/` tree, declaring only the assembly-backed operations we use - `importer.sh` — imports x86_64 backend and processes assembly with s2n-bignum macros **Commit 2 — Clean import output:** - Solely the output of `GITHUB_SHA=08d40f9403a9ca80f160118bc63e96bf36627866 ./importer.sh --force` - Contains no manual changes — reproducible by running the above command ## Functions accelerated All imported functions have completed HOL-Light formal verification proofs: - NTT (forward) — `mldsa_ntt.ml` - INTT (inverse) — `mldsa_intt.ml` - NTT unpack (custom coefficient order) — `mldsa_nttunpack.ml` - Pointwise Montgomery multiplication — `mldsa_pointwise.ml` - Polyvec pointwise accumulate for L=4/5/7 — `mldsa_pointwise_acc_l{4,5,7}.ml` - Conditional add Q (caddq) — `mldsa_caddq.ml` ## Importer simplifications (addressing review feedback) The importer is now significantly simpler compared to the previous revision: - **Removed** `MLD_INTERNAL_API` sed — upstream now uses `MLD_INTERNAL_DATA_DECLARATION/DEFINITION` - **Removed** ELF section move — upstream already places it correctly - **Removed** file renaming — upstream now uses unique `_avx2_asm.S` names (no collision with mlkem) - **Glob copy** for all `.S` files — no need to enumerate each assembly file individually - **Single sed pattern** to strip C-intrinsic includes from BCM (keeps only `consts.c`) - **Simple substitution** for `MLD_ASM_FN_SIZE` → `S2N_BN_SIZE_DIRECTIVE` (upstream now defines it) ## Call-outs - Supersedes #2986 which only imported NTT/INTT — this PR imports all formally verified assembly-backed operations - C intrinsics (rej_uniform, decompose, use_hint, chknorm, polyz_unpack) are intentionally excluded - Runtime dispatch via `CRYPTO_is_AVX2_capable()` — falls back to C on non-AVX2 systems - AArch64 native backend is not included in this PR ## Testing - All ML-DSA tests pass (KAT, Wycheproof, expanded key validation, PQDSA parameter tests) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.Jake Massimo · 340c501f · 2026-06-11
- 2.8ETVAdd Symbol Versioning Support (#3096)Sean McGrail · db0dc865 · 2026-07-06
- 2.0ETVAdd ML-KEM support to HPKE (draft-ietf-hpke-pq-05) (#3277) Implements ML-KEM-512, ML-KEM-768 and ML-KEM-1024 as HPKE KEMs per [draft-ietf-hpke-pq-05](https://datatracker.ietf.org/doc/html/draft-ietf-hpke-pq-05), enabling post-quantum HPKE. Also adds HKDF-SHA384, so the priority suite HPKE(ML-KEM-1024, HKDF-SHA384, AES-256-GCM) is available. Built on AWS-LC's existing ML-KEM implementation in `crypto/fipsmodule/ml_kem/`; no new cryptographic primitives are introduced. ## Spec conformance | | Value | Source | |---|---|---| | KEM IDs | 0x0040 / 0x0041 / 0x0042 | draft §8.1 Table 2, IANA HPKE registry | | Nsecret | 32 (all three) | draft §3 | | Nenc | 768 / 1088 / 1568 | draft §8.1 | | Npk | 800 / 1184 / 1568 | draft §8.1 | | Nsk | **64** (all three) | draft §3 | Three points worth calling out explicitly: **The ML-KEM shared secret is used directly, with no `ExtractAndExpand`.** DHKEM runs its shared secret through `ExtractAndExpand`; ML-KEM does not. The draft defines Encap/Decap as ML-KEM.Encaps and ML-KEM.Decaps directly, and Nsecret = 32 matches ML-KEM's native shared secret length. The generic RFC 9180 key schedule is unchanged and still processes the shared secret. **A private key is the 64-byte `d || z` seed, not the expanded decapsulation key.** FIPS 203 returns the expanded form, `dk = dk_PKE || ek_PKE || H(ek_PKE) || z`: FIPS 203 page 16 <img width="622" height="120" alt="Screenshot 2026-05-29 at 1 46 17 PM" src="https://github.com/user-attachments/assets/4edf7c44-c335-4b8e-a1e6-af367db62940" /> The draft does not use that form. It is explicit: "the decapsulation key is returned in seed format rather than the expanded form returned by ML-KEM.KeyGen", and Nsk is 64 for every parameter set. So `EVP_HPKE_KEM_private_key_len()` returns 64, not 1632/2400/3168, and `EVP_HPKE_MAX_PRIVATE_KEY_LENGTH` stays at 64. That is the serialized form: the seed is what `EVP_HPKE_KEY_init` accepts and what `EVP_HPKE_KEY_private_key` emits. Internally the seed is expanded once, at import or generation, with `ml_kem_*_keypair_deterministic`, which takes exactly the 64-byte seed, and the expanded key is cached in the struct — see the FIPS section for why. This matters for interoperability — every published test vector's `skRm` is 64 bytes, and a peer implementing the draft cannot import an expanded key. BoringSSL reaches the same conclusion independently: [`EVP_HPKE_MAX_PRIVATE_KEY_LENGTH` is 64](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/include/openssl/hpke.h#L72) there too. **Auth mode is refused for ML-KEM.** ML-KEM cannot do AuthEncap/AuthDecap (draft §7.2), so the `auth_encap_with_seed` and `auth_decap` hooks are NULL and `EVP_HPKE_CTX_setup_auth_sender`/`_auth_recipient` fail with `EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE`. The encapsulation key is validated on encap via `ml_kem_*_check_pk`, since the draft requires an encapsulation key check failure to surface as an HPKE EncapError. `mlk_kem_enc_derand` in the backend already performs the same modulus check, so this call is belt-and-braces — it makes the intent explicit and gives the failure a distinguishable reason code. Error codes differ slightly from upstream: a bad ML-KEM peer key raises `EVP_R_INVALID_PEER_KEY` where upstream raises `EVP_R_DECODE_ERROR`. Ours is consistent with the X25519 path in the same file and with RFC 9180's EncapError, so it is intentional. ## Behaviour change to `EVP_HPKE_KEY_cleanup` `EVP_HPKE_KEY_cleanup` was a documented no-op. It now cleanses both secrets — the seed and the cached expanded decapsulation key — and clears `kem`, returning the key to the zero state, and tolerates NULL. Clearing `kem` matters more than it looks. Cleansing alone would leave `kem` set and the public key intact with an all-zero private key — and every 64-byte string is a valid ML-KEM seed, while a zero X25519 scalar is clamped to a valid one. A use-after-cleanup would therefore *succeed*, decapsulating under a key anyone can compute, rather than failing. Clearing `kem` makes that path fail instead. Six entry points read `key->kem` without checking it, so on a key with no KEM they dereference NULL: `EVP_HPKE_KEY_public_key`, `EVP_HPKE_KEY_private_key`, `EVP_HPKE_CTX_setup_recipient`, both auth-sender setups, and `EVP_HPKE_CTX_setup_auth_recipient`. That is pre-existing — `main` has the same unguarded code — but there it was only reachable by passing a key that had never been initialized, and clearing `kem` in cleanup adds a second route to it. All six now fail with `EVP_R_NO_KEY_SET`. `HPKETest.ZeroedKeyFailsCleanly` covers them; with the guards removed it terminates with SIGSEGV rather than failing, so the test detects the guard rather than passing by construction. Secrets also no longer outlive the calls that use them. The derived shared secret is cleansed once the key schedule has consumed it, and the encapsulation entropy is cleansed after sender setup returns, in both the base and auth paths. A failed `EVP_HPKE_KEY_init` or `EVP_HPKE_KEY_generate` now cleanses as well, rather than only clearing `kem`. `mlkem_init_key` derives the expanded decapsulation key straight into the struct, so a failure after that point would otherwise leave key material behind in a key the caller has been told is unusable. ## FIPS builds **Service indicator.** HPKE is not an approved service, so these APIs must leave the service indicator unchanged. Without that, an HPKE call leaves a counter differential from the approved primitives underneath — HKDF (`hkdf.c:35-50`), AES-GCM (`AEAD_GCM_verify_service_indicator`) and `RAND_bytes` (`rand.c:588`) — which a caller would read as an approved service having been performed. The 11 public entry points that perform crypto now lock the indicator for the duration of the call, with the bodies moved to `static` functions; the other ten are lifecycle and accessor functions that only move or clear memory, so there is nothing to suppress. Exported symbols are unchanged. This is not ML-KEM specific — HPKE over X25519 was moving the counter before this change too. Verified on FIPS builds on x86-64 and aarch64: a `ServiceIndicatorNotApproved` test per ML-KEM suite, plus an X25519 test covering the four entry points the ML-KEM suites cannot reach (auth-mode sender, auth-mode recipient, and both deterministic sender setups). Removing only the lock/unlock calls makes the corresponding tests fail, so they detect the lock rather than passing by construction. **Where the keygen PCT lands.** In FIPS builds the ML-KEM key generation entry point runs a pairwise consistency test — a full encapsulation and decapsulation — gated by `MLK_CONFIG_KEYGEN_PCT`, which is set exactly when AWS-LC is built in FIPS mode. The expanded decapsulation key has to be derived through that entry point, because `crypto/fipsmodule/ml_kem/ml_kem.h` exposes no PCT-free seed-expansion function today and `crypto/fipsmodule/ml_kem/mlkem/` is a pristine import driven by `importer.sh`, so it should not be patched from `crypto/hpke`. The struct therefore caches the expanded key at import or generation, so the PCT is paid once per key rather than on every decapsulation. That placement is deliberate: without the cache, a key generation health test with fatal module-failure semantics would sit on a path reached from the network. One consequence worth noting for reviewers is that `EVP_HPKE_KEY_init`, which imports a caller-supplied seed rather than generating a key, also pays the PCT. ## ABI impact — needs a maintainer decision `struct evp_hpke_key_st` is public and stack-allocatable. An ML-KEM-1024 encapsulation key is 1568 bytes, so the struct cannot keep holding keys in 32-byte inline arrays; `sizeof(EVP_HPKE_KEY)` goes from 72 to 4808 bytes. `EVP_HPKE_MAX_PUBLIC_KEY_LENGTH` and `EVP_HPKE_MAX_ENC_LENGTH` also change from 32 to 1568, and the struct additionally caches the expanded ML-KEM decapsulation key (`EVP_HPKE_MAX_EXPANDED_PRIVATE_KEY_LENGTH`, 3168 bytes) so that decapsulation performs no key generation. See the FIPS section above for why that cache is there. The four `abidiff` jobs will therefore report an ABI change, and there is no suppression mechanism in `.github/docker_images/abidiff/diff.sh` — it fails on any `abidiff` exit >= 4. Per `docs/SymbolVersioning.md` this implies an `ABI_VERSION` bump and a new SONAME, which is a release-level decision affecting every consumer, so I have not made it here. **Please advise whether you want the bump in this PR or handled as part of a release.** Some notes to inform that decision: - No layout preserves ABI. Any struct able to hold a 1568-byte key changes size, so the break is unavoidable rather than a consequence of this particular design. - The layout follows [upstream BoringSSL's](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/include/openssl/hpke.h#L394-L398) fixed-size-inline-array approach, though it is no longer byte-identical to it: we additionally cache the expanded decapsulation key, which upstream does not, because upstream's seed expansion is PCT-free and ours is not. Adding a future KEM is still a matter of raising these constants rather than changing the shape of the struct. - An earlier revision of this PR used heap pointers inside the struct, which kept `sizeof` small but made `EVP_HPKE_KEY` a non-trivially-copyable type with owning pointers behind an API documented as stack-allocatable. That caused several problems — re-initialising a key leaked its old key material, `EVP_HPKE_KEY_copy(k, k)` freed the key and returned success, and `EVP_HPKE_KEY_zero` silently stopped scrubbing the private key. Fixed-size inline storage removes that whole class of bug, and matches how the library handles the same situation elsewhere (`union evp_aead_ctx_st_state`'s `opaque[564]` in `include/openssl/aead.h`, and `CRYPTO_MUTEX`'s sized padding in `include/openssl/thread.h`). Two ECH stack buffers in libssl are sized by these macros and grow accordingly (`ssl/handshake_client.cc:339`, `ssl/encrypted_client_hello.cc:522`). ECH negotiates only DHKEM(X25519, HKDF-SHA256), so they cannot actually be filled beyond 32 bytes; sizing them by the X25519 lengths instead would avoid the growth, but that is a separate cleanup and is left out to keep this PR focused. Relatedly, `ECHServerConfig::Init` now rejects an ECHConfig whose `kem_id` is not DHKEM(X25519, HKDF-SHA256). It already required the config's `kem_id` to match the configured key, but an `EVP_HPKE_KEY` can now hold an ML-KEM key, so a config and key which agreed on ML-KEM were accepted for a protocol that is only defined over X25519. `SSL_marshal_ech_config` takes the KEM from the key, so such a config was reachable through the public API. `SSLTest.UnsupportedECHConfig` covers it, and fails if the check is removed. ## Relationship to BoringSSL Upstream implements the same draft. Links below are pinned to [`e5a214a2`](https://github.com/google/boringssl/commit/e5a214a259892b5b9b9384a69d2beb61fb8c3521): - [`crypto/hpke/hpke.cc`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/crypto/hpke/hpke.cc) — their implementation, in particular [`struct MLKEMHPKE`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/crypto/hpke/hpke.cc#L811-L913), [`PRIVATE_KEY_LEN = MLKEM_SEED_BYTES`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/crypto/hpke/hpke.cc#L813) and [`HpkeDecap`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/crypto/hpke/hpke.cc#L895-L911), which re-expands the stored seed on every decapsulation. This change stores the same 64-byte seed, but expands it once at import or generation and caches the result, for the FIPS reason above. - [`include/openssl/hpke.h`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/include/openssl/hpke.h) — their public header, in particular [`struct evp_hpke_key_st`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/include/openssl/hpke.h#L394-L398) and [`EVP_HPKE_MAX_PRIVATE_KEY_LENGTH 64`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/include/openssl/hpke.h#L72), which independently corroborates the seed key format. This change deliberately follows upstream's public API while diverging on implementation: - **API alignment.** Macros are `EVP_HPKE_MLKEM512` / `_MLKEM768` / `_MLKEM1024` (no `KEM_` infix), matching upstream's spelling, as `EVP_hpke_mlkem768` and `EVP_HPKE_HKDF_SHA384` already did. `EVP_HPKE_MAX_PRIVATE_KEY_LENGTH 64` and the seed-format private key match upstream too, so consumers built against either library see the same API and the same serialized key format. - **Kept in C.** Upstream's HPKE is now C++ (`hpke.cc`), part of a library-wide C-to-C++ migration — their `crypto/fipsmodule` has no `.c` files left. AWS-LC is not following that migration, so this stays in C. Upstream's ML-KEM parameterisation needs C++ templates because their ML-KEM API is built on opaque types; ours is byte buffers plus lengths, so a small `MLKEM_METHOD` table of function pointers expresses the same thing and the three parameter sets share one implementation. - **Uses AWS-LC's ML-KEM.** Upstream's HPKE is written against its own `<openssl/mlkem.h>` and a BCM layer (`BCM_mlkem768_encap_external_entropy`) that AWS-LC does not have. We use `crypto/fipsmodule/ml_kem/`, which already exposes deterministic encapsulation directly, so no equivalent plumbing is needed. - **ML-KEM-512 is exposed.** Upstream ships only 768 and 1024. The draft registers 512 and includes it "in the interest of completeness" while preferring 768/1024, so it is available here for callers that need it. ## Testing Known-answer tests come from the WG's machine-readable vectors, the `[TestVectors]` citation in the draft: `crypto/hpke/test-vectors-pq.json`, fetched from https://github.com/hpkewg/hpke-pq. These are vendored the same way RFC 9180's `test-vectors.json` already is, so `translate_test_vectors.py` stays reproducible. Following upstream, which keeps [`hpke_test_vectors_pq.txt`](https://github.com/google/boringssl/blob/e5a214a259892b5b9b9384a69d2beb61fb8c3521/crypto/hpke/hpke_test_vectors_pq.txt) separate from the RFC 9180 file, the PQ vectors are generated into their own `crypto/hpke/hpke_test_vectors_pq.txt`. `crypto/hpke/hpke_test_vectors.txt` is regenerated and is byte-for-byte unchanged from `main`. Three suites are covered — (ML-KEM-512, HKDF-SHA256, AES-128-GCM), (ML-KEM-768, HKDF-SHA256, AES-128-GCM) and (ML-KEM-1024, HKDF-SHA384, AES-256-GCM). The fourth ML-KEM vector in the JSON uses TurboSHAKE256, which this library does not implement, and is filtered out by the script. These are real KATs, so they pin the wire format rather than just internal self-consistency: `enc` fixes Nenc and the encapsulation, `skRm` at 64 bytes fixes Nsk, `pkRm` fixes Npk, and the ciphertexts and exported values fix the key schedule including the HKDF-SHA384 (Nh = 48) path. I confirmed the vectors are genuinely exercised by corrupting one `enc` value and checking the suite fails, rather than trusting a green run. The vector harness gained an optional `kem_id` attribute, defaulting to DHKEM(X25519) when absent so the RFC 9180 vectors are unaffected. ML-KEM vectors carry `ikmE` and `enc` where DHKEM vectors carry `skEm` and `pkEm`, because ML-KEM has no ephemeral key pair — encapsulation takes 32 bytes of entropy and emits a ciphertext. `HPKETest.RoundTrip` now sweeps every KEM rather than just X25519, as upstream's does, skipping auth mode for the ML-KEM KEMs. That covers combinations the fixed parameter table misses, notably ML-KEM-512/768 with HKDF-SHA384 and with ChaCha20-Poly1305, across three `info` and three `ad` values. New tests beyond the KATs cover round-trip and multi-message sealing for each suite, key serialization round-trip, copy/move including self-copy and self-move, auth-mode rejection, re-initialising an already-initialised key, rejection of an invalid encapsulation key on encap, an ML-KEM-1024 encapsulation key passed where an `enc` is expected, seed perturbation producing a distinct valid key, implicit rejection of a corrupted encapsulation, use-after-cleanup failing, re-initialization after cleanup succeeding, the zero state after a failed initialization, every entry point rejecting a key with no KEM, cleanup of a NULL key and cleanup leaving the zero state, the service indicator behaviour described above, and the length and buffer-size error paths. Every negative test asserts the specific reason code rather than just a false return, matching the existing X25519 tests. One note on the vendored JSON: the WG file double-encodes `info` and `pt` — the values are the hex encoding of an ASCII hex string, so `info` decodes to the text `4f6465206f6e2061204772656369616e2055726e` rather than to "Ode on a Grecian Urn". The published ciphertexts were computed over those literal bytes, so they are passed through verbatim. There is a comment in `translate_test_vectors.py` to stop someone "fixing" it later. ## Performance 1000 iterations. Encap/Decap are `setup_sender`/`setup_recipient`, so they include the key schedule. Keygen is measured separately, and for ML-KEM it carries the seed expansion. Re-measured after the expanded key was cached, so decap no longer expands the seed. | Ciphersuite | Keygen | Encap | Decap | Encap+Decap | |---|---|---|---|---| | X25519 + SHA256 + AES-128-GCM | 28.7 us | 25.9 us | 18.5 us | 44.4 us | | ML-KEM-512 + SHA256 + AES-128-GCM | 5.6 us | 8.0 us | 7.3 us | 15.3 us | | ML-KEM-768 + SHA256 + AES-256-GCM | 8.1 us | 10.7 us | 10.6 us | 21.3 us | | ML-KEM-1024 + SHA384 + AES-256-GCM | 10.5 us | 15.0 us | 15.7 us | 30.7 us | Non-FIPS `RelWithDebInfo` build, Apple M-series, macOS. Relative numbers are what matter; these are not comparable to the x86 figures in an earlier revision of this description. Every ML-KEM suite is faster than X25519 DHKEM here, including ML-KEM-1024. The seed expansion sits in the keygen column rather than in decap, which is the point of caching it: a key is imported or generated once and then decapsulates many times. FIPS builds add the keygen PCT to keygen and to key import, as described above; these numbers do not include it. ## Re-verified against draft-ietf-hpke-pq-05 Draft -05 was published on 6 July 2026, after this work started. Section 3 (ML-KEM) is unchanged from -04, and the IANA parameter table rows for 0x0040/0x0041/0x0042 are identical, so nothing normative moved for this implementation and no code change was needed. The section numbers cited here and in the code (§3, §7.2, §8.1) are unchanged in -05. The published test vectors *were* regenerated in -05 — none of the -04 ML-KEM values survive. The vendored `test-vectors-pq.json` is already the -05 set: all four ML-KEM suites' `ikmE`, `skRm` and `shared_secret` values appear in -05 and none appear in -04. Regenerating from it leaves `hpke_test_vectors_pq.txt` byte-identical, so the KATs are current and were not touched. One item for future work: TurboSHAKE is now RFC 9861 rather than `draft-irtf-cfrg-kangarootwelve`, which matters if the single-stage SHAKE/TurboSHAKE KDFs (0x0010-0x0013) are implemented later. ## Out of scope Deliberately not included, to keep this reviewable: - The PQ/T hybrids, including X-Wing (0x647a). - The single-stage SHAKE and TurboSHAKE KDFs (0x0010-0x0013), and therefore the one ML-KEM test vector that uses TurboSHAKE256. - `DeriveKeyPair` / `EVP_HPKE_KEY_derive`, which the draft defines over SHAKE256. Upstream has it. Not required by anything here, since the KATs load `skRm` directly, but it does mean the vectors' `ikmR` path is not exercised. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.Jake Massimo · 3b4b0895 · 2026-09-03
- 2.0ETVImplement more options for req CLI (#2775)Nghi Ho · a3c40ea4 · 2025-11-11
- 1.7ETVCreate pre-production stage for CI pipeline (#2282) ### Issues: Resolves #CryptoAlg-2983 and #CryptoAlg-2984 ### Description of changes: This PR implements steps to deploy to pre-production in the CI pipeline, which will act as template for production deployment later on. The PR also sets up a dev deployment environment for testing purposes. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.Nghi Ho · 3c87703e · 2025-05-27