From d2804216368425762cc3a4e37d58050817c55db4 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 3 Aug 2023 11:49:18 +1000 Subject: [PATCH] Remote Key signing using Aws KMS (#837) signing/secp256k1/aws AwsKmsSigner.java: Implements Signer interface. For each kms configuration metadata file, an instance of this class will be registered. AwsKmsSignerFactory.java: Create an instance of AwsKmsSigner based on AwsKmsMetadata. The instance of this class is initialized in Eth1Runner and FilecoinRunner. This class utilizes CachedAwsKmsClientFactory. AwsKmsClient.java: Wraps Aws Kms Client library. Exposes sign and get ECPublicKey. Used by AwsKmsSigner CachedAwsKmsClientFactory.java: Factory class for providing cached instances of AwsKmsClient. The AwsKmsClientKey.java is used as a key for cache entry. The credentials and region are used for equals and hashcode. It is anticipated that same credentials/region will be specified in AwsKmsMetadata yaml files to perform various operations of multiple keys. signing/secp256k1/util Eth1SignatureUtil.java - Refactored signature calculation for internal usage i.e. R, S and V. Azure return P1363Encoded signature while AWS returns ANS.1/DER encoded signature. common/config AwsAuthenticationMode.java is moved into common as this file is common both for Aws Secrets Manager and Aws Kms Client. In addition, AwsCredentials.java is also created in common. This file is currently used by Aws Kms Client, however, in future, it should also be used by Aws Secrets Manager related classes. signing/config AwsCredentialsProviderFactory.java: Factory class that return AWS library's AwsCredentialsProvider from AwsAuthenticationMode and AwsCredentials (which are derived from metadata deserializer). signing/config/metadata AwsKmsMetadata.java - Represents new configuration type aws-kms. Supports only SECP256K1. AwsKmsMetadataDeserializer.java is responsible to convert yaml -> AwsKmsMetadata during metadata files loading. It performs validation of required fields. Very similar to existingAwsKeySigningMetadataDeserializer.java. core Eth1Runner and FilecoinRunner are modified to construct AwsKmsSignerFactory with awsKmsClientCacheSize and applySha3Hash flag. commandline/subcommands Eth1SubCommand.java and FilecoinSubCommand.java are modified to introduce cli option --aws-kms-client-cache-size which is used to construct AwsKmsSignerFactory in the runner classes mentioned above. --- CHANGELOG.md | 1 + acceptance-tests/build.gradle | 2 + .../dsl/utils/MetadataFileHelpers.java | 35 ++++ .../AwsSecretsManagerAcceptanceTest.java | 2 +- ...ecretsManagerMultiValueAcceptanceTest.java | 2 +- ...cretsManagerPerformanceAcceptanceTest.java | 2 +- .../signing/SecpSigningAcceptanceTest.java | 113 +++++++++++ .../PicoCliAwsSecretsManagerParameters.java | 2 +- .../subcommands/Eth1SubCommand.java | 21 ++ .../subcommands/Eth2SubCommand.java | 2 +- .../subcommands/FilecoinSubCommand.java | 20 +- .../commandline/CommandlineParserTest.java | 2 +- .../common}/config/AwsAuthenticationMode.java | 2 +- .../common/config/AwsCredentials.java | 89 +++++++++ .../jsonrpcproxy/support/TestEth1Config.java | 5 + .../pegasys/web3signer/core/Eth1Runner.java | 5 + .../web3signer/core/FilecoinRunner.java | 14 +- .../web3signer/core/config/Eth1Config.java | 2 + gradle/versions.gradle | 3 +- keystorage/build.gradle | 3 +- signing/build.gradle | 4 + .../config/AwsCredentialsProviderFactory.java | 79 ++++++++ .../config/AwsSecretsManagerParameters.java | 2 + .../metadata/ArtifactSignerFactory.java | 5 + .../metadata/AwsKeySigningMetadata.java | 2 +- .../AwsKeySigningMetadataDeserializer.java | 2 +- .../config/metadata/AwsKmsMetadata.java | 72 +++++++ .../metadata/AwsKmsMetadataDeserializer.java | 155 +++++++++++++++ .../Secp256k1ArtifactSignerFactory.java | 9 + .../config/metadata/SigningMetadata.java | 3 +- .../signing/secp256k1/aws/AwsKmsClient.java | 94 +++++++++ .../secp256k1/aws/AwsKmsClientKey.java | 65 +++++++ .../signing/secp256k1/aws/AwsKmsSigner.java | 53 +++++ .../secp256k1/aws/AwsKmsSignerFactory.java | 63 ++++++ .../aws/CachedAwsKmsClientFactory.java | 66 +++++++ .../secp256k1/azure/AzureKeyVaultSigner.java | 41 +--- .../secp256k1/util/Eth1SignatureUtil.java | 133 +++++++++++++ ...AwsKeySigningMetadataDeserializerTest.java | 2 +- .../AwsKmsMetadataDeserializerTest.java | 181 ++++++++++++++++++ .../secp256k1/aws/AwsKmsSignerTest.java | 161 ++++++++++++++++ .../aws/CachedAwsKmsClientFactoryTest.java | 120 ++++++++++++ .../secp256k1/util/Eth1SignatureUtilTest.java | 93 +++++++++ .../AwsSecretsManagerParametersBuilder.java | 2 + 43 files changed, 1679 insertions(+), 55 deletions(-) rename {signing/src/main/java/tech/pegasys/web3signer/signing => common/src/main/java/tech/pegasys/web3signer/common}/config/AwsAuthenticationMode.java (93%) create mode 100644 common/src/main/java/tech/pegasys/web3signer/common/config/AwsCredentials.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsCredentialsProviderFactory.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadata.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializer.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClient.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClientKey.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSigner.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerFactory.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactory.java create mode 100644 signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java create mode 100644 signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializerTest.java create mode 100644 signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java create mode 100644 signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactoryTest.java create mode 100644 signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtilTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e3877ad..5eae2e95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Eth2 Azure command line option --azure-secrets-tags is now deprecated and is replaced with --azure-tags. The --azure-secrets-tags option will be removed in a future release. ### Features Added +- Add support for SECP256K1 remote signing using AWS Key Management Service. [#501](https://github.com/Consensys/web3signer/issues/501) - Azure bulk mode support for loading multiline (`\n` delimited, up to 200) keys per secret. - Hashicorp connection properties can now override http protocol to HTTP/1.1 from the default of HTTP/2. [#817](https://github.com/ConsenSys/web3signer/pull/817) - Add --key-config-path as preferred alias to --key-store-path [#826](https://github.com/Consensys/web3signer/pull/826) diff --git a/acceptance-tests/build.gradle b/acceptance-tests/build.gradle index 802501abf..cf178f66b 100644 --- a/acceptance-tests/build.gradle +++ b/acceptance-tests/build.gradle @@ -25,6 +25,7 @@ dependencies { testImplementation project(":core") testImplementation project(":signing") testImplementation project(":commandline") + testImplementation project(":common") testImplementation 'org.apache.tuweni:tuweni-bytes' testImplementation 'org.apache.tuweni:tuweni-units' @@ -84,6 +85,7 @@ dependencies { implementation 'software.amazon.awssdk:auth' implementation 'software.amazon.awssdk:secretsmanager' + implementation 'software.amazon.awssdk:kms' } test.enabled = false diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/MetadataFileHelpers.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/MetadataFileHelpers.java index 5091d3f41..9c3c8eb06 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/MetadataFileHelpers.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/utils/MetadataFileHelpers.java @@ -25,12 +25,16 @@ import tech.pegasys.signers.bls.keystore.model.Pbkdf2Param; import tech.pegasys.signers.bls.keystore.model.SCryptParam; import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.dsl.HashicorpSigningParams; import tech.pegasys.web3signer.keystore.hashicorp.dsl.certificates.CertificateHelpers; import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadata; +import tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer; import java.io.IOException; import java.io.Serializable; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.security.SecureRandom; @@ -262,6 +266,37 @@ public void createAwsYamlFileAt( } } + public void createAwsKmsYamlFileAt( + final Path metadataFilePath, + final String awsRegion, + final String accessKeyId, + final String secretAccessKey, + final Optional sessionToken, + final Optional endpointOverride, + final String kmsKeyId) { + try { + final Map signingMetadata = new HashMap<>(); + + signingMetadata.put("type", AwsKmsMetadata.TYPE); + signingMetadata.put( + AwsKmsMetadataDeserializer.AUTH_MODE, AwsAuthenticationMode.SPECIFIED.toString()); + signingMetadata.put(AwsKmsMetadataDeserializer.REGION, awsRegion); + signingMetadata.put(AwsKmsMetadataDeserializer.ACCESS_KEY_ID, accessKeyId); + signingMetadata.put(AwsKmsMetadataDeserializer.SECRET_ACCESS_KEY, secretAccessKey); + sessionToken.ifPresent( + token -> signingMetadata.put(AwsKmsMetadataDeserializer.SESSION_TOKEN, token)); + endpointOverride.ifPresent( + endpoint -> + signingMetadata.put( + AwsKmsMetadataDeserializer.ENDPOINT_OVERRIDE, endpoint.toString())); + signingMetadata.put(AwsKmsMetadataDeserializer.KMS_KEY_ID, kmsKeyId); + + createYamlFile(metadataFilePath, signingMetadata); + } catch (final Exception e) { + throw new RuntimeException("Unable to construct aws-kms yaml file", e); + } + } + private void createPasswordFile(final Path passwordFilePath, final String password) { try { Files.writeString(passwordFilePath, password); diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerAcceptanceTest.java index 2f1c8b369..ad407da1e 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerAcceptanceTest.java @@ -18,9 +18,9 @@ import tech.pegasys.teku.bls.BLSKeyPair; import tech.pegasys.web3signer.AwsSecretsManagerUtil; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; import tech.pegasys.web3signer.signing.KeyType; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParameters; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParametersBuilder; import tech.pegasys.web3signer.tests.AcceptanceTestBase; diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerMultiValueAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerMultiValueAcceptanceTest.java index d51501654..44bbb0e00 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerMultiValueAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerMultiValueAcceptanceTest.java @@ -18,9 +18,9 @@ import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.web3signer.AwsSecretsManagerUtil; import tech.pegasys.web3signer.BLSTestUtil; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; import tech.pegasys.web3signer.signing.KeyType; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParameters; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParametersBuilder; import tech.pegasys.web3signer.tests.AcceptanceTestBase; diff --git a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerPerformanceAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerPerformanceAcceptanceTest.java index a94378f2f..1a431aeec 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerPerformanceAcceptanceTest.java +++ b/acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/bulkloading/AwsSecretsManagerPerformanceAcceptanceTest.java @@ -16,9 +16,9 @@ import tech.pegasys.teku.bls.BLSKeyPair; import tech.pegasys.web3signer.AwsSecretsManagerUtil; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.dsl.signer.SignerConfigurationBuilder; import tech.pegasys.web3signer.signing.KeyType; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParameters; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParametersBuilder; import tech.pegasys.web3signer.tests.AcceptanceTestBase; 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 cbc8de83c..71e39dabd 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 @@ -17,20 +17,28 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.web3j.crypto.Sign.signedMessageToKey; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsCredentials; import tech.pegasys.web3signer.dsl.HashicorpSigningParams; import tech.pegasys.web3signer.dsl.utils.MetadataFileHelpers; import tech.pegasys.web3signer.keystore.hashicorp.dsl.HashicorpNode; import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.config.AwsCredentialsProviderFactory; import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.signing.secp256k1.aws.AwsKmsClient; +import tech.pegasys.web3signer.signing.secp256k1.aws.CachedAwsKmsClientFactory; import java.io.File; import java.math.BigInteger; +import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.security.SignatureException; import java.security.interfaces.ECPublicKey; +import java.util.Map; import java.util.Optional; +import com.google.common.collect.Maps; import com.google.common.io.Resources; import io.restassured.response.Response; import org.apache.tuweni.bytes.Bytes; @@ -38,6 +46,11 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; import org.web3j.crypto.Sign.SignatureData; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.services.kms.model.CreateKeyRequest; +import software.amazon.awssdk.services.kms.model.KeySpec; +import software.amazon.awssdk.services.kms.model.KeyUsageType; +import software.amazon.awssdk.services.kms.model.ScheduleKeyDeletionRequest; public class SecpSigningAcceptanceTest extends SigningAcceptanceTestBase { @@ -110,6 +123,57 @@ public void signDataWithKeyInAzure() { signAndVerifySignature(AZURE_PUBLIC_KEY_HEX_STRING); } + @Test + @EnabledIfEnvironmentVariables({ + @EnabledIfEnvironmentVariable( + named = "RW_AWS_ACCESS_KEY_ID", + matches = ".*", + disabledReason = "RW_AWS_ACCESS_KEY_ID env variable is required"), + @EnabledIfEnvironmentVariable( + named = "RW_AWS_SECRET_ACCESS_KEY", + matches = ".*", + disabledReason = "RW_AWS_SECRET_ACCESS_KEY env variable is required"), + @EnabledIfEnvironmentVariable( + named = "AWS_ACCESS_KEY_ID", + matches = ".*", + disabledReason = "AWS_ACCESS_KEY_ID env variable is required"), + @EnabledIfEnvironmentVariable( + named = "AWS_SECRET_ACCESS_KEY", + matches = ".*", + disabledReason = "AWS_SECRET_ACCESS_KEY env variable is required"), + }) + public void remoteSignWithAwsKMS() { + final String roAwsAccessKeyId = System.getenv("AWS_ACCESS_KEY_ID"); + final String roAwsSecretAccessKey = System.getenv("AWS_SECRET_ACCESS_KEY"); + final Optional awsSessionToken = + Optional.ofNullable(System.getenv("AWS_SESSION_TOKEN")); + // default region to us-east-2 if environment variable is not defined. + final String region = Optional.ofNullable(System.getenv("AWS_REGION")).orElse("us-east-2"); + // can be pointed to localstack + final Optional awsEndpointOverride = + Optional.ofNullable(System.getenv("AWS_ENDPOINT_OVERRIDE")).map(URI::create); + + final Map.Entry remoteAWSKMSKey = createRemoteAWSKMSKey(); + final String awsKeyId = remoteAWSKMSKey.getKey(); + final ECPublicKey ecPublicKey = remoteAWSKMSKey.getValue(); + + try { + METADATA_FILE_HELPERS.createAwsKmsYamlFileAt( + testDirectory.resolve("aws_kms_test.yaml"), + region, + roAwsAccessKeyId, + roAwsSecretAccessKey, + awsSessionToken, + awsEndpointOverride, + awsKeyId); + + signAndVerifySignature(EthPublicKeyUtils.toHexString(ecPublicKey)); + + } finally { + markAwsKeyForDeletion(region, awsEndpointOverride, awsKeyId); + } + } + private void signAndVerifySignature() { signAndVerifySignature(PUBLIC_KEY_HEX_STRING); } @@ -141,4 +205,53 @@ private BigInteger recoverPublicKey(final SignatureData signature) { throw new IllegalStateException("signature cannot be recovered", e); } } + + private static Map.Entry createRemoteAWSKMSKey() { + final String region = Optional.ofNullable(System.getenv("AWS_REGION")).orElse("us-east-2"); + final Optional awsEndpointOverride = + System.getenv("AWS_ENDPOINT_OVERRIDE") != null + ? Optional.of(URI.create(System.getenv("AWS_ENDPOINT_OVERRIDE"))) + : Optional.empty(); + + final AwsCredentialsProvider rwAwsCredentialsProvider = + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, Optional.of(getAwsCredentialsFromEnvVar())); + final AwsKmsClient rwKmsClient = + new CachedAwsKmsClientFactory(1) + .createKmsClient(rwAwsCredentialsProvider, region, awsEndpointOverride); + // create a test key + final CreateKeyRequest web3SignerTestingKey = + CreateKeyRequest.builder() + .keySpec(KeySpec.ECC_SECG_P256_K1) + .description("Web3Signer Testing Key") + .keyUsage(KeyUsageType.SIGN_VERIFY) + .build(); + + final String testKeyId = rwKmsClient.createKey(web3SignerTestingKey); + final ECPublicKey ecPublicKey = rwKmsClient.getECPublicKey(testKeyId); + return Maps.immutableEntry(testKeyId, ecPublicKey); + } + + private static void markAwsKeyForDeletion( + String region, Optional awsEndpointOverride, String awsKeyId) { + // mark aws key for deletion + ScheduleKeyDeletionRequest deletionRequest = + ScheduleKeyDeletionRequest.builder().keyId(awsKeyId).pendingWindowInDays(7).build(); + final AwsCredentialsProvider rwAwsCredentialsProvider = + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, Optional.of(getAwsCredentialsFromEnvVar())); + + final AwsKmsClient rwKmsClient = + new CachedAwsKmsClientFactory(1) + .createKmsClient(rwAwsCredentialsProvider, region, awsEndpointOverride); + rwKmsClient.scheduleKeyDeletion(deletionRequest); + } + + private static AwsCredentials getAwsCredentialsFromEnvVar() { + return AwsCredentials.builder() + .withAccessKeyId(System.getenv("RW_AWS_ACCESS_KEY_ID")) + .withSecretAccessKey(System.getenv("RW_AWS_SECRET_ACCESS_KEY")) + .withSessionToken(System.getenv("AWS_SESSION_TOKEN")) + .build(); + } } diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliAwsSecretsManagerParameters.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliAwsSecretsManagerParameters.java index 43de68870..a3e1f6a63 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliAwsSecretsManagerParameters.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/PicoCliAwsSecretsManagerParameters.java @@ -12,7 +12,7 @@ */ package tech.pegasys.web3signer.commandline; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParameters; import java.net.URI; diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth1SubCommand.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth1SubCommand.java index 49847c4c8..47f296ab4 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth1SubCommand.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth1SubCommand.java @@ -140,6 +140,8 @@ public void setDownstreamHttpPath(final String path) { arity = "1") private String httpProxyPassword = null; + private long awsKmsClientCacheSize = 1; + @CommandLine.Mixin private PicoCliClientTlsOptions clientTlsOptions; @CommandLine.Mixin private PicoCliEth1AzureKeyVaultParameters azureKeyVaultParameters; @@ -213,4 +215,23 @@ public ChainIdProvider getChainId() { public AzureKeyVaultParameters getAzureKeyVaultConfig() { return azureKeyVaultParameters; } + + @CommandLine.Option( + names = {"--aws-kms-client-cache-size"}, + paramLabel = "", + defaultValue = "1", + description = + "AWS Kms Client cache size. Should be set based on different set of credentials and region (default: ${DEFAULT-VALUE})") + public void setAwsKmsClientCacheSize(long awsKmsClientCacheSize) { + if (awsKmsClientCacheSize < 1) { + throw new CommandLine.ParameterException( + spec.commandLine(), "--aws-kms-client-cache-size must be positive"); + } + this.awsKmsClientCacheSize = awsKmsClientCacheSize; + } + + @Override + public long getAwsKmsClientCacheSize() { + return awsKmsClientCacheSize; + } } diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java index 58c721a3a..2e5303f32 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/Eth2SubCommand.java @@ -29,9 +29,9 @@ import tech.pegasys.web3signer.commandline.PicoCliEth2AzureKeyVaultParameters; import tech.pegasys.web3signer.commandline.PicoCliSlashingProtectionParameters; import tech.pegasys.web3signer.commandline.config.PicoKeystoresParameters; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.core.Eth2Runner; import tech.pegasys.web3signer.core.Runner; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.KeystoresParameters; import tech.pegasys.web3signer.slashingprotection.SlashingProtectionParameters; diff --git a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/FilecoinSubCommand.java b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/FilecoinSubCommand.java index 52371cc3c..cfbd7497e 100644 --- a/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/FilecoinSubCommand.java +++ b/commandline/src/main/java/tech/pegasys/web3signer/commandline/subcommands/FilecoinSubCommand.java @@ -16,6 +16,7 @@ import tech.pegasys.web3signer.core.Runner; import tech.pegasys.web3signer.signing.filecoin.FilecoinNetwork; +import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.HelpCommand; import picocli.CommandLine.Option; @@ -28,6 +29,7 @@ public class FilecoinSubCommand extends ModeSubCommand { public static final String COMMAND_NAME = "filecoin"; + @CommandLine.Spec private CommandLine.Model.CommandSpec spec; // injected by picocli @Option( names = {"--network"}, @@ -36,9 +38,11 @@ public class FilecoinSubCommand extends ModeSubCommand { arity = "1") private final FilecoinNetwork network = FilecoinNetwork.MAINNET; + private long awsKmsClientCacheSize = 1; + @Override public Runner createRunner() { - return new FilecoinRunner(config, network); + return new FilecoinRunner(config, network, awsKmsClientCacheSize); } @Override @@ -50,4 +54,18 @@ public String getCommandName() { protected void validateArgs() { // no special validation required } + + @CommandLine.Option( + names = {"--aws-kms-client-cache-size"}, + paramLabel = "", + defaultValue = "1", + description = + "AWS Kms Client cache size. Should be set based on different set of credentials and region (default: ${DEFAULT-VALUE})") + public void setAwsKmsClientCacheSize(long awsKmsClientCacheSize) { + if (awsKmsClientCacheSize < 1) { + throw new CommandLine.ParameterException( + spec.commandLine(), "--aws-kms-client-cache-size must be positive"); + } + this.awsKmsClientCacheSize = awsKmsClientCacheSize; + } } diff --git a/commandline/src/test/java/tech/pegasys/web3signer/commandline/CommandlineParserTest.java b/commandline/src/test/java/tech/pegasys/web3signer/commandline/CommandlineParserTest.java index e5c39fdb7..80bc077aa 100644 --- a/commandline/src/test/java/tech/pegasys/web3signer/commandline/CommandlineParserTest.java +++ b/commandline/src/test/java/tech/pegasys/web3signer/commandline/CommandlineParserTest.java @@ -25,11 +25,11 @@ import static tech.pegasys.web3signer.commandline.PicoCliAwsSecretsManagerParameters.AWS_SECRETS_TAG_VALUES_FILTER_OPTION; import tech.pegasys.web3signer.commandline.subcommands.Eth2SubCommand; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.core.Context; import tech.pegasys.web3signer.core.Runner; import tech.pegasys.web3signer.core.config.BaseConfig; import tech.pegasys.web3signer.signing.ArtifactSignerProvider; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.DefaultArtifactSignerProvider; import java.io.PrintWriter; diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsAuthenticationMode.java b/common/src/main/java/tech/pegasys/web3signer/common/config/AwsAuthenticationMode.java similarity index 93% rename from signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsAuthenticationMode.java rename to common/src/main/java/tech/pegasys/web3signer/common/config/AwsAuthenticationMode.java index b557a41a5..aa21be3e4 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsAuthenticationMode.java +++ b/common/src/main/java/tech/pegasys/web3signer/common/config/AwsAuthenticationMode.java @@ -10,7 +10,7 @@ * 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.config; +package tech.pegasys.web3signer.common.config; public enum AwsAuthenticationMode { ENVIRONMENT, diff --git a/common/src/main/java/tech/pegasys/web3signer/common/config/AwsCredentials.java b/common/src/main/java/tech/pegasys/web3signer/common/config/AwsCredentials.java new file mode 100644 index 000000000..d73e5ff9f --- /dev/null +++ b/common/src/main/java/tech/pegasys/web3signer/common/config/AwsCredentials.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023 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.common.config; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Objects; +import java.util.Optional; + +public class AwsCredentials { + private String accessKeyId; + private String secretAccessKey; + private Optional sessionToken; + + public static AwsCredentialsBuilder builder() { + return new AwsCredentialsBuilder(); + } + + public String getAccessKeyId() { + return accessKeyId; + } + + public String getSecretAccessKey() { + return secretAccessKey; + } + + public Optional getSessionToken() { + return sessionToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AwsCredentials that = (AwsCredentials) o; + return Objects.equals(accessKeyId, that.accessKeyId) + && Objects.equals(secretAccessKey, that.secretAccessKey) + && Objects.equals(sessionToken, that.sessionToken); + } + + @Override + public int hashCode() { + return Objects.hash(accessKeyId, secretAccessKey, sessionToken); + } + + public static final class AwsCredentialsBuilder { + private String accessKeyId; + private String secretAccessKey; + private String sessionToken; + + private AwsCredentialsBuilder() {} + + public AwsCredentialsBuilder withAccessKeyId(final String accessKeyId) { + this.accessKeyId = accessKeyId; + return this; + } + + public AwsCredentialsBuilder withSecretAccessKey(final String secretAccessKey) { + this.secretAccessKey = secretAccessKey; + return this; + } + + public AwsCredentialsBuilder withSessionToken(final String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + public AwsCredentials build() { + checkArgument(accessKeyId != null, "Access Key Id must be provided"); + checkArgument(secretAccessKey != null, "Secret Access Key must be provided"); + + final AwsCredentials awsCredentials = new AwsCredentials(); + awsCredentials.accessKeyId = this.accessKeyId; + awsCredentials.secretAccessKey = this.secretAccessKey; + awsCredentials.sessionToken = Optional.ofNullable(this.sessionToken); + return awsCredentials; + } + } +} diff --git a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TestEth1Config.java b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TestEth1Config.java index eddbf8ecb..3f72270eb 100644 --- a/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TestEth1Config.java +++ b/core/src/integrationTest/java/tech/pegasys/web3signer/core/jsonrpcproxy/support/TestEth1Config.java @@ -96,4 +96,9 @@ public ChainIdProvider getChainId() { public AzureKeyVaultParameters getAzureKeyVaultConfig() { return new DefaultAzureKeyVaultParameters("", "", "", ""); } + + @Override + public long getAwsKmsClientCacheSize() { + return 1; + } } diff --git a/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java b/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java index a28ef7831..ea670893f 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/Eth1Runner.java @@ -54,6 +54,7 @@ import tech.pegasys.web3signer.signing.config.metadata.parser.YamlMapperFactory; import tech.pegasys.web3signer.signing.config.metadata.parser.YamlSignerParser; import tech.pegasys.web3signer.signing.config.metadata.yubihsm.YubiHsmOpaqueDataProvider; +import tech.pegasys.web3signer.signing.secp256k1.aws.AwsKmsSignerFactory; import tech.pegasys.web3signer.signing.secp256k1.azure.AzureKeyVaultSignerFactory; import java.util.ArrayList; @@ -179,6 +180,9 @@ private MappedResults loadSignersFromKeyConfigFiles( final AzureKeyVaultFactory azureKeyVaultFactory, final AzureKeyVaultSignerFactory azureSignerFactory) { final HashicorpConnectionFactory hashicorpConnectionFactory = new HashicorpConnectionFactory(); + final boolean applySha3Hash = true; + final AwsKmsSignerFactory awsKmsSignerFactory = + new AwsKmsSignerFactory(eth1Config.getAwsKmsClientCacheSize(), applySha3Hash); try (final InterlockKeyProvider interlockKeyProvider = new InterlockKeyProvider(vertx); final YubiHsmOpaqueDataProvider yubiHsmOpaqueDataProvider = new YubiHsmOpaqueDataProvider()) { @@ -192,6 +196,7 @@ private MappedResults loadSignersFromKeyConfigFiles( yubiHsmOpaqueDataProvider, EthSecpArtifactSigner::new, azureKeyVaultFactory, + awsKmsSignerFactory, true); return new SignerLoader(baseConfig.keystoreParallelProcessingEnabled()) diff --git a/core/src/main/java/tech/pegasys/web3signer/core/FilecoinRunner.java b/core/src/main/java/tech/pegasys/web3signer/core/FilecoinRunner.java index e5a9e8532..331c339e3 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/FilecoinRunner.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/FilecoinRunner.java @@ -38,6 +38,7 @@ import tech.pegasys.web3signer.signing.config.metadata.parser.YamlSignerParser; import tech.pegasys.web3signer.signing.config.metadata.yubihsm.YubiHsmOpaqueDataProvider; import tech.pegasys.web3signer.signing.filecoin.FilecoinNetwork; +import tech.pegasys.web3signer.signing.secp256k1.aws.AwsKmsSignerFactory; import tech.pegasys.web3signer.signing.secp256k1.azure.AzureKeyVaultSignerFactory; import java.util.List; @@ -53,10 +54,15 @@ public class FilecoinRunner extends Runner { private static final int AWS_CACHE_MAXIMUM_SIZE = 1; private static final String FC_JSON_RPC_PATH = "/rpc/v0"; private final FilecoinNetwork network; + private final long awsKmsClientCacheSize; - public FilecoinRunner(final BaseConfig baseConfig, final FilecoinNetwork network) { + public FilecoinRunner( + final BaseConfig baseConfig, + final FilecoinNetwork network, + final long awsKmsClientCacheSize) { super(baseConfig); this.network = network; + this.awsKmsClientCacheSize = awsKmsClientCacheSize; } @Override @@ -109,6 +115,9 @@ protected ArtifactSignerProvider createArtifactSignerProvider( registerClose(azureKeyVaultFactory::close); final AzureKeyVaultSignerFactory azureSignerFactory = new AzureKeyVaultSignerFactory(azureKeyVaultFactory); + final boolean applySha3Hash = false; + final AwsKmsSignerFactory awsKmsSignerFactory = + new AwsKmsSignerFactory(awsKmsClientCacheSize, applySha3Hash); try (final HashicorpConnectionFactory hashicorpConnectionFactory = new HashicorpConnectionFactory(); @@ -138,7 +147,8 @@ protected ArtifactSignerProvider createArtifactSignerProvider( yubiHsmOpaqueDataProvider, signer -> new FcSecpArtifactSigner(signer, network), azureKeyVaultFactory, - false); + awsKmsSignerFactory, + applySha3Hash); return new SignerLoader(baseConfig.keystoreParallelProcessingEnabled()) .load( diff --git a/core/src/main/java/tech/pegasys/web3signer/core/config/Eth1Config.java b/core/src/main/java/tech/pegasys/web3signer/core/config/Eth1Config.java index febccb5d3..2757eec5e 100644 --- a/core/src/main/java/tech/pegasys/web3signer/core/config/Eth1Config.java +++ b/core/src/main/java/tech/pegasys/web3signer/core/config/Eth1Config.java @@ -42,4 +42,6 @@ public interface Eth1Config { ChainIdProvider getChainId(); AzureKeyVaultParameters getAzureKeyVaultConfig(); + + long getAwsKmsClientCacheSize(); } diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 210489dcb..4c9664270 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -153,11 +153,12 @@ dependencyManagement { dependency 'com.github.ipld:java-cid:1.3.3' dependency 'net.jodah:failsafe:2.4.4' - dependencySet(group: 'software.amazon.awssdk', version: '2.20.12') { + dependencySet(group: 'software.amazon.awssdk', version: '2.20.101') { entry 'bom' entry 'auth' entry 'secretsmanager' entry 'sts' + entry 'kms' } dependency 'io.rest-assured:rest-assured:4.4.0' diff --git a/keystorage/build.gradle b/keystorage/build.gradle index 5f8579d28..045f46657 100644 --- a/keystorage/build.gradle +++ b/keystorage/build.gradle @@ -31,7 +31,7 @@ testFixturesJar { } dependencies { - + implementation project(":common") implementation 'com.azure:azure-identity' implementation 'com.azure:azure-security-keyvault-keys' implementation 'com.azure:azure-security-keyvault-secrets' @@ -48,6 +48,7 @@ dependencies { implementation 'org.xipki.iaik:sunpkcs11-wrapper' implementation 'software.amazon.awssdk:auth' implementation 'software.amazon.awssdk:secretsmanager' + implementation 'software.amazon.awssdk:kms' runtimeOnly 'software.amazon.awssdk:sts' runtimeOnly 'org.apache.logging.log4j:log4j-core' diff --git a/signing/build.gradle b/signing/build.gradle index 94af373ae..f3866810a 100644 --- a/signing/build.gradle +++ b/signing/build.gradle @@ -38,6 +38,8 @@ dependencies { implementation 'org.apache.tuweni:tuweni-net' implementation 'com.google.guava:guava' implementation 'org.bouncycastle:bcprov-jdk18on' + implementation 'software.amazon.awssdk:auth' + implementation 'software.amazon.awssdk:kms' runtimeOnly 'com.squareup.okhttp3:okhttp' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl' @@ -60,4 +62,6 @@ dependencies { testFixturesImplementation 'tech.pegasys.signers.internal:bls-keystore' testFixturesImplementation 'software.amazon.awssdk:auth' testFixturesImplementation 'software.amazon.awssdk:secretsmanager' + testFixturesImplementation 'software.amazon.awssdk:kms' + testFixturesImplementation project(":common") } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsCredentialsProviderFactory.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsCredentialsProviderFactory.java new file mode 100644 index 000000000..0e57f7f8d --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsCredentialsProviderFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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.config; + +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsCredentials; + +import java.util.Optional; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; + +public class AwsCredentialsProviderFactory { + /** + * Create AWS DefaultCredentialsProvider or StaticCredentialsProvider + * + * @param authMode ENVIRONMENT or SPECIFIED + * @param awsCredentials optional aws credentials. Must be present for SPECIFIED mode + * @return an instance of AwsCredentialsProvider + */ + public static AwsCredentialsProvider createAwsCredentialsProvider( + final AwsAuthenticationMode authMode, final Optional awsCredentials) { + final AwsCredentialsProvider awsCredentialsProvider; + switch (authMode) { + case ENVIRONMENT: + awsCredentialsProvider = DefaultCredentialsProvider.create(); + break; + case SPECIFIED: + awsCredentialsProvider = + getStaticCredentialsProvider( + awsCredentials.orElseThrow( + () -> + new IllegalArgumentException( + "AWS Credentials must be provided for SPECIFIED mode"))); + break; + default: + throw new IllegalStateException("Aws Auth mode not implemented: " + authMode); + } + + return awsCredentialsProvider; + } + + /** + * Create static credentials provider using session credentials or basic credentials + * + * @param awsCredentials AwsCredentials + * @return instance of AwsCredentialsProvider + */ + private static AwsCredentialsProvider getStaticCredentialsProvider( + final AwsCredentials awsCredentials) { + final software.amazon.awssdk.auth.credentials.AwsCredentials awsSdkCredentials; + if (awsCredentials.getSessionToken().isPresent()) { + awsSdkCredentials = + AwsSessionCredentials.create( + awsCredentials.getAccessKeyId(), + awsCredentials.getSecretAccessKey(), + awsCredentials.getSessionToken().get()); + } else { + awsSdkCredentials = + AwsBasicCredentials.create( + awsCredentials.getAccessKeyId(), awsCredentials.getSecretAccessKey()); + } + + return StaticCredentialsProvider.create(awsSdkCredentials); + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParameters.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParameters.java index f1fbe1c23..99573615a 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParameters.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParameters.java @@ -12,6 +12,8 @@ */ package tech.pegasys.web3signer.signing.config; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; + import java.net.URI; import java.util.Collection; import java.util.Collections; diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/ArtifactSignerFactory.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/ArtifactSignerFactory.java index 2e34a716b..4e4593bef 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/ArtifactSignerFactory.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/ArtifactSignerFactory.java @@ -55,4 +55,9 @@ default ArtifactSigner create(final AwsKeySigningMetadata awsKeySigningMetadata) throw new UnsupportedOperationException( "Unable to generate a signer of requested type from supplied metadata"); } + + default ArtifactSigner create(final AwsKmsMetadata awsKmsMetadata) { + throw new UnsupportedOperationException( + "Unable to generate a signer of requested type from supplied metadata"); + } } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadata.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadata.java index c8aa54044..2f81ab2fa 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadata.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadata.java @@ -12,9 +12,9 @@ */ package tech.pegasys.web3signer.signing.config.metadata; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.ArtifactSigner; import tech.pegasys.web3signer.signing.KeyType; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.AwsSecretsManagerParameters; import java.net.URI; diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializer.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializer.java index 359d5af72..5a6e172dd 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializer.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializer.java @@ -12,7 +12,7 @@ */ package tech.pegasys.web3signer.signing.config.metadata; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import java.io.IOException; import java.net.URI; diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadata.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadata.java new file mode 100644 index 000000000..fe9f126c9 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadata.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 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.config.metadata; + +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsCredentials; +import tech.pegasys.web3signer.signing.ArtifactSigner; +import tech.pegasys.web3signer.signing.KeyType; + +import java.net.URI; +import java.util.Optional; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(using = AwsKmsMetadataDeserializer.class) +public class AwsKmsMetadata extends SigningMetadata { + public static final String TYPE = "aws-kms"; + private final AwsAuthenticationMode authenticationMode; + private final String region; + private final Optional awsCredentials; + private final String kmsKeyId; + private final Optional endpointOverride; + + public AwsKmsMetadata( + final AwsAuthenticationMode authenticationMode, + final String region, + final Optional awsCredentials, + final String kmsKeyId, + final Optional endpointOverride) { + super(TYPE, KeyType.SECP256K1); + this.authenticationMode = authenticationMode; + this.region = region; + this.awsCredentials = awsCredentials; + this.kmsKeyId = kmsKeyId; + this.endpointOverride = endpointOverride; + } + + public AwsAuthenticationMode getAuthenticationMode() { + return this.authenticationMode; + } + + public Optional getAwsCredentials() { + return awsCredentials; + } + + public String getKmsKeyId() { + return kmsKeyId; + } + + public String getRegion() { + return region; + } + + @Override + public ArtifactSigner createSigner(final ArtifactSignerFactory factory) { + return factory.create(this); + } + + public Optional getEndpointOverride() { + return endpointOverride; + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializer.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializer.java new file mode 100644 index 000000000..412efca76 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializer.java @@ -0,0 +1,155 @@ +/* + * Copyright 2022 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.config.metadata; + +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsCredentials; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +public class AwsKmsMetadataDeserializer extends StdDeserializer { + + public static final String AUTH_MODE = "authenticationMode"; + public static final String REGION = "region"; + public static final String ACCESS_KEY_ID = "accessKeyId"; + public static final String SECRET_ACCESS_KEY = "secretAccessKey"; + public static final String SESSION_TOKEN = "sessionToken"; + public static final String KMS_KEY_ID = "kmsKeyId"; + public static final String ENDPOINT_OVERRIDE = "endpointOverride"; + + @SuppressWarnings("Unused") + public AwsKmsMetadataDeserializer() { + this(null); + } + + protected AwsKmsMetadataDeserializer(final Class vc) { + super(vc); + } + + @Override + public AwsKmsMetadata deserialize(final JsonParser parser, final DeserializationContext context) + throws IOException { + + AwsAuthenticationMode authMode = AwsAuthenticationMode.SPECIFIED; + String region = null; + + String accessKeyId = null; + String secretAccessKey = null; + String sessionToken = null; + Optional awsCredentials = Optional.empty(); + + String kmsKeyId = null; + Optional endpointOverride = Optional.empty(); + + final JsonNode node = parser.getCodec().readTree(parser); + + if (node.get(AUTH_MODE) != null) { + try { + authMode = AwsAuthenticationMode.valueOf(node.get(AUTH_MODE).asText()); + } catch (final IllegalArgumentException e) { + throw new JsonMappingException(parser, "Invalid value for parameter: " + AUTH_MODE + "."); + } + } + + if (node.get(REGION) != null) { + region = node.get(REGION).asText(); + } + + if (node.get(ACCESS_KEY_ID) != null) { + accessKeyId = node.get(ACCESS_KEY_ID).asText(); + } + + if (node.get(SECRET_ACCESS_KEY) != null) { + secretAccessKey = node.get(SECRET_ACCESS_KEY).asText(); + } + + if (node.get(SESSION_TOKEN) != null) { + sessionToken = node.get(SESSION_TOKEN).asText(); + } + + if (node.get(KMS_KEY_ID) != null) { + kmsKeyId = node.get(KMS_KEY_ID).asText(); + } + + if (node.get(ENDPOINT_OVERRIDE) != null) { + try { + endpointOverride = Optional.of(URI.create(node.get(ENDPOINT_OVERRIDE).asText())); + } catch (final IllegalArgumentException e) { + throw new JsonMappingException( + parser, "Invalid value for parameter: " + ENDPOINT_OVERRIDE + "."); + } + } + + // validate + validate(parser, authMode, region, accessKeyId, secretAccessKey, kmsKeyId); + + if (authMode == AwsAuthenticationMode.SPECIFIED) { + awsCredentials = + Optional.of( + AwsCredentials.builder() + .withAccessKeyId(accessKeyId) + .withSecretAccessKey(secretAccessKey) + .withSessionToken(sessionToken) + .build()); + } + + return new AwsKmsMetadata(authMode, region, awsCredentials, kmsKeyId, endpointOverride); + } + + private void validate( + final JsonParser parser, + final AwsAuthenticationMode authMode, + final String region, + final String accessKeyId, + final String secretAccessKey, + final String kmsKeyId) + throws JsonMappingException { + final List missingParameters = new ArrayList<>(); + + // globally required fields + if (region == null) { + missingParameters.add(REGION); + } + + if (kmsKeyId == null) { + missingParameters.add(KMS_KEY_ID); + } + + // Specified auth mode required fields + if (authMode == AwsAuthenticationMode.SPECIFIED) { + if (accessKeyId == null) { + missingParameters.add(ACCESS_KEY_ID); + } + + if (secretAccessKey == null) { + missingParameters.add(SECRET_ACCESS_KEY); + } + } + + if (!missingParameters.isEmpty()) { + throw new JsonMappingException( + parser, + "Missing values for required parameters: " + String.join(", ", missingParameters)); + } + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/Secp256k1ArtifactSignerFactory.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/Secp256k1ArtifactSignerFactory.java index cadf8f5f9..fc23832b6 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/Secp256k1ArtifactSignerFactory.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/Secp256k1ArtifactSignerFactory.java @@ -19,6 +19,7 @@ import tech.pegasys.web3signer.signing.config.metadata.interlock.InterlockKeyProvider; import tech.pegasys.web3signer.signing.config.metadata.yubihsm.YubiHsmOpaqueDataProvider; import tech.pegasys.web3signer.signing.secp256k1.Signer; +import tech.pegasys.web3signer.signing.secp256k1.aws.AwsKmsSignerFactory; import tech.pegasys.web3signer.signing.secp256k1.azure.AzureConfig; import tech.pegasys.web3signer.signing.secp256k1.azure.AzureKeyVaultSignerFactory; import tech.pegasys.web3signer.signing.secp256k1.filebased.CredentialSigner; @@ -36,6 +37,7 @@ public class Secp256k1ArtifactSignerFactory extends AbstractArtifactSignerFactory { private final AzureKeyVaultSignerFactory azureCloudSignerFactory; + final AwsKmsSignerFactory awsKmsSignerFactory; private final Function signerFactory; private final boolean needToHash; @@ -48,6 +50,7 @@ public Secp256k1ArtifactSignerFactory( final YubiHsmOpaqueDataProvider yubiHsmOpaqueDataProvider, final Function signerFactory, final AzureKeyVaultFactory azureKeyVaultFactory, + final AwsKmsSignerFactory awsKmsSignerFactory, final boolean needToHash) { super( hashicorpConnectionFactory, @@ -56,6 +59,7 @@ public Secp256k1ArtifactSignerFactory( yubiHsmOpaqueDataProvider, azureKeyVaultFactory); this.azureCloudSignerFactory = azureCloudSignerFactory; + this.awsKmsSignerFactory = awsKmsSignerFactory; this.signerFactory = signerFactory; this.needToHash = needToHash; } @@ -123,6 +127,11 @@ public ArtifactSigner create(final YubiHsmSigningMetadata yubiHsmSigningMetadata return createCredentialSigner(credentials); } + @Override + public ArtifactSigner create(final AwsKmsMetadata awsKmsMetadata) { + return signerFactory.apply(awsKmsSignerFactory.createSigner(awsKmsMetadata)); + } + private ArtifactSigner createCredentialSigner(final Credentials credentials) { return signerFactory.apply(new CredentialSigner(credentials, needToHash)); } diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/SigningMetadata.java b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/SigningMetadata.java index f21383f18..f16362dd6 100644 --- a/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/SigningMetadata.java +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/config/metadata/SigningMetadata.java @@ -31,7 +31,8 @@ @JsonSubTypes.Type(value = AzureKeySigningMetadata.class, name = AzureKeySigningMetadata.TYPE), @JsonSubTypes.Type(value = InterlockSigningMetadata.class, name = InterlockSigningMetadata.TYPE), @JsonSubTypes.Type(value = YubiHsmSigningMetadata.class, name = YubiHsmSigningMetadata.TYPE), - @JsonSubTypes.Type(value = AwsKeySigningMetadata.class, name = AwsKeySigningMetadata.TYPE) + @JsonSubTypes.Type(value = AwsKeySigningMetadata.class, name = AwsKeySigningMetadata.TYPE), + @JsonSubTypes.Type(value = AwsKmsMetadata.class, name = AwsKmsMetadata.TYPE), }) public abstract class SigningMetadata { diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClient.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClient.java new file mode 100644 index 000000000..8780616ff --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClient.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import com.google.common.annotations.VisibleForTesting; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.CreateKeyRequest; +import software.amazon.awssdk.services.kms.model.GetPublicKeyRequest; +import software.amazon.awssdk.services.kms.model.GetPublicKeyResponse; +import software.amazon.awssdk.services.kms.model.KeySpec; +import software.amazon.awssdk.services.kms.model.MessageType; +import software.amazon.awssdk.services.kms.model.ScheduleKeyDeletionRequest; +import software.amazon.awssdk.services.kms.model.SignRequest; +import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec; + +/** + * Wraps KmsClient to allow the same instance to be cached and re-used. It exposes the methods that + * our code use. Since AwsKmsClient is meant to live for the duration of web3signer life, we have + * not implemented close method. + */ +public class AwsKmsClient { + private static final Provider BC_PROVIDER = new BouncyCastleProvider(); + private final KmsClient kmsClient; + + public AwsKmsClient(final KmsClient kmsClient) { + this.kmsClient = kmsClient; + } + + public ECPublicKey getECPublicKey(final String kmsKeyId) { + // kmsClient can be null/closed if close method has been called. + checkArgument(kmsClient != null, "KmsClient is not initialized"); + + final GetPublicKeyRequest getPublicKeyRequest = + GetPublicKeyRequest.builder().keyId(kmsKeyId).build(); + final GetPublicKeyResponse publicKeyResponse = kmsClient.getPublicKey(getPublicKeyRequest); + final KeySpec keySpec = publicKeyResponse.keySpec(); + if (keySpec != KeySpec.ECC_SECG_P256_K1) { + throw new RuntimeException("Unsupported key spec from AWS KMS: " + keySpec.toString()); + } + + final X509EncodedKeySpec encodedKeySpec = + new X509EncodedKeySpec(publicKeyResponse.publicKey().asByteArray()); + try { + final KeyFactory keyFactory = KeyFactory.getInstance("EC", BC_PROVIDER); + return (ECPublicKey) keyFactory.generatePublic(encodedKeySpec); + } catch (final NoSuchAlgorithmException | InvalidKeySpecException e) { + // very unlikely to happen unless BouncyCastle suddenly stop supporting EC curve based keys + throw new RuntimeException(e); + } + } + + public byte[] sign(final String kmsKeyId, final byte[] data) { + final SignRequest signRequest = + SignRequest.builder() + .keyId(kmsKeyId) + .signingAlgorithm(SigningAlgorithmSpec.ECDSA_SHA_256) + .messageType(MessageType.DIGEST) + .message(SdkBytes.fromByteArray(data)) + .build(); + + return kmsClient.sign(signRequest).signature().asByteArray(); + } + + @VisibleForTesting + public String createKey(CreateKeyRequest createKeyRequest) { + return kmsClient.createKey(createKeyRequest).keyMetadata().keyId(); + } + + @VisibleForTesting + public void scheduleKeyDeletion(ScheduleKeyDeletionRequest deletionRequest) { + kmsClient.scheduleKeyDeletion(deletionRequest); + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClientKey.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClientKey.java new file mode 100644 index 000000000..4794fac9c --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsClientKey.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import java.net.URI; +import java.util.Objects; +import java.util.Optional; + +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; + +/** This class acts as a key to identify Aws KmsClient from the cache. */ +final class AwsKmsClientKey { + private final AwsCredentialsProvider awsCredentialsProvider; + private final AwsCredentials awsCredentials; + private final String region; + private final Optional endpointOverride; + + AwsKmsClientKey( + final AwsCredentialsProvider awsCredentialsProvider, + final String region, + final Optional endpointOverride) { + this.awsCredentialsProvider = awsCredentialsProvider; + this.awsCredentials = awsCredentialsProvider.resolveCredentials(); + this.region = region; + this.endpointOverride = endpointOverride; + } + + public AwsCredentialsProvider getAwsCredentialsProvider() { + return awsCredentialsProvider; + } + + public String getRegion() { + return region; + } + + public Optional getEndpointOverride() { + return endpointOverride; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AwsKmsClientKey that = (AwsKmsClientKey) o; + return Objects.equals(awsCredentials, that.awsCredentials) + && Objects.equals(region, that.region) + && Objects.equals(endpointOverride, that.endpointOverride); + } + + @Override + public int hashCode() { + return Objects.hash(awsCredentials, region, endpointOverride); + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSigner.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSigner.java new file mode 100644 index 000000000..73c0e32a9 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSigner.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import tech.pegasys.web3signer.signing.secp256k1.Signature; +import tech.pegasys.web3signer.signing.secp256k1.Signer; +import tech.pegasys.web3signer.signing.secp256k1.util.Eth1SignatureUtil; + +import java.security.interfaces.ECPublicKey; + +import org.web3j.crypto.Hash; + +public class AwsKmsSigner implements Signer { + private final ECPublicKey ecPublicKey; + private final AwsKmsClient awsKmsClient; + private final String kmsKeyId; + // required for eth1 signing. Filecoin signing doesn't need it. + private final boolean applySha3Hash; + + public AwsKmsSigner( + final ECPublicKey ecPublicKey, + final AwsKmsClient awsKmsClient, + final String kmsKeyId, + final boolean applySha3Hash) { + this.ecPublicKey = ecPublicKey; + this.awsKmsClient = awsKmsClient; + this.kmsKeyId = kmsKeyId; + this.applySha3Hash = applySha3Hash; + } + + @Override + public Signature sign(final byte[] data) { + // sha3hash is required for eth1 signing. Filecoin signing doesn't need hashing. + final byte[] dataToSign = applySha3Hash ? Hash.sha3(data) : data; + final byte[] signature = awsKmsClient.sign(kmsKeyId, dataToSign); + return Eth1SignatureUtil.deriveSignatureFromDerEncoded(dataToSign, ecPublicKey, signature); + } + + @Override + public ECPublicKey getPublicKey() { + return ecPublicKey; + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerFactory.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerFactory.java new file mode 100644 index 000000000..5f569904d --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import static com.google.common.base.Preconditions.checkArgument; + +import tech.pegasys.web3signer.signing.config.AwsCredentialsProviderFactory; +import tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadata; +import tech.pegasys.web3signer.signing.secp256k1.Signer; + +import java.security.interfaces.ECPublicKey; + +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; + +/** A Signer factory that create an instance of `Signer` type backed by AWS KMS. */ +public class AwsKmsSignerFactory { + + private final CachedAwsKmsClientFactory factory; + + private final boolean applySha3Hash; + + /** + * Construct AwsKmsSignerFactory + * + * @param kmsClientCacheSize The cache size of AWS kms clients. This size should be set based on + * the number of credentials/region used. If same set of credentials/region used to access + * kms, set to 1. + * @param applySha3Hash Set to true for eth1 signing. Set false for filecoin signing. + */ + public AwsKmsSignerFactory(final long kmsClientCacheSize, final boolean applySha3Hash) { + checkArgument(kmsClientCacheSize > 0, "Kms client cache Size must be positive."); + factory = new CachedAwsKmsClientFactory(kmsClientCacheSize); + this.applySha3Hash = applySha3Hash; + } + + public Signer createSigner(final AwsKmsMetadata awsKmsMetadata) { + checkArgument(awsKmsMetadata != null, "awsKmsMetadata must not be null"); + + final AwsCredentialsProvider awsCredentialsProvider = + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + awsKmsMetadata.getAuthenticationMode(), awsKmsMetadata.getAwsCredentials()); + + final AwsKmsClient kmsClient = + factory.createKmsClient( + awsCredentialsProvider, + awsKmsMetadata.getRegion(), + awsKmsMetadata.getEndpointOverride()); + + // lookup public key as it is required to create AwsKmsSigner instance + final ECPublicKey ecPublicKey = kmsClient.getECPublicKey(awsKmsMetadata.getKmsKeyId()); + return new AwsKmsSigner(ecPublicKey, kmsClient, awsKmsMetadata.getKmsKeyId(), applySha3Hash); + } +} diff --git a/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactory.java b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactory.java new file mode 100644 index 000000000..21d74a430 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.net.URI; +import java.util.Optional; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.KmsClientBuilder; + +/** + * Factory class that provide cached instances of KmsClient. Each cached KmsClient is identified by + * aws credentials, region and optional override endpoint URL. + * + *

It is anticipated that web3signer instance would be using same aws host/credentials for its + * kms operations, hence the default cache size should be set to 1. The cache size should be + * increased if different set of credentials or region is anticipated. + */ +public class CachedAwsKmsClientFactory { + private final LoadingCache cache; + + public CachedAwsKmsClientFactory(final long cacheSize) { + checkArgument(cacheSize > 0, "Cache size must be positive"); + cache = + CacheBuilder.newBuilder() + .maximumSize(cacheSize) + .build( + new CacheLoader<>() { + @Override + public AwsKmsClient load(final AwsKmsClientKey key) { + final KmsClientBuilder kmsClientBuilder = KmsClient.builder(); + key.getEndpointOverride().ifPresent(kmsClientBuilder::endpointOverride); + kmsClientBuilder + .credentialsProvider(key.getAwsCredentialsProvider()) + .region(Region.of(key.getRegion())); + + return new AwsKmsClient(kmsClientBuilder.build()); + } + }); + } + + public AwsKmsClient createKmsClient( + final AwsCredentialsProvider awsCredentialsProvider, + final String region, + final Optional endpointOverride) { + return cache.getUnchecked( + new AwsKmsClientKey(awsCredentialsProvider, region, endpointOverride)); + } +} 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 6bf23484f..f6dec14ae 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 @@ -19,10 +19,9 @@ import tech.pegasys.web3signer.signing.secp256k1.Signature; import tech.pegasys.web3signer.signing.secp256k1.Signer; import tech.pegasys.web3signer.signing.secp256k1.common.SignerInitializationException; +import tech.pegasys.web3signer.signing.secp256k1.util.Eth1SignatureUtil; -import java.math.BigInteger; import java.security.interfaces.ECPublicKey; -import java.util.Arrays; import com.azure.security.keyvault.keys.cryptography.CryptographyClient; import com.azure.security.keyvault.keys.cryptography.models.SignResult; @@ -30,10 +29,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; -import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.Hash; -import org.web3j.crypto.Sign; -import org.web3j.utils.Numeric; public class AzureKeyVaultSigner implements Signer { @@ -92,44 +88,11 @@ public Signature sign(byte[] data) { "Invalid signature from the key vault signing service, must be 64 bytes long"); } - // reference: blog by Tomislav Markovski - // https://tomislav.tech/2018-02-05-ethereum-keyvault-signing-transactions/ - // The output of this will be a 64 byte array. The first 32 are the value for R and the rest is - // S. - final BigInteger R = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)); - final BigInteger S = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)); - - // The Azure Signature MAY be in the "top" of the curve, which is illegal in Ethereum - // thus it must be transposed to the lower intersection. - final ECDSASignature initialSignature = new ECDSASignature(R, S); - final ECDSASignature canonicalSignature = initialSignature.toCanonicalised(); - - // Now we have to work backwards to figure out the recId needed to recover the signature. - final int recId = recoverKeyIndex(canonicalSignature, dataToSign); - if (recId == -1) { - throw new RuntimeException( - "Could not construct a recoverable key. Are your credentials valid?"); - } - - final int headerByte = recId + 27; - return new Signature( - BigInteger.valueOf(headerByte), canonicalSignature.r, canonicalSignature.s); + return Eth1SignatureUtil.deriveSignatureFromP1363Encoded(dataToSign, publicKey, signature); } @Override public ECPublicKey getPublicKey() { return publicKey; } - - private int recoverKeyIndex(final ECDSASignature sig, final byte[] hash) { - final BigInteger publicKey = Numeric.toBigInt(EthPublicKeyUtils.toByteArray(this.publicKey)); - for (int i = 0; i < 4; i++) { - final BigInteger k = Sign.recoverFromSignature(i, sig, hash); - LOG.trace("recovered key: {}", k); - if (k != null && k.equals(publicKey)) { - return i; - } - } - return -1; - } } 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 new file mode 100644 index 000000000..0e81f85d9 --- /dev/null +++ b/signing/src/main/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtil.java @@ -0,0 +1,133 @@ +/* + * Copyright 2023 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.secp256k1.util; + +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.signing.secp256k1.Signature; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.security.interfaces.ECPublicKey; +import java.util.Arrays; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +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(); + + /** + * Java or OpenSSL signature is ANS.1/DER encoded. i.e. SEQUENCE := {r INTEGER, s INTEGER} + * + * @param dataToSign The data that was signed + * @param ecPublicKey the EC Public Key to verify + * @param signature ANS.1/DER encoded signature + * @return instance of Signature that contains r and s along with v + */ + public static Signature deriveSignatureFromDerEncoded( + final byte[] dataToSign, final ECPublicKey ecPublicKey, final byte[] signature) { + final BigInteger[] sig = extractRAndSFromDERSignature(signature); + return deriveSignature(dataToSign, ecPublicKey, sig[0], sig[1]); + } + + /** + * P1363 signature is 32+32 size for r and s (due to secp-256 encoding) + * + * @param dataToSign The data that was signed + * @param ecPublicKey the EC Public Key to verify + * @param signature P1363 encoded data + * @return instance of Signature that contains r and s along with v + */ + public static Signature deriveSignatureFromP1363Encoded( + final byte[] dataToSign, final ECPublicKey ecPublicKey, final byte[] signature) { + // The output of this will be a 64 byte array. The first 32 are the value for R and the rest is + // S. + + final BigInteger R = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)); + final BigInteger S = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)); + return deriveSignature(dataToSign, ecPublicKey, R, S); + } + + private static Signature deriveSignature( + final byte[] dataToSign, + final ECPublicKey ecPublicKey, + final BigInteger R, + final BigInteger S) { + // reference: blog by Tomislav Markovski + // https://tomislav.tech/2018-02-05-ethereum-keyvault-signing-transactions/ + + // The Azure/AWS Signature MAY be in the "top" of the curve, which is illegal in Ethereum + // thus it must be transposed to the lower intersection. + final ECDSASignature initialSignature = new ECDSASignature(R, S); + final ECDSASignature canonicalSignature = initialSignature.toCanonicalised(); + + // Now we have to work backwards to figure out the recId needed to recover the signature. + final int recId = recoverKeyIndex(ecPublicKey, canonicalSignature, dataToSign); + if (recId == -1) { + throw new RuntimeException( + "Could not construct a recoverable key. Are your credentials valid?"); + } + + final int headerByte = recId + 27; + return new Signature( + BigInteger.valueOf(headerByte), canonicalSignature.r, canonicalSignature.s); + } + + private static int recoverKeyIndex( + final ECPublicKey ecPublicKey, final ECDSASignature sig, final byte[] hash) { + final BigInteger publicKey = Numeric.toBigInt(EthPublicKeyUtils.toByteArray(ecPublicKey)); + for (int i = 0; i < 4; i++) { + final BigInteger k = Sign.recoverFromSignature(i, sig, hash); + LOG.trace("recovered key: {}", k); + if (k != null && k.equals(publicKey)) { + return i; + } + } + return -1; + } + + /** + * Uses Bouncycastle to decode DER signature. A DER signature format is SEQUENCE := {r INTEGER, s + * INTEGER} + * + * @param der DER encoded byte[] + * @return Array of BigInteger containing R and S. + */ + private static BigInteger[] extractRAndSFromDERSignature(final byte[] der) { + try (final ASN1InputStream asn1InputStream = new ASN1InputStream(der)) { + final DLSequence seq = (DLSequence) asn1InputStream.readObject(); + if (seq == null) { + throw new RuntimeException("Unexpected end of ASN.1 stream."); + } + + try { + final ASN1Integer r = (ASN1Integer) seq.getObjectAt(0); + final ASN1Integer s = (ASN1Integer) seq.getObjectAt(1); + + return new BigInteger[] {r.getPositiveValue(), s.getPositiveValue()}; + } catch (final ClassCastException e) { + throw new IllegalArgumentException(e); + } + + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializerTest.java index 25e9c024b..f83af9479 100644 --- a/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializerTest.java +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKeySigningMetadataDeserializerTest.java @@ -15,8 +15,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.KeyType; -import tech.pegasys.web3signer.signing.config.AwsAuthenticationMode; import tech.pegasys.web3signer.signing.config.metadata.parser.YamlMapperFactory; import java.io.File; diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializerTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializerTest.java new file mode 100644 index 000000000..d8feae064 --- /dev/null +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/config/metadata/AwsKmsMetadataDeserializerTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2023 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.config.metadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static tech.pegasys.web3signer.common.config.AwsAuthenticationMode.ENVIRONMENT; +import static tech.pegasys.web3signer.common.config.AwsAuthenticationMode.SPECIFIED; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.ACCESS_KEY_ID; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.AUTH_MODE; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.ENDPOINT_OVERRIDE; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.KMS_KEY_ID; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.REGION; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.SECRET_ACCESS_KEY; +import static tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadataDeserializer.SESSION_TOKEN; + +import tech.pegasys.web3signer.signing.KeyType; +import tech.pegasys.web3signer.signing.config.metadata.parser.YamlMapperFactory; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.junit.jupiter.api.Test; + +class AwsKmsMetadataDeserializerTest { + private static final YAMLMapper YAML_MAPPER = YamlMapperFactory.createYamlMapper(); + + @Test + void minimalRequiredFieldsAreDeserialized() throws JsonProcessingException { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + metadataMap.put(REGION, "us-east-2"); + metadataMap.put(KMS_KEY_ID, "aaabbbcccddd"); + metadataMap.put(AUTH_MODE, ENVIRONMENT); + + final AwsKmsMetadata metadata = + YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class); + + assertThat(metadata.getType()).isEqualTo("aws-kms"); + assertThat(metadata.getKeyType()).isEqualTo(KeyType.SECP256K1); + assertThat(metadata.getKmsKeyId()).isEqualTo("aaabbbcccddd"); + + assertThat(metadata.getRegion()).isEqualTo("us-east-2"); + assertThat(metadata.getAuthenticationMode()).isEqualTo(ENVIRONMENT); + assertThat(metadata.getAwsCredentials()).isEmpty(); + assertThat(metadata.getEndpointOverride()).isEmpty(); + } + + @Test + void defaultAuthenticationModeIsDeserialized() throws JsonProcessingException { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + metadataMap.put(REGION, "us-east-2"); + metadataMap.put(KMS_KEY_ID, "aaabbbcccddd"); + metadataMap.put(ACCESS_KEY_ID, "acc_key_id"); + metadataMap.put(SECRET_ACCESS_KEY, "sec_acc_key"); + + final AwsKmsMetadata metadata = + YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class); + + assertThat(metadata.getType()).isEqualTo("aws-kms"); + assertThat(metadata.getKeyType()).isEqualTo(KeyType.SECP256K1); + assertThat(metadata.getKmsKeyId()).isEqualTo("aaabbbcccddd"); + assertThat(metadata.getRegion()).isEqualTo("us-east-2"); + + assertThat(metadata.getAuthenticationMode()).isEqualTo(SPECIFIED); + assertThat(metadata.getAwsCredentials()).isNotEmpty(); + assertThat(metadata.getAwsCredentials().get().getAccessKeyId()).isEqualTo("acc_key_id"); + assertThat(metadata.getAwsCredentials().get().getSecretAccessKey()).isEqualTo("sec_acc_key"); + assertThat(metadata.getAwsCredentials().get().getSessionToken()).isEmpty(); + + assertThat(metadata.getEndpointOverride()).isEmpty(); + } + + @Test + void credentialsAreIgnoredWhenEnvironmentAuthModeIsUsed() throws JsonProcessingException { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + metadataMap.put(REGION, "us-east-2"); + metadataMap.put(KMS_KEY_ID, "aaabbbcccddd"); + metadataMap.put(AUTH_MODE, ENVIRONMENT); + metadataMap.put(ACCESS_KEY_ID, "acc_key_id"); + metadataMap.put(SECRET_ACCESS_KEY, "sec_acc_key"); + + final AwsKmsMetadata metadata = + YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class); + + assertThat(metadata.getType()).isEqualTo("aws-kms"); + assertThat(metadata.getKeyType()).isEqualTo(KeyType.SECP256K1); + assertThat(metadata.getKmsKeyId()).isEqualTo("aaabbbcccddd"); + assertThat(metadata.getRegion()).isEqualTo("us-east-2"); + + assertThat(metadata.getAuthenticationMode()).isEqualTo(ENVIRONMENT); + assertThat(metadata.getAwsCredentials()).isEmpty(); + assertThat(metadata.getEndpointOverride()).isEmpty(); + } + + @Test + void allOptionalValuesAreDeserialized() throws JsonProcessingException { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + metadataMap.put(REGION, "us-east-2"); + metadataMap.put(KMS_KEY_ID, "aaabbbcccddd"); + metadataMap.put(AUTH_MODE, SPECIFIED); + metadataMap.put(ACCESS_KEY_ID, "acc_key_id"); + metadataMap.put(SECRET_ACCESS_KEY, "sec_acc_key"); + metadataMap.put(SESSION_TOKEN, "sess_token"); + metadataMap.put(ENDPOINT_OVERRIDE, "http://localhost:4566"); + metadataMap.put("extraField", "shouldBeIgnored"); + + final AwsKmsMetadata metadata = + YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class); + + assertThat(metadata.getType()).isEqualTo("aws-kms"); + assertThat(metadata.getKeyType()).isEqualTo(KeyType.SECP256K1); + assertThat(metadata.getRegion()).isEqualTo("us-east-2"); + assertThat(metadata.getAuthenticationMode()).isEqualTo(SPECIFIED); + assertThat(metadata.getAwsCredentials()).isNotEmpty(); + assertThat(metadata.getAwsCredentials().get().getAccessKeyId()).isEqualTo("acc_key_id"); + assertThat(metadata.getAwsCredentials().get().getSecretAccessKey()).isEqualTo("sec_acc_key"); + assertThat(metadata.getAwsCredentials().get().getSessionToken()).get().isEqualTo("sess_token"); + assertThat(metadata.getEndpointOverride()).get().isEqualTo(URI.create("http://localhost:4566")); + assertThat(metadata.getKmsKeyId()).isEqualTo("aaabbbcccddd"); + } + + @Test + void missingRequiredFieldsThrowsException() { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class)) + .withMessageContaining( + "Missing values for required parameters: region, kmsKeyId, accessKeyId, secretAccessKey"); + } + + @Test + void invalidAuthTypeThrowsException() { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + metadataMap.put(REGION, "us-east-2"); + metadataMap.put(AUTH_MODE, "unknown_mode"); + + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class)) + .withMessageContaining("Invalid value for parameter: " + AUTH_MODE + "."); + } + + @Test + void invalidEndpointURIThrowsException() { + final Map metadataMap = new HashMap<>(); + metadataMap.put("type", "aws-kms"); + metadataMap.put(REGION, "us-east-2"); + metadataMap.put(KMS_KEY_ID, "aaabbbcccddd"); + metadataMap.put(ACCESS_KEY_ID, "acc_key_id"); + metadataMap.put(SECRET_ACCESS_KEY, "sec_acc_key"); + metadataMap.put(SESSION_TOKEN, "sess_token"); + metadataMap.put(ENDPOINT_OVERRIDE, "invalid_url:80:80"); + + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> YAML_MAPPER.readValue(metadataYaml(metadataMap), AwsKmsMetadata.class)) + .withMessageContaining("Invalid value for parameter: " + ENDPOINT_OVERRIDE + "."); + } + + private static String metadataYaml(Map map) throws JsonProcessingException { + return YAML_MAPPER.writeValueAsString(map); + } +} 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 new file mode 100644 index 000000000..2d0b8d1cd --- /dev/null +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/AwsKmsSignerTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsCredentials; +import tech.pegasys.web3signer.signing.config.AwsCredentialsProviderFactory; +import tech.pegasys.web3signer.signing.config.metadata.AwsKmsMetadata; +import tech.pegasys.web3signer.signing.secp256k1.EthPublicKeyUtils; +import tech.pegasys.web3signer.signing.secp256k1.Signature; +import tech.pegasys.web3signer.signing.secp256k1.Signer; + +import java.math.BigInteger; +import java.net.URI; +import java.security.SignatureException; +import java.util.Optional; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.web3j.crypto.Hash; +import org.web3j.crypto.Sign; +import org.web3j.utils.Numeric; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.services.kms.model.CreateKeyRequest; +import software.amazon.awssdk.services.kms.model.KeySpec; +import software.amazon.awssdk.services.kms.model.KeyUsageType; +import software.amazon.awssdk.services.kms.model.ScheduleKeyDeletionRequest; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@EnabledIfEnvironmentVariable( + named = "RW_AWS_ACCESS_KEY_ID", + matches = ".*", + disabledReason = "RW_AWS_ACCESS_KEY_ID env variable is required") +@EnabledIfEnvironmentVariable( + named = "RW_AWS_SECRET_ACCESS_KEY", + matches = ".*", + disabledReason = "RW_AWS_SECRET_ACCESS_KEY env variable is required") +@EnabledIfEnvironmentVariable( + named = "AWS_ACCESS_KEY_ID", + matches = ".*", + disabledReason = "AWS_ACCESS_KEY_ID env variable is required") +@EnabledIfEnvironmentVariable( + named = "AWS_SECRET_ACCESS_KEY", + matches = ".*", + disabledReason = "AWS_SECRET_ACCESS_KEY env variable is required") +public class AwsKmsSignerTest { + private static final String AWS_ACCESS_KEY_ID = System.getenv("AWS_ACCESS_KEY_ID"); + private static final String AWS_SECRET_ACCESS_KEY = System.getenv("AWS_SECRET_ACCESS_KEY"); + + private static final String RW_AWS_ACCESS_KEY_ID = System.getenv("RW_AWS_ACCESS_KEY_ID"); + private static final String RW_AWS_SECRET_ACCESS_KEY = System.getenv("RW_AWS_SECRET_ACCESS_KEY"); + + // optional. + private static final String AWS_REGION = + Optional.ofNullable(System.getenv("AWS_REGION")).orElse("us-east-2"); + private static final String AWS_SESSION_TOKEN = System.getenv("AWS_SESSION_TOKEN"); + private static final Optional ENDPOINT_OVERRIDE = + Optional.ofNullable(System.getenv("AWS_ENDPOINT_OVERRIDE")).map(URI::create); + + private static final AwsCredentials AWS_RW_CREDENTIALS = + AwsCredentials.builder() + .withAccessKeyId(RW_AWS_ACCESS_KEY_ID) + .withSecretAccessKey(RW_AWS_SECRET_ACCESS_KEY) + .withSessionToken(AWS_SESSION_TOKEN) + .build(); + + private static final AwsCredentials AWS_CREDENTIALS = + AwsCredentials.builder() + .withAccessKeyId(AWS_ACCESS_KEY_ID) + .withSecretAccessKey(AWS_SECRET_ACCESS_KEY) + .withSessionToken(AWS_SESSION_TOKEN) + .build(); + + private static final CachedAwsKmsClientFactory KMS_CLIENT_FACTORY = + new CachedAwsKmsClientFactory(1); + private static AwsKmsClient awsKMSClient; + private static String testKeyId; + + @BeforeAll + static void init() { + AwsCredentialsProvider awsCredentialsProvider = + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, Optional.of(AWS_RW_CREDENTIALS)); + awsKMSClient = + KMS_CLIENT_FACTORY.createKmsClient(awsCredentialsProvider, AWS_REGION, ENDPOINT_OVERRIDE); + + // create a test key + final CreateKeyRequest web3SignerTestingKey = + CreateKeyRequest.builder() + .keySpec(KeySpec.ECC_SECG_P256_K1) + .description("Web3Signer Testing Key") + .keyUsage(KeyUsageType.SIGN_VERIFY) + .build(); + testKeyId = awsKMSClient.createKey(web3SignerTestingKey); + assertThat(testKeyId).isNotEmpty(); + } + + @AfterAll + static void cleanup() { + if (awsKMSClient == null) { + return; + } + // delete key + ScheduleKeyDeletionRequest deletionRequest = + ScheduleKeyDeletionRequest.builder().keyId(testKeyId).pendingWindowInDays(7).build(); + awsKMSClient.scheduleKeyDeletion(deletionRequest); + } + + @Test + void awsSignatureCanBeVerified() throws SignatureException { + final AwsKmsMetadata awsKmsMetadata = + new AwsKmsMetadata( + AwsAuthenticationMode.SPECIFIED, + AWS_REGION, + Optional.of(AWS_CREDENTIALS), + testKeyId, + ENDPOINT_OVERRIDE); + final long kmsClientCacheSize = 1; + final boolean applySha3Hash = true; + final Signer signer = + new AwsKmsSignerFactory(kmsClientCacheSize, applySha3Hash).createSigner(awsKmsMetadata); + final BigInteger publicKey = + Numeric.toBigInt(EthPublicKeyUtils.toByteArray(signer.getPublicKey())); + + final byte[] dataToSign = "Hello".getBytes(UTF_8); + + for (int i = 0; i < 2; i++) { + final Signature signature = signer.sign(dataToSign); + // Use web3j library to recover the primary key from the signature + final BigInteger recoveredPublicKey = recoverPublicKeyFromSignature(signature, dataToSign); + assertThat(recoveredPublicKey).isEqualTo(publicKey); + } + } + + private static BigInteger recoverPublicKeyFromSignature( + final Signature signature, final byte[] dataToSign) throws SignatureException { + final Sign.SignatureData sigData = + new Sign.SignatureData( + signature.getV().toByteArray(), + Numeric.toBytesPadded(signature.getR(), 32), + Numeric.toBytesPadded(signature.getS(), 32)); + + return Sign.signedMessageHashToKey(Hash.sha3(dataToSign), sigData); + } +} diff --git a/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactoryTest.java b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactoryTest.java new file mode 100644 index 000000000..dab8c34bd --- /dev/null +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/aws/CachedAwsKmsClientFactoryTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2023 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.secp256k1.aws; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; +import tech.pegasys.web3signer.common.config.AwsCredentials; +import tech.pegasys.web3signer.signing.config.AwsCredentialsProviderFactory; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CachedAwsKmsClientFactoryTest { + private CachedAwsKmsClientFactory cachedAwsKmsClientFactory; + + @BeforeEach + void init() { + cachedAwsKmsClientFactory = new CachedAwsKmsClientFactory(2); + } + + @Test + void cachedInstanceOfKmsClientIsReturnedForSpecifiedCredentials() { + final AwsKmsClient kmsClient_1 = + cachedAwsKmsClientFactory.createKmsClient( + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, + Optional.of( + AwsCredentials.builder() + .withAccessKeyId("test") + .withSecretAccessKey("test") + .build())), + "us-east-2", + Optional.empty()); + + final AwsKmsClient kmsClient_2 = + cachedAwsKmsClientFactory.createKmsClient( + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, + Optional.of( + AwsCredentials.builder() + .withAccessKeyId("test3") + .withSecretAccessKey("test3") + .build())), + "us-east-2", + Optional.empty()); + + final AwsKmsClient kmsClient_3 = + cachedAwsKmsClientFactory.createKmsClient( + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, + Optional.of( + AwsCredentials.builder() + .withAccessKeyId("test") + .withSecretAccessKey("test") + .build())), + "us-east-2", + Optional.empty()); + + final AwsKmsClient kmsClient_4 = + cachedAwsKmsClientFactory.createKmsClient( + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, + Optional.of( + AwsCredentials.builder() + .withAccessKeyId("test3") + .withSecretAccessKey("test3") + .build())), + "us-east-2", + Optional.empty()); + + assertThat(kmsClient_1).isEqualTo(kmsClient_3); + assertThat(kmsClient_2).isEqualTo(kmsClient_4); + + assertThat(kmsClient_1).isNotEqualTo(kmsClient_2); + } + + @Test + void cachedInstanceOfKmsClientIsReturnedForSpecifiedCredentialsWithSessionToken() { + final AwsKmsClient kmsClient_1 = + cachedAwsKmsClientFactory.createKmsClient( + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, + Optional.of( + AwsCredentials.builder() + .withAccessKeyId("test") + .withSecretAccessKey("test") + .withSessionToken("test") + .build())), + "us-east-2", + Optional.empty()); + + final AwsKmsClient kmsClient_2 = + cachedAwsKmsClientFactory.createKmsClient( + AwsCredentialsProviderFactory.createAwsCredentialsProvider( + AwsAuthenticationMode.SPECIFIED, + Optional.of( + AwsCredentials.builder() + .withAccessKeyId("test") + .withSecretAccessKey("test") + .withSessionToken("test") + .build())), + "us-east-2", + Optional.empty()); + + assertThat(kmsClient_1).isEqualTo(kmsClient_2); + } +} 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 new file mode 100644 index 000000000..66a147638 --- /dev/null +++ b/signing/src/test/java/tech/pegasys/web3signer/signing/secp256k1/util/Eth1SignatureUtilTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2023 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.secp256k1.util; + +import static org.assertj.core.api.Assertions.assertThat; + +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(); + + @Test + void signatureIsDerivedFromDerEncoded() throws Exception { + final byte[] dataToSign = Hash.sha3("hello".getBytes(StandardCharsets.UTF_8)); + + final KeyPair keyPair = generateEC_SECPKeyPair(); + final ECKeyPair web3jECKeyPair = ECKeyPair.create(keyPair); + + // sign using web3j (which uses BouncyCastle ECDSA Signer) + final ECDSASignature web3jSig = web3jECKeyPair.sign(dataToSign); + + // convert web3j's P1363 to ANS1/DER Encoded signature + final ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(web3jSig.r)); + v.add(new ASN1Integer(web3jSig.s)); + final byte[] derEncodedSignedData = new DERSequence(v).getEncoded(); + + // verify our logic + final tech.pegasys.web3signer.signing.secp256k1.Signature signature = + Eth1SignatureUtil.deriveSignatureFromDerEncoded( + dataToSign, (ECPublicKey) keyPair.getPublic(), derEncodedSignedData); + + assertThat(signature.getR()).isEqualTo(web3jSig.r); + assertThat(signature.getS()).isEqualTo(web3jSig.s); + } + + @Test + void signatureIsDerivedFromP1363Encoded() throws Exception { + final byte[] dataToSign = Hash.sha3("hello".getBytes(StandardCharsets.UTF_8)); + + final KeyPair keyPair = generateEC_SECPKeyPair(); + final ECKeyPair web3jECKeyPair = ECKeyPair.create(keyPair); + // sign using web3j (which uses BouncyCastle ECDSASigner) + final ECDSASignature web3jSig = web3jECKeyPair.sign(dataToSign); + + // concatenate r || s to create byte[] + final Bytes rBytes = Bytes.of(web3jSig.r.toByteArray()).trimLeadingZeros(); + final Bytes sBytes = Bytes.of(web3jSig.s.toByteArray()).trimLeadingZeros(); + final byte[] p1363Signature = Bytes.concatenate(rBytes, sBytes).toArray(); + + // verify our logic + final tech.pegasys.web3signer.signing.secp256k1.Signature signature = + Eth1SignatureUtil.deriveSignatureFromP1363Encoded( + dataToSign, (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(); + } +} diff --git a/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParametersBuilder.java b/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParametersBuilder.java index f1aa9898c..897da217f 100644 --- a/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParametersBuilder.java +++ b/signing/src/testFixtures/java/tech/pegasys/web3signer/signing/config/AwsSecretsManagerParametersBuilder.java @@ -12,6 +12,8 @@ */ package tech.pegasys.web3signer.signing.config; +import tech.pegasys.web3signer.common.config.AwsAuthenticationMode; + import java.net.URI; import java.util.Collection; import java.util.Collections;