aere-contracts/contracts/pqc/AereHybridAuth.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

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