diff --git a/.gitignore b/.gitignore index c51d341..82115f5 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,12 @@ project/plugins/project/ # adapted from https://www.gitignore.io/api/playframework,sbt,eclipse,intellij,scala,osx + +.gradle/ + + +# temp files + +*.bak +*.backup +*.swp diff --git a/README.md b/README.md index c0b36e0..851a543 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,33 @@ -Pollster service +# Pollster service +Restful api sopporting privacy preserving verifiable +elections -# build +# How to Build and run + +Build with the following: sbt package +Run assembled jar file using java -jar + +# Polls and Ballots +The polling mechanism uses el-gamal encryption along with non interactive +zero knowledge proofs in ballots to establish the following properties: + +1. Ballots have encrypted ballots and have voter identity in the clear Everyone can see +and verify these ballots. The ballots in their clear form would indicate a yay or a nay vote +but the vote can not be deciphered from the encrypted ballot. + +2. At end of the poll (no more votes can be cast) the service produces a total transcript of the +poll at which time, the following can be verified by anyone + a. The encrypted total is correct using modular arithmetic + b. The decryped value is the total number of positive votes + +More details in the white paper (TBP). The mechanism is quite similar to that in the following +: https://www.win.tue.nl/%7Eberry/papers/euro97.pdf . Zero knowledge additions for 2b are similar +to the question asked here: https://crypto.stackexchange.com/questions/9997/perfect-zero-knowledge-for-the-schnorr-protocol + +### Ballot sketch +![](./docs/ballot-spec.png "Ballot") diff --git a/docs/ballot-spec.png b/docs/ballot-spec.png new file mode 100644 index 0000000..dc9605b Binary files /dev/null and b/docs/ballot-spec.png differ diff --git a/src/main/scala/com/newlibertie/pollster/impl/Ballot.scala b/src/main/scala/com/newlibertie/pollster/impl/Ballot.scala new file mode 100644 index 0000000..c34417b --- /dev/null +++ b/src/main/scala/com/newlibertie/pollster/impl/Ballot.scala @@ -0,0 +1,279 @@ +package com.newlibertie.pollster.impl + +import java.math.BigInteger + +import scala.collection.mutable.ListBuffer + +/** + * Implementation of ballot + * + * A ballot object is empty but permanently associated with a specific poll and + * a specific voter upon construction + * + * The two ways to make it a valid ballot are as follows: + * 1. Using the cast(vote:Boolean) method to populate its internal fields, or + * 2. Instantiate it from pre-computed fields. In this scenario, the implementation + * will verify the ballot as can anyway be done using the verify() method. + * + * @param cp cryptographic parameters of the poll this ballot is about + * @param voter String identifier identifying the voter + */ +class Ballot(cp: CryptographicParameters, voter: String) { + + final val isdbg = false + var x, y: BigInteger = _ + + var a1, b1, a2, b2: BigInteger = _ + + var d1, d2, r1, r2: BigInteger = _ + var c_val: BigInteger = _ + + /** + * Cast a vote - populates all parameters to become a verifiable ballot + * + * @param vote boolean vote, true = yay, false = nay + */ + // TODO: Can be made to have only-once semantics (in that case there may + // TODO: also save the metadata - where this ballot was casted, what UTC ts, + // TODO: etc - that can serve other purposes when aggregated at scale) + + def cast(vote: Boolean): Unit = { + // Voter votes m^0 or m^1 + // Ballot is a tuple (x,y,a1,b1,a2,b2) + // x g^alpha, + // y h^alpha G OR h^alpha / G + // a1 g^r1 x^{d1} OR g ^ omega + // b1 h^r1 (yG)^{d1} OR h ^ omega + // a2 g ^ omega OR g ^ {r2} x ^ {d2} + // b2 h ^ omega OR h ^ {r2} (y/G)^{d2} + + val alpha = if (isdbg) + new BigInteger("11774") + else + CryptographicParameters.random(CryptographicParameters.BITS).mod(cp.large_prime_p) +// this.c_val = if (isdbg) +// new BigInteger("257600") +// else +// CryptographicParameters.random(CryptographicParameters.BITS).mod(cp.large_prime_p) + + // TODO : insecure, delete please + println(s"voter\t${this.voter}") + println(s"alpha\t$alpha") + + this.x = cp.generator_g.modPow(alpha, cp.large_prime_p) + if (vote) { // is positive vote + this.y = cp.public_key_h.modPow(alpha, cp.large_prime_p).multiply( + cp.zkp_generator_G).mod(cp.large_prime_p) + this.r1 = if (isdbg) + new BigInteger("123456") + else + CryptographicParameters.random(CryptographicParameters.BITS).mod(cp.large_prime_p) + this.c_val = getC(this.r1) + val omega = if (isdbg) + new BigInteger("2281424433") + else + this.c_val.multiply(alpha) //.mod(cp.large_prime_p) + println(s"omega\t$omega") + this.a2 = cp.generator_g.modPow(omega, cp.large_prime_p) + this.b2 = cp.public_key_h.modPow(omega, cp.large_prime_p) + this.d1 = if (isdbg) + new BigInteger("63832") + else + CryptographicParameters.random(CryptographicParameters.BITS).mod(this.c_val) // TODO : adjust and check if "we will use SHA-512 for zkp" can work with c = d1 + d2 + this.a1 = cp.generator_g.modPow(r1, cp.large_prime_p) + .multiply(x.modPow(d1, cp.large_prime_p)) + .mod(cp.large_prime_p) + this.b1 = cp.public_key_h.modPow(r1, cp.large_prime_p) + .multiply(y.multiply(cp.zkp_generator_G).modPow(d1, cp.large_prime_p)) + .mod(cp.large_prime_p) + this.d2 = this.c_val.subtract(this.d1) + this.r2 = omega.subtract(alpha.multiply(this.d2).mod(omega)) + } + else { // vote is negative + this.y = cp.public_key_h.modPow(alpha, cp.large_prime_p).multiply( + cp.zkp_generator_G.modInverse(cp.large_prime_p) + ).mod(cp.large_prime_p) + this.r2 = if (isdbg) + new BigInteger("123456") + else + CryptographicParameters.random(CryptographicParameters.BITS).mod(cp.large_prime_p) + this.c_val = getC(r2) + val omega = if (isdbg) + new BigInteger("2281424433") + else + this.c_val.multiply(alpha) //.mod(cp.large_prime_p) + println(s"omega\t$omega") + this.a1 = cp.generator_g.modPow(omega, cp.large_prime_p) + this.b1 = cp.public_key_h.modPow(omega, cp.large_prime_p) + this.d2 = if (isdbg) + new BigInteger("193768") + else + CryptographicParameters.random(CryptographicParameters.BITS).mod(this.c_val) // TODO : adjust and check if "we will use SHA-512 for zkp" can work with c = d1 + d2 + this.a2 = cp.generator_g.modPow(r2, cp.large_prime_p) + .multiply(x.modPow(d2, cp.large_prime_p)) + .mod(cp.large_prime_p) + + val yByG = cp.zkp_generator_G.modInverse(cp.large_prime_p).multiply(y).mod(cp.large_prime_p) + val hr2 = cp.public_key_h.modPow(r2, cp.large_prime_p) + val yByGpowd2 = yByG.modPow(d2, cp.large_prime_p) + val hr2yByGpowd2 = hr2.multiply(yByGpowd2).mod(cp.large_prime_p) + this.b2 = hr2yByGpowd2 + this.d1 = this.c_val.subtract(this.d2) + this.r1 = omega.subtract(alpha.multiply(this.d1).mod(omega)) + } + println(s"large_prime_p\t${this.cp.large_prime_p}") + println(s"generator_g\t${this.cp.generator_g}") + println(s"private_key_s\t${this.cp.private_key_s}") + println(s"public_key_h\t${this.cp.public_key_h}") + println(s"zkp_generator_G\t${this.cp.zkp_generator_G}") + println(s"a1\t${this.a1}") + println(s"a2\t${this.a2}") + println(s"b1\t${this.b1}") + println(s"b2\t${this.b2}") + println(s"c_val\t${this.c_val}") + println(s"d1\t${this.d1}") + println(s"d2\t${this.d2}") + println(s"r1\t${this.r1}") + println(s"r2\t${this.r2}") + println(s"x\t${this.x}") + println(s"y\t${this.y}") + } + + private def getC(r: BigInteger) = { + // take hash of . the hash of this content is used to calculate the + // parameters d1 and d2 - which are used to produce the zero knowledge proof that the encrypted + // ballot is a valid ballot for the given poll + // + // this.voter is the name of the voter - eg: https://www.facebook.com/vpathak000 + // the remaining parameters can be described as follows: + // h poll public key - which is also the identifier for the polls + // x some random numbers, refer to the white paper + // + val s = // the "formula" for the content hash is same as in the while paper : H(v_i||T) + s"""${this.voter} + |h=${this.cp.public_key_h} + |h=${this.cp.zkp_generator_G} + |x=${this.x} + |y=${this.y} + |a2=$r + |""".stripMargin + val shaBin = java.security.MessageDigest.getInstance("SHA-512").digest(s.getBytes("utf-8")) + println(s"string to hash $s -> ${new BigInteger(1, shaBin).toString}") + new BigInteger(1, shaBin).mod(cp.large_prime_p) + } + + /** + * ZKP Verify integrity of the ballot + */ + def verify(_outBuffer: ListBuffer[String] = null): Boolean = { + // TODO : fix design-ish issue. why we would allowed + val outBuffer = if (_outBuffer == null) + new ListBuffer[String]() + else + _outBuffer + outBuffer += + s"""C=${this.c_val} + |d1=${this.d1} + |d2=${this.d2} + |C = d1 + d2 ?\n + |""".stripMargin + + try { + val c = this.c_val + val shouldBec = this.d1.add(this.d2) + if (!c.equals(shouldBec)) { + println(s"shouldBec\t$shouldBec") + return false + } + + outBuffer += + s"""a1=${this.a1} + |g=${cp.generator_g} + |r1=${this.r1} + |x=${this.x} + |d1=${this.d1} + |a1 = g^r1 . x ^ d1 ?\n + |""".stripMargin + val gpowr1 = cp.generator_g.modPow(r1, cp.large_prime_p) + val xpowd1 = x.modPow(d1, cp.large_prime_p) + val prod = gpowr1.multiply(xpowd1).mod(cp.large_prime_p) + println(s"gpowr1\t$gpowr1") + println(s"xpowd1\t$xpowd1") + println(s"prod\t$prod") + if (!prod.equals(a1)) { + println(s"a1 != prod") + return false + } + // if (!cp.generator_g.modPow(r1, cp.large_prime_p).multiply(x.modPow(d1, cp.large_prime_p)).mod(cp.large_prime_p).equals(a1)) { + // println(s"a1 != $a1") + // return false + // } + + outBuffer += + s"""b1=${this.b1} + |h=${cp.public_key_h} + |r1=${this.r1} + |y=${this.y} + |G=${cp.zkp_generator_G} + |d1=${this.d1} + |b1 = h^r1 (yG)^d1 ?\n + |""".stripMargin + val yG = y.multiply(cp.zkp_generator_G) + if (!cp.public_key_h.modPow(r1, cp.large_prime_p).multiply( + yG.modPow(d1, cp.large_prime_p)).mod(cp.large_prime_p).equals(b1)) { + println(s"b1 !=\t$b1") + return false + } + + outBuffer += + s"""a2=${this.a2} + |g=${cp.generator_g} + |r2=${this.r2} + |x=${this.x} + |d2=${this.d2} + |a2 = g^r2 x^d2 ?\n + |""".stripMargin + val gr2 = cp.generator_g.modPow(r2, cp.large_prime_p) + val xd2 = x.modPow(d2, cp.large_prime_p) + val shouldBea2 = gr2.multiply(xd2).mod(cp.large_prime_p) + println(s"gr2\t$gr2") + println(s"xd2\t$xd2") + println(s"shouldBea2\t$shouldBea2") + if (!shouldBea2.equals(a2)) { + println(s"shouldBea2 != a2") + return false // TODO : debug using algebra proof in voting protocol paper + } + + outBuffer += + s"""b2=${this.b2} + |h=${cp.public_key_h} + |r2=${this.r2} + |y=${this.y} + |G=${cp.zkp_generator_G} + |d2=${this.d2} + |b2 = h^r2 (y/G)^d2 ?\n + |""".stripMargin + + val yByG = cp.zkp_generator_G.modInverse(cp.large_prime_p).multiply(y).mod(cp.large_prime_p) + val hr2 = cp.public_key_h.modPow(r2, cp.large_prime_p) + val yByGpowd2 = yByG.modPow(d2, cp.large_prime_p) + val hr2yByGpowd2 = hr2.multiply(yByGpowd2).mod(cp.large_prime_p) + println(s"yByG\t$yByG") + println(s"hr2\t$hr2") + println(s"yByGpowd2\t$yByGpowd2") + println(s"hr2yByGpowd2\t$hr2yByGpowd2") + if (!hr2yByGpowd2.equals(b2)) { + println(s"hr2yByGpowd2 != b2") + return false + } + //println(outBuffer.toString()) + //println("done") + true + } + catch { + case ex: Throwable => + println(s"Failed to verify because of exception ${ex.toString}") + false + } + } +} diff --git a/src/main/scala/com/newlibertie/pollster/impl/CryptographicParameters.scala b/src/main/scala/com/newlibertie/pollster/impl/CryptographicParameters.scala index cf42da9..9fa06a8 100644 --- a/src/main/scala/com/newlibertie/pollster/impl/CryptographicParameters.scala +++ b/src/main/scala/com/newlibertie/pollster/impl/CryptographicParameters.scala @@ -2,14 +2,17 @@ package com.newlibertie.pollster.impl import java.math.BigInteger +import com.newlibertie.pollster.impl.CryptographicParameters.{BITS, isdbg, rng} + object CryptographicParameters { - val BITS = 1000 + final val isdbg = false + val BITS = 32 // 1000 // TODO : check for various values and then choose a production secure value import java.security.SecureRandom; private val rng = new SecureRandom() def probablePrime() = BigInteger.probablePrime(BITS, rng) - def random() = new BigInteger(BITS, rng) + def random(bits:Int = BITS) = new BigInteger(bits, rng) def apply(p:BigInteger, g:BigInteger, s:BigInteger) = new CryptographicParameters(p, g, s) } @@ -19,10 +22,14 @@ case class CryptographicParameters ( large_prime_p:BigInteger = CryptographicParameters.probablePrime(), generator_g:BigInteger = CryptographicParameters.random(), - private_key_s:BigInteger = CryptographicParameters.random() + private_key_s:BigInteger = CryptographicParameters.random(), ) { - val public_key_h = generator_g.modPow(private_key_s, large_prime_p) + val zkp_generator_G:BigInteger = CryptographicParameters.random().mod(large_prime_p) + val public_key_h = if (isdbg) + new BigInteger("1048806755") + else + generator_g.modPow(private_key_s, large_prime_p) } diff --git a/src/main/scala/com/newlibertie/pollster/impl/Poll.scala b/src/main/scala/com/newlibertie/pollster/impl/Poll.scala index 58ae50a..7b4e253 100644 --- a/src/main/scala/com/newlibertie/pollster/impl/Poll.scala +++ b/src/main/scala/com/newlibertie/pollster/impl/Poll.scala @@ -10,7 +10,7 @@ object Poll { def apply(params: PollParameters): Poll = new Poll(params) def apply(pollDetails:String): Poll = { - implicit val formats = DefaultFormats + implicit val formats: DefaultFormats.type = DefaultFormats val jValue = parse(pollDetails) val pollParameters = jValue.extract[PollParameters] val pollJsonMap = jValue.values.asInstanceOf[Map[String, String]] @@ -29,11 +29,11 @@ object Poll { new Poll(pollParameters, cryptographicParameters) } - def write(poll: Poll) = { + def write(poll: Poll): Any = { DataAdapter.createPoll(poll) } - def read() = { + def read(): Unit = { } } diff --git a/src/test/scala/com/newlibertie/pollster/impl/BallotSpec.scala b/src/test/scala/com/newlibertie/pollster/impl/BallotSpec.scala new file mode 100644 index 0000000..ed5329c --- /dev/null +++ b/src/test/scala/com/newlibertie/pollster/impl/BallotSpec.scala @@ -0,0 +1,67 @@ +package com.newlibertie.pollster.impl + +import java.math.BigInteger + +import org.scalatest.{FlatSpec, Matchers} + +import scala.collection.mutable.ListBuffer + +class BallotSpec extends FlatSpec with Matchers { + final val isdbg = false + val big_p: BigInteger = CryptographicParameters.probablePrime() + val p: Poll = Poll( + Predef.augmentString( + x = if (isdbg) + s""" + |{ + | "id":"abacadabra", + | "title":"abacadabra", + | "tags":["abacadabra", "abacadabra2"], + | "creator_id":"abacadabra", + | "opening_ts": "2019-07-01T02:51:00Z" , + | "closing_ts": "2019-07-01T02:51:00Z" , + | "creation_ts": "2019-07-01T02:51:00Z" , + | "last_modification_ts": "2019-07-01T02:51:00Z" , + | "poll_type":"abacadabra", + | "poll_spec":"abacadabra", + | "p" : "3032992489", + | "g" : "1199476689", + | "s" : "315998446" + |} + """ + else{ + s""" + |{ + | "id":"abacadabra", + | "title":"abacadabra", + | "tags":["abacadabra", "abacadabra2"], + | "creator_id":"abacadabra", + | "opening_ts": "2019-07-01T02:51:00Z" , + | "closing_ts": "2019-07-01T02:51:00Z" , + | "creation_ts": "2019-07-01T02:51:00Z" , + | "last_modification_ts": "2019-07-01T02:51:00Z" , + | "poll_type":"abacadabra", + | "poll_spec":"abacadabra", + | "p" : "$big_p", + | "g" : "${CryptographicParameters.random().mod(big_p)}", + | "s" : "${CryptographicParameters.random().mod(big_p)}" + |} + """ + }).stripMargin) + + "Ballot" should "pass verification for positive vote" in { + val b = new Ballot(p.cp, "test-voter positive") + b.cast(true) + val transcript = ListBuffer[String]() + b.verify(transcript) shouldBe true + transcript.foreach(line => println(line)) + } + + "Ballot" should "pass verification for negative vote" in { + val b = new Ballot(p.cp, "test-voter negative") + b.cast(false) + val transcript = ListBuffer[String]() + b.verify(transcript) shouldBe true + transcript.foreach(line => println(line)) + } +}