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.
63 lines
2.8 KiB
JavaScript
63 lines
2.8 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
|
|
// Off-chain WOTS+ signer matching AerePQCVerifier exactly.
|
|
function chain(x, count) { for (let i = 0; i < count; i++) x = ethers.keccak256(x); return x; }
|
|
function digits(mHex) {
|
|
const m = ethers.getBytes(mHex); const d = new Array(67); let csum = 0;
|
|
for (let i = 0; i < 32; i++) { const b = m[i], hi = b >> 4, lo = b & 0x0f; d[2 * i] = hi; d[2 * i + 1] = lo; csum += (15 - hi) + (15 - lo); }
|
|
d[64] = (csum >> 8) & 0x0f; d[65] = (csum >> 4) & 0x0f; d[66] = csum & 0x0f; return d;
|
|
}
|
|
function keygen() {
|
|
const sk = []; for (let i = 0; i < 67; i++) sk.push(ethers.hexlify(ethers.randomBytes(32)));
|
|
const pkEnds = sk.map(s => chain(s, 15));
|
|
const compressedPK = ethers.keccak256(ethers.concat(pkEnds));
|
|
return { sk, compressedPK };
|
|
}
|
|
function sign(sk, msgHash) { const d = digits(msgHash); return sk.map((s, i) => chain(s, d[i])); }
|
|
|
|
describe("AerePQCVerifier (WOTS+ post-quantum)", function () {
|
|
async function deploy() {
|
|
const V = await ethers.getContractFactory("AerePQCVerifier");
|
|
const v = await V.deploy(); await v.waitForDeployment(); return v;
|
|
}
|
|
|
|
it("accepts a valid WOTS+ signature", async function () {
|
|
const v = await deploy();
|
|
const { sk, compressedPK } = keygen();
|
|
const msgHash = ethers.keccak256(ethers.toUtf8Bytes("AERE is post-quantum ready"));
|
|
const sig = sign(sk, msgHash);
|
|
expect(await v.verify(msgHash, sig, compressedPK)).to.equal(true);
|
|
// derivePublicKey helper agrees
|
|
expect(await v.derivePublicKey(sk)).to.equal(compressedPK);
|
|
});
|
|
|
|
it("rejects a wrong public key", async function () {
|
|
const v = await deploy();
|
|
const { sk } = keygen();
|
|
const msgHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
|
|
const sig = sign(sk, msgHash);
|
|
expect(await v.verify(msgHash, sig, ethers.keccak256("0xdead"))).to.equal(false);
|
|
});
|
|
|
|
it("rejects a signature reused on a DIFFERENT message (forgery-resistant)", async function () {
|
|
const v = await deploy();
|
|
const { sk, compressedPK } = keygen();
|
|
const m1 = ethers.keccak256(ethers.toUtf8Bytes("original"));
|
|
const sig = sign(sk, m1);
|
|
const m2 = ethers.keccak256(ethers.toUtf8Bytes("forged"));
|
|
expect(await v.verify(m2, sig, compressedPK)).to.equal(false);
|
|
});
|
|
|
|
it("checksum blocks a naive forge (raising a digit without lowering checksum)", async function () {
|
|
const v = await deploy();
|
|
const { sk, compressedPK } = keygen();
|
|
const m = ethers.keccak256(ethers.toUtf8Bytes("x"));
|
|
const sig = sign(sk, m);
|
|
// advance one message-chain by 1 (attacker can hash forward) — checksum digits stay, so PK mismatch
|
|
const tampered = [...sig];
|
|
tampered[0] = ethers.keccak256(tampered[0]);
|
|
expect(await v.verify(m, tampered, compressedPK)).to.equal(false);
|
|
});
|
|
});
|