Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
168 lines
8.5 KiB
Python
168 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
# -----------------------------------------------------------------------------
|
|
# agentdid_session_smt.py
|
|
#
|
|
# SMT proof (z3) of the SESSION AUTHORIZATION invariants for AereAgentDID: the
|
|
# per-session cumulative SPEND CAP, the expiry / revocation / root-lifecycle
|
|
# gating, and the anti-replay action nonce.
|
|
#
|
|
# Contract: contracts/contracts/pqc/AereAgentDID.sol (authorize / issueSession /
|
|
# revokeSession / _isSessionLive)
|
|
#
|
|
# This is a hot-key authorization surface: a leaked secp256k1 session key must be
|
|
# contained by (a) a cumulative spend cap it can never exceed, (b) an expiry after
|
|
# which it stops working, (c) an on-chain revocation, (d) its root Falcon key still
|
|
# being ACTIVE, and (e) a strictly-increasing action nonce so a captured signature
|
|
# cannot be replayed. We model authorize() as an inductive transition system on
|
|
# (spent, actionNonce) with the exact Solidity guards.
|
|
#
|
|
# Per session:
|
|
# cap = spendCap (immutable at issuance)
|
|
# spent = cumulative spend recorded so far
|
|
# n = actionNonce
|
|
# live = exists AND NOT revoked AND now <= expiry AND rootActive AND rootFalcon
|
|
#
|
|
# authorize(amount) Solidity guards, then effects:
|
|
# require live (_isSessionLive)
|
|
# require scope == scopeHash (scope gate)
|
|
# require spent+amount >= spent AND spent+amount <= cap (overflow + CAP gate)
|
|
# require ecrecover(...) == sessionAddr (sig gate)
|
|
# s.actionNonce = n + 1; s.spent = spent + amount
|
|
#
|
|
# SPEND-CAP INVARIANT (task target): spent <= cap, ALWAYS.
|
|
# NONCE INVARIANT (anti-replay): actionNonce strictly increases on every
|
|
# successful authorize, so no two successful actions share a digest.
|
|
# GATING INVARIANT: a non-live session (revoked / expired / root-inactive) can
|
|
# NEVER record spend (authorize reverts, state unchanged).
|
|
#
|
|
# Method: PROVED = negation UNSAT under guards. NEG-CTRL = buggy variant's
|
|
# violation SAT (real counterexample => guard is load-bearing).
|
|
#
|
|
# ASSUMPTIONS (bound every PROVED):
|
|
# A1. ecrecover is a sound signature oracle: it returns sessionAddr ONLY for a
|
|
# genuine session-key signature over the exact digest. Its cryptographic
|
|
# soundness is out of scope (same class as the halmos precompile-oracle
|
|
# assumption). We model the sig gate as a boolean the adversary can only
|
|
# satisfy with a real signature; the cap/nonce/gating proofs hold for BOTH
|
|
# branches (sig ok / not ok), i.e. regardless of what ecrecover returns.
|
|
# A2. spendCap is fixed at issuance and never mutated (confirmed: no setter).
|
|
# A3. Design math (unbounded Ints). Solidity 0.8 checked add on spent+amount is
|
|
# modelled by the explicit overflow guard already present in the source.
|
|
# -----------------------------------------------------------------------------
|
|
from z3 import Int, Bool, Solver, And, Or, Not, If, sat, unsat
|
|
|
|
results = []
|
|
def check(name, s, expect_unsat=True, kind="PROOF"):
|
|
r = s.check()
|
|
if expect_unsat:
|
|
ok = (r == unsat); tag = "PROVED" if ok else "FAILED"
|
|
else:
|
|
ok = (r == sat); tag = "CEX-FOUND" if ok else "FAILED"
|
|
results.append((name, tag, ok, kind))
|
|
print(f"[{tag:9}] ({kind}) {name}: z3={r} (expected {'unsat' if expect_unsat else 'sat'})")
|
|
if r == sat:
|
|
m = s.model()
|
|
wit = {}
|
|
for d in m.decls():
|
|
try: wit[str(d)] = m[d].as_long()
|
|
except Exception:
|
|
try: wit[str(d)] = bool(m[d])
|
|
except Exception: wit[str(d)] = "?"
|
|
print(" witness:", wit)
|
|
return ok
|
|
|
|
print("### AereAgentDID -- session spend-cap / gating / anti-replay\n")
|
|
|
|
# ---- BASE CASE: a freshly issued session. issueSession sets spent=0, and
|
|
# InvalidSessionParams requires expiry > block.timestamp, so cap>=0 and
|
|
# spent(0) <= cap holds at birth. ------------------------------------------
|
|
s = Solver()
|
|
cap = Int('cap')
|
|
s.add(cap >= 0)
|
|
s.add(Not(0 <= cap))
|
|
check("base case (freshly issued session) satisfies spent<=cap", s)
|
|
|
|
# ---- C1 SPEND CAP inductive: authorize() preserves spent <= cap ---------------
|
|
# Guards: overflow guard (spent+amount does not wrap) AND spent+amount <= cap.
|
|
# The Solidity condition is: revert IF (spent+amount < spent) OR (spent+amount > cap).
|
|
# So the SUCCESS path requires: spent+amount >= spent AND spent+amount <= cap.
|
|
s = Solver()
|
|
cap, spent, amount = Int('cap'), Int('spent'), Int('amount')
|
|
s.add(spent >= 0, spent <= cap, amount >= 0) # INV(pre) + amount is a uint
|
|
s.add(spent + amount >= spent, spent + amount <= cap) # authorize success guard
|
|
spent2 = spent + amount
|
|
s.add(Not(spent2 <= cap)) # negate INV(post)
|
|
check("C1 authorize() preserves spend cap (spent <= cap inductive)", s)
|
|
|
|
# ---- C2 GATING: a non-live session can never record spend --------------------
|
|
# _isSessionLive must be TRUE to reach the effects. If the session is revoked OR
|
|
# expired OR its root is not ACTIVE Falcon, authorize reverts (no state change).
|
|
# We show: (NOT live) AND (reached effects) is infeasible -- the guard blocks it.
|
|
s = Solver()
|
|
revoked, expired, rootInactive, reachedEffects = Bool('revoked'), Bool('expired'), Bool('rootInactive'), Bool('reachedEffects')
|
|
live = And(Not(revoked), Not(expired), Not(rootInactive))
|
|
# reachedEffects can only be true when live is true (that is the guard).
|
|
s.add(reachedEffects == live)
|
|
s.add(Or(revoked, expired, rootInactive)) # session is NOT live
|
|
s.add(reachedEffects) # but we claim spend was recorded
|
|
check("C2 gating: revoked/expired/root-inactive session cannot record spend", s)
|
|
|
|
# ---- C3 ANTI-REPLAY: actionNonce strictly increases on each successful authorize
|
|
# (n -> n+1), so two successful actions never share the signed digest. ------
|
|
s = Solver()
|
|
n = Int('n')
|
|
s.add(n >= 0)
|
|
n2 = n + 1
|
|
s.add(Not(n2 > n)) # negate strict-increase
|
|
check("C3 anti-replay: actionNonce strictly increases (n+1 > n)", s)
|
|
|
|
# ---- C4 EXPIRY monotonic: once now > expiry the session is dead forever (expiry
|
|
# is immutable and now is non-decreasing across blocks). ------------------
|
|
s = Solver()
|
|
expiry, now1, now2 = Int('expiry'), Int('now1'), Int('now2')
|
|
s.add(now2 >= now1, now1 > expiry) # already expired at now1, time advances
|
|
s.add(Not(now2 > expiry)) # can it become un-expired later?
|
|
check("C4 expiry is terminal: now>expiry stays true as time advances", s)
|
|
|
|
# ---- NEG-CTRL 1: a BUGGY cap gate that checks the PER-ACTION amount against the
|
|
# cap (amount <= cap) instead of the CUMULATIVE spent+amount. Over several
|
|
# actions the cumulative spent blows past cap. z3 finds the overshoot. ------
|
|
s = Solver()
|
|
cap, spent, amount = Int('cap'), Int('spent'), Int('amount')
|
|
s.add(spent >= 0, spent <= cap, amount >= 0)
|
|
s.add(amount <= cap) # BUG: per-action check only
|
|
spent2 = spent + amount
|
|
s.add(spent2 > cap) # cumulative exceeds cap
|
|
check("NEG-CTRL per-action cap check CAN let cumulative spent exceed cap", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ---- NEG-CTRL 2: a BUGGY gate that OMITS the liveness check (records spend even
|
|
# when revoked). z3 finds a revoked session recording spend. ---------------
|
|
s = Solver()
|
|
revoked = Bool('revoked')
|
|
reachedEffects = Bool('reachedEffects')
|
|
s.add(reachedEffects == True) # BUG: effects reached unconditionally
|
|
s.add(revoked == True) # session revoked
|
|
s.add(reachedEffects, revoked) # revoked session still spends
|
|
check("NEG-CTRL missing-liveness gate CAN let a revoked session record spend", s,
|
|
expect_unsat=False, kind="NEG-CTRL")
|
|
|
|
# ------------------------------------------------------------------------------
|
|
print("\n=== SUMMARY ===")
|
|
allok = True
|
|
for name, tag, ok, kind in results:
|
|
print(f" {tag:9} [{kind}] {name}")
|
|
allok = allok and ok
|
|
print()
|
|
print("AereAgentDID session authorization:")
|
|
if allok:
|
|
print(" PROVED the spend cap is inductive (C1: spent<=cap for any sequence of actions),")
|
|
print(" non-live sessions cannot spend (C2: revoked/expired/root-inactive all blocked),")
|
|
print(" the action nonce strictly increases (C3: no signature replay), and expiry is")
|
|
print(" terminal (C4). Two NEG-CTRLs confirm the CUMULATIVE cap check and the liveness")
|
|
print(" gate are load-bearing. Signature soundness of ecrecover is assumed (A1).")
|
|
else:
|
|
print(" NOT fully established (see FAILED / unexpected CEX above).")
|
|
import sys
|
|
sys.exit(0 if allok else 1)
|