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.
202 lines
8.4 KiB
Solidity
202 lines
8.4 KiB
Solidity
// 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);
|
|
}
|
|
}
|