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.
142 lines
6.2 KiB
JavaScript
142 lines
6.2 KiB
JavaScript
const { expect } = require("chai");
|
|
const { ethers } = require("hardhat");
|
|
const kat = require("./fixtures/xmss-sha2_10_256-kat.json");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Independent in-repo oracle: RFC 8391 XMSS-SHA2_10_256 verify path in pure JS
|
|
// (ethers sha256), reimplemented from the spec, to cross-check the Solidity
|
|
// verifier against the OFFICIAL reference vector without trusting either side.
|
|
// ---------------------------------------------------------------------------
|
|
const N = 32, W = 16, LOGW = 4, LEN1 = 64, LEN2 = 3, LEN = 67, H = 10;
|
|
const cat = (...a) => ethers.concat(a);
|
|
const sha = (b) => ethers.sha256(b);
|
|
const tob = (x, l) => ethers.toBeHex(BigInt(x), l);
|
|
const xor = (a, b) => {
|
|
const A = ethers.getBytes(a), B = ethers.getBytes(b), O = new Uint8Array(A.length);
|
|
for (let i = 0; i < A.length; i++) O[i] = A[i] ^ B[i];
|
|
return ethers.hexlify(O);
|
|
};
|
|
const addr = (typ, a4, a5, a6, km) =>
|
|
tob((BigInt(typ) << 128n) | (BigInt(a4) << 96n) | (BigInt(a5) << 64n) | (BigInt(a6) << 32n) | BigInt(km), 32);
|
|
const prf = (seed, a) => sha(cat(tob(3, 32), seed, a));
|
|
const F = (seed, typ, a4, a5, a6, x) => {
|
|
const key = prf(seed, addr(typ, a4, a5, a6, 0));
|
|
const mask = prf(seed, addr(typ, a4, a5, a6, 1));
|
|
return sha(cat(tob(0, 32), key, xor(x, mask)));
|
|
};
|
|
const Hh = (seed, typ, a4, a5, a6, l, r) => {
|
|
const key = prf(seed, addr(typ, a4, a5, a6, 0));
|
|
const m0 = prf(seed, addr(typ, a4, a5, a6, 1));
|
|
const m1 = prf(seed, addr(typ, a4, a5, a6, 2));
|
|
return sha(cat(tob(1, 32), key, xor(l, m0), xor(r, m1)));
|
|
};
|
|
function chainLengths(mHex) {
|
|
const m = ethers.getBytes(mHex), d = new Array(LEN);
|
|
let csum = 0;
|
|
for (let i = 0; i < 32; i++) { const b = m[i], hi = b >> 4, lo = b & 15; d[2 * i] = hi; d[2 * i + 1] = lo; csum += (15 - hi) + (15 - lo); }
|
|
const c = csum << 4;
|
|
d[64] = (c >> 12) & 15; d[65] = (c >> 8) & 15; d[66] = (c >> 4) & 15;
|
|
return d;
|
|
}
|
|
function wotsPkFromSig(seed, idxLeaf, mhash, wots) {
|
|
const lens = chainLengths(mhash), pk = [];
|
|
for (let i = 0; i < LEN; i++) { let x = wots[i]; for (let s = lens[i]; s < W - 1; s++) x = F(seed, 0, idxLeaf, i, s, x); pk.push(x); }
|
|
return pk;
|
|
}
|
|
function lTree(seed, idxLeaf, pk) {
|
|
let nodes = pk.slice(), l = LEN, height = 0;
|
|
while (l > 1) {
|
|
const parent = l >> 1;
|
|
for (let i = 0; i < parent; i++) nodes[i] = Hh(seed, 1, idxLeaf, height, i, nodes[2 * i], nodes[2 * i + 1]);
|
|
if (l & 1) { nodes[parent] = nodes[l - 1]; l = parent + 1; } else { l = parent; }
|
|
height++;
|
|
}
|
|
return nodes[0];
|
|
}
|
|
function computeRoot(seed, leaf, leafIdx, auth) {
|
|
let left, right;
|
|
if (leafIdx & 1) { left = auth[0]; right = leaf; } else { left = leaf; right = auth[0]; }
|
|
let li = leafIdx;
|
|
for (let i = 0; i < H - 1; i++) {
|
|
li >>= 1;
|
|
const node = Hh(seed, 2, 0, i, li, left, right);
|
|
if (li & 1) { left = auth[i + 1]; right = node; } else { left = node; right = auth[i + 1]; }
|
|
}
|
|
li >>= 1;
|
|
return Hh(seed, 2, 0, H - 1, li, left, right);
|
|
}
|
|
function jsVerify(pubRoot, pubSeed, idx, R, msg, wots, auth) {
|
|
const mhash = sha(cat(tob(2, 32), R, pubRoot, tob(idx, 32), msg));
|
|
const idxLeaf = idx & ((1 << H) - 1);
|
|
const wpk = wotsPkFromSig(pubSeed, idxLeaf, mhash, wots);
|
|
const leaf = lTree(pubSeed, idxLeaf, wpk);
|
|
return computeRoot(pubSeed, leaf, idxLeaf, auth);
|
|
}
|
|
|
|
describe("AereXmssVerifier (RFC 8391 XMSS-SHA2_10_256)", function () {
|
|
async function deploy() {
|
|
const V = await ethers.getContractFactory("AereXmssVerifier");
|
|
const v = await V.deploy();
|
|
await v.waitForDeployment();
|
|
return v;
|
|
}
|
|
const V = () => [kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, kat.auth];
|
|
|
|
it("independent JS oracle recomputes the official reference root", function () {
|
|
const root = jsVerify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, kat.auth);
|
|
expect(root).to.equal(kat.pubRoot);
|
|
});
|
|
|
|
it("accepts the OFFICIAL reference KAT signature (verify -> true)", async function () {
|
|
const v = await deploy();
|
|
expect(await v.verify(...V())).to.equal(true);
|
|
});
|
|
|
|
it("chainLengths matches the WOTS+ checksum spec for the KAT message hash", async function () {
|
|
const v = await deploy();
|
|
// mhash as computed in verify()
|
|
const mhash = ethers.sha256(ethers.concat([ethers.toBeHex(2n, 32), kat.R, kat.pubRoot, ethers.toBeHex(BigInt(kat.idx), 32), kat.msg]));
|
|
const onchain = (await v.chainLengths(mhash)).map((x) => Number(x));
|
|
expect(onchain).to.deep.equal(chainLengths(mhash));
|
|
});
|
|
|
|
it("rejects a tampered WOTS+ signature (verify -> false)", async function () {
|
|
const v = await deploy();
|
|
const wots = kat.wots.slice();
|
|
wots[0] = xor(wots[0], "0x01" + "00".repeat(31));
|
|
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, wots, kat.auth)).to.equal(false);
|
|
});
|
|
|
|
it("rejects a tampered authentication path (verify -> false)", async function () {
|
|
const v = await deploy();
|
|
const auth = kat.auth.slice();
|
|
auth[9] = xor(auth[9], "0x01" + "00".repeat(31));
|
|
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, auth)).to.equal(false);
|
|
});
|
|
|
|
it("rejects a tampered message (verify -> false)", async function () {
|
|
const v = await deploy();
|
|
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, "0x26", kat.wots, kat.auth)).to.equal(false);
|
|
});
|
|
|
|
it("rejects a wrong leaf index (verify -> false)", async function () {
|
|
const v = await deploy();
|
|
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx + 1, kat.R, kat.msg, kat.wots, kat.auth)).to.equal(false);
|
|
});
|
|
|
|
it("rejects a wrong public root (verify -> false)", async function () {
|
|
const v = await deploy();
|
|
const badRoot = xor(kat.pubRoot, "0x01" + "00".repeat(31));
|
|
expect(await v.verify(badRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, kat.auth)).to.equal(false);
|
|
});
|
|
|
|
it("verifyAndRecord records the result and stays under the EIP-7825 cap", async function () {
|
|
const v = await deploy();
|
|
const est = await v.verifyAndRecord.estimateGas(...V());
|
|
expect(est).to.be.lessThan(16777216n); // 2^24 Fusaka per-tx cap
|
|
await (await v.verifyAndRecord(...V())).wait();
|
|
expect(await v.lastResult()).to.equal(true);
|
|
expect(await v.verifyCount()).to.equal(1n);
|
|
});
|
|
});
|