Repository navigation
x86-64 AES-GCM: verified out-of-place encryption from a list of slices (vg_aes_gcm_seal_gather) and AesGcm::encrypt - #1228
Merged
Merged
Conversation
AesGcm::encrypt and ChaCha20Poly1305::encrypt encrypt the concatenation of a list of slices into a separate output buffer, and AesGcmEncryptor::update_into encrypts the next piece of a stream from one buffer into another (#990). A TLS record layer is handed the plaintext in buffers it does not own, often in pieces, plus TLS 1.3's content-type byte, and must write the ciphertext to its own output: this is the API it needs, the one-call equivalent of aws-lc-rs's seal_out_of_place_scatter (which also takes several pieces). For now they gather the pieces into the output and encrypt it there with the in-place functions, on every architecture; the verified out-of-place functions (contracts in the Spec PR) replace that copy target by target without changing the API. Every length is checked before anything is written, and an output of the wrong length is InvalidOutputLength. Tests: every split of a message into pieces against in-place encryption, and the Wycheproof vectors in two pieces (and update_into). Benchmarks: out-of-place encryption of a TLS 1.3-shaped record (plaintext plus the content-type byte) against aws-lc-rs's seal_out_of_place_scatter and OpenSSL's EVP interface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
vg_aes_gcm_encrypt_blocks_to (and _precomputed) encrypts the first 16*floor(n/16) blocks from src to dst with the variant's out-of-place interleaved loops, if it has them (so far StitchZTo, on 512-bit registers), then copies the rest to dst and encrypts them there with the variant's vg_aes_gcm_encrypt_blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
The text so far ending a block, the whole blocks go out of place through vg_aes_gcm_encrypt_blocks_to on the state's counter block and GHASH accumulator; the rest is copied and encrypted in place by vg_aes_gcm_stream_encrypt. Constant time, registration and Rust to follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
…l data is whole blocks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
…pt_to) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
…nerate the x86-64 asm AesGcm::encrypt streams a text of at least STREAM_MIN_LEN (4096) bytes from its pieces to the output with vg_aes_gcm_stream_encrypt_to; a shorter one costs less copied into the output and encrypted there in one call. update_into pads the additional data to a whole block before the first block of text, so that the whole blocks go from the input to the output in one pass from the start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
…e counts only on failure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
AesGcm::encrypt(nonce, aad, plaintext, out) encrypts one buffer into another. On x86-64 it is one call of vg_aes_gcm_stream_encrypt_to, after padding the additional data to a whole block (as GHASH does) so that its whole blocks go from the input to the output in one pass; elsewhere it copies and encrypts in place. Drops update_into, the list of pieces and the length cutoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
…as update_precomputed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
… slices vg_aes_gcm_seal_gather (and _precomputed) encrypts a plaintext given as a list of slices, out of place, in one call: it runs the streaming functions on a state of its own, absorbing the additional data padded with zeros to a whole block when there is text, then calling vg_aes_gcm_stream_encrypt_to on each slice, from where it is to the output, and vg_aes_gcm_stream_finish. Proved correct and constant time against sealGatherContract from the callees' shared contracts, in a frame of 240 bytes for its working space. AesGcm::encrypt takes the plaintext as a slice of slices (at most MAX_PIECES) and is one call of it on x86-64; elsewhere it copies the pieces into the output and encrypts in place. The Rust code no longer calls vg_aes_gcm_stream_encrypt_to directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
alex
enabled auto-merge
October 8, 2026 03:45
alex
pushed a commit
that referenced
this pull request
Oct 8, 2026
The record encrypters no longer copy the plaintext into the output buffer before encrypting it there: AES-GCM records go through AesGcm::encrypt (#1228), which reads the payload's chunks (and, for TLS 1.3, the content type byte) where they are and writes only the record. ChaCha20-Poly1305 has the contracts (#1209) but no implementation yet, so it, and an AES-GCM plaintext in more pieces than AesGcm::encrypt takes, still copy and encrypt in place. tests/records.rs checks that every suite seals contiguous and fragmented plaintext (up to 200 pieces) alike, writing every byte, and opens what it sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAf76RQfGxGymBaZASPY7q
alex
pushed a commit
that referenced
this pull request
Oct 8, 2026
The record encrypters no longer copy the plaintext into the output buffer before encrypting it there: AES-GCM records go through AesGcm::encrypt (#1228), which reads the payload's chunks (and, for TLS 1.3, the content type byte) where they are and writes only the record. ChaCha20-Poly1305 has the contracts (#1209) but no implementation yet, so it, and an AES-GCM plaintext in more pieces than AesGcm::encrypt takes, still copy and encrypt in place. tests/records.rs checks that every suite seals contiguous and fragmented plaintext (up to 200 pieces) alike, writing every byte, and opens what it sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAf76RQfGxGymBaZASPY7q
This was referenced Oct 8, 2026
alex
pushed a commit
that referenced
this pull request
Oct 9, 2026
The record encrypters no longer copy the plaintext into the output buffer before encrypting it there: AES-GCM records go through AesGcm::encrypt (#1228), which reads the payload's chunks (and, for TLS 1.3, the content type byte) where they are and writes only the record. ChaCha20-Poly1305 has the contracts (#1209) but no implementation yet, so it, and an AES-GCM plaintext in more pieces than AesGcm::encrypt takes, still copy and encrypt in place. tests/records.rs checks that every suite seals contiguous and fragmented plaintext (up to 200 pieces) alike, writing every byte, and opens what it sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAf76RQfGxGymBaZASPY7q
This was referenced Oct 9, 2026
alex
pushed a commit
that referenced
this pull request
Oct 10, 2026
The record encrypters no longer copy the plaintext into the output buffer before encrypting it there: AES-GCM records go through AesGcm::encrypt (#1228), which reads the payload's chunks (and, for TLS 1.3, the content type byte) where they are and writes only the record. ChaCha20-Poly1305 has the contracts (#1209) but no implementation yet, so it, and an AES-GCM plaintext in more pieces than AesGcm::encrypt takes, still copy and encrypt in place. tests/records.rs checks that every suite seals contiguous and fragmented plaintext (up to 200 pieces) alike, writing every byte, and opens what it sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAf76RQfGxGymBaZASPY7q
This was referenced Oct 10, 2026
alex
pushed a commit
that referenced
this pull request
Oct 10, 2026
The record encrypters no longer copy the plaintext into the output buffer before encrypting it there: AES-GCM records go through AesGcm::encrypt (#1228), which reads the payload's chunks (and, for TLS 1.3, the content type byte) where they are and writes only the record. ChaCha20-Poly1305 has the contracts (#1209) but no implementation yet, so it, and an AES-GCM plaintext in more pieces than AesGcm::encrypt takes, still copy and encrypt in place. tests/records.rs checks that every suite seals contiguous and fragmented plaintext (up to 200 pieces) alike, writing every byte, and opens what it sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAf76RQfGxGymBaZASPY7q
This was referenced Oct 11, 2026
alex
pushed a commit
that referenced
this pull request
Oct 13, 2026
The record encrypters no longer copy the plaintext into the output buffer before encrypting it there: AES-GCM records go through AesGcm::encrypt (#1228), which reads the payload's chunks (and, for TLS 1.3, the content type byte) where they are and writes only the record. ChaCha20-Poly1305 has the contracts (#1209) but no implementation yet, so it, and an AES-GCM plaintext in more pieces than AesGcm::encrypt takes, still copy and encrypt in place. tests/records.rs checks that every suite seals contiguous and fragmented plaintext (up to 200 pieces) alike, writing every byte, and opens what it sealed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAf76RQfGxGymBaZASPY7q
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #990. This implements the out-of-place contracts of #1209 on x86-64,
vg_aes_gcm_seal_gatherincluded, and adds a non-in-place AEAD encrypt on top that is one asm call.No
TCB/orSpec/changes. This is an implementation PR:Impl/+Proof/+ registration (in the generic AES-GCM file) + generated code + Rust.Rust API
One new method, one constant and two new error variants:
&[buf].vg_aes_gcm_seal_gather(or_precomputed), with the pieces' descriptors in a stack array. The array is why the number of pieces is capped.outand encrypts it there.Verified functions (x86-64, every AES-GCM variant)
vg_aes_gcm_encrypt_blocks_to(and_precomputed):Spec.Gcm.encryptBlocksToContract._vaes_vpclmul_avx512variants get new out-of-place interleaved loops (StitchZTo).dstand callvg_aes_gcm_encrypt_blocks.vg_aes_gcm_stream_encrypt_to(and_precomputed):Spec.Gcm.streamEncryptToContract.encrypt_blocks_towhen the text so far ends a block, and is not empty or follows additional data of whole blocks.dstand encrypts it there withvg_aes_gcm_stream_encrypt.vg_aes_gcm_seal_gather(and_precomputed):Spec.Gcm.sealGatherContract.Impl/AesGcm/X86_64/SealGather.leanruns the streaming functions on a state of its own:stream_init, thenstream_aad.stream_aad. GHASH pads it the same way, so the message is the same (Proof.Gcm.streamRepr_padded). The padding letsstream_encrypt_totake the whole blocks in one pass even after a 5- or 13-byte TLS header.stream_encrypt_toper slice, from where the slice is to the output, thenstream_finish.Proof/AesGcm/X86_64/Gather/) work from the callees' shared contracts (Gather/Callee.lean): correctness through a loop invariant over the slices, and constant time with the relational framework. The descriptors are public, as the contract says.Verified.stackArgScratchL), 5128 bytes of stack in all.Proof/AesGcm/ScratchGather.leanshows the conditions read only the buffers, the descriptors and the slices.Benchmarks
Group
aes-128-gcm-encrypt-out-of-place(one piece), against OpenSSL'sCrypterand aws-lc-rs'sseal_out_of_place_scatter. Local AVX-512 run, key setup included in each iteration:The single call is 4–13% faster than this PR's previous stream-based
encrypt.Checks
Emit.lean --checkpass, with the emitter's audits.ci/scripts pass, includingcheck_variantsandcheck_mcdt.cargo fmtandclippy -D warningspass on x86-64 (with and withoutalloc), AArch64, x86 and ARMv7.MAX_PIECESpieces, too many pieces, the precomputed path.🤖 Generated with Claude Code
https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb