Bouncy Castle Fallback (#1232)

* Added Bouncy Castle fallback to all Secp calls in ECKey.scala

* Fixed bugs and made ExtKey support use of BouncyCastle

* An attempt to add bouncy castle testing to CI

* Responded to review

* De-coupled libsecp256k1 uses from Bouncy Castle uses

* Responded to review
This commit is contained in:
Nadav Kohen 2020-03-17 12:20:06 -06:00 committed by GitHub
parent 552135dd68
commit e0b234843d
6 changed files with 371 additions and 91 deletions

View file

@ -0,0 +1,99 @@
package org.bitcoins.core.crypto
import org.bitcoin.{NativeSecp256k1, Secp256k1Context}
import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator}
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.Gen
import org.scalatest.{Outcome, Succeeded}
import scodec.bits.ByteVector
class BouncyCastleSecp256k1Test extends BitcoinSUnitTest {
behavior of "CryptoLibraries"
override def withFixture(test: NoArgTest): Outcome = {
if (Secp256k1Context.isEnabled) {
super.withFixture(test)
} else {
logger.warn(s"Test ${test.name} skipped as Secp256k1 is not available.")
Succeeded
}
}
it must "add private keys the same" in {
forAll(CryptoGenerators.privateKey, CryptoGenerators.privateKey) {
case (priv1, priv2) =>
val sumWithBouncyCastle =
BouncyCastleUtil.addNumbers(priv1.bytes, priv2.bytes)
val sumWithSecp = NativeSecp256k1.privKeyTweakAdd(priv1.bytes.toArray,
priv2.bytes.toArray)
val sumKeyWithBouncyCastle =
ECPrivateKey(ByteVector(sumWithBouncyCastle.toByteArray))
val sumKeyWithSecp = ECPrivateKey(ByteVector(sumWithSecp))
assert(sumKeyWithBouncyCastle == sumKeyWithSecp)
}
}
it must "add public keys the same" in {
forAll(CryptoGenerators.publicKey, CryptoGenerators.privateKey) {
case (pubKey, privKey) =>
val sumKeyBytes =
NativeSecp256k1.pubKeyTweakAdd(pubKey.bytes.toArray,
privKey.bytes.toArray,
true)
val sumKeyExpected = ECPublicKey.fromBytes(ByteVector(sumKeyBytes))
val sumKey = pubKey.addWithBouncyCastle(privKey.publicKey)
assert(sumKey == sumKeyExpected)
}
}
it must "validate keys the same" in {
val keyOrGarbageGen = Gen.oneOf(CryptoGenerators.publicKey.map(_.bytes),
NumberGenerator.bytevector(33))
forAll(keyOrGarbageGen) { bytes =>
assert(
ECPublicKey.isFullyValidWithBouncyCastle(bytes) ==
ECPublicKey.isFullyValidWithSecp(bytes)
)
}
}
it must "decompress keys the same" in {
forAll(CryptoGenerators.publicKey) { pubKey =>
assert(pubKey.decompressedWithBouncyCastle == pubKey.decompressedWithSecp)
}
}
it must "compute public keys the same" in {
forAll(CryptoGenerators.privateKey) { privKey =>
assert(privKey.publicKeyWithBouncyCastle == privKey.publicKeyWithSecp)
}
}
it must "compute signatures the same" in {
forAll(CryptoGenerators.privateKey, NumberGenerator.bytevector(32)) {
case (privKey, bytes) =>
assert(
privKey.signWithBouncyCastle(bytes) == privKey.signWithSecp(bytes))
}
}
it must "verify signatures the same" in {
forAll(CryptoGenerators.privateKey,
NumberGenerator.bytevector(32),
CryptoGenerators.digitalSignature) {
case (privKey, bytes, badSig) =>
val sig = privKey.sign(bytes)
val pubKey = privKey.publicKey
assert(
pubKey.verifyWithBouncyCastle(bytes, sig) == pubKey
.verifyWithSecp(bytes, sig))
assert(
pubKey.verifyWithBouncyCastle(bytes, badSig) == pubKey
.verifyWithSecp(bytes, badSig))
}
}
}

View file

@ -1,10 +1,12 @@
package org.bitcoins.core.crypto
import org.bitcoin.NativeSecp256k1
import org.bitcoin.{NativeSecp256k1, Secp256k1Context}
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.util.BitcoinSUnitTest
import scodec.bits._
import scala.concurrent.ExecutionContext
class ECPublicKeyTest extends BitcoinSUnitTest {
it must "be able to decompress keys" in {
@ -37,16 +39,20 @@ class ECPublicKeyTest extends BitcoinSUnitTest {
}
}
it must "add keys correctly" in {
forAll(CryptoGenerators.publicKey, CryptoGenerators.privateKey) {
case (pubKey, privKey) =>
val sumKeyBytes = NativeSecp256k1.pubKeyTweakAdd(pubKey.bytes.toArray,
privKey.bytes.toArray,
true)
val sumKeyExpected = ECPublicKey.fromBytes(ByteVector(sumKeyBytes))
val sumKey = pubKey.add(privKey.publicKey)
it must "decompress keys correctly" in {
forAll(CryptoGenerators.privateKey) { privKey =>
val pubKey = privKey.publicKey
assert(sumKey == sumKeyExpected)
assert(privKey.isCompressed)
assert(pubKey.isCompressed)
val decompressedPrivKey =
ECPrivateKey(privKey.bytes, isCompressed = false)(
ExecutionContext.global)
val decompressedPubKey = pubKey.decompressed
assert(decompressedPrivKey.publicKey == decompressedPubKey)
assert(pubKey.bytes.tail == decompressedPubKey.bytes.splitAt(33)._1.tail)
}
}
}

View file

@ -0,0 +1,114 @@
package org.bitcoins.core.crypto
import java.math.BigInteger
import org.bitcoins.core.util.BitcoinSUtil
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.params.{
ECPrivateKeyParameters,
ECPublicKeyParameters
}
import org.bouncycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator}
import org.bouncycastle.math.ec.{ECCurve, ECPoint}
import scodec.bits.ByteVector
import scala.util.Try
object BouncyCastleUtil {
private val curve: ECCurve = CryptoParams.curve.getCurve
private val G: ECPoint = CryptoParams.curve.getG
private val N: BigInteger = CryptoParams.curve.getN
private def getBigInteger(bytes: ByteVector): BigInteger = {
new BigInteger(1, bytes.toArray)
}
def addNumbers(num1: ByteVector, num2: ByteVector): BigInteger = {
val bigInteger1 = getBigInteger(num1)
val bigInteger2 = getBigInteger(num2)
bigInteger1.add(bigInteger2).mod(N)
}
def decodePoint(bytes: ByteVector): ECPoint = {
curve.decodePoint(bytes.toArray)
}
def validatePublicKey(bytes: ByteVector): Boolean = {
Try(decodePoint(bytes))
.map(_.getCurve == curve)
.getOrElse(false)
}
def decompressPublicKey(publicKey: ECPublicKey): ECPublicKey = {
if (publicKey.isCompressed) {
val point = decodePoint(publicKey.bytes)
val decompressedBytes =
ByteVector.fromHex("04").get ++
ByteVector(point.getXCoord.getEncoded) ++
ByteVector(point.getYCoord.getEncoded)
ECPublicKey(decompressedBytes)
} else publicKey
}
def computePublicKey(privateKey: ECPrivateKey): ECPublicKey = {
val priv = getBigInteger(privateKey.bytes)
val point = G.multiply(priv)
val pubBytes = ByteVector(point.getEncoded(privateKey.isCompressed))
require(
ECPublicKey.isFullyValid(pubBytes),
s"Bouncy Castle failed to generate a valid public key, got: ${BitcoinSUtil
.encodeHex(pubBytes)}")
ECPublicKey(pubBytes)
}
def sign(
dataToSign: ByteVector,
privateKey: ECPrivateKey): ECDigitalSignature = {
val signer: ECDSASigner = new ECDSASigner(
new HMacDSAKCalculator(new SHA256Digest()))
val privKey: ECPrivateKeyParameters =
new ECPrivateKeyParameters(getBigInteger(privateKey.bytes),
CryptoParams.curve)
signer.init(true, privKey)
val components: Array[BigInteger] =
signer.generateSignature(dataToSign.toArray)
val (r, s) = (components(0), components(1))
val signature = ECDigitalSignature(r, s)
//make sure the signature follows BIP62's low-s value
//https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures
//bitcoinj implementation
//https://github.com/bitcoinj/bitcoinj/blob/1e66b9a8e38d9ad425507bf5f34d64c5d3d23bb8/core/src/main/java/org/bitcoinj/core/ECKey.java#L551
val signatureLowS = DERSignatureUtil.lowS(signature)
require(
signatureLowS.isDEREncoded,
"We must create DER encoded signatures when signing a piece of data, got: " + signatureLowS)
signatureLowS
}
def verifyDigitalSignature(
data: ByteVector,
publicKey: ECPublicKey,
signature: ECDigitalSignature): Boolean = {
val resultTry = Try {
val publicKeyParams =
new ECPublicKeyParameters(decodePoint(publicKey.bytes),
CryptoParams.curve)
val signer = new ECDSASigner
signer.init(false, publicKeyParams)
signature match {
case EmptyDigitalSignature =>
signer.verifySignature(data.toArray,
java.math.BigInteger.valueOf(0),
java.math.BigInteger.valueOf(0))
case _: ECDigitalSignature =>
val rBigInteger: BigInteger = new BigInteger(signature.r.toString())
val sBigInteger: BigInteger = new BigInteger(signature.s.toString())
signer.verifySignature(data.toArray, rBigInteger, sBigInteger)
}
}
resultTry.getOrElse(false)
}
}

View file

@ -3,19 +3,16 @@ package org.bitcoins.core.crypto
import java.math.BigInteger
import java.security.SecureRandom
import org.bitcoin.NativeSecp256k1
import org.bitcoin.{NativeSecp256k1, Secp256k1Context}
import org.bitcoins.core.config.{NetworkParameters, Networks}
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.util.{BitcoinSUtil, _}
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.generators.ECKeyPairGenerator
import org.bouncycastle.crypto.params.{
ECKeyGenerationParameters,
ECPrivateKeyParameters,
ECPublicKeyParameters
ECPrivateKeyParameters
}
import org.bouncycastle.crypto.signers.{ECDSASigner, HMacDSAKCalculator}
import org.bouncycastle.math.ec.ECPoint
import scodec.bits.ByteVector
@ -49,53 +46,61 @@ sealed abstract class ECPrivateKey
* @return the digital signature
*/
override def sign(dataToSign: ByteVector): ECDigitalSignature = {
sign(dataToSign, Secp256k1Context.isEnabled)
}
def sign(dataToSign: ByteVector, useSecp: Boolean): ECDigitalSignature = {
require(dataToSign.length == 32 && bytes.length <= 32)
if (useSecp) {
signWithSecp(dataToSign)
} else {
signWithBouncyCastle(dataToSign)
}
}
def signWithSecp(dataToSign: ByteVector): ECDigitalSignature = {
val signature =
NativeSecp256k1.sign(dataToSign.toArray, bytes.toArray)
ECDigitalSignature(ByteVector(signature))
}
def signWithBouncyCastle(dataToSign: ByteVector): ECDigitalSignature = {
BouncyCastleUtil.sign(dataToSign, this)
}
def sign(hash: HashDigest): ECDigitalSignature = sign(hash.bytes)
def signFuture(hash: HashDigest)(
implicit ec: ExecutionContext): Future[ECDigitalSignature] =
Future(sign(hash))
def signWithBouncyCastle(dataToSign: ByteVector): ECDigitalSignature = {
val signer: ECDSASigner = new ECDSASigner(
new HMacDSAKCalculator(new SHA256Digest()))
val privKey: ECPrivateKeyParameters = new ECPrivateKeyParameters(
new BigInteger(1, bytes.toArray),
CryptoParams.curve)
signer.init(true, privKey)
val components: Array[BigInteger] =
signer.generateSignature(dataToSign.toArray)
val (r, s) = (components(0), components(1))
val signature = ECDigitalSignature(r, s)
//make sure the signature follows BIP62's low-s value
//https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#Low_S_values_in_signatures
//bitcoinj implementation
//https://github.com/bitcoinj/bitcoinj/blob/1e66b9a8e38d9ad425507bf5f34d64c5d3d23bb8/core/src/main/java/org/bitcoinj/core/ECKey.java#L551
val signatureLowS = DERSignatureUtil.lowS(signature)
require(
signatureLowS.isDEREncoded,
"We must create DER encoded signatures when signing a piece of data, got: " + signatureLowS)
signatureLowS
}
/** Signifies if the this private key corresponds to a compressed public key */
def isCompressed: Boolean
override def publicKey: ECPublicKey = publicKey(Secp256k1Context.isEnabled)
/** Derives the public for a the private key */
def publicKey: ECPublicKey = {
def publicKey(useSecp: Boolean): ECPublicKey = {
if (useSecp) {
publicKeyWithSecp
} else {
publicKeyWithBouncyCastle
}
}
def publicKeyWithSecp: ECPublicKey = {
val pubKeyBytes: Array[Byte] =
NativeSecp256k1.computePubkey(bytes.toArray, isCompressed)
val pubBytes = ByteVector(pubKeyBytes)
require(
NativeSecp256k1.isValidPubKey(pubKeyBytes),
ECPublicKey.isFullyValid(pubBytes),
s"secp256k1 failed to generate a valid public key, got: ${BitcoinSUtil
.encodeHex(ByteVector(pubKeyBytes))}"
)
ECPublicKey(ByteVector(pubKeyBytes))
.encodeHex(pubBytes)}")
ECPublicKey(pubBytes)
}
def publicKeyWithBouncyCastle: ECPublicKey = {
BouncyCastleUtil.computePublicKey(this)
}
/**
@ -124,8 +129,14 @@ object ECPrivateKey extends Factory[ECPrivateKey] {
isCompressed: Boolean,
ec: ExecutionContext)
extends ECPrivateKey {
require(NativeSecp256k1.secKeyVerify(bytes.toArray),
s"Invalid key according to secp256k1, hex: ${bytes.toHex}")
if (Secp256k1Context.isEnabled) {
require(NativeSecp256k1.secKeyVerify(bytes.toArray),
s"Invalid key according to secp256k1, hex: ${bytes.toHex}")
} else {
require(CryptoParams.curve.getCurve
.isValidFieldElement(new BigInteger(1, bytes.toArray)),
s"Invalid key according to Bouncy Castle, hex: ${bytes.toHex}")
}
}
def apply(bytes: ByteVector, isCompressed: Boolean)(
@ -274,9 +285,28 @@ sealed abstract class ECPublicKey extends BaseECKey {
* [[org.bitcoins.core.crypto.ECPrivateKey ECPrivateKey]]'s corresponding
* [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]]. */
def verify(data: ByteVector, signature: ECDigitalSignature): Boolean = {
val result = NativeSecp256k1.verify(data.toArray,
signature.bytes.toArray,
bytes.toArray)
verify(data, signature, Secp256k1Context.isEnabled)
}
def verify(
data: ByteVector,
signature: ECDigitalSignature,
useSecp: Boolean): Boolean = {
if (useSecp) {
verifyWithSecp(data, signature)
} else {
verifyWithBouncyCastle(data, signature)
}
}
def verifyWithSecp(
data: ByteVector,
signature: ECDigitalSignature): Boolean = {
val result =
NativeSecp256k1.verify(data.toArray,
signature.bytes.toArray,
bytes.toArray)
if (!result) {
//if signature verification fails with libsecp256k1 we need to use our old
//verification function from spongy castle, this is needed because early blockchain
@ -284,68 +314,56 @@ sealed abstract class ECPublicKey extends BaseECKey {
//bitcoin core implements this functionality here:
//https://github.com/bitcoin/bitcoin/blob/master/src/pubkey.cpp#L16-L165
//TODO: Implement functionality in Bitcoin Core linked above
oldVerify(data, signature)
verifyWithBouncyCastle(data, signature)
} else result
}
def verifyWithBouncyCastle(
data: ByteVector,
signature: ECDigitalSignature): Boolean = {
BouncyCastleUtil.verifyDigitalSignature(data, this, signature)
}
def verify(hex: String, signature: ECDigitalSignature): Boolean =
verify(BitcoinSUtil.decodeHex(hex), signature)
override def toString = "ECPublicKey(" + hex + ")"
@deprecated(
"Deprecated in favor of using verify functionality inside of secp256k1",
"2/20/2017")
private def oldVerify(
data: ByteVector,
signature: ECDigitalSignature): Boolean = {
/** The elliptic curve used by bitcoin. */
def curve = CryptoParams.curve
/** This represents this public key in the bouncy castle library */
def publicKeyParams =
new ECPublicKeyParameters(curve.getCurve.decodePoint(bytes.toArray),
curve)
val resultTry = Try {
val signer = new ECDSASigner
signer.init(false, publicKeyParams)
signature match {
case EmptyDigitalSignature =>
signer.verifySignature(data.toArray,
java.math.BigInteger.valueOf(0),
java.math.BigInteger.valueOf(0))
case _: ECDigitalSignature =>
val rBigInteger: BigInteger = new BigInteger(signature.r.toString())
val sBigInteger: BigInteger = new BigInteger(signature.s.toString())
signer.verifySignature(data.toArray, rBigInteger, sBigInteger)
}
}
resultTry.getOrElse(false)
}
/** Checks if the [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]] is compressed */
def isCompressed: Boolean = bytes.size == 33
/** Checks if the [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]] is valid according to secp256k1 */
def isFullyValid = ECPublicKey.isFullyValid(bytes)
def isFullyValid: Boolean = ECPublicKey.isFullyValid(bytes)
/** Returns the decompressed version of this [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]] */
def decompressed: ECPublicKey = {
def decompressed: ECPublicKey = decompressed(Secp256k1Context.isEnabled)
def decompressed(useSecp: Boolean): ECPublicKey = {
if (useSecp) {
decompressedWithSecp
} else {
decompressedWithBouncyCastle
}
}
def decompressedWithSecp: ECPublicKey = {
if (isCompressed) {
val decompressed = NativeSecp256k1.decompress(bytes.toArray)
ECPublicKey.fromBytes(ByteVector(decompressed))
} else this
}
def decompressedWithBouncyCastle: ECPublicKey = {
BouncyCastleUtil.decompressPublicKey(this)
}
/** Decodes a [[org.bitcoins.core.crypto.ECPublicKey ECPublicKey]] in bitcoin-s
* to a [[org.bouncycastle.math.ec.ECPoint ECPoint]] data structure that is internal to the
* bouncy castle library
* @return
*/
def toPoint: ECPoint = {
CryptoParams.curve.getCurve.decodePoint(bytes.toArray)
BouncyCastleUtil.decodePoint(bytes)
}
/** Adds this ECPublicKey to another as points and returns the resulting ECPublicKey.
@ -354,6 +372,10 @@ sealed abstract class ECPublicKey extends BaseECKey {
* get wrapped in NativeSecp256k1 to speed things up.
*/
def add(otherKey: ECPublicKey): ECPublicKey = {
addWithBouncyCastle(otherKey)
}
def addWithBouncyCastle(otherKey: ECPublicKey): ECPublicKey = {
val sumPoint = toPoint.add(otherKey.toPoint)
ECPublicKey.fromPoint(sumPoint)
@ -388,9 +410,26 @@ object ECPublicKey extends Factory[ECPublicKey] {
* Mimics this function in bitcoin core
* [[https://github.com/bitcoin/bitcoin/blob/27765b6403cece54320374b37afb01a0cfe571c3/src/pubkey.cpp#L207-L212]]
*/
def isFullyValid(bytes: ByteVector): Boolean =
def isFullyValid(bytes: ByteVector): Boolean = {
isFullyValid(bytes, Secp256k1Context.isEnabled)
}
def isFullyValid(bytes: ByteVector, useSecp: Boolean): Boolean = {
if (useSecp) {
isFullyValidWithSecp(bytes)
} else {
isFullyValidWithBouncyCastle(bytes)
}
}
def isFullyValidWithSecp(bytes: ByteVector): Boolean = {
Try(NativeSecp256k1.isValidPubKey(bytes.toArray))
.getOrElse(false) && isValid(bytes)
}
def isFullyValidWithBouncyCastle(bytes: ByteVector): Boolean = {
BouncyCastleUtil.validatePublicKey(bytes) && isValid(bytes)
}
/**
* Mimics the CPubKey::IsValid function in Bitcoin core, this is a consensus rule

View file

@ -1,6 +1,6 @@
package org.bitcoins.core.crypto
import org.bitcoin.NativeSecp256k1
import org.bitcoin.{NativeSecp256k1, Secp256k1Context}
import org.bitcoins.core.hd.{BIP32Node, BIP32Path}
import org.bitcoins.core.number.{UInt32, UInt8}
import org.bitcoins.core.protocol.NetworkElement
@ -200,7 +200,12 @@ sealed abstract class ExtPrivateKey
val (il, ir) = hmac.splitAt(32)
//should be ECGroup addition
//parse256(IL) + kpar (mod n)
val tweak = NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray)
val tweak = if (Secp256k1Context.isEnabled) {
NativeSecp256k1.privKeyTweakAdd(il.toArray, key.bytes.toArray)
} else {
val sum = BouncyCastleUtil.addNumbers(key.bytes, il)
sum.toByteArray
}
val childKey = ECPrivateKey(ByteVector(tweak))
val fp = CryptoUtil.sha256Hash160(key.publicKey.bytes).bytes.take(4)
ExtPrivateKey(version, depth + UInt8.one, fp, idx, ChainCode(ir), childKey)
@ -353,10 +358,15 @@ sealed abstract class ExtPublicKey extends ExtKey {
val hmac = CryptoUtil.hmac512(chainCode.bytes, data)
val (il, ir) = hmac.splitAt(32)
val priv = ECPrivateKey(il)
val tweaked = NativeSecp256k1.pubKeyTweakAdd(key.bytes.toArray,
hmac.toArray,
priv.isCompressed)
val childPubKey = ECPublicKey(ByteVector(tweaked))
val childPubKey = if (Secp256k1Context.isEnabled) {
val tweaked = NativeSecp256k1.pubKeyTweakAdd(key.bytes.toArray,
il.toArray,
priv.isCompressed)
ECPublicKey(ByteVector(tweaked))
} else {
val tweak = ECPrivateKey.fromBytes(il).publicKey
key.add(tweak)
}
//we do not handle this case since it is impossible
//In case parse256(IL) n or Ki is the point at infinity, the resulting key is invalid,

View file

@ -45,8 +45,20 @@ public class Secp256k1Context {
context = contextRef;
}
/**
* Detects whether or not the libsecp256k1 binaries were successfully
* loaded in static initialization above. Useful in enabling a fallback
* to Bouncy Castle implementations in the case of having no libsecp present.
*/
public static boolean isEnabled() {
return enabled;
String secpDisabled = System.getenv("DISABLE_SECP256K1");
if (secpDisabled != null &&
(secpDisabled.toLowerCase().equals("true") ||
secpDisabled.equals("1"))) {
return false;
} else {
return enabled;
}
}
public static long getContext() {