This commit is contained in:
jude 2023-04-24 14:20:44 +01:00
parent 88cf76f815
commit f4020aadec
8 changed files with 265 additions and 112 deletions

View File

@ -32,7 +32,8 @@ selecting 512 if you want the program to run in less than 3-5 business days.
Key generation will still take some time. The browser may tell you the tab is not
responding, but just wait and it'll be fine eventually. If not, short-circuit the
`generate_safe_prime` function in `random_primes.js` to just return a normal prime.
`generate_safe_prime` function in `random_primes.js` to just return a normal prime
(there is a comment on the conditional that needs to be removed).
### Playing the game

View File

@ -100,8 +100,11 @@ export function generate_prime() {
export function generate_safe_prime() {
while (true) {
let n = generate_prime();
if (small_prime_test((n - 1n) / 2n) && miller_rabin((n - 1n) / 2n, 40)) {
if (small_prime_test((n - 1n) / 2n)) {
// Remove this conditional if you want it to run fast! It (probably) won't break it
// if (miller_rabin((n - 1n) / 2n, 40)) {
return n;
// }
}
}
}

View File

@ -203,10 +203,14 @@ document.addEventListener("DOMContentLoaded", () => {
let amount = modal.querySelector(".amount").value;
let endRegion = modal.querySelector(".target").value;
if (action === "ATTACK") {
socket.emit(
"message",
Packet.createAction(action, startRegion, endRegion, amount)
);
} else {
game.us.sendFortify(startRegion, endRegion, amount);
}
});
document

View File

@ -103,6 +103,14 @@ export class Packet {
});
}
static createFortify(fortify, verification) {
return this._sign({
...this._createBase("ACT"),
fortify: fortify,
verification: verification,
});
}
static createDefense(amount) {
return this._sign({
...this._createBase("ACT"),

View File

@ -4,7 +4,7 @@ import { RsaPubKey } from "../crypto/rsa.js";
import { PaillierPubKey, ReadOnlyCiphertext } from "../crypto/paillier.js";
import { Region } from "./map.js";
import { showDefenseDom } from "./dom.js";
import { proveRange, proveRegions, verifyRegions } from "./proofs.js";
import { proveFortify, proveRange, proveRegions, verifyRegions } from "./proofs.js";
// Timeout to consider a player disconnected
const TIMEOUT = 30_000;
@ -178,6 +178,45 @@ export class Player {
}
}
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
let sender = Region.getRegion(data.startRegion);
let receiver = Region.getRegion(data.endRegion);
let strength = parseInt(data.strength);
if (
sender.owner === this &&
receiver.owner === this &&
sender.strength > strength &&
strength > 0
) {
receiver.reinforce(strength);
sender.strength -= strength;
return true;
} else {
return false;
}
}
sendFortify(startRegion, endRegion, amount) {
let fortify = {
[startRegion]: new Ciphertext(this.paillierPubKey, BigInt(-amount)),
[endRegion]: new Ciphertext(this.paillierPubKey, BigInt(amount)),
};
for (let r of this.getRegions()) {
if (!fortify.hasOwnProperty(r)) {
fortify[r] = new Ciphertext(this.paillierPubKey, 0n);
}
}
socket.emit("message", Packet.createFortify(fortify, proveFortify(fortify)));
}
/**
* Process a generic action packet representing a player's move.
*
@ -213,9 +252,13 @@ export class Player {
this.turnPhase = PHASE_FORTIFY;
if (data.action === "FORTIFY") {
if (this === game.us) {
return;
} else {
return this.fortify(data);
}
}
}
return false;
}
@ -352,30 +395,6 @@ export class Player {
return promise;
}
/**
* Process an action which is to attack another region.
*
* @param data Data received via socket
*/
fortify(data) {
let sender = Region.getRegion(data.startRegion);
let receiver = Region.getRegion(data.endRegion);
let strength = parseInt(data.strength);
if (
sender.owner === this &&
receiver.owner === this &&
sender.strength > strength &&
strength > 0
) {
receiver.reinforce(strength);
sender.strength -= strength;
return true;
} else {
return false;
}
}
/**
* Calculate the number of additional reinforcements to gain on this turn.
*/

View File

@ -295,7 +295,8 @@ export function verifyRange(obj, key) {
window.verifyRange = verifyRange;
/**
* - We prove that the set contains |S| - 2 zeros, with the final pair summing to zero
* - 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
*/
@ -311,11 +312,13 @@ export function proveFortify(fortify) {
psiMap[regionNames[i]] = psi[i];
}
let newRegions = structuredClone(fortify);
let newRegions = { regions: {} };
// Rearrange keys
for (let r of regionNames) {
newRegions[psiMap[r]] = fortify[r].pubKey.encrypt(fortify[r].plainText);
newRegions.regions[psiMap[r]] = fortify[r].pubKey.encrypt(
-fortify[r].plainText
);
}
let edges = [];
@ -363,8 +366,12 @@ export function proveFortify(fortify) {
for (let i = 0; i < ROUNDS; i++) {
let coin = coins[i];
let proof = proofs[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
@ -389,17 +396,29 @@ export function proveFortify(fortify) {
// Show pair is joined by edge
let pairName = pair.sort();
console.log(input);
verification.pairEdgeSalt = input.edges.find(
(e) => e.edge[0] === pairName[0] && e.edge[1] === pairName[1]
);
).salt;
verifications.push(verification);
} else {
// Show isomorphism
let edges = {};
for (let e of input.edges) {
edges[e.edge[0] + e.edge[1]] = e.salt;
}
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: input.edges.map((e) => e.salt),
salts: edges,
zeroProofs: zeroProofs,
});
}
}
@ -415,4 +434,116 @@ export function proveFortify(fortify) {
window.proveFortify = proveFortify;
// proveRegions({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)})
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(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(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(fortify[psiMap[r]]);
let p = c.verifyNI(verification.zeroProofs[r]);
if (p !== 0n) {
console.log(p);
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(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)})

Binary file not shown.

View File

@ -212,22 +212,6 @@ A typical example for zero-knowledge proofs is graph isomorphism \cite{10.1145/1
Identifying Risk as a graph therefore enables us to construct isomorphisms as part of the proof protocol. For example, when a player wishes to commit to a movement, it is important to prove that the initial node and the new node are adjacent. This can be proven by communicating isomorphic graphs, and constructing challenges based on the edges of the original graph.
\subsubsection{Adjacency proofs}
Proving adjacency of two nodes is akin to proving isomorphism of two graphs. A protocol using challenges could be constructed as follows: \begin{enumerate}
\item The prover commits a new edge between two nodes.
\item The prover constructs an isomorphic graph to the game, and encrypts the edges.
\item The verified challenges either: \begin{itemize}
\item That the graphs are isomorphic.
\item That the new edge is valid.
\end{itemize}
\item The prover sends a total decryption key for the graph's nodes, to prove isomorphism to the game board; or a decryption key for the new edge to the isomorphism, to prove adjacency.
\end{enumerate}
These challenges restrict the ability for the prover to cheat: if the two nodes they are committing to are not adjacent, either the prover will need to commit an invalid isomorphism (detected by challenge 1), or lie about the edge they have committed (detected by challenge 2).
Selection between two challenges is the ideal number of challenges to use, as the probability of cheating being detected is $\frac{1}{2}$. Using more challenge options (e.g, $n$) means the likelihood of the prover cheating a single challenge reduces to $\frac{1}{n}$. This would require much larger numbers of communications to then convince the verifier to the same level of certainty.
\subsubsection{Cheating with negative values}
Zerocash is a ledger system that uses zero-knowledge proofs to ensure consistency and prevent cheating. Ledgers are the main existing use case of zero-knowledge proofs, and there are some limited similarities between ledgers and Risk in how they wish to obscure values of tokens within the system.
@ -388,13 +372,15 @@ The game is broken down into three main stages, each of which handles events in
\node[draw=blue!50,rectangle,very thick,rounded corners=0.1mm,anchor=north] (Act1) at (56pt, -50pt) {Player acts};
\end{tikzpicture}\end{landscape}
\subsection{Message structure}
\section{Message structure}
Messages are given a fixed structure to make processing simpler. Each JSON message holds an \texttt{author} field, being the sender's ID; a message ID to prevent replay attacks and associate related messages; and an \texttt{action}, which at a high level dictates how each client should process the message.
Each JSON message holds an \texttt{author} field, being the sender's ID; a message ID to associate related messages; a timestamp to prevent replay attacks; and an \texttt{action}, which at a high level dictates how each client should process the message.
The action more specifically is one of \texttt{ANNOUNCE}, \texttt{DISCONNECT}, \texttt{KEEPALIVE}, \texttt{RANDOM}, \texttt{PROOF}, and \texttt{ACT}. The first three of these are used for managing the network by ensuring peers are aware of each other and know the state of the network. \texttt{RANDOM} and \texttt{PROOF} are designated to be used by sub-protocols defined later on. \texttt{ACT} is used by players to submit actions for their turn during gameplay.
The "action" is one of \texttt{ANNOUNCE}, \texttt{DISCONNECT}, \texttt{KEEPALIVE}, \texttt{RANDOM}, \texttt{PROOF}, and \texttt{ACT}. The first three of these are used for managing the network by ensuring peers are aware of each other and know the state of the network. \texttt{ANNOUNCE} is transmitted upon a player joining to ensure the new player is aware of all other players. The \texttt{ANNOUNCE} message contains the player's encryption keys and the player's ID.
Each message is also signed to verify the author. This is a standard application of RSA. A hash of the message is taken, then encrypted with the private key. This can be verified with the public key.
\texttt{RANDOM} and \texttt{PROOF} are designated to be used by sub-protocols defined later on. \texttt{ACT} is used by players to submit actions for their turn during gameplay.
Each message is also signed to verify the author. This is a standard application of RSA. A SHA-3 hash of the message is taken, then encrypted with the private key. This can be verified with the public key.
Players trust RSA keys on a trust-on-first-use (TOFU) basis. TOFU is the same protocol as used by Gemini \cite{projectgemini}. The main issue with TOFU is that if a malicious party intercepts the first communication, they may substitute the RSA credentials transmitted by the intended party, resulting in a man-in-the-middle attack.
@ -445,7 +431,7 @@ Besides reducing the number of operations to perform exponentiation, exponentiat
In Jurik's form, we also need to compute $h$, a generator of the Jacobi subgroup, and impose restrictions on $p, q$. In particular, it is required that $p \equiv q \equiv 3 \mod 4$, $\gcd(p-1, q-1) = 2$, and that $p-1, q-1$ consist of large factors except for 2. One method to guarantee this is to use safe primes, which are primes of form $2p+1$ for $p$ prime.
\begin{proposition}
Safe primes are of form $p \equiv 3 \mod 4$
For $p > 5$ a safe prime, $p \equiv 3 \mod 4$
\end{proposition}
\begin{proof}
@ -457,7 +443,7 @@ In Jurik's form, we also need to compute $h$, a generator of the Jacobi subgroup
\end{proof}
\begin{proposition}
Safe primes $p \neq q$ satisfy $\gcd(p - 1, q - 1) = 2$
For safe primes $p \neq q$ with $p, q > 5$, $\gcd(p - 1, q - 1) = 2$
\end{proposition}
\begin{proof}
@ -498,18 +484,17 @@ To achieve a better speed-up, pre-computation of the fixed base $h^n \bmod n$ is
\EndFunction
\end{algorithmic}
%todo got up to here
\subsection{Private key}
The private key is the value of the Carmichael function $\lambda = \lambda(n)$, defined as the exponent of the group $\mathbb{Z}^*_n$. From the Chinese remainder theorem, $\lambda(n) = \lambda(pq)$ can be computed as $\lcm(\lambda(p), \lambda(q))$. From Carmichael's theorem, this is equivalent to $\lcm(\phi(p), \phi(q))$. Hence, from the definition of $\phi$, and as $p, q$ are equal length, $\lambda = (p - 1)(q - 1) = \phi(n)$.
We are also interested in the ability to compute $\mu = \lambda^{-1} \mod n$ as part of decryption. Fortunately, this is easy, as from Euler's theorem, $\lambda^{\phi(n)} \equiv 1 \mod n$, and so we propose $\mu = \lambda^{\phi(n) - 1} \mod n$. As $\phi(n)$ is easily computable with knowledge of $p, q$, we get ${\mu = \lambda^{(p - 1)(q - 1)} \mod n}$, a relatively straight-forward computation.
We also need to compute $\mu = \lambda^{-1} \mod n$ as part of decryption. Fortunately, this is easy, as from Euler's theorem, $\lambda^{\phi(n)} \equiv 1 \mod n$, and so we propose $\mu = \lambda^{\phi(n) - 1} \mod n$. As $\phi(n)$ is easily computable with knowledge of $p, q$, we get ${\mu = \lambda^{(p - 1)(q - 1)} \mod n}$, a relatively straight-forward computation.
\subsection{Decryption}
Let $c$ be the ciphertext. The corresponding plaintext is computed as \begin{align*}
m = L(c^\lambda \mod n^2) \cdot \mu \mod n,
\end{align*} where $L(x) = \frac{x - 1}{n}$. This operation can be optimised by applying Chinese remainder theorem. However, in the application presented, decryption is not used and is only useful as a debugging measure. So this need not be optimised.
\end{align*} where $L(x) = \frac{x - 1}{n}$. This operation can be optimised by applying Chinese remainder theorem. However, in the application presented, decryption is not used and is only useful as a debugging measure. So this optimisation is not applied.
\subsection{Implementation details}
@ -551,7 +536,7 @@ This is achieved through bit-commitment and properties of $\mathbb{Z}_n$. The pr
To generalise this to $n$ peers, we ensure that each peer waits to receive all encrypted noises before transmitting their decryption key.
Depending on how $N_A + N_B$ is then turned into a random value within a range, this system may be manipulated by an attacker who has some knowledge of how participants are generating their noise. As a basic example, suppose a random value within range is generated by taking $N_A + N_B \mod 3$, and participants are producing 2-bit noises. An attacker could submit a 3-bit noise with the most-significant bit set, in which case the probability of the final result being a 1 are significantly higher than the probability of a 0 or a 2. This is a typical example of modular bias. To avoid this problem, peers should agree beforehand on the number of bits to transmit. Addition of noise will then operate modulo $2^\ell$, where $\ell$ is the agreed-upon number of bits.
Depending on how $N_A + N_B$ is then turned into a random value within a range, this system may be manipulated by an attacker who has some knowledge of how participants are generating their noise. As an example, suppose a random value within range is generated by taking $N_A + N_B \mod 3$, and participants are producing 2-bit noises. An attacker could submit a 3-bit noise with the most-significant bit set, in which case the probability of the final result being a 1 is significantly higher than the probability of a 0 or a 2. This is a typical example of modular bias. To avoid this problem, peers should agree beforehand on the number of bits to transmit. To combine noises, then use the XOR operation.
The encryption function used must also guarantee the integrity of decrypted ciphertexts to prevent a malicious party creating a ciphertext which decrypts to multiple valid values through using different keys.
@ -573,7 +558,9 @@ The encryption function used must also guarantee the integrity of decrypted ciph
This extends inductively to support $n-1$ cheating participants, even if colluding. Finally, we must consider how to reduce random noise to useful values.
\subsection{Avoiding modular bias}
\subsection{Modular bias}
A common approach is to take the modulus of the random noise. This causes modular bias to appear however, where some values are less likely to be generated.
The typical way to avoid modular bias is by resampling. To avoid excessive communication, resampling can be performed within the bit sequence by partitioning into blocks of $n$ bits and taking blocks until one falls within range. This is appropriate in the presented use case as random values need only be up to 6, so the probability of consuming over 63 bits of noise when resampling for a value in the range 0 to 5 is $\left(\frac{1}{4}\right)^{21} \approx 2.3 \times 10^{-13}$.
@ -711,9 +698,9 @@ Additionally, we can consider this protocol perfect zero-knowledge.
In reality, as we are using Jurik's form of Paillier, the best we can hope for is computational zero-knowledge, as Jurik's form relies upon the computational indistinguishability of the sequence generated by powers of $h$ to random powers.
\subsection{Extending \hyperref[protocol1]{Protocol~\ref*{protocol1}} for adjacency}
\subsection{Proving fortifications}
Performing a "fortify" action is a more complicated variant of the "reinforce" action. We devise a modified
Performing a "fortify" action is distinct to the "reinforce" action and requires its own verification.
Firstly, the set being proven on changes form to $k, -k, 0, \dots, 0$, for a movement of $k$ units from one region to another. The challenges then change form to be proving that either these sum to zero, or that all but two are zero, with the remaining pair summing to zero (this appropriately hides the true value of $k$).