Add low R signing (#1342)

* Added signing with entropy to the JNI and implemented low R signing

* Added check for determinism of low R signing

* Cleaned up test

* Implemented signing with entropy in Bouncy Castle

* Added docs for signing with entropy

* Fixed things after rebase

* De-parallelized signLowR and added scaladoc warnings to signWithEntropy
This commit is contained in:
Nadav Kohen 2020-07-29 14:03:18 -06:00 committed by GitHub
parent 51d35e24e9
commit 666d53d94a
12 changed files with 425 additions and 30 deletions

View File

@ -249,6 +249,12 @@ sealed abstract class ExtPrivateKey
key.signFunction
}
override def signWithEntropyFunction: (
ByteVector,
ByteVector) => Future[ECDigitalSignature] = {
key.signWithEntropyFunction
}
/** Signs the given bytes with the given [[BIP32Path path]] */
override def deriveAndSignFuture: (
ByteVector,

View File

@ -138,7 +138,9 @@ object TxUtil {
case (inputInfo, index) =>
val mockSigners = inputInfo.pubKeys.take(inputInfo.requiredSigs).map {
pubKey =>
Sign(_ => Future.successful(DummyECDigitalSignature), pubKey)
Sign(_ => Future.successful(DummyECDigitalSignature),
(_, _) => Future.successful(DummyECDigitalSignature),
pubKey)
}
val mockSpendingInfo =

View File

@ -90,6 +90,19 @@ class BouncyCastleSecp256k1Test extends BitcoinSUnitTest {
}
}
it must "compute signatures with entropy the same" in {
forAll(CryptoGenerators.privateKey,
NumberGenerator.bytevector(32),
NumberGenerator.bytevector(32)) {
case (privKey, bytes, entropy) =>
assert(
privKey.signWithEntropy(bytes,
entropy,
context = BouncyCastle) == privKey
.signWithEntropy(bytes, entropy, context = LibSecp256k1))
}
}
it must "verify signatures the same" in {
forAll(CryptoGenerators.privateKey,
NumberGenerator.bytevector(32),

View File

@ -1,35 +1,56 @@
package org.bitcoins.crypto
import org.bitcoins.testkit.core.gen.CryptoGenerators
import org.bitcoins.testkit.util.BitcoinSUnitTest
import scodec.bits.ByteVector
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import scala.concurrent.{ExecutionContext, Future}
class SignTest extends BitcoinSAsyncTest {
class SignTest extends BitcoinSUnitTest {
implicit val ec = ExecutionContext.global
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
//ECPrivateKey implements the sign interface
//so just use it for testing purposes
val signTestImpl = new Sign {
private val key = ECPrivateKey.freshPrivateKey
override def signFunction: ByteVector => Future[ECDigitalSignature] = {
key.signFunction
}
override def publicKey: ECPublicKey = key.publicKey
}
val privKey: Sign = ECPrivateKey.freshPrivateKey
val pubKey: ECPublicKey = privKey.publicKey
behavior of "Sign"
it must "sign arbitrary pieces of data correctly" in {
forAll(CryptoGenerators.sha256Digest) {
case hash: Sha256Digest =>
val pubKey = signTestImpl.publicKey
val sigF = signTestImpl.signFunction(hash.bytes)
forAllAsync(CryptoGenerators.sha256Digest) { hash =>
val sigF = privKey.signFunction(hash.bytes)
sigF.map(sig => assert(pubKey.verify(hash.hex, sig)))
sigF.map { sig =>
assert(pubKey.verify(hash.bytes, sig))
}
}
}
it must "sign arbitrary pieces of data with arbitrary entropy correctly" in {
forAllAsync(CryptoGenerators.sha256Digest, CryptoGenerators.sha256Digest) {
case (hash, entropy) =>
val sigF = privKey.signWithEntropyFunction(hash.bytes, entropy.bytes)
sigF.map { sig =>
assert(pubKey.verify(hash.bytes, sig))
}
}
}
it must "sign arbitrary data correctly with low R values" in {
forAllAsync(CryptoGenerators.sha256Digest) { hash =>
val bytes = hash.bytes
for {
sig1 <- privKey.signLowRFuture(bytes)
sig2 <- privKey.signLowRFuture(bytes) // Check for determinism
} yield {
assert(pubKey.verify(bytes, sig1))
assert(
sig1.bytes.length <= 70
) // This assertion fails if Low R is not used
assert(sig1.bytes == sig2.bytes)
assert(sig1 == sig2)
}
}
}
}

View File

@ -83,6 +83,39 @@ object BouncyCastleUtil {
signatureLowS
}
/** Create an ECDSA signature adding specified entropy.
*
* This can be used to include your own entropy to nonce generation
* in addition to the message and private key, while still doing so deterministically.
*
* In particular, this is used when generating low R signatures.
* @see [[https://github.com/bitcoin/bitcoin/pull/13666/]]
*/
def signWithEntropy(
dataToSign: ByteVector,
privateKey: ECPrivateKey,
entropy: ByteVector): ECDigitalSignature = {
val signer: ECDSASigner = new ECDSASigner(
new HMacDSAKCalculatorWithEntropy(new SHA256Digest(), entropy))
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,

View File

@ -71,6 +71,47 @@ sealed abstract class ECPrivateKey
ec: ExecutionContext): Future[ECDigitalSignature] =
Future(sign(hash))
override def signWithEntropy(
bytes: ByteVector,
entropy: ByteVector): ECDigitalSignature = {
signWithEntropy(bytes, entropy, CryptoContext.default)
}
def signWithEntropy(
bytes: ByteVector,
entropy: ByteVector,
context: CryptoContext): ECDigitalSignature = {
context match {
case CryptoContext.LibSecp256k1 => signWithEntropyWithSecp(bytes, entropy)
case CryptoContext.BouncyCastle =>
signWithEntropyWithBouncyCastle(bytes, entropy)
}
}
def signWithEntropyWithSecp(
bytes: ByteVector,
entropy: ByteVector): ECDigitalSignature = {
val sigBytes = NativeSecp256k1.signWithEntropy(bytes.toArray,
this.bytes.toArray,
entropy.toArray)
ECDigitalSignature(ByteVector(sigBytes))
}
def signWithEntropyWithBouncyCastle(
bytes: ByteVector,
entropy: ByteVector): ECDigitalSignature = {
BouncyCastleUtil.signWithEntropy(bytes, this, entropy)
}
override def signWithEntropyFunction: (
ByteVector,
ByteVector) => Future[ECDigitalSignature] = {
case (bytes, entropy) =>
import scala.concurrent.ExecutionContext.Implicits.global
Future(signWithEntropy(bytes, entropy))
}
def schnorrSign(dataToSign: ByteVector): SchnorrDigitalSignature = {
val auxRand = ECPrivateKey.freshPrivateKey.bytes
schnorrSign(dataToSign, auxRand)

View File

@ -0,0 +1,138 @@
package org.bitcoins.crypto
import java.math.BigInteger
import java.security.SecureRandom
import org.bouncycastle.crypto.Digest
import org.bouncycastle.crypto.macs.HMac
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.signers.DSAKCalculator
import org.bouncycastle.util.{Arrays, BigIntegers}
import scodec.bits.ByteVector
/** Entirely copied from [[org.bouncycastle.crypto.signers.HMacDSAKCalculator HMacDSAKCalculator]]
* with an added entropy parameter as well as two lines added adding the entropy to the hash.
*
* For a reference in secp256k1, see nonce_function_rfc6979 in secp256k1.c
* For a description of the altered part, see RFC 6979 section 3.2d
* here [[https://tools.ietf.org/html/rfc6979#section-3.2]]
*
* The added lines are marked below with comments.
*/
class HMacDSAKCalculatorWithEntropy(digest: Digest, entropy: ByteVector)
extends DSAKCalculator {
require(entropy.length == 32, "Entropy must be 32 bytes")
private val ZERO = BigInteger.valueOf(0)
private val hMac = new HMac(digest)
private val K = new Array[Byte](hMac.getMacSize)
private val V = new Array[Byte](hMac.getMacSize)
private var n = CryptoParams.curve.getN
override def isDeterministic = true
override def init(n: BigInteger, random: SecureRandom): Unit = {
throw new IllegalStateException("Operation not supported")
}
override def init(
n: BigInteger,
d: BigInteger,
message: Array[Byte]): Unit = {
this.n = n
Arrays.fill(V, 0x01.toByte)
Arrays.fill(K, 0.toByte)
val x = new Array[Byte]((n.bitLength + 7) / 8)
val dVal = BigIntegers.asUnsignedByteArray(d)
System.arraycopy(dVal, 0, x, x.length - dVal.length, dVal.length)
val m = new Array[Byte]((n.bitLength + 7) / 8)
var mInt = bitsToInt(message)
if (mInt.compareTo(n) >= 0)
mInt = mInt.subtract(n)
val mVal = BigIntegers.asUnsignedByteArray(mInt)
System.arraycopy(mVal, 0, m, m.length - mVal.length, mVal.length)
hMac.init(new KeyParameter(K))
hMac.update(V, 0, V.length)
hMac.update(0x00.toByte)
hMac.update(x, 0, x.length)
hMac.update(m, 0, m.length)
hMac.update(entropy.toArray, 0, 32) // Added entropy
hMac.doFinal(K, 0)
hMac.init(new KeyParameter(K))
hMac.update(V, 0, V.length)
hMac.doFinal(V, 0)
hMac.update(V, 0, V.length)
hMac.update(0x01.toByte)
hMac.update(x, 0, x.length)
hMac.update(m, 0, m.length)
hMac.update(entropy.toArray, 0, 32) // Added entropy
hMac.doFinal(K, 0)
hMac.init(new KeyParameter(K))
hMac.update(V, 0, V.length)
hMac.doFinal(V, 0)
()
}
override def nextK(): BigInteger = {
val t = new Array[Byte]((n.bitLength + 7) / 8)
while (true) {
var tOff = 0
while (tOff < t.length) {
hMac.update(V, 0, V.length)
hMac.doFinal(V, 0)
val len = Math.min(t.length - tOff, V.length)
System.arraycopy(V, 0, t, tOff, len)
tOff += len
}
val k = bitsToInt(t)
if (k.compareTo(ZERO) > 0 && k.compareTo(n) < 0)
return k
hMac.update(V, 0, V.length)
hMac.update(0x00.toByte)
hMac.doFinal(K, 0)
hMac.init(new KeyParameter(K))
hMac.update(V, 0, V.length)
hMac.doFinal(V, 0)
}
throw new IllegalStateException("Return was supposed to happen above.")
}
private def bitsToInt(t: Array[Byte]) = {
var v = new BigInteger(1, t)
if (t.length * 8 > n.bitLength) v = v.shiftRight(t.length * 8 - n.bitLength)
v
}
}

View File

@ -3,7 +3,7 @@ package org.bitcoins.crypto
import scodec.bits.ByteVector
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, Future}
import scala.concurrent.{Await, ExecutionContext, Future}
/**
* This is meant to be an abstraction for a [[org.bitcoins.crypto.ECPrivateKey]], sometimes we will not
@ -24,10 +24,69 @@ trait Sign {
def signFuture(bytes: ByteVector): Future[ECDigitalSignature] =
signFunction(bytes)
/** Note that using this function to generate digital signatures with specific
* properties (by trying a bunch of entropy values) can reduce privacy as it will
* fingerprint your wallet. Additionally it could lead to a loss of entropy in
* the resulting nonce should the property you are interested in cause a constraint
* on the input space.
*
* In short, ALL USES OF THIS FUNCTION THAT SIGN THE SAME DATA WITH DIFFERENT ENTROPY
* HAVE THE POTENTIAL TO CAUSE REDUCTIONS IN SECURITY AND PRIVACY, BEWARE!
*/
def signWithEntropyFunction: (
ByteVector,
ByteVector) => Future[ECDigitalSignature]
/** Note that using this function to generate digital signatures with specific
* properties (by trying a bunch of entropy values) can reduce privacy as it will
* fingerprint your wallet. Additionally it could lead to a loss of entropy in
* the resulting nonce should the property you are interested in cause a constraint
* on the input space.
*
* In short, ALL USES OF THIS FUNCTION THAT SIGN THE SAME DATA WITH DIFFERENT ENTROPY
* HAVE THE POTENTIAL TO CAUSE REDUCTIONS IN SECURITY AND PRIVACY, BEWARE!
*/
def signWithEntropyFuture(
bytes: ByteVector,
entropy: ByteVector): Future[ECDigitalSignature] =
signWithEntropyFunction(bytes, entropy)
private def signLowRFuture(bytes: ByteVector, startAt: Long)(implicit
ec: ExecutionContext): Future[ECDigitalSignature] = {
val startBytes = ByteVector.fromLong(startAt).padLeft(32)
val sigF: Future[ECDigitalSignature] =
signWithEntropyFunction(bytes, startBytes)
sigF.flatMap { sig =>
if (sig.bytes.length <= 70) {
Future.successful(sig)
} else {
signLowRFuture(bytes, startAt + 1)
}
}
}
def signLowRFuture(bytes: ByteVector)(implicit
ec: ExecutionContext): Future[ECDigitalSignature] = {
signLowRFuture(bytes, startAt = 0)
}
def sign(bytes: ByteVector): ECDigitalSignature = {
Await.result(signFuture(bytes), 30.seconds)
}
def signLowR(bytes: ByteVector)(implicit
ec: ExecutionContext): ECDigitalSignature = {
Await.result(signLowRFuture(bytes), 30.seconds)
}
def signWithEntropy(
bytes: ByteVector,
entropy: ByteVector): ECDigitalSignature = {
Await.result(signWithEntropyFuture(bytes, entropy), 30.seconds)
}
def publicKey: ECPublicKey
}
@ -35,17 +94,25 @@ object Sign {
private case class SignImpl(
signFunction: ByteVector => Future[ECDigitalSignature],
signWithEntropyFunction: (
ByteVector,
ByteVector) => Future[ECDigitalSignature],
publicKey: ECPublicKey)
extends Sign
def apply(
signFunction: ByteVector => Future[ECDigitalSignature],
signWithEntropyFunction: (
ByteVector,
ByteVector) => Future[ECDigitalSignature],
pubKey: ECPublicKey): Sign = {
SignImpl(signFunction, pubKey)
SignImpl(signFunction, signWithEntropyFunction, pubKey)
}
def constant(sig: ECDigitalSignature, pubKey: ECPublicKey): Sign = {
SignImpl(_ => Future.successful(sig), pubKey)
SignImpl(_ => Future.successful(sig),
(_, _) => Future.successful(sig),
pubKey)
}
/**
@ -57,9 +124,6 @@ object Sign {
* a specific private key on another server
*/
def dummySign(publicKey: ECPublicKey): Sign = {
SignImpl({ _: ByteVector =>
Future.successful(EmptyDigitalSignature)
},
publicKey)
constant(EmptyDigitalSignature, publicKey)
}
}

@ -1 +1 @@
Subproject commit e0239255f1f386cf76279c008253b70cd17fe466
Subproject commit c9bab11ef05ef0cdc0cb0f0e02d3c8e052799767

View File

@ -113,6 +113,52 @@ public class NativeSecp256k1 {
return retVal == 0 ? new byte[0] : sigArr;
}
/**
* libsecp256k1 Create an ECDSA signature adding specified entropy.
*
* This can be used to include your own entropy to nonce generation
* in addition to the message and private key, while still doing so deterministically.
*
* In particular, this is used when generating low R signatures.
* See https://github.com/bitcoin/bitcoin/pull/13666/
*
* @param data Message hash, 32 bytes
* @param seckey ECDSA Secret key, 32 bytes
* @param entropy 32 bytes of entropy
* @return sig byte array of signature
*/
public static byte[] signWithEntropy(byte[] data, byte[] seckey, byte[] entropy) throws AssertFailException{
checkArgument(data.length == 32 && seckey.length == 32 && entropy.length == 32);
ByteBuffer byteBuff = nativeECDSABuffer.get();
if (byteBuff == null || byteBuff.capacity() < 32 + 32 + 32) {
byteBuff = ByteBuffer.allocateDirect(32 + 32 + 32);
byteBuff.order(ByteOrder.nativeOrder());
nativeECDSABuffer.set(byteBuff);
}
byteBuff.rewind();
byteBuff.put(data);
byteBuff.put(seckey);
byteBuff.put(entropy);
byte[][] retByteArray;
r.lock();
try {
retByteArray = secp256k1_ecdsa_sign_with_entropy(byteBuff, Secp256k1Context.getContext());
} finally {
r.unlock();
}
byte[] sigArr = retByteArray[0];
int sigLen = new BigInteger(new byte[] { retByteArray[1][0] }).intValue();
int retVal = new BigInteger(new byte[] { retByteArray[1][1] }).intValue();
assertEquals(sigArr.length, sigLen, "Got bad signature length.");
return retVal == 0 ? new byte[0] : sigArr;
}
/**
* libsecp256k1 Seckey Verify - Verifies an ECDSA secret key
*
@ -526,6 +572,8 @@ public class NativeSecp256k1 {
private static native byte[][] secp256k1_ecdsa_sign(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_ecdsa_sign_with_entropy(ByteBuffer byteBuff, long context);
private static native int secp256k1_ec_seckey_verify(ByteBuffer byteBuff, long context);
private static native byte[][] secp256k1_ec_pubkey_create(ByteBuffer byteBuff, long context, boolean compressed);

View File

@ -86,8 +86,8 @@ public class NativeSecp256k1Test {
}
/**
* This tests sign() for a valid secretkey
*/
* This tests sign() for a valid secretkey
*/
@Test
public void testSignPos() throws AssertFailException{
@ -112,6 +112,35 @@ public class NativeSecp256k1Test {
assertEquals( sigString, "" , "testSignNeg");
}
/**
* This tests signWithEntropy() for a valid secretkey
*/
@Test
public void testSignWithEntropyPos() throws AssertFailException{
byte[] data = toByteArray("53702647283D86B3D6410ADEF184EC608372CC3DD8B9202795D731EB1EA54275");
byte[] sec = toByteArray("B4F62DE42D38D5D24B66FF01761C3FD0A6E7C8B719E0DC54D168FA013BFAF97F");
byte[] entropy = toByteArray("EDF312C904B610B11442320FFB94C4F976831051A481A17176CE2B81EB3A8B6F");
byte[] resultArr = NativeSecp256k1.signWithEntropy(data, sec, entropy);
String sigString = toHex(resultArr);
assertEquals( sigString, "30450221009D9714BE0CE9A3FD08497125C6D01362FDE2FF118FC817FDB14EE4C38CADFB7A022033B082E161F7D75ABC25642ED71226049DC59EC14AB19DF2A8EFEA47A6C75FAC" , "testSignWithEntropyPos");
}
/**
* This tests signWithEntropy() for a invalid secretkey
*/
@Test
public void testSignWithEntropyNeg() throws AssertFailException{
byte[] data = toByteArray("CF80CD8AED482D5D1527D7DC72FCEFF84E6326592848447D2DC0B0E87DFC9A90"); //sha256hash of "testing"
byte[] sec = toByteArray("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
byte[] entropy = toByteArray("EDF312C904B610B11442320FFB94C4F976831051A481A17176CE2B81EB3A8B6F");
byte[] resultArr = NativeSecp256k1.signWithEntropy(data, sec, entropy);
String sigString = toHex(resultArr);
assertEquals( sigString, "" , "testSignWithEntropyNeg");
}
/**
* This tests private key tweak-add
*/