Track point compression in LazyECPoint, rather than ECPoint.

The reason is BouncyCastle 1.64 removed point compression tracking.
This commit is contained in:
Peter Dettman 2019-10-22 21:03:02 +07:00 committed by Andreas Schildbach
parent 7eb9c73655
commit 7629677103
9 changed files with 62 additions and 75 deletions

View file

@ -63,9 +63,6 @@ import java.util.Objects;
import static com.google.common.base.Preconditions.*;
// TODO: Move this class to tracking compression state itself.
// The Bouncy Castle developers are deprecating their own tracking of the compression state.
/**
* <p>Represents an elliptic curve public and (optionally) private key, usable for digital signatures but not encryption.
* Creating a new ECKey with the empty constructor will generate a new random keypair. Other static methods can be used
@ -179,12 +176,12 @@ public class ECKey implements EncryptableItem {
ECPrivateKeyParameters privParams = (ECPrivateKeyParameters) keypair.getPrivate();
ECPublicKeyParameters pubParams = (ECPublicKeyParameters) keypair.getPublic();
priv = privParams.getD();
pub = new LazyECPoint(CURVE.getCurve(), pubParams.getQ().getEncoded(true));
pub = getPointWithCompression(pubParams.getQ(), true);
creationTimeSeconds = Utils.currentTimeSeconds();
}
protected ECKey(@Nullable BigInteger priv, ECPoint pub) {
this(priv, new LazyECPoint(checkNotNull(pub)));
protected ECKey(@Nullable BigInteger priv, ECPoint pub, boolean compressed) {
this(priv, getPointWithCompression(checkNotNull(pub), compressed));
}
protected ECKey(@Nullable BigInteger priv, LazyECPoint pub) {
@ -204,33 +201,20 @@ public class ECKey implements EncryptableItem {
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed.
* See the ECKey class docs for a discussion of point compression.
*/
public static ECPoint compressPoint(ECPoint point) {
return getPointWithCompression(point, true);
}
public static LazyECPoint compressPoint(LazyECPoint point) {
return point.isCompressed() ? point : new LazyECPoint(compressPoint(point.get()));
return point.isCompressed() ? point : getPointWithCompression(point.get(), true);
}
/**
* Utility for decompressing an elliptic curve point. Returns the same point if it's already compressed.
* Utility for decompressing an elliptic curve point. Returns the same point if it's already uncompressed.
* See the ECKey class docs for a discussion of point compression.
*/
public static ECPoint decompressPoint(ECPoint point) {
return getPointWithCompression(point, false);
}
public static LazyECPoint decompressPoint(LazyECPoint point) {
return !point.isCompressed() ? point : new LazyECPoint(decompressPoint(point.get()));
return !point.isCompressed() ? point : getPointWithCompression(point.get(), false);
}
private static ECPoint getPointWithCompression(ECPoint point, boolean compressed) {
if (point.isCompressed() == compressed)
return point;
point = point.normalize();
BigInteger x = point.getAffineXCoord().toBigInteger();
BigInteger y = point.getAffineYCoord().toBigInteger();
return CURVE.getCurve().createPoint(x, y, compressed);
private static LazyECPoint getPointWithCompression(ECPoint point, boolean compressed) {
return new LazyECPoint(point, compressed);
}
/**
@ -250,8 +234,8 @@ public class ECKey implements EncryptableItem {
}
/**
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow), either
* compressed or not.
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow).
* @param compressed Determines whether the resulting ECKey will use a compressed encoding for the public key.
*/
public static ECKey fromPrivate(BigInteger privKey, boolean compressed) {
ECPoint point = publicPointFromPrivate(privKey);
@ -267,8 +251,8 @@ public class ECKey implements EncryptableItem {
}
/**
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow), either
* compressed or not.
* Creates an ECKey given the private key only. The public key is calculated from it (this is slow).
* @param compressed Determines whether the resulting ECKey will use a compressed encoding for the public key.
*/
public static ECKey fromPrivate(byte[] privKeyBytes, boolean compressed) {
return fromPrivate(new BigInteger(1, privKeyBytes), compressed);
@ -277,10 +261,11 @@ public class ECKey implements EncryptableItem {
/**
* Creates an ECKey that simply trusts the caller to ensure that point is really the result of multiplying the
* generator point by the private key. This is used to speed things up when you know you have the right values
* already. The compression state of pub will be preserved.
* already.
* @param compressed Determines whether the resulting ECKey will use a compressed encoding for the public key.
*/
public static ECKey fromPrivateAndPrecalculatedPublic(BigInteger priv, ECPoint pub) {
return new ECKey(priv, pub);
public static ECKey fromPrivateAndPrecalculatedPublic(BigInteger priv, ECPoint pub, boolean compressed) {
return new ECKey(priv, pub, compressed);
}
/**
@ -291,15 +276,15 @@ public class ECKey implements EncryptableItem {
public static ECKey fromPrivateAndPrecalculatedPublic(byte[] priv, byte[] pub) {
checkNotNull(priv);
checkNotNull(pub);
return new ECKey(new BigInteger(1, priv), CURVE.getCurve().decodePoint(pub));
return new ECKey(new BigInteger(1, priv), new LazyECPoint(CURVE.getCurve(), pub));
}
/**
* Creates an ECKey that cannot be used for signing, only verifying signatures, from the given point. The
* compression state of pub will be preserved.
* Creates an ECKey that cannot be used for signing, only verifying signatures, from the given point.
* @param compressed Determines whether the resulting ECKey will use a compressed encoding for the public key.
*/
public static ECKey fromPublicOnly(ECPoint pub) {
return new ECKey(null, pub);
public static ECKey fromPublicOnly(ECPoint pub, boolean compressed) {
return new ECKey(null, pub, compressed);
}
/**
@ -307,7 +292,11 @@ public class ECKey implements EncryptableItem {
* The compression state of pub will be preserved.
*/
public static ECKey fromPublicOnly(byte[] pub) {
return new ECKey(null, CURVE.getCurve().decodePoint(pub));
return new ECKey(null, new LazyECPoint(CURVE.getCurve(), pub));
}
public static ECKey fromPublicOnly(ECKey key) {
return fromPublicOnly(key.getPubKeyPoint(), key.isCompressed());
}
/**
@ -318,7 +307,7 @@ public class ECKey implements EncryptableItem {
if (!pub.isCompressed())
return this;
else
return new ECKey(priv, decompressPoint(pub.get()));
return new ECKey(priv, getPointWithCompression(pub.get(), false));
}
/**
@ -373,8 +362,7 @@ public class ECKey implements EncryptableItem {
if (pubKey == null) {
// Derive public from private.
ECPoint point = publicPointFromPrivate(privKey);
point = getPointWithCompression(point, compressed);
this.pub = new LazyECPoint(point);
this.pub = getPointWithCompression(point, compressed);
} else {
// We expect the pubkey to be in regular encoded form, just as a BigInteger. Therefore the first byte is
// a special marker byte.
@ -853,7 +841,7 @@ public class ECKey implements EncryptableItem {
// Now sanity check to ensure the pubkey bytes match the privkey.
boolean compressed = isPubKeyCompressed(pubbits);
ECKey key = new ECKey(privkey, null, compressed);
ECKey key = new ECKey(privkey, (byte[]) null, compressed);
if (!Arrays.equals(key.getPubKey(), pubbits))
throw new IllegalArgumentException("Public key in ASN.1 structure does not match private key.");
return key;
@ -1036,7 +1024,7 @@ public class ECKey implements EncryptableItem {
BigInteger srInv = rInv.multiply(sig.s).mod(n);
BigInteger eInvrInv = rInv.multiply(eInv).mod(n);
ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv);
return ECKey.fromPublicOnly(q.getEncoded(compressed));
return ECKey.fromPublicOnly(q, compressed);
}
/** Decompress a compressed public key (x co-ord and low-bit of y-coord). */
@ -1121,9 +1109,7 @@ public class ECKey implements EncryptableItem {
if (unencryptedPrivateKey.length != 32)
throw new KeyCrypterException.InvalidCipherText(
"Decrypted key must be 32 bytes long, but is " + unencryptedPrivateKey.length);
ECKey key = ECKey.fromPrivate(unencryptedPrivateKey);
if (!isCompressed())
key = key.decompress();
ECKey key = ECKey.fromPrivate(unencryptedPrivateKey, isCompressed());
if (!Arrays.equals(key.getPubKey(), getPubKey()))
throw new KeyCrypterException("Provided AES key is wrong");
key.setCreationTimeSeconds(creationTimeSeconds);

View file

@ -79,9 +79,10 @@ public class DeterministicKey extends ECKey {
public DeterministicKey(List<ChildNumber> childNumberPath,
byte[] chainCode,
ECPoint publicAsPoint,
boolean compressed,
@Nullable BigInteger priv,
@Nullable DeterministicKey parent) {
this(childNumberPath, chainCode, new LazyECPoint(publicAsPoint), priv, parent);
this(childNumberPath, chainCode, new LazyECPoint(publicAsPoint, compressed), priv, parent);
}
/** Constructs a key from its components. This is not normally something you should use. */
@ -89,7 +90,7 @@ public class DeterministicKey extends ECKey {
byte[] chainCode,
BigInteger priv,
@Nullable DeterministicKey parent) {
super(priv, compressPoint(ECKey.publicPointFromPrivate(priv)));
super(priv, ECKey.publicPointFromPrivate(priv), true);
checkArgument(chainCode.length == 32);
this.parent = parent;
this.childNumberPath = HDPath.M(checkNotNull(childNumberPath));
@ -157,7 +158,7 @@ public class DeterministicKey extends ECKey {
@Nullable DeterministicKey parent,
int depth,
int parentFingerprint) {
super(priv, compressPoint(ECKey.publicPointFromPrivate(priv)));
super(priv, ECKey.publicPointFromPrivate(priv), true);
checkArgument(chainCode.length == 32);
this.parent = parent;
this.childNumberPath = HDPath.M(checkNotNull(childNumberPath));
@ -169,7 +170,7 @@ public class DeterministicKey extends ECKey {
/** Clones the key */
public DeterministicKey(DeterministicKey keyToClone, DeterministicKey newParent) {
super(keyToClone.priv, keyToClone.pub.get());
super(keyToClone.priv, keyToClone.pub.get(), keyToClone.pub.isCompressed());
this.parent = newParent;
this.childNumberPath = keyToClone.childNumberPath;
this.chainCode = keyToClone.chainCode;

View file

@ -37,6 +37,7 @@ public class LazyECPoint {
private final ECCurve curve;
private final byte[] bits;
private final boolean compressed;
// This field is effectively final - once set it won't change again. However it can be set after
// construction.
@ -46,10 +47,12 @@ public class LazyECPoint {
public LazyECPoint(ECCurve curve, byte[] bits) {
this.curve = curve;
this.bits = bits;
this.compressed = ECKey.isPubKeyCompressed(bits);
}
public LazyECPoint(ECPoint point) {
this.point = checkNotNull(point);
public LazyECPoint(ECPoint point, boolean compressed) {
this.point = checkNotNull(point).normalize();
this.compressed = compressed;
this.curve = null;
this.bits = null;
}
@ -60,17 +63,17 @@ public class LazyECPoint {
return point;
}
// Delegated methods.
public ECPoint getDetachedPoint() {
return get().getDetachedPoint();
}
public byte[] getEncoded() {
if (bits != null)
return Arrays.copyOf(bits, bits.length);
else
return get().getEncoded();
return get().getEncoded(compressed);
}
// Delegated methods.
public ECPoint getDetachedPoint() {
return get().getDetachedPoint();
}
public boolean isInfinity() {
@ -94,10 +97,7 @@ public class LazyECPoint {
}
public boolean isCompressed() {
if (bits != null)
return ECKey.isPubKeyCompressed(bits);
else
return get().isCompressed();
return compressed;
}
public ECPoint multiply(BigInteger k) {

View file

@ -79,7 +79,7 @@ public class BloomFilterTest {
KeyChainGroup group = KeyChainGroup.builder(MAINNET).build();
// Add a random key which happens to have been used in a recent generation
group.importKeys(ECKey.fromPublicOnly(privKey.getKey().getPubKeyPoint()), ECKey.fromPublicOnly(HEX.decode("03cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99")));
group.importKeys(ECKey.fromPublicOnly(privKey.getKey()), ECKey.fromPublicOnly(HEX.decode("03cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99")));
Wallet wallet = new Wallet(MAINNET, group);
wallet.commitTx(new Transaction(MAINNET, HEX.decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d038754030114062f503253482fffffffff01c05e559500000000232103cb219f69f1b49468bd563239a86667e74a06fcba69ac50a08a5cbc42a5808e99ac00000000")));

View file

@ -152,7 +152,7 @@ public class ECKeyTest {
// Now re-encode and decode the ASN.1 to see if it is equivalent (it does not produce the exact same byte
// sequence, some integers are padded now).
ECKey roundtripKey =
ECKey.fromPrivateAndPrecalculatedPublic(decodedKey.getPrivKey(), decodedKey.getPubKeyPoint());
ECKey.fromPrivateAndPrecalculatedPublic(decodedKey.getPrivKey(), decodedKey.getPubKeyPoint(), decodedKey.isCompressed());
for (ECKey key : new ECKey[] {decodedKey, roundtripKey}) {
byte[] message = reverseBytes(HEX.decode(
@ -239,7 +239,7 @@ public class ECKeyTest {
String message = "Hello World!";
Sha256Hash hash = Sha256Hash.of(message.getBytes());
ECKey.ECDSASignature sig = key.sign(hash);
key = ECKey.fromPublicOnly(key.getPubKeyPoint());
key = ECKey.fromPublicOnly(key);
List<Byte> possibleRecIds = Lists.newArrayList((byte) 0, (byte) 1, (byte) 2, (byte) 3);
byte recId = key.findRecoveryId(hash, sig);
@ -255,13 +255,13 @@ public class ECKeyTest {
String message = "Maarten Bodewes generated this test vector on 2016-11-08";
Sha256Hash hash = Sha256Hash.of(message.getBytes());
ECKey.ECDSASignature sig = key.sign(hash);
key = ECKey.fromPublicOnly(key.getPubKeyPoint());
key = ECKey.fromPublicOnly(key);
byte recId = key.findRecoveryId(hash, sig);
byte expectedRecId = 0;
assertEquals(recId, expectedRecId);
ECKey pubKey = ECKey.fromPublicOnly(key.getPubKeyPoint());
ECKey pubKey = ECKey.fromPublicOnly(key);
ECKey recoveredKey = ECKey.recoverFromSignature(recId, sig, hash, true);
assertEquals(recoveredKey, pubKey);
}
@ -274,7 +274,7 @@ public class ECKeyTest {
ECKey.ECDSASignature sig = key.sign(hash);
byte recId = key.findRecoveryId(hash, sig);
ECKey pubKey = ECKey.fromPublicOnly(key.getPubKeyPoint());
ECKey pubKey = ECKey.fromPublicOnly(key);
ECKey recoveredKey = ECKey.recoverFromSignature(recId, sig, hash, true);
assertEquals(recoveredKey, pubKey);
}
@ -285,7 +285,7 @@ public class ECKeyTest {
String message = "Hello World!";
Sha256Hash hash = Sha256Hash.of(message.getBytes());
ECKey.ECDSASignature sig = key.sign(hash);
key = ECKey.fromPublicOnly(key.getPubKeyPoint());
key = ECKey.fromPublicOnly(key);
boolean found = false;
for (int i = 0; i < 4; i++) {
ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true);
@ -384,7 +384,7 @@ public class ECKeyTest {
String message = "Goodbye Jupiter!";
Sha256Hash hash = Sha256Hash.of(message.getBytes());
ECKey.ECDSASignature sig = encryptedKey.sign(hash, aesKey);
unencryptedKey = ECKey.fromPublicOnly(unencryptedKey.getPubKeyPoint());
unencryptedKey = ECKey.fromPublicOnly(unencryptedKey);
boolean found = false;
for (int i = 0; i < 4; i++) {
ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true);
@ -520,7 +520,7 @@ public class ECKeyTest {
@Test
public void testPublicKeysAreEqual() {
ECKey key = new ECKey();
ECKey pubKey1 = ECKey.fromPublicOnly(key.getPubKeyPoint());
ECKey pubKey1 = ECKey.fromPublicOnly(key);
assertTrue(pubKey1.isCompressed());
ECKey pubKey2 = pubKey1.decompress();
assertEquals(pubKey1, pubKey2);

View file

@ -91,7 +91,7 @@ public class ScriptTest {
Script script = ScriptBuilder.createMultiSigOutputScript(3, keys);
assertTrue(ScriptPattern.isSentToMultisig(script));
List<ECKey> pubkeys = new ArrayList<>(3);
for (ECKey key : keys) pubkeys.add(ECKey.fromPublicOnly(key.getPubKeyPoint()));
for (ECKey key : keys) pubkeys.add(ECKey.fromPublicOnly(key));
assertEquals(script.getPubKeys(), pubkeys);
assertFalse(ScriptPattern.isSentToMultisig(ScriptBuilder.createP2PKOutputScript(new ECKey())));
try {

View file

@ -233,7 +233,7 @@ public class BasicKeyChainTest {
@Test
public void watching() throws UnreadableWalletException {
ECKey key1 = new ECKey();
ECKey pub = ECKey.fromPublicOnly(key1.getPubKeyPoint());
ECKey pub = ECKey.fromPublicOnly(key1);
chain.importKeys(pub);
assertEquals(1, chain.numKeys());
List<Protos.Key> keys = chain.serializeToProtobuf();

View file

@ -649,7 +649,7 @@ public class KeyChainGroupTest {
"xpub69bjfJ91ikC5ghsqsVDHNq2dRGaV2HHVx7Y9LXi27LN9BWWAXPTQr4u8U3wAtap8bLdHdkqPpAcZmhMS5SnrMQC4ccaoBccFhh315P4UYzo",
MAINNET)).outputScriptType(Script.ScriptType.P2PKH).build())
.build();
final ECKey watchingKey = ECKey.fromPublicOnly(new ECKey().getPubKeyPoint());
final ECKey watchingKey = ECKey.fromPublicOnly(new ECKey());
group.importKeys(watchingKey);
assertTrue(group.isWatching());
}

View file

@ -3197,7 +3197,7 @@ public class WalletTest extends TestWithWallet {
public void completeTxPartiallySigned(Wallet.MissingSigsMode missSigMode, byte[] expectedSig) throws Exception {
// Check the wallet will write dummy scriptSigs for inputs that we have only pubkeys for without the privkey.
ECKey priv = new ECKey();
ECKey pub = ECKey.fromPublicOnly(priv.getPubKeyPoint());
ECKey pub = ECKey.fromPublicOnly(priv);
wallet.importKey(pub);
ECKey priv2 = wallet.freshReceiveKey();
// Send three transactions, with one being an address type and the other being a raw CHECKSIG type pubkey only,