diff --git a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java index d74062c888..c336da6f1b 100644 --- a/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java +++ b/cli/config-cli/src/main/java/com/quorum/tessera/config/cli/KeyGenCommand.java @@ -158,7 +158,7 @@ private Optional keyVaultConfig() { if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } - } else { + } else if (vaultType.equals(KeyVaultType.HASHICORP)) { if (Objects.isNull(keyOut) || keyOut.isEmpty()) { throw new CliException( "At least one -filename must be provided when saving generated keys in a Hashicorp Vault"); @@ -171,6 +171,21 @@ private Optional keyVaultConfig() { Set> violations = validator.validate((HashicorpKeyVaultConfig) keyVaultConfig); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } else { + DefaultKeyVaultConfig awsKeyVaultConfig = new DefaultKeyVaultConfig(); + awsKeyVaultConfig.setKeyVaultType(KeyVaultType.AWS); + + if (Objects.nonNull(vaultUrl)) { + awsKeyVaultConfig.setProperty("endpoint", vaultUrl); + } + + keyVaultConfig = awsKeyVaultConfig; + + Set> violations = validator.validate(awsKeyVaultConfig); + if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } diff --git a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java index e3d6c9e858..3916e402bc 100644 --- a/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java +++ b/cli/config-cli/src/test/java/com/quorum/tessera/config/cli/KeyGenCommandTest.java @@ -442,7 +442,7 @@ public void invalidHashicorpKeyVaultConfigThrowsException() { } @Test - public void hashicorpTlsPathsDontExistThrowsException() throws Exception { + public void hashicorpTlsPathsDontExistThrowsException() { final String vaultUrl = "someurl"; final String approlePath = "someapprole"; final Path nonExistentPath = Paths.get(UUID.randomUUID().toString()); @@ -479,6 +479,88 @@ public void hashicorpTlsPathsDontExistThrowsException() throws Exception { verifyNoMoreInteractions(encryptorOptions, keyGenerator); } + @Test + public void validAWSKeyVaultConfig() { + String endpointUrl = "https://someurl.com"; + + final DefaultKeyVaultConfig keyVaultConfig = new DefaultKeyVaultConfig(); + keyVaultConfig.setKeyVaultType(KeyVaultType.AWS); + keyVaultConfig.setProperty("endpoint", endpointUrl); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.AWS; + command.vaultUrl = endpointUrl; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(refEq(keyVaultConfig), isNull()); + assertThat(result).isEqualToComparingFieldByField(wantResult); + + verify(keyGenerator).generate(anyString(), any(), any()); + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void validAWSKeyVaultConfigNoVaultUrl() { + final DefaultKeyVaultConfig keyVaultConfig = new DefaultKeyVaultConfig(); + keyVaultConfig.setKeyVaultType(KeyVaultType.AWS); + + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.AWS; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + CliResult result = command.call(); + + // verify the correct config is used + verify(keyGeneratorFactory).create(refEq(keyVaultConfig), isNull()); + assertThat(result).isEqualToComparingFieldByField(wantResult); + + verify(keyGenerator).generate(anyString(), any(), any()); + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions, keyGenerator); + } + + @Test + public void invalidAWSKeyVaultConfigThrowsException() { + final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); + when(encryptorOptions.parseEncryptorConfig()).thenReturn(null); + + command.encryptorOptions = encryptorOptions; + command.vaultType = KeyVaultType.AWS; + command.vaultUrl = "not a valid url"; + + final KeyGenerator keyGenerator = mock(KeyGenerator.class); + when(keyGeneratorFactory.create(any(), any())).thenReturn(keyGenerator); + + Throwable ex = catchThrowable(() -> command.call()); + + assertThat(ex).isInstanceOf(ConstraintViolationException.class); + + Set> violations = ((ConstraintViolationException) ex).getConstraintViolations(); + + assertThat(violations.size()).isEqualTo(1); + + ConstraintViolation violation = violations.iterator().next(); + + assertThat(violation.getMessage()).isEqualTo("must be a valid AWS service endpoint URL with scheme"); + + verify(encryptorOptions).parseEncryptorConfig(); + verifyNoMoreInteractions(encryptorOptions); + } + @Test public void vaultUrlButNoVaultTypeThrowsException() { final EncryptorOptions encryptorOptions = mock(EncryptorOptions.class); diff --git a/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java b/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java index c88cdb7df0..c61293765c 100644 --- a/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java +++ b/config-migration/src/test/java/com/quorum/tessera/config/migration/TomlConfigFactoryTest.java @@ -141,7 +141,8 @@ public void ifPublicAndPrivateKeyListAreEmptyThenKeyConfigurationIsAllNulls() th @Test public void ifPublicKeyListIsEmptyThenKeyConfigurationIsAllNulls() throws IOException { try (InputStream configData = getClass().getResourceAsStream("/sample-with-only-private-keys.conf")) { - final Throwable throwable = catchThrowable(() -> tomlConfigFactory.createKeyDataBuilder(configData).build()); + final Throwable throwable = + catchThrowable(() -> tomlConfigFactory.createKeyDataBuilder(configData).build()); assertThat(throwable).isInstanceOf(ConfigException.class).hasCauseExactlyInstanceOf(RuntimeException.class); diff --git a/config/src/main/java/com/quorum/tessera/config/DefaultKeyVaultConfig.java b/config/src/main/java/com/quorum/tessera/config/DefaultKeyVaultConfig.java index 960021a7ce..e1227aa449 100644 --- a/config/src/main/java/com/quorum/tessera/config/DefaultKeyVaultConfig.java +++ b/config/src/main/java/com/quorum/tessera/config/DefaultKeyVaultConfig.java @@ -1,6 +1,7 @@ package com.quorum.tessera.config; import com.quorum.tessera.config.adapters.MapAdapter; +import com.quorum.tessera.config.constraints.ValidKeyVaultConfig; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.*; @@ -9,13 +10,12 @@ import java.util.HashMap; import java.util.Map; +@ValidKeyVaultConfig @XmlType(name = "keyVaultConfig") @XmlAccessorType(XmlAccessType.FIELD) public class DefaultKeyVaultConfig extends ConfigItem implements KeyVaultConfig { - @NotNull - @XmlAttribute - private KeyVaultType keyVaultType; + @NotNull @XmlAttribute private KeyVaultType keyVaultType; @XmlJavaTypeAdapter(MapAdapter.class) @XmlElement diff --git a/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java b/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java index 25560cc26c..84d58b1ed7 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyConfiguration.java @@ -2,7 +2,6 @@ import com.quorum.tessera.config.adapters.KeyDataAdapter; import com.quorum.tessera.config.adapters.PathAdapter; -import com.quorum.tessera.config.constraints.ValidKeyVaultConfig; import com.quorum.tessera.config.constraints.ValidPath; import com.quorum.tessera.config.keypairs.ConfigKeyPair; @@ -36,7 +35,7 @@ public class KeyConfiguration extends ConfigItem { @XmlJavaTypeAdapter(KeyDataAdapter.class) private List<@Valid ConfigKeyPair> keyData; - @ValidKeyVaultConfig @XmlElement private DefaultKeyVaultConfig keyVaultConfig; + @Valid @XmlElement private DefaultKeyVaultConfig keyVaultConfig; @Valid @XmlElement private AzureKeyVaultConfig azureKeyVaultConfig; @@ -53,6 +52,7 @@ public KeyConfiguration( this.keyData = keyData; this.azureKeyVaultConfig = azureKeyVaultConfig; this.hashicorpKeyVaultConfig = hashicorpKeyVaultConfig; + if (null != azureKeyVaultConfig) { this.keyVaultConfig = KeyVaultConfigConverter.convert(azureKeyVaultConfig); } else if (null != hashicorpKeyVaultConfig) { diff --git a/config/src/main/java/com/quorum/tessera/config/KeyData.java b/config/src/main/java/com/quorum/tessera/config/KeyData.java index f25dc49cbc..3d44a7e06b 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyData.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyData.java @@ -58,7 +58,13 @@ public class KeyData extends ConfigItem { @XmlElement private String hashicorpVaultSecretVersion; - public KeyData(KeyDataConfig config, String privateKey, String publicKey, Path privateKeyPath, Path publicKeyPath, String azureVaultPublicKeyId, String azureVaultPrivateKeyId, String azureVaultPublicKeyVersion, String azureVaultPrivateKeyVersion, String hashicorpVaultPublicKeyId, String hashicorpVaultPrivateKeyId, String hashicorpVaultSecretEngineName, String hashicorpVaultSecretName, String hashicorpVaultSecretVersion) { + @XmlElement + private String awsSecretsManagerPublicKeyId; + + @XmlElement + private String awsSecretsManagerPrivateKeyId; + + public KeyData(KeyDataConfig config, String privateKey, String publicKey, Path privateKeyPath, Path publicKeyPath, String azureVaultPublicKeyId, String azureVaultPrivateKeyId, String azureVaultPublicKeyVersion, String azureVaultPrivateKeyVersion, String hashicorpVaultPublicKeyId, String hashicorpVaultPrivateKeyId, String hashicorpVaultSecretEngineName, String hashicorpVaultSecretName, String hashicorpVaultSecretVersion, String awsSecretsManagerPublicKeyId, String awsSecretsManagerPrivateKeyId) { this.config = config; this.privateKey = privateKey; this.publicKey = publicKey; @@ -73,6 +79,8 @@ public KeyData(KeyDataConfig config, String privateKey, String publicKey, Path p this.hashicorpVaultSecretEngineName = hashicorpVaultSecretEngineName; this.hashicorpVaultSecretName = hashicorpVaultSecretName; this.hashicorpVaultSecretVersion = hashicorpVaultSecretVersion; + this.awsSecretsManagerPublicKeyId = awsSecretsManagerPublicKeyId; + this.awsSecretsManagerPrivateKeyId = awsSecretsManagerPrivateKeyId; } public KeyData() { @@ -135,6 +143,14 @@ public String getHashicorpVaultSecretVersion() { return hashicorpVaultSecretVersion; } + public String getAwsSecretsManagerPublicKeyId() { + return awsSecretsManagerPublicKeyId; + } + + public String getAwsSecretsManagerPrivateKeyId() { + return awsSecretsManagerPrivateKeyId; + } + public void setConfig(KeyDataConfig config) { this.config = config; } @@ -191,4 +207,11 @@ public void setHashicorpVaultSecretVersion(String hashicorpVaultSecretVersion) { this.hashicorpVaultSecretVersion = hashicorpVaultSecretVersion; } + public void setAwsSecretsManagerPublicKeyId(String awsSecretsManagerPublicKeyId) { + this.awsSecretsManagerPublicKeyId = awsSecretsManagerPublicKeyId; + } + + public void setAwsSecretsManagerPrivateKeyId(String awsSecretsManagerPrivateKeyId) { + this.awsSecretsManagerPrivateKeyId = awsSecretsManagerPrivateKeyId; + } } diff --git a/config/src/main/java/com/quorum/tessera/config/KeyVaultConfigConverter.java b/config/src/main/java/com/quorum/tessera/config/KeyVaultConfigConverter.java index 5829db809f..c5a2ed74ea 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyVaultConfigConverter.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyVaultConfigConverter.java @@ -21,20 +21,22 @@ static DefaultKeyVaultConfig convert(AzureKeyVaultConfig azureKeyVaultConfig) { static DefaultKeyVaultConfig convert(HashicorpKeyVaultConfig hashicorpKeyVaultConfig) { DefaultKeyVaultConfig config = new DefaultKeyVaultConfig(); config.setKeyVaultType(hashicorpKeyVaultConfig.getKeyVaultType()); - config.setProperty("url",hashicorpKeyVaultConfig.getUrl()); - config.setProperty("approlePath",hashicorpKeyVaultConfig.getApprolePath()); + config.setProperty("url", hashicorpKeyVaultConfig.getUrl()); + config.setProperty("approlePath", hashicorpKeyVaultConfig.getApprolePath()); Optional.ofNullable(hashicorpKeyVaultConfig.getTlsKeyStorePath()) - .map(Objects::toString) - .ifPresent(v -> { - config.setProperty("tlsKeyStorePath",v); - }); + .map(Objects::toString) + .ifPresent( + v -> { + config.setProperty("tlsKeyStorePath", v); + }); Optional.ofNullable(hashicorpKeyVaultConfig.getTlsTrustStorePath()) - .map(Objects::toString) - .ifPresent(v -> { - config.setProperty("tlsTrustStorePath",v); - }); + .map(Objects::toString) + .ifPresent( + v -> { + config.setProperty("tlsTrustStorePath", v); + }); return config; } diff --git a/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java b/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java index fa0dfd6e07..e61da3397c 100644 --- a/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java +++ b/config/src/main/java/com/quorum/tessera/config/KeyVaultType.java @@ -1,5 +1,5 @@ package com.quorum.tessera.config; public enum KeyVaultType { - AZURE, HASHICORP + AZURE, HASHICORP, AWS } diff --git a/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java b/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java index fed510cad6..90f859cea1 100644 --- a/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java +++ b/config/src/main/java/com/quorum/tessera/config/adapters/KeyDataAdapter.java @@ -59,13 +59,19 @@ public ConfigKeyPair unmarshal(final KeyData keyData) { keyData.getHashicorpVaultSecretVersion()); } - // case 5, the keys are provided inside a file + // case 5, the AWS Secrets Manager data is provided + if (keyData.getAwsSecretsManagerPublicKeyId() != null && keyData.getAwsSecretsManagerPrivateKeyId() != null) { + return new AWSKeyPair( + keyData.getAwsSecretsManagerPublicKeyId(), keyData.getAwsSecretsManagerPrivateKeyId()); + } + + // case 6, the keys are provided inside a file if (keyData.getPublicKeyPath() != null && keyData.getPrivateKeyPath() != null) { final KeyEncryptor keyEncryptor = keyEncryptorHolder.getKeyEncryptor().get(); return new FilesystemKeyPair(keyData.getPublicKeyPath(), keyData.getPrivateKeyPath(), keyEncryptor); } - // case 6, the key config specified is invalid + // case 7, the key config specified is invalid return new UnsupportedKeyPair( keyData.getConfig(), keyData.getPrivateKey(), @@ -80,7 +86,9 @@ public ConfigKeyPair unmarshal(final KeyData keyData) { keyData.getHashicorpVaultPrivateKeyId(), keyData.getHashicorpVaultSecretEngineName(), keyData.getHashicorpVaultSecretName(), - keyData.getHashicorpVaultSecretVersion()); + keyData.getHashicorpVaultSecretVersion(), + keyData.getAwsSecretsManagerPublicKeyId(), + keyData.getAwsSecretsManagerPrivateKeyId()); } @Override @@ -128,6 +136,14 @@ public KeyData marshal(final ConfigKeyPair keyPair) { return keyData; } + if (keyPair instanceof AWSKeyPair) { + AWSKeyPair kp = (AWSKeyPair) keyPair; + + keyData.setAwsSecretsManagerPublicKeyId(kp.getPublicKeyId()); + keyData.setAwsSecretsManagerPrivateKeyId(kp.getPrivateKeyId()); + return keyData; + } + if (keyPair instanceof FilesystemKeyPair) { FilesystemKeyPair kp = (FilesystemKeyPair) keyPair; @@ -152,7 +168,9 @@ public KeyData marshal(final ConfigKeyPair keyPair) { kp.getHashicorpVaultPublicKeyId(), kp.getHashicorpVaultSecretEngineName(), kp.getHashicorpVaultSecretName(), - kp.getHashicorpVaultSecretVersion()); + kp.getHashicorpVaultSecretVersion(), + kp.getAwsSecretsManagerPublicKeyId(), + kp.getAwsSecretsManagerPrivateKeyId()); } throw new UnsupportedOperationException("The keypair type " + keyPair.getClass() + " is not allowed"); diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigValidator.java b/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigValidator.java index 8256106884..ee97a2ece7 100644 --- a/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigValidator.java +++ b/config/src/main/java/com/quorum/tessera/config/constraints/KeyVaultConfigValidator.java @@ -66,6 +66,19 @@ public boolean isValid( } } + if (keyVaultType == KeyVaultType.AWS) { + + if (keyVaultConfig.getProperties().containsKey("endpoint")) { + if (!keyVaultConfig.getProperties().get("endpoint").matches("^https?://.+$")) { + constraintValidatorContext.disableDefaultConstraintViolation(); + constraintValidatorContext + .buildConstraintViolationWithTemplate("must be a valid AWS service endpoint URL with scheme") + .addConstraintViolation(); + outcomes.add(Boolean.FALSE); + } + } + } + return outcomes.stream().allMatch(Boolean::booleanValue); } } diff --git a/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java b/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java index a2f4504137..fce35fc1b2 100644 --- a/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java +++ b/config/src/main/java/com/quorum/tessera/config/constraints/UnsupportedKeyPairValidator.java @@ -11,27 +11,32 @@ public class UnsupportedKeyPairValidator implements ConstraintValidator result = converter.convert(Collections.singletonList(keyPair)); + + assertThat(result).hasSize(1); + + KeyPair resultKeyPair = result.iterator().next(); + KeyPair expected = + new KeyPair(PublicKey.from(decodeBase64("publicSecret")), PrivateKey.from(decodeBase64("privSecret"))); assertThat(resultKeyPair).isEqualToComparingFieldByField(expected); } diff --git a/enclave/enclave-api/src/test/java/com/quorum/tessera/enclave/MockAwsKeyVaultServiceFactory.java b/enclave/enclave-api/src/test/java/com/quorum/tessera/enclave/MockAwsKeyVaultServiceFactory.java new file mode 100644 index 0000000000..a2cd26c432 --- /dev/null +++ b/enclave/enclave-api/src/test/java/com/quorum/tessera/enclave/MockAwsKeyVaultServiceFactory.java @@ -0,0 +1,28 @@ +package com.quorum.tessera.enclave; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.KeyVaultType; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.AWSGetSecretData; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.KeyVaultServiceFactory; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockAwsKeyVaultServiceFactory implements KeyVaultServiceFactory { + @Override + public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { + KeyVaultService mock = mock(KeyVaultService.class); + + when(mock.getSecret(any(AWSGetSecretData.class))).thenReturn("publicSecret").thenReturn("privSecret"); + + return mock; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.AWS; + } +} diff --git a/enclave/enclave-api/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory b/enclave/enclave-api/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory index 28e5298b89..160e74aae0 100644 --- a/enclave/enclave-api/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory +++ b/enclave/enclave-api/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory @@ -1,2 +1,3 @@ com.quorum.tessera.enclave.MockAzureKeyVaultServiceFactory -com.quorum.tessera.enclave.MockHashicorpKeyVaultServiceFactory \ No newline at end of file +com.quorum.tessera.enclave.MockHashicorpKeyVaultServiceFactory +com.quorum.tessera.enclave.MockAwsKeyVaultServiceFactory diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/AWSSecretManagerKeyGenerator.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/AWSSecretManagerKeyGenerator.java new file mode 100644 index 0000000000..c0cc408f72 --- /dev/null +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/AWSSecretManagerKeyGenerator.java @@ -0,0 +1,65 @@ +package com.quorum.tessera.key.generation; + +import com.quorum.tessera.config.ArgonOptions; +import com.quorum.tessera.config.keypairs.AWSKeyPair; +import com.quorum.tessera.config.vault.data.AWSGetSecretData; +import com.quorum.tessera.config.vault.data.AWSSetSecretData; +import com.quorum.tessera.encryption.Encryptor; +import com.quorum.tessera.encryption.Key; +import com.quorum.tessera.encryption.KeyPair; +import com.quorum.tessera.key.vault.KeyVaultService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.UnsupportedCharsetException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class AWSSecretManagerKeyGenerator implements KeyGenerator { + private static final Logger LOGGER = LoggerFactory.getLogger(AWSSecretManagerKeyGenerator.class); + + private final Encryptor encryptor; + private final KeyVaultService keyVaultService; + + public AWSSecretManagerKeyGenerator( + Encryptor encryptor, KeyVaultService keyVaultService) { + + this.encryptor = encryptor; + this.keyVaultService = keyVaultService; + } + + @Override + public AWSKeyPair generate(String filename, ArgonOptions encryptionOptions, KeyVaultOptions keyVaultOptions) { + final KeyPair keys = this.encryptor.generateNewKeys(); + + final StringBuilder publicId = new StringBuilder(); + final StringBuilder privateId = new StringBuilder(); + + if (filename != null) { + final Path path = Paths.get(filename); + final String secretId = path.getFileName().toString(); + + if (!secretId.matches("^[0-9a-zA-Z\\-/_+=.@]*$")) { + throw new UnsupportedCharsetException( + "Generated key ID for AWS Secret Manager can contain only 0-9, a-z, A-Z and /_+=.@- characters"); + } + + publicId.append(secretId); + privateId.append(secretId); + } + + publicId.append("Pub"); + privateId.append("Key"); + + saveKeyInSecretManager(publicId.toString(), keys.getPublicKey()); + saveKeyInSecretManager(privateId.toString(), keys.getPrivateKey()); + + return new AWSKeyPair(publicId.toString(), privateId.toString()); + } + + private void saveKeyInSecretManager(String id, Key key) { + keyVaultService.setSecret(new AWSSetSecretData(id, key.encodeToBase64())); + LOGGER.debug("Key {} saved to vault with id {}", key.encodeToBase64(), id); + LOGGER.info("Key saved to vault with id {}", id); + } +} diff --git a/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java b/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java index dfbe9fb2e1..6de4c0f52c 100644 --- a/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java +++ b/key-generation/src/main/java/com/quorum/tessera/key/generation/DefaultKeyGeneratorFactory.java @@ -4,6 +4,8 @@ import com.quorum.tessera.config.keys.KeyEncryptor; import com.quorum.tessera.config.keys.KeyEncryptorFactory; import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.AWSGetSecretData; +import com.quorum.tessera.config.vault.data.AWSSetSecretData; import com.quorum.tessera.config.vault.data.AzureGetSecretData; import com.quorum.tessera.config.vault.data.AzureSetSecretData; import com.quorum.tessera.config.vault.data.HashicorpGetSecretData; @@ -42,6 +44,19 @@ public KeyGenerator create(KeyVaultConfig keyVaultConfig, EncryptorConfig encryp return new AzureVaultKeyGenerator(encryptor, keyVaultService); + } else if (keyVaultConfig.getKeyVaultType().equals(KeyVaultType.AWS)) { + if (!(keyVaultConfig instanceof DefaultKeyVaultConfig)) { + throw new IllegalArgumentException("AWS key vault config not instance of DefaultKeyVaultConfig"); + } + + keyConfiguration.setKeyVaultConfig((DefaultKeyVaultConfig) keyVaultConfig); + + config.setKeys(keyConfiguration); + + final KeyVaultService keyVaultService = + keyVaultServiceFactory.create(config, new EnvironmentVariableProvider()); + + return new AWSSecretManagerKeyGenerator(encryptor, keyVaultService); } else { keyConfiguration.setHashicorpKeyVaultConfig((HashicorpKeyVaultConfig) keyVaultConfig); diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/AWSSecretManagerKeyGeneratorTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/AWSSecretManagerKeyGeneratorTest.java new file mode 100644 index 0000000000..cf3db0e5d9 --- /dev/null +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/AWSSecretManagerKeyGeneratorTest.java @@ -0,0 +1,141 @@ +package com.quorum.tessera.key.generation; + +import com.quorum.tessera.config.ArgonOptions; +import com.quorum.tessera.config.keypairs.AWSKeyPair; +import com.quorum.tessera.config.vault.data.AWSSetSecretData; +import com.quorum.tessera.encryption.Encryptor; +import com.quorum.tessera.encryption.KeyPair; +import com.quorum.tessera.encryption.PrivateKey; +import com.quorum.tessera.encryption.PublicKey; +import com.quorum.tessera.key.vault.KeyVaultService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.nio.charset.UnsupportedCharsetException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.Mockito.*; + +public class AWSSecretManagerKeyGeneratorTest { + + private final String pubStr = "public"; + private final String privStr = "private"; + private final PublicKey pub = PublicKey.from(pubStr.getBytes()); + private final PrivateKey priv = PrivateKey.from(privStr.getBytes()); + + private KeyVaultService keyVaultService; + private AWSSecretManagerKeyGenerator awsSecretManagerKeyGenerator; + + @Before + public void setUp() { + final Encryptor encryptor = mock(Encryptor.class); + this.keyVaultService = mock(KeyVaultService.class); + + final KeyPair keyPair = new KeyPair(pub, priv); + + when(encryptor.generateNewKeys()).thenReturn(keyPair); + + awsSecretManagerKeyGenerator = new AWSSecretManagerKeyGenerator(encryptor, keyVaultService); + } + + @Test + public void keysSavedInVaultWithProvidedVaultIdAndCorrectSuffix() { + final String vaultId = "vaultId"; + final String pubVaultId = vaultId + "Pub"; + final String privVaultId = vaultId + "Key"; + + final AWSKeyPair result = awsSecretManagerKeyGenerator.generate(vaultId, null, null); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AWSSetSecretData.class); + + verify(keyVaultService, times(2)).setSecret(captor.capture()); + + List capturedArgs = captor.getAllValues(); + assertThat(capturedArgs).hasSize(2); + + AWSSetSecretData expectedDataPub = new AWSSetSecretData(pubVaultId, pub.encodeToBase64()); + AWSSetSecretData expectedDataPriv = new AWSSetSecretData(privVaultId, priv.encodeToBase64()); + + assertThat(capturedArgs) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(expectedDataPub, expectedDataPriv); + + verifyNoMoreInteractions(keyVaultService); + + final AWSKeyPair expected = new AWSKeyPair(pubVaultId, privVaultId); + + assertThat(result).isExactlyInstanceOf(AWSKeyPair.class); + assertThat(result).isEqualToComparingFieldByField(expected); + } + + @Test + public void vaultIdIsFinalComponentOfFilePath() { + final String vaultId = "vaultId"; + final String pubVaultId = vaultId + "Pub"; + final String privVaultId = vaultId + "Key"; + final String path = "/some/path/" + vaultId; + + awsSecretManagerKeyGenerator.generate(path, null, null); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AWSSetSecretData.class); + + verify(keyVaultService, times(2)).setSecret(captor.capture()); + + List capturedArgs = captor.getAllValues(); + assertThat(capturedArgs).hasSize(2); + + AWSSetSecretData expectedDataPub = new AWSSetSecretData(pubVaultId, pub.encodeToBase64()); + AWSSetSecretData expectedDataPriv = new AWSSetSecretData(privVaultId, priv.encodeToBase64()); + + assertThat(capturedArgs) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(expectedDataPub, expectedDataPriv); + + verifyNoMoreInteractions(keyVaultService); + } + + @Test + public void ifNoVaultIdProvidedThenSuffixOnlyIsUsed() { + awsSecretManagerKeyGenerator.generate(null, null, null); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AWSSetSecretData.class); + + verify(keyVaultService, times(2)).setSecret(captor.capture()); + + List capturedArgs = captor.getAllValues(); + assertThat(capturedArgs).hasSize(2); + + AWSSetSecretData expectedDataPub = new AWSSetSecretData("Pub", pub.encodeToBase64()); + AWSSetSecretData expectedDataPriv = new AWSSetSecretData("Key", priv.encodeToBase64()); + + assertThat(capturedArgs) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(expectedDataPub, expectedDataPriv); + + verifyNoMoreInteractions(keyVaultService); + } + + @Test + public void exceptionThrownIfDisallowedCharactersUsedInVaultId() { + final String invalidId = "/tmp/abc@+!"; + + final Throwable throwable = catchThrowable(() -> awsSecretManagerKeyGenerator.generate(invalidId, null, null)); + + assertThat(throwable).isInstanceOf(UnsupportedCharsetException.class); + assertThat(throwable) + .hasMessageContaining( + "Generated key ID for AWS Secret Manager can contain only 0-9, a-z, A-Z and /_+=.@- characters"); + } + + @Test + public void encryptionIsNotUsedWhenSavingToVault() { + final ArgonOptions argonOptions = mock(ArgonOptions.class); + + awsSecretManagerKeyGenerator.generate("vaultId", argonOptions, null); + + verifyNoMoreInteractions(argonOptions); + } +} diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java index 248dbdc899..30876b9d35 100644 --- a/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/KeyGeneratorFactoryTest.java @@ -1,14 +1,13 @@ package com.quorum.tessera.key.generation; -import com.quorum.tessera.config.AzureKeyVaultConfig; -import com.quorum.tessera.config.EncryptorConfig; -import com.quorum.tessera.config.EncryptorType; -import com.quorum.tessera.config.HashicorpKeyVaultConfig; +import com.quorum.tessera.config.*; import com.quorum.tessera.config.util.EnvironmentVariableProvider; -import java.util.Collections; import org.junit.Test; +import java.util.Collections; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -57,4 +56,34 @@ public void hashicorpVaultKeyGeneratorWhenHashicorpConfigProvided() { assertThat(keyGenerator).isNotNull(); assertThat(keyGenerator).isExactlyInstanceOf(HashicorpVaultKeyGenerator.class); } + + @Test + public void awsVaultKeyGeneratorWhenAwsConfigProvided() { + final DefaultKeyVaultConfig keyVaultConfig = new DefaultKeyVaultConfig(); + keyVaultConfig.setKeyVaultType(KeyVaultType.AWS); + + EncryptorConfig encryptorConfig = mock(EncryptorConfig.class); + when(encryptorConfig.getType()).thenReturn(EncryptorType.NACL); + when(encryptorConfig.getProperties()).thenReturn(Collections.EMPTY_MAP); + + final KeyGenerator keyGenerator = KeyGeneratorFactory.newFactory().create(keyVaultConfig, encryptorConfig); + + assertThat(keyGenerator).isNotNull(); + assertThat(keyGenerator).isExactlyInstanceOf(AWSSecretManagerKeyGenerator.class); + } + + @Test + public void awsVaultKeyGeneratorWhenNonDefaultKeyVaultConfig() { + final KeyVaultConfig keyVaultConfig = mock(KeyVaultConfig.class); + when(keyVaultConfig.getKeyVaultType()).thenReturn(KeyVaultType.AWS); + + EncryptorConfig encryptorConfig = mock(EncryptorConfig.class); + when(encryptorConfig.getType()).thenReturn(EncryptorType.NACL); + when(encryptorConfig.getProperties()).thenReturn(Collections.EMPTY_MAP); + + final Throwable ex = + catchThrowable(() -> KeyGeneratorFactory.newFactory().create(keyVaultConfig, encryptorConfig)); + + assertThat(ex).isInstanceOf(IllegalArgumentException.class); + } } diff --git a/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAwsVaultServiceFactory.java b/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAwsVaultServiceFactory.java new file mode 100644 index 0000000000..87b751021a --- /dev/null +++ b/key-generation/src/test/java/com/quorum/tessera/key/generation/MockAwsVaultServiceFactory.java @@ -0,0 +1,28 @@ +package com.quorum.tessera.key.generation; + +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.KeyVaultType; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.config.vault.data.AWSGetSecretData; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.KeyVaultServiceFactory; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockAwsVaultServiceFactory implements KeyVaultServiceFactory { + @Override + public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { + KeyVaultService mock = mock(KeyVaultService.class); + + when(mock.getSecret(any(AWSGetSecretData.class))).thenReturn("publicSecret").thenReturn("privSecret"); + + return mock; + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.AWS; + } +} diff --git a/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory b/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory index ec7218aba2..b305ceabaf 100644 --- a/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory +++ b/key-generation/src/test/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory @@ -1,2 +1,3 @@ com.quorum.tessera.key.generation.MockAzureKeyVaultServiceFactory -com.quorum.tessera.key.generation.MockHashicorpKeyVaultServiceFactory \ No newline at end of file +com.quorum.tessera.key.generation.MockHashicorpKeyVaultServiceFactory +com.quorum.tessera.key.generation.MockAwsVaultServiceFactory \ No newline at end of file diff --git a/key-vault/aws-key-vault/build.gradle b/key-vault/aws-key-vault/build.gradle new file mode 100644 index 0000000000..fd3d5bed4f --- /dev/null +++ b/key-vault/aws-key-vault/build.gradle @@ -0,0 +1,6 @@ + +dependencies { + implementation project(':config') + implementation 'software.amazon.awssdk:secretsmanager:2.10.25' + implementation project(':key-vault:key-vault-api') +} diff --git a/key-vault/aws-key-vault/pom.xml b/key-vault/aws-key-vault/pom.xml new file mode 100644 index 0000000000..3272d5663c --- /dev/null +++ b/key-vault/aws-key-vault/pom.xml @@ -0,0 +1,52 @@ + + + + key-vault + com.jpmorgan.quorum + 0.11-SNAPSHOT + + 4.0.0 + + aws-key-vault + + + + + com.jpmorgan.quorum + config + + + + software.amazon.awssdk + secretsmanager + + + + com.jpmorgan.quorum + key-vault-api + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + + + + + + diff --git a/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultService.java b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultService.java new file mode 100644 index 0000000000..9920e4afee --- /dev/null +++ b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultService.java @@ -0,0 +1,59 @@ +package com.quorum.tessera.key.vault.aws; + +import com.quorum.tessera.config.vault.data.AWSGetSecretData; +import com.quorum.tessera.config.vault.data.AWSSetSecretData; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.VaultSecretNotFoundException; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import software.amazon.awssdk.services.secretsmanager.model.InvalidParameterException; +import software.amazon.awssdk.services.secretsmanager.model.InvalidRequestException; +import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException; + +public class AWSKeyVaultService implements KeyVaultService { + private final SecretsManagerClient secretsManager; + + AWSKeyVaultService(SecretsManagerClient secretsManager) { + this.secretsManager = secretsManager; + } + + @Override + public String getSecret(AWSGetSecretData getSecretData) { + GetSecretValueRequest getSecretValueRequest = + GetSecretValueRequest.builder() + .secretId(getSecretData.getSecretName()) + .build(); + GetSecretValueResponse secretValueResponse; + + try { + secretValueResponse = secretsManager.getSecretValue(getSecretValueRequest); + } catch (ResourceNotFoundException e) { + throw new VaultSecretNotFoundException( + "The requested secret '" + + getSecretData.getSecretName() + + "' was not found in AWS Secrets Manager"); + } catch (InvalidRequestException | InvalidParameterException e) { + throw new AWSSecretsManagerException(e); + } + + if (secretValueResponse != null && secretValueResponse.secretString() != null) { + return secretValueResponse.secretString(); + } + + throw new VaultSecretNotFoundException( + "The requested secret '" + getSecretData.getSecretName() + "' was not found in AWS Secrets Manager"); + } + + @Override + public Object setSecret(AWSSetSecretData setSecretData) { + CreateSecretRequest createSecretRequest = + CreateSecretRequest.builder() + .name(setSecretData.getSecretName()) + .secretString(setSecretData.getSecret()) + .build(); + + return secretsManager.createSecret(createSecretRequest); + } +} diff --git a/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceFactory.java b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceFactory.java new file mode 100644 index 0000000000..c268e2f559 --- /dev/null +++ b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceFactory.java @@ -0,0 +1,73 @@ +package com.quorum.tessera.key.vault.aws; + +import com.quorum.tessera.config.*; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.key.vault.KeyVaultService; +import com.quorum.tessera.key.vault.KeyVaultServiceFactory; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.Optional; + +import static com.quorum.tessera.config.util.EnvironmentVariables.AWS_ACCESS_KEY_ID; +import static com.quorum.tessera.config.util.EnvironmentVariables.AWS_SECRET_ACCESS_KEY; + +public class AWSKeyVaultServiceFactory implements KeyVaultServiceFactory { + @Override + public KeyVaultService create(Config config, EnvironmentVariableProvider envProvider) { + String accessKeyID = envProvider.getEnv(AWS_ACCESS_KEY_ID); + String secretAccessKey = envProvider.getEnv(AWS_SECRET_ACCESS_KEY); + + if ((accessKeyID != null && secretAccessKey == null) || (secretAccessKey != null && accessKeyID == null)) { + throw new IncompleteAWSCredentialsException( + "If using environment variables, both " + + AWS_ACCESS_KEY_ID + + " and " + + AWS_SECRET_ACCESS_KEY + + " must be set"); + } + + KeyVaultConfig keyVaultConfig = + Optional.ofNullable(config.getKeys()) + .map(KeyConfiguration::getKeyVaultConfig) + .orElseThrow( + () -> + new ConfigException( + new RuntimeException( + "Trying to create AWS Secrets Manager connection but no configuration provided"))); + + return new AWSKeyVaultService(getAwsSecretsManager(keyVaultConfig)); + } + + @Override + public KeyVaultType getType() { + return KeyVaultType.AWS; + } + + private SecretsManagerClient getAwsSecretsManager(KeyVaultConfig keyVaultConfig) { + SecretsManagerClientBuilder secretsManagerClient = SecretsManagerClient.builder(); + + Optional endpoint = keyVaultConfig.getProperty("endpoint"); + endpoint.ifPresent( + s -> { + final URI uri; + + try { + uri = new URI(s); + } catch (URISyntaxException e) { + throw new ConfigException(new RuntimeException("Invalid AWS endpoint URL provided")); + } + + if (Objects.isNull(uri.getScheme())) { + throw new ConfigException( + new RuntimeException("Invalid AWS endpoint URL provided - no scheme")); + } + + secretsManagerClient.endpointOverride(uri); + }); + return secretsManagerClient.build(); + } +} diff --git a/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSSecretsManagerException.java b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSSecretsManagerException.java new file mode 100644 index 0000000000..248090036e --- /dev/null +++ b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/AWSSecretsManagerException.java @@ -0,0 +1,9 @@ +package com.quorum.tessera.key.vault.aws; + +import com.quorum.tessera.key.vault.KeyVaultException; + +class AWSSecretsManagerException extends KeyVaultException { + AWSSecretsManagerException(Throwable cause) { + super(cause); + } +} diff --git a/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/IncompleteAWSCredentialsException.java b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/IncompleteAWSCredentialsException.java new file mode 100644 index 0000000000..b306cab6d4 --- /dev/null +++ b/key-vault/aws-key-vault/src/main/java/com/quorum/tessera/key/vault/aws/IncompleteAWSCredentialsException.java @@ -0,0 +1,8 @@ +package com.quorum.tessera.key.vault.aws; + +class IncompleteAWSCredentialsException extends IllegalStateException { + + IncompleteAWSCredentialsException(String message) { + super(message); + } +} diff --git a/key-vault/aws-key-vault/src/main/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory b/key-vault/aws-key-vault/src/main/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory new file mode 100644 index 0000000000..fd84f37e69 --- /dev/null +++ b/key-vault/aws-key-vault/src/main/resources/META-INF/services/com.quorum.tessera.key.vault.KeyVaultServiceFactory @@ -0,0 +1 @@ +com.quorum.tessera.key.vault.aws.AWSKeyVaultServiceFactory \ No newline at end of file diff --git a/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceFactoryTest.java b/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceFactoryTest.java new file mode 100644 index 0000000000..7291fc4d3b --- /dev/null +++ b/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceFactoryTest.java @@ -0,0 +1,167 @@ +package com.quorum.tessera.key.vault.aws; + +import com.quorum.tessera.config.*; +import com.quorum.tessera.config.util.EnvironmentVariableProvider; +import com.quorum.tessera.key.vault.KeyVaultService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static com.quorum.tessera.config.util.EnvironmentVariables.AWS_ACCESS_KEY_ID; +import static com.quorum.tessera.config.util.EnvironmentVariables.AWS_SECRET_ACCESS_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AWSKeyVaultServiceFactoryTest { + + private AWSKeyVaultServiceFactory awsKeyVaultServiceFactory; + + private Config config; + + private EnvironmentVariableProvider envProvider; + + @Before + public void setUp() { + this.config = mock(Config.class); + this.envProvider = mock(EnvironmentVariableProvider.class); + this.awsKeyVaultServiceFactory = new AWSKeyVaultServiceFactory(); + + // required by the AWS SDK + System.setProperty("aws.region", "a-region"); + } + + @After + public void tearDown() { + System.clearProperty("aws.region"); + } + + @Test(expected = NullPointerException.class) + public void nullConfigThrowsException() { + awsKeyVaultServiceFactory.create(null, envProvider); + } + + @Test + public void nullKeyConfigurationThrowsException() { + when(envProvider.getEnv(anyString())).thenReturn("envVar"); + when(config.getKeys()).thenReturn(null); + + Throwable ex = catchThrowable(() -> awsKeyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isExactlyInstanceOf(ConfigException.class); + assertThat(ex.getMessage()) + .contains("Trying to create AWS Secrets Manager connection but no configuration provided"); + } + + @Test + public void nullKeyVaultConfigurationThrowsException() { + when(envProvider.getEnv(anyString())).thenReturn("envVar"); + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + when(keyConfiguration.getKeyVaultConfig()).thenReturn(null); + when(config.getKeys()).thenReturn(keyConfiguration); + + Throwable ex = catchThrowable(() -> awsKeyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isExactlyInstanceOf(ConfigException.class); + assertThat(ex.getMessage()) + .contains("Trying to create AWS Secrets Manager connection but no configuration provided"); + } + + @Test + public void onlyAWSAccessKeyIDEnvVarProvidedThrowsException() { + Config config = mock(Config.class); + + when(envProvider.getEnv(AWS_ACCESS_KEY_ID)).thenReturn("id"); + Throwable ex = catchThrowable(() -> awsKeyVaultServiceFactory.create(config, envProvider)); + assertThat(ex).isInstanceOf(IncompleteAWSCredentialsException.class); + assertThat(ex) + .hasMessageContaining( + "If using environment variables, both " + + AWS_ACCESS_KEY_ID + + " and " + + AWS_SECRET_ACCESS_KEY + + " must be set"); + } + + @Test + public void onlyAWSSecretAccessKeyEnvVarProvidedThrowsException() { + Config config = mock(Config.class); + + when(envProvider.getEnv(AWS_SECRET_ACCESS_KEY)).thenReturn("secret"); + Throwable ex = catchThrowable(() -> awsKeyVaultServiceFactory.create(config, envProvider)); + assertThat(ex).isInstanceOf(IncompleteAWSCredentialsException.class); + assertThat(ex) + .hasMessageContaining( + "If using environment variables, both " + + AWS_ACCESS_KEY_ID + + " and " + + AWS_SECRET_ACCESS_KEY + + " must be set"); + } + + @Test + public void envVarsAndKeyVaultConfigProvidedCreatesAWSKeyVaultService() { + when(envProvider.getEnv(anyString())).thenReturn("envVar"); + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + DefaultKeyVaultConfig keyVaultConfig = mock(DefaultKeyVaultConfig.class); + when(keyVaultConfig.getProperty("endpoint")).thenReturn(Optional.of("http://URL")); + when(keyConfiguration.getKeyVaultConfig()).thenReturn(keyVaultConfig); + when(config.getKeys()).thenReturn(keyConfiguration); + + KeyVaultService result = awsKeyVaultServiceFactory.create(config, envProvider); + + assertThat(result).isInstanceOf(AWSKeyVaultService.class); + } + + @Test + public void envVarsAndKeyVaultConfigWithNoEndpointProvidedCreatesAWSKeyVaultService() { + when(envProvider.getEnv(anyString())).thenReturn("envVar"); + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + DefaultKeyVaultConfig keyVaultConfig = mock(DefaultKeyVaultConfig.class); + when(keyConfiguration.getKeyVaultConfig()).thenReturn(keyVaultConfig); + when(config.getKeys()).thenReturn(keyConfiguration); + + KeyVaultService result = awsKeyVaultServiceFactory.create(config, envProvider); + + assertThat(result).isInstanceOf(AWSKeyVaultService.class); + } + + @Test + public void invalidEndpointUrlThrowsException() { + when(envProvider.getEnv(anyString())).thenReturn("envVar"); + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + DefaultKeyVaultConfig keyVaultConfig = mock(DefaultKeyVaultConfig.class); + when(keyVaultConfig.getProperty("endpoint")).thenReturn(Optional.of("\\invalid")); + when(keyConfiguration.getKeyVaultConfig()).thenReturn(keyVaultConfig); + when(config.getKeys()).thenReturn(keyConfiguration); + + Throwable ex = catchThrowable(() -> awsKeyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(ConfigException.class); + assertThat(ex).hasMessageEndingWith("Invalid AWS endpoint URL provided"); + } + + @Test + public void noSchemeEndpointUrlThrowsException() { + when(envProvider.getEnv(anyString())).thenReturn("envVar"); + KeyConfiguration keyConfiguration = mock(KeyConfiguration.class); + DefaultKeyVaultConfig keyVaultConfig = mock(DefaultKeyVaultConfig.class); + when(keyVaultConfig.getProperty("endpoint")).thenReturn(Optional.of("noscheme")); + when(keyConfiguration.getKeyVaultConfig()).thenReturn(keyVaultConfig); + when(config.getKeys()).thenReturn(keyConfiguration); + + Throwable ex = catchThrowable(() -> awsKeyVaultServiceFactory.create(config, envProvider)); + + assertThat(ex).isInstanceOf(ConfigException.class); + assertThat(ex).hasMessageEndingWith("Invalid AWS endpoint URL provided - no scheme"); + } + + @Test + public void getType() { + assertThat(awsKeyVaultServiceFactory.getType()).isEqualTo(KeyVaultType.AWS); + } +} diff --git a/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceTest.java b/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceTest.java new file mode 100644 index 0000000000..68950a1ac9 --- /dev/null +++ b/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/AWSKeyVaultServiceTest.java @@ -0,0 +1,114 @@ +package com.quorum.tessera.key.vault.aws; + +import com.quorum.tessera.config.vault.data.AWSGetSecretData; +import com.quorum.tessera.config.vault.data.AWSSetSecretData; +import com.quorum.tessera.key.vault.VaultSecretNotFoundException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest; +import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse; +import software.amazon.awssdk.services.secretsmanager.model.InvalidParameterException; +import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AWSKeyVaultServiceTest { + + private AWSKeyVaultService keyVaultService; + + private String endpoint = "endpoint"; + + private SecretsManagerClient secretsManager; + + @Before + public void setUp() { + this.secretsManager = mock(SecretsManagerClient.class); + + this.keyVaultService = new AWSKeyVaultService(secretsManager); + } + + @Test + public void getSecret() { + String secretName = "name"; + + AWSGetSecretData getSecretData = mock(AWSGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretName); + + GetSecretValueResponse secretValueResponse = GetSecretValueResponse.builder().secretString("secret").build(); + + when(secretsManager.getSecretValue(Mockito.any(GetSecretValueRequest.class))).thenReturn(secretValueResponse); + + assertThat(keyVaultService.getSecret(getSecretData)).isEqualTo("secret"); + } + + @Test + public void getSecretThrowsExceptionIfSecretReturnedIsNull() { + String secretName = "secret"; + + AWSGetSecretData getSecretData = mock(AWSGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretName); + + Throwable throwable = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(throwable).isInstanceOf(VaultSecretNotFoundException.class); + assertThat(throwable) + .hasMessageContaining("The requested secret '" + secretName + "' was not found in AWS Secrets Manager"); + } + + @Test + public void getSecretThrowsExceptionIfKeyNotFoundInVault() { + String secretName = "secret"; + + AWSGetSecretData getSecretData = mock(AWSGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretName); + + when(secretsManager.getSecretValue(Mockito.any(GetSecretValueRequest.class))) + .thenThrow(ResourceNotFoundException.builder().build()); + + Throwable throwable = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(throwable).isInstanceOf(VaultSecretNotFoundException.class); + assertThat(throwable) + .hasMessageContaining("The requested secret '" + secretName + "' was not found in AWS Secrets Manager"); + } + + @Test + public void getSecretThrowsExceptionIfAWSException() { + String secretName = "secret"; + + AWSGetSecretData getSecretData = mock(AWSGetSecretData.class); + when(getSecretData.getSecretName()).thenReturn(secretName); + + when(secretsManager.getSecretValue(Mockito.any(GetSecretValueRequest.class))) + .thenThrow(InvalidParameterException.builder().build()); + + Throwable throwable = catchThrowable(() -> keyVaultService.getSecret(getSecretData)); + + assertThat(throwable).isInstanceOf(AWSSecretsManagerException.class); + } + + @Test + public void setSecret() { + AWSSetSecretData setSecretData = mock(AWSSetSecretData.class); + String secretName = "id"; + String secret = "secret"; + when(setSecretData.getSecretName()).thenReturn(secretName); + when(setSecretData.getSecret()).thenReturn(secret); + + keyVaultService.setSecret(setSecretData); + + CreateSecretRequest expected = CreateSecretRequest.builder().name(secretName).secretString(secret).build(); + + ArgumentCaptor argument = ArgumentCaptor.forClass(CreateSecretRequest.class); + verify(secretsManager).createSecret(argument.capture()); + + assertThat(argument.getValue()).isEqualToComparingFieldByField(expected); + } +} diff --git a/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/IncompleteAWSCredentialsExceptionTest.java b/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/IncompleteAWSCredentialsExceptionTest.java new file mode 100644 index 0000000000..4c421988b2 --- /dev/null +++ b/key-vault/aws-key-vault/src/test/java/com/quorum/tessera/key/vault/aws/IncompleteAWSCredentialsExceptionTest.java @@ -0,0 +1,16 @@ +package com.quorum.tessera.key.vault.aws; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IncompleteAWSCredentialsExceptionTest { + + @Test + public void createWithMessage() { + final String msg = "msg"; + IncompleteAWSCredentialsException exception = new IncompleteAWSCredentialsException(msg); + + assertThat(exception).hasMessage(msg); + } +} diff --git a/key-vault/pom.xml b/key-vault/pom.xml index d6015ec182..434590ec4d 100644 --- a/key-vault/pom.xml +++ b/key-vault/pom.xml @@ -13,6 +13,7 @@ azure-key-vault key-vault-api hashicorp-key-vault + aws-key-vault diff --git a/pom.xml b/pom.xml index aa240f8dd5..67327a81b1 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,7 @@ 7.0 1.9.3 4.0.4 + 2.10.25 @@ -516,7 +517,6 @@ 0.11-SNAPSHOT - com.jpmorgan.quorum security @@ -654,6 +654,12 @@ 0.11-SNAPSHOT + + com.jpmorgan.quorum + aws-key-vault + 0.11-SNAPSHOT + + com.jpmorgan.quorum hashicorp-key-vault @@ -1274,6 +1280,33 @@ ${picocli.version} + + software.amazon.awssdk + secretsmanager + ${awssdk.version} + + + + + io.netty + netty-handler + 4.1.42.Final + + + + + io.netty + netty-codec-http + 4.1.42.Final + + + + + org.apache.httpcomponents + httpclient + 4.5.9 + + diff --git a/settings.gradle b/settings.gradle index 0c00259abc..edc95b08f2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,6 +29,7 @@ include(':config-migration') include(':shared') include(':tessera-core') include(':key-vault:azure-key-vault') +include(':key-vault:aws-key-vault') include(':key-vault:key-vault-api') include(':key-vault:hashicorp-key-vault') include(':key-vault') @@ -72,6 +73,7 @@ project(':encryption:encryption-jnacl').projectDir = file('encryption/encryption project(':encryption:encryption-kalium').projectDir = file('encryption/encryption-kalium') project(':encryption:encryption-ec').projectDir = file('encryption/encryption-ec') project(':key-vault:azure-key-vault').projectDir = file('key-vault/azure-key-vault') +project(':key-vault:aws-key-vault').projectDir = file('key-vault/aws-key-vault') project(':key-vault:key-vault-api').projectDir = file('key-vault/key-vault-api') project(':key-vault:hashicorp-key-vault').projectDir = file('key-vault/hashicorp-key-vault') project(':enclave:enclave-api').projectDir = file('enclave/enclave-api') diff --git a/tessera-dist/tessera-app/pom.xml b/tessera-dist/tessera-app/pom.xml index 0a6eae6a23..f05d72c0f2 100644 --- a/tessera-dist/tessera-app/pom.xml +++ b/tessera-dist/tessera-app/pom.xml @@ -143,6 +143,18 @@ runtime + + com.jpmorgan.quorum + aws-key-vault + runtime + + + commons-logging + commons-logging + + + + org.glassfish.jersey.media jersey-media-json-processing diff --git a/tests/acceptance-test/pom.xml b/tests/acceptance-test/pom.xml index e8b4a4669b..fd465d1008 100644 --- a/tests/acceptance-test/pom.xml +++ b/tests/acceptance-test/pom.xml @@ -267,6 +267,7 @@ RunHashicorpIT RunAzureIT + RunAwsIT @@ -308,6 +309,23 @@ + + aws-vault-acceptance-tests + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + RunAwsIT + + + + + + + simple-acceptance-tests diff --git a/tests/acceptance-test/src/test/java/com/quorum/tessera/test/vault/aws/AwsStepDefs.java b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/vault/aws/AwsStepDefs.java new file mode 100644 index 0000000000..26fd229aa6 --- /dev/null +++ b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/vault/aws/AwsStepDefs.java @@ -0,0 +1,326 @@ +package com.quorum.tessera.test.vault.aws; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; +import com.quorum.tessera.config.Config; +import com.quorum.tessera.config.util.JaxbUtil; +import com.quorum.tessera.test.util.ElUtil; +import cucumber.api.java8.En; +import exec.NodeExecManager; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.ws.rs.core.UriBuilder; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +public class AwsStepDefs implements En { + + private static final String AWS_SECRETS_MANAGER_URL = "/"; + private static final String AWS_REGION = "AWS_REGION"; + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final AtomicReference tesseraProcess = new AtomicReference<>(); + private final AtomicReference wireMockServer = new AtomicReference<>(); + + private final String publicKey = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo="; + + public AwsStepDefs() { + + Before( + () -> { + // // only needed when running outside of maven build process + // System.setProperty( + // "application.jar", + // "path/to/tessera-app-VERSION.jar"); + }); + + After( + () -> { + if (wireMockServer.get() != null && wireMockServer.get().isRunning()) { + wireMockServer.get().stop(); + System.out.println("Stopped WireMockServer..."); + } + + if (tesseraProcess.get() != null && tesseraProcess.get().isAlive()) { + tesseraProcess.get().destroy(); + System.out.println("Stopped Tessera node..."); + } + }); + + Given( + "^the mock AWS Secrets Manager server has been started$", + () -> { + final URL keystore = getClass().getResource("/certificates/localhost-with-san-keystore.jks"); + + // wiremock configures an HTTP server by default. Even though we'll only use the HTTPS server we + // dynamically assign the HTTP port to ensure the default of 8080 is not used + wireMockServer.set( + new WireMockServer( + options() + .dynamicPort() + .dynamicHttpsPort() + .keystoreType("JKS") + .keystorePath(keystore.getFile()) + .keystorePassword("testtest") + .extensions(new ResponseTemplateTransformer(false)) + // .notifier(new ConsoleNotifier(true)) //enable to turn + // on verbose debug msgs + )); + + wireMockServer.get().start(); + }); + + Given( + "^the mock AWS Secrets Manager server has stubs for the endpoints used to get secrets$", + () -> { + wireMockServer + .get() + .stubFor( + post(urlPathEqualTo(AWS_SECRETS_MANAGER_URL)) + .willReturn( + okJson( + String.format( + "{\"ARN\": \"arn\",\n" + + " \"CreatedDate\": 121211444,\n" + + " \"Name\": \"publicKey\",\n" + + " \"SecretBinary\": null,\n" + + " \"SecretString\": \"%s\",\n" + + " \"VersionId\": \"123\",\n" + + " \"VersionStages\": [ \"stage1\" ]\n" + + "}", + publicKey)))); + }); + + When( + "^Tessera is started with the correct AWS Secrets Manager environment variables$", + () -> { + Map params = new HashMap<>(); + params.put("awsSecretsManagerEndpoint", wireMockServer.get().baseUrl()); + + Path tempTesseraConfig = + ElUtil.createTempFileFromTemplate( + getClass().getResource("/vault/tessera-aws-config.json"), params); + tempTesseraConfig.toFile().deleteOnExit(); + + final String jarfile = System.getProperty("application.jar"); + + final URL logbackConfigFile = NodeExecManager.class.getResource("/logback-node.xml"); + Path pid = Paths.get(System.getProperty("java.io.tmpdir"), "pidA.pid"); + + final URL truststore = getClass().getResource("/certificates/truststore.jks"); + + List args = + new ArrayList<>( + Arrays.asList( + "java", + // we set the truststore so that Tessera can trust the wiremock server + "-Djavax.net.ssl.trustStore=" + truststore.getFile(), + "-Djavax.net.ssl.trustStorePassword=testtest", + "-Dspring.profiles.active=disable-unixsocket", + "-Dlogback.configurationFile=" + logbackConfigFile.getFile(), + "-Daws.region=a-region", + "-Daws.accessKeyId=an-id", + "-Daws.secretAccessKey=a-key", + "-Ddebug=true", + "-jar", + jarfile, + "-configfile", + tempTesseraConfig.toString(), + "-pidfile", + pid.toAbsolutePath().toString(), + "-jdbc.autoCreateTables", + "true")); + + startTessera(args, tempTesseraConfig); + }); + + Then( + "^Tessera will retrieve the key pair from AWS Secrets Manager$", + () -> { + wireMockServer.get().verify(2, postRequestedFor(urlEqualTo(AWS_SECRETS_MANAGER_URL))); + + final URL partyInfoUrl = + UriBuilder.fromUri("http://localhost").port(8080).path("partyinfo").build().toURL(); + + HttpURLConnection partyInfoUrlConnection = (HttpURLConnection) partyInfoUrl.openConnection(); + partyInfoUrlConnection.connect(); + + int partyInfoResponseCode = partyInfoUrlConnection.getResponseCode(); + assertThat(partyInfoResponseCode).isEqualTo(HttpURLConnection.HTTP_OK); + + JsonReader jsonReader = Json.createReader(partyInfoUrlConnection.getInputStream()); + + JsonObject partyInfoObject = jsonReader.readObject(); + + assertThat(partyInfoObject).isNotNull(); + assertThat(partyInfoObject.getJsonArray("keys")).hasSize(1); + assertThat(partyInfoObject.getJsonArray("keys").getJsonObject(0).getString("key")) + .isEqualTo(publicKey); + }); + + Given( + "^the mock AWS Secrets Manager server has stubs for the endpoints used to store secrets$", + () -> { + wireMockServer + .get() + .stubFor( + post(urlPathEqualTo(AWS_SECRETS_MANAGER_URL)) + .willReturn( + okJson( + ("{\n" + + " \"ARN\": \"string\",\n" + + " \"Name\": \"string\",\n" + + " \"VersionId\": \"string\"\n" + + "}")))); + }); + + When( + "^Tessera keygen is run with the following CLI args and AWS Secrets Manager environment variables$", + (String cliArgs) -> { + final String jarfile = System.getProperty("application.jar"); + + final URL logbackConfigFile = NodeExecManager.class.getResource("/logback-test.xml"); + + final URL truststore = getClass().getResource("/certificates/truststore.jks"); + + String formattedArgs = String.format(cliArgs, wireMockServer.get().baseUrl()); + + List args = new ArrayList<>(); + args.addAll( + Arrays.asList( + "java", + // we set the truststore so that Tessera can trust the wiremock server + "-Djavax.net.ssl.trustStore=" + truststore.getFile(), + "-Djavax.net.ssl.trustStorePassword=testtest", + "-Dspring.profiles.active=disable-unixsocket", + "-Dlogback.configurationFile=" + logbackConfigFile.getFile(), + "-Ddebug=true", + "-Daws.region=a-region", + "-Daws.accessKeyId=an-id", + "-Daws.secretAccessKey=a-key", + "-jar", + jarfile)); + args.addAll(Arrays.asList(formattedArgs.split(" "))); + + startTessera(args, null); // node is not started during keygen so do not want to verify + }); + + Then( + "^key pairs nodeA and nodeB will have been added to the AWS Secrets Manager$", + () -> { + wireMockServer.get().verify(4, postRequestedFor(urlEqualTo(AWS_SECRETS_MANAGER_URL))); + }); + } + + private void startTessera(List args, Path verifyConfig) throws Exception { + System.out.println(String.join(" ", args)); + + ProcessBuilder tesseraProcessBuilder = new ProcessBuilder(args); + + Map tesseraEnvironment = tesseraProcessBuilder.environment(); + tesseraEnvironment.put(AWS_REGION, "us-east-1"); + + try { + tesseraProcess.set(tesseraProcessBuilder.redirectErrorStream(true).start()); + } catch (NullPointerException ex) { + throw new NullPointerException("Check that application.jar property has been set"); + } + + executorService.submit( + () -> { + try (BufferedReader reader = + Stream.of(tesseraProcess.get().getInputStream()) + .map(InputStreamReader::new) + .map(BufferedReader::new) + .findAny() + .get()) { + + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + CountDownLatch startUpLatch = new CountDownLatch(1); + + if (Objects.nonNull(verifyConfig)) { + final Config config = JaxbUtil.unmarshal(Files.newInputStream(verifyConfig), Config.class); + + final URL bindingUrl = + UriBuilder.fromUri(config.getP2PServerConfig().getBindingUri()).path("upcheck").build().toURL(); + + executorService.submit( + () -> { + while (true) { + try { + HttpURLConnection conn = (HttpURLConnection) bindingUrl.openConnection(); + conn.connect(); + + System.out.println(bindingUrl + " started." + conn.getResponseCode()); + + startUpLatch.countDown(); + return; + } catch (IOException ex) { + try { + TimeUnit.MILLISECONDS.sleep(200L); + } catch (InterruptedException ex1) { + } + } + } + }); + + boolean started = startUpLatch.await(30, TimeUnit.SECONDS); + + if (!started) { + System.err.println(bindingUrl + " Not started. "); + } + } + + executorService.submit( + () -> { + try { + int exitCode = tesseraProcess.get().waitFor(); + startUpLatch.countDown(); + if (0 != exitCode) { + System.err.println("Tessera node exited with code " + exitCode); + } + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + }); + + startUpLatch.await(30, TimeUnit.SECONDS); + } +} diff --git a/tests/acceptance-test/src/test/java/com/quorum/tessera/test/vault/aws/RunAwsIT.java b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/vault/aws/RunAwsIT.java new file mode 100644 index 0000000000..ec0e3997a6 --- /dev/null +++ b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/vault/aws/RunAwsIT.java @@ -0,0 +1,11 @@ +package com.quorum.tessera.test.vault.aws; + +import cucumber.api.CucumberOptions; +import cucumber.api.junit.Cucumber; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions( + features = "classpath:features/vault/aws.feature", + plugin = {"pretty"}) +public class RunAwsIT {} diff --git a/tests/acceptance-test/src/test/resources/features/vault/aws.feature b/tests/acceptance-test/src/test/resources/features/vault/aws.feature new file mode 100644 index 0000000000..06d8b35074 --- /dev/null +++ b/tests/acceptance-test/src/test/resources/features/vault/aws.feature @@ -0,0 +1,18 @@ +Feature: Aws Key Vault support + Storing and retrieving Tessera public/private key pairs from an AWS Secrets Manager + + Background: + Given the mock AWS Secrets Manager server has been started + + Scenario: Tessera authenticates with AWS Secrets Manager and retrieves a key pair + Given the mock AWS Secrets Manager server has stubs for the endpoints used to get secrets + When Tessera is started with the correct AWS Secrets Manager environment variables + Then Tessera will retrieve the key pair from AWS Secrets Manager + + Scenario: Tessera generates and stores multiple keypairs in AWS Secrets Manager + Given the mock AWS Secrets Manager server has stubs for the endpoints used to store secrets + When Tessera keygen is run with the following CLI args and AWS Secrets Manager environment variables + """ + -keygen -keygenvaulttype AWS -filename nodeA,nodeB -keygenvaulturl %s + """ + Then key pairs nodeA and nodeB will have been added to the AWS Secrets Manager diff --git a/tests/acceptance-test/src/test/resources/vault/tessera-aws-config.json b/tests/acceptance-test/src/test/resources/vault/tessera-aws-config.json new file mode 100644 index 0000000000..a8a8302ab5 --- /dev/null +++ b/tests/acceptance-test/src/test/resources/vault/tessera-aws-config.json @@ -0,0 +1,43 @@ +{ + "useWhiteList": false, + "jdbc": { + "username": "sa", + "password": "", + "url": "jdbc:h2:./target/h2/rest1;MODE=Oracle;TRACE_LEVEL_SYSTEM_OUT=0;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9090" + }, + "serverConfigs": [ + { + "app": "Q2T", + "enabled": true, + "serverAddress": "http://localhost:18080", + "communicationType": "REST" + }, + { + "app": "P2P", + "enabled": true, + "serverAddress": "http://localhost:8080", + "communicationType": "REST" + } + ], + "peer": [ + { + "url": "http://localhost:8081/" + } + ], + "keys": { + "passwords": [], + "keyVaultConfig": { + "keyVaultType": "AWS", + "properties": { + "endpoint": "${awsSecretsManagerEndpoint}" + } + }, + "keyData": [ + { + "awsSecretsManagerPublicKeyId": "secretIdPub", + "awsSecretsManagerPrivateKeyId": "secretIdKey" + } + ] + }, + "alwaysSendTo": [] +}