[DRAFT] ECKey: when signing, grind for low R values

The goal is to make transaction sizes more predictable, simplifying the
fee calculation. This change will make sure signatures will always have
DER encodings of 70 bytes or less, without counting the sighash flags byte.

Note that one of our tests for a BIP143 native P2WPKH test vector had
to be changed, because that vector had been produced pre-grinding and
hasn't been updated since. However, the change only affects a P2PK input
which isn't really the focus of the test vector.

Also see similar changes in other projects:
https://github.com/bitcoin/bitcoin/pull/13666
https://github.com/rust-bitcoin/rust-secp256k1/pull/259
https://github.com/bitcoin-s/bitcoin-s/pull/1342 + https://github.com/bitcoin-s/bitcoin-s/pull/2408
https://github.com/bisq-network/bisq/pull/7238

TODO extract HMacDSAKCalculatorWithEntrophy to top level class?
TODO make encodeToCompact() its own commit?
TODO more test vectors from other projects
TODO should sigs with DER encodings *less* than 70 bytes also be retried?
This commit is contained in:
Andreas Schildbach 2022-01-26 19:46:20 +01:00 committed by Andreas Schildbach
parent 5edb96cbb8
commit f2701dceee
3 changed files with 112 additions and 6 deletions

View file

@ -46,9 +46,11 @@ import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
@ -69,6 +71,8 @@ import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.SignatureException;
@ -491,6 +495,26 @@ public class ECKey implements EncryptableItem {
}
}
/**
* Check that the sig has a low R component, and will thus be 70 bytes or less in DER encoding (without the
* sighash flags byte).
*
* @return true if sig has a low R component
*/
public boolean hasLowR() {
byte[] compact = ByteUtils.bigIntegerToBytes(r, 32);
return compact[0] >= 0;
}
public byte[] encodeToCompact() {
byte[] compactR = ByteUtils.bigIntegerToBytes(r, 32);
byte[] compactS = ByteUtils.bigIntegerToBytes(s, 32);
byte[] compact = new byte[64];
System.arraycopy(compactR, 0, compact, 32 - compactR.length, compactR.length);
System.arraycopy(compactS, 0, compact, 64 - compactS.length, compactS.length);
return compact;
}
/**
* DER is an international standard for serializing data structures which is widely used in cryptography.
* It's somewhat like protocol buffers but less convenient. This method returns a standard DER encoding
@ -600,11 +624,26 @@ public class ECKey implements EncryptableItem {
protected ECDSASignature doSign(Sha256Hash input, BigInteger privateKeyForSigning) {
Objects.requireNonNull(privateKeyForSigning);
ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
HMacDSAKCalculatorWithEntrophy kCalculator = new HMacDSAKCalculatorWithEntrophy(new SHA256Digest());
ECDSASigner signer = new ECDSASigner(kCalculator);
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, CURVE);
signer.init(true, privKey);
// first try to sign without additional entropy
BigInteger[] components = signer.generateSignature(input.getBytes());
return new ECDSASignature(components[0], components[1]).toCanonicalised();
ECDSASignature signature = new ECDSASignature(components[0], components[1]);
// grind for low R values by adding entropy to the K calculation via RFC 6979 section 3.6.
// see discussion at https://github.com/bitcoin/bitcoin/pull/13666
for (int counter = 1; !signature.hasLowR() && counter < Integer.MAX_VALUE; counter++) {
byte[] entrophy =
ByteBuffer.allocate(32).order(ByteOrder.LITTLE_ENDIAN).putInt(0, counter).array();
kCalculator.setEntropy(entrophy);
components = signer.generateSignature(input.getBytes());
signature = new ECDSASignature(components[0], components[1]);
}
return signature.toCanonicalised();
}
/**
@ -1384,4 +1423,42 @@ public class ECKey implements EncryptableItem {
throw new RuntimeException(e); // Cannot happen.
}
}
/**
* Custom K calculator with ability to add additional entropy to the calculation. This is needed for grinding low
* signature R values. Before calling {@link #setEntropy(byte[])} no entropy is added.
*/
private static class HMacDSAKCalculatorWithEntrophy extends HMacDSAKCalculator {
@Nullable
byte[] entrophy = null;
private HMacDSAKCalculatorWithEntrophy(Digest digest) {
super(digest);
}
/**
* Add 32 bytes of additional entropy to the K calculation via RFC 6979.
*
* @param entropy 32 bytes of entropy
* @see
* <a href="https://www.rfc-editor.org/rfc/rfc6979#section-3.6">RFC 6979 section 3.6. "Additional data…"</a>
*/
public void setEntropy(byte[] entropy) {
Objects.requireNonNull(entropy);
checkArgument(entropy.length == 32, () -> "entropy must be 32 bytes");
this.entrophy = entropy;
}
@Override
protected void initAdditionalInput0(HMac hmac0) {
if (entrophy != null)
hmac0.update(entrophy, 0, 32);
}
@Override
protected void initAdditionalInput1(HMac hmac1) {
if (entrophy != null)
hmac1.update(entrophy, 0, 32);
}
}
}

View file

@ -287,7 +287,7 @@ public class TransactionTest {
@Test
public void testWitnessSignatureP2WPKH() {
// test vector P2WPKH from:
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#native-p2wpkh
String txHex = "01000000" // version
+ "02" // num txIn
+ "fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f" + "00000000" + "00" + "eeffffff" // txIn
@ -319,7 +319,11 @@ public class TransactionTest {
TransactionSignature txSig0 = tx.calculateSignature(0, key0,
scriptPubKey0,
Transaction.SigHash.ALL, false);
assertEquals("30450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01",
assertEquals(
// with grinding for low R values:
"30440220414110cf9b6d81fe3f22a8cc8bf867c1bbe0672ccfb2e8338db9d6f907105b2802201cfee6a5cb8a6239572963de01744ae66dd4a3af5b1413b489c165bf14ce297601",
// without grinding:
// "30450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01",
ByteUtils.formatHex(txSig0.encodeToBitcoin()));
Script witnessScript = ScriptBuilder.createP2PKHOutputScript(key1);
@ -351,7 +355,10 @@ public class TransactionTest {
+ "01" // flag
+ "02" // num txIn
+ "fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f" + "00000000"
+ "494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01"
// with grinding for low R values:
+ "484730440220414110cf9b6d81fe3f22a8cc8bf867c1bbe0672ccfb2e8338db9d6f907105b2802201cfee6a5cb8a6239572963de01744ae66dd4a3af5b1413b489c165bf14ce297601"
// without grinding:
// + "494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01"
+ "eeffffff" // txIn
+ "ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a" + "01000000" + "00" + "ffffffff" // txIn
+ "02" // num txOut
@ -371,7 +378,7 @@ public class TransactionTest {
@Test
public void testWitnessSignatureP2SH_P2WPKH() {
// test vector P2SH-P2WPKH from:
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#p2sh-p2wpkh
String txHex = "01000000" // version
+ "01" // num txIn
+ "db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477" + "01000000" + "00" + "feffffff" // txIn

View file

@ -614,4 +614,26 @@ public class ECKeyTest {
bytes[0] = 42;
ECKey.fromPrivate(bytes);
}
@Test
public void signGrindLowR() {
// test case from https://github.com/rust-bitcoin/rust-secp256k1/pull/259/files
Sha256Hash msg = Sha256Hash.wrap(
ByteUtils.parseHex("887d04bb1cf1b1554f1b268dfe62d13064ca67ae45348d50d1392ce2d13418ac"));
ECKey sk = ECKey.fromPrivate(
ByteUtils.parseHex("57f0148f94d13095cfda539d0da0d1541304b678d8b36e243980aab4e1b7cead"));
ECDSASignature sig = sk.sign(msg); // grinds 5 times
assertEquals(
"047dd4d049db02b430d24c41c7925b2725bcd5a85393513bdec04b4dc363632b1054d0180094122b380f4cfa391e6296244da773173e78fc745c1b9c79f7b713",
ByteUtils.formatHex(sig.encodeToCompact()));
}
@Test
public void signAlwaysProducesMax70ByteDER() {
for (int i = 0; i < 100; i++) {
ECKey sk = new ECKey();
ECDSASignature sig = sk.sign(Sha256Hash.ZERO_HASH);
assertTrue(sig.encodeToDER().length <= 70); // without sighash flags byte
}
}
}