diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java index 1ef6afba1..92ea0f7ac 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/SecpV3KeystoresBulkLoadAcceptanceTest.java @@ -60,7 +60,8 @@ static void initV3Keystores() throws IOException, GeneralSecurityException, Ciph publicKeys = new ArrayList<>(); for (int i = 0; i < 4; i++) { final ECKeyPair ecKeyPair = Keys.createEcKeyPair(); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(ecKeyPair.getPublicKey()); + final ECPublicKey ecPublicKey = + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey()); final String publicKeyHex = IdentifierUtils.normaliseIdentifier(EthPublicKeyUtils.toHexString(ecPublicKey)); publicKeys.add(publicKeyHex); diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java index 297ad1b13..3d0a555fa 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/YubiHsmKeysAcceptanceTest.java @@ -121,7 +121,7 @@ private void createConfigurationFiles(final Set opaqueDataIds, final Ke private String getPublicKey(final String key) { return normaliseIdentifier( EthPublicKeyUtils.toHexString( - EthPublicKeyUtils.createPublicKey( + EthPublicKeyUtils.web3JPublicKeyToECPublicKey( Credentials.create(key).getEcKeyPair().getPublicKey()))); } } diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java index a4d5769c8..cc2e3603c 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/signing/SecpSigningAcceptanceTest.java @@ -188,13 +188,14 @@ private void signAndVerifySignature(final String publicKeyHex) { void verifySignature(final Bytes signature, final String publicKeyHex) { final ECPublicKey expectedPublicKey = - EthPublicKeyUtils.createPublicKey(Bytes.fromHexString(publicKeyHex)); + EthPublicKeyUtils.bytesToECPublicKey(Bytes.fromHexString(publicKeyHex)); final byte[] r = signature.slice(0, 32).toArray(); final byte[] s = signature.slice(32, 32).toArray(); final byte[] v = signature.slice(64).toArray(); final BigInteger messagePublicKey = recoverPublicKey(new SignatureData(v, r, s)); - assertThat(EthPublicKeyUtils.createPublicKey(messagePublicKey)).isEqualTo(expectedPublicKey); + assertThat(EthPublicKeyUtils.web3JPublicKeyToECPublicKey(messagePublicKey)) + .isEqualTo(expectedPublicKey); } private BigInteger recoverPublicKey(final SignatureData signature) { diff --git a/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java b/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java index 4a1ad0892..9e22cefd3 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth1AddressSignerIdentifier.java @@ -13,6 +13,7 @@ package tech.pegasys.web3signer.core; import static org.web3j.crypto.Keys.getAddress; +import static tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils.ecPublicKeyToWeb3JPublicKey; import static tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils.toHexString; import static tech.pegasys.web3signer.signing.secp256k1.util.AddressUtil.remove0xPrefix; @@ -31,11 +32,7 @@ public Eth1AddressSignerIdentifier(final String address) { } public static SignerIdentifier fromPublicKey(final ECPublicKey publicKey) { - return new Eth1AddressSignerIdentifier(getAddress(toHexString(publicKey))); - } - - public static SignerIdentifier fromPublicKey(final String publicKey) { - return new Eth1AddressSignerIdentifier(getAddress(publicKey)); + return new Eth1AddressSignerIdentifier(getAddress(ecPublicKeyToWeb3JPublicKey(publicKey))); } @Override diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java index b33695eb4..8f0caf6fd 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/Eth1AddressSignerIdentifierTest.java @@ -15,53 +15,95 @@ import static org.assertj.core.api.Assertions.assertThat; import tech.pegasys.web3signer.core.Eth1AddressSignerIdentifier; -import tech.pegasys.web3signer.core.util.PublicKeyUtils; import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; import tech.pegasys.web3signer.signing.secp256k1.SignerIdentifier; -import tech.pegasys.web3signer.signing.secp256k1.util.AddressUtil; +import java.security.KeyPair; import java.security.interfaces.ECPublicKey; -import java.util.Locale; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.web3j.crypto.Keys; class Eth1AddressSignerIdentifierTest { + private static KeyPair secp256k1KeyPair; + private static KeyPair secp256k1KeyPair2; + + @BeforeAll + static void generateKeyPair() { + secp256k1KeyPair = EthPublicKeyUtils.generateK256KeyPair(); + secp256k1KeyPair2 = EthPublicKeyUtils.generateK256KeyPair(); + } @Test void prefixIsRemovedFromAddress() { - final Eth1AddressSignerIdentifier signerIdentifier = new Eth1AddressSignerIdentifier("0xAb"); - assertThat(signerIdentifier.toStringIdentifier()).isEqualTo("ab"); + // web3j.crypto.Keys.getAddress() returns lower case address without 0x prefix + final String address = + Keys.getAddress( + EthPublicKeyUtils.ecPublicKeyToWeb3JPublicKey( + (ECPublicKey) secp256k1KeyPair.getPublic())); + // forcefully convert first two alphabets to uppercase and add prefix + final String mixCaseAddress = "0X" + convertHexToMixCase(address); + + final Eth1AddressSignerIdentifier signerIdentifier = + new Eth1AddressSignerIdentifier(mixCaseAddress); + assertThat(signerIdentifier.toStringIdentifier()).isEqualTo(address); + assertThat(signerIdentifier.toStringIdentifier()).doesNotStartWithIgnoringCase("0x"); + assertThat(signerIdentifier.toStringIdentifier()).isLowerCase(); } @Test void validateWorksForSamePrimaryKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); assertThat(signerIdentifier.validate(publicKey)).isTrue(); } @Test void validateFailsForDifferentPrimaryKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); - assertThat(signerIdentifier.validate(PublicKeyUtils.createKeyFrom("0xbb"))).isFalse(); + assertThat(signerIdentifier.validate((ECPublicKey) secp256k1KeyPair2.getPublic())).isFalse(); } @Test void validateFailsForNullPrimaryKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); assertThat(signerIdentifier.validate(null)).isFalse(); } @Test void correctEth1AddressIsGeneratedFromPublicKey() { - final ECPublicKey publicKey = PublicKeyUtils.createKeyFrom("0xab"); + final ECPublicKey publicKey = (ECPublicKey) secp256k1KeyPair.getPublic(); final SignerIdentifier signerIdentifier = Eth1AddressSignerIdentifier.fromPublicKey(publicKey); - final String prefixRemovedAddress = - AddressUtil.remove0xPrefix( - Keys.getAddress(EthPublicKeyUtils.toHexString(publicKey)).toLowerCase(Locale.US)); - assertThat(signerIdentifier.toStringIdentifier()).isEqualTo(prefixRemovedAddress); + + // web3j.crypto.Keys.getAddress() returns lower case address without 0x prefix + final String expectedAddress = + Keys.getAddress(EthPublicKeyUtils.ecPublicKeyToWeb3JPublicKey(publicKey)); + assertThat(signerIdentifier.toStringIdentifier()).isEqualTo(expectedAddress); + assertThat(signerIdentifier.toStringIdentifier()).doesNotStartWithIgnoringCase("0x"); + assertThat(signerIdentifier.toStringIdentifier()).isLowerCase(); + } + + /** + * Converts first two alphabets to uppercase that can be used to test the case sensitivity of the + * address + * + * @param input address string in hex, assuming all characters are lowercase. + * @return address with first two alphabets converted to uppercase + */ + private static String convertHexToMixCase(final String input) { + final char[] chars = input.toCharArray(); + int count = 0; + + for (int i = 0; i < chars.length && count < 2; i++) { + if (Character.isLetter(chars[i]) && Character.isLowerCase(chars[i])) { + chars[i] = Character.toUpperCase(chars[i]); + count++; + } + } + + return new String(chars); } } diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java index 6923303dd..89cec7c64 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthAccountsResultProviderTest.java @@ -27,25 +27,31 @@ import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.collect.Sets; -import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.web3j.crypto.Keys; -@SuppressWarnings("unchecked") public class EthAccountsResultProviderTest { - - final ECPublicKey publicKeyA = createKeyFrom("A".repeat(128)); - final ECPublicKey publicKeyB = createKeyFrom("B".repeat(128)); - final ECPublicKey publicKeyC = createKeyFrom("C".repeat(128)); - - final String addressA = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyA)); - final String addressB = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyB)); - final String addressC = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyC)); - - final ECPublicKey createKeyFrom(final String hexString) { - return EthPublicKeyUtils.createPublicKey(Bytes.fromHexString(hexString)); + private static ECPublicKey publicKeyA; + private static ECPublicKey publicKeyB; + private static ECPublicKey publicKeyC; + + private static String addressA; + private static String addressB; + private static String addressC; + + @BeforeAll + static void init() { + publicKeyA = (ECPublicKey) EthPublicKeyUtils.generateK256KeyPair().getPublic(); + publicKeyB = (ECPublicKey) EthPublicKeyUtils.generateK256KeyPair().getPublic(); + publicKeyC = (ECPublicKey) EthPublicKeyUtils.generateK256KeyPair().getPublic(); + + addressA = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyA)); + addressB = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyB)); + addressC = Keys.getAddress(EthPublicKeyUtils.toHexString(publicKeyC)); } @Test @@ -103,9 +109,7 @@ public void missingParametersIsOk() { final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_accounts"); request.setId(new JsonRpcRequestId(id)); - final Object body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - final List addressses = (List) body; + final List addressses = resultProvider.createResponseResult(request); assertThat(addressses).containsExactly("0x" + addressA); } @@ -120,10 +124,7 @@ public void multipleValueFromBodyProviderInsertedToResult() { request.setId(new JsonRpcRequestId(id)); request.setParams(emptyList()); - final Object body = resultProvider.createResponseResult(request); - - assertThat(body).isInstanceOf(List.class); - final List reportedAddresses = (List) body; + final List reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyInAnyOrder("0x" + addressA, "0x" + addressB, "0x" + addressC); } @@ -139,25 +140,19 @@ public void accountsReturnedAreDynamicallyFetchedFromProvider() { request.setId(new JsonRpcRequestId(1)); request.setParams(emptyList()); - Object body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - List reportedAddresses = (List) body; + List reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyElementsOf( - List.of("0x" + addressA, "0x" + addressB, "0x" + addressC).stream() + Stream.of("0x" + addressA, "0x" + addressB, "0x" + addressC) .sorted() .collect(Collectors.toList())); addresses.remove(publicKeyA); - body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - reportedAddresses = (List) body; + reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyElementsOf( - List.of("0x" + addressB, "0x" + addressC).stream() - .sorted() - .collect(Collectors.toList())); + Stream.of("0x" + addressB, "0x" + addressC).sorted().collect(Collectors.toList())); } @Test @@ -169,12 +164,10 @@ public void accountsReturnedAreSortedAlphabetically() { request.setId(new JsonRpcRequestId(1)); request.setParams(emptyList()); - final Object body = resultProvider.createResponseResult(request); - assertThat(body).isInstanceOf(List.class); - List reportedAddresses = (List) body; + List reportedAddresses = resultProvider.createResponseResult(request); assertThat(reportedAddresses) .containsExactlyElementsOf( - List.of("0x" + addressA, "0x" + addressB, "0x" + addressC).stream() + Stream.of("0x" + addressA, "0x" + addressB, "0x" + addressC) .sorted() .collect(Collectors.toList())); } diff --git a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java index 39820959d..6562768fa 100644 --- a/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java +++ b/core/src/test/java/tech/pegasys/web3signer/core/service/jsonrpc/EthSignTransactionResultProviderTest.java @@ -108,7 +108,8 @@ public void ifAddressIsNotUnlockedExceptionIsThrownWithSigningNotUnlocked() { public void signatureHasTheExpectedFormat() { final Credentials cs = Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); - final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + final ECPublicKey key = + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(cs.getEcKeyPair().getPublicKey()); final String addr = Keys.getAddress(EthPublicKeyUtils.toHexString(key)); final BigInteger v = BigInteger.ONE; @@ -169,7 +170,8 @@ public void returnsExpectedSignatureForEip1559Transaction() { private String executeEthSignTransaction(final JsonObject params) { final Credentials cs = Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); - final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + final ECPublicKey key = + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(cs.getEcKeyPair().getPublicKey()); final String addr = Keys.getAddress(EthPublicKeyUtils.toHexString(key)); doAnswer( diff --git a/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java b/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java deleted file mode 100644 index 7f639069f..000000000 --- a/core/src/test/java/tech/pegasys/web3signer/core/util/PublicKeyUtils.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 ConsenSys AG. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package tech.pegasys.web3signer.core.util; - -import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; - -import java.security.interfaces.ECPublicKey; - -import org.apache.tuweni.bytes.Bytes; - -public class PublicKeyUtils { - - public static ECPublicKey createKeyFrom(final String hexString) { - Bytes bytes = Bytes.fromHexString(hexString, 64); - return EthPublicKeyUtils.createPublicKey(bytes); - } -} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java index 2ae294201..8a1f18c4e 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtils.java @@ -12,63 +12,259 @@ */ package tech.pegasys.web3signer.signing.secp256k1; -import static com.google.common.base.Preconditions.checkArgument; -import static org.bouncycastle.util.BigIntegers.asUnsignedByteArray; - import java.math.BigInteger; -import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.EllipticCurve; import java.security.spec.InvalidKeySpecException; -import java.security.spec.InvalidParameterSpecException; import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.web3j.utils.Numeric; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; +import org.web3j.crypto.ECKeyPair; +/** + * Utility class for working with secp256k1 public keys. This class provides methods for converting + * between Java and Web3J library based SECP keys. + */ public class EthPublicKeyUtils { - private static final int PUBLIC_KEY_SIZE = 64; + private static final BouncyCastleProvider BC_PROVIDER = new BouncyCastleProvider(); + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final ECDomainParameters SECP256K1_DOMAIN; + private static final ECParameterSpec BC_SECP256K1_SPEC; + private static final java.security.spec.ECParameterSpec JAVA_SECP256K1_SPEC; + private static final String SECP256K1_CURVE = "secp256k1"; + private static final ECGenParameterSpec EC_KEYGEN_PARAM = new ECGenParameterSpec(SECP256K1_CURVE); + private static final String EC_ALGORITHM = "EC"; - public static ECPublicKey createPublicKey(final ECPoint publicPoint) { + static { + final X9ECParameters params = CustomNamedCurves.getByName(SECP256K1_CURVE); + SECP256K1_DOMAIN = + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + BC_SECP256K1_SPEC = + new ECParameterSpec(params.getCurve(), params.getG(), params.getN(), params.getH()); + final ECCurve bcCurve = BC_SECP256K1_SPEC.getCurve(); + JAVA_SECP256K1_SPEC = + new java.security.spec.ECParameterSpec( + new EllipticCurve( + new java.security.spec.ECFieldFp(bcCurve.getField().getCharacteristic()), + bcCurve.getA().toBigInteger(), + bcCurve.getB().toBigInteger()), + new java.security.spec.ECPoint( + BC_SECP256K1_SPEC.getG().getAffineXCoord().toBigInteger(), + BC_SECP256K1_SPEC.getG().getAffineYCoord().toBigInteger()), + BC_SECP256K1_SPEC.getN(), + BC_SECP256K1_SPEC.getH().intValue()); + } + + /** + * Create a new secp256k1 key pair. + * + * @return The generated java security key pair + */ + public static KeyPair generateK256KeyPair() { try { - final AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); - parameters.init(new ECGenParameterSpec("secp256k1")); - final ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); - final ECPublicKeySpec pubSpec = new ECPublicKeySpec(publicPoint, ecParameters); - final KeyFactory kf = KeyFactory.getInstance("EC"); - return (ECPublicKey) kf.generatePublic(pubSpec); - } catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) { - throw new IllegalStateException("Unable to create Ethereum public key", e); + final KeyPairGenerator keyPairGenerator = + KeyPairGenerator.getInstance(EC_ALGORITHM, BC_PROVIDER); + keyPairGenerator.initialize(EC_KEYGEN_PARAM, SECURE_RANDOM); + return keyPairGenerator.generateKeyPair(); + } catch (final GeneralSecurityException e) { + throw new RuntimeException(e); } } - public static ECPublicKey createPublicKey(final Bytes value) { - checkArgument(value.size() == PUBLIC_KEY_SIZE, "Invalid public key size must be 64 bytes"); - final Bytes x = value.slice(0, 32); - final Bytes y = value.slice(32, 32); - final ECPoint ecPoint = - new ECPoint(Numeric.toBigInt(x.toArrayUnsafe()), Numeric.toBigInt(y.toArrayUnsafe())); - return createPublicKey(ecPoint); + /** + * Convert a Web3J ECKeyPair to a Java security KeyPair using SECP256K1 curve. + * + * @param web3JECKeypair The Web3J keypair to convert + * @return The converted Java security KeyPair + */ + public static KeyPair web3JECKeypairToJavaKeyPair(final ECKeyPair web3JECKeypair) { + try { + final PrivateKey ecPrivateKey = + KeyFactory.getInstance(EC_ALGORITHM, BC_PROVIDER) + .generatePrivate( + new ECPrivateKeySpec(web3JECKeypair.getPrivateKey(), JAVA_SECP256K1_SPEC)); + return new KeyPair(web3JPublicKeyToECPublicKey(web3JECKeypair.getPublicKey()), ecPrivateKey); + } catch (final Exception e) { + throw new RuntimeException("Unable to convert web3j to Java EC keypair", e); + } } - public static ECPublicKey createPublicKey(final BigInteger value) { - final Bytes ethBytes = Bytes.wrap(Numeric.toBytesPadded(value, PUBLIC_KEY_SIZE)); - return createPublicKey(ethBytes); + /** + * Convert a public key in bytes format to a java security ECPublicKey. + * + * @param value The public key in bytes format. This can be either 33 bytes (compressed), 64 bytes + * (uncompressed), or 65 bytes (uncompressed with prefix). + * @return The java security ECPublicKey + */ + public static ECPublicKey bytesToECPublicKey(final Bytes value) { + return bcECPointToECPublicKey(bytesToBCECPoint(value)); } - public static byte[] toByteArray(final ECPublicKey publicKey) { - final ECPoint ecPoint = publicKey.getW(); - final Bytes xBytes = Bytes32.wrap(asUnsignedByteArray(32, ecPoint.getAffineX())); - final Bytes yBytes = Bytes32.wrap(asUnsignedByteArray(32, ecPoint.getAffineY())); - return Bytes.concatenate(xBytes, yBytes).toArray(); + /** + * Convert a public key in bytes format to a Bouncy Castle ECPoint on SECP256K1 curve. + * + * @param value The public key in bytes format. This can be either 33 bytes (compressed), 64 bytes + * (uncompressed), or 65 bytes (uncompressed with prefix). + * @return The Bouncy Castle ECPoint on SECP256K1 curve + */ + public static ECPoint bytesToBCECPoint(final Bytes value) { + if (value.size() != 33 && value.size() != 65 && value.size() != 64) { + throw new IllegalArgumentException( + "Invalid public key length. Expected 33, 64, or 65 bytes."); + } + + final ECPoint point; + final byte[] key; + if (value.size() == 64) { + // For 64-byte input, we need to prepend the 0x04 prefix for uncompressed format + key = new byte[65]; + key[0] = 0x04; + System.arraycopy(value.toArrayUnsafe(), 0, key, 1, 64); + } else { + key = value.toArrayUnsafe(); + } + point = SECP256K1_DOMAIN.getCurve().decodePoint(key); + + return point; + } + + /** + * Convert a Bouncy Castle ECPoint to a Java security ECPublicKey. + * + * @param point The Bouncy Castle ECPoint to convert + * @return The converted Java security ECPublicKey on SECP256K1 curve + */ + public static ECPublicKey bcECPointToECPublicKey(final ECPoint point) { + try { + // Convert Bouncy Castle ECPoint to Java ECPoint + final java.security.spec.ECPoint ecPoint = + new java.security.spec.ECPoint( + point.getAffineXCoord().toBigInteger(), point.getAffineYCoord().toBigInteger()); + + final java.security.spec.ECPublicKeySpec pubSpec = + new java.security.spec.ECPublicKeySpec(ecPoint, JAVA_SECP256K1_SPEC); + return (ECPublicKey) + KeyFactory.getInstance(EC_ALGORITHM, BC_PROVIDER).generatePublic(pubSpec); + } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unable to create EC public key", e); + } } + /** + * Create a java security ECPublicKey from Web3J representation of public key as BigInteger. Web3J + * uses Bouncy castle ECPoint getEncoded with false to get uncompressed public key and then create + * BigInteger from it without using the prefix byte. + * + * @param publicKeyValue The BigInteger representation of the public key (64 bytes, without + * prefix) + * @return The created ECPublicKey + * @throws IllegalArgumentException if the input is invalid + */ + public static ECPublicKey web3JPublicKeyToECPublicKey(final BigInteger publicKeyValue) { + if (publicKeyValue == null) { + throw new IllegalArgumentException("Public key value cannot be null"); + } + + byte[] publicKeyBytes = ensure64Bytes(publicKeyValue.toByteArray()); + + // Use the existing bytesToECPublicKey method + return bytesToECPublicKey(Bytes.wrap(publicKeyBytes)); + } + + /** + * Ensures that the given byte array is exactly 64 bytes long. If the input array is shorter than + * 64 bytes, it pads the array with leading zeros. If the input array is longer than 64 bytes, it + * trims the excess leading bytes. + * + * @param publicKeyBytes The input byte array representing the public key. + * @return A byte array of exactly 64 bytes. + */ + private static byte[] ensure64Bytes(final byte[] publicKeyBytes) { + if (publicKeyBytes.length == 64) { + return publicKeyBytes; + } + + final byte[] result = new byte[64]; + if (publicKeyBytes.length < 64) { + // pad with leading 0s + System.arraycopy( + publicKeyBytes, 0, result, 64 - publicKeyBytes.length, publicKeyBytes.length); + } else { + // trim excess bytes + System.arraycopy(publicKeyBytes, publicKeyBytes.length - 64, result, 0, 64); + } + return result; + } + + /** + * Convert a java ECPublicKey to an uncompressed (64 bytes) hex string. + * + * @param publicKey The public key to convert + * @return The public key as a hex string + */ public static String toHexString(final ECPublicKey publicKey) { - return Bytes.wrap(toByteArray(publicKey)).toHexString(); + return getEncoded(publicKey, false).toHexString(); + } + + /** + * Convert a java ECPublicKey to a compressed (33 bytes) hex string. + * + * @param publicKey The public key to convert + * @return The public key as a hex string + */ + public static String toHexStringCompressed(final ECPublicKey publicKey) { + return getEncoded(publicKey, true).toHexString(); + } + + /** + * Convert a java ECPublicKey to a Web3J public key as BigInteger. + * + * @param publicKey The public key to convert + * @return The Web3J public key as a BigInteger + */ + public static BigInteger ecPublicKeyToWeb3JPublicKey(final ECPublicKey publicKey) { + // Convert to BigInteger from uncompressed public key (64 bytes) + return new BigInteger(1, getEncoded(publicKey, false).toArrayUnsafe()); + } + + /** + * Convert java ECPublicKey to Bytes. + * + * @param publicKey The public key to convert + * @param compressed Whether to return the compressed form 33 bytes or the uncompressed form 64 + * bytes + * @return The encoded public key. + */ + private static Bytes getEncoded(final ECPublicKey publicKey, final boolean compressed) { + final ECPoint point; + if (publicKey instanceof BCECPublicKey) { + // If it's already a Bouncy Castle key, we can get the ECPoint directly + point = ((BCECPublicKey) publicKey).getQ(); + } else { + // If it's not a BC key, we need to create the ECPoint from the coordinates + final BigInteger x = publicKey.getW().getAffineX(); + final BigInteger y = publicKey.getW().getAffineY(); + point = BC_SECP256K1_SPEC.getCurve().createPoint(x, y); + } + + return compressed + ? Bytes.wrap(point.getEncoded(true)) + : Bytes.wrap(point.getEncoded(false), 1, 64); } } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java index 83e9edfbd..18a9ab749 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSigner.java @@ -51,7 +51,7 @@ public class AzureKeyVaultSigner implements Signer { final AzureKeyVault azureKeyVault, final AzureHttpClientFactory azureHttpClientFactory) { this.config = config; - this.publicKey = EthPublicKeyUtils.createPublicKey(publicKey); + this.publicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKey); this.needsToHash = needsToHash; this.signingAlgo = useDeprecatedSignatureAlgorithm diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java index 090f77747..0070e52d2 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/filebased/CredentialSigner.java @@ -31,7 +31,8 @@ public class CredentialSigner implements Signer { public CredentialSigner(final Credentials credentials, final boolean needToHash) { this.credentials = credentials; - this.publicKey = EthPublicKeyUtils.createPublicKey(credentials.getEcKeyPair().getPublicKey()); + this.publicKey = + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(credentials.getEcKeyPair().getPublicKey()); this.needToHash = needToHash; } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java index 0e81f85d9..1e51396a4 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java @@ -28,7 +28,6 @@ import org.bouncycastle.asn1.DLSequence; import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.Sign; -import org.web3j.utils.Numeric; public class Eth1SignatureUtil { private static final Logger LOG = LogManager.getLogger(); @@ -92,7 +91,7 @@ private static Signature deriveSignature( private static int recoverKeyIndex( final ECPublicKey ecPublicKey, final ECDSASignature sig, final byte[] hash) { - final BigInteger publicKey = Numeric.toBigInt(EthPublicKeyUtils.toByteArray(ecPublicKey)); + final BigInteger publicKey = EthPublicKeyUtils.ecPublicKeyToWeb3JPublicKey(ecPublicKey); for (int i = 0; i < 4; i++) { final BigInteger k = Sign.recoverFromSignature(i, sig, hash); LOG.trace("recovered key: {}", k); diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java index c28ced922..1c72573e1 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/config/DefaultArtifactSignerProviderTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.when; import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.web3signer.BLSTestUtil; import tech.pegasys.web3signer.KeystoreUtil; import tech.pegasys.web3signer.signing.ArtifactSigner; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; @@ -28,8 +29,7 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.security.GeneralSecurityException; -import java.security.SecureRandom; +import java.security.KeyPair; import java.util.List; import java.util.Map; import java.util.Optional; @@ -39,7 +39,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.web3j.crypto.ECKeyPair; -import org.web3j.crypto.Keys; import org.web3j.crypto.WalletUtils; import org.web3j.crypto.exception.CipherException; @@ -109,19 +108,18 @@ void signerProviderCanMapInTwoSigners() { @Test void proxySignersAreLoadedCorrectly() throws IOException { - final SecureRandom secureRandom = new SecureRandom(); // create random proxy signers final KeystoresParameters commitBoostParameters = new TestCommitBoostParameters(commitBoostKeystoresPath, commitBoostPasswordDir); // create random BLS key pairs as proxy keys for public key1 and public key2 - final List key1ProxyKeyPairs = randomBLSV4Keystores(secureRandom, PUBLIC_KEY1); - final List key2ProxyKeyPairs = randomBLSV4Keystores(secureRandom, PUBLIC_KEY2); + final List key1ProxyKeyPairs = randomBLSV4Keystores(PUBLIC_KEY1); + final List key2ProxyKeyPairs = randomBLSV4Keystores(PUBLIC_KEY2); // create random secp key pairs as proxy keys for public key1 and public key2 - final List key1SecpKeyPairs = randomSecpV3Keystores(secureRandom, PUBLIC_KEY1); - final List key2SecpKeyPairs = randomSecpV3Keystores(secureRandom, PUBLIC_KEY2); + final List key1SecpKeyPairs = randomSecpV3Keystores(PUBLIC_KEY1); + final List key2SecpKeyPairs = randomSecpV3Keystores(PUBLIC_KEY2); // set up mock signers final ArtifactSigner mockSigner1 = mock(ArtifactSigner.class); @@ -180,8 +178,7 @@ void emptyProxySignersAreLoadedSuccessfully() { } } - private List randomBLSV4Keystores(SecureRandom secureRandom, String identifier) - throws IOException { + private List randomBLSV4Keystores(final String identifier) throws IOException { final Path v4Dir = Files.createDirectories( commitBoostKeystoresPath.resolve(identifier).resolve(KeyType.BLS.name())); @@ -189,15 +186,14 @@ private List randomBLSV4Keystores(SecureRandom secureRandom, String return IntStream.range(0, 4) .mapToObj( i -> { - final BLSKeyPair blsKeyPair = BLSKeyPair.random(secureRandom); + final BLSKeyPair blsKeyPair = BLSTestUtil.randomKeyPair(i); KeystoreUtil.createKeystoreFile(blsKeyPair, v4Dir, "password"); return blsKeyPair; }) .toList(); } - private List randomSecpV3Keystores( - final SecureRandom secureRandom, final String identifier) throws IOException { + private List randomSecpV3Keystores(final String identifier) throws IOException { final Path v3Dir = Files.createDirectories( commitBoostKeystoresPath.resolve(identifier).resolve(KeyType.SECP256K1.name())); @@ -205,10 +201,12 @@ private List randomSecpV3Keystores( .mapToObj( i -> { try { - final ECKeyPair ecKeyPair = Keys.createEcKeyPair(secureRandom); + final KeyPair secp256k1KeyPair = EthPublicKeyUtils.generateK256KeyPair(); + final ECKeyPair ecKeyPair = ECKeyPair.create(secp256k1KeyPair); + WalletUtils.generateWalletFile("password", ecKeyPair, v3Dir.toFile(), false); return ecKeyPair; - } catch (GeneralSecurityException | CipherException | IOException e) { + } catch (final CipherException | IOException e) { throw new RuntimeException(e); } }) @@ -227,7 +225,7 @@ private static String[] getSecpPublicKeysArray(final List ecKeyPairs) .map( keyPair -> EthPublicKeyUtils.toHexString( - EthPublicKeyUtils.createPublicKey(keyPair.getPublicKey()))) + EthPublicKeyUtils.web3JPublicKeyToECPublicKey(keyPair.getPublicKey()))) .toList() .toArray(String[]::new); } diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java index c75cfb9a8..3390a5819 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/EthPublicKeyUtilsTest.java @@ -14,22 +14,27 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.Fail.fail; import java.math.BigInteger; +import java.security.KeyPair; import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; +import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X962Parameters; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.ec.CustomNamedCurves; -import org.bouncycastle.math.ec.ECFieldElement; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.web3j.utils.Numeric; @@ -41,47 +46,83 @@ class EthPublicKeyUtilsTest { "0xaf80b90d25145da28c583359beb47b21796b2fe1a23c1511e443e7a64dfdb27d7434c380f0aa4c500e220aa1a9d068514b1ff4d5019e624e7ba1efe82b340a59"; private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); - @Test - public void createsPublicKeyFromECPoint() { - final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPoint expectedEcPoint = createEcPoint(publicKeyBytes); - - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(expectedEcPoint); - verifyPublicKey(ecPublicKey, publicKeyBytes, expectedEcPoint); - } - @Test public void createsPublicKeyFromBytes() { final Bytes expectedPublicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(expectedPublicKeyBytes); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(expectedPublicKeyBytes); final ECPoint expectedEcPoint = createEcPoint(expectedPublicKeyBytes); verifyPublicKey(ecPublicKey, expectedPublicKeyBytes, expectedEcPoint); } @Test - public void createsPublicKeyFromBigInteger() { + public void createsPublicKeyFromWeb3JBigInteger() { final BigInteger publicKey = Numeric.toBigInt(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(publicKey); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.web3JPublicKeyToECPublicKey(publicKey); final Bytes expectedPublicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); final ECPoint expectedEcPoint = createEcPoint(expectedPublicKeyBytes); verifyPublicKey(ecPublicKey, expectedPublicKeyBytes, expectedEcPoint); } + private static Stream validPublicKeys() { + final KeyPair keyPair = EthPublicKeyUtils.generateK256KeyPair(); + return Stream.of( + // Compressed (33 bytes) + Bytes.fromHexString( + EthPublicKeyUtils.toHexStringCompressed((ECPublicKey) keyPair.getPublic())), + // Uncompressed without prefix (64 bytes) + Bytes.fromHexString(EthPublicKeyUtils.toHexString((ECPublicKey) keyPair.getPublic())), + // Uncompressed with prefix (65 bytes) + Bytes.concatenate( + Bytes.of(0x04), + Bytes.fromHexString(EthPublicKeyUtils.toHexString((ECPublicKey) keyPair.getPublic())))); + } + + @ParameterizedTest + @MethodSource("validPublicKeys") + void acceptsValidPublicKeySizes(final Bytes publicKey) { + assertThatCode(() -> EthPublicKeyUtils.bytesToECPublicKey(publicKey)) + .doesNotThrowAnyException(); + } + @ParameterizedTest - @ValueSource(ints = {0, 63, 65}) - public void throwsInvalidArgumentExceptionForInvalidPublicKeySize(final int size) { - assertThatThrownBy(() -> EthPublicKeyUtils.createPublicKey(Bytes.random(size))) + @ValueSource(ints = {0, 32, 34, 63, 66}) + void throwsIllegalArgumentExceptionForInvalidPublicKeySize(final int size) { + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(Bytes.random(size))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid public key length. Expected 33, 64, or 65 bytes."); + } + + @Test + void throwsIllegalArgumentExceptionForInvalid33ByteKey() { + Bytes invalidCompressedKey = Bytes.concatenate(Bytes.of(0x00), Bytes.random(32)); + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(invalidCompressedKey)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Incorrect length for infinity encoding"); + } + + @Test + void throwsIllegalArgumentExceptionForInvalid65ByteKey() { + Bytes invalidUncompressedKey = Bytes.concatenate(Bytes.of(0x00), Bytes.random(64)); + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(invalidUncompressedKey)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid public key size must be 64 bytes"); + .hasMessageContaining("Incorrect length for infinity encoding"); + } + + @Test + void throwsIllegalArgumentExceptionForInvalidCompressedKeyPrefix() { + Bytes invalidCompressedKey = Bytes.concatenate(Bytes.of(0x04), Bytes.random(32)); + assertThatThrownBy(() -> EthPublicKeyUtils.bytesToECPublicKey(invalidCompressedKey)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Incorrect length for uncompressed encoding"); } @Test public void publicKeyIsConvertedToEthHexString() { final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(publicKeyBytes); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKeyBytes); final String hexString = EthPublicKeyUtils.toHexString(ecPublicKey); assertThat(hexString).isEqualTo(PUBLIC_KEY); } @@ -90,33 +131,73 @@ public void publicKeyIsConvertedToEthHexString() { public void publicKeyIsConvertedToEthBytes() { final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); - final ECPublicKey ecPublicKey = EthPublicKeyUtils.createPublicKey(publicKeyBytes); - final Bytes bytes = Bytes.wrap(EthPublicKeyUtils.toByteArray(ecPublicKey)); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKeyBytes); + final Bytes bytes = Bytes.fromHexString(EthPublicKeyUtils.toHexString(ecPublicKey)); assertThat(bytes).isEqualTo(publicKeyBytes); assertThat(bytes.size()).isEqualTo(64); assertThat(bytes.get(0)).isNotEqualTo(0x4); } + @Test + public void encodePublicKey() { + final Bytes publicKeyBytes = Bytes.fromHexString(PUBLIC_KEY); + final ECPublicKey ecPublicKey = EthPublicKeyUtils.bytesToECPublicKey(publicKeyBytes); + + final Bytes uncompressedWithoutPrefix = + Bytes.fromHexString(EthPublicKeyUtils.toHexString(ecPublicKey)); + final Bytes compressed = + Bytes.fromHexString(EthPublicKeyUtils.toHexStringCompressed(ecPublicKey)); + + assertThat(uncompressedWithoutPrefix.size()).isEqualTo(64); + assertThat(compressed.size()).isEqualTo(33); + } + private void verifyPublicKey( final ECPublicKey ecPublicKey, final Bytes publicKeyBytes, final ECPoint ecPoint) { + // verify public point assertThat(ecPublicKey.getW()).isEqualTo(ecPoint); + + // verify algorithm assertThat(ecPublicKey.getAlgorithm()).isEqualTo("EC"); + // verify curve parameters final ECParameterSpec params = ecPublicKey.getParams(); assertThat(params.getCofactor()).isEqualTo(CURVE_PARAMS.getCurve().getCofactor().intValue()); - assertThat(params.getOrder()).isEqualTo(CURVE_PARAMS.getCurve().getOrder()); - assertThat(params.getGenerator()).isEqualTo(fromBouncyCastleECPoint(CURVE_PARAMS.getG())); - + assertThat(params.getOrder()).isEqualTo(CURVE_PARAMS.getN()); + assertThat(params.getGenerator().getAffineX()) + .isEqualTo(CURVE_PARAMS.getG().getAffineXCoord().toBigInteger()); + assertThat(params.getGenerator().getAffineY()) + .isEqualTo(CURVE_PARAMS.getG().getAffineYCoord().toBigInteger()); + assertThat(params.getCurve().getA()).isEqualTo(CURVE_PARAMS.getCurve().getA().toBigInteger()); + assertThat(params.getCurve().getB()).isEqualTo(CURVE_PARAMS.getCurve().getB().toBigInteger()); + assertThat(params.getCurve().getField().getFieldSize()).isEqualTo(256); + + // Verify format assertThat(ecPublicKey.getFormat()).isEqualTo("X.509"); + // Verify encoded form SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(ecPublicKey.getEncoded())); assertThat(subjectPublicKeyInfo.getPublicKeyData().getBytes()) .isEqualTo(Bytes.concatenate(Bytes.of(0x4), publicKeyBytes).toArray()); + // Verify algorithm identifier final AlgorithmIdentifier algorithm = subjectPublicKeyInfo.getAlgorithm(); assertThat(algorithm.getAlgorithm().getId()).isEqualTo(EC_OID); - assertThat(algorithm.getParameters().toASN1Primitive().toString()).isEqualTo(SECP_OID); + + // Verify curve identifier + X962Parameters x962Params = X962Parameters.getInstance(algorithm.getParameters()); + if (x962Params.isNamedCurve()) { + assertThat(x962Params.getParameters()).isEqualTo(new ASN1ObjectIdentifier(SECP_OID)); + } else if (x962Params.isImplicitlyCA()) { + fail("Implicitly CA parameters are not expected for secp256k1"); + } else { + X9ECParameters ecParams = X9ECParameters.getInstance(x962Params.getParameters()); + assertThat(ecParams.getCurve()).isEqualTo(CURVE_PARAMS.getCurve()); + assertThat(ecParams.getG()).isEqualTo(CURVE_PARAMS.getG()); + assertThat(ecParams.getN()).isEqualTo(CURVE_PARAMS.getN()); + assertThat(ecParams.getH()).isEqualTo(CURVE_PARAMS.getH()); + } } private ECPoint createEcPoint(final Bytes publicKeyBytes) { @@ -124,18 +205,4 @@ private ECPoint createEcPoint(final Bytes publicKeyBytes) { final Bytes y = publicKeyBytes.slice(32, 32); return new ECPoint(Numeric.toBigInt(x.toArrayUnsafe()), Numeric.toBigInt(y.toArrayUnsafe())); } - - private ECPoint fromBouncyCastleECPoint( - final org.bouncycastle.math.ec.ECPoint bouncyCastleECPoint) { - final ECFieldElement xCoord = bouncyCastleECPoint.getAffineXCoord(); - final ECFieldElement yCoord = bouncyCastleECPoint.getAffineYCoord(); - - final Bytes32 xEncoded = Bytes32.wrap(xCoord.getEncoded()); - final Bytes32 yEncoded = Bytes32.wrap(yCoord.getEncoded()); - - final BigInteger x = xEncoded.toUnsignedBigInteger(); - final BigInteger y = yEncoded.toUnsignedBigInteger(); - - return new ECPoint(x, y); - } } diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java index 527c3e854..03e397dbc 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java @@ -117,7 +117,7 @@ void awsSignatureCanBeVerified() throws SignatureException { new AwsKmsSignerFactory(cachedAwsKmsClientFactory, applySha3Hash) .createSigner(awsKmsMetadata); final BigInteger publicKey = - Numeric.toBigInt(EthPublicKeyUtils.toByteArray(signer.getPublicKey())); + EthPublicKeyUtils.ecPublicKeyToWeb3JPublicKey(signer.getPublicKey()); final byte[] dataToSign = "Hello".getBytes(UTF_8); diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java index 3fa551e11..4933fa90c 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/azure/AzureKeyVaultSignerTest.java @@ -71,7 +71,7 @@ void azureSignerCanSign() throws SignatureException { new AzureKeyVaultSignerFactory(new AzureKeyVaultFactory(), new AzureHttpClientFactory()) .createSigner(config); final BigInteger publicKey = - Numeric.toBigInt(EthPublicKeyUtils.toByteArray(azureNonHashedDataSigner.getPublicKey())); + EthPublicKeyUtils.ecPublicKeyToWeb3JPublicKey(azureNonHashedDataSigner.getPublicKey()); final byte[] dataToSign = "Hello World".getBytes(UTF_8); diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtilTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtilTest.java index 66a147638..6816d9510 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtilTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtilTest.java @@ -14,37 +14,31 @@ import static org.assertj.core.api.Assertions.assertThat; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; + import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.Provider; -import java.security.SecureRandom; import java.security.interfaces.ECPublicKey; -import java.security.spec.ECGenParameterSpec; import org.apache.tuweni.bytes.Bytes; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Test; import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Hash; public class Eth1SignatureUtilTest { - private static final Provider BC_PROVIDER = new BouncyCastleProvider(); + public static final byte[] DATA_TO_SIGN = Hash.sha3("hello".getBytes(StandardCharsets.UTF_8)); @Test void signatureIsDerivedFromDerEncoded() throws Exception { - final byte[] dataToSign = Hash.sha3("hello".getBytes(StandardCharsets.UTF_8)); - - final KeyPair keyPair = generateEC_SECPKeyPair(); + final KeyPair keyPair = EthPublicKeyUtils.generateK256KeyPair(); final ECKeyPair web3jECKeyPair = ECKeyPair.create(keyPair); // sign using web3j (which uses BouncyCastle ECDSA Signer) - final ECDSASignature web3jSig = web3jECKeyPair.sign(dataToSign); + final ECDSASignature web3jSig = web3jECKeyPair.sign(DATA_TO_SIGN); // convert web3j's P1363 to ANS1/DER Encoded signature final ASN1EncodableVector v = new ASN1EncodableVector(); @@ -55,7 +49,7 @@ void signatureIsDerivedFromDerEncoded() throws Exception { // verify our logic final tech.pegasys.web3signer.signing.secp256k1.Signature signature = Eth1SignatureUtil.deriveSignatureFromDerEncoded( - dataToSign, (ECPublicKey) keyPair.getPublic(), derEncodedSignedData); + DATA_TO_SIGN, (ECPublicKey) keyPair.getPublic(), derEncodedSignedData); assertThat(signature.getR()).isEqualTo(web3jSig.r); assertThat(signature.getS()).isEqualTo(web3jSig.s); @@ -63,12 +57,10 @@ void signatureIsDerivedFromDerEncoded() throws Exception { @Test void signatureIsDerivedFromP1363Encoded() throws Exception { - final byte[] dataToSign = Hash.sha3("hello".getBytes(StandardCharsets.UTF_8)); - - final KeyPair keyPair = generateEC_SECPKeyPair(); + final KeyPair keyPair = EthPublicKeyUtils.generateK256KeyPair(); final ECKeyPair web3jECKeyPair = ECKeyPair.create(keyPair); // sign using web3j (which uses BouncyCastle ECDSASigner) - final ECDSASignature web3jSig = web3jECKeyPair.sign(dataToSign); + final ECDSASignature web3jSig = web3jECKeyPair.sign(DATA_TO_SIGN); // concatenate r || s to create byte[] final Bytes rBytes = Bytes.of(web3jSig.r.toByteArray()).trimLeadingZeros(); @@ -78,16 +70,9 @@ void signatureIsDerivedFromP1363Encoded() throws Exception { // verify our logic final tech.pegasys.web3signer.signing.secp256k1.Signature signature = Eth1SignatureUtil.deriveSignatureFromP1363Encoded( - dataToSign, (ECPublicKey) keyPair.getPublic(), p1363Signature); + DATA_TO_SIGN, (ECPublicKey) keyPair.getPublic(), p1363Signature); assertThat(signature.getR()).isEqualTo(web3jSig.r); assertThat(signature.getS()).isEqualTo(web3jSig.s); } - - public static KeyPair generateEC_SECPKeyPair() throws GeneralSecurityException { - // Generate the key pair - final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", BC_PROVIDER); - keyPairGenerator.initialize(new ECGenParameterSpec("secp256k1"), new SecureRandom()); - return keyPairGenerator.generateKeyPair(); - } }