Skip to content

Commit

Permalink
Separate hashToElementModQ() from binaryToElementModQ()
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JohnLCaron committed May 8, 2024
1 parent f05bbf7 commit 45d7494
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 140 deletions.
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(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),
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/org/cryptobiotic/eg/core/GroupContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 17 additions & 3 deletions src/main/kotlin/org/cryptobiotic/eg/core/UInt256.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@ 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

/**
* Superficially similar to an [ElementModQ], but guaranteed to be exactly 32 bytes long. Use with
* 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<UInt256> {
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 {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down
187 changes: 116 additions & 71 deletions src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/RFC9380.kt
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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 <tt>ceil(log_256 n)</tt>, the number of bytes needed to encode
* the integer <tt>n</tt>.
*
* @param n the integer
* @return the number of bytes needed to encode <tt>n</tt>
*/
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 9 additions & 5 deletions src/main/kotlin/org/cryptobiotic/eg/core/ecgroup/VecGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 45d7494

Please sign in to comment.