Skip to content

Commit 5654e99

Browse files
committed
ETCM-167: Testing the SigAlg methods.
1 parent 103619e commit 5654e99

File tree

14 files changed

+210
-18
lines changed

14 files changed

+210
-18
lines changed

src/main/scala/io/iohk/ethereum/crypto/ECDSASignature.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ object ECDSASignature {
1515
val RLength = 32
1616
val VLength = 1
1717
val EncodedLength: Int = RLength + SLength + VLength
18+
1819
//byte value that indicates that bytes representing ECC point are in uncompressed format, and should be decoded properly
19-
val uncompressedIndicator: Byte = 0x04
20+
val UncompressedIndicator: Byte = 0x04
21+
val CompressedEvenIndicator: Byte = 0x02
22+
val CompressedOddIndicator: Byte = 0x03
2023

2124
//only naming convention
2225
val negativePointSign: Byte = 27
@@ -158,7 +161,7 @@ case class ECDSASignature(r: BigInt, s: BigInt, v: Byte) {
158161
import ECDSASignature.RLength
159162

160163
def bigInt2Bytes(b: BigInt) =
161-
ByteUtils.padLeft(ByteString(b.toByteArray).takeRight(RLength), RLength, 0)
164+
ByteString(ByteUtils.bigIntToBytes(b, RLength))
162165

163166
bigInt2Bytes(r) ++ bigInt2Bytes(s) :+ v
164167
}

src/main/scala/io/iohk/ethereum/crypto/package.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
44
import java.security.SecureRandom
55

66
import akka.util.ByteString
7+
import io.iohk.ethereum.utils.ByteUtils
78
import org.bouncycastle.asn1.sec.SECNamedCurves
89
import org.bouncycastle.asn1.x9.X9ECParameters
910
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
@@ -62,9 +63,11 @@ package object crypto {
6263
bytes
6364
}
6465

65-
/** @return (privateKey, publicKey) pair */
66+
/** @return (privateKey, publicKey) pair.
67+
* The public key will be uncompressed and have its prefix dropped.
68+
*/
6669
def keyPairToByteArrays(keyPair: AsymmetricCipherKeyPair): (Array[Byte], Array[Byte]) = {
67-
val prvKey = keyPair.getPrivate.asInstanceOf[ECPrivateKeyParameters].getD.toByteArray
70+
val prvKey = ByteUtils.bigIntegerToBytes(keyPair.getPrivate.asInstanceOf[ECPrivateKeyParameters].getD, 32)
6871
val pubKey = keyPair.getPublic.asInstanceOf[ECPublicKeyParameters].getQ.getEncoded(false).tail
6972
(prvKey, pubKey)
7073
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package io.iohk.ethereum.network.discovery.crypto
2+
3+
import io.iohk.ethereum.crypto
4+
import io.iohk.ethereum.crypto.ECDSASignature
5+
import io.iohk.ethereum.nodebuilder.SecureRandomBuilder
6+
import io.iohk.scalanet.discovery.crypto.{SigAlg, PublicKey, PrivateKey, Signature}
7+
import scodec.bits.BitVector
8+
import scodec.Attempt
9+
import scodec.bits.BitVector
10+
import akka.util.ByteString
11+
12+
object Secp256k1SigAlg extends SigAlg with SecureRandomBuilder {
13+
override val name = "secp256k1"
14+
15+
override val PrivateKeyBytesSize = 32
16+
17+
// A Secp256k1 public key is 32 bytes compressed or 64 bytes uncompressed,
18+
// with a 1 byte prefix showing which version it is.
19+
// See https://davidederosa.com/basic-blockchain-programming/elliptic-curve-keys
20+
//
21+
// However in the discovery v4 protocol the prefix is omitted.
22+
override val PublicKeyBytesSize = 64
23+
24+
// A normal Secp256k1 signature consists of 2 bigints `r` and `s` followed by a recovery ID `v`,
25+
// but it can be just 64 bytes if that's omitted, like in the ENR.
26+
override val SignatureBytesSize = 65
27+
28+
val SignatureWithoutRecoveryBytesSize = 64
29+
val PublicKeyCompressedBytesSize = 33
30+
31+
override def newKeyPair: (PublicKey, PrivateKey) = {
32+
val keyPair = crypto.generateKeyPair(secureRandom)
33+
val (privateKeyBytes, publicKeyBytes) = crypto.keyPairToByteArrays(keyPair)
34+
35+
val publicKey = toPublicKey(publicKeyBytes)
36+
val privateKey = toPrivateKey(privateKeyBytes)
37+
38+
publicKey -> privateKey
39+
}
40+
41+
override def sign(privateKey: PrivateKey, data: BitVector): Signature = {
42+
val keyPair = crypto.keyPairFromPrvKey(privateKey.toByteArray)
43+
val sig = ECDSASignature.sign(data.toByteArray, keyPair)
44+
toSignature(sig)
45+
}
46+
47+
// ENR wants the signature without recovery ID, just 64 bytes.
48+
// The Packet on the other hand has the full 65 bytes.
49+
override def removeRecoveryId(signature: Signature): Signature = {
50+
signature.size / 8 match {
51+
case SignatureBytesSize =>
52+
Signature(signature.dropRight(8))
53+
case SignatureWithoutRecoveryBytesSize =>
54+
signature
55+
case other =>
56+
throw new IllegalArgumentException(s"Unexpected signature size: $other bytes")
57+
}
58+
}
59+
60+
override def compressPublicKey(publicKey: PublicKey): PublicKey = {
61+
publicKey.size / 8 match {
62+
case PublicKeyBytesSize =>
63+
// This is a public key without the prefix, it consists of an x and y bigint.
64+
// To compress we drop y, and the first byte becomes 02 for even values of y and 03 for odd values.
65+
val (xbs, ybs) = publicKey.splitAt(publicKey.length / 2)
66+
val y = BigInt(1, ybs.toByteArray)
67+
val prefix: Byte =
68+
if (y.mod(2) == 0) ECDSASignature.CompressedEvenIndicator
69+
else ECDSASignature.CompressedOddIndicator
70+
val compressed = PublicKey(BitVector(prefix) ++ xbs)
71+
assert(compressed.size == PublicKeyCompressedBytesSize * 8)
72+
compressed
73+
74+
case PublicKeyCompressedBytesSize =>
75+
publicKey
76+
77+
case other =>
78+
throw new IllegalArgumentException(s"Unexpected public key size: $other bytes")
79+
}
80+
}
81+
82+
override def verify(publicKey: PublicKey, signature: Signature, data: BitVector): Boolean = ???
83+
84+
override def recoverPublicKey(signature: Signature, data: BitVector): Attempt[PublicKey] = ???
85+
86+
override def toPublicKey(privateKey: PrivateKey): PublicKey = {
87+
val publicKeyBytes = crypto.pubKeyFromPrvKey(privateKey.toByteArray)
88+
toPublicKey(publicKeyBytes)
89+
}
90+
91+
private def toPublicKey(publicKeyBytes: Array[Byte]): PublicKey = {
92+
// Discovery uses 64 byte keys, without the prefix.
93+
val publicKey = PublicKey(BitVector(publicKeyBytes))
94+
assert(publicKey.size == PublicKeyBytesSize * 8, s"Unexpected public key size: ${publicKey.size / 8} bytes")
95+
publicKey
96+
}
97+
98+
private def toPrivateKey(privateKeyBytes: Array[Byte]): PrivateKey = {
99+
val privateKey = PrivateKey(BitVector(privateKeyBytes))
100+
assert(privateKey.size == PrivateKeyBytesSize * 8, s"Unexpected private key size: ${privateKey.size / 8} bytes")
101+
privateKey
102+
}
103+
104+
private def toSignature(sig: ECDSASignature): Signature = {
105+
val signatureBytes = sig.toBytes.toArray[Byte]
106+
assert(signatureBytes.size == SignatureBytesSize)
107+
108+
// Apparently the `v` has to adjusted by 27.
109+
val adjusted = signatureBytes.take(SignatureBytesSize - 1) :+ (signatureBytes.last - 27).toByte
110+
111+
Signature(BitVector(adjusted))
112+
}
113+
114+
private def toECDSASignature(signature: Signature): ECDSASignature = {
115+
signature.size / 8 match {
116+
case SignatureBytesSize =>
117+
// Undo the adjustment of `v`.
118+
val signatureBytes = signature.toByteArray
119+
val unadjusted = signatureBytes.take(SignatureBytesSize - 1) :+ (signatureBytes.last + 27).toByte
120+
121+
ECDSASignature.fromBytes(ByteString(unadjusted)) getOrElse {
122+
throw new IllegalArgumentException(s"Could not convert to ECDSA signature.")
123+
}
124+
125+
case SignatureWithoutRecoveryBytesSize =>
126+
???
127+
128+
case other =>
129+
throw new IllegalArgumentException(s"Unexpected signature size: $other bytes")
130+
}
131+
}
132+
}

src/main/scala/io/iohk/ethereum/network/package.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.iohk.ethereum
33
import java.io.{File, PrintWriter}
44
import java.net.{Inet6Address, InetAddress}
55
import java.security.SecureRandom
6-
76
import io.iohk.ethereum.crypto._
87
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
98
import org.bouncycastle.crypto.params.ECPublicKeyParameters
@@ -26,7 +25,7 @@ package object network {
2625
}
2726

2827
def publicKeyFromNodeId(nodeId: String): ECPoint = {
29-
val bytes = ECDSASignature.uncompressedIndicator +: Hex.decode(nodeId)
28+
val bytes = ECDSASignature.UncompressedIndicator +: Hex.decode(nodeId)
3029
curve.getCurve.decodePoint(bytes)
3130
}
3231

src/main/scala/io/iohk/ethereum/network/rlpx/AuthHandshaker.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ case class AuthHandshaker(
161161

162162
val signaturePubBytes = signature.publicKey(signed).get
163163

164-
curve.getCurve.decodePoint(ECDSASignature.uncompressedIndicator +: signaturePubBytes)
164+
curve.getCurve.decodePoint(ECDSASignature.UncompressedIndicator +: signaturePubBytes)
165165
}
166166

167167
private def createAuthInitiateMessageV4(remotePubKey: ECPoint) = {

src/main/scala/io/iohk/ethereum/network/rlpx/AuthInitiateMessage.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ object AuthInitiateMessage extends AuthInitiateEcdsaCodec {
2121
signature = decodeECDSA(input.take(ECDSASignature.EncodedLength)),
2222
ephemeralPublicHash = ByteString(input.slice(ECDSASignature.EncodedLength, publicKeyIndex)),
2323
publicKey =
24-
curve.getCurve.decodePoint(ECDSASignature.uncompressedIndicator +: input.slice(publicKeyIndex, nonceIndex)),
24+
curve.getCurve.decodePoint(ECDSASignature.UncompressedIndicator +: input.slice(publicKeyIndex, nonceIndex)),
2525
nonce = ByteString(input.slice(nonceIndex, knownPeerIndex)),
2626
knownPeer = input(knownPeerIndex) == 1
2727
)

src/main/scala/io/iohk/ethereum/network/rlpx/AuthInitiateMessageV4.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ object AuthInitiateMessageV4 extends AuthInitiateEcdsaCodec {
2222
case RLPList(signatureBytes, publicKeyBytes, nonce, version, _*) =>
2323
val signature = decodeECDSA(signatureBytes)
2424
val publicKey =
25-
curve.getCurve.decodePoint(ECDSASignature.uncompressedIndicator +: (publicKeyBytes: Array[Byte]))
25+
curve.getCurve.decodePoint(ECDSASignature.UncompressedIndicator +: (publicKeyBytes: Array[Byte]))
2626
AuthInitiateMessageV4(signature, publicKey, ByteString(nonce: Array[Byte]), version)
2727
case _ => throw new RuntimeException("Cannot decode auth initiate message")
2828
}

src/main/scala/io/iohk/ethereum/network/rlpx/AuthResponseMessage.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ object AuthResponseMessage {
1515
def decode(input: Array[Byte]): AuthResponseMessage = {
1616
AuthResponseMessage(
1717
ephemeralPublicKey =
18-
curve.getCurve.decodePoint(ECDSASignature.uncompressedIndicator +: input.take(PublicKeyLength)),
18+
curve.getCurve.decodePoint(ECDSASignature.UncompressedIndicator +: input.take(PublicKeyLength)),
1919
nonce = ByteString(input.slice(PublicKeyLength, PublicKeyLength + NonceLength)),
2020
knownPeer = input(PublicKeyLength + NonceLength) == 1
2121
)

src/main/scala/io/iohk/ethereum/network/rlpx/AuthResponseMessageV4.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ object AuthResponseMessageV4 {
1919
override def decode(rlp: RLPEncodeable): AuthResponseMessageV4 = rlp match {
2020
case RLPList(ephemeralPublicKeyBytes, nonce, version, _*) =>
2121
val ephemeralPublicKey =
22-
curve.getCurve.decodePoint(ECDSASignature.uncompressedIndicator +: (ephemeralPublicKeyBytes: Array[Byte]))
22+
curve.getCurve.decodePoint(ECDSASignature.UncompressedIndicator +: (ephemeralPublicKeyBytes: Array[Byte]))
2323
AuthResponseMessageV4(ephemeralPublicKey, ByteString(nonce: Array[Byte]), version)
2424
case _ => throw new RuntimeException("Cannot decode auth response message")
2525
}

src/main/scala/io/iohk/ethereum/rlp/RLP.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private[rlp] object RLP {
175175
case 3 => ((bytes(0) & 0xff) << 16) + ((bytes(1) & 0xff) << 8) + (bytes(2) & 0xff)
176176
case Integer.BYTES =>
177177
((bytes(0) & 0xff) << 24) + ((bytes(1) & 0xff) << 16) + ((bytes(2) & 0xff) << 8) + (bytes(3) & 0xff)
178-
case _ => throw new RLPException("Bytes don't represent an int")
178+
case _ => throw RLPException("Bytes don't represent an int")
179179
}
180180
}
181181

@@ -197,7 +197,7 @@ private[rlp] object RLP {
197197
val binaryLength: Array[Byte] = intToBytesNoLeadZeroes(length)
198198
(binaryLength.length + offset + SizeThreshold - 1).toByte +: binaryLength
199199
} else if (length < MaxItemLength && length <= 0xff) Array((1 + offset + SizeThreshold - 1).toByte, length.toByte)
200-
else throw new RLPException("Input too long")
200+
else throw RLPException("Input too long")
201201
}
202202

203203
/**
@@ -209,7 +209,7 @@ private[rlp] object RLP {
209209
* @see [[io.iohk.ethereum.rlp.ItemBounds]]
210210
*/
211211
private[rlp] def getItemBounds(data: Array[Byte], pos: Int): ItemBounds = {
212-
if (data.isEmpty) throw new RLPException("Empty Data")
212+
if (data.isEmpty) throw RLPException("Empty Data")
213213
else {
214214
val prefix: Int = data(pos) & 0xff
215215
if (prefix == OffsetShortItem) {
@@ -239,7 +239,7 @@ private[rlp] object RLP {
239239
}
240240

241241
private def decodeWithPos(data: Array[Byte], pos: Int): (RLPEncodeable, Int) =
242-
if (data.isEmpty) throw new RLPException("data is too short")
242+
if (data.isEmpty) throw RLPException("data is too short")
243243
else {
244244
getItemBounds(data, pos) match {
245245
case ItemBounds(start, end, false, isEmpty) =>

src/main/scala/io/iohk/ethereum/utils/ByteUtils.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ object ByteUtils {
3434
bytes
3535
}
3636

37+
def bigIntToBytes(b: BigInt, numBytes: Int): Array[Byte] =
38+
bigIntegerToBytes(b.bigInteger, numBytes)
39+
3740
def toBigInt(bytes: ByteString): BigInt =
3841
bytes.foldLeft(BigInt(0)) { (n, b) => (n << 8) + (b & 0xff) }
3942

src/test/scala/io/iohk/ethereum/crypto/ECDSASignatureSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class ECDSASignatureSpec extends AnyFlatSpec with Matchers with ScalaCheckProper
4545
val recPubKey = signature.publicKey(msg)
4646

4747
val result = recPubKey
48-
.map(a => ECDSASignature.uncompressedIndicator +: a)
48+
.map(a => ECDSASignature.UncompressedIndicator +: a)
4949
.map(curve.getCurve.decodePoint)
5050
.map(_.getEncoded(true))
5151
.map(ByteString(_))

src/test/scala/io/iohk/ethereum/network/discovery/codecs/RLPCodecsSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.iohk.ethereum.network.discovery.codecs
22

3-
import org.scalatest.matchers.should.Matchers._
3+
import org.scalatest.matchers.should.Matchers
44
import org.scalatest.flatspec.AnyFlatSpec
55
import io.iohk.scalanet.discovery.ethereum.Node
66
import io.iohk.scalanet.discovery.ethereum.v4.{Packet, Payload}
@@ -13,7 +13,7 @@ import io.iohk.ethereum.rlp.RLPValue
1313
import io.iohk.ethereum.rlp.RLPDecoder
1414
import org.scalatest.compatible.Assertion
1515

16-
class RLPCodecsSpec extends AnyFlatSpec {
16+
class RLPCodecsSpec extends AnyFlatSpec with Matchers {
1717

1818
import RLPCodecs._
1919

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.iohk.ethereum.network.discovery.crypto
2+
3+
import org.scalatest.flatspec.AnyFlatSpec
4+
import org.scalatest.matchers.should.Matchers
5+
import scodec.bits.BitVector
6+
import scala.util.Random
7+
import io.iohk.ethereum.crypto
8+
import io.iohk.ethereum.nodebuilder.SecureRandomBuilder
9+
10+
class Secp256k1SigAlgSpec extends AnyFlatSpec with Matchers with SecureRandomBuilder {
11+
behavior of "Secp256k1SigAlg"
12+
13+
def randomData: BitVector = {
14+
val size = Random.nextInt(1000)
15+
val bytes = Array.ofDim[Byte](size)
16+
Random.nextBytes(bytes)
17+
BitVector(bytes)
18+
}
19+
20+
it should "generate new keypairs" in {
21+
val (publicKey, privateKey) = Secp256k1SigAlg.newKeyPair
22+
publicKey.toByteVector should have size 64
23+
privateKey.toByteVector should have size 32
24+
}
25+
26+
it should "compress a public key" in {
27+
val (publicKey, _) = Secp256k1SigAlg.newKeyPair
28+
val compressedPublicKey = Secp256k1SigAlg.compressPublicKey(publicKey)
29+
compressedPublicKey.toByteVector should have size 33
30+
31+
Secp256k1SigAlg.compressPublicKey(compressedPublicKey) shouldBe compressedPublicKey
32+
}
33+
34+
it should "turn a private key into a public key" in {
35+
val (publicKey, privateKey) = Secp256k1SigAlg.newKeyPair
36+
Secp256k1SigAlg.toPublicKey(privateKey) shouldBe publicKey
37+
}
38+
39+
it should "sign some data" in {
40+
val (_, privateKey) = Secp256k1SigAlg.newKeyPair
41+
val data = randomData
42+
val signature = Secp256k1SigAlg.sign(privateKey, data)
43+
44+
signature.toByteVector should have size 65
45+
}
46+
47+
it should "sign some data with ECDSA" in {
48+
val keyPair = crypto.generateKeyPair(secureRandom)
49+
val data = randomData
50+
val signature = crypto.ECDSASignature.sign(data.toByteArray, keyPair)
51+
}
52+
}

0 commit comments

Comments
 (0)