From 45d749453cb9f536da6c1669ef6c0deecd3ab5b3 Mon Sep 17 00:00:00 2001 From: JohnLCaron Date: Wed, 8 May 2024 12:59:55 -0600 Subject: [PATCH] Separate hashToElementModQ() from binaryToElementModQ() Fix issues with conflating UInt256 and ElementModQ. KeyCeremonyTrustee pilbytes are the bytes for an ElementModQ, not UInt256. PreSelectionVector.selectionHash is a UInt256. PreEncryptedSelection.selectionHash is a UInt256. RecordedSelectionVector.selectionHash is a UInt256. Add UInt256.compareTo(). Add TestRfc9380. --- README.md | 4 +- .../org/cryptobiotic/eg/core/GroupContext.kt | 3 + .../org/cryptobiotic/eg/core/UInt256.kt | 20 +- .../eg/core/ecgroup/EcElementModQ.kt | 1 - .../eg/core/ecgroup/EcGroupContext.kt | 13 +- .../cryptobiotic/eg/core/ecgroup/RFC9380.kt | 187 +++++++++++------- .../eg/core/ecgroup/VecElementPnative.kt | 2 +- .../cryptobiotic/eg/core/ecgroup/VecGroup.kt | 14 +- .../eg/keyceremony/KeyCeremonyTrustee.kt | 4 +- .../cryptobiotic/eg/preencrypt/PreBallot.kt | 4 +- .../eg/preencrypt/PreEncryptedBallot.kt | 2 +- .../eg/preencrypt/PreEncryptor.kt | 4 +- .../eg/preencrypt/RecordedPreBallot.kt | 2 +- .../eg/publish/json/EncryptedBallotJson.kt | 4 +- .../org/cryptobiotic/eg/core/HashTest.kt | 28 ++- .../eg/core/ecgroup/TestRfc9380.kt | 43 ++++ .../eg/keyceremony/KeyCeremonyTrusteeTest.kt | 2 +- .../eg/keyceremony/ShareEncryptDecryptTest.kt | 2 +- .../eg/preencrypt/PreEncryptorOutputTest.kt | 5 +- .../eg/preencrypt/PreEncryptorTest.kt | 45 +++-- 20 files changed, 249 insertions(+), 140 deletions(-) create mode 100644 src/test/kotlin/org/cryptobiotic/eg/core/ecgroup/TestRfc9380.kt diff --git a/README.md b/README.md index b12664b..4d2cfe9 100644 --- a/README.md +++ b/README.md @@ -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(6991/7729)-blue) +![Coverage](https://img.shields.io/badge/coverage-90.6%25%20LOC%20(7055/7784)-blue) # ElectionGuard-Kotlin Elliptic Curve -_last update 05/05/2024_ +_last update 05/08/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), diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/GroupContext.kt b/src/main/kotlin/org/cryptobiotic/eg/core/GroupContext.kt index cd49435..98581c0 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/GroupContext.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/GroupContext.kt @@ -63,6 +63,9 @@ interface GroupContext { */ fun binaryToElementModQ(b: ByteArray): ElementModQ + /** Converts a hash value to an [ElementModQ] */ + fun hashToElementModQ(hash: UInt256): ElementModQ = binaryToElementModQ(hash.bytes) + /** * Returns a random number in [2, Q). * Add "statistical distance" when generating. diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/UInt256.kt b/src/main/kotlin/org/cryptobiotic/eg/core/UInt256.kt index 63db8c4..ec0e094 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/UInt256.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/UInt256.kt @@ -2,6 +2,7 @@ package org.cryptobiotic.eg.core import org.cryptobiotic.eg.core.Base16.toHex import org.cryptobiotic.eg.core.Base64.toBase64 +import java.nio.ByteBuffer import kotlin.experimental.xor /** @@ -9,11 +10,24 @@ import kotlin.experimental.xor * care, because [ByteArray] allows for mutation, and the internal representation is available for * external use. */ -data class UInt256(val bytes: ByteArray) { +data class UInt256(val bytes: ByteArray): Comparable { init { require(bytes.size == 32) { "UInt256 must have exactly 32 bytes" } } + override fun compareTo(other: UInt256): Int { + var mismatch = -1 + for (idx in 0..31) { + if (bytes[idx] != other.bytes[idx]) { + mismatch = idx + break + } + } + + if (mismatch == -1) return 0 + return bytes[mismatch].toUByte().compareTo(other.bytes[mismatch].toUByte()) + } + override fun equals(other: Any?): Boolean = other is UInt256 && other.bytes.contentEquals(bytes) override fun hashCode(): Int { @@ -80,11 +94,11 @@ fun ByteArray.normalize(nbytes: Int): ByteArray { } /** - * Safely converts a [UInt256] to an [ElementModQ], wrapping values outside the range back to the + * Converts a [UInt256] to an [ElementModQ], wrapping values outside the range back to the * beginning by computing "mod q". */ fun UInt256.toElementModQ(context: GroupContext): ElementModQ = - context.binaryToElementModQ(bytes) + context.hashToElementModQ(this) fun ElementModQ.toUInt256safe(): UInt256 = this.byteArray().toUInt256safe() fun ULong.toUInt256(): UInt256 = this.toByteArray().toUInt256safe() diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcElementModQ.kt b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcElementModQ.kt index b61e68f..6e6b5a9 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcElementModQ.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcElementModQ.kt @@ -4,7 +4,6 @@ import org.cryptobiotic.eg.core.* import org.cryptobiotic.eg.core.Base64.toBase64 import java.math.BigInteger -// Theres not really any difference with the Integer Group ElementModQ. class EcElementModQ(override val group: EcGroupContext, val element: BigInteger): ElementModQ { override fun byteArray(): ByteArray = element.toByteArray().normalize(32) diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcGroupContext.kt b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcGroupContext.kt index dfd1df0..e6d7fa1 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcGroupContext.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/EcGroupContext.kt @@ -1,7 +1,6 @@ package org.cryptobiotic.eg.core.ecgroup import org.cryptobiotic.eg.core.* -import org.cryptobiotic.eg.core.intgroup.ProductionElementModQ import org.cryptobiotic.eg.core.intgroup.toBigInteger import java.math.BigInteger import java.util.concurrent.atomic.AtomicInteger @@ -15,22 +14,24 @@ class EcGroupContext(val name: String, useNative: Boolean = true): GroupContext override val MAX_BYTES_P: Int = vecGroup.pbyteLength + 1 // x plus sign of y override val ONE_MOD_P: ElementModP = this.ONE - override val MAX_BYTES_Q: Int = vecGroup.qbyteLength + override val MAX_BYTES_Q: Int = vecGroup.nbyteLength override val ZERO_MOD_Q: ElementModQ = EcElementModQ(this, BigInteger.ZERO) override val ONE_MOD_Q: ElementModQ = EcElementModQ(this, BigInteger.ONE) override val TWO_MOD_Q: ElementModQ = EcElementModQ(this, BigInteger.TWO) override val constants = vecGroup.constants - val dlogg = DLogarithm(G_MOD_P) + + private val dlogg = DLogarithm(G_MOD_P) + private val rfc9380 = RFC9380(this, "QUUX-V01-CS02-with-P256_XMD:SHA-256_SSWU_RO_".toByteArray(), 16) override fun binaryToElementModP(b: ByteArray): ElementModP? { val elem = vecGroup.elementFromByteArray(b) return if (elem != null) EcElementModP(this, elem) else null } - override fun binaryToElementModQ(b: ByteArray): ElementModQ { - return EcElementModQ(this, BigInteger(1, b)) - } + override fun binaryToElementModQ(b: ByteArray) = EcElementModQ(this, BigInteger(1, b)) + + override fun hashToElementModQ(hash: UInt256): ElementModQ = rfc9380.hash_to_field(hash.bytes) /** Returns a random number in [2, Q). */ override fun randomElementModQ(statBytes:Int) : ElementModQ { diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/RFC9380.kt b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/RFC9380.kt index e2fed9e..0751302 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/RFC9380.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/RFC9380.kt @@ -1,27 +1,38 @@ package org.cryptobiotic.eg.core.ecgroup -// import jdk.vm.ci.code.CodeUtil.log2 -import kotlin.math.ceil +// implement what is in Section 5 of RFC 9380: Hashing to Elliptic Curves (rfc-editor.org). +// This produces a larger output before it is reduced modulo Q. +// You’ll need that algorithms expand_message in 5.3 and hash_to_field in 5.2, which uses expand_message. +// Note that you only need to do this for m=1 (and then their q = their p) and count = 1. + +// TODO see example output in Appendix J.1 of https://www.rfc-editor.org/rfc/rfc9380.pdf + +import java.math.BigInteger +import java.security.MessageDigest +import kotlin.experimental.xor // https://www.rfc-editor.org/rfc/rfc9380.pdf -class RFC9380(val DST: ByteArray, p: Int, k: Int, val m: Int) { - // val L = ceil((ceil(log2(p)) + k) / 8) - - // hash_to_field(msg, count) - //Parameters: - //- DST, a domain separation tag (see Section 3.1). - //- F, a finite field of characteristic p and order q = p^m. - //- p, the characteristic of F (see immediately above). - //- m, the extension degree of F, m >= 1 (see immediately above). - //- L = ceil((ceil(log2(p)) + k) / 8), where k is the security - // parameter of the suite (e.g., k = 128). - // - //- expand_message, a function that expands a byte string and - // domain separation tag into a uniformly random byte string (see Section 5.3). +//- DST, a domain separation tag (see Section 3.1). +//- F, a finite field of characteristic p and order q = p^m. +//- p, the characteristic of F (see immediately above). +//- m, the extension degree of F, m >= 1 (see immediately above). m == 1 +//- count, the number of elements of F to output. count == 1 +//- k is the security parameter of the suite (e.g., k = 128, kBytes = 16). +class RFC9380(val group: EcGroupContext, val DST: ByteArray, kBytes: Int) { + + companion object { + val b_in_bytes = 32 // output of hashFunction in bytes = 32 + val s_in_bytes = 64 // the input block size of H, measured in bytes. + } + + val len_in_bytes = group.vecGroup.pbyteLength + kBytes + + // hash_to_field(msg) + //- expand_message, a function that expands a byte string and domain separation tag into a uniformly random byte + // string (see Section 5.3). //Input: //- msg, a byte string containing the message to hash. - //- count, the number of elements of F to output. //Output: //- (u_0, ..., u_(count - 1)), a list of field elements. //Steps: @@ -35,72 +46,106 @@ class RFC9380(val DST: ByteArray, p: Int, k: Int, val m: Int) { //8. u_i = (e_0, ..., e_(m - 1)) //9. return (u_0, ..., u_(count - 1)) - /* - fun hash_to_field(msg: ByteArray, count: Int) : ByteArray { + fun hash_to_field(msg: ByteArray): EcElementModQ { //Steps: - val len_in_bytes = count * m * L - val uniform_bytes = expand_message(msg, DST, len_in_bytes) - //3. for i in (0, ..., count - 1): - //4. for j in (0, ..., m - 1): - //5. elm_offset = L * (j + i * m) - //6. tv = substr(uniform_bytes, elm_offset, L) - //7. e_j = OS2IP(tv) mod p - //8. u_i = (e_0, ..., e_(m - 1)) - //9. return (u_0, ..., u_(count - 1)) + val uniform_bytes = expand_message(msg) + val bi = BigInteger(1, uniform_bytes) // OS2IP equiv + return EcElementModQ(group, bi.mod(group.vecGroup.primeModulus)) } - */ - -// expand_message_xmd(msg, DST, len_in_bytes) -// Parameters: -// - H, a hash function (see requirements above). -// - b_in_bytes, b / 8 for b the output size of H in bits. -// For example, for b = 256, b_in_bytes = 32. -// - s_in_bytes, the input block size of H, measured in bytes (see -// discussion above). For example, for SHA-256, s_in_bytes = 64. - -// Input: -// - msg, a byte string. -// - DST, a byte string of at most 255 bytes. -// See below for information on using longer DSTs. -// - len_in_bytes, the length of the requested output in bytes, -// not greater than the lesser of (255 * b_in_bytes) or 2^16-1. -// Output: -// - uniform_bytes, a byte string. - -// Steps: -// 1. ell = ceil(len_in_bytes / b_in_bytes) -// 2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255 -// 3. DST_prime = DST || I2OSP(len(DST), 1) -// 4. Z_pad = I2OSP(0, s_in_bytes) -// 5. l_i_b_str = I2OSP(len_in_bytes, 2) -// 6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime -// 7. b_0 = H(msg_prime) -// 8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) -// 9. for i in (2, ..., ell): -// 10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime) -// 11. uniform_bytes = b_1 || ... || b_ell -// 12. return substr(uniform_bytes, 0, len_in_bytes) - - /* - fun expand_message(msg: ByteArray, DST: ByteArray, len_in_bytes: Int) { - val ell = ceil(len_in_bytes / b_in_bytes) + // expand_message_xmd(msg, DST, len_in_bytes) + // Parameters: + // - H, a hash function (see requirements above). + // - b_in_bytes, b / 8 for b the output size of H in bits. + // For example, for b = 256, b_in_bytes = 32. + // - s_in_bytes, the input block size of H, measured in bytes. For example, for SHA-256, s_in_bytes = 64. + // Input: + // - msg, a byte string. + // - DST, a byte string of at most 255 bytes. See below for information on using longer DSTs. + // - len_in_bytes, the length of the requested output in bytes, not greater than the lesser of (255 * b_in_bytes) or 2^16-1. + // - b_in_bytes: output of hashFunction in bytes = 32 + // - s_in_bytes: output of hashFunction in bytes = 64 + // Output: + // - uniform_bytes, a byte string. + fun expand_message(msg: ByteArray): ByteArray { + // 1. ell = ceil(len_in_bytes / b_in_bytes) + val ell = (len_in_bytes + b_in_bytes - 1)/ b_in_bytes + // 2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255 require (ell < 255 && len_in_bytes < 65535 && DST.size < 255) - val DST_prime = DST || I2OSP(len(DST), 1) + // 3. DST_prime = DST || I2OSP(len(DST), 1) + val DST_prime = DST + I2OSP(DST.size, 1) + // 4. Z_pad = I2OSP(0, s_in_bytes) val Z_pad = I2OSP(0, s_in_bytes) + // 5. l_i_b_str = I2OSP(len_in_bytes, 2) val l_i_b_str = I2OSP(len_in_bytes, 2) - val msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime + // 6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime + val msg_prime = Z_pad + msg + l_i_b_str + I2OSP(0, 1) + DST_prime + // 7. b_0 = H(msg_prime) val b_0 = H(msg_prime) - val b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) + // 8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) + val b_1 = H(b_0 + I2OSP(1, 1) + DST_prime) + var prev = b_1 + var uniform_bytes = b_1 + + // 9. for i in (2, ..., ell): for (i in (2..ell)) { - val b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime) + // 10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime) + val b_i = H(strxor(b_0, prev) + I2OSP(i, 1) + DST_prime) + prev = b_i + // 11. uniform_bytes = b_1 || ... || b_ell + uniform_bytes += b_i } - val uniform_bytes = b_1 || ... || b_ell - return substr(uniform_bytes, 0, len_in_bytes) + // 12. return substr(uniform_bytes, 0, len_in_bytes) + return ByteArray(len_in_bytes) { uniform_bytes[it] } } - */ + fun H(ba: ByteArray): ByteArray { + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(ba) + } + fun strxor(b0: ByteArray, bi: ByteArray): ByteArray { + require(b0.size == bi.size) + return ByteArray(b0.size) { b0[it].xor(bi[it]) } + } +} + +// https://github.com/rackerlabs/atlas-lb/blob/master/common/ca/bouncycastle/src/main/java/org/bouncycastle/pqc/math/linearalgebra/BigEndianConversions.java + +@Throws(ArithmeticException::class) +fun I2OSP(x: Int, oLen: Int): ByteArray { + if (x < 0) { + throw RuntimeException("x must be unsigned") + } + val octL: Int = ceilLog256(x) + if (octL > oLen) { + throw ArithmeticException("Cannot encode given integer into specified number of octets.") + } + val result = ByteArray(oLen) + for (i in oLen - 1 downTo oLen - octL) { + result[i] = (x ushr (8 * (oLen - 1 - i))).toByte() + } + return result +} + +/** + * Compute ceil(log_256 n), the number of bytes needed to encode + * the integer n. + * + * @param n the integer + * @return the number of bytes needed to encode n + */ +fun ceilLog256(n: Int): Int { + if (n == 0) { + return 1 + } + var m: Int = if (n < 0) -n else n + var d = 0 + while (m > 0) { + d++ + m = m ushr 8 + } + return d } \ No newline at end of file diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecElementPnative.kt b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecElementPnative.kt index 7a00a9e..3f13a80 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecElementPnative.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecElementPnative.kt @@ -103,7 +103,7 @@ class VecElementPnativeAcc( // final BigInteger basisx, // final BigInteger basisy, // final int size) { - VEC.fmul_precompute(vgNative.nativePointer, x, y, vgNative.qbitLength) + VEC.fmul_precompute(vgNative.nativePointer, x, y, vgNative.nbitLength) } override fun exp(exponent: BigInteger): VecElementP { diff --git a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecGroup.kt b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecGroup.kt index 021722f..437f842 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecGroup.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecGroup.kt @@ -40,20 +40,21 @@ import java.math.BigInteger * @param a x-coefficient * @param b constant * @param primeModulus Field over which to perform calculations. - * @param order Order of the group, for Q. + * @param order Order of the group (n). * @param gx x-coordinate of the generator. * @param gy y-coordinate of the generator. * @throws RuntimeException If the input parameters are * inconsistent * @throws RuntimeException If the input parameters are * inconsistentaqrt */ -// the order of G is the smallest positive number n such that ng = O (the point at infinity of the curve, and the identity element) +// See https://www.rfc-editor.org/rfc/rfc9380.pdf for more depth on EC groups. +// This version assume a Prime group where h = 1. Then q = p. open class VecGroup( val curveName: String, val a: BigInteger, val b: BigInteger, val primeModulus: BigInteger, // Prime primeModulus of the underlying field - val order: BigInteger, + val order: BigInteger, // n gx: BigInteger, gy: BigInteger, val h: BigInteger @@ -66,8 +67,8 @@ open class VecGroup( val pbitLength: Int = primeModulus.bitLength() val pbyteLength = (pbitLength + 7) / 8 - val qbitLength: Int = order.bitLength() - val qbyteLength = (qbitLength + 7) / 8 + val nbitLength: Int = order.bitLength() + val nbyteLength = (nbitLength + 7) / 8 val constants by lazy { ElectionConstants(curveName, GroupType.EllipticCurve, protocolVersion, @@ -85,7 +86,9 @@ open class VecGroup( val pIs3mod4: Boolean init { + require (h.equals(BigInteger.ONE)) require (!primeModulus.equals(BigInteger.TWO)) + require (!primeModulus.equals(BigInteger.valueOf(3))) pIs3mod4 = primeModulus.testBit(0) && primeModulus.testBit(1) // p mod 4 = 3, true for P-256 } @@ -174,6 +177,7 @@ open class VecGroup( return a.modPow(v, p) /* TODO this code is failing when p mod 4 != 3, remove for now + // TODO see Appendix I of https://www.rfc-editor.org/rfc/rfc9380.pdf println("not p = 3 mod 4") // Compute k and s, where p = 2^s (2k+1) +1 diff --git a/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt b/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt index 5fabd9e..68def29 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrustee.kt @@ -119,7 +119,9 @@ open class KeyCeremonyTrustee( // decrypt Pi(l) val pilbytes = shareDecryption(share) ?: return Err("Trustee '$id' couldnt decrypt EncryptedKeyShare for missingGuardianId '${share.polynomialOwner}'") - val expectedPil: ElementModQ = pilbytes.toUInt256safe().toElementModQ(group) // Pi(ℓ) + // in this case, pilbytes are the bytes of an ElementModQ, not the results of the hash function that needs to be converted + // to an ElementModQ, which messes up when the group is an elliptic curve. + val expectedPil: ElementModQ = group.binaryToElementModQ(pilbytes) // Pi(ℓ) // The other's Kij val publicKeys = otherPublicKeys[share.polynomialOwner] diff --git a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreBallot.kt index 05a8604..526e71d 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreBallot.kt @@ -40,7 +40,7 @@ internal data class PreContest( internal data class PreSelectionVector( val selectionId: String, // do not serialize - val selectionHash: ElementModQ, // ψi (92) + val selectionHash: UInt256, // ψi (92) val shortCode: String, val encryptions: List, // Ej, size = nselections, in order by sequence_order val nonces: List, // size = nselections, in order by sequence_order, do not serialize @@ -103,7 +103,7 @@ internal fun MarkedPreEncryptedBallot.makePreBallot(preeBallot : PreEncryptedBal PreSelectionVector(preeSelection.selectionId, preeSelection.selectionHash, preeSelection.shortCode, preeSelection.selectionVector, preeSelection.selectionNonces) } - val allSortedSelectedHashes = preeContest.selections.sortedBy { it.selectionHash }.map { it.selectionHash.toUInt256safe() } + val allSortedSelectedHashes = preeContest.selections.sortedBy { it.selectionHash }.map { it.selectionHash } contests.add( PreContest( diff --git a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptedBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptedBallot.kt index 0b2b856..c73497c 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptedBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptedBallot.kt @@ -34,7 +34,7 @@ data class PreEncryptedContest( data class PreEncryptedSelection( val selectionId: String, // could just pass the manifest selection, in case other info is needed val sequenceOrder: Int, // matches the Manifest - val selectionHash: ElementModQ, // allow numerical sorting with ElementModQ, eq 92 + val selectionHash: UInt256, // allow numerical sorting with ElementModQ, eq 92 val shortCode: String, val selectionVector: List, // nselections, in sequenceOrder, eq 91 val selectionNonces: List, // nselections, in sequenceOrder (optional) diff --git a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptor.kt b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptor.kt index 0535bdb..eaf2797 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptor.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptor.kt @@ -69,7 +69,7 @@ class PreEncryptor( } // numerically sorted selectionHashes - val selectionHashes = preeSelections.sortedBy { it.selectionHash }.map { it.selectionHash.toUInt256safe() } + val selectionHashes = preeSelections.sortedBy { it.selectionHash }.map { it.selectionHash } // χl = H(HE ; 0x41, indc (Λl ), K, ψσ(1) , ψσ(2) , . . . , ψσ(m+L) ) ; 94 val preencryptionHash = hashFunction(extendedBaseHash.bytes, 0x41.toByte(), this.sequenceOrder, publicKey, selectionHashes) @@ -107,7 +107,7 @@ class PreEncryptor( return PreEncryptedSelection( thisSelectionId, thisSelectionIndex, - selectionHash.toElementModQ(group), + selectionHash, sigma(selectionHash), encryptionVector, encryptionNonces, diff --git a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/RecordedPreBallot.kt b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/RecordedPreBallot.kt index 465fccf..0486289 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/preencrypt/RecordedPreBallot.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/preencrypt/RecordedPreBallot.kt @@ -33,7 +33,7 @@ data class RecordedPreEncryption( data class RecordedSelectionVector( val selectionId: String, // do not serialize - val selectionHash: ElementModQ, // ψi (92) + val selectionHash: UInt256, // ψi (92) val shortCode: String, val encryptions: List, // Ej, size = nselections, in order by sequence_order ) { diff --git a/src/main/kotlin/org/cryptobiotic/eg/publish/json/EncryptedBallotJson.kt b/src/main/kotlin/org/cryptobiotic/eg/publish/json/EncryptedBallotJson.kt index cbc045d..e4b3693 100644 --- a/src/main/kotlin/org/cryptobiotic/eg/publish/json/EncryptedBallotJson.kt +++ b/src/main/kotlin/org/cryptobiotic/eg/publish/json/EncryptedBallotJson.kt @@ -187,7 +187,7 @@ private fun RecordedPreEncryption.publishJson(): PreEncryptionJson { private fun RecordedSelectionVector.publishJson(): SelectionVectorJson { return SelectionVectorJson( - this.selectionHash.toUInt256safe().publishJson(), + this.selectionHash.publishJson(), this.shortCode, this.encryptions.map { it.publishJson() }, ) @@ -214,7 +214,7 @@ fun PreEncryptionJson.import(group: GroupContext, errs: ErrorMessages): Encrypte val selectedVectors = this.selected_vectors.mapIndexed { idx,it -> it.import(group, errs.nested("selectedVectors $idx")) } return if (errs.hasErrors()) null - else EncryptedBallot.PreEncryption( + else EncryptedBallot.PreEncryption( preencryptionHash!!, allSelectionHashes.filterNotNull(), selectedVectors.filterNotNull(), diff --git a/src/test/kotlin/org/cryptobiotic/eg/core/HashTest.kt b/src/test/kotlin/org/cryptobiotic/eg/core/HashTest.kt index 11048c5..1811710 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/core/HashTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/core/HashTest.kt @@ -4,6 +4,7 @@ import io.kotest.property.checkAll import io.kotest.property.forAll import org.cryptobiotic.eg.core.Base16.fromHexSafe import org.cryptobiotic.eg.core.Base16.toHex +import org.cryptobiotic.eg.core.Base64.fromBase64 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -35,21 +36,18 @@ class HashTest { @Test fun testNonce() { - runTest { - val group = productionGroup() - val contestDescriptionHashQ = "00C49A1E8053FBA95F6B7CD3F3B30B101CDD595C435A46AECF2872F47F1C601206".fromHexSafe() - .toUInt256safe().toElementModQ(group) - println(" contestDescriptionHashQ = $contestDescriptionHashQ hex = ${contestDescriptionHashQ}") - val ballotNonce = "13E7A2F4253E6CCE42ED5576CF7B01A06BE07835227E7AFE5F538FB94E9A9B73".fromHexSafe() - .toUInt256safe().toElementModQ(group) - val nonceSequence = Nonces(contestDescriptionHashQ, ballotNonce) - val nonce0: ElementModQ = nonceSequence[0] - println(" nonce seed in hex = ${nonceSequence.internalSeed.toHex()}") - println(" nonce0 in hex = ${nonce0}") - val expect = "ACDE405F255D4C3101A895AE80863EA4639A889593D557EB5AD5B855684D5B50".fromHexSafe() - .toUInt256safe().toElementModQ(group) - assertEquals(expect, nonceSequence[0]) - } + val group = productionGroup() + val contestDescriptionHashQ = "00C49A1E8053FBA95F6B7CD3F3B30B101CDD595C435A46AECF2872F47F1C601206".fromHexSafe() + .toUInt256safe().toElementModQ(group) + println(" contestDescriptionHashQ = $contestDescriptionHashQ hex = ${contestDescriptionHashQ}") + val ballotNonce = "13E7A2F4253E6CCE42ED5576CF7B01A06BE07835227E7AFE5F538FB94E9A9B73".fromHexSafe() + .toUInt256safe().toElementModQ(group) + val nonceSequence = Nonces(contestDescriptionHashQ, ballotNonce) + val nonce0: ElementModQ = nonceSequence[0] + println(" nonce seed in hex = ${nonceSequence.internalSeed.toHex()}") + println(" nonce0 in hex = ${nonce0}") + val expect = group.binaryToElementModQ("IwEC3GZhDUHC+BlFdMfakdtbnPQ6TCcvz77EcKMuNnI=".fromBase64()!!) + assertEquals(expect, nonceSequence[0]) } @Test diff --git a/src/test/kotlin/org/cryptobiotic/eg/core/ecgroup/TestRfc9380.kt b/src/test/kotlin/org/cryptobiotic/eg/core/ecgroup/TestRfc9380.kt new file mode 100644 index 0000000..8333bf6 --- /dev/null +++ b/src/test/kotlin/org/cryptobiotic/eg/core/ecgroup/TestRfc9380.kt @@ -0,0 +1,43 @@ +package org.cryptobiotic.eg.core.ecgroup + +import org.cryptobiotic.eg.core.Base16.fromHex +import org.cryptobiotic.eg.core.UInt256 +import org.cryptobiotic.eg.core.elGamalKeyPairFromRandom +import org.cryptobiotic.eg.core.hashFunction +import kotlin.test.Test +import kotlin.test.assertEquals + +// https://www.rfc-editor.org/rfc/rfc9380.pdf Appendix J-1 +class TestRfc9380 { + + @Test + fun testRfc() { + val group = EcGroupContext("P-256") + val dst = "QUUX-V01-CS02-with-P256_XMD:SHA-256_SSWU_RO_" + val test = RFC9380(group, dst.toByteArray(), 16) + + testRfc(test, ByteArray(0)) + testRfc(test, "abcdef0123456789".fromHex()!!) + testRfc(test, "abcdef0123456789".toByteArray()) + } + + @Test + fun testRfcWithHash() { + val group = EcGroupContext("P-256") + val dst = "QUUX-V01-CS02-with-P256_XMD:SHA-256_SSWU_RO_" + val test = RFC9380(group, dst.toByteArray(), 16) + + val keypair = elGamalKeyPairFromRandom(group) + val extendedBaseHash = UInt256.random() + val h = hashFunction(extendedBaseHash.bytes, 0x42.toByte(), keypair.publicKey, keypair.secretKey.key) + testRfc(test, h.bytes) + } + + fun testRfc(rfc: RFC9380, msg: ByteArray) { + val peat = rfc.hash_to_field(msg) + val repeat = rfc.hash_to_field(msg) + + println("q = ${peat}") + assertEquals(peat, repeat) + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrusteeTest.kt b/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrusteeTest.kt index 8bc3b23..df56aaf 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrusteeTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/keyceremony/KeyCeremonyTrusteeTest.kt @@ -102,7 +102,7 @@ class KeyCeremonyTrusteeTest { val pilbytes: ByteArray? = trustee2.shareDecryption(encryptedShare) assertNotNull(pilbytes) - val decodedPil: ElementModQ = pilbytes.toUInt256safe().toElementModQ(group) // Pi(ℓ) + val decodedPil: ElementModQ = group.binaryToElementModQ(pilbytes) // Pi(ℓ) assertEquals(pil, decodedPil) } diff --git a/src/test/kotlin/org/cryptobiotic/eg/keyceremony/ShareEncryptDecryptTest.kt b/src/test/kotlin/org/cryptobiotic/eg/keyceremony/ShareEncryptDecryptTest.kt index 58cd3f4..e242a54 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/keyceremony/ShareEncryptDecryptTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/keyceremony/ShareEncryptDecryptTest.kt @@ -39,7 +39,7 @@ class ShareEncryptDecryptTest { val pilbytes: ByteArray? = trustee2.shareDecryption(encryptedShare) assertNotNull(pilbytes) - val decodedPil: ElementModQ = pilbytes.toUInt256safe().toElementModQ(group) // Pi(ℓ) + val decodedPil: ElementModQ = group.binaryToElementModQ(pilbytes) // Pi(ℓ) assertEquals(pil, decodedPil) } } diff --git a/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorOutputTest.kt b/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorOutputTest.kt index de1332f..22d4429 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorOutputTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorOutputTest.kt @@ -12,6 +12,7 @@ import org.cryptobiotic.util.ErrorMessages import org.cryptobiotic.util.Testing import kotlin.random.Random import kotlin.test.Test +import kotlin.test.fail private val random = Random @@ -71,7 +72,7 @@ class PreEncryptorOutputTest { } if (errs.hasErrors()) { println(errs) - return + fail() } val (recordedBallot, ciphertextBallot) = pair!! @@ -125,7 +126,7 @@ class PreEncryptorOutputTest { while (doneIdx.size < pcontest.contestLimit) { val idx = random.nextInt(nselections) if (!doneIdx.contains(idx)) { - shortCodes.add(sigma(pcontest.selections[idx].selectionHash.toUInt256safe())) + shortCodes.add(sigma(pcontest.selections[idx].selectionHash)) selections.add(pcontest.selections[idx].selectionId) doneIdx.add(idx) } diff --git a/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorTest.kt b/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorTest.kt index 68ce56e..ba84bca 100644 --- a/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorTest.kt +++ b/src/test/kotlin/org/cryptobiotic/eg/preencrypt/PreEncryptorTest.kt @@ -47,33 +47,32 @@ class PreEncryptorTest { // sanity check that Recorder.record doesnt barf @Test fun testRecord() { - runTest { - val electionRecord = readElectionRecord( input) - val electionInit = electionRecord.electionInit()!! - val manifest = electionRecord.manifest() - val ballotStyle = manifest.ballotStyles[0].ballotStyleId + val electionRecord = readElectionRecord( input) + val electionInit = electionRecord.electionInit()!! + val manifest = electionRecord.manifest() + val ballotStyle = manifest.ballotStyles[0].ballotStyleId - val preEncryptor = - PreEncryptor(electionRecord.group, manifest, electionInit.jointPublicKey, electionInit.extendedBaseHash, ::sigma) + val preEncryptor = + PreEncryptor(electionRecord.group, manifest, electionInit.jointPublicKey, electionInit.extendedBaseHash, ::sigma) - manifest.ballotStyles.forEach { println(it) } + manifest.ballotStyles.forEach { println(it) } - val primaryNonce = 42U.toUInt256() - val pballot = preEncryptor.preencrypt("testDecrypt_ballot_id", ballotStyle, primaryNonce) - assertNotNull(pballot) + val primaryNonce = 42U.toUInt256() + val pballot = preEncryptor.preencrypt("testDecrypt_ballot_id", ballotStyle, primaryNonce) + assertNotNull(pballot) - val mballot = markBallotChooseOne(manifest, pballot) - assertNotNull(mballot) + val mballot = markBallotChooseOne(manifest, pballot) + assertNotNull(mballot) - val recorder = - Recorder(electionRecord.group, manifest, electionInit.jointPublicKey, electionInit.extendedBaseHash, "device", ::sigma) + val recorder = + Recorder(electionRecord.group, manifest, electionInit.jointPublicKey, electionInit.extendedBaseHash, "device", ::sigma) - val errs = ErrorMessages("MarkedBallot ${mballot.ballotId}") - with (recorder) { - mballot.record(primaryNonce, errs) - } - assertFalse(errs.hasErrors()) + val errs = ErrorMessages("MarkedBallot ${mballot.ballotId}") + with (recorder) { + mballot.record(primaryNonce, errs) } + if (errs.hasErrors()) println(errs) + assertFalse(errs.hasErrors()) } // check that CiphertextBallot is correctly formed @@ -307,7 +306,7 @@ internal class ChosenBallot(val selectedIdx: Int) { pcontests.add( MarkedPreEncryptedContest( pcontest.contestId, - listOf(sigma(pselection.selectionHash.toUInt256safe())), + listOf(sigma(pselection.selectionHash)), listOf(pselection.selectionId), ) ) @@ -340,7 +339,7 @@ internal fun markBallotChooseOne(manifest: Manifest, pballot: PreEncryptedBallot pcontests.add( MarkedPreEncryptedContest( pcontest.contestId, - listOf(sigma(pselection.selectionHash.toUInt256safe())), + listOf(sigma(pselection.selectionHash)), listOf(pselection.selectionId), ) ) @@ -365,7 +364,7 @@ internal fun markBallotToLimit(manifest: Manifest, pballot: PreEncryptedBallot): while (doneIdx.size < pcontest.contestLimit) { val idx = random.nextInt(nselections) if (!doneIdx.contains(idx)) { - shortCodes.add(sigma(pcontest.selections[idx].selectionHash.toUInt256safe())) + shortCodes.add(sigma(pcontest.selections[idx].selectionHash)) selections.add(pcontest.selections[idx].selectionId) doneIdx.add(idx) }