Skip to content

x86-64 AES-GCM: verified out-of-place encryption from a list of slices (vg_aes_gcm_seal_gather) and AesGcm::encrypt - #1228

Merged
alex merged 12 commits into
mainfrom
claude/compassionate-fermi-rjdmyl
Oct 8, 2026
Merged

alex merged 12 commits into
mainfrom
claude/compassionate-fermi-rjdmyl

Conversation

@alex

@alex alex commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Part of #990. This implements the out-of-place contracts of #1209 on x86-64, vg_aes_gcm_seal_gather included, and adds a non-in-place AEAD encrypt on top that is one asm call.

No TCB/ or Spec/ 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:

impl AesGcm {
    pub const MAX_PIECES: usize = 64;
    /// Encrypts the plaintext made of `plaintext`'s pieces, in order, into `out`
    /// (exactly as long as they are in all) and returns the tag.
    pub fn encrypt(&self, nonce: &[u8], aad: &[u8], plaintext: &[&[u8]], out: &mut [u8]) -> Result<[u8; 16], Error>;
}
// Error::InvalidOutputLength: `out` is not as long as the pieces in all.
// Error::TooManyPieces: more than MAX_PIECES pieces.
  • A plaintext in one buffer is one piece: &[buf].
  • On x86-64 it is one call of the verified 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.
  • On other targets it copies the pieces into out and encrypts it there.

Verified functions (x86-64, every AES-GCM variant)

  • vg_aes_gcm_encrypt_blocks_to (and _precomputed): Spec.Gcm.encryptBlocksToContract.
    • The _vaes_vpclmul_avx512 variants get new out-of-place interleaved loops (StitchZTo).
    • The other variants copy the blocks to dst and call vg_aes_gcm_encrypt_blocks.
  • vg_aes_gcm_stream_encrypt_to (and _precomputed): Spec.Gcm.streamEncryptToContract.
    • It takes the whole blocks with one call of encrypt_blocks_to when the text so far ends a block, and is not empty or follows additional data of whole blocks.
    • It copies the rest to dst and encrypts it there with vg_aes_gcm_stream_encrypt.
    • It runs in a 2232-byte frame, 4856 bytes of stack in all.
  • vg_aes_gcm_seal_gather (and _precomputed): Spec.Gcm.sealGatherContract.
    • Impl/AesGcm/X86_64/SealGather.lean runs the streaming functions on a state of its own: stream_init, then stream_aad.
    • If there is any text, it then pads the additional data with zeros to a whole block with a second stream_aad. GHASH pads it the same way, so the message is the same (Proof.Gcm.streamRepr_padded). The padding lets stream_encrypt_to take the whole blocks in one pass even after a 5- or 13-byte TLS header.
    • Then one stream_encrypt_to per slice, from where the slice is to the output, then stream_finish.
    • The proofs (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.
    • It runs in a 240-byte frame for its working space (Verified.stackArgScratchL), 5128 bytes of stack in all. Proof/AesGcm/ScratchGather.lean shows the conditions read only the buffers, the descriptors and the slices.

Benchmarks

Group aes-128-gcm-encrypt-out-of-place (one piece), against OpenSSL's Crypter and aws-lc-rs's seal_out_of_place_scatter. Local AVX-512 run, key setup included in each iteration:

bytes ours aws-lc-rs OpenSSL
64 177 ns 221 ns 1.05 µs
384 282 ns 249 ns 1.07 µs
1024 273 ns 304 ns 1.13 µs
16384 1.37 µs 1.37 µs 2.30 µs

The single call is 4–13% faster than this PR's previous stream-based encrypt.

Checks

  • The full Lean build and Emit.lean --check pass, with the emitter's audits.
  • The ci/ scripts pass, including check_variants and check_mcdt.
  • cargo fmt and clippy -D warnings pass on x86-64 (with and without alloc), AArch64, x86 and ARMv7.
  • The AES-GCM unit tests pass on every backend configuration this CPU can run.
    • Covered: pieces split on and off block boundaries, empty pieces, exactly MAX_PIECES pieces, too many pieces, the precomputed path.
  • The Wycheproof vectors pass out of place, with the plaintext whole and in two pieces.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb

claude added 8 commits October 7, 2026 13:51
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
…l data is whole blocks

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
claude added 2 commits October 7, 2026 15:05
…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
@alex alex changed the title x86-64 AES-GCM: verified out-of-place encryption (encrypt_blocks_to, stream_encrypt_to) and its Rust API x86-64 AES-GCM: verified out-of-place encryption and AesGcm::encrypt Oct 7, 2026
claude added 2 commits October 7, 2026 21:23
…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 alex changed the title x86-64 AES-GCM: verified out-of-place encryption and AesGcm::encrypt x86-64 AES-GCM: verified out-of-place encryption from a list of slices (vg_aes_gcm_seal_gather) and AesGcm::encrypt Oct 8, 2026
@alex
alex enabled auto-merge October 8, 2026 03:45
@alex
alex added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 93f5444 Oct 8, 2026
47 checks passed
@alex
alex deleted the claude/compassionate-fermi-rjdmyl branch October 8, 2026 03:50
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
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
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
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants