Riskless/static/js/modules/interface/proofs.js
2023-04-30 18:42:52 +01:00

727 lines
20 KiB
JavaScript

import { cryptoRandom } from "../crypto/random_primes.js";
import { Region } from "./map.js";
const ROUNDS = 12;
function cryptoRange(upper) {
// This is ridiculous: why implement a BigInt primitive, have it behave like a number, and then _not_ offer
// mathematical operations like ilog2? Thankfully JavaScript is, for some reason, relatively fast at processing
// strings (that is relative to it processing anything else)
//
// Actual comment: subtract 1 is important as otherwise this overestimates the logarithm for powers of two.
let bitLength = BigInt((upper - 1n).toString(2).length + 1);
let mask = 2n ** bitLength - 1n;
let r = cryptoRandom(1024);
while ((r & mask) >= BigInt(upper)) {
r >>= bitLength;
}
return r & mask;
}
/**
* CSPRNG Fisher-Yates shuffle.
*
* Only works on lists up to 255 elements.
*/
function cryptoShuffle(l) {
let out = [];
for (let i = l.length - 1; i > 0; i--) {
let value = new Uint8Array([0]);
crypto.getRandomValues(value);
while (value[0] > i) {
crypto.getRandomValues(value);
}
let v = l.splice(value[0], 1);
out.push(v[0]);
}
out.push(l[0]);
return out;
}
/**
* R-S transform.
*
* Uses the hash of the proof content to produce verifier coins
*/
function getCoins(text) {
// Construct verifier coins
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(text);
let hash = hasher.getHash("UINT8ARRAY");
let verifierCoins = [];
for (let i = 0; i < ROUNDS / 8; i++) {
let v = hash[i];
for (let j = 0; j < 8; j++) {
verifierCoins.push(v & 1);
v >>= 1;
}
}
return verifierCoins;
}
window.cryptoShuffle = cryptoShuffle;
export function proveRegions(regions) {
// Construct prover coins
let proofs = [];
let privateInputs = [];
let regionNames = Object.keys(regions).sort();
for (let x = 0; x < ROUNDS; x++) {
let psi = cryptoShuffle(structuredClone(regionNames)).join("");
let newRegions = structuredClone(regions);
// Rearrange keys
for (let index = 0; index < regionNames.length; index++) {
newRegions[regionNames[index]] = regions[psi[index]].pubKey.encrypt(
regions[psi[index]].plainText * -1n
);
}
proofs.push(newRegions);
privateInputs.push(psi);
}
let verifierCoins = getCoins(JSON.stringify(proofs));
let verifications = [];
// Construct prover proofs
for (let i = 0; i < ROUNDS; i++) {
let coin = verifierCoins[i];
let proof = proofs[i];
let privateInput = privateInputs[i];
if (coin === 1) {
// Reveal bijection and proof for zero
let zeroProofs = {};
for (let i = 0; i < regionNames.length; i++) {
let name = regionNames[i];
let psiName = privateInput[i];
let c = proof[name].clone();
c.update(regions[psiName]);
zeroProofs[name] = c.proveNI();
}
let ver = {
psi: privateInput,
zeroProofs: zeroProofs,
};
verifications.push(ver);
} else {
// Reveal proof for plaintext
let valueProofs = {};
for (let name of regionNames) {
valueProofs[name] = proof[name].proveNI();
}
verifications.push({ valueProofs: valueProofs });
}
}
return {
regions: regions,
proofs: proofs,
verifications: verifications,
};
}
window.proveRegions = proveRegions;
export function verifyRegions(obj, key) {
let verifierCoins = getCoins(JSON.stringify(obj.proofs));
let regions = obj.regions;
let regionNames = Object.keys(regions).sort();
for (let i = 0; i < ROUNDS; i++) {
let proof = obj.proofs[i];
let verification = obj.verifications[i];
if (verifierCoins[i] === 1) {
for (let regionName of regionNames) {
// Undo psi
let originalRegion = proof[regionName];
// Compute product
let c = new ReadOnlyCiphertext(key, BigInt(regions[regionName]));
c.update(new ReadOnlyCiphertext(key, BigInt(originalRegion)));
// Check ciphertext is zero
let plaintext = c.verifyNI(verification.zeroProofs[regionName]);
if (plaintext !== 0n) {
return false;
}
}
} else {
let foundOne = false;
for (let name of Object.keys(verification.valueProofs)) {
let ciphertext = new ReadOnlyCiphertext(key, BigInt(proof[name]));
let plaintext = ciphertext.verifyNI(verification.valueProofs[name]);
if (plaintext === null) {
return false;
} else if (plaintext === 1n) {
if (foundOne) {
return false;
} else {
foundOne = true;
}
}
}
}
}
return true;
}
window.verifyRegions = verifyRegions;
// verifyRegions(proveRegions({A:paillier.pubKey.encrypt(0n),B:paillier.pubKey.encrypt(1n),C:paillier.pubKey.encrypt(0n),D:paillier.pubKey.encrypt(0n),E:paillier.pubKey.encrypt(0n)}), paillier.pubKey)
/**
* BCDG Range proof
*/
export function proveRange(cipherText, rangeUpper) {
if (cipherText.readOnly) {
throw "Cannot prove range of ReadOnlyCiphertext";
}
let key = cipherText.pubKey;
let proofs = [];
// Construct \omega_1, \omega_2, ciphertexts
for (let i = 0; i < ROUNDS; i++) {
let o1 = cryptoRange(rangeUpper);
let o2 = o1 - rangeUpper;
let cs = cryptoShuffle([key.encrypt(o1), key.encrypt(o2)]);
proofs.push({
cs: cs,
});
}
let coins = getCoins(JSON.stringify(proofs));
let verifications = [];
for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i];
let proof = proofs[i];
if (coin === 1) {
// Prove that ciphertexts are valid
verifications.push({
c1: proof.cs[0].proveNI(),
c2: proof.cs[1].proveNI(),
});
} else {
// Prove that one of the sums is valid
if ((cipherText.plainText + proof.cs[0].plainText) % key.n2 <= rangeUpper) {
let ct = cipherText.clone();
ct.update(proof.cs[0]);
verifications.push({
csIndex: 0,
proof: ct.proveNI(),
});
} else {
let ct = cipherText.clone();
ct.update(proof.cs[1]);
verifications.push({
csIndex: 1,
proof: ct.proveNI(),
});
}
}
}
return {
cipherText: cipherText,
rangeUpper: "0x" + rangeUpper.toString(16),
proofs: proofs,
verifications: verifications,
};
}
window.proveRange = proveRange;
export function verifyRange(obj, key) {
let coins = getCoins(JSON.stringify(obj.proofs));
let rangeUpper = BigInt(obj.rangeUpper);
for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i];
let proof = obj.proofs[i];
let verification = obj.verifications[i];
if (coin === 1) {
let c1 = new ReadOnlyCiphertext(key, BigInt(proof.cs[0]));
let c2 = new ReadOnlyCiphertext(key, BigInt(proof.cs[1]));
let o1 = c1.verifyNI(verification.c1);
let o2 = c2.verifyNI(verification.c2);
let diff;
if (o1 < o2) {
o2 -= key.n2;
diff = o1 - o2;
} else {
o1 -= key.n2;
diff = o2 - o1;
}
if (diff !== rangeUpper) {
return false;
}
} else {
let c = new ReadOnlyCiphertext(key, BigInt(proof.cs[verification.csIndex]));
c.update(new ReadOnlyCiphertext(key, BigInt(obj.cipherText)));
let value = c.verifyNI(verification.proof);
if (value === null || value > rangeUpper) {
return false;
}
}
}
return true;
}
window.verifyRange = verifyRange;
export function proveBitLength(cipherText) {
if (cipherText.readOnly) {
throw "Cannot prove readonly ciphertext";
}
let key = cipherText.pubKey;
// Compute decomposition
let bitCommitments = [];
let m = cipherText.plainText;
let prod = cipherText.clone();
let e = 1n;
while (m !== 0n) {
let bit = m & 0b1n;
let cBit = key.encrypt(-bit);
bitCommitments.push(cBit);
let cBit2 = cBit.clone();
cBit2.mul(e);
prod.update(cBit2);
e <<= 1n;
m >>= 1n;
}
// Pad out
while (bitCommitments.length < 8) {
let c = key.encrypt(0n);
bitCommitments.push(c);
let c2 = c.clone();
c2.mul(e);
prod.update(c2);
e <<= 1n;
}
let bitProofs = [];
for (let bitCommitment of bitCommitments) {
let p = proveOneOfTwo(bitCommitment);
bitProofs.push(p);
}
return {
cipherText: cipherText,
bitCommitments: bitCommitments,
prodProof: prod.proveNI(),
bitProofs: bitProofs,
};
}
window.proveBitLength = proveBitLength;
export function verifyBitLength(obj, key) {
// Check product is fine
let prod = new ReadOnlyCiphertext(key, BigInt(obj.cipherText));
let m = 1n;
for (let bit of obj.bitCommitments) {
let cBit = new ReadOnlyCiphertext(key, BigInt(bit));
cBit.mul(m);
prod.update(cBit);
m >>= 1n;
}
let p = prod.verifyNI(obj.prodProof);
if (p !== 0n) {
return null;
}
for (let proof of obj.bitProofs) {
let r = verifyOneOfTwo(proof, key);
if (!r) {
return null;
}
}
return obj.bitCommitments.length;
}
window.verifyBitLength = verifyBitLength;
/**
* Prove that a ciphertext is either a 0 or a -1
*/
function proveOneOfTwo(cipherText) {
let key = cipherText.pubKey;
let proofs = [];
for (let x = 0; x < ROUNDS; x++) {
proofs.push({
cs: cryptoShuffle([key.encrypt(0n), key.encrypt(1n)]),
});
}
let verifications = [];
let coins = getCoins(JSON.stringify(proofs));
for (let x = 0; x < ROUNDS; x++) {
let coin = coins[x];
let proof = proofs[x];
if (coin === 1) {
verifications.push({
cProofs: proof.cs.map((p) => p.proveNI()),
});
} else {
let c1Index;
if (cipherText.plainText === paillier.pubKey.n2 - 1n) {
c1Index = proof.cs.findIndex((c) => c.plainText === 1n);
} else {
c1Index = proof.cs.findIndex((c) => c.plainText === 0n);
}
let c1 = proof.cs[c1Index].clone();
c1.update(cipherText);
verifications.push({
csIndex: c1Index,
zeroProof: c1.proveNI(),
});
}
}
return {
cipherText: cipherText,
proofs: proofs,
verifications: verifications,
};
}
window.proveOneOfTwo = proveOneOfTwo;
function verifyOneOfTwo(obj, key) {
let coins = getCoins(JSON.stringify(obj.proofs));
for (let x = 0; x < ROUNDS; x++) {
let coin = coins[x];
let proof = obj.proofs[x];
let verification = obj.verifications[x];
if (coin === 1) {
let c1 = new ReadOnlyCiphertext(key, BigInt(proof.cs[0]));
let p1 = c1.verifyNI(verification.cProofs[0]);
let c2 = new ReadOnlyCiphertext(key, BigInt(proof.cs[1]));
let p2 = c2.verifyNI(verification.cProofs[1]);
if (!(p1 === 0n && p2 === 1n) && !(p2 === 0n && p1 === 1n)) {
return false;
}
} else {
let c = new ReadOnlyCiphertext(key, BigInt(proof.cs[verification.csIndex]));
c.update(obj.cipherText);
let p = c.verifyNI(verification.zeroProof);
if (p !== 0n) {
return false;
}
}
}
return true;
}
window.verifyOneOfTwo = verifyOneOfTwo;
/**
* - We prove that the set contains |S| - 2 zeros, with the final pair summing to zero and sums with the original
* set are zero.
* - We also attach some form of adjacency guarantee: that is, we prove the sums on all adjacent pairs are zero
* - We also attach a range proof for the new region values
*/
export function proveFortify(fortify) {
let proofs = [];
let privateInputs = [];
let regionNames = Object.keys(fortify).sort();
for (let x = 0; x < ROUNDS; x++) {
let psi = cryptoShuffle(structuredClone(regionNames)).join("");
let psiMap = {};
for (let i = 0; i < regionNames.length; i++) {
psiMap[regionNames[i]] = psi[i];
}
let newRegions = { regions: {} };
// Rearrange keys
for (let r of regionNames) {
newRegions.regions[psiMap[r]] = fortify[r].pubKey.encrypt(
-fortify[r].plainText
);
}
let edges = [];
let proofEdges = [];
// Attach edges
for (let i = 0; i < regionNames.length; i++) {
let region = regionNames[i];
let psiRegion = psi[i];
for (let n of Region.getRegion(region).neighbours) {
if (regionNames.includes(n.name)) {
let psiNeighbour = psiMap[n.name];
if (psiNeighbour > psiRegion) {
let salt = cryptoRandom(128);
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(psiRegion);
hasher.update(psiNeighbour);
hasher.update(salt.toString(16));
let hash = hasher.getHash("HEX");
edges.push({
hash: hash,
salt: salt,
edge: [psiRegion, psiNeighbour],
});
proofEdges.push(hash);
}
}
}
}
newRegions.edges = cryptoShuffle(proofEdges);
proofs.push(newRegions);
privateInputs.push({
psi: psi,
edges: edges,
});
}
let coins = getCoins(JSON.stringify(proofs));
let verifications = [];
for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i];
let proof = proofs[i].regions;
let input = privateInputs[i];
let psiMap = {};
for (let i = 0; i < regionNames.length; i++) {
psiMap[regionNames[i]] = input.psi[i];
}
if (coin === 1) {
// Show |S| - 2 zeroes
let verification = {
regions: {},
};
let pairCipherText = null;
let pair = [];
for (let r of regionNames) {
if (proof[r].plainText === 0n) {
verification.regions[r] = proof[r].proveNI();
} else if (pairCipherText === null) {
pairCipherText = proof[r].clone();
pair.push(r);
} else {
pairCipherText.update(proof[r]);
verification.pairCipherText = pairCipherText.proveNI();
pair.push(r);
}
}
// Show pair is joined by edge
let pairName = pair.sort();
verification.pairEdgeSalt =
"0x" +
input.edges
.find((e) => e.edge[0] === pairName[0] && e.edge[1] === pairName[1])
.salt.toString(16);
verifications.push(verification);
} else {
// Show isomorphism
let edges = {};
for (let e of input.edges) {
edges[e.edge[0] + e.edge[1]] = "0x" + e.salt.toString(16);
}
let zeroProofs = {};
for (let r of regionNames) {
let c = proof[psiMap[r]].clone();
c.update(fortify[r]);
zeroProofs[r] = c.proveNI();
}
verifications.push({
psi: input.psi,
salts: edges,
zeroProofs: zeroProofs,
});
}
}
return {
fortify: fortify,
proofs: proofs,
verifications: verifications,
};
}
window.proveFortify = proveFortify;
export function verifyFortify(obj, key) {
let coins = getCoins(JSON.stringify(obj.proofs));
let fortify = obj.fortify;
let regionNames = Object.keys(fortify).sort();
for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i];
let proof = obj.proofs[i];
let verification = obj.verifications[i];
if (coin === 1) {
// Check |S| - 2 zeroes
if (regionNames.length - 2 !== Object.keys(verification.regions).length) {
return false;
}
let regions = new Set(regionNames);
for (let r of Object.keys(verification.regions)) {
let c = new ReadOnlyCiphertext(key, BigInt(proof.regions[r]));
let p = c.verifyNI(verification.regions[r]);
if (p !== 0n) {
return false;
}
regions.delete(r);
}
// Check remaining pair sums to zero
let pair = [...regions.values()].sort();
let c = new ReadOnlyCiphertext(key, BigInt(proof.regions[pair[0]]));
c.update(new ReadOnlyCiphertext(key, BigInt(proof.regions[pair[1]])));
let p = c.verifyNI(verification.pairCipherText);
if (p !== 0n) {
return false;
}
// Check edge is okay
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(pair[0]);
hasher.update(pair[1]);
hasher.update(BigInt(verification.pairEdgeSalt).toString(16));
let hash = hasher.getHash("HEX");
if (!proof.edges.includes(hash)) {
return false;
}
} else {
// Check isomorphism
let psi = verification.psi;
let psiMap = {};
for (let i = 0; i < regionNames.length; i++) {
psiMap[regionNames[i]] = psi[i];
}
let salts = verification.salts;
// Psi matches?
if (psi.split("").sort().join("") !== regionNames.join("")) {
return false;
}
// Ciphertexts match?
for (let r of regionNames) {
let c = new ReadOnlyCiphertext(key, BigInt(proof.regions[r]));
c.update(new ReadOnlyCiphertext(key, BigInt(fortify[psiMap[r]])));
let p = c.verifyNI(verification.zeroProofs[r]);
if (p !== 0n) {
return false;
}
}
// Edges match?
let proverEdges = new Set(proof.edges);
for (let r of regionNames) {
let psiRegion = psiMap[r];
for (let n of Region.getRegion(r).neighbours) {
let psiNeighbour = psiMap[n.name];
if (psiNeighbour > psiRegion) {
let edgeSalt = salts[psiRegion + psiNeighbour];
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(psiRegion);
hasher.update(psiNeighbour);
hasher.update(BigInt(edgeSalt).toString(16));
let hash = hasher.getHash("HEX");
if (proverEdges.has(hash)) {
proverEdges.delete(hash);
} else {
return false;
}
}
}
}
// Check it is not over-specified
if (proverEdges.size !== 0) {
return false;
}
}
}
return true;
}
window.verifyFortify = verifyFortify;
// proveFortify({A:paillier.pubKey.encrypt(0n),B:paillier.pubKey.encrypt(3n),C:paillier.pubKey.encrypt(-3n),D:paillier.pubKey.encrypt(0n),E:paillier.pubKey.encrypt(0n)})