Visitar URL original
AES-XTS encrypt: prove the len<0x20 memory-safety lemma by sgmenda · Pull Request #451 · awslabs/s2n-bignum · GitHub
Skip to content

AES-XTS encrypt: prove the len<0x20 memory-safety lemma - #451

Open
sgmenda wants to merge 3 commits into
awslabs:mainfrom
sgmenda:xts-memsafe
Open

sgmenda wants to merge 3 commits into
awslabs:mainfrom
sgmenda:xts-memsafe

Conversation

@sgmenda

@sgmenda sgmenda commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Prove AES_XTS_ENCRYPT_LT_2BLOCK_MEMSAFE: for AES-256-XTS encrypt of a single full block plus optional tail (16 <= len < 32), every memory access stays in-bounds. Memory-safety only (in-bounds event trace), no CHEATing.

The readable region includes ctxt_p: cipherstealing reads back the previously-written ciphertext, which the caller owns. Rationale and an address-layout diagram are in the comment at the lemma. The MAYCHANGE frame is declared narrowly up front with ENSURES_FRAME_SUBSUMED at each segment, matching the idiom already used elsewhere in this file. (h/t @manastasova)

This is the first per-length-class lemma; the general AES256_XTS_ENCRYPT_MEMSAFE will dispatch on len over these (mirroring AES256_XTS_ENCRYPT_CORRECT). Follow-up PRs will add the remanining length classes, constant-time, and decrypt.

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.

Prove AES_XTS_ENCRYPT_LT_2BLOCK_MEMSAFE: for AES-256-XTS encrypt of a single full block plus optional tail (16 <= len < 32), every memory access stays in-bounds. Memory-safety only (in-bounds event trace), no CHEAT.

The readable region includes ctxt_p: cipher stealing reads back the previously-written ciphertext, which the caller owns. Rationale and an address-layout diagram are in the comment at the lemma.

This is the first per-length-class lemma; the general AES256_XTS_ENCRYPT_MEMSAFE will dispatch on len over these (mirroring AES256_XTS_ENCRYPT_CORRECT), so it is kept, not replaced. Remaining length classes, subroutine wrapper, constant-time, and decrypt are follow-ups.
@sgmenda sgmenda self-assigned this Sep 8, 2026

@manastasova manastasova left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal definition looks good to me - it matches AES_XTS_ENCRYPT_LT_2BLOCK_CORRECT, and the readable/writable lists look right.
I haven't gone through every lemma/tactic, so please don't take this as a blocker. I was wondering if you could try avoiding the four SUBSUMED_* lemmas and the xts_ms_* surgery group?

Comment thread arm/proofs/aes_xts_encrypt.ml Resolved
Comment thread arm/proofs/aes_xts_encrypt.ml Resolved
Comment thread arm/proofs/aes_xts_encrypt.ml Resolved
Comment thread arm/proofs/aes_xts_encrypt.ml Resolved
sgmenda and others added 2 commits September 16, 2026 17:57
@sgmenda

sgmenda commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, @manastasova for the tip.

Per your suggestion, I narrowed the frame up front with ENSURES_FRAME_SUBSUMED, which let me drop the four SUBSUMED_*/MAYCHANGE_BYTES* lemmas and the xts_ms_* surgery.

@nebeid nebeid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No soundness objection — the lemma statement and the val len < 0x20 bound check out.

Two substantive items: the rebase, and the file-local SAFETY_EXISTS_TAC, which re-implements DISCHARGE_MEMSAFE_ASM_TAC from your own cbc763a hoist — three sites (the definition, the use at 5802, and the open-coded copy at 5936-5943). Then a time prove nit and one optional comment request.

Suggestions attached where they apply, but I have not run HOL Light — treat the tactic ones as proposals to try.

(* (see the comment on the read-back at the cipher-stealing tail below). *)
(* ========================================================================= *)

needs "arm/proofs/consttime.ml";;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase needed due to #452's generalization for 32-bit backends

(* *)
(* Every access offset is < len, so all lie within the named regions. *)
(* ------------------------------------------------------------------------- *)
let AES_XTS_ENCRYPT_LT_2BLOCK_MEMSAFE = prove(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its correctness twin AES_XTS_ENCRYPT_LT_2BLOCK_CORRECT (:2547) is time prove, as is every other simulation-level theorem in this file: :1509, :2722, :2891, :3064. The only two that aren't — AES256_XTS_ENCRYPT_CORRECT (:3246) and AES_XTS_ENCRYPT_SUBROUTINE_CORRECT (:5563) — are the ones that just dispatch and wrap, with no ARM_STEPS_TAC. This lemma is on the simulation side of that line.

With four more length classes plus decrypt queued, that timing line is the only per-theorem cost signal — CI reports just the 4h17m aggregate.

Suggested change
let AES_XTS_ENCRYPT_LT_2BLOCK_MEMSAFE = prove(
let AES_XTS_ENCRYPT_LT_2BLOCK_MEMSAFE = time prove(

Comment on lines +5632 to +5663
(* ------------------------------------------------------------------------- *)
(* File-local helper tactic for the memory-safety proof, kept file-local *)
(* mirroring arm/proofs/mlkem_rej_uniform_VARIABLE_TIME.ml. The MAYCHANGE *)
(* frame is narrowed up front with ENSURES_FRAME_SUBSUMED at each segment *)
(* (as elsewhere in this file), so no bespoke frame-widening machinery is *)
(* needed here. *)
(* ------------------------------------------------------------------------- *)

(* Discharge `exists e2. read events s = APPEND e2 e /\ memaccess_inbounds` *)
(* for a CONCRETE event trace (events = CONS ... e), as arises at a *)
(* straight-line final state. The prefix e2 is read off the events equation. *)
let SAFETY_EXISTS_TAC : tactic =
fun (asl,w) ->
let evth = try snd(find (fun (_,th) -> let c = concl th in is_eq c &&
can (fun () -> ignore(find_term (fun s -> s = `events`) (lhs c))) ()) asl)
with _ -> failwith "SAFETY_EXISTS_TAC: no events assumption" in
let rhs_t = rhs (concl evth) in
let rec split_at_e t =
if is_comb t && is_comb(rator t) &&
(try fst(dest_const(rator(rator t)))="CONS" with _ -> false)
then let h = rand(rator t) in let (tl,hs) = split_at_e (rand t) in (tl, h::hs)
else (t, []) in
let tl,hs = split_at_e rhs_t in ignore tl;
let prefix = mk_list(hs, type_of(hd hs)) in
(EXISTS_TAC prefix THEN CONJ_TAC THENL
[ ASM_REWRITE_TAC[APPEND];
REWRITE_TAC[memaccess_inbounds; ALL; EX] THEN
REWRITE_TAC[contained; DIMINDEX_64] THEN REPEAT CONJ_TAC THEN
FIRST [ REWRITE_TAC[CONTAINED_MODULO_REFL] THEN DISJ1_TAC THEN ASM_ARITH_TAC;
REPEAT ((DISJ1_TAC THEN CONTAINED_TAC) ORELSE DISJ2_TAC ORELSE
CONTAINED_TAC) ] ]) (asl,w);;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DISCHARGE_MEMSAFE_ASM_TAC already does this, from your own cbc763a ("Hoist loop-based memory-safety tactics into common/consttime.ml", 2026-09-01) — which is also the commit that emptied the mlkem_rej_uniform_VARIABLE_TIME.ml precedent cited at 5633-5634. No rebase needed to reach it: common/consttime.ml:1079 on this head, in scope via arm/proofs/consttime.ml:10.

Two bugs in the local copy, both failure modes rather than unsoundness: the find can match the precondition read events s0 = e (then hd hs raises), and ignore tl never checks the tail is e.

Suggested change
(* ------------------------------------------------------------------------- *)
(* File-local helper tactic for the memory-safety proof, kept file-local *)
(* mirroring arm/proofs/mlkem_rej_uniform_VARIABLE_TIME.ml. The MAYCHANGE *)
(* frame is narrowed up front with ENSURES_FRAME_SUBSUMED at each segment *)
(* (as elsewhere in this file), so no bespoke frame-widening machinery is *)
(* needed here. *)
(* ------------------------------------------------------------------------- *)
(* Discharge `exists e2. read events s = APPEND e2 e /\ memaccess_inbounds` *)
(* for a CONCRETE event trace (events = CONS ... e), as arises at a *)
(* straight-line final state. The prefix e2 is read off the events equation. *)
let SAFETY_EXISTS_TAC : tactic =
fun (asl,w) ->
let evth = try snd(find (fun (_,th) -> let c = concl th in is_eq c &&
can (fun () -> ignore(find_term (fun s -> s = `events`) (lhs c))) ()) asl)
with _ -> failwith "SAFETY_EXISTS_TAC: no events assumption" in
let rhs_t = rhs (concl evth) in
let rec split_at_e t =
if is_comb t && is_comb(rator t) &&
(try fst(dest_const(rator(rator t)))="CONS" with _ -> false)
then let h = rand(rator t) in let (tl,hs) = split_at_e (rand t) in (tl, h::hs)
else (t, []) in
let tl,hs = split_at_e rhs_t in ignore tl;
let prefix = mk_list(hs, type_of(hd hs)) in
(EXISTS_TAC prefix THEN CONJ_TAC THENL
[ ASM_REWRITE_TAC[APPEND];
REWRITE_TAC[memaccess_inbounds; ALL; EX] THEN
REWRITE_TAC[contained; DIMINDEX_64] THEN REPEAT CONJ_TAC THEN
FIRST [ REWRITE_TAC[CONTAINED_MODULO_REFL] THEN DISJ1_TAC THEN ASM_ARITH_TAC;
REPEAT ((DISJ1_TAC THEN CONTAINED_TAC) ORELSE DISJ2_TAC ORELSE
CONTAINED_TAC) ] ]) (asl,w);;

Paired with the swap at 5802. Untested — I have not run HOL Light.

ASM_REWRITE_TAC[]; ALL_TAC] THEN
ARM_STEPS_TAC AES256_XTS_ENCRYPT_EXEC (3--118) THEN
ENSURES_FINAL_STATE_TAC THEN
REPEAT CONJ_TAC THEN TRY(FIRST_X_ASSUM ACCEPT_TAC) THEN SAFETY_EXISTS_TAC;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
REPEAT CONJ_TAC THEN TRY(FIRST_X_ASSUM ACCEPT_TAC) THEN SAFETY_EXISTS_TAC;
REPEAT CONJ_TAC THEN TRY(FIRST_X_ASSUM ACCEPT_TAC) THEN DISCHARGE_MEMSAFE_ASM_TAC;

Untested: DISCHARGE_MEMSAFE_ASM_TAC opens with SAFE_META_EXISTS_TAC allowed_vars_e (metavariable) where the local tactic does an explicit EXISTS_TAC, so allowed_vars_e may need seeding first. Worth a trial before taking it.

Comment on lines +5936 to +5943
(EXISTS_TAC
`APPEND [EventStore (ctxt_p:int64,16); EventLoad (ctxt_p:int64,16); EventJump (word(pc+2568),word(pc+2572))] e_acc` THEN
CONJ_TAC THENL
[ REWRITE_TAC[APPEND];
REWRITE_TAC[MEMACCESS_INBOUNDS_APPEND] THEN CONJ_TAC THENL
[ (REWRITE_TAC[memaccess_inbounds; ALL; EX; FST; SND] THEN REPEAT CONJ_TAC THEN
REPEAT ((DISJ1_TAC THEN CONTAINED_ASM_TAC) ORELSE DISJ2_TAC ORELSE CONTAINED_ASM_TAC));
FIRST_ASSUM ACCEPT_TAC ]])]]]);;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point as the SAFETY_EXISTS_TAC thread: DISCHARGE_MEMSAFE_ASM_TAC's body open-coded (common/consttime.ml:1079-1090, your cbc763a), with SAFE_META_EXISTS_TAC/EXISTS_E2_TAC swapped for an EXISTS_TAC of a literal event list. The library tactic avoids the literals, which is what lets these leaves survive an instruction-schedule change — otherwise each remaining length class needs its own list.

Suggested change
(EXISTS_TAC
`APPEND [EventStore (ctxt_p:int64,16); EventLoad (ctxt_p:int64,16); EventJump (word(pc+2568),word(pc+2572))] e_acc` THEN
CONJ_TAC THENL
[ REWRITE_TAC[APPEND];
REWRITE_TAC[MEMACCESS_INBOUNDS_APPEND] THEN CONJ_TAC THENL
[ (REWRITE_TAC[memaccess_inbounds; ALL; EX; FST; SND] THEN REPEAT CONJ_TAC THEN
REPEAT ((DISJ1_TAC THEN CONTAINED_ASM_TAC) ORELSE DISJ2_TAC ORELSE CONTAINED_ASM_TAC));
FIRST_ASSUM ACCEPT_TAC ]])]]]);;
DISCHARGE_MEMSAFE_ASM_TAC]]]);;

Untested, and this one needs a careful look: the suggestion keeps the ]]]);; outer closers and drops the (/[/[ opened on the replaced lines. Check the balance before taking it.

(* ARM_STEPS derives their code non-overlap from the PAIRWISE *)
(* nonoverlapping hypothesis. *)
REPEAT STRIP_TAC THEN
SUBGOAL_THEN `val (len:int64) = 16 + val (word_and len (word 15))` ASSUME_TAC THENL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional nit. The 16 + i < val len obligation is already an explicit subgoal at 5858, so nothing is hidden. What's missing is why the equation on this line holds — it is exactly what val len < 0x20 buys, and it's the step each remaining length class has to re-derive.

Suggested change
SUBGOAL_THEN `val (len:int64) = 16 + val (word_and len (word 15))` ASSUME_TAC THENL
(* 16 <= val len < 0x20 means exactly one full block, so the tail is *)
(* val len - 16. Each length class re-derives its own version. *)
SUBGOAL_THEN `val (len:int64) = 16 + val (word_and len (word 15))` ASSUME_TAC THENL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants