Case 756
The key was rotated and the old key was still required
the_key_was_rotated_and_the_old_key_was_still_required.eml - Encryption keys rotate every ninety days, automatically, verified by a canary decrypt after each rotation, with no failed decrypts in eight rotations. What rotation changes is computed below.
ok: true — round-trip fixpoint reached (python1 == python2)updated 2026-09-08
EML
eml# Self-authored for the EML case corpus (no external origin). Encryption keys
# rotate every ninety days, automatically, verified by a canary decrypt after
# each rotation, with no failed decrypts in eight rotations. What rotation
# changes is computed below.
#
# The rotation is properly automated. It is not a runbook someone runs when
# they remember; it is scheduled, it re-wraps every data key under the new key
# encryption key, a canary object is decrypted after each rotation before the
# rotation is declared done, the old key encryption key is retired only after
# that canary passes, and eight rotations have completed with no failed
# decrypts and no application involvement.
#
# Rotation changes the key that WRAPS the data keys. The ciphertext itself is
# not rewritten, so every object is still encrypted under the data key it was
# written with, and a data key that leaked before a rotation still opens them.
#
# Forty-one million objects have never been re-encrypted.
90 => rotation_period_days
8 => rotations_completed
0 => failed_decrypts
1 => canary_decrypts_per_rotation
41000000 => encrypted_objects
0 => objects_re_encrypted_under_a_new_data_key
0 => re_encryption_jobs_run
14 => estimated_days_to_re_encrypt_everything
rotations_completed * rotation_period_days => days_of_rotation_history
encrypted_objects - objects_re_encrypted_under_a_new_data_key => objects_a_pre_rotation_data_key_still_opens
int(objects_re_encrypted_under_a_new_data_key * 10000 / encrypted_objects) => re_encrypted_per_myriad
"rotation period, days : " + str(rotation_period_days) ^0
"rotations completed : " + str(rotations_completed) ^0
" days of history : " + str(days_of_rotation_history) ^0
" canary decrypts per rotation : " + str(canary_decrypts_per_rotation) ^0
" failed decrypts : " + str(failed_decrypts) ^0
"" ^0
"encrypted objects : " + str(encrypted_objects) ^0
" re-encrypted under a new data key : " + str(objects_re_encrypted_under_a_new_data_key) ^0
" share : " + str(re_encrypted_per_myriad) + " per ten thousand" ^0
" a pre-rotation data key still opens : " ^0
" " + str(objects_a_pre_rotation_data_key_still_opens) ^0
"" ^0
"re-encryption jobs run : " + str(re_encryption_jobs_run) ^0
"estimated days to re-encrypt everything : " + str(estimated_days_to_re_encrypt_everything) ^0
"" ^0
# ---- what rotation verified ----
"the rotation" ^0
" scheduled or remembered : scheduled, every " + str(rotation_period_days) + " days" ^0
" what it re-wraps : every data key, under the new key" ^0
" encryption key" ^0
" proof before it is declared done : a canary object is" ^0
" decrypted, " + str(canary_decrypts_per_rotation) + " per rotation" ^0
" when the old wrapping key is retired : only after that" ^0
" canary passes" ^0
" rotations completed : " + str(rotations_completed) + ", failed decrypts " + str(failed_decrypts) ^0
" verdict : ROTATED" ^0
"" ^0
" retiring the old key only after a real decrypt is the" ^0
" step that turns this from a calendar entry into a" ^0
" guarantee, and it is done every time" ^0
"" ^0
# ---- which key moved ----
"the two keys" ^0
" the key encryption key : rotated, " + str(rotations_completed) + " times" ^0
" the data key on each object : unchanged since the write" ^0
" what the ciphertext is encrypted under : the data key" ^0
" what rotation rewrites : the wrapping, not the object" ^0
" objects rewritten : " + str(objects_re_encrypted_under_a_new_data_key) ^0
" what a leaked data key from before a rotation opens : " ^0
" " + str(objects_a_pre_rotation_data_key_still_opens) + " objects, still" ^0
"" ^0
" the rotation is complete over the layer it names, and" ^0
" the layer below it has not moved in " + str(days_of_rotation_history) + " days" ^0
"" ^0
# ---- what rotation is for ----
# Rotation bounds the window in which a compromised KEY ENCRYPTION KEY is
# useful, and it does that exactly. The thing people usually believe it bounds
# is the window in which a compromised object is readable, and no wrapping
# operation can affect that.
"the two windows" ^0
" a compromised key encryption key is useful for : at" ^0
" most " + str(rotation_period_days) + " days, which is what rotation buys" ^0
" a compromised data key is useful for : the lifetime of" ^0
" the objects it wrote" ^0
" what shortens the second : re-encrypting the objects" ^0
" re-encryption jobs run : " + str(re_encryption_jobs_run) ^0
" estimated cost if run : " + str(estimated_days_to_re_encrypt_everything) + " days" ^0
"" ^0
# ---- what the canary proves ----
"the canary decrypt" ^0
" what it reads : one object" ^0
" what it proves : the new wrapping key can unwrap a data" ^0
" key, and that data key still opens its object" ^0
" is that the right check for the rotation : yes, exactly" ^0
" does it say anything about the data key's age : no" ^0
" the data key it exercised : the same one as last time" ^0
"" ^0
# ---- null control ----
# The same rotation, with a re-encryption pass that rewrites objects under a
# fresh data key on a schedule of its own.
1 => nc_re_encryption_jobs_run
encrypted_objects => nc_objects_re_encrypted
0 => nc_objects_a_pre_rotation_data_key_still_opens
"null control - the objects are rewritten too" ^0
" rotation period : " + str(rotation_period_days) + " days, unchanged" ^0
" re-encryption jobs : " + str(nc_re_encryption_jobs_run) ^0
" objects re-encrypted : " + str(nc_objects_re_encrypted) ^0
" objects a pre-rotation data key still opens : " ^0
" " + str(nc_objects_a_pre_rotation_data_key_still_opens) ^0
" the rotation did not become more frequent; the layer it" ^0
" never touched acquired a rotation of its own" ^0
"" ^0
# ---- the rule ----
"what key rotation guarantees" ^0
" a compromised wrapping key stops being useful within a" ^0
" period : exactly, " + str(rotations_completed) + " times, proved by a real decrypt" ^0
" old data stops being readable with old material : not" ^0
" addressed; rotation rewrites the wrapping and the" ^0
" ciphertext is not the wrapping" ^0
"" ^0
"a rotation bounds the exposure of the thing it rotates;" ^0
"where a key hierarchy has two layers and only the upper one" ^0
"turns, the guarantee is exact and the quantity people say it" ^0
"out loud about belongs to the layer that did not move" ^0
"" ^0
"Rotation is scheduled rather than remembered, re-wraps every data key, proves" ^0
"itself with a real decrypt before retiring the old key, and has completed " ^0
str(rotations_completed) + " times with " + str(failed_decrypts) + " failed decrypts. It rewrites the wrapping and not the" ^0
"ciphertext, so " + str(objects_re_encrypted_under_a_new_data_key) + " of " + str(encrypted_objects) + " objects have been re-encrypted - " ^0
str(re_encrypted_per_myriad) + " per ten thousand - across " + str(days_of_rotation_history) + " days and " + str(re_encryption_jobs_run) + " re-encryption jobs." ^0Python (deterministic transpilation)
pythonrotation_period_days = 90
rotations_completed = 8
failed_decrypts = 0
canary_decrypts_per_rotation = 1
encrypted_objects = 41000000
objects_re_encrypted_under_a_new_data_key = 0
re_encryption_jobs_run = 0
estimated_days_to_re_encrypt_everything = 14
days_of_rotation_history = rotations_completed * rotation_period_days
objects_a_pre_rotation_data_key_still_opens = encrypted_objects - objects_re_encrypted_under_a_new_data_key
re_encrypted_per_myriad = int(objects_re_encrypted_under_a_new_data_key * 10000 / encrypted_objects)
print("rotation period, days : " + str(rotation_period_days))
print("rotations completed : " + str(rotations_completed))
print(" days of history : " + str(days_of_rotation_history))
print(" canary decrypts per rotation : " + str(canary_decrypts_per_rotation))
print(" failed decrypts : " + str(failed_decrypts))
print("")
print("encrypted objects : " + str(encrypted_objects))
print(" re-encrypted under a new data key : " + str(objects_re_encrypted_under_a_new_data_key))
print(" share : " + str(re_encrypted_per_myriad) + " per ten thousand")
print(" a pre-rotation data key still opens : ")
print(" " + str(objects_a_pre_rotation_data_key_still_opens))
print("")
print("re-encryption jobs run : " + str(re_encryption_jobs_run))
print("estimated days to re-encrypt everything : " + str(estimated_days_to_re_encrypt_everything))
print("")
print("the rotation")
print(" scheduled or remembered : scheduled, every " + str(rotation_period_days) + " days")
print(" what it re-wraps : every data key, under the new key")
print(" encryption key")
print(" proof before it is declared done : a canary object is")
print(" decrypted, " + str(canary_decrypts_per_rotation) + " per rotation")
print(" when the old wrapping key is retired : only after that")
print(" canary passes")
print(" rotations completed : " + str(rotations_completed) + ", failed decrypts " + str(failed_decrypts))
print(" verdict : ROTATED")
print("")
print(" retiring the old key only after a real decrypt is the")
print(" step that turns this from a calendar entry into a")
print(" guarantee, and it is done every time")
print("")
print("the two keys")
print(" the key encryption key : rotated, " + str(rotations_completed) + " times")
print(" the data key on each object : unchanged since the write")
print(" what the ciphertext is encrypted under : the data key")
print(" what rotation rewrites : the wrapping, not the object")
print(" objects rewritten : " + str(objects_re_encrypted_under_a_new_data_key))
print(" what a leaked data key from before a rotation opens : ")
print(" " + str(objects_a_pre_rotation_data_key_still_opens) + " objects, still")
print("")
print(" the rotation is complete over the layer it names, and")
print(" the layer below it has not moved in " + str(days_of_rotation_history) + " days")
print("")
print("the two windows")
print(" a compromised key encryption key is useful for : at")
print(" most " + str(rotation_period_days) + " days, which is what rotation buys")
print(" a compromised data key is useful for : the lifetime of")
print(" the objects it wrote")
print(" what shortens the second : re-encrypting the objects")
print(" re-encryption jobs run : " + str(re_encryption_jobs_run))
print(" estimated cost if run : " + str(estimated_days_to_re_encrypt_everything) + " days")
print("")
print("the canary decrypt")
print(" what it reads : one object")
print(" what it proves : the new wrapping key can unwrap a data")
print(" key, and that data key still opens its object")
print(" is that the right check for the rotation : yes, exactly")
print(" does it say anything about the data key's age : no")
print(" the data key it exercised : the same one as last time")
print("")
nc_re_encryption_jobs_run = 1
nc_objects_re_encrypted = encrypted_objects
nc_objects_a_pre_rotation_data_key_still_opens = 0
print("null control - the objects are rewritten too")
print(" rotation period : " + str(rotation_period_days) + " days, unchanged")
print(" re-encryption jobs : " + str(nc_re_encryption_jobs_run))
print(" objects re-encrypted : " + str(nc_objects_re_encrypted))
print(" objects a pre-rotation data key still opens : ")
print(" " + str(nc_objects_a_pre_rotation_data_key_still_opens))
print(" the rotation did not become more frequent; the layer it")
print(" never touched acquired a rotation of its own")
print("")
print("what key rotation guarantees")
print(" a compromised wrapping key stops being useful within a")
print(" period : exactly, " + str(rotations_completed) + " times, proved by a real decrypt")
print(" old data stops being readable with old material : not")
print(" addressed; rotation rewrites the wrapping and the")
print(" ciphertext is not the wrapping")
print("")
print("a rotation bounds the exposure of the thing it rotates;")
print("where a key hierarchy has two layers and only the upper one")
print("turns, the guarantee is exact and the quantity people say it")
print("out loud about belongs to the layer that did not move")
print("")
print("Rotation is scheduled rather than remembered, re-wraps every data key, proves")
print("itself with a real decrypt before retiring the old key, and has completed ")
print(str(rotations_completed) + " times with " + str(failed_decrypts) + " failed decrypts. It rewrites the wrapping and not the")
print("ciphertext, so " + str(objects_re_encrypted_under_a_new_data_key) + " of " + str(encrypted_objects) + " objects have been re-encrypted - ")
print(str(re_encrypted_per_myriad) + " per ten thousand - across " + str(days_of_rotation_history) + " days and " + str(re_encryption_jobs_run) + " re-encryption jobs.")stdout (executed)
textrotation period, days : 90
rotations completed : 8
days of history : 720
canary decrypts per rotation : 1
failed decrypts : 0
encrypted objects : 41000000
re-encrypted under a new data key : 0
share : 0 per ten thousand
a pre-rotation data key still opens :
41000000
re-encryption jobs run : 0
estimated days to re-encrypt everything : 14
the rotation
scheduled or remembered : scheduled, every 90 days
what it re-wraps : every data key, under the new key
encryption key
proof before it is declared done : a canary object is
decrypted, 1 per rotation
when the old wrapping key is retired : only after that
canary passes
rotations completed : 8, failed decrypts 0
verdict : ROTATED
retiring the old key only after a real decrypt is the
step that turns this from a calendar entry into a
guarantee, and it is done every time
the two keys
the key encryption key : rotated, 8 times
the data key on each object : unchanged since the write
what the ciphertext is encrypted under : the data key
what rotation rewrites : the wrapping, not the object
objects rewritten : 0
what a leaked data key from before a rotation opens :
41000000 objects, still
the rotation is complete over the layer it names, and
the layer below it has not moved in 720 days
the two windows
a compromised key encryption key is useful for : at
most 90 days, which is what rotation buys
a compromised data key is useful for : the lifetime of
the objects it wrote
what shortens the second : re-encrypting the objects
re-encryption jobs run : 0
estimated cost if run : 14 days
the canary decrypt
what it reads : one object
what it proves : the new wrapping key can unwrap a data
key, and that data key still opens its object
is that the right check for the rotation : yes, exactly
does it say anything about the data key's age : no
the data key it exercised : the same one as last time
null control - the objects are rewritten too
rotation period : 90 days, unchanged
re-encryption jobs : 1
objects re-encrypted : 41000000
objects a pre-rotation data key still opens :
0
the rotation did not become more frequent; the layer it
never touched acquired a rotation of its own
what key rotation guarantees
a compromised wrapping key stops being useful within a
period : exactly, 8 times, proved by a real decrypt
old data stops being readable with old material : not
addressed; rotation rewrites the wrapping and the
ciphertext is not the wrapping
a rotation bounds the exposure of the thing it rotates;
where a key hierarchy has two layers and only the upper one
turns, the guarantee is exact and the quantity people say it
out loud about belongs to the layer that did not move
Rotation is scheduled rather than remembered, re-wraps every data key, proves
itself with a real decrypt before retiring the old key, and has completed
8 times with 0 failed decrypts. It rewrites the wrapping and not the
ciphertext, so 0 of 41000000 objects have been re-encrypted -
0 per ten thousand - across 720 days and 0 re-encryption jobs.Trace event types
eml:run:starteml:assigneml:outputeml:run:done