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.
81 lines
3.5 KiB
Solidity
81 lines
3.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
/**
|
|
* @title AerePQCVerifier — on-chain post-quantum signature verification
|
|
* @notice Verifies a WOTS+ (Winternitz One-Time Signature) — a HASH-BASED
|
|
* signature that is secure against quantum computers (its security rests
|
|
* only on the pre-image resistance of keccak256, which Shor's algorithm
|
|
* does not break, unlike ECDSA). This lets AERE contracts natively
|
|
* verify a post-quantum signature TODAY, without touching consensus.
|
|
*
|
|
* Scheme (n=32, w=16): the 256-bit message hash is split into 64 base-16
|
|
* digits plus a 3-digit checksum (67 chains total). Each signature
|
|
* element is a keccak hash-chain value; verification completes each
|
|
* chain to its end and checks that the hash of all chain-ends equals the
|
|
* committed public key. The checksum makes forgery infeasible: raising
|
|
* any message digit lowers the checksum, which would require inverting
|
|
* keccak.
|
|
*
|
|
* WOTS+ is ONE-TIME (each key signs one message); it is the building
|
|
* block of the stateful many-time schemes XMSS / SPHINCS+. A public
|
|
* AereXmssVerifier (WOTS+ leaf + Merkle authentication path to a long-
|
|
* term root) is the natural extension.
|
|
*/
|
|
contract AerePQCVerifier {
|
|
uint256 public constant W = 16; // Winternitz parameter
|
|
uint256 public constant LEN1 = 64; // message digits (256 bits / 4)
|
|
uint256 public constant LEN2 = 3; // checksum digits
|
|
uint256 public constant LEN = 67; // total chains
|
|
|
|
/// @notice Derive the 67 base-16 digits (message + checksum) from a hash.
|
|
function digits(bytes32 m) public pure returns (uint8[67] memory d) {
|
|
uint256 csum = 0;
|
|
for (uint256 i = 0; i < 32; i++) {
|
|
uint8 b = uint8(m[i]);
|
|
uint8 hi = b >> 4;
|
|
uint8 lo = b & 0x0f;
|
|
d[2 * i] = hi;
|
|
d[2 * i + 1] = lo;
|
|
csum += (15 - uint256(hi)) + (15 - uint256(lo));
|
|
}
|
|
// checksum (max 64*15 = 960 < 16^3) as 3 base-16 digits, MSB first
|
|
d[64] = uint8((csum >> 8) & 0x0f);
|
|
d[65] = uint8((csum >> 4) & 0x0f);
|
|
d[66] = uint8(csum & 0x0f);
|
|
}
|
|
|
|
function _chain(bytes32 x, uint256 count) internal pure returns (bytes32) {
|
|
for (uint256 i = 0; i < count; i++) {
|
|
x = keccak256(abi.encodePacked(x));
|
|
}
|
|
return x;
|
|
}
|
|
|
|
/**
|
|
* @notice Verify a WOTS+ signature.
|
|
* @param msgHash keccak256 of the message being signed
|
|
* @param sig the 67 hash-chain signature elements
|
|
* @param compressedPK keccak256 of the 67 public-key chain-ends
|
|
* @return ok true iff the signature is valid for compressedPK
|
|
*/
|
|
function verify(bytes32 msgHash, bytes32[67] calldata sig, bytes32 compressedPK) public pure returns (bool ok) {
|
|
uint8[67] memory d = digits(msgHash);
|
|
bytes32[67] memory ends;
|
|
for (uint256 i = 0; i < LEN; i++) {
|
|
// complete each chain from the signature to the public-key end
|
|
ends[i] = _chain(sig[i], 15 - uint256(d[i]));
|
|
}
|
|
return keccak256(abi.encodePacked(ends)) == compressedPK;
|
|
}
|
|
|
|
/// @notice Compute a WOTS+ public key from a full private key (test/helper).
|
|
function derivePublicKey(bytes32[67] calldata sk) external pure returns (bytes32 compressedPK) {
|
|
bytes32[67] memory ends;
|
|
for (uint256 i = 0; i < LEN; i++) {
|
|
ends[i] = _chain(sk[i], 15);
|
|
}
|
|
return keccak256(abi.encodePacked(ends));
|
|
}
|
|
}
|