Ensure ciphertexts are cloned to avoid mutating actual object.
This commit is contained in:
parent
5557a8bff6
commit
848c56ff84
@ -23,7 +23,6 @@ class Cyphertext {
|
||||
this.cyphertext += key.n2;
|
||||
}
|
||||
|
||||
console.log(performance.now());
|
||||
this.r = r;
|
||||
this.pubKey = key;
|
||||
this.plainText = plainText;
|
||||
@ -32,16 +31,13 @@ class Cyphertext {
|
||||
}
|
||||
|
||||
update(c) {
|
||||
this.cyphertext = (this.cyphertext * c.cyphertext) % this.pubKey.n ** 2n;
|
||||
this.r = (this.r * c.r) % this.pubKey.n ** 2n;
|
||||
this.cyphertext = (this.cyphertext * c.cyphertext) % this.pubKey.n2;
|
||||
this.r = (this.r * c.r) % this.pubKey.n2;
|
||||
this.plainText += c.plainText;
|
||||
|
||||
// Force into range
|
||||
while (this.cyphertext < 0n) {
|
||||
this.cyphertext += this.pubKey.n ** 2n;
|
||||
}
|
||||
while (this.r < 0n) {
|
||||
this.r += this.pubKey.n ** 2n;
|
||||
this.cyphertext += this.pubKey.n2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,11 +65,11 @@ class ProofSessionProver {
|
||||
}
|
||||
|
||||
get a() {
|
||||
return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n ** 2n);
|
||||
return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n2);
|
||||
}
|
||||
|
||||
noise() {
|
||||
return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n ** 2n);
|
||||
return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n2);
|
||||
}
|
||||
|
||||
prove(challenge) {
|
||||
@ -102,26 +98,33 @@ export class ReadOnlyCyphertext {
|
||||
}
|
||||
|
||||
update(c) {
|
||||
this.cyphertext = (this.cyphertext * c.cyphertext) % this.pubKey.n ** 2n;
|
||||
this.cyphertext = (this.cyphertext * c.cyphertext) % this.pubKey.n2;
|
||||
|
||||
// Force into range
|
||||
while (this.cyphertext < 0n) {
|
||||
this.cyphertext += this.pubKey.n ** 2n;
|
||||
this.cyphertext += this.pubKey.n2;
|
||||
}
|
||||
}
|
||||
|
||||
prove(plainText, a) {
|
||||
return new ProofSessionVerifier(this, plainText, a);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new ReadOnlyCyphertext(this.pubKey, this.cyphertext);
|
||||
}
|
||||
}
|
||||
|
||||
class ProofSessionVerifier {
|
||||
constructor(cipherText, plainText, a) {
|
||||
this.cipherText = cipherText;
|
||||
// Clone, otherwise the update below will mutate the original value
|
||||
this.cipherText = cipherText.clone();
|
||||
this.cipherText.update(this.cipherText.pubKey.encrypt(-1n * plainText, 1n));
|
||||
// Shift the challenge down by 1 to ensure it is smaller than either prime factor.
|
||||
this.challenge = cryptoRandom(2048) << 1n;
|
||||
this.a = a;
|
||||
|
||||
this.plainText = plainText;
|
||||
}
|
||||
|
||||
verify(proof) {
|
||||
@ -204,3 +207,5 @@ export function generate_keypair() {
|
||||
|
||||
return { pubKey, privKey };
|
||||
}
|
||||
|
||||
// p = a.prove(); v = p.asVerifier(); v.verify(p.prove(v.challenge));
|
||||
|
@ -34,7 +34,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
socket.on("message", async (packet) => {
|
||||
window.console.log(`Received size: ${JSON.stringify(packet).length}`);
|
||||
// window.console.log(`Received size: ${JSON.stringify(packet).length}`);
|
||||
|
||||
let data = packet.payload;
|
||||
if (data.type !== "KEEPALIVE") window.console.log(data);
|
||||
|
@ -11,9 +11,46 @@ class Continent {
|
||||
}
|
||||
|
||||
class Strength {
|
||||
constructor(cipherText) {
|
||||
constructor(cipherText, regionName) {
|
||||
this.cipherText = cipherText;
|
||||
this.assumedStrength = null;
|
||||
|
||||
this.prover = null;
|
||||
document.addEventListener("PROOF", (ev) => {
|
||||
const data = ev.detail;
|
||||
|
||||
if (
|
||||
data.region === regionName &&
|
||||
data.stage === "CHALLENGE" &&
|
||||
this.prover !== null
|
||||
) {
|
||||
let z = this.prover.prove(BigInt(data.challenge));
|
||||
|
||||
socket.emit(
|
||||
"message",
|
||||
Packet.createProof(regionName, "0x" + z.toString(16))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.verifier = null;
|
||||
document.addEventListener("PROOF", (ev) => {
|
||||
const data = ev.detail;
|
||||
|
||||
if (
|
||||
data.region === regionName &&
|
||||
data.stage === "PROOF" &&
|
||||
this.verifier !== null
|
||||
) {
|
||||
let result = this.verifier.verify(BigInt(data.z));
|
||||
if (result > 0) {
|
||||
this.assumedStrength = this.verifier.plainText;
|
||||
document.dispatchEvent(new CustomEvent("updateStrengths"));
|
||||
} else {
|
||||
console.warn(`Failed to verify ciphertext! ${result}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(cipherText) {
|
||||
@ -31,33 +68,14 @@ class Strength {
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
let proofSessionProver = this.cipherText.prove();
|
||||
|
||||
document.addEventListener(
|
||||
"PROOF",
|
||||
(ev) => {
|
||||
const data = ev.detail;
|
||||
|
||||
if (data.region === region && data.stage === "CHALLENGE") {
|
||||
let z = proofSessionProver.prove(BigInt(data.challenge));
|
||||
|
||||
socket.emit(
|
||||
"message",
|
||||
Packet.createProof(region, "0x" + z.toString(16))
|
||||
);
|
||||
controller.abort();
|
||||
}
|
||||
},
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
this.prover = this.cipherText.prove();
|
||||
|
||||
socket.emit(
|
||||
"message",
|
||||
Packet.createProofConjecture(
|
||||
region,
|
||||
"0x" + this.cipherText.plainText.toString(),
|
||||
"0x" + proofSessionProver.a.toString(16)
|
||||
"0x" + this.prover.a.toString(16)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -67,33 +85,13 @@ class Strength {
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
let proofSessionVerifier = this.cipherText.prove(plainText, a);
|
||||
|
||||
document.addEventListener(
|
||||
"PROOF",
|
||||
(ev) => {
|
||||
const data = ev.detail;
|
||||
|
||||
if (data.region === region && data.stage === "PROOF") {
|
||||
let result = proofSessionVerifier.verify(BigInt(data.z));
|
||||
if (result > 0) {
|
||||
this.assumedStrength = plainText;
|
||||
document.dispatchEvent(new CustomEvent("updateStrengths"));
|
||||
controller.abort();
|
||||
} else {
|
||||
console.warn(`Failed to verify ciphertext! ${result}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
this.verifier = this.cipherText.prove(plainText, a);
|
||||
|
||||
socket.emit(
|
||||
"message",
|
||||
Packet.createProofChallenge(
|
||||
region,
|
||||
"0x" + proofSessionVerifier.challenge.toString(16)
|
||||
"0x" + this.verifier.challenge.toString(16)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -103,7 +101,7 @@ export class Region {
|
||||
constructor(name, continent) {
|
||||
this.name = name;
|
||||
this.owner = null;
|
||||
this.strength = new Strength(null);
|
||||
this.strength = new Strength(null, name);
|
||||
this.neighbours = new Set();
|
||||
this.continent = continent;
|
||||
|
||||
|
@ -156,7 +156,10 @@ export class Player {
|
||||
|
||||
// send proofs
|
||||
for (let region of this.getRegions()) {
|
||||
region.prove();
|
||||
// eh
|
||||
if ([...region.neighbours.values()].find((r) => r.owner !== this)) {
|
||||
region.prove();
|
||||
}
|
||||
}
|
||||
|
||||
this.endTurn();
|
||||
|
Binary file not shown.
@ -307,9 +307,74 @@ Let $c$ be the ciphertext. The corresponding plaintext is computed as $m = L(c^\
|
||||
|
||||
Paillier is implemented by four classes: \texttt{PubKey}, \texttt{PrivKey}, \texttt{Ciphertext}, and \texttt{ReadOnlyCiphertext}. \texttt{PubKey.encrypt} converts a \texttt{BigInt} into either a \texttt{Ciphertext} or a \texttt{ReadOnlyCiphertext} by the encryption function above. The distinction between these is that a \texttt{ReadOnlyCiphertext} does not know the random $r$ that was used to form it, and so is created by decrypting a ciphertext that originated with another peer. A regular \texttt{Ciphertext} maintains knowledge of $r$ and the plaintext it enciphers. This makes it capable of proving by the scheme presented below.
|
||||
|
||||
\subsection{Shared random values}
|
||||
|
||||
A large part of Risk involves random behaviour dictated by rolling some number of dice. To achieve this, some fair protocol must be used to generate random values consistently across each peer without any peer being able to manipulate the outcomes.
|
||||
|
||||
This is achieved through bit-commitment and properties of $\mathbb{Z}_n$. The protocol for two peers is as follows, and generalises to $n$ peers trivially.
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[
|
||||
every node/.append style={very thick,rounded corners=0.1mm}
|
||||
]
|
||||
|
||||
\node[draw,rectangle] (A) at (0,0) {Peer A};
|
||||
|
||||
\node[draw,rectangle] (B) at (6,0) {Peer B};
|
||||
|
||||
\node[draw=blue!50,rectangle,thick,text width=4cm] (NoiseA) at (0,-1.5) {Generate random noise $N_A$, random key $k_A$};
|
||||
\node[draw=blue!50,rectangle,thick,text width=4cm] (NoiseB) at (6,-1.5) {Generate random noise $N_B$, random key $k_B$};
|
||||
|
||||
\draw [->,very thick] (0,-3)--node [auto] {$E_{k_A}(N_A)$}++(6,0);
|
||||
\draw [<-,very thick] (0,-4)--node [auto] {$E_{k_B}(N_B)$}++(6,0);
|
||||
|
||||
\draw [->,very thick] (0,-5)--node [auto] {$k_A$}++(6,0);
|
||||
\draw [<-,very thick] (0,-6)--node [auto] {$k_B$}++(6,0);
|
||||
|
||||
\node[draw=blue!50,rectangle,thick] (CA) at (0,-7) {Compute $N_A + N_B$};
|
||||
\node[draw=blue!50,rectangle,thick] (CB) at (6,-7) {Compute $N_A + N_B$};
|
||||
|
||||
\draw [very thick] (A)-- (NoiseA)-- (CA)-- (0,-7);
|
||||
\draw [very thick] (B)-- (NoiseB)-- (CB)-- (6,-7);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
\begin{proposition}
|
||||
With the above considerations, the scheme shown is not manipulable by a single cheater.
|
||||
\end{proposition}
|
||||
|
||||
\begin{proof}
|
||||
Suppose $P_1, \dots, P_{n-1}$ are honest participants, and $P_n$ is a cheater with a desired outcome.
|
||||
|
||||
In step 1, each participant $P_i$ commits $E_{k_i}(N_i)$. The cheater $P_n$ commits a constructed noise $E_{k_n}(N_n)$.
|
||||
|
||||
The encryption function $E_k$ holds the confidentiality property: that is, without $k$, $P_i$ cannot retrieve $m$ given $E_k(m)$. So $P_n$'s choice of $N_n$ cannot be directed by other commitments.
|
||||
|
||||
The final value is dictated by the sum of all decrypted values. $P_n$ is therefore left in a position of choosing $N_n$ to control the outcome of $a + N_n$, where $a$ is selected uniformly at random from the abelian group $\mathbb{Z}_{2^\ell}$ for $\ell$ the agreed upon bit length.
|
||||
|
||||
As every element of this group is of order $2^\ell$, the distribution of $a + N_n$ is identical no matter the choice of $N_n$. So $P_n$ maintains no control over the outcome of $a + N_n$.
|
||||
\end{proof}
|
||||
|
||||
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}
|
||||
|
||||
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}$.
|
||||
|
||||
\subsection{Application to domain}
|
||||
|
||||
Random values are used in two places. \begin{itemize}
|
||||
\item Selecting the first player.
|
||||
\item Rolling dice.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Proof system}
|
||||
|
||||
The proof system is that of \cite{damgard2003}. The authors give a method to prove knowledge of an encrypted value. The importance of using a zero-knowledge method for this is that it verifies knowledge to a single party. This party should be an honest verifier: this is an assumption we have made of the context, but in general this is not true, and so this provides an attack surface for colluding parties.
|
||||
The first proof to discuss is that of \cite{damgard2003}. The authors give a method to prove knowledge of an encrypted value. The importance of using a zero-knowledge method for this is that it verifies knowledge to a single party. This party should be an honest verifier: this is an assumption we have made of the context, but in general this is not true, and so this provides an attack surface for colluding parties.
|
||||
|
||||
The proof system presented is an interactive proof for a given ciphertext $c$ being an encryption of zero.
|
||||
|
||||
@ -376,9 +441,6 @@ Players should prove a number of properties of their game state to each other to
|
||||
|
||||
\cite{Boudot2000EfficientPT}'s proof is a multi-round proof more similar in structure to the graph isomorphism proof presented in \cite{10.1145/116825.116852}. We select public parameter $\ell$ to be some sufficiently high value that a player's unit count should not exceed during play: an appropriate choice may be 1000. Select $n$ as the number of units that the player is defending with, or in the case of attacking, let $n$ be the number of units that the player is attacking with plus 1 (as is required by the rules of Risk).
|
||||
|
||||
To reduce the number of times the proof must be conducted, we use the Fiat-Shamir heuristic, with the shared random values scheme %todo move this
|
||||
acting as the random oracle.
|
||||
|
||||
\subsection{Cheating with negative values}
|
||||
|
||||
By using negative values, a player can cheat stage (1) of the above. This is a severe issue, as potentially the cheat could be completely unnoticed even in the conclusion of the game. To overcome this, we apply proofs on each committed value that are verified by all players.
|
||||
@ -432,70 +494,9 @@ Additionally, we can consider this protocol perfect zero-knowledge.
|
||||
This gives $T^*$ such that $T^*(S) = T(P, V, S)$, and the output distributions are identical. Hence, this proof is perfect zero-knowledge under random oracle model.
|
||||
\end{proof}
|
||||
|
||||
\subsection{Shared random values}
|
||||
\subsection{Optimising}
|
||||
|
||||
A large part of Risk involves random behaviour dictated by rolling some number of dice. To achieve this, some fair protocol must be used to generate random values consistently across each peer without any peer being able to manipulate the outcomes.
|
||||
|
||||
This is achieved through bit-commitment and properties of $\mathbb{Z}_n$. The protocol for two peers is as follows, and generalises to $n$ peers trivially.
|
||||
|
||||
\begin{center}
|
||||
\begin{tikzpicture}[
|
||||
every node/.append style={very thick,rounded corners=0.1mm}
|
||||
]
|
||||
|
||||
\node[draw,rectangle] (A) at (0,0) {Peer A};
|
||||
|
||||
\node[draw,rectangle] (B) at (6,0) {Peer B};
|
||||
|
||||
\node[draw=blue!50,rectangle,thick,text width=4cm] (NoiseA) at (0,-1.5) {Generate random noise $N_A$, random key $k_A$};
|
||||
\node[draw=blue!50,rectangle,thick,text width=4cm] (NoiseB) at (6,-1.5) {Generate random noise $N_B$, random key $k_B$};
|
||||
|
||||
\draw [->,very thick] (0,-3)--node [auto] {$E_{k_A}(N_A)$}++(6,0);
|
||||
\draw [<-,very thick] (0,-4)--node [auto] {$E_{k_B}(N_B)$}++(6,0);
|
||||
|
||||
\draw [->,very thick] (0,-5)--node [auto] {$k_A$}++(6,0);
|
||||
\draw [<-,very thick] (0,-6)--node [auto] {$k_B$}++(6,0);
|
||||
|
||||
\node[draw=blue!50,rectangle,thick] (CA) at (0,-7) {Compute $N_A + N_B$};
|
||||
\node[draw=blue!50,rectangle,thick] (CB) at (6,-7) {Compute $N_A + N_B$};
|
||||
|
||||
\draw [very thick] (A)-- (NoiseA)-- (CA)-- (0,-7);
|
||||
\draw [very thick] (B)-- (NoiseB)-- (CB)-- (6,-7);
|
||||
\end{tikzpicture}
|
||||
\end{center}
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
\begin{proposition}
|
||||
With the above considerations, the scheme shown is not manipulable by a single cheater.
|
||||
\end{proposition}
|
||||
|
||||
\begin{proof}
|
||||
Suppose $P_1, \dots, P_{n-1}$ are honest participants, and $P_n$ is a cheater with a desired outcome.
|
||||
|
||||
In step 1, each participant $P_i$ commits $E_{k_i}(N_i)$. The cheater $P_n$ commits a constructed noise $E_{k_n}(N_n)$.
|
||||
|
||||
The encryption function $E_k$ holds the confidentiality property: that is, without $k$, $P_i$ cannot retrieve $m$ given $E_k(m)$. So $P_n$'s choice of $N_n$ cannot be directed by other commitments.
|
||||
|
||||
The final value is dictated by the sum of all decrypted values. $P_n$ is therefore left in a position of choosing $N_n$ to control the outcome of $a + N_n$, where $a$ is selected uniformly at random from the abelian group $\mathbb{Z}_{2^\ell}$ for $\ell$ the agreed upon bit length.
|
||||
|
||||
As every element of this group is of order $2^\ell$, the distribution of $a + N_n$ is identical no matter the choice of $N_n$. So $P_n$ maintains no control over the outcome of $a + N_n$.
|
||||
\end{proof}
|
||||
|
||||
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}
|
||||
|
||||
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}$.
|
||||
|
||||
\subsection{Application to domain}
|
||||
|
||||
Random values are used in two places. \begin{itemize}
|
||||
\item Selecting the first player.
|
||||
\item Rolling dice.
|
||||
\end{itemize}
|
||||
To reduce the number of times the proof must be conducted, we use the Fiat-Shamir heuristic. For this, we need a random oracle: in our situation, this will be substituted by using a cryptographic hash function.
|
||||
|
||||
\section{Review}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user