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.
118 lines
5.7 KiB
Solidity
118 lines
5.7 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {AereCryptoRegistry} from "../../contracts/pqc/AereCryptoRegistry.sol";
|
|
|
|
/**
|
|
* @title Symbolic (halmos) proofs for the AereCryptoRegistry governance/routing layer.
|
|
*
|
|
* WHAT IS PROVEN (see PROVEN-PROPERTIES.md):
|
|
* R1 FAIL-CLOSED: a REVOKED or UNKNOWN algorithm id can NEVER pass verify(), for
|
|
* any pubkey/message/signature, regardless of what the routed verifier returns.
|
|
* R2 status transitions are OWNER-ONLY: a non-owner call to setStatus (and
|
|
* setSuccessor) cannot change registry state.
|
|
* R3 resolveActive TERMINATES: the successor walk always halts (returns an ACTIVE
|
|
* id or reverts) within the bounded number of registered rows; it never
|
|
* diverges, even on a successor cycle.
|
|
*
|
|
* ASSUMPTIONS:
|
|
* - R1 does not need the precompile oracle at all: verify() gates on status BEFORE
|
|
* touching the verifier, so fail-closed holds independently of any precompile.
|
|
* - R3's termination proof relies on the loop being structurally bounded by _count
|
|
* and on halmos unrolling the loop to at least _count+1 iterations (--loop >= 12,
|
|
* with _count == 7 rows seeded here). We deliberately seed a successor CYCLE so
|
|
* the cycle-detection path is exercised.
|
|
* - Ownership: the test contract is the deployer, hence the Ownable owner. The
|
|
* symbolic non-owner caller is constrained to differ from it.
|
|
*/
|
|
contract AereCryptoRegistrySymbolic is Test {
|
|
AereCryptoRegistry internal reg;
|
|
|
|
function setUp() public {
|
|
reg = new AereCryptoRegistry();
|
|
reg.seedLiveSchemes(); // rows 1..5 (Falcon-512/1024, ML-DSA-44, SLH-DSA-128s ACTIVE; SHAKE256 hash-only)
|
|
|
|
// Make row 1 REVOKED so the REVOKED branch of R1 is reachable and terminal.
|
|
reg.setStatus(1, AereCryptoRegistry.Status.REVOKED);
|
|
|
|
// Two extra rows (6, 7) put into a DEPRECATED successor CYCLE to exercise R3's
|
|
// cycle detection (neither is ACTIVE, so resolveActive must walk then revert).
|
|
reg.addAlgorithm("legacy-a", 100, address(0xBEEF), 32, 100, 0, 1000, true); // id 6
|
|
reg.addAlgorithm("legacy-b", 101, address(0xCAFE), 32, 100, 0, 1000, true); // id 7
|
|
reg.setStatus(6, AereCryptoRegistry.Status.DEPRECATED);
|
|
reg.setStatus(7, AereCryptoRegistry.Status.DEPRECATED);
|
|
reg.setSuccessor(6, 7);
|
|
reg.setSuccessor(7, 6); // 6 -> 7 -> 6 -> ... (cycle)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// R1 : fail-closed. Any REVOKED or UNKNOWN id can never verify true.
|
|
// ---------------------------------------------------------------------
|
|
function check_revokedOrUnknownNeverVerifies(
|
|
uint256 id,
|
|
bytes calldata pubKey,
|
|
bytes32 messageHash,
|
|
bytes calldata signature
|
|
) external view {
|
|
AereCryptoRegistry.Status st = reg.statusOf(id);
|
|
vm.assume(st == AereCryptoRegistry.Status.UNKNOWN || st == AereCryptoRegistry.Status.REVOKED);
|
|
|
|
// For these statuses verify() short-circuits to false before ever routing to a
|
|
// verifier, so the result is false for every possible signature/key/message.
|
|
bool ok = reg.verify(id, pubKey, messageHash, signature);
|
|
assert(ok == false);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// R2 : setStatus is owner-only. A non-owner cannot change status.
|
|
// ---------------------------------------------------------------------
|
|
function check_setStatusIsOwnerOnly(address caller, uint256 id, uint8 newStatusRaw) external {
|
|
vm.assume(caller != reg.owner());
|
|
vm.assume(newStatusRaw >= 1 && newStatusRaw <= 3); // ACTIVE/DEPRECATED/REVOKED (valid enum, decode-safe)
|
|
|
|
AereCryptoRegistry.Status pre = reg.statusOf(id);
|
|
|
|
vm.prank(caller);
|
|
(bool ok,) = address(reg).call(
|
|
abi.encodeCall(reg.setStatus, (id, AereCryptoRegistry.Status(newStatusRaw)))
|
|
);
|
|
|
|
assert(!ok); // onlyOwner must revert the non-owner call
|
|
assert(reg.statusOf(id) == pre); // and status is provably unchanged
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// R2b : setSuccessor is owner-only.
|
|
// ---------------------------------------------------------------------
|
|
function check_setSuccessorIsOwnerOnly(address caller, uint256 id, uint256 successorId) external {
|
|
vm.assume(caller != reg.owner());
|
|
|
|
AereCryptoRegistry.Status preStatus = reg.statusOf(id);
|
|
|
|
vm.prank(caller);
|
|
(bool ok,) = address(reg).call(abi.encodeCall(reg.setSuccessor, (id, successorId)));
|
|
|
|
assert(!ok);
|
|
assert(reg.statusOf(id) == preStatus);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// R3 : resolveActive terminates. Over any symbolic start id, the call always
|
|
// halts within the unroll bound and, when it returns, returns an ACTIVE id.
|
|
// (Reaching the end of this function at all == termination was demonstrated
|
|
// by halmos fully exploring the bounded loop.)
|
|
// ---------------------------------------------------------------------
|
|
function check_resolveActiveTerminatesAndReturnsActive(uint256 startId) external view {
|
|
(bool ok, bytes memory ret) = address(reg).staticcall(
|
|
abi.encodeCall(reg.resolveActive, (startId))
|
|
);
|
|
if (ok) {
|
|
uint256 resolved = abi.decode(ret, (uint256));
|
|
assert(reg.isActive(resolved)); // resolveActive only ever returns an ACTIVE row
|
|
}
|
|
// If !ok it reverted (UnknownAlgorithm / NoActiveSuccessor / SuccessorCycle):
|
|
// that is still termination. The cycle 6->7->6 forces the SuccessorCycle path.
|
|
}
|
|
}
|