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.
336 lines
16 KiB
Solidity
336 lines
16 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
|
/**
|
|
* @title AereHybridAuthorizer, a crypto-agility CONSUMER over AereCryptoRegistry (chain 2800)
|
|
*
|
|
* @notice AereCryptoRegistry (0xaE6fC596bb3eCcbf5c5D02D67B0Ef065b3Afbaa5) turns AERE's five
|
|
* live native crypto precompiles into a governed table
|
|
*
|
|
* algorithmId -> { verifier, wire-format, status, gas, successor }
|
|
*
|
|
* A registry has value only when something CONSUMES it. This contract is that
|
|
* consumer. Instead of pinning a scheme, an account/app authorizes against
|
|
* "whatever scheme is currently ACTIVE": every check first calls
|
|
* {AereCryptoRegistry.resolveActive} to follow the successor chain to the live
|
|
* ACTIVE row, then verifies through the registry's routed verifier.
|
|
*
|
|
* The payoff is a ZERO-REDEPLOY migration. When the Foundation (owner of the
|
|
* registry, later the Timelock) retires a scheme with the standard swap
|
|
*
|
|
* 1. addAlgorithm(newScheme) // register the successor, ACTIVE
|
|
* 2. setSuccessor(oldId, newId) // point old -> new
|
|
* 3. setStatus(oldId, DEPRECATED|REVOKED)
|
|
*
|
|
* every consumer that routes through this authorizer starts accepting the NEW
|
|
* scheme and rejecting the OLD one on the very next block, with no code change and
|
|
* no redeploy on this contract or on any dApp that calls it.
|
|
*
|
|
* @dev HONEST SCOPE. Application-layer authorization only. It changes NOTHING about AERE
|
|
* consensus: blocks are still produced and signed by Besu QBFT validators with
|
|
* classical ECDSA / secp256k1. All post-quantum verification is delegated to the
|
|
* SAME live native precompiles the registry routes to (Falcon-512/1024, ML-DSA-44,
|
|
* SLH-DSA-128s). This is NOT post-quantum consensus. No funds are ever held: there
|
|
* is no payable function, no selfdestruct, and no custody of any kind. The
|
|
* per-account "spend"/preference here is a routing choice, not money.
|
|
*
|
|
* @dev POLICY. This authorizer only ever accepts an ACTIVE scheme. {resolveActive}
|
|
* returns the first ACTIVE row on the successor chain, so:
|
|
* - A REVOKED id is NEVER used: it is skipped in favour of its ACTIVE successor,
|
|
* or, if it has none, resolveActive reverts and the fail-closed check returns
|
|
* false. A signature made under a revoked scheme's key can never authorize.
|
|
* - A DEPRECATED id with an ACTIVE successor transparently routes to the
|
|
* successor (a legacy signature under the deprecated key stops authorizing).
|
|
* - A DEPRECATED-or-REVOKED id with NO ACTIVE successor authorizes nothing.
|
|
* This is stricter than {AereCryptoRegistry.verify} (which still verifies a
|
|
* DEPRECATED row for legacy support) precisely because this surface is about
|
|
* "the currently recommended scheme", not legacy verification.
|
|
*
|
|
* @dev FAIL-CLOSED VIEWS. {checkAuthorized} / {checkAuthorizedFor} never revert: any
|
|
* registry revert (unknown id, dead-end successor chain, cycle) is caught and
|
|
* reported as `false`. The state-changing {authorize} lets a genuine registry
|
|
* revert surface (so a caller learns WHY), and reverts {AuthorizationFailed} when
|
|
* the routed verifier simply returns false.
|
|
*/
|
|
interface IAereCryptoRegistry {
|
|
/// @notice Follow the successor chain to the first ACTIVE id. Reverts on unknown id,
|
|
/// a dead-end with no ACTIVE row, or a cycle.
|
|
function resolveActive(uint256 algorithmId) external view returns (uint256);
|
|
|
|
/// @notice Fail-closed routed signature verification (returns false, never reverts,
|
|
/// for unknown/hash-only/UNKNOWN/REVOKED/malformed inputs).
|
|
function verify(uint256 algorithmId, bytes calldata pubKey, bytes32 messageHash, bytes calldata signature)
|
|
external
|
|
view
|
|
returns (bool);
|
|
|
|
/// @notice True iff `id` is a signature scheme safe to adopt for NEW work (ACTIVE).
|
|
function isUsable(uint256 id) external view returns (bool);
|
|
}
|
|
|
|
contract AereHybridAuthorizer is Ownable {
|
|
// ==========================================================================
|
|
// Immutable wiring
|
|
// ==========================================================================
|
|
|
|
/// @notice The governed crypto-agility registry this authorizer consumes.
|
|
IAereCryptoRegistry public immutable registry;
|
|
|
|
// ==========================================================================
|
|
// Storage
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice The app-wide DEFAULT algorithm id used for any account that has not chosen a
|
|
* personal preference. Owner-governed. Stored as a stable reference id: every
|
|
* check re-resolves it through {resolveActive}, so it auto-follows a registry
|
|
* swap even before the owner calls {migrate}. {migrate} simply crystallizes it
|
|
* to the resolved successor.
|
|
*/
|
|
uint256 public defaultAlgorithmId;
|
|
|
|
/// @dev Per-account preference; 0 means "use the app default" (the SAFE DEFAULT).
|
|
mapping(address => uint256) private _preferred;
|
|
|
|
// Authorization ledger (a state-changing proof surface; holds no value).
|
|
/// @notice Number of successful {authorize} calls recorded.
|
|
uint256 public authorizationCount;
|
|
/// @notice keccak256(pubKey || messageHash) => was this (key, message) ever authorized.
|
|
mapping(bytes32 => bool) private _authorizedProof;
|
|
/// @notice The ACTIVE algorithm id used by the most recent successful authorization.
|
|
uint256 public lastResolvedId;
|
|
/// @notice The message hash of the most recent successful authorization.
|
|
bytes32 public lastMessageHash;
|
|
|
|
// ==========================================================================
|
|
// Events
|
|
// ==========================================================================
|
|
|
|
event DefaultAlgorithmSet(uint256 indexed id);
|
|
event DefaultMigrated(uint256 indexed oldId, uint256 indexed newId);
|
|
event PreferredAlgorithmSet(address indexed account, uint256 indexed id);
|
|
event Authorized(
|
|
address indexed caller, uint256 indexed requestedId, uint256 indexed resolvedId, bytes32 messageHash
|
|
);
|
|
|
|
// ==========================================================================
|
|
// Errors
|
|
// ==========================================================================
|
|
|
|
error ZeroRegistry();
|
|
error UnusableAlgorithm(uint256 id); // chosen/default id does not resolve to a usable ACTIVE signature scheme
|
|
error NotCurrentDefault(uint256 got, uint256 current); // migrate(oldId) with oldId != defaultAlgorithmId
|
|
error AuthorizationFailed(uint256 requestedId, uint256 resolvedId);
|
|
|
|
// ==========================================================================
|
|
// Constructor
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @param registry_ the AereCryptoRegistry to route through (non-zero).
|
|
* @param defaultAlgorithmId_ the initial app default. It MUST resolve, via the
|
|
* registry, to a usable ACTIVE signature scheme right now,
|
|
* so the contract can never be born pointing at an unusable
|
|
* or hash-only default (fail-fast "safe default").
|
|
*/
|
|
constructor(address registry_, uint256 defaultAlgorithmId_) {
|
|
if (registry_ == address(0)) revert ZeroRegistry();
|
|
registry = IAereCryptoRegistry(registry_);
|
|
// Fail-fast: the initial default must be a usable ACTIVE signature scheme.
|
|
if (!_resolvesToUsable(defaultAlgorithmId_)) revert UnusableAlgorithm(defaultAlgorithmId_);
|
|
defaultAlgorithmId = defaultAlgorithmId_;
|
|
emit DefaultAlgorithmSet(defaultAlgorithmId_);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Owner: default governance
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Set the app-wide default algorithm id. Owner only. The id must resolve to a
|
|
* usable ACTIVE signature scheme (guards against pointing the default at a
|
|
* revoked, dead-ended, or hash-only scheme).
|
|
*/
|
|
function setDefaultAlgorithm(uint256 id) external onlyOwner {
|
|
if (!_resolvesToUsable(id)) revert UnusableAlgorithm(id);
|
|
defaultAlgorithmId = id;
|
|
emit DefaultAlgorithmSet(id);
|
|
}
|
|
|
|
/**
|
|
* @notice Owner helper: crystallize the app default onto the ACTIVE successor of
|
|
* `oldId` after a registry swap. `oldId` must equal the current
|
|
* {defaultAlgorithmId} (a guard so a stale call cannot silently retarget it).
|
|
* Because {authorize}/{checkAuthorized} already re-resolve on every call, this
|
|
* is an OPTIMIZATION/CLARITY step (shorter successor walk, fresh views), not a
|
|
* correctness requirement: consumers are already migrated the moment the
|
|
* registry swap lands. Reverts (from {resolveActive}) if the chain dead-ends or
|
|
* cycles, so the default can never be moved onto a non-ACTIVE row.
|
|
* @return newId the ACTIVE id the default now points at (== oldId if already ACTIVE).
|
|
*/
|
|
function migrate(uint256 oldId) external onlyOwner returns (uint256 newId) {
|
|
if (defaultAlgorithmId != oldId) revert NotCurrentDefault(oldId, defaultAlgorithmId);
|
|
newId = registry.resolveActive(oldId); // reverts on unknown/dead-end/cycle
|
|
if (newId != oldId) {
|
|
defaultAlgorithmId = newId;
|
|
emit DefaultMigrated(oldId, newId);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Account: preference (self-service)
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Choose a personal preferred algorithm id for msg.sender. Pass 0 to clear the
|
|
* preference and fall back to the app default. A non-zero id must resolve to a
|
|
* usable ACTIVE signature scheme at set time.
|
|
* @dev The stored value is a stable reference: like the default, it is re-resolved
|
|
* through {resolveActive} on every check, so a later registry swap migrates a
|
|
* preferring account automatically too.
|
|
*/
|
|
function setPreferredAlgorithm(uint256 id) external {
|
|
if (id != 0 && !_resolvesToUsable(id)) revert UnusableAlgorithm(id);
|
|
_preferred[msg.sender] = id;
|
|
emit PreferredAlgorithmSet(msg.sender, id);
|
|
}
|
|
|
|
/// @notice The raw stored preference for `account` (0 == none / use default).
|
|
function preferredAlgorithmOf(address account) external view returns (uint256) {
|
|
return _preferred[account];
|
|
}
|
|
|
|
/**
|
|
* @notice The effective (pre-resolution) algorithm id for `account`: its preference if
|
|
* set, else the app default. This is the STABLE reference id, before the
|
|
* successor chain is walked.
|
|
*/
|
|
function algorithmFor(address account) public view returns (uint256) {
|
|
uint256 p = _preferred[account];
|
|
return p == 0 ? defaultAlgorithmId : p;
|
|
}
|
|
|
|
/**
|
|
* @notice The ACTIVE algorithm id `account` currently authorizes under, after resolving
|
|
* the successor chain. Reverts (from {resolveActive}) if the chain dead-ends or
|
|
* cycles with no ACTIVE row.
|
|
*/
|
|
function resolvedAlgorithmFor(address account) external view returns (uint256) {
|
|
return registry.resolveActive(algorithmFor(account));
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Authorization (fail-closed views)
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Fail-closed check: is `signature` a valid signature over `messageHash` by
|
|
* `pubKey` under the currently-ACTIVE scheme reachable from `algorithmId`?
|
|
* Resolves the successor chain, then verifies through the registry's routed
|
|
* verifier. Returns FALSE (never reverts) for an unknown id, a dead-end or
|
|
* cyclic successor chain, a malformed input, or a failed verification.
|
|
*/
|
|
function checkAuthorized(
|
|
uint256 algorithmId,
|
|
bytes calldata pubKey,
|
|
bytes32 messageHash,
|
|
bytes calldata signature
|
|
) public view returns (bool) {
|
|
uint256 active;
|
|
try registry.resolveActive(algorithmId) returns (uint256 a) {
|
|
active = a;
|
|
} catch {
|
|
return false; // unknown id, dead-end, or cycle -> not authorized
|
|
}
|
|
try registry.verify(active, pubKey, messageHash, signature) returns (bool ok) {
|
|
return ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Fail-closed check under `account`'s effective algorithm (its preference, else
|
|
* the app default). Same semantics as {checkAuthorized}.
|
|
*/
|
|
function checkAuthorizedFor(
|
|
address account,
|
|
bytes calldata pubKey,
|
|
bytes32 messageHash,
|
|
bytes calldata signature
|
|
) external view returns (bool) {
|
|
return checkAuthorized(algorithmFor(account), pubKey, messageHash, signature);
|
|
}
|
|
|
|
/// @notice Whether (pubKey, messageHash) was ever recorded by a successful {authorize}.
|
|
function isProofAuthorized(bytes calldata pubKey, bytes32 messageHash) external view returns (bool) {
|
|
return _authorizedProof[keccak256(abi.encodePacked(pubKey, messageHash))];
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Authorization (state-changing, recorded)
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice State-changing authorization under the ACTIVE scheme reachable from
|
|
* `algorithmId`. Reverts if the successor chain has no ACTIVE row (surfacing
|
|
* the registry's reason) or {AuthorizationFailed} if the routed verifier
|
|
* returns false. On success it records the (pubKey, messageHash) proof, bumps
|
|
* {authorizationCount}, and emits {Authorized}.
|
|
* @return resolvedId the ACTIVE id the signature was verified under.
|
|
*/
|
|
function authorize(
|
|
uint256 algorithmId,
|
|
bytes calldata pubKey,
|
|
bytes32 messageHash,
|
|
bytes calldata signature
|
|
) external returns (uint256 resolvedId) {
|
|
return _authorize(algorithmId, pubKey, messageHash, signature);
|
|
}
|
|
|
|
/**
|
|
* @notice State-changing authorization under msg.sender's effective algorithm (its
|
|
* preference, else the app default). See {authorize}.
|
|
*/
|
|
function authorizeWithPreferred(bytes calldata pubKey, bytes32 messageHash, bytes calldata signature)
|
|
external
|
|
returns (uint256 resolvedId)
|
|
{
|
|
return _authorize(algorithmFor(msg.sender), pubKey, messageHash, signature);
|
|
}
|
|
|
|
function _authorize(
|
|
uint256 algorithmId,
|
|
bytes calldata pubKey,
|
|
bytes32 messageHash,
|
|
bytes calldata signature
|
|
) internal returns (uint256 resolvedId) {
|
|
resolvedId = registry.resolveActive(algorithmId); // reverts on unknown/dead-end/cycle
|
|
bool ok = registry.verify(resolvedId, pubKey, messageHash, signature);
|
|
if (!ok) revert AuthorizationFailed(algorithmId, resolvedId);
|
|
|
|
_authorizedProof[keccak256(abi.encodePacked(pubKey, messageHash))] = true;
|
|
authorizationCount += 1;
|
|
lastResolvedId = resolvedId;
|
|
lastMessageHash = messageHash;
|
|
emit Authorized(msg.sender, algorithmId, resolvedId, messageHash);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Internal
|
|
// ==========================================================================
|
|
|
|
/// @dev True iff `id` resolves (via the successor chain) to a usable ACTIVE signature
|
|
/// scheme. Fail-closed: a revert in resolveActive (unknown/dead-end/cycle) is
|
|
/// treated as "not usable".
|
|
function _resolvesToUsable(uint256 id) internal view returns (bool) {
|
|
try registry.resolveActive(id) returns (uint256 active) {
|
|
return registry.isUsable(active);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|