This commit is contained in:
jude 2023-04-10 19:05:10 +01:00
parent 8c5b6dbac9
commit 1647615595
8 changed files with 154 additions and 34 deletions

View File

@ -16,11 +16,11 @@ class Ciphertext {
let gm = (1n + key.n * plainText) % key.n2;
// Compute g^m r^n from crt.
this.cyphertext = (gm * mod_exp(r, key.n, key.n2)) % key.n2;
this.cipherText = (gm * mod_exp(r, key.n, key.n2)) % key.n2;
// Force into range.
while (this.cyphertext < 0n) {
this.cyphertext += key.n2;
while (this.cipherText < 0n) {
this.cipherText += key.n2;
}
this.r = r;
@ -31,30 +31,69 @@ class Ciphertext {
}
update(c) {
this.cyphertext = (this.cyphertext * c.cyphertext) % this.pubKey.n2;
this.cipherText = (this.cipherText * c.cipherText) % 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.n2;
while (this.cipherText < 0n) {
this.cipherText += this.pubKey.n2;
}
}
toString() {
return "0x" + this.cyphertext.toString(16);
return "0x" + this.cipherText.toString(16);
}
prove() {
return new ZeroProofSessionProver(this);
return new ValueProofSessionProver(this);
}
asReadOnlyCyphertext() {
return new ReadOnlyCiphertext(this.pubKey, this.cyphertext);
// Construct a non-interactive proof
proveNI() {
let rp = cryptoRandom(4096);
while (rp >= this.pubKey.n) {
rp = cryptoRandom(4096);
}
let a = mod_exp(rp, this.pubKey.n, this.pubKey.n2);
let hasher = new jsSHA("SHAKE256", "HEX");
let plainText = this.plainText.toString(16);
if (plainText.length % 2 !== 0) {
plainText = "0" + plainText;
}
let aStr = a.toString(16);
if (aStr.length % 2 !== 0) {
aStr = "0" + aStr;
}
hasher.update(this.pubKey.g.toString(16));
hasher.update(plainText);
hasher.update(aStr);
let challenge = BigInt("0x" + hasher.getHash("HEX", { outputLen: 2048 }));
return {
plainText: "0x" + this.plainText.toString(16),
a: "0x" + a.toString(16),
challenge: "0x" + challenge.toString(16),
proof:
"0x" +
(
((rp % this.pubKey.n) * mod_exp(this.r, challenge, this.pubKey.n)) %
this.pubKey.n
).toString(16),
};
}
asReadOnlyCiphertext() {
return new ReadOnlyCiphertext(this.pubKey, this.cipherText);
}
}
class ZeroProofSessionProver {
class ValueProofSessionProver {
constructor(cipherText) {
this.cipherText = cipherText;
@ -82,46 +121,63 @@ class ZeroProofSessionProver {
asVerifier() {
return this.cipherText
.asReadOnlyCyphertext()
.asReadOnlyCiphertext()
.prove(this.cipherText.plainText, this.noise());
}
}
window.Cyphertext = Ciphertext;
window.Ciphertext = Ciphertext;
export class ReadOnlyCiphertext {
constructor(key, cyphertext) {
this.cyphertext = cyphertext;
constructor(key, cipherText) {
this.cipherText = cipherText;
this.pubKey = key;
this.readOnly = true;
}
update(c) {
this.cyphertext = (this.cyphertext * c.cyphertext) % this.pubKey.n2;
this.cipherText = (this.cipherText * c.cipherText) % this.pubKey.n2;
// Force into range
while (this.cyphertext < 0n) {
this.cyphertext += this.pubKey.n2;
while (this.cipherText < 0n) {
this.cipherText += this.pubKey.n2;
}
}
prove(plainText, a) {
return new ZeroProofSessionVerifier(this, plainText, a);
return new ValueProofSessionVerifier(this, plainText, a);
}
verifyNI(statement) {
let verifier = new ValueProofSessionVerifier(
this,
BigInt(statement.plainText),
BigInt(statement.a),
BigInt(statement.challenge)
);
return verifier.verify(BigInt(statement.proof));
}
clone() {
return new ReadOnlyCiphertext(this.pubKey, this.cyphertext);
return new ReadOnlyCiphertext(this.pubKey, this.cipherText);
}
}
class ZeroProofSessionVerifier {
constructor(cipherText, plainText, a) {
class ValueProofSessionVerifier {
constructor(cipherText, plainText, a, challenge) {
// 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;
if (challenge === undefined) {
// Shift the challenge down by 1 to ensure it is smaller than either prime factor.
this.challenge = cryptoRandom(2048) << 1n;
} else {
this.challenge = challenge;
}
this.a = a;
this.plainText = plainText;
@ -130,14 +186,14 @@ class ZeroProofSessionVerifier {
verify(proof) {
// check coprimality
if (gcd(proof, this.cipherText.pubKey.n) !== 1n) return -1;
if (gcd(this.cipherText.cyphertext, this.cipherText.pubKey.n) !== 1n) return -2;
if (gcd(this.cipherText.cipherText, this.cipherText.pubKey.n) !== 1n) return -2;
if (gcd(this.a, this.cipherText.pubKey.n) !== 1n) return -3;
// check exp
return mod_exp(proof, this.cipherText.pubKey.n, this.cipherText.pubKey.n2) ===
(this.a *
mod_exp(
this.cipherText.cyphertext,
this.cipherText.cipherText,
this.challenge,
this.cipherText.pubKey.n2
)) %
@ -147,7 +203,7 @@ class ZeroProofSessionVerifier {
}
}
window.ReadOnlyCyphertext = ReadOnlyCiphertext;
window.ReadOnlyCiphertext = ReadOnlyCiphertext;
export class PaillierPubKey {
constructor(n) {

View File

@ -5,6 +5,7 @@ import { Packet } from "./packet.js";
import { Game } from "./game.js";
import { Region } from "./map.js";
import "./dom.js";
import "./proofs.js";
export const ID = window.crypto.randomUUID();
export const game = new Game();

View File

@ -0,0 +1,17 @@
function cryptoShuffle(l) {
for (let i = 0; i < l.length - 1; i++) {}
}
window.cryptoShuffle = cryptoShuffle;
function proveRegions(regions) {
// Construct prover coins
let regionNames = Object.keys(regions.keys());
let psi = [regionNames];
// Construct verifier coins
let hasher = new jsSHA("SHA3-256", "TEXT");
hasher.update(JSON.stringify(regions));
// Construct prover proofs
}

21
static/js/sha3.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="{{ url_for('static', filename='js/sha3.js') }}" type="module"></script>
<script src="{{ url_for('static', filename='js/modules/interface/main.js') }}" type="module"></script>
<script src="{{ url_for('static', filename='js/modules/crypto/main.js') }}" type="module"></script>
</head>

View File

@ -263,6 +263,15 @@ doi={10.1109/SP.2014.36}}
howpublished = {\url{https://github.com/msgpack/msgpack}},
}
@misc{jssha,
author = {Caligatio},
title = {jsSHA: A JavaScript/TypeScript implementation of the complete Secure Hash Standard (SHA) family},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/Caligatio/jsSHA}},
}
@article{RABIN1980128,
title = {Probabilistic algorithm for testing primality},
journal = {Journal of Number Theory},
@ -320,4 +329,13 @@ doi={10.1109/SP.2014.36}}
year={2000}
}
@misc{projectgemini, url={gemini://gemini.circumlunar.space/docs/specification.gmi}, journal={Project gemini}}
@misc{projectgemini, url={gemini://gemini.circumlunar.space/docs/specification.gmi}, journal={Project gemini}}
@techreport{FIPS202,
author = {National Institute of Standards and Technology},
title = {SHA-3 Standard: Permutation-Based Hash and Extendable-Output Functions},
institution = {U.S. Department of Commerce},
address= {Washington, D.C.},
DOI = {10.6028/NIST.FIPS.202},
year = {2015},
}

Binary file not shown.

View File

@ -406,8 +406,6 @@ The proof system presented is an interactive proof for a given ciphertext $c$ be
A proof for the following homologous problem can be trivially constructed: given some ciphertext $c = g^mr^n \mod n^2$, prove that the text $cg^{-m} \mod n^2$ is an encryption of 0. The text $cg^{-m}$ is constructed by the verifier. The prover then proceeds with the proof as normal, since $cg^{-m}$ is an encryption of 0 under the same noise as the encryption of $m$ given.
% Furthermore, the above protocol can be made non-interactive using the Fiat-Shamir heuristic \citep{fiatshamir}. (this contradicts the lit review)
\subsection{Implementation details}
Proofs of zero use messages labelled as \texttt{"PROOF"} to resolve, and resolve between two parties. The proof is initiated by the verifier as part of the game protocol, who sends a request containing the region to prove. Initiating proofs on the verifier side has benefits to synchronisation, and helps to reduce race conditions, as the proof is only requested after the verifier has updated their state.
@ -445,7 +443,7 @@ Players should prove a number of properties of their game state to each other to
\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.
Using just the additive homomorphic property to guarantee (1) opens up the ability for a player to cheat by using negative values. This is a severe issue, as potentially the cheat could be completely unnoticed even in the conclusion of the game. To overcome this, we need a new protocol that is still in zero-knowledge, but proves a different property of a player's move.
One consideration is to use a range proof as above. The full proof would then be the combination of a proof that the sum of all ciphertexts is 1, and the range of each ciphertext is as tight as possible, which is within the range $[0, 3]$. This is acceptable in the specific application, however we can achieve a better proof that is similar in operation to \cite{Boudot2000EfficientPT}.
@ -462,7 +460,7 @@ Instead of proving a value is within a range, the prover will demonstrate that a
\item Prover transmits $\{ (\psi(R_i), E(n_i, r_i^*)) \mid 0 < i \leq N \}$ where $\psi$ is a random bijection on the regions.
\item Verifier chooses a random $c \in \{0, 1\}$. \begin{enumerate}
\item If $c = 0$, the verifier requests the definition of $\psi$, to indeed see that this is a valid bijection. They then compute the product of the $E(x, r_1) \cdot E(x^*, r_1^*)$ and verify proofs that each of these is zero.
\item If $c = 0$, the verifier requests the definition of $\psi$. They then compute the product of the $E(x, r_i) \cdot E(x, r_i^*)$ and verify proofs that each of these is zero.
\item If $c = 1$, the verifier requests a proof that each $E(n_i, r_i^*)$ is as claimed.
\end{enumerate}
@ -498,13 +496,21 @@ Additionally, we can consider this protocol perfect zero-knowledge.
\subsection{Optimising}
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.
It is preferred that these proofs can be performed with only a few communications: this issue is particularly prevalent here as this protocol requires multiple rounds to complete. The independence of each round on the next is a beneficial property, as it means the proof can be performed in parallel, so the prover transmits \textit{all} of their $\psi$'s, then the verifier transmits all of their challenges. However, still is the issue of performing proofs of zero.
We can apply the Fiat-Shamir heuristic \cite{fiatshamir} to make proofs of zero non-interactive. In place of a random oracle, we use a cryptographic hash function. %todo cite & discuss
We take the hash of some public parameters to prevent cheating by searching for some values that hash in a preferable manner. In this case, selecting $e = H(g, m, a)$ is a valid choice. To get a hash of desired length, an extendable output function such as SHAKE256 \cite{FIPS202} could be used.
The library jsSHA \cite{jssha} provides an implementation of SHAKE256 that works within a browser.
\section{Review}
\subsection{Random oracles}
Various parts of the implementation use the random oracle model: in particular, the zero-knowledge proof sections. The extent to which the random oracle model is used is in the construction of truly random values that will not reveal information about the prover's state. In practice, a cryptographically secure pseudo random number generator will suffice for this application, as CSPRNGs typically incorporate environmental data to ensure outputs are unpredictable \cite{random(4)}.
Various parts of the implementation use the random oracle model: in particular, the zero-knowledge proof sections.
The random oracle model is used for two guarantees. The first is in the construction of truly random values that will not reveal information about the prover's state. In practice, a cryptographically secure pseudo random number generator will suffice for this application, as CSPRNGs typically incorporate environmental data to ensure outputs are unpredictable \cite{random(4)}.
The second is to associate a non-random value with a random value. In practice, a cryptographic hash function such as SHA-3 is used. This gives appropriately pseudo-random outputs that appear truly random, and additionally are assumed to be preimage resistant: a necessary property when constructing non-interactive proofs in order to prevent a prover manipulating the signature used to derive the proof.
\subsection{Efficiency}