diff --git a/static/js/modules/crypto/paillier.js b/static/js/modules/crypto/paillier.js index 1a9376d..04fb92d 100644 --- a/static/js/modules/crypto/paillier.js +++ b/static/js/modules/crypto/paillier.js @@ -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) { diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js index 97052c9..f3d8306 100644 --- a/static/js/modules/interface/main.js +++ b/static/js/modules/interface/main.js @@ -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(); diff --git a/static/js/modules/interface/proofs.js b/static/js/modules/interface/proofs.js new file mode 100644 index 0000000..9868111 --- /dev/null +++ b/static/js/modules/interface/proofs.js @@ -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 +} diff --git a/static/js/sha3.js b/static/js/sha3.js new file mode 100644 index 0000000..8799e0d --- /dev/null +++ b/static/js/sha3.js @@ -0,0 +1,21 @@ +/** + * A JavaScript implementation of the SHA family of hashes - defined in FIPS PUB 180-4, FIPS PUB 202, + * and SP 800-185 - as well as the corresponding HMAC implementation as defined in FIPS PUB 198-1. + * + * Copyright 2008-2022 Brian Turek, 1998-2009 Paul Johnston & Contributors + * Distributed under the BSD License + * See http://caligatio.github.com/jsSHA/ for more information + * + * Two ECMAScript polyfill functions carry the following license: + * + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + * MERCHANTABLITY OR NON-INFRINGEMENT. + * + * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. + */ +!function(r,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(r="undefined"!=typeof globalThis?globalThis:r||self).jsSHA=t()}(this,(function(){"use strict";var r=function(t,n){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(r,t){r.__proto__=t}||function(r,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(r[n]=t[n])},r(t,n)};var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n="ARRAYBUFFER not supported by this environment",e="UINT8ARRAY not supported by this environment";function i(r,t,n,e){var i,o,u,s=t||[0],f=(n=n||0)>>>3,a=-1===e?3:0;for(i=0;i>>2,s.length<=o&&s.push(0),s[o]|=r[i]<<8*(a+e*(u%4));return{value:s,binLen:8*r.length+n}}function o(r,o,u){switch(o){case"UTF8":case"UTF16BE":case"UTF16LE":break;default:throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE")}switch(r){case"HEX":return function(r,t,n){return function(r,t,n,e){var i,o,u,s;if(0!=r.length%2)throw new Error("String of HEX type must be in byte increments");var f=t||[0],a=(n=n||0)>>>3,h=-1===e?3:0;for(i=0;i>>1)+a)>>>2;f.length<=u;)f.push(0);f[u]|=o<<8*(h+e*(s%4))}return{value:f,binLen:4*r.length+n}}(r,t,n,u)};case"TEXT":return function(r,t,n){return function(r,t,n,e,i){var o,u,s,f,a,h,c,w,v=0,l=n||[0],E=(e=e||0)>>>3;if("UTF8"===t)for(c=-1===i?3:0,s=0;s(o=r.charCodeAt(s))?u.push(o):2048>o?(u.push(192|o>>>6),u.push(128|63&o)):55296>o||57344<=o?u.push(224|o>>>12,128|o>>>6&63,128|63&o):(s+=1,o=65536+((1023&o)<<10|1023&r.charCodeAt(s)),u.push(240|o>>>18,128|o>>>12&63,128|o>>>6&63,128|63&o)),f=0;f>>2;l.length<=a;)l.push(0);l[a]|=u[f]<<8*(c+i*(h%4)),v+=1}else for(c=-1===i?2:0,w="UTF16LE"===t&&1!==i||"UTF16LE"!==t&&1===i,s=0;s>>8),a=(h=v+E)>>>2;l.length<=a;)l.push(0);l[a]|=o<<8*(c+i*(h%4)),v+=2}return{value:l,binLen:8*v+e}}(r,o,t,n,u)};case"B64":return function(r,n,e){return function(r,n,e,i){var o,u,s,f,a,h,c=0,w=n||[0],v=(e=e||0)>>>3,l=-1===i?3:0,E=r.indexOf("=");if(-1===r.search(/^[a-zA-Z0-9=+/]+$/))throw new Error("Invalid character in base-64 string");if(r=r.replace(/=/g,""),-1!==E&&E diff --git a/whitepaper/Dissertation.bib b/whitepaper/Dissertation.bib index 545cb32..5ccb5dd 100644 --- a/whitepaper/Dissertation.bib +++ b/whitepaper/Dissertation.bib @@ -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}} \ No newline at end of file +@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}, +} \ No newline at end of file diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf index 96d8e19..e0bb78c 100644 Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex index 3b3c271..4c413d9 100644 --- a/whitepaper/Dissertation.tex +++ b/whitepaper/Dissertation.tex @@ -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}