Skip to content

Commit

Permalink
Fix CommitBoostAcceptanceTest
Browse files Browse the repository at this point in the history
  • Loading branch information
usmansaleem committed Nov 22, 2024
1 parent b5e2efb commit 98b3be5
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
*/
package tech.pegasys.web3signer.tests.commitboost;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasSize;

import tech.pegasys.teku.bls.BLSKeyPair;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.web3signer.KeystoreUtil;
import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder;
import tech.pegasys.web3signer.dsl.utils.DefaultKeystoresParameters;
Expand Down Expand Up @@ -101,9 +101,26 @@ void listCommitBoostPublicKeys() {
.statusCode(200)
.contentType(ContentType.JSON)
.body("keys", hasSize(2));
// .body("keys[0].consensus", equalTo(CONSENSUS_PUB_KEY.getPublicKey().toHexString()))
// .body("keys[0].proxy_bls", containsInAnyOrder(proxyBlsPubKeys.toArray()))
// .body("keys[0].proxy_ecdsa", containsInAnyOrder(proxyECPubKeys.toArray()));

// extract consensus public keys from response
final List<Map<String, Object>> responseKeys = response.jsonPath().getList("keys");
for (final Map<String, Object> responseKeyMap : responseKeys) {
final String consensusKeyHex = (String) responseKeyMap.get("consensus");
// verify if consensus public key is present in the map
assertThat(proxyBLSKeysMap.keySet()).contains(consensusKeyHex);

// verify if proxy BLS keys are present in the response
final List<String> responseProxyBlsKeys = (List<String>) responseKeyMap.get("proxy_bls");
final List<String> expectedProxyBLSKeys = getProxyBLSPubKeys(consensusKeyHex);
assertThat(responseProxyBlsKeys)
.containsExactlyInAnyOrder(expectedProxyBLSKeys.toArray(String[]::new));

// verify if proxy SECP keys are present in the response
final List<String> responseProxySECPKeys = (List<String>) responseKeyMap.get("proxy_ecdsa");
final List<String> expectedProxySECPKeys = getProxyECPubKeys(consensusKeyHex);
assertThat(responseProxySECPKeys)
.containsExactlyInAnyOrder(expectedProxySECPKeys.toArray(String[]::new));
}
}

private List<String> getProxyECPubKeys(final String consensusKeyHex) {
Expand All @@ -112,8 +129,7 @@ private List<String> getProxyECPubKeys(final String consensusKeyHex) {
.map(
ecKeyPair ->
EthPublicKeyUtils.toHexStringCompressed(
EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey()))
.toHexString())
EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey())))
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,13 @@ public Optional<String> sign(final String identifier, final Bytes data) {
/**
* Sign data for given identifier and return ArtifactSignature. Useful for SECP signing usages.
*
* @param <T> The specific type of ArtifactSignature
* @param identifier The identifier for which to sign data.
* @param data Bytes which is signed
* @return Optional ArtifactSignature of type T. Empty if no signer available for given identifier
*/
@SuppressWarnings("unchecked")
public <T extends ArtifactSignature<?>> Optional<T> signAndGetArtifactSignature(
public Optional<ArtifactSignature> signAndGetArtifactSignature(
final String identifier, final Bytes data) {
return signerProvider.getSigner(identifier).map(signer -> (T) signer.sign(data));
return signerProvider.getSigner(identifier).map(signer -> signer.sign(data));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ private static byte[] prependEip1559TransactionType(byte[] bytesToSign) {

private SignatureData sign(final String eth1Address, final byte[] bytesToSign) {
final SecpArtifactSignature artifactSignature =
secpSigner
.<SecpArtifactSignature>signAndGetArtifactSignature(
normaliseIdentifier(eth1Address), Bytes.of(bytesToSign))
.orElseThrow(() -> new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT));
(SecpArtifactSignature)
secpSigner
.signAndGetArtifactSignature(
normaliseIdentifier(eth1Address), Bytes.of(bytesToSign))
.orElseThrow(() -> new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT));

final Signature signature = artifactSignature.getSignatureData();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2024 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.signing;

import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils;
import tech.pegasys.web3signer.signing.util.IdentifierUtils;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.MutableBytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
import org.web3j.crypto.ECDSASignature;
import org.web3j.crypto.ECKeyPair;

/**
* An artifact signer for SECP256K1 keys used specifically for Commit Boost API ECDSA proxy keys. It
* uses compressed public key as identifier and signs the message with just sha256 digest. The
* signature complies with RFC-6979.
*/
public class K256ArtifactSigner implements ArtifactSigner {
private final ECKeyPair ecKeyPair;
public static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
public static final ECDomainParameters CURVE =
new ECDomainParameters(
CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());

public K256ArtifactSigner(final ECKeyPair web3JECKeypair) {
this.ecKeyPair = web3JECKeypair;
}

@Override
public String getIdentifier() {
final String hexString =
EthPublicKeyUtils.toHexStringCompressed(
EthPublicKeyUtils.web3JPublicKeyToECPublicKey(ecKeyPair.getPublicKey()));
return IdentifierUtils.normaliseIdentifier(hexString);
}

@Override
public K256ArtifactSignature sign(final Bytes message) {
try {
// Use BouncyCastle's ECDSASigner with HMacDSAKCalculator for deterministic ECDSA
final ECPrivateKeyParameters privKey =
new ECPrivateKeyParameters(ecKeyPair.getPrivateKey(), CURVE);
final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
signer.init(true, privKey);

// apply sha256 digest to the message before sending it to signing
final BigInteger[] components = signer.generateSignature(calculateSHA256(message.toArray()));

// create a canonicalised signature using Web3J ECDSASignature class
final ECDSASignature signature =
new ECDSASignature(components[0], components[1]).toCanonicalised();

return new K256ArtifactSignature(signature);
} catch (final Exception e) {
throw new RuntimeException("Error signing message", e);
}
}

@Override
public KeyType getKeyType() {
return KeyType.SECP256K1;
}

public static byte[] calculateSHA256(byte[] message) {
try {
// Create a MessageDigest instance for SHA-256
MessageDigest digest = MessageDigest.getInstance("SHA-256");

// Update the MessageDigest with the message bytes
return digest.digest(message);
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not found", e);
}
}

/** An artifact signature for SECP256K1 keys used specifically for Commit Boost API ECDSA proxy */
public static class K256ArtifactSignature implements ArtifactSignature<Bytes> {
final Bytes signature;

public K256ArtifactSignature(final ECDSASignature signature) {
// convert to compact signature format
final MutableBytes concatenated = MutableBytes.create(64);
UInt256.valueOf(signature.r).copyTo(concatenated, 0);
UInt256.valueOf(signature.s).copyTo(concatenated, 32);
this.signature = concatenated;
}

@Override
public KeyType getType() {
return KeyType.SECP256K1;
}

@Override
public String asHex() {
return signature.toHexString();
}

@Override
public Bytes getSignatureData() {
return signature;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
K256ArtifactSignature that = (K256ArtifactSignature) o;
return Objects.equals(signature, that.signature);
}

@Override
public int hashCode() {
return Objects.hashCode(signature);
}

@Override
public String toString() {
return signature.toHexString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import tech.pegasys.web3signer.keystorage.common.MappedResults;
import tech.pegasys.web3signer.signing.ArtifactSigner;
import tech.pegasys.web3signer.signing.EthSecpArtifactSigner;
import tech.pegasys.web3signer.signing.K256ArtifactSigner;
import tech.pegasys.web3signer.signing.secp256k1.filebased.CredentialSigner;
import tech.pegasys.web3signer.signing.secp256k1.util.JsonFilesUtil;

Expand All @@ -34,8 +35,38 @@
public class SecpV3KeystoresBulkLoader {
private static final Logger LOG = LogManager.getLogger();

/**
* Bulk-load Ethereum compatible SECP Artifact Signers from encrypted v3 keystores.
*
* @param keystoresPath Path to the directory containing the v3 keystores
* @param pwrdFileOrDirPath Path to the password file or directory containing the passwords for
* the v3 keystores
* @return MappedResults containing the loaded ArtifactSigners
*/
public static MappedResults<ArtifactSigner> loadV3KeystoresUsingPasswordFileOrDir(
final Path keystoresPath, final Path pwrdFileOrDirPath) {
return loadV3KeystoresUsingPasswordFileOrDir(keystoresPath, pwrdFileOrDirPath, true);
}

/**
* Bulk-load generic SECP Artifact Signers from encrypted v3 keystores that can be used by Commit
* Boost API. It uses compressed public key as identifier and apply SHA256 digest on the message
* before signing.
*
* @param keystoresPath Path to the directory containing the v3 keystores
* @param pwrdFileOrDirPath Path to the password file or directory containing the passwords for
* the v3 keystores
* @return MappedResults containing the loaded ArtifactSigners
*/
public static MappedResults<ArtifactSigner> loadECDSAProxyKeystores(
final Path keystoresPath, final Path pwrdFileOrDirPath) {
return loadV3KeystoresUsingPasswordFileOrDir(keystoresPath, pwrdFileOrDirPath, false);
}

private static MappedResults<ArtifactSigner> loadV3KeystoresUsingPasswordFileOrDir(
final Path keystoresPath,
final Path pwrdFileOrDirPath,
final boolean ethereumSECPCompatible) {
if (!Files.exists(pwrdFileOrDirPath)) {
LOG.error("Password file or directory doesn't exist.");
return MappedResults.errorResult();
Expand Down Expand Up @@ -67,12 +98,16 @@ public static MappedResults<ArtifactSigner> loadV3KeystoresUsingPasswordFileOrDi
}

return keystoresFiles.parallelStream()
.map(keystoreFile -> createSecpArtifactSigner(keystoreFile, passwordReader))
.map(
keystoreFile ->
createSecpArtifactSigner(keystoreFile, passwordReader, ethereumSECPCompatible))
.reduce(MappedResults.newSetInstance(), MappedResults::merge);
}

private static MappedResults<ArtifactSigner> createSecpArtifactSigner(
final Path v3KeystorePath, final PasswordReader passwordReader) {
final Path v3KeystorePath,
final PasswordReader passwordReader,
final boolean ethereumSECPCompatible) {
try {
final String fileNameWithoutExt =
FilenameUtils.removeExtension(v3KeystorePath.getFileName().toString());
Expand All @@ -81,8 +116,11 @@ private static MappedResults<ArtifactSigner> createSecpArtifactSigner(

final Credentials credentials =
WalletUtils.loadCredentials(password, v3KeystorePath.toFile());
final EthSecpArtifactSigner artifactSigner =
new EthSecpArtifactSigner(new CredentialSigner(credentials));
final ArtifactSigner artifactSigner =
ethereumSECPCompatible
? new EthSecpArtifactSigner(new CredentialSigner(credentials))
: new K256ArtifactSigner(credentials.getEcKeyPair());

return MappedResults.newInstance(Set.of(artifactSigner), 0);
} catch (final IOException | CipherException | RuntimeException e) {
LOG.error("Error loading v3 keystore {}", v3KeystorePath, e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 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.signing;

import static org.assertj.core.api.Assertions.assertThat;

import tech.pegasys.web3signer.K256TestUtil;

import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.Test;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;

class K256ArtifactSignerTest {
private static final String PRIVATE_KEY_HEX =
"8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63";
private static final Bytes OBJECT_ROOT =
Bytes.fromHexString("419a4f6b748659b3ac4fc3534f3767fffe78127d210af0b2e1c1c8e7b345cf64");
private static final ECKeyPair EC_KEY_PAIR = ECKeyPair.create(Numeric.toBigInt(PRIVATE_KEY_HEX));

@Test
void signCreatesVerifiableSignature() {
// generate using K256ArtifactSigner
final K256ArtifactSigner k256ArtifactSigner = new K256ArtifactSigner(EC_KEY_PAIR);
final ArtifactSignature artifactSignature = k256ArtifactSigner.sign(OBJECT_ROOT);
final byte[] signature = Bytes.fromHexString(artifactSignature.asHex()).toArray();

// Verify the signature against public key
assertThat(
K256TestUtil.verifySignature(
Sign.publicPointFromPrivate(EC_KEY_PAIR.getPrivateKey()),
OBJECT_ROOT.toArray(),
signature))
.isTrue();

// copied from Rust K-256 and Python ecdsa module
final Bytes expectedSignature =
Bytes.fromHexString(
"8C32902BE980399CA59FCC222CCF0A5FE355A159122DEA58789A3938E29D89797FC6C9C0ECCCD29705915729F5326BB7D245F8E54D3A793A06DE3C92ABA85057");
assertThat(Bytes.fromHexString(artifactSignature.asHex())).isEqualTo(expectedSignature);
}
}
Loading

0 comments on commit 98b3be5

Please sign in to comment.