Implement BN256's G1 functionality.

- Ported over the BN256's G1 functionality from derohe's bh256.go file.
- Updated CurePoint's toString() method: Create a copy of the object to avoid updating the values of the object when calling toString().
- Used UByteArray in GfP's marshal/unmarshal methods to fix issue with negative numbers.
- Fixed an issue with how the carry is calculated in gfpMul method and added a test to confirm its correct behavior.
- Used UByteArray in Lattice's multi method to avoid issues with negative numbers.
- Added a test to confirm that G1 marshal and unmarshal methods work correctly.
This commit is contained in:
Severiano Jaramillo 2024-06-01 22:51:01 -07:00
parent 9352f82894
commit 01be5d24ab
7 changed files with 211 additions and 22 deletions

View file

@ -0,0 +1,145 @@
@file:OptIn(ExperimentalUnsignedTypes::class)
package net.agorise.library.crypto.bn256
import java.math.BigInteger
import java.security.SecureRandom
/*
* Package bn256 implements a particular bilinear group at the 128-bit security
* level.
*
* Bilinear groups are the basis of many of the new cryptographic protocols that
* have been proposed over the past decade. They consist of a triplet of groups
* (G₁, G₂ and GT) such that there exists a function e(g₁ˣ,g₂ʸ)=gTˣʸ (where gₓ
* is a generator of the respective group). That function is called a pairing
* function.
*
* This package specifically implements the Optimal Ate pairing over a 256-bit
* Barreto-Naehrig curve as described in
* http://cryptojedi.org/papers/dclxvi-20100714.pdf. Its output is not
* compatible with the implementation described in that paper, as different
* parameters are chosen.
*
* (This package previously claimed to operate at a 128-bit security level.
* However, recent improvements in attacks mean that is no longer true. See
* https://moderncrypto.org/mail-archive/curves/2016/000740.html.)
*
* Ported over from https://github.com/deroproject/derohe/blob/main/cryptography/bn256/bn256.go
*/
fun randomK(random: SecureRandom): BigInteger {
var k: BigInteger
do {
k = BigInteger(Constants.Order.bitLength(), random).mod(Constants.Order)
} while (k.signum() != 1)
return k
}
/*
* G1 is an abstract cyclic group. The zero value is suitable for use as the
* output of an operation, but cannot be used as an input.
*/
class G1(var p: CurvePoint = CurvePoint()) {
override fun toString(): String {
return "bn256.G1: $p"
}
// ScalarBaseMult sets e to g*k where g is the generator of the group and then returns e.
fun scalarBaseMult(k: BigInteger): G1 {
this.p.mul(CurvePoint.curveGen, k)
return this
}
// ScalarMult sets e to a*k and then returns e.
fun scalarMult(a: G1, k: BigInteger): G1 {
this.p.mul(a.p, k)
return this
}
// Add sets e to a+b and then returns e.
fun add(a: G1, b: G1): G1 {
this.p.add(a.p, b.p)
return this
}
// Neg sets e to -a and then returns e.
fun neg(a: G1): G1 {
this.p.neg(a.p)
return this
}
// Set sets e to a and then returns e.
fun set(a: G1): G1 {
p.set(a.p)
return this
}
// Marshal converts e to a byte slice.
fun marshal(): UByteArray {
// Each value is a 256-bit number.
val numBytes = 256 / 8
this.p.makeAffine()
val ret = UByteArray(numBytes * 2)
if (this.p.isInfinity()) {
return ret
}
val temp = GfP()
montDecode(temp, this.p.x)
temp.marshal(ret)
montDecode(temp, this.p.y)
temp.marshal(ret, numBytes)
return ret
}
// Unmarshal sets e to the result of converting the output of Marshal back into
// a group element and then returns e.
fun unmarshal(m: UByteArray): Result<UByteArray> {
// Each value is a 256-bit number.
val numBytes = 256 / 8
if (m.size < 2 * numBytes) {
return Result.failure(IllegalArgumentException("bn256: not enough data"))
}
// Unmarshal the points and check their caps
this.p.x = GfP()
this.p.y = GfP()
try {
this.p.x.unmarshal(m)
this.p.y.unmarshal(m, numBytes)
} catch (e: Throwable) {
return Result.failure(e)
}
// Encode into Montgomery form and ensure it's on the curve
montEncode(this.p.x, this.p.x)
montEncode(this.p.y, this.p.y)
val zero = GfP()
if (this.p.x == zero && this.p.y == zero) {
// This is the point at infinity.
this.p.y = GfP.newGfP(1)
this.p.z = GfP()
this.p.t = GfP()
} else {
this.p.z = GfP.newGfP(1)
this.p.t = GfP.newGfP(1)
if (this.p.isOnCurve().not()) {
return Result.failure(IllegalArgumentException("bn256: malformed point"))
}
}
return Result.success(m.copyOfRange(2 * numBytes, m.size))
}
companion object {
// RandomG1 returns x and g₁ˣ where x is a random, non-zero number read from r.
fun randomG1(random: SecureRandom): Pair<BigInteger, G1> {
val k = randomK(random)
return Pair(k, G1().scalarBaseMult(k))
}
}
}

View file

@ -7,20 +7,23 @@ import java.math.BigInteger
* form and t= when valid. G₁ is the set of points of this curve on GF(p).
* Ported over from https://github.com/deroproject/derohe/blob/main/cryptography/bn256/curve.go
*/
internal class CurvePoint(
private var x: GfP,
private var y: GfP,
private var z: GfP,
private var t: GfP,
class CurvePoint(
var x: GfP,
var y: GfP,
var z: GfP,
var t: GfP,
) {
private constructor() : this(GfP(0UL), GfP(0UL), GfP(0UL), GfP(0UL))
constructor() : this(GfP(), GfP(), GfP(), GfP())
override fun toString(): String {
makeAffine()
// Create a copy of the object to avoid modifying the original one
val copy = CurvePoint(this.x.copy(), this.y.copy(), this.z.copy(), this.t.copy())
copy.makeAffine()
val x = GfP()
val y = GfP()
montDecode(x, this.x)
montDecode(y, this.y)
montDecode(x, copy.x)
montDecode(y, copy.y)
return "($x, $y)"
}
@ -223,10 +226,10 @@ internal class CurvePoint(
// curveGen is the generator of G₁.
internal val curveGen = CurvePoint(
GfP.newGfP(1),
GfP.newGfP(2),
GfP.newGfP(1),
GfP.newGfP(1),
x = GfP.newGfP(1),
y = GfP.newGfP(2),
z = GfP.newGfP(1),
t = GfP.newGfP(1),
)
}
}

View file

@ -18,7 +18,9 @@ class GfP(val data: ULongArray) {
}
override fun toString(): String {
return String.format("%016x %016x %016x %016x", data[3].toLong(), data[2].toLong(), data[1].toLong(), data[0].toLong())
// Easier for testing
return data.joinToString(separator = " ", transform = { it.toString(10) })
// return data.reversed().joinToString(separator = " ", transform = { it.toString(16) })
}
override fun equals(other: Any?): Boolean {
@ -54,19 +56,19 @@ class GfP(val data: ULongArray) {
set(sum)
}
fun marshal(out: ByteArray) {
fun marshal(out: UByteArray, offset: Int = 0) {
for (w in 0 until 4) {
for (b in 0 until 8) {
out[8 * w + b] = (data[3 - w] shr (56 - 8 * b)).toByte()
out[offset + 8 * w + b] = (data[3 - w] shr (56 - 8 * b)).toUByte()
}
}
}
fun unmarshal(`in`: ByteArray) {
fun unmarshal(`in`: UByteArray, offset: Int = 0) {
for (w in 0 until 4) {
data[3 - w] = 0UL
for (b in 0 until 8) {
data[3 - w] += `in`[8 * w + b].toULong() shl (56 - 8 * b)
data[3 - w] += `in`[offset + 8 * w + b].toULong() shl (56 - 8 * b)
}
}

View file

@ -78,7 +78,7 @@ fun gfpMul(c: GfP, a: GfP, b: GfP) {
val ti = t[i]
val zi = Ti + ti + carry
T[i] = zi
carry = (Ti and ti or (Ti or ti) and zi.inv()) shr 63
carry = ((Ti and ti) or ((Ti or ti) and zi.inv())) shr 63
}
c.data[0] = T[4]

View file

@ -1,3 +1,5 @@
@file:OptIn(ExperimentalUnsignedTypes::class)
package net.agorise.library.crypto.bn256
import java.math.BigInteger
@ -52,14 +54,15 @@ internal class Lattice(
}
}
fun multi(scalar: BigInteger): ByteArray {
fun multi(scalar: BigInteger): UByteArray {
val decomp = decompose(scalar)
val maxLen = decomp.maxOf { it.bitLength() }
val out = ByteArray(maxLen)
val out = UByteArray(maxLen)
for ((j, x) in decomp.withIndex()) {
for (i in 0 until maxLen) {
out[i] = (out[i] + (if (x.testBit(i)) 1 else 0) shl j).toByte()
val bitValue = if (x.testBit(i)) 1u else 0u
out[i] = (out[i] + (bitValue shl j)).toUByte()
}
}
return out

View file

@ -0,0 +1,24 @@
@file:OptIn(ExperimentalUnsignedTypes::class)
package net.agorise.library.crypto.bn256
import java.security.SecureRandom
import kotlin.test.Test
import kotlin.test.assertEquals
class Bn256Test {
@Test
fun `given a randomly generated G1 - when marshal and unmarshal are called - then result is correct`() {
val random = SecureRandom()
val (_, ga) = G1.randomG1(random)
val ma = ga.marshal()
val gb = G1()
gb.unmarshal(ma)
val mb = gb.marshal()
println(ma.toList())
assertEquals(ma.toList(), mb.toList(), "bytes are different")
}
}

View file

@ -51,4 +51,16 @@ class GfPTest {
assertEquals(expectedGfP.data.toList(), actualGfP.data.toList())
}
@Test
fun `given two dec GfP values - when gfpMul is called - then result is correct`() {
val aGfP = GfP(ulongArrayOf(15230403791020821917UL, 754611498739239741UL, 7381016538464732716UL, 1011752739694698287UL))
val bGfP = GfP(ulongArrayOf(4792239181621121553UL, 12809537390334529128UL, 17379557665190828237UL, 110455090769457822UL))
val expectedGfP = GfP(ulongArrayOf(4792239181621121553UL, 12809537390334529128UL, 17379557665190828237UL, 110455090769457822UL))
val actualGfP = GfP()
gfpMul(actualGfP, aGfP, bGfP)
assertEquals(expectedGfP.data.toList(), actualGfP.data.toList())
}
}