Skip to content

Commit

Permalink
Merge pull request #80 from JohnLCaron/hashToElementModQ
Browse files Browse the repository at this point in the history
Separate hashToElementModQ() from binaryToElementModQ()
  • Loading branch information
JohnLCaron authored May 8, 2024
2 parents 50d1753 + 45d7494 commit fecda4d
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 fecda4d

Please sign in to comment.