// 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; } } }