// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereHybridAuth, defense-in-depth ECDSA + NIST Falcon-512 authorization (roadmap #59) * @notice Authorizes an action only if BOTH cryptographic legs verify over the * SAME 32-byte message hash: * * 1. a classical secp256k1 ECDSA signature (EIP-191 personal-sign of the * 32-byte messageHash) that recovers to the identity's registered * classical address, AND * 2. a real NIST Falcon-512 (post-quantum lattice) signature over the * same 32-byte messageHash, verified through the LIVE on-chain * AereFalcon512Verifier against the identity's registered 897-byte * Falcon public key. * * This is a hybrid, transition-era authorization primitive: an action is * authorized iff a holder can produce a valid signature under BOTH a * pre-quantum (ECDSA) and a post-quantum (Falcon-512) key. A break of * either scheme alone is insufficient; an attacker must break both. * * @dev GAS / EIP-7825: a Falcon-512 verify is roughly 10.5M gas. The * state-changing `authorize` call therefore lands well under the Fusaka * EIP-7825 per-transaction cap of 2^24 = 16,777,216 gas, so authorization * is a normal transaction. `checkHybrid` is additionally exposed as a * `view` so integrators can pre-flight (or fully demonstrate) the result * via eth_call for free, and see exactly which leg passed or failed. * * The Falcon-512 message is exactly `abi.encodePacked(messageHash)` (the * raw 32 bytes). The ECDSA leg signs the EIP-191 personal-sign wrapper of * the same 32 bytes. Both legs are therefore bound to the identical * 32-byte messageHash. * * No owner, no admin, no upgrade. Identity registration is permissionless * and append-only (an identityId cannot be re-bound once registered). */ interface IFalcon512Verifier { /// @notice Full on-chain Falcon-512 verification. Returns true iff (pk, message, /// nonce, compSig) is a valid Falcon-512 signature. function verify( bytes memory pk, bytes memory message, bytes memory nonce, bytes memory compSig ) external view returns (bool); } contract AereHybridAuth { /* ------------------------------- immutable ------------------------------- */ /// @notice The live AereFalcon512Verifier (chain 2800: 0x4E8e9682…D8fFC). IFalcon512Verifier public immutable FALCON; /* --------------------------------- state -------------------------------- */ struct Identity { bool registered; address ecdsaSigner; // classical secp256k1 authority bytes falconPubKey; // 897-byte NIST Falcon-512 public key (0x09 header) } mapping(bytes32 => Identity) private _identities; // identityId => Identity bytes32[] public identityIds; /// @notice Successful-authorization ledger (state-changing proof surface). uint256 public authCount; mapping(bytes32 => mapping(bytes32 => bool)) public authorized; // identityId => messageHash => authorized bytes32 public lastIdentityId; bytes32 public lastMessageHash; /* --------------------------------- events ------------------------------- */ event IdentityRegistered(bytes32 indexed identityId, address indexed ecdsaSigner); event Authorized(bytes32 indexed identityId, bytes32 indexed messageHash, address caller, uint256 index); /* --------------------------------- errors ------------------------------- */ error AlreadyRegistered(); error NotRegistered(); error BadFalconPubKey(); error ZeroSigner(); error HybridAuthFailed(bool ecdsaOk, bool falconOk); /* ----------------------------- constructor ------------------------------ */ constructor(address falconVerifier) { require(falconVerifier != address(0), "falcon=0"); FALCON = IFalcon512Verifier(falconVerifier); } /* ----------------------------- registration ----------------------------- */ /// @notice Permissionlessly bind a hybrid identity: a classical ECDSA address /// plus a 897-byte NIST Falcon-512 public key. Append-only: an /// identityId can be registered exactly once. function registerIdentity( bytes32 identityId, address ecdsaSigner, bytes calldata falconPubKey ) external { if (_identities[identityId].registered) revert AlreadyRegistered(); if (ecdsaSigner == address(0)) revert ZeroSigner(); // NIST Falcon-512 public key: 897 bytes, header byte 0x09 (0x00 | logn=9). if (falconPubKey.length != 897 || uint8(falconPubKey[0]) != 0x09) revert BadFalconPubKey(); _identities[identityId] = Identity({ registered: true, ecdsaSigner: ecdsaSigner, falconPubKey: falconPubKey }); identityIds.push(identityId); emit IdentityRegistered(identityId, ecdsaSigner); } /* ------------------------------- checking ------------------------------- */ /// @notice Pure verification of a hybrid authorization. Safe for eth_call. /// @return ecdsaOk true iff the ECDSA leg recovers to the registered signer. /// @return falconOk true iff the Falcon-512 leg verifies against the registered key. /// @return ok true iff BOTH legs verify (ecdsaOk && falconOk). function checkHybrid( bytes32 identityId, bytes32 messageHash, bytes calldata ecdsaSig, bytes calldata falconNonce, bytes calldata falconCompSig ) public view returns (bool ecdsaOk, bool falconOk, bool ok) { Identity storage id = _identities[identityId]; if (!id.registered) revert NotRegistered(); // Leg 1: ECDSA over EIP-191 personal-sign of the 32-byte messageHash. bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); address rec = _recover(ethHash, ecdsaSig); ecdsaOk = (rec != address(0) && rec == id.ecdsaSigner); // Leg 2: Falcon-512 over the SAME 32-byte messageHash (raw bytes). falconOk = FALCON.verify(id.falconPubKey, abi.encodePacked(messageHash), falconNonce, falconCompSig); ok = ecdsaOk && falconOk; } /* ------------------------------ authorizing ----------------------------- */ /// @notice State-changing hybrid authorization. Reverts unless BOTH the ECDSA /// and Falcon-512 legs verify over the same 32-byte messageHash; on /// success it records the authorization and emits `Authorized`. function authorize( bytes32 identityId, bytes32 messageHash, bytes calldata ecdsaSig, bytes calldata falconNonce, bytes calldata falconCompSig ) external returns (bool) { (bool e, bool f, bool okBoth) = checkHybrid(identityId, messageHash, ecdsaSig, falconNonce, falconCompSig); if (!okBoth) revert HybridAuthFailed(e, f); authorized[identityId][messageHash] = true; uint256 idx = authCount; authCount = idx + 1; lastIdentityId = identityId; lastMessageHash = messageHash; emit Authorized(identityId, messageHash, msg.sender, idx); return true; } /* ---------------------------------- views ------------------------------- */ function getIdentity(bytes32 identityId) external view returns (bool registered, address ecdsaSigner, bytes memory falconPubKey) { Identity storage id = _identities[identityId]; return (id.registered, id.ecdsaSigner, id.falconPubKey); } function identityCount() external view returns (uint256) { return identityIds.length; } function isAuthorized(bytes32 identityId, bytes32 messageHash) external view returns (bool) { return authorized[identityId][messageHash]; } /* ------------------------------- internal ------------------------------- */ function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) { if (sig.length != 65) return address(0); bytes32 r; bytes32 s; uint8 v; assembly { r := calldataload(sig.offset) s := calldataload(add(sig.offset, 32)) v := byte(0, calldataload(add(sig.offset, 64))) } if (v < 27) v += 27; if (v != 27 && v != 28) return address(0); return ecrecover(hash, v, r, s); } }