Repository navigation
Spec: out-of-place AEAD encryption from a list of slices (AES-GCM, ChaCha20-Poly1305) - #1209
Merged
Merged
Conversation
…aCha20-Poly1305) Contracts for encrypting a plaintext held in buffers the caller does not own into a separate output buffer, as a TLS record layer must (#990): * Spec/Gcm/OutOfPlace.lean: vg_aes_gcm_encrypt_blocks_to, vg_aes_gcm_stream_encrypt_to and vg_aes_gcm_seal_gather, and their _precomputed forms: the existing functions with the plaintext read from src (for seal_gather, the concatenation of the slices a list of slices lists) and the ciphertext written to dst. * Spec/ChaCha20/OutOfPlace.lean: vg_chacha20_xor_to and vg_chacha20_apply_to, the same for the keystream XOR. * Spec/ChaCha20Poly1305/OutOfPlace.lean: vg_chacha20_poly1305_seal_gather. No TCB change: the output is a mutable slice, which Sig.contract already keeps disjoint from every other buffer and from the listed slices. Existing contracts are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
5 tasks done
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
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.
Trust change: new specs only. Three new files:
lean/VerifiedGarbage/Spec/Gcm/OutOfPlace.leanlean/VerifiedGarbage/Spec/ChaCha20/OutOfPlace.leanlean/VerifiedGarbage/Spec/ChaCha20Poly1305/OutOfPlace.leanNo TCB change, no implementation, no generated code. Existing contracts are unchanged.
This is the first step for #990. The implementations follow in stacked PRs.
Why
A TLS record layer gets the plaintext in buffers it doesn't own. It must write
ciphertext ‖ taginto a separate output buffer. TodayAesGcmandChaCha20Poly1305only encrypt in place, so the rustls provider (#991) copies each record intooutand then encrypts it there. The copy is about 9% of an AES-GCM record's cost.What rustls's
RecordEncrypter::encrypthands the provider (at the revision #991 pins):OutboundPlain::Single(chunk)or::Multiple { chunks, .. }, then a one-byte inner content type that is encrypted tooout= ciphertext ‖ tagout= 8-byte explicit nonce ‖ ciphertext ‖ tag; the explicit nonce is outside the AEADout= ciphertext ‖ tagrustls's aws-lc-rs provider uses aws-lc-rs 1.18's
seal_out_of_place_scatter(nonce, aad, in, out, extra_in, extra_out_and_tag), with the content-type byte asextra_in, when the plaintext is a single chunk. ForMultiple, it still gathers intooutand seals in place, becauseseal_out_of_place_scatterneeds a contiguous input.API
The functions are iovec-style, as suggested in #990: a gather input and one contiguous output. The plaintext is a list of slices,
Param.slices(*const [usize; 2]plus a count), asvg_aes_siv_encrypt's associated data already is. The ciphertext goes to onedstslice.That covers all three rows above in one call, including
Multiple, which aws-lc-rs doesn't:[chunk…, &[typ]].[chunk…], withdststarting after the explicit nonce.No TCB change is needed.
Sig.contractalready makes amutbuffer disjoint from every other buffer, including the listed slices and their descriptors. So the output never overlaps the input, and in-place callers keep using the existing functions. Exact aliasing (dst == src) would need a newParamkind inTCB/Sig.lean, and nothing needs it.AES-GCM (
Spec/Gcm/OutOfPlace.lean)vg_aes_gcm_seal_gather(ctx, rounds, nonce, nonce_len, aad, aad_len, src: *const [usize; 2], src_count, dst: *mut u8, len, tag)rounds ∈ {10, 12, 14}, and the listed slices totallenbytes (gatheredLen).encryptWith … (gathered src) aad = (dst, tag). This issealPostwith the plaintext gathered.vg_aes_gcm_stream_encrypt_to(…, src, len, dst, dst_len)streamEncryptPostwith the plaintext read fromsrc.dst_len = len.AesGcmEncryptor::update_into). The plannedseal_gathercalls it once per slice.vg_aes_gcm_encrypt_blocks_to(…, src, n, dst, dst_n, scratch)encryptBlocksContract's postcondition with the plaintext blocks read fromsrc.dst_n = n._precomputedforms of all three, for a key context ofvg_aes_gcm_init_precomputed. They work exactly as the existing_precomputedfunctions do (PowersRepradded to the precondition, same postcondition).ChaCha20 (
Spec/ChaCha20/OutOfPlace.lean)vg_chacha20_xor_to(state, src, len, dst, dst_len, buf):xorContractwith the data read fromsrc.vg_chacha20_apply_to(state, src, len, dst, dst_len) -> u32:applyContractwith the data read fromsrc. It may leakleftAt, asapplydoes.ChaCha20-Poly1305 (
Spec/ChaCha20Poly1305/OutOfPlace.lean)vg_chacha20_poly1305_seal_gather(key, nonce, aad, aad_len, src: *const [usize; 2], src_count, dst, len, tag)encrypt key nonce aad (gathered src) = (dst, tag).lenbytes, andlen ≤ P_MAX(2³² − 1 blocks of 64 bytes).sealContract, whose counter wraps, this needs theP_MAXbound so it can be built onapply_to, which refuses to pass the end of the keystream. The Rust wrapper already rejects longer texts (check_len).Constant time and lengths
Only the pointers, lengths,
src_countand where the slices are (their descriptors) are public.Sig.contractalready makes descriptor contents public forParam.slices. Each_tooutput has its own length argument because everyParam.slicecarries one; the preconditions require it to equal the input's, and the# Safetydocs say so.Planned Rust API (in the implementation PRs)
rustls's TLS 1.3 encrypter then becomes
key.encrypt(&nonce, &aad, &[plain, &[typ]], &mut out[..n]), with the tag written after it.Decryption stays in place, which is what rustls does.
Checks
lake build +VerifiedGarbage.Spec.Gcm.OutOfPlace +VerifiedGarbage.Spec.ChaCha20.OutOfPlace +VerifiedGarbage.Spec.ChaCha20Poly1305.OutOfPlacepasses.ci/check_lean_imports.py,ci/check_lean_speed.py,ci/check_vectors.pyandci/check_arch_gates.pypass.🤖 Generated with Claude Code
https://claude.ai/code/session_01MmkzVkkRyGFLYBPdnXPeVb
Generated by Claude Code