From 89942bda9e6d7978008a73fe03c05b4b6396e502 Mon Sep 17 00:00:00 2001 From: Carl Norburn Date: Wed, 26 Jun 2024 14:00:16 +0100 Subject: [PATCH 1/5] Version bump --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c12027d8..58f1e4e2 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 From 9717acf29e129717cc5f951b1ac46e1bb7c514c5 Mon Sep 17 00:00:00 2001 From: Carl Norburn Date: Thu, 27 Jun 2024 09:48:14 +0100 Subject: [PATCH 2/5] Remove unused exceptions and imports --- .../java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java b/src/test/java/com/syntifi/crypto/key/Secp256k1PrivateKeyTests.java index b62e6a0c..2b0f65b0 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)); From f0f7874340344079a0da0a8bcfdc549a9d6d8d46 Mon Sep 17 00:00:00 2001 From: Carl Norburn Date: Thu, 27 Jun 2024 11:56:22 +0100 Subject: [PATCH 3/5] Fixed the 50% Secp problem when creating a public short key --- .../crypto/key/Secp256k1PublicKey.java | 28 ++++++------------- .../crypto/key/Secp256k1PublicKeyTests.java | 26 ++++++++++++++++- .../resources/secp256k1/private-padded.pem | 3 ++ 3 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 src/test/resources/secp256k1/private-padded.pem diff --git a/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java b/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java index f63120c7..42ab4890 100644 --- a/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java +++ b/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java @@ -98,7 +98,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,6 +114,10 @@ public Boolean verify(byte[] message, byte[] signature) { /** * Gets a short key + * 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 + * startBit determines this * * @param key the key as a byte array * @return short key as byte array @@ -121,25 +125,11 @@ public Boolean verify(byte[] message, byte[] signature) { 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 - * - * @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); - } + final int startBit = key[0] == (byte) 0 ? 1 : 0; + + final byte[] pubKeyBytes = Arrays.copyOfRange(key, startBit, (AlgorithmTag.SECP256K1.getLength() - 1) + startBit); + return Hex.decode(pubKeyPrefix + Hex.encode(pubKeyBytes)); } } diff --git a/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java b/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java index a9b1a59f..b02687d4 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 padded 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 00000000..4dde15e7 --- /dev/null +++ b/src/test/resources/secp256k1/private-padded.pem @@ -0,0 +1,3 @@ +-----BEGIN EC PRIVATE KEY----- +MC4CAQEEIGbGCLXUsZFyrJe4nAGW+V5Zd7z+AbccZ5SU3LBQi/Q6oAcGBSuBBAAK +-----END EC PRIVATE KEY----- From a95d9a9b0d91cfbf8c5532ce73066763fd90a8b8 Mon Sep 17 00:00:00 2001 From: Carl Norburn Date: Fri, 28 Jun 2024 10:19:53 +0100 Subject: [PATCH 4/5] Tests for the 50% Secp problem when creating a public short key --- build.gradle | 1 + script/debug-test | 4 + script/docker-run | 2 +- .../crypto/key/Secp256k1PublicKey.java | 2 - .../sdk/service/CasperServiceTestsNctl.java | 96 ++++++++++++++++++- .../crypto/key/Secp256k1PublicKeyTests.java | 2 +- .../resources/secp256k1/private-padded.pem | 2 +- 7 files changed, 101 insertions(+), 8 deletions(-) create mode 100755 script/debug-test diff --git a/build.gradle b/build.gradle index 58f1e4e2..76a17d43 100644 --- a/build.gradle +++ b/build.gradle @@ -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 00000000..29412781 --- /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 0987ca2c..26584abe 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 42ab4890..9496ebe5 100644 --- a/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java +++ b/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java @@ -125,9 +125,7 @@ public Boolean verify(byte[] message, byte[] signature) { public static byte[] getShortKey(final byte[] key) { final BigInteger pubKey = new BigInteger(key); final String pubKeyPrefix = pubKey.testBit(0) ? "03" : "02"; - final int startBit = key[0] == (byte) 0 ? 1 : 0; - final byte[] pubKeyBytes = Arrays.copyOfRange(key, startBit, (AlgorithmTag.SECP256K1.getLength() - 1) + startBit); return Hex.decode(pubKeyPrefix + Hex.encode(pubKeyBytes)); } diff --git a/src/test/java/com/casper/sdk/service/CasperServiceTestsNctl.java b/src/test/java/com/casper/sdk/service/CasperServiceTestsNctl.java index 79996ad5..a66d3fe5 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/Secp256k1PublicKeyTests.java b/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java index b02687d4..eccba87a 100644 --- a/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java +++ b/src/test/java/com/syntifi/crypto/key/Secp256k1PublicKeyTests.java @@ -70,7 +70,7 @@ void signAndRecoverPublicKeyWithPaddedPK() throws URISyntaxException, IOExceptio String filePath = getResourcesKeyPath("secp256k1/private-padded.pem"); privKey.readPrivateKey(filePath); - //Check that the public key is padded with a 0 byte + //Check that the public key is private-padded.pem with a 0 byte assert privKey.getKeyPair().getPublicKey().toByteArray()[0] == (byte) 0; diff --git a/src/test/resources/secp256k1/private-padded.pem b/src/test/resources/secp256k1/private-padded.pem index 4dde15e7..3c034ebb 100644 --- a/src/test/resources/secp256k1/private-padded.pem +++ b/src/test/resources/secp256k1/private-padded.pem @@ -1,3 +1,3 @@ -----BEGIN EC PRIVATE KEY----- -MC4CAQEEIGbGCLXUsZFyrJe4nAGW+V5Zd7z+AbccZ5SU3LBQi/Q6oAcGBSuBBAAK +MC4CAQEEIF6d87JG1raI7KNzkaXUbtTezTHE7oziYJ04VGZv4YOdoAcGBSuBBAAK -----END EC PRIVATE KEY----- From e5098f08e06658c78efcc25993ad839de90cff90 Mon Sep 17 00:00:00 2001 From: Carl Norburn Date: Fri, 28 Jun 2024 16:13:21 +0100 Subject: [PATCH 5/5] Tests for the 50% Secp problem when creating a public short key --- .../com/syntifi/crypto/key/Secp256k1PublicKey.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java b/src/main/java/com/syntifi/crypto/key/Secp256k1PublicKey.java index 9496ebe5..f17b0fe7 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.*; @@ -123,11 +122,16 @@ public Boolean verify(byte[] message, byte[] signature) { * @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 int startBit = key[0] == (byte) 0 ? 1 : 0; - final byte[] pubKeyBytes = Arrays.copyOfRange(key, startBit, (AlgorithmTag.SECP256K1.getLength() - 1) + startBit); - return Hex.decode(pubKeyPrefix + Hex.encode(pubKeyBytes)); + + 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; + } }