Skip to content

Commit

Permalink
add EdDSA decoder, fix twisted edwards coordinate recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
rec0de committed Jan 2, 2025
1 parent 23fddd3 commit b9e077a
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/ByteWitch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import bitmage.hex
import decoders.*

object ByteWitch {
private val decoders = listOf<ByteWitchDecoder>(BPListParser, OpackParser, Utf8Decoder, Utf16Decoder, Sec1Ec, AppleProtobuf, ProtobufParser, ASN1BER, GenericTLV, TLV8, ECCurves, EntropyDetector, HeuristicSignatureDetector)
private val decoders = listOf<ByteWitchDecoder>(BPListParser, OpackParser, Utf8Decoder, Utf16Decoder, Sec1Ec, AppleProtobuf, ProtobufParser, ASN1BER, GenericTLV, TLV8, EdDSA, ECCurves, EntropyDetector, HeuristicSignatureDetector)

fun analyzeHex(data: String, tryhard: Boolean = false): List<Pair<String, ByteWitchResult>> {
val filtered = data.filter { it in "0123456789abcdefABCDEF" }
Expand Down
100 changes: 95 additions & 5 deletions src/commonMain/kotlin/decoders/Sec1Ec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,14 @@ object ECCurves : ByteWitchDecoder {
}

override fun decode(data: ByteArray, sourceOffset: Int, inlineDisplay: Boolean): ByteWitchResult {
var curves = whichCurves(data, errorOnInvalidSize = true)
var littleEndian = false
// little endian format seems to be more prevalent, let's default to that
var curves = whichCurves(data.reversedArray(), errorOnInvalidSize = true)
var littleEndian = true

// try little endian encoding
if(curves.isEmpty()) {
littleEndian = true
curves = whichCurves(data.reversedArray(), errorOnInvalidSize = true)
littleEndian = false
curves = whichCurves(data, errorOnInvalidSize = true)
}

check(curves.isNotEmpty()){ "no curves pass modulo / square / equation checks" }
Expand Down Expand Up @@ -238,6 +239,63 @@ class Sec1Ec {
}
}

object EdDSA : ByteWitchDecoder {
override val name = "EdDSA"

private val expectedSizes = setOf(57)
private val tryhardSizes = setOf(32)

override fun decodesAsValid(data: ByteArray): Pair<Boolean, ByteWitchResult?> = Pair(data.size in expectedSizes, null)

override fun decode(data: ByteArray, sourceOffset: Int, inlineDisplay: Boolean): ByteWitchResult {
return when(data.size) {
57 -> decodeEd448(data, sourceOffset, inlineDisplay)
else -> throw Exception("unknown EdDSA pubkey size: ${data.size}")
}
}

override fun tryhardDecode(data: ByteArray): ByteWitchResult? {
try {
return when(data.size) {
32 -> decodeEd25519(data, 0, false)
57 -> decodeEd448(data, 0, false)
else -> null
}
}
catch (e: Exception) {
Logger.log(e.toString())
return null
}

}

private fun decodeEd448(data: ByteArray, sourceOffset: Int, inlineDisplay: Boolean): ByteWitchResult {
if(data.last().toInt() != 0x00 && data.last().toInt() != 0x80)
throw Exception("Ed448 pubkey should end with 0x00 or 0x80")
val reordered = data.reversedArray().fromIndex(1)
val curves = ECCurves.whichCurves(reordered, includeUncompressed = false)

if("Ed448-Goldilocks" !in curves)
throw Exception("Ed448 pubkey does not seem to be on Ed448 curve")

return EdDSAPubkey(reordered, data.last().toInt(), "Ed448", inlineDisplay, Pair(sourceOffset, sourceOffset+data.size))
}

private fun decodeEd25519(data: ByteArray, sourceOffset: Int, inlineDisplay: Boolean): ByteWitchResult {
val reordered = data.reversedArray()
val sign = data.last().toInt() and 0x80
reordered[0] = (reordered[0].toInt() and 0x7F).toByte()

println(reordered.hex())
val curves = ECCurves.whichCurves(reordered, includeUncompressed = false)

if("Ed25519" !in curves)
throw Exception("Ed25519 pubkey does not seem to be on Ed25519 curve")

return EdDSAPubkey(reordered, sign, "Ed25519", inlineDisplay, Pair(sourceOffset, sourceOffset+data.size))
}
}

class Sec1Point(private val x: ByteArray, private val y: ByteArray, private val curve: String, val inlineDisplay: Boolean, override val sourceByteRange: Pair<Int, Int>) : ByteWitchResult {
override fun renderHTML(): String {

Expand Down Expand Up @@ -274,6 +332,21 @@ class GenericECPoint(private val x: ByteArray, private val y: ByteArray?, privat
}
}

class EdDSAPubkey(private val x: ByteArray, private val y: Int, private val curve: String, val inlineDisplay: Boolean, override val sourceByteRange: Pair<Int, Int>) : ByteWitchResult {
override fun renderHTML(): String {

val identifier = if (inlineDisplay)
"<div class=\"bpvalue\">EdDSA $curve</div>"
else
"<div class=\"bpvalue\">$curve</div>"

val xHtml = "<div class=\"bpvalue data\" ${rangeTagsFor(sourceByteRange.first, sourceByteRange.first+x.size)}>X: 0x${x.hex()}</div>"
val yHtml = "<div class=\"bpvalue data\" ${rangeTagsFor(sourceByteRange.first+x.size, sourceByteRange.second)}>Y: 0x${y.toString(16)}</div>"

return "<div class=\"roundbox generic\" $byteRangeDataTags>$identifier$xHtml$yHtml</div>"
}
}

interface ECCurveModulus {
val bitSize: Int
val byteSize: Int
Expand Down Expand Up @@ -376,7 +449,7 @@ class MontgomeryCurve(val a: BigInteger) : EcEqEquation {
}
}

open class EdwardsCurve(private val d: BigInteger) : EcEqEquation {
open class EdwardsCurve(protected val d: BigInteger) : EcEqEquation {

protected open val sign = 1

Expand All @@ -397,6 +470,7 @@ open class EdwardsCurve(private val d: BigInteger) : EcEqEquation {

// xsq + ysq = 1 + d xsq ysq

// based on https://www.rfc-editor.org/rfc/rfc8032.html
override fun checkYSquare(x: BigInteger, mod: BigInteger): Boolean {
val modCreator = ModularBigInteger.creatorForModulo(mod)
val xm = modCreator.fromBigInteger(x)
Expand All @@ -413,6 +487,22 @@ open class EdwardsCurve(private val d: BigInteger) : EcEqEquation {

class TwistedEdwardsCurve(d: BigInteger) : EdwardsCurve(d) {
override val sign = -1

// based on https://www.rfc-editor.org/rfc/rfc8032.html
override fun checkYSquare(x: BigInteger, mod: BigInteger): Boolean {
val modCreator = ModularBigInteger.creatorForModulo(mod)
val xm = modCreator.fromBigInteger(x)
val xsq = xm.pow(2)

val u = xsq - 1
val v = xsq * modCreator.fromBigInteger(d) + 1

val y = u * v.pow(3) * (u * v.pow(7)).pow((mod-5)/8)

val r = v * y.pow(2)

return r == u || r == -u
}
}

interface EcExpression {
Expand Down
36 changes: 36 additions & 0 deletions src/commonTest/kotlin/EcTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,40 @@ class EcTests {
check("Curve448" in guesses) { "compressed curve448 points should be detected as curve448: ${it.hex()}" }
}
}

@Test
fun compressedDetectionEd25519() {
// putting more keys in here results in a test timeout so let's keep this slim
val pubkeys = listOf(
// test vectors from RFC8032 https://www.rfc-editor.org/rfc/rfc8032.html
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".fromHex().reversedArray(),
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c".fromHex().reversedArray(),
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025".fromHex().reversedArray(),
"278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e".fromHex().reversedArray(),
"ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e23f".fromHex().reversedArray() // changed last 0xbf to 0x3f to remove encoded sign bit

).shuffled().subList(0, 2)

pubkeys.forEach {
val guesses = ECCurves.whichCurves(it, includeCompressed = true, includeUncompressed = false, errorOnInvalidSize = true)
check("Ed25519" in guesses) { "compressed Ed25519 points should be detected as Ed25519: ${it.hex()}" }
}
}

@Test
fun compressedDetectionEd448() {
// putting more keys in here results in a test timeout so let's keep this slim
val pubkeys = listOf(
// test vectors from RFC8032 https://www.rfc-editor.org/rfc/rfc8032.html
"5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778edf124769b46c7061bd6783df1e50f6cd1fa1abeafe82561".fromHex().reversedArray(),
"43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a94".fromHex().reversedArray(),
"dcea9e78f35a1bf3499a831b10b86c90aac01cd84b67a0109b55a36e9328b1e365fce161d71ce7131a543ea4cb5f7e9f1d8b006964470014".fromHex().reversedArray(),
"3ba16da0c6f2cc1f30187740756f5e798d6bc5fc015d7c63cc9510ee3fd44adc24d8e968b6e46e6f94d19b945361726bd75e149ef09817f5".fromHex().reversedArray(),
).shuffled().subList(0, 2)

pubkeys.forEach {
val guesses = ECCurves.whichCurves(it, includeCompressed = true, includeUncompressed = false, errorOnInvalidSize = true)
check("Ed448-Goldilocks" in guesses) { "compressed Ed448 points should be detected as Ed448: ${it.hex()}" }
}
}
}

0 comments on commit b9e077a

Please sign in to comment.