import sys
try:
from z3 import (
BitVec, BitVecSort, BitVecVal, Concat, Extract, Function, If,
Solver, prove, sat, unsat,
)
except ImportError:
print("z3 not installed: pip install z3-solver", file=sys.stderr)
sys.exit(2)
B = BitVecSort(128)
def byte(b, i):
return Extract(127 - 8 * i, 120 - 8 * i, b)
def bytes_be_u32(b3, b2, b1, b0):
return Concat(b3, b2, b1, b0)
def check(name, lhs, rhs):
s = Solver()
s.add(lhs != rhs)
r = s.check()
if r == unsat:
print(f" PROVED {name}")
return True
print(f" FAILED {name}: counterexample {s.model() if r == sat else r}")
return False
def check_bool(name, claim):
s = Solver()
s.add(claim == False) r = s.check()
if r == unsat:
print(f" PROVED {name}")
return True
print(f" FAILED {name}: {s.model() if r == sat else r}")
return False
def gcm_increment_counter(b):
low = bytes_be_u32(byte(b, 12), byte(b, 13), byte(b, 14), byte(b, 15))
low = low + BitVecVal(1, 32)
new = bytes_be_u32(
Extract(31, 24, low), Extract(23, 16, low),
Extract(15, 8, low), Extract(7, 0, low),
)
return Concat(Extract(127, 32, b), new)
def gcm_inc32_spec(b):
return Concat(Extract(127, 32, b), Extract(31, 0, b) + BitVecVal(1, 32))
def siv_increment_counter(b):
low = bytes_be_u32(byte(b, 3), byte(b, 2), byte(b, 1), byte(b, 0))
low = low + BitVecVal(1, 32)
nb0 = Extract(7, 0, low)
nb1 = Extract(15, 8, low)
nb2 = Extract(23, 16, low)
nb3 = Extract(31, 24, low)
return Concat(nb0, nb1, nb2, nb3, Extract(95, 0, b))
def siv_inc_spec(b):
le = bytes_be_u32(byte(b, 3), byte(b, 2), byte(b, 1), byte(b, 0)) + BitVecVal(1, 32)
nb0 = Extract(7, 0, le)
nb1 = Extract(15, 8, le)
nb2 = Extract(23, 16, le)
nb3 = Extract(31, 24, le)
return Concat(nb0, nb1, nb2, nb3, Extract(95, 0, b))
def gcm_j0(nonce96):
return Concat(nonce96, BitVecVal(1, 32))
def siv_derive_input(counter_u32_le_bytes, nonce96):
return Concat(counter_u32_le_bytes, nonce96)
def le32(i):
v = BitVecVal(i, 32)
return Concat(Extract(7, 0, v), Extract(15, 8, v), Extract(23, 16, v), Extract(31, 24, v))
E_enc = Function("AES_enc", B, B)
def siv_tag_model(nonce96, digest):
hi = Extract(127, 32, digest) ^ nonce96
masked = Concat(hi, Extract(31, 0, digest))
cleared = Concat(Extract(127, 8, masked), Extract(7, 0, masked) & BitVecVal(0x7F, 8))
return E_enc(cleared)
def siv_counter_init(tag):
return Concat(Extract(127, 8, tag), Extract(7, 0, tag) | BitVecVal(0x80, 8))
def main():
ok = True
b = BitVec("b", 128)
nonce96 = BitVec("nonce", 96)
digest = BitVec("digest", 128)
print("1. Counter increments match their specs (all 2^128 blocks) ...")
ok &= check("GCM increment_counter == SP 800-38D inc_32",
gcm_increment_counter(b), gcm_inc32_spec(b))
ok &= check("SIV increment_siv_counter == RFC 8452 LE32 increment",
siv_increment_counter(b), siv_inc_spec(b))
ok &= check("GCM increment leaves the leading 96 bits unchanged",
Extract(127, 32, gcm_increment_counter(b)), Extract(127, 32, b))
ok &= check("SIV increment leaves the trailing 96 bits unchanged",
Extract(95, 0, siv_increment_counter(b)), Extract(95, 0, b))
print("\n2. Block-construction layouts match the specs (all inputs) ...")
ok &= check("GCM J0 == IV || 0^31 || 1",
gcm_j0(nonce96), Concat(nonce96, BitVecVal(1, 32)))
for i in (0, 1, 2, 3, 4, 5):
blk = siv_derive_input(le32(i), nonce96)
ok &= check(f"SIV derive input[{i}] low 4 bytes == LE32({i})",
Extract(127, 96, blk), le32(i))
ok &= check(f"SIV derive input[{i}] high 12 bytes == nonce",
Extract(95, 0, blk), nonce96)
print("\n3. SIV tag construction and CTR init (RFC 8452 s4) ...")
tag = siv_tag_model(nonce96, digest)
init = siv_counter_init(tag)
ok &= check("SIV CTR init sets bit 0x80 of the last byte of the tag",
Extract(7, 0, init), Extract(7, 0, tag) | BitVecVal(0x80, 8))
ok &= check("SIV CTR init leaves all other bytes equal to the tag",
Extract(127, 8, init), Extract(127, 8, tag))
print("\n4. Decryption inverts encryption and accepts genuine ciphertext ...")
GHASH = Function("GHASH", B, B) E_gcm = Function("AES_gcm", B, B) p = BitVec("p", 128)
j0_seal = gcm_j0(nonce96)
ks_seal = E_gcm(gcm_increment_counter(j0_seal))
c_gcm = p ^ ks_seal
tag_seal_gcm = GHASH(c_gcm) ^ E_gcm(j0_seal)
j0_open = gcm_j0(nonce96)
ks_open = E_gcm(gcm_increment_counter(j0_open))
p_rec_gcm = c_gcm ^ ks_open
tag_open_gcm = GHASH(c_gcm) ^ E_gcm(j0_open)
ok &= check("GCM open recovers the plaintext block", p_rec_gcm, p)
ok &= check("GCM open re-derives the sealed tag (genuine ciphertext accepts)",
tag_open_gcm, tag_seal_gcm)
POLY = Function("POLYVAL_digest", B, B)
tag_siv = siv_tag_model(nonce96, POLY(p))
ks_siv_seal = E_enc(siv_counter_init(tag_siv))
c_siv = p ^ ks_siv_seal
ks_siv_open = E_enc(siv_counter_init(tag_siv))
p_rec_siv = c_siv ^ ks_siv_open
tag_recomputed_siv = siv_tag_model(nonce96, POLY(p_rec_siv))
ok &= check("SIV open recovers the plaintext block", p_rec_siv, p)
ok &= check("SIV open re-derives the sealed tag (genuine ciphertext accepts)",
tag_recomputed_siv, tag_siv)
print("\n5. Non-vacuity: the harness rejects a deliberately broken wiring ...")
broken_ks = E_gcm(j0_open) broken_rec = c_gcm ^ broken_ks
s = Solver()
s.add(broken_rec != p)
found = s.check() == sat
if found:
print(" PROVED a missing-counter-increment open is detected (counterexample exists)")
else:
print(" FAILED broken wiring was NOT detected - the harness is vacuous")
ok &= found
if ok:
print("\nPROVED for all inputs: the GCM and SIV composition glue (J0/counter")
print("construction, counter increments, SIV key-derivation and tag layout,")
print("and CTR-inverts-CTR round-trip) matches SP 800-38D / RFC 8452, given a")
print("correct AES and authenticator (supplied by the field proofs and the")
print("FIPS-197 / CAVP / RFC 8452 known-answer suites).")
return 0
print("\nFAILED")
return 1
if __name__ == "__main__":
sys.exit(main())