diff --git a/build.gradle b/build.gradle index c12027d85..76a17d435 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ apply plugin: 'java' group = 'network.casper' // Version number update for release -version='2.5.8' +version='2.5.9' sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -49,6 +49,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter:${jupiterVersion}" // Used to compare json strings while testing testImplementation "org.skyscreamer:jsonassert:${jsonassertVersion}" + testImplementation files('assets') } java { diff --git a/script/debug-test b/script/debug-test new file mode 100755 index 000000000..29412781c --- /dev/null +++ b/script/debug-test @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")/.."|| exit 1 +./gradlew --stop +./gradlew cleanTest test -Dorg.gradle.jvmargs='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005' --no-daemon --debug-jvm \ No newline at end of file diff --git a/script/docker-run b/script/docker-run index 0987ca2c9..26584abe2 100644 --- a/script/docker-run +++ b/script/docker-run @@ -1,3 +1,3 @@ #!/usr/bin/env bash # run the cspr-nctl container in docker -docker run --rm -it --name cspr-nctl -d -p 25101:25101 -p 11101:11101 -p 14101:14101 -p 18101:18101 stormeye2000/cspr-nctl:linux-1.5.5 +docker run --rm -it --name cspr-nctl -d -p 25101:25101 -p 11101:11101 -p 14101:14101 -p 18101:18101 stormeye2000/cspr-nctl:release-1.5.5 diff --git a/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java b/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java index f63120c7c..f17b0fe74 100644 --- a/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java +++ b/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java @@ -1,7 +1,6 @@ package com.syntifi.crypto.key; import com.casper.sdk.model.key.AlgorithmTag; -import com.syntifi.crypto.key.encdec.Hex; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.bouncycastle.asn1.*; @@ -98,7 +97,7 @@ public Boolean verify(byte[] message, byte[] signature) { if (recoveredKey != null) { - final byte[] keyFromSignature = getRecoveredShortKey(recoveredKey.toByteArray()); + final byte[] keyFromSignature = getShortKey(recoveredKey.toByteArray()); if (Arrays.equals(keyFromSignature, keyToFind)) { return true; @@ -114,32 +113,25 @@ public Boolean verify(byte[] message, byte[] signature) { /** * Gets a short key - * - * @param key the key as a byte array - * @return short key as byte array - */ - public static byte[] getShortKey(final byte[] key) { - final BigInteger pubKey = new BigInteger(key); - final String pubKeyPrefix = pubKey.testBit(0) ? "03" : "02"; - final byte[] pubKeyBytes = Arrays.copyOfRange(key, 0, (AlgorithmTag.SECP256K1.getLength() - 1)); - return Hex.decode(pubKeyPrefix + Hex.encode(pubKeyBytes)); - } - - /** * There's around a 50% chance the elliptical curve algo will generate a 65 byte * public key instead of 66 byte. * Luckily the algo pads the first byte as zero when this happens - * Determine this and then return the byte array to be shortened + * startBit determines this * * @param key the key as a byte array * @return short key as byte array */ - public static byte[] getRecoveredShortKey(final byte[] key){ - if (key[0] == (byte) 0) { - return getShortKey(Arrays.copyOfRange(key, 1, (key.length - 1))); - } else { - return getShortKey(key); - } + public static byte[] getShortKey(final byte[] key) { + + final int startBit = key[0] == (byte) 0 ? 1 : 0; + + final byte[] shortKey = new byte[AlgorithmTag.SECP256K1.getLength()]; + shortKey[0] = (byte) (new BigInteger(key).testBit(0) ? 3 : 2); + + System.arraycopy(key, startBit, shortKey, 1, AlgorithmTag.SECP256K1.getLength() - 1); + + return shortKey; + } } diff --git a/src/test/java/com/casper/sdk/service/CasperServiceTestsNctl.java b/src/test/java/com/casper/sdk/service/CasperServiceTestsNctl.java index 79996ad54..a66d3fe5e 100644 --- a/src/test/java/com/casper/sdk/service/CasperServiceTestsNctl.java +++ b/src/test/java/com/casper/sdk/service/CasperServiceTestsNctl.java @@ -1,6 +1,8 @@ package com.casper.sdk.service; import com.casper.sdk.exception.CasperInvalidStateException; +import com.casper.sdk.exception.NoSuchTypeException; +import com.casper.sdk.helper.CasperTransferHelper; import com.casper.sdk.identifier.block.HashBlockIdentifier; import com.casper.sdk.identifier.block.HeightBlockIdentifier; import com.casper.sdk.identifier.global.GlobalStateIdentifier; @@ -9,22 +11,29 @@ import com.casper.sdk.identifier.purse.PurseIdentifier; import com.casper.sdk.model.balance.QueryBalanceData; import com.casper.sdk.model.block.JsonBlockData; +import com.casper.sdk.model.common.Ttl; +import com.casper.sdk.model.deploy.Deploy; +import com.casper.sdk.model.deploy.DeployData; +import com.casper.sdk.model.deploy.DeployResult; import com.casper.sdk.model.era.EraInfoData; import com.casper.sdk.model.key.PublicKey; import com.casper.sdk.model.status.ChainspecData; import com.casper.sdk.model.status.StatusData; -import com.syntifi.crypto.key.AbstractPrivateKey; -import com.syntifi.crypto.key.Ed25519PrivateKey; -import org.junit.Ignore; +import com.syntifi.crypto.key.*; +import dev.oak3.sbs4j.exception.ValueSerializationException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; import java.math.BigInteger; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -179,5 +188,86 @@ void getGlobalStateAndReadAsString() { } } + /** + * We're testing the 50% Secp problem here + * When a Secp public key is generated from prime numbers, there's a 50% chance it will 65bit or 66bit + * When it's 65% the long public key is padded with zero + * This test uses a Secp private key that will generate a 65bit padded public key + */ + @Test + void testDeployWithPaddedPublicKey() throws URISyntaxException, IOException, NoSuchTypeException, GeneralSecurityException, ValueSerializationException { + + DeployData deploy; + + //First fund the private-padded.pem account from the faucet account + final Ed25519PrivateKey faucetPrivateKey = new Ed25519PrivateKey(); + final URL faucetKey = getClass().getResource("/net-1/faucet/secret_key.pem"); + assert faucetKey != null; + faucetPrivateKey.readPrivateKey(faucetKey.getFile()); + + Secp256k1PrivateKey paddedPrivateKey = new Secp256k1PrivateKey(); + String filePath = getResourcesKeyPath("secp256k1/private-padded.pem"); + paddedPrivateKey.readPrivateKey(filePath); + + + byte[] paddedPublicKeyFull = paddedPrivateKey.getKeyPair().getPublicKey().toByteArray(); + assert paddedPublicKeyFull[0] == (byte) 0; + + DeployResult deployResult = doDeploy(faucetPrivateKey, paddedPrivateKey.derivePublicKey()); + + assert deployResult != null; + + //wait for deploy to be accepted + do { + deploy = casperServiceNctl.getDeploy(deployResult.getDeployHash()); + } while (deploy.getDeploy().getApprovals() == null || deploy.getDeploy().getApprovals().size() <= 0); + + + //Now transfer from private-padded.pem to another user + Secp256k1PrivateKey toPrivateKey = new Secp256k1PrivateKey(); + filePath = getResourcesKeyPath("secp256k1/secret_key.pem"); + toPrivateKey.readPrivateKey(filePath); + + deployResult = doDeploy(paddedPrivateKey, toPrivateKey.derivePublicKey()); + + assert deployResult != null; + + //wait for deploy to be accepted + do { + deploy = casperServiceNctl.getDeploy(deployResult.getDeployHash()); + } while (deploy.getDeploy().getApprovals() == null || deploy.getDeploy().getApprovals().size() <= 0); + + + assert Arrays.equals(deploy.getDeploy().getApprovals().get(0).getSigner().getPubKey().getKey(), paddedPrivateKey.derivePublicKey().getKey()); + + //now verify signature + + Secp256k1PublicKey paddedPublicKey = (Secp256k1PublicKey) paddedPrivateKey.derivePublicKey(); + + assert paddedPublicKey.verify(deploy.getDeploy().getHash().getDigest(), deploy.getDeploy().getApprovals().get(0).getSignature().getKey()); + + } + + private DeployResult doDeploy(final AbstractPrivateKey sk, final AbstractPublicKey pk) throws NoSuchTypeException, GeneralSecurityException, ValueSerializationException { + + final Deploy deploy = CasperTransferHelper.buildTransferDeploy( + sk, + PublicKey.fromAbstractPublicKey(pk), + BigInteger.valueOf(2500000000L), + "casper-net-1", + Math.abs(new Random().nextLong()), + BigInteger.valueOf(100000000L), + 1L, + Ttl.builder().ttl("30m").build(), + new Date(), + new ArrayList<>()); + + return casperServiceNctl.putDeploy(deploy); + } + + + protected String getResourcesKeyPath(String filename) throws URISyntaxException { + return Paths.get(Objects.requireNonNull(getClass().getClassLoader().getResource(filename)).toURI()).toString(); + } } diff --git a/src/test/java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java b/src/test/java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java index b62e6a0cc..2b0f65b0f 100644 --- a/src/test/java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java +++ b/src/test/java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java @@ -8,7 +8,6 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; -import java.security.GeneralSecurityException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -82,7 +81,7 @@ void sign_should_sign_message() throws URISyntaxException, IOException { } @Test - void create_random_key() throws GeneralSecurityException, IOException { + void create_random_key() { Secp256k1PrivateKey sk = Secp256k1PrivateKey.deriveRandomKey(); Secp256k1PublicKey pk = (Secp256k1PublicKey) sk.derivePublicKey(); LOGGER.info(sk.getKeyPair().getPrivateKey().toString(16)); diff --git a/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java b/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java index a9b1a59fa..eccba87ad 100644 --- a/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java +++ b/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java @@ -63,7 +63,31 @@ void verify_should_be_ok() throws URISyntaxException, IOException { } @Test - void signAndRecoverPublicKey_1() throws URISyntaxException, IOException { + void signAndRecoverPublicKeyWithPaddedPK() throws URISyntaxException, IOException { + + //Get the private key + Secp256k1PrivateKey privKey = new Secp256k1PrivateKey(); + String filePath = getResourcesKeyPath("secp256k1/private-padded.pem"); + privKey.readPrivateKey(filePath); + + //Check that the public key is private-padded.pem with a 0 byte + assert privKey.getKeyPair().getPublicKey().toByteArray()[0] == (byte) 0; + + + //Derive the public key + Secp256k1PublicKey publicKey = (Secp256k1PublicKey) privKey.derivePublicKey(); + + String message = "bc81ca4de9b3a991a6514eddf0e994e0035c7ba58f333c4d7ba5dd18b4c9c547"; + + //Generate the signature + byte[] signature = privKey.sign(message.getBytes()); + + //Test + assert publicKey.verify(message.getBytes(), signature); + + } + @Test + void signAndRecoverPublicKey_1() throws URISyntaxException, IOException { //Get the private key Secp256k1PrivateKey privKey = new Secp256k1PrivateKey(); diff --git a/src/test/resources/secp256k1/private-padded.pem b/src/test/resources/secp256k1/private-padded.pem new file mode 100644 index 000000000..3c034ebb5 --- /dev/null +++ b/src/test/resources/secp256k1/private-padded.pem @@ -0,0 +1,3 @@ +-----BEGIN EC PRIVATE KEY----- +MC4CAQEEIF6d87JG1raI7KNzkaXUbtTezTHE7oziYJ04VGZv4YOdoAcGBSuBBAAK +-----END EC PRIVATE KEY-----