Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate TODOs in decrypt classes. #63

Merged
merged 2 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[![License](https://img.shields.io/github/license/JohnLCaron/egk-ec)](https://github.com/JohnLCaron/egk-ec/blob/main/LICENSE.txt)
![GitHub branch checks state](https://img.shields.io/github/actions/workflow/status/JohnLCaron/egk-ec/unit-tests.yml)
![Coverage](https://img.shields.io/badge/coverage-90.5%25%20LOC%20(7001/7733)-blue)
![Coverage](https://img.shields.io/badge/coverage-90.6%25%20LOC%20(6958/7679)-blue)

# ElectionGuard-Kotlin Elliptic Curve

_last update 04/13/2024_
_last update 04/17/2024_

EGK Elliptic Curve (egk-ec) is an experimental implementation of [ElectionGuard](https://github.com/microsoft/electionguard),
[version 2.0](https://github.com/microsoft/electionguard/releases/download/v2.0/EG_Spec_2_0.pdf),
Expand Down
26 changes: 14 additions & 12 deletions docs/CommandLineInterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,17 @@ last update 04/16/2024
2. Use existing fake ballots for testing in _src/test/data/fakeBallots_.

5. **Encryption**.
1. The [_RunAddEncryptedBallots_ CLI](#run-addencryptedballots) reads plaintext ballots from a directory and
writes their encryptions into the specified election record.
1. The [_RunEncryptBallot_ CLI](#run-encrypt-ballot) reads a plaintext ballot from disk and writes its encryption to disk.
1. The [_RunExampleEncryption_ CLI](#run-example-encryption) Is an example of running RunEncryptBallot to encrypt ballots.
This can simulate more complex election records with multiple voting devices.
1. The [_RunBatchEncryption_ CLI](#run-batch-encryption) reads plaintext ballots from a directory and writes their encryptions to the
specified election record. It is multithreaded.
1. _org.cryptobiotic.eg.encrypt.AddEncryptedBallot_ is a class that your program calls to encrypt plaintext ballots
and add them to the election record. (See _org.cryptobiotic.eg.cli.ExampleEncryption_ as an example of using AddEncryptedBallot).
1. To run encryption with the Encryption server, see the webapps CLI. This allows you to run the encryption on a
different machine than where ballots are generated, and/or to call from a non-JVM program.
1. The [_RunAddEncryptedBallots_ CLI](#run-addencryptedballots) reads plaintext ballots from a directory and
writes their encryptions into the specified election record.
1. The [_RunEncryptBallot_ CLI](#run-encrypt-ballot) reads a plaintext ballot from disk and writes its encryption to disk.
1. The [_RunExampleEncryption_ CLI](#run-example-encryption) Is an example of running RunEncryptBallot to encrypt ballots.
This can simulate more complex election records with multiple voting devices.
1. The [_RunBatchEncryption_ CLI](#run-batch-encryption) reads plaintext ballots from a directory and writes their encryptions to the
specified election record. It is multithreaded.
1. _org.cryptobiotic.eg.encrypt.AddEncryptedBallot_ is a class that your program calls to encrypt plaintext ballots
and add them to the election record. (See _org.cryptobiotic.eg.cli.ExampleEncryption_ as an example of using AddEncryptedBallot).
1. To run encryption with the Encryption server, see the webapps CLI. This allows you to run the encryption on a
different machine than where ballots are generated, and/or to call from a non-JVM program.

6. **Accumulate Tally**.
1. [_RunAccumulateTally_ CLI](#run-accumulate_tally) reads an ElectionInitialized record and EncryptedBallot
Expand All @@ -85,7 +85,9 @@ last update 04/16/2024

## Make ekglib uberJar

For classpath simplicity, the examples below use the [ekglib uberJar](https://github.com/JohnLCaron/egk-ec/blob/main/docs/GettingStarted.md#building-a-library-with-all-dependencies-uber-jar).
For classpath simplicity, the examples below use the
[ekglib uberJar](https://github.com/JohnLCaron/egk-ec/blob/main/docs/GettingStarted.md#building-a-library-with-all-dependencies-uber-jar).
https://github.com/JohnLCaron/egk-ec/blob/main/docs/GettingStarted.md#building-a-library-with-all-dependencies-uber-jar

## Election setup

Expand Down
2 changes: 1 addition & 1 deletion docs/JsonSerializationSpec1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ data class HashedElGamalCiphertextJson(
val c0: ElementModPJson, // ElementModP,
val c1: String, // ByteArray,
val c2: UInt256Json, // UInt256,
val numBytes: Int // TODO needed?
val numBytes: Int
)

@Serializable
Expand Down
4 changes: 3 additions & 1 deletion docs/JsonSerializationSpec2.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ data class HashedElGamalCiphertextJson(
val c0: ElementModPJson, // ElementModP,
val c1: String, // ByteArray,
val c2: UInt256Json, // UInt256,
val numBytes: Int // TODO needed?
val numBytes: Int
)

@Serializable
Expand Down Expand Up @@ -701,6 +701,8 @@ data class ContestDataJson(
val status: String,
)

TODO ContestDataJson is not in any spec.

````

Example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ class RunTrustedBallotDecryption {
decryptingTrustees,
)

// TODO you may want to put the decryption results in the same directory, but sinks now are append-only.
val publisher = makePublisher(outputDir, false)
val sink: DecryptedBallotSinkIF = publisher.decryptedBallotSink()

Expand Down
41 changes: 6 additions & 35 deletions src/main/kotlin/org/cryptobiotic/eg/decrypt/BallotDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,8 @@ class BallotDecryptor(
val guardians: Guardians, // all guardians
decryptingTrustees: List<DecryptingTrusteeIF>, // the trustees available to decrypt
) {
val lagrangeCoordinates: Map<String, LagrangeCoordinate>
val stats = Stats()
val nguardians = guardians.guardians.size // number of guardinas
val quorum = guardians.guardians[0].coefficientCommitments().size
val decryptor = CipherDecryptor(group, extendedBaseHash, publicKey, guardians, decryptingTrustees)

init {
// check that the DecryptingTrustee's match their public key
val badTrustees = mutableListOf<String>()
for (trustee in decryptingTrustees) {
val guardian = guardians.guardianMap[trustee.id()]
if (guardian == null) {
badTrustees.add(trustee.id())
} else {
if (trustee.guardianPublicKey().key != guardian.publicKey()) {
badTrustees.add(trustee.id())
logger.error { "trustee public key = ${trustee.guardianPublicKey()} not equal guardian = ${guardian.publicKey()}" }
}
}
}
if (badTrustees.isNotEmpty()) {
throw RuntimeException("DecryptingTrustee(s) ${badTrustees.joinToString(",")} do not match the public record")
}

// build the lagrangeCoordinates once and for all
val dguardians = mutableListOf<LagrangeCoordinate>()
for (trustee in decryptingTrustees) {
val present: List<Int> = // available trustees minus me
decryptingTrustees.filter { it.id() != trustee.id() }.map { it.xCoordinate() }
val coeff: ElementModQ = group.computeLagrangeCoefficient(trustee.xCoordinate(), present)
dguardians.add(LagrangeCoordinate(trustee.id(), trustee.xCoordinate(), coeff))
}
this.lagrangeCoordinates = dguardians.associateBy { it.guardianId }
}
val stats = Stats()

fun decrypt(eballot: EncryptedBallotIF, errs : ErrorMessages): DecryptedTallyOrBallot? {
if (eballot.electionId != extendedBaseHash) {
Expand Down Expand Up @@ -87,7 +55,7 @@ class BallotDecryptor(
val result = makeBallot(eballot, decryptionAndProofs, contestDecryptionAndProofs, errs.nested("BallotDecryptor.decrypt"))
if (!errs.hasErrors()) {
val ndecrypt = decryptionAndProofs.size + contestDecryptionAndProofs.size
stats.of("decryptTally").accum(stopwatch.stop(), ndecrypt)
stats.of("decryptBallot").accum(stopwatch.stop(), ndecrypt)
}
return if (errs.hasErrors()) null else result!!
}
Expand All @@ -103,10 +71,13 @@ class BallotDecryptor(
val selections = econtest.selections.map { eselection ->
val (decryption, proof) = decryptions[selectionCount++]
val (T, tally) = decryption.decryptCiphertext(publicKey)
if (tally == null) {
errs.add("Cant decrypt tally for ${econtest.contestId}.${eselection.selectionId}")
}

DecryptedTallyOrBallot.Selection(
eselection.selectionId,
tally?: 0, // TODO error handling
tally?: 0,
T,
(decryption.cipher as Ciphertext).delegate,
proof
Expand Down
102 changes: 15 additions & 87 deletions src/main/kotlin/org/cryptobiotic/eg/decrypt/CipherDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,80 +6,39 @@ import org.cryptobiotic.eg.core.*
import org.cryptobiotic.eg.core.Base16.toHex
import org.cryptobiotic.eg.election.*
import org.cryptobiotic.util.ErrorMessages
import org.cryptobiotic.util.Stats

/** Orchestrates the decryption of List<ElGamalCiphertext> or List<HashedElGamalCiphertext> using DecryptingTrustees. */
class CipherDecryptor(
val group: GroupContext,
val extendedBaseHash: UInt256,
val publicKey: ElGamalPublicKey,
val guardians: Guardians, // all guardians
guardians: Guardians, // all guardians
private val decryptingTrustees: List<DecryptingTrusteeIF>, // the trustees available to decrypt
) {
val lagrangeCoordinates: Map<String, LagrangeCoordinate>
val stats = Stats()
val nguardians = guardians.guardians.size // number of guardinas
val quorum = guardians.guardians[0].coefficientCommitments().size

init {
// TODO put these in the guardians
// check that the DecryptingTrustee's match their public key
val badTrustees = mutableListOf<String>()
for (trustee in decryptingTrustees) {
val guardian = guardians.guardianMap[trustee.id()]
if (guardian == null) {
badTrustees.add(trustee.id())
} else {
if (trustee.guardianPublicKey().key != guardian.publicKey()) {
badTrustees.add(trustee.id())
logger.error { "trustee public key = ${trustee.guardianPublicKey()} not equal guardian = ${guardian.publicKey()}" }
}
}
}
if (badTrustees.isNotEmpty()) {
throw RuntimeException("DecryptingTrustee(s) ${badTrustees.joinToString(",")} do not match the public record")
}

// build the lagrangeCoordinates once and for all
val dguardians = mutableListOf<LagrangeCoordinate>()
for (trustee in decryptingTrustees) {
val present: List<Int> = // available trustees minus me
decryptingTrustees.filter { it.id() != trustee.id() }.map { it.xCoordinate() }
val coeff: ElementModQ = group.computeLagrangeCoefficient(trustee.xCoordinate(), present)
dguardians.add(LagrangeCoordinate(trustee.id(), trustee.xCoordinate(), coeff))
}
this.lagrangeCoordinates = dguardians.associateBy { it.guardianId }
}
val lagrangeCoordinates: Map<String, LagrangeCoordinate> = guardians.buildLagrangeCoordinates(decryptingTrustees)

fun decrypt(texts: List<Cipher>, errs : ErrorMessages): List<CipherDecryptionAndProof>? {
if (texts.isEmpty()) return emptyList()

// get the PartialDecryptions from each of the trustees
val partialDecryptions = decryptingTrustees.map { // partialDecryptions are in the order of the decryptingTrustees
it.getPartialDecryptionsFromTrustee(texts, errs)
val partialDecryptions = decryptingTrustees.map { trustee -> // partialDecryptions are in order of the decryptingTrustees
trustee.getPartialDecryptionsFromTrustee(texts, errs)
}
if (errs.hasErrors()) {
logger.error { "partial decryptions failed = ${errs}" }
logger.error { "partial decryptions failed = $errs" }
return null
}

// Do the decryption for each text
val decryptions = texts.mapIndexed { idx, text ->

// TODO could use the shares
// lagrange weighted product of the shares, M = Prod(M_i^w_i) mod p; spec 2.0.0, eq 68
val weightedProduct = with(group) {
val weightedProduct = with (group) {
// for this idx, run over all the trustees
partialDecryptions.mapIndexed { tidx, pds ->
val trustee = decryptingTrustees[tidx]
val lagrange = lagrangeCoordinates[trustee.id()]
val coeff = if (lagrange == null) { // TODO check to make sure this cant happen
errs.add("missing lagrangeCoordinate for ${trustee.id()}")
group.ONE_MOD_Q
} else {
lagrange.lagrangeCoefficient
}
pds.partial[idx].Mi powP coeff
val lagrange = lagrangeCoordinates[trustee.id()]!! // buildLagrangeCoordinates() guarentees exists
pds.partial[idx].Mi powP lagrange.lagrangeCoefficient
}.multP()
}

Expand All @@ -93,7 +52,7 @@ class CipherDecryptor(
CipherDecryption(text, weightedProduct, collectiveChallenge)
}
if (errs.hasErrors()) {
logger.error { "decrypt failed = ${errs}" }
logger.error { "decrypt failed = $errs" }
return null
}

Expand All @@ -103,12 +62,12 @@ class CipherDecryptor(
trustee.getResponsesFromTrustee(batchId, decryptions, errs.nested("trusteeChallengeResponses"))
}
if (errs.hasErrors()) {
logger.error { "decrypt failed = ${errs}" }
logger.error { "decrypt failed = $errs" }
return null
}

// After gathering the challenge responses from the available trustees, we can create the proof
return makeHDecryptionAndProofs( decryptions, challengeResponses, errs)
return makeHDecryptionAndProofs( decryptions, challengeResponses)
}

private fun DecryptingTrusteeIF.getPartialDecryptionsFromTrustee(texts: List<Cipher>, errs : ErrorMessages) : PartialDecryptions {
Expand All @@ -122,9 +81,9 @@ class CipherDecryptor(
return pds
}

// send all challenges for a ballot / tally to one trustee, get all its responses
// send all challenges for a ballot / tally to one trustee, get all its responses, in one call
fun DecryptingTrusteeIF.getResponsesFromTrustee(batchId: Int, decryptions: List<CipherDecryption>, errs : ErrorMessages) : ChallengeResponses {
val wi = lagrangeCoordinates[this.id()]!!.lagrangeCoefficient // TODO ensure cant fail
val wi = lagrangeCoordinates[this.id()]!!.lagrangeCoefficient // buildLagrangeCoordinates() guarentees exists
// Create all the challenges from each Decryption for this trustee
val requests: MutableList<ElementModQ> = mutableListOf()
decryptions.forEach { decryption ->
Expand All @@ -142,14 +101,13 @@ class CipherDecryptor(
fun makeHDecryptionAndProofs(
decryptions: List<CipherDecryption>, // for each text
challengeResponses: List<ChallengeResponses>, // for each trustee, list of responses for each text
errs : ErrorMessages, // TODO errors
): List<CipherDecryptionAndProof> {
val ds = mutableListOf<CipherDecryptionAndProof>()
decryptions.forEachIndexed { idx, decryption ->
val responsesForIdx = challengeResponses.map { it.responses[idx] }
ds.add( makeHDecryptionAndProof( decryption, responsesForIdx) )
}
return ds // for each text
return ds // one for each text
}

private fun makeHDecryptionAndProof(
Expand Down Expand Up @@ -205,6 +163,7 @@ class CipherDecryption(
// the return value of the decryption
data class CipherDecryptionAndProof(val decryption: CipherDecryption, val proof: ChaumPedersenProof)

// abstraction so we can work with either ElGamalCiphertext or HashedElGamalCiphertext
interface Cipher {
fun pad() : ElementModP
fun collectiveChallenge(extendedBaseHash:UInt256, publicKey: ElGamalPublicKey, a: ElementModP, b: ElementModP, beta: ElementModP): UInt256
Expand Down Expand Up @@ -234,35 +193,4 @@ data class HashedCiphertext(val delegate: HashedElGamalCiphertext): Cipher {
delegate.c1.toHex(),
delegate.c2,
a, b, beta)
}

//////////////////////////////////////////////////////////////////////////////

data class LagrangeCoordinate(
var guardianId: String,
var xCoordinate: Int,
var lagrangeCoefficient: ElementModQ, // wℓ, spec 2.0.0 eq 67
) {
init {
require(guardianId.isNotEmpty())
require(xCoordinate > 0)
}
}

/** Compute the lagrange coefficient, now that we know which guardians are present; 2.0, section 3.6.2, eq 67. */
fun GroupContext.computeLagrangeCoefficient(coordinate: Int, present: List<Int>): ElementModQ {
val others: List<Int> = present.filter { it != coordinate }
if (others.isEmpty()) {
return this.ONE_MOD_Q
}
val numerator: Int = others.reduce { a, b -> a * b }

val diff: List<Int> = others.map { degree -> degree - coordinate }
val denominator = diff.reduce { a, b -> a * b }

val denomQ =
if (denominator > 0) denominator.toElementModQ(this) else (-denominator).toElementModQ(this)
.unaryMinus()

return numerator.toElementModQ(this) / denomQ
}
Loading
Loading