diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index 992658f21..557f7e8a4 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -867,6 +867,24 @@ public final class RegistryConfig { return config.registryPolicy.greetingServerId; } + /** + * The name to use for the Cloud KMS KeyRing containing encryption keys for Nomulus secrets. + * + * @see projects.locations.keyRings + */ + @Provides + @Config("cloudKmsKeyRing") + public static String provideCloudKmsKeyRing(RegistryConfigSettings config) { + return config.kms.keyringName; + } + + @Provides + @Config("cloudKmsProjectId") + public static String provideCloudKmsProjectId(RegistryConfigSettings config) { + return config.kms.projectId; + } + @Provides @Config("customLogicFactoryClass") public static String provideCustomLogicFactoryClass(RegistryConfigSettings config) { diff --git a/java/google/registry/config/RegistryConfigSettings.java b/java/google/registry/config/RegistryConfigSettings.java index 5f9cf099d..c735cfc7b 100644 --- a/java/google/registry/config/RegistryConfigSettings.java +++ b/java/google/registry/config/RegistryConfigSettings.java @@ -31,6 +31,7 @@ public class RegistryConfigSettings { public Misc misc; public Rdap rdap; public Braintree braintree; + public Kms kms; /** Configuration options that apply to the entire App Engine project. */ public static class AppEngine { @@ -78,6 +79,12 @@ public class RegistryConfigSettings { public int baseOfyRetryMillis; } + /** Configuration for Cloud KMS. */ + public static class Kms { + public String keyringName; + public String projectId; + } + /** Configuration for caching. */ public static class Caching { public int singletonCacheRefreshSeconds; diff --git a/java/google/registry/config/files/default-config.yaml b/java/google/registry/config/files/default-config.yaml index 13581d1a9..e286f0aad 100644 --- a/java/google/registry/config/files/default-config.yaml +++ b/java/google/registry/config/files/default-config.yaml @@ -182,3 +182,11 @@ braintree: # currency). For example, one entry might be: # USD: accountIdUsingUSD merchantAccountIdsMap: {} + +kms: + # GCP project containing the KMS keyring. Should only be used for KMS in + # order to keep a simple locked down IAM configuration. + projectId: registry-kms-project-id + # The name to use for the Cloud KMS KeyRing which will store encryption keys + # for Nomulus secrets. + keyringName: nomulus diff --git a/java/google/registry/keyring/api/KeyringException.java b/java/google/registry/keyring/api/KeyringException.java new file mode 100644 index 000000000..28f9ce383 --- /dev/null +++ b/java/google/registry/keyring/api/KeyringException.java @@ -0,0 +1,29 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.api; + +/** Base class for all {@link Keyring} specific unchecked exceptions. */ +public class KeyringException extends RuntimeException { + + /** @see RuntimeException#RuntimeException(String) */ + public KeyringException(String msg) { + super(msg); + } + + /** @see RuntimeException#RuntimeException(String, Throwable) */ + public KeyringException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/java/google/registry/keyring/kms/BUILD b/java/google/registry/keyring/kms/BUILD new file mode 100644 index 000000000..e5afcdc24 --- /dev/null +++ b/java/google/registry/keyring/kms/BUILD @@ -0,0 +1,25 @@ +package( + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # Apache 2.0 + +java_library( + name = "kms", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/config", + "//java/google/registry/keyring/api", + "//java/google/registry/model", + "//java/google/registry/util", + "//third_party/java/objectify:objectify-v4_1", + "@com_google_api_client", + "@com_google_apis_google_api_services_cloudkms", + "@com_google_code_findbugs_jsr305", + "@com_google_dagger", + "@com_google_guava", + "@com_google_http_client", + "@org_bouncycastle_bcpg_jdk15on", + "@org_bouncycastle_bcpkix_jdk15on", + ], +) diff --git a/java/google/registry/keyring/kms/KmsKeyring.java b/java/google/registry/keyring/kms/KmsKeyring.java new file mode 100644 index 000000000..d638064e8 --- /dev/null +++ b/java/google/registry/keyring/kms/KmsKeyring.java @@ -0,0 +1,243 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import static com.google.common.base.Preconditions.checkState; +import static google.registry.keyring.api.PgpHelper.KeyRequirement.ENCRYPT; +import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.api.services.cloudkms.v1beta1.CloudKMS; +import com.google.api.services.cloudkms.v1beta1.model.DecryptRequest; +import com.googlecode.objectify.Key; +import google.registry.config.RegistryConfig.Config; +import google.registry.keyring.api.Keyring; +import google.registry.keyring.api.KeyringException; +import google.registry.keyring.api.PgpHelper; +import google.registry.model.server.KmsSecret; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import javax.inject.Inject; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRing; +import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; + +/** + * A {@link Keyring} implementation which stores encrypted secrets in Datastore and decrypts them + * using encryption keys stored in Cloud KMS. + * + * @see Google Cloud Key Management Service + * Documentation + */ +public class KmsKeyring implements Keyring { + + private static final String KMS_KEYRING_NAME_FORMAT = "projects/%s/locations/global/keyRings/%s"; + private static final String KMS_CRYPTO_KEY_NAME_FORMAT = + "projects/%s/locations/global/keyRings/%s/cryptoKeys/%s"; + private static final String KMS_CRYPTO_KEY_VERSION_NAME_FORMAT = + "projects/%s/locations/global/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions"; + + static final String BRAINTREE_PRIVATE_KEY_NAME = "braintree-private-key"; + static final String BRDA_RECEIVER_PUBLIC_NAME = "brda-receiver-public"; + static final String BRDA_SIGNING_PRIVATE_NAME = "brda-signing-private"; + static final String BRDA_SIGNING_PUBLIC_NAME = "brda-signing-public"; + static final String ICANN_REPORTING_PASSWORD_NAME = "icann-reporting-password"; + static final String JSON_CREDENTIAL_NAME = "json-credential"; + static final String MARKSDB_DNL_LOGIN_NAME = "marksdb-dnl-login"; + static final String MARKSDB_LORDN_PASSWORD_NAME = "marksdb-lordn-password"; + static final String MARKSDB_SMDRL_LOGIN_NAME = "marksdb-smdrl-login"; + static final String RDE_RECEIVER_PUBLIC_NAME = "rde-receiver-public"; + static final String RDE_SIGNING_PRIVATE_NAME = "rde-signing-private"; + static final String RDE_SIGNING_PUBLIC_NAME = "rde-signing-public"; + static final String RDE_SSH_CLIENT_PRIVATE_NAME = "rde-ssh-client-private"; + static final String RDE_SSH_CLIENT_PUBLIC_NAME = "rde-ssh-client-public"; + static final String RDE_STAGING_PRIVATE_NAME = "rde-staging-private"; + static final String RDE_STAGING_PUBLIC_NAME = "rde-staging-public"; + + private final CloudKMS kms; + private final String kmsKeyRingName; + private final String projectId; + + @Inject + KmsKeyring( + @Config("cloudKmsProjectId") String projectId, + @Config("cloudKmsKeyRing") String kmsKeyringName, + CloudKMS kms) { + this.projectId = projectId; + this.kmsKeyRingName = kmsKeyringName; + this.kms = kms; + } + + @Override + public PGPKeyPair getRdeSigningKey() { + return getKeyPair(RDE_SIGNING_PUBLIC_NAME, RDE_SIGNING_PRIVATE_NAME); + } + + @Override + public PGPPublicKey getRdeStagingEncryptionKey() { + return getPublicKeyForEncrypting(RDE_STAGING_PUBLIC_NAME); + } + + @Override + public PGPPrivateKey getRdeStagingDecryptionKey() { + return getPrivateKey(RDE_STAGING_PRIVATE_NAME); + } + + @Override + public PGPPublicKey getRdeReceiverKey() { + return getPublicKeyForEncrypting(RDE_RECEIVER_PUBLIC_NAME); + } + + @Override + public PGPKeyPair getBrdaSigningKey() { + return getKeyPair(BRDA_SIGNING_PUBLIC_NAME, BRDA_SIGNING_PRIVATE_NAME); + } + + @Override + public PGPPublicKey getBrdaReceiverKey() { + return getPublicKeyForEncrypting(BRDA_RECEIVER_PUBLIC_NAME); + } + + @Override + public String getRdeSshClientPublicKey() { + return new String(getDecryptedData((RDE_SSH_CLIENT_PUBLIC_NAME)), UTF_8); + } + + @Override + public String getRdeSshClientPrivateKey() { + return new String(getDecryptedData(RDE_SSH_CLIENT_PRIVATE_NAME), UTF_8); + } + + @Override + public String getIcannReportingPassword() { + return new String(getDecryptedData(ICANN_REPORTING_PASSWORD_NAME), UTF_8); + } + + @Override + public String getMarksdbDnlLogin() { + return new String(getDecryptedData(MARKSDB_DNL_LOGIN_NAME), UTF_8); + } + + @Override + public String getMarksdbLordnPassword() { + return new String(getDecryptedData(MARKSDB_LORDN_PASSWORD_NAME), UTF_8); + } + + @Override + public String getMarksdbSmdrlLogin() { + return new String(getDecryptedData(MARKSDB_SMDRL_LOGIN_NAME), UTF_8); + } + + @Override + public String getJsonCredential() { + return new String(getDecryptedData(JSON_CREDENTIAL_NAME), UTF_8); + } + + @Override + public String getBraintreePrivateKey() { + return new String(getDecryptedData(BRAINTREE_PRIVATE_KEY_NAME), UTF_8); + } + + /** No persistent resources are maintained for this Keyring implementation. */ + @Override + public void close() {} + + private PGPKeyPair getKeyPair(String publicKeyName, String privateKeyName) { + try { + PGPPublicKey publicKey = + new BcPGPPublicKeyRing(getPgpInputStream(publicKeyName)).getPublicKey(); + return new PGPKeyPair(publicKey, getPrivateKey(privateKeyName)); + } catch (IOException e) { + throw new KeyringException( + String.format( + "Could not parse public key %s and private key %s", publicKeyName, privateKeyName), + e); + } + } + + private PGPPublicKey getPublicKeyForEncrypting(String publicKeyName) { + try { + return PgpHelper.lookupPublicSubkey( + new BcPGPPublicKeyRing(getPgpInputStream(publicKeyName)), ENCRYPT) + .get(); + } catch (IOException e) { + throw new KeyringException(String.format("Could not parse public key %s", publicKeyName), e); + } + } + + private PGPPrivateKey getPrivateKey(String privateKeyName) { + try { + PGPSecretKeyRing privateKeyRing = new BcPGPSecretKeyRing(getPgpInputStream(privateKeyName)); + // There shouldn't be a passphrase on the key + return privateKeyRing + .getSecretKey() + .extractPrivateKey( + new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build(new char[0])); + } catch (IOException | PGPException e) { + throw new KeyringException( + String.format("Could not parse private key %s", privateKeyName), e); + } + } + + private InputStream getPgpInputStream(String privateKeyName) throws IOException { + return PGPUtil.getDecoderStream(new ByteArrayInputStream(getDecryptedData(privateKeyName))); + } + + private byte[] getDecryptedData(String keyName) { + KmsSecret secret = + ofy().load().key(Key.create(getCrossTldKey(), KmsSecret.class, keyName)).now(); + checkState(secret != null, "Requested secret '%s' does not exist.", keyName); + String encryptedData = ofy().load().key(secret.getLatestRevision()).now().getEncryptedValue(); + + try { + return kms.projects() + .locations() + .keyRings() + .cryptoKeys() + .decrypt( + getCryptoKeyName(projectId, kmsKeyRingName, secret.getName()), + new DecryptRequest().setCiphertext(encryptedData)) + .execute() + .decodePlaintext(); + } catch (IOException e) { + throw new KeyringException( + String.format("CloudKMS decrypt operation failed for secret %s", keyName), e); + } + } + + static String getKeyRingName(String projectId, String kmsKeyRingName) { + return String.format(KMS_KEYRING_NAME_FORMAT, projectId, kmsKeyRingName); + } + + static String getCryptoKeyName(String projectId, String kmsKeyRingName, String cryptoKeyName) { + return String.format(KMS_CRYPTO_KEY_NAME_FORMAT, projectId, kmsKeyRingName, cryptoKeyName); + } + + static String getCryptoKeyVersionName( + String projectId, String kmsKeyRingName, String cryptoKeyName) { + return String.format( + KMS_CRYPTO_KEY_VERSION_NAME_FORMAT, projectId, kmsKeyRingName, cryptoKeyName); + } +} diff --git a/java/google/registry/keyring/kms/KmsModule.java b/java/google/registry/keyring/kms/KmsModule.java new file mode 100644 index 000000000..877bcfb83 --- /dev/null +++ b/java/google/registry/keyring/kms/KmsModule.java @@ -0,0 +1,42 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import com.google.api.client.http.HttpRequestInitializer; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.services.cloudkms.v1beta1.CloudKMS; +import com.google.api.services.cloudkms.v1beta1.CloudKMSScopes; +import com.google.common.base.Function; +import dagger.Module; +import dagger.Provides; +import google.registry.config.RegistryConfig.Config; +import java.util.Set; + +/** Dagger module for Cloud KMS connection objects. */ +@Module +public final class KmsModule { + + @Provides + static CloudKMS provideKms( + HttpTransport transport, + JsonFactory jsonFactory, + Function, ? extends HttpRequestInitializer> credential, + @Config("cloudKmsProjectId") String projectId) { + return new CloudKMS.Builder(transport, jsonFactory, credential.apply(CloudKMSScopes.all())) + .setApplicationName(projectId) + .build(); + } +} diff --git a/java/google/registry/keyring/kms/KmsUpdater.java b/java/google/registry/keyring/kms/KmsUpdater.java new file mode 100644 index 000000000..8dc8d51f5 --- /dev/null +++ b/java/google/registry/keyring/kms/KmsUpdater.java @@ -0,0 +1,281 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static google.registry.keyring.kms.KmsKeyring.BRAINTREE_PRIVATE_KEY_NAME; +import static google.registry.keyring.kms.KmsKeyring.BRDA_RECEIVER_PUBLIC_NAME; +import static google.registry.keyring.kms.KmsKeyring.BRDA_SIGNING_PRIVATE_NAME; +import static google.registry.keyring.kms.KmsKeyring.BRDA_SIGNING_PUBLIC_NAME; +import static google.registry.keyring.kms.KmsKeyring.ICANN_REPORTING_PASSWORD_NAME; +import static google.registry.keyring.kms.KmsKeyring.JSON_CREDENTIAL_NAME; +import static google.registry.keyring.kms.KmsKeyring.MARKSDB_DNL_LOGIN_NAME; +import static google.registry.keyring.kms.KmsKeyring.MARKSDB_LORDN_PASSWORD_NAME; +import static google.registry.keyring.kms.KmsKeyring.MARKSDB_SMDRL_LOGIN_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_RECEIVER_PUBLIC_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_SIGNING_PRIVATE_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_SIGNING_PUBLIC_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_SSH_CLIENT_PRIVATE_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_SSH_CLIENT_PUBLIC_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_STAGING_PRIVATE_NAME; +import static google.registry.keyring.kms.KmsKeyring.RDE_STAGING_PUBLIC_NAME; +import static google.registry.keyring.kms.KmsKeyring.getCryptoKeyName; +import static google.registry.keyring.kms.KmsKeyring.getCryptoKeyVersionName; +import static google.registry.keyring.kms.KmsKeyring.getKeyRingName; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.services.cloudkms.v1beta1.CloudKMS; +import com.google.api.services.cloudkms.v1beta1.model.CryptoKey; +import com.google.api.services.cloudkms.v1beta1.model.CryptoKeyVersion; +import com.google.api.services.cloudkms.v1beta1.model.EncryptRequest; +import com.google.api.services.cloudkms.v1beta1.model.EncryptResponse; +import com.google.api.services.cloudkms.v1beta1.model.KeyRing; +import com.google.common.collect.ImmutableMap; +import com.googlecode.objectify.VoidWork; +import google.registry.config.RegistryConfig.Config; +import google.registry.model.server.KmsSecret; +import google.registry.model.server.KmsSecretRevision; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.inject.Inject; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRing; + +/** + * The {@link KmsUpdater} accumulates updates to a {@link KmsKeyring} and persists them to KMS and + * Datastore when closed. + */ +public final class KmsUpdater { + + private static final int RESOURCE_NOT_FOUND = 404; + + private final String projectId; + private final String kmsKeyRingName; + private final CloudKMS kms; + + private final HashMap secretValues; + + @Inject + public KmsUpdater( + @Config("cloudKmsProjectId") String projectId, + @Config("cloudKmsKeyRing") String kmsKeyRingName, + CloudKMS kms) { + this.projectId = projectId; + this.kmsKeyRingName = kmsKeyRingName; + this.kms = kms; + + // Use LinkedHashMap to preserve insertion order on update() to simplify testing and debugging + this.secretValues = new LinkedHashMap<>(); + } + + public KmsUpdater setRdeSigningKey(BcPGPSecretKeyRing secretKeyRing) throws IOException { + checkArgumentNotNull(secretKeyRing); + setSecret(RDE_SIGNING_PRIVATE_NAME, checkArgumentNotNull(secretKeyRing).getEncoded()); + setSecret(RDE_SIGNING_PUBLIC_NAME, secretKeyRing.getPublicKey().getEncoded()); + return this; + } + + public KmsUpdater setRdeStagingKey(BcPGPSecretKeyRing secretKeyRing) throws IOException { + checkArgumentNotNull(secretKeyRing); + + setSecret(RDE_STAGING_PRIVATE_NAME, secretKeyRing.getEncoded()); + setSecret(RDE_STAGING_PUBLIC_NAME, secretKeyRing.getPublicKey().getEncoded()); + return this; + } + + public KmsUpdater setRdeReceiverPublicKey(PGPPublicKey rdeReceiverPublicKey) throws IOException { + setSecret(RDE_RECEIVER_PUBLIC_NAME, checkArgumentNotNull(rdeReceiverPublicKey).getEncoded()); + return this; + } + + public KmsUpdater setBrdaSigningKey(BcPGPSecretKeyRing secretKeyRing) throws IOException { + checkArgumentNotNull(secretKeyRing); + setSecret(BRDA_SIGNING_PRIVATE_NAME, secretKeyRing.getEncoded()); + setSecret(BRDA_SIGNING_PUBLIC_NAME, secretKeyRing.getPublicKey().getEncoded()); + return this; + } + + public KmsUpdater setBrdaReceiverPublicKey(PGPPublicKey publicKey) throws IOException { + setSecret(BRDA_RECEIVER_PUBLIC_NAME, checkArgumentNotNull(publicKey).getEncoded()); + return this; + } + + public KmsUpdater setRdeSshClientPublicKey(String asciiPublicKey) { + setSecret(RDE_SSH_CLIENT_PUBLIC_NAME, checkArgumentNotNull(asciiPublicKey).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setRdeSshClientPrivateKey(String asciiPrivateKey) { + setSecret(RDE_SSH_CLIENT_PRIVATE_NAME, checkArgumentNotNull(asciiPrivateKey).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setIcannReportingPassword(String password) { + setSecret(ICANN_REPORTING_PASSWORD_NAME, checkArgumentNotNull(password).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setMarksdbDnlLogin(String login) { + setSecret(MARKSDB_DNL_LOGIN_NAME, checkArgumentNotNull(login).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setMarksdbLordnPassword(String password) { + setSecret(MARKSDB_LORDN_PASSWORD_NAME, checkArgumentNotNull(password).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setMarksdbSmdrlLogin(String login) { + setSecret(MARKSDB_SMDRL_LOGIN_NAME, checkArgumentNotNull(login).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setJsonCredential(String credential) { + setSecret(JSON_CREDENTIAL_NAME, checkArgumentNotNull(credential).getBytes(UTF_8)); + return this; + } + + public KmsUpdater setBraintreePrivateKey(String braintreePrivateKey) { + setSecret( + BRAINTREE_PRIVATE_KEY_NAME, checkArgumentNotNull(braintreePrivateKey).getBytes(UTF_8)); + return this; + } + + /** + * Generates new encryption keys in KMS, encrypts the updated secrets with them, and persists the + * encrypted secrets to Datastore. + * + *

The operations in this method are organized so that existing {@link KmsSecretRevision} + * entities remain primary and decryptable if a failure occurs. + */ + public void update() throws IOException { + checkState(!secretValues.isEmpty(), "At least one Keyring value must be persisted"); + + persistEncryptedValues(encryptValues(secretValues)); + } + + /** + * Encrypts updated secrets using KMS. If the configured {@code KeyRing} or {@code CryptoKey} + * associated with a secret doesn't exist, they will first be created. + * + * @see google.registry.config.RegistryConfigSettings#kms + */ + private ImmutableMap encryptValues(Map keyValues) + throws IOException { + String fullKeyRingName = getKeyRingName(projectId, kmsKeyRingName); + try { + kms.projects().locations().keyRings().get(fullKeyRingName).execute(); + } catch (GoogleJsonResponseException jsonException) { + if (jsonException.getStatusCode() == RESOURCE_NOT_FOUND) { + // Create the KeyRing in the "global" namespace. Encryption keys will be accessible from all + // GCP regions. + kms.projects() + .locations() + .keyRings() + .create("global", new KeyRing().setName(fullKeyRingName)) + .execute(); + } else { + throw jsonException; + } + } + + ImmutableMap.Builder encryptedValues = new ImmutableMap.Builder<>(); + for (Map.Entry entry : keyValues.entrySet()) { + String keyName = entry.getKey(); + String fullKeyName = getCryptoKeyName(projectId, kmsKeyRingName, keyName); + + try { + kms.projects().locations().keyRings().cryptoKeys().get(fullKeyName).execute(); + } catch (GoogleJsonResponseException jsonException) { + if (jsonException.getStatusCode() == RESOURCE_NOT_FOUND) { + kms.projects() + .locations() + .keyRings() + .cryptoKeys() + .create(fullKeyName, new CryptoKey().setName(keyName).setPurpose("ENCRYPT_DECRYPT")) + .execute(); + } else { + throw jsonException; + } + } + + CryptoKeyVersion cryptoKeyVersion = + kms.projects() + .locations() + .keyRings() + .cryptoKeys() + .cryptoKeyVersions() + .create( + getCryptoKeyVersionName(projectId, kmsKeyRingName, keyName), + new CryptoKeyVersion()) + .execute(); + + encryptedValues.put( + keyName, + kms.projects() + .locations() + .keyRings() + .cryptoKeys() + .encrypt( + cryptoKeyVersion.getName(), + new EncryptRequest().encodePlaintext(entry.getValue())) + .execute()); + } + return encryptedValues.build(); + } + + /** + * Persists encrypted secrets to Datastore as {@link KmsSecretRevision} entities and makes them + * primary. {@link KmsSecret} entities point to the latest {@link KmsSecretRevision}. + * + *

The changes are committed transactionally; if an error occurs, all existing {@link + * KmsSecretRevision} entities will remain primary. + */ + private static void persistEncryptedValues( + final ImmutableMap encryptedValues) { + ofy() + .transact( + new VoidWork() { + @Override + public void vrun() { + for (Map.Entry entry : encryptedValues.entrySet()) { + String secretName = entry.getKey(); + EncryptResponse revisionData = entry.getValue(); + + KmsSecretRevision secretRevision = + new KmsSecretRevision.Builder() + .setEncryptedValue(revisionData.getCiphertext()) + .setKmsCryptoKeyVersionName(revisionData.getName()) + .setParent(secretName) + .build(); + ofy() + .save() + .entities(secretRevision, KmsSecret.create(secretName, secretRevision)); + } + } + }); + } + + private void setSecret(String secretName, byte[] value) { + checkArgument(!secretValues.containsKey(secretName), "Attempted to set %s twice", secretName); + secretValues.put(secretName, value); + } +} diff --git a/java/google/registry/model/EntityClasses.java b/java/google/registry/model/EntityClasses.java index 5cc468ac4..1e8f2e907 100644 --- a/java/google/registry/model/EntityClasses.java +++ b/java/google/registry/model/EntityClasses.java @@ -48,6 +48,8 @@ import google.registry.model.registry.Registry; import google.registry.model.registry.label.PremiumList; import google.registry.model.registry.label.ReservedList; import google.registry.model.reporting.HistoryEntry; +import google.registry.model.server.KmsSecret; +import google.registry.model.server.KmsSecretRevision; import google.registry.model.server.Lock; import google.registry.model.server.ServerSecret; import google.registry.model.smd.SignedMarkRevocationList; @@ -90,6 +92,8 @@ public final class EntityClasses { GaeUserIdConverter.class, HistoryEntry.class, HostResource.class, + KmsSecret.class, + KmsSecretRevision.class, Lock.class, LogsExportCursor.class, LrpTokenEntity.class, diff --git a/java/google/registry/model/server/KmsSecret.java b/java/google/registry/model/server/KmsSecret.java new file mode 100644 index 000000000..7bdaf0f69 --- /dev/null +++ b/java/google/registry/model/server/KmsSecret.java @@ -0,0 +1,57 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.model.server; + +import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; +import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.Cache; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Parent; +import google.registry.model.ImmutableObject; +import google.registry.model.annotations.ReportedOn; +import google.registry.model.common.EntityGroupRoot; + +/** Pointer to the latest {@link KmsSecretRevision}. */ +@Entity +@ReportedOn +@Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION) +public class KmsSecret extends ImmutableObject { + + /** The unique name of this {@link KmsSecret}. */ + @Id String name; + + @Parent Key parent = getCrossTldKey(); + + /** The pointer to the latest {@link KmsSecretRevision}. */ + Key latestRevision; + + public String getName() { + return name; + } + + public Key getLatestRevision() { + return latestRevision; + } + + public static KmsSecret create(String name, KmsSecretRevision latestRevision) { + KmsSecret instance = new KmsSecret(); + instance.name = name; + instance.latestRevision = Key.create(latestRevision); + return instance; + } +} diff --git a/java/google/registry/model/server/KmsSecretRevision.java b/java/google/registry/model/server/KmsSecretRevision.java new file mode 100644 index 000000000..4043774e2 --- /dev/null +++ b/java/google/registry/model/server/KmsSecretRevision.java @@ -0,0 +1,117 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.model.server; + +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; +import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION; + +import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.Cache; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import com.googlecode.objectify.annotation.Parent; +import google.registry.model.Buildable; +import google.registry.model.CreateAutoTimestamp; +import google.registry.model.ImmutableObject; +import google.registry.model.annotations.ReportedOn; + +/** + * An encrypted value. + * + *

Used to store passwords and other sensitive information in Datastore. Multiple versions of a + * {@link KmsSecretRevision} may be persisted but only the latest version is primary. A key to the + * primary version is stored by {@link KmsSecret#latestRevision}. + * + *

The value can be encrypted and decrypted using Cloud KMS. + * + * @see Google Cloud Key Management Service + * Documentation + * @see google.registry.keyring.kms.KmsKeyring + */ +@Entity +@ReportedOn +@Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION) +public class KmsSecretRevision extends ImmutableObject { + + /** + * The maximum allowable secret size. Although Datastore allows entities up to 1 MB in size, + * BigQuery imports of Datastore backups limit individual columns (entity attributes) to 64 KB. + */ + private static final int MAX_SECRET_SIZE_BYTES = 64 * 1024 * 1024; + + /** The revision of this secret. */ + @Id long revisionKey; + + /** The parent {@link KmsSecret} which contains metadata about this {@link KmsSecretRevision}. */ + @Parent Key parent; + + /** + * The name of the {@code cryptoKeyVersion} associated with this {@link KmsSecretRevision}. + * + * @see projects.locations.keyRings.cryptoKeys.cryptoKeyVersions + */ + String kmsCryptoKeyVersionName; + + /** + * The base64-encoded encrypted value of this {@link KmsSecretRevision} as returned by the Cloud + * KMS API. + * + * @see projects.locations.keyRings.cryptoKeys.encrypt + */ + String encryptedValue; + + /** An automatically managed creation timestamp. */ + CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null); + + public String getKmsCryptoKeyVersionName() { + return kmsCryptoKeyVersionName; + } + + public String getEncryptedValue() { + return encryptedValue; + } + + /** A builder for constructing {@link KmsSecretRevision} entities, since they are immutable. */ + public static class Builder extends Buildable.Builder { + + public Builder setKmsCryptoKeyVersionName(String kmsCryptoKeyVersionName) { + getInstance().kmsCryptoKeyVersionName = kmsCryptoKeyVersionName; + return this; + } + + public Builder setEncryptedValue(String encryptedValue) { + checkArgument( + encryptedValue.length() <= MAX_SECRET_SIZE_BYTES, + "Secret is greater than %s bytes", + MAX_SECRET_SIZE_BYTES); + + getInstance().encryptedValue = encryptedValue; + return this; + } + + /** + * Set the parent {@link KmsSecret}. + * + *

The secret may not exist yet, so it is referred to by name rather than by reference. + */ + public Builder setParent(String secretName) { + getInstance().parent = Key.create(getCrossTldKey(), KmsSecret.class, secretName); + return this; + } + } +} diff --git a/java/google/registry/repositories.bzl b/java/google/registry/repositories.bzl index 46c334c7b..f24f2fcdc 100644 --- a/java/google/registry/repositories.bzl +++ b/java/google/registry/repositories.bzl @@ -32,6 +32,7 @@ def domain_registry_repositories( omit_com_google_api_client_servlet=False, omit_com_google_apis_google_api_services_admin_directory=False, omit_com_google_apis_google_api_services_bigquery=False, + omit_com_google_apis_google_api_services_cloudkms=False, omit_com_google_apis_google_api_services_dns=False, omit_com_google_apis_google_api_services_drive=False, omit_com_google_apis_google_api_services_groupssettings=False, @@ -129,6 +130,8 @@ def domain_registry_repositories( com_google_apis_google_api_services_admin_directory() if not omit_com_google_apis_google_api_services_bigquery: com_google_apis_google_api_services_bigquery() + if not omit_com_google_apis_google_api_services_cloudkms: + com_google_apis_google_api_services_cloudkms() if not omit_com_google_apis_google_api_services_dns: com_google_apis_google_api_services_dns() if not omit_com_google_apis_google_api_services_drive: @@ -424,6 +427,18 @@ def com_google_apis_google_api_services_bigquery(): deps = ["@com_google_api_client"], ) +def com_google_apis_google_api_services_cloudkms(): + java_import_external( + name = "com_google_apis_google_api_services_cloudkms", + licenses = ["notice"], # The Apache Software License, Version 2.0 + jar_sha256 = "82e5995e9dd248d24edfeace90261c1be0e905ecbae0b2c5ee19bb06a3e7dfdf", + jar_urls = [ + "http://domain-registry-maven.storage.googleapis.com/repo1.maven.org/maven2/com/google/apis/google-api-services-cloudkms/v1beta1-rev409-1.22.0/google-api-services-cloudkms-v1beta1-rev409-1.22.0.jar", + "http://repo1.maven.org/maven2/com/google/apis/google-api-services-cloudkms/v1beta1-rev409-1.22.0/google-api-services-cloudkms-v1beta1-rev409-1.22.0.jar", + ], + deps = ["@com_google_api_client"], + ) + def com_google_apis_google_api_services_dns(): java_import_external( name = "com_google_apis_google_api_services_dns", diff --git a/javatests/google/registry/export/backup_kinds.txt b/javatests/google/registry/export/backup_kinds.txt index f3366963d..5828e18c5 100644 --- a/javatests/google/registry/export/backup_kinds.txt +++ b/javatests/google/registry/export/backup_kinds.txt @@ -10,6 +10,8 @@ ForeignKeyDomainIndex ForeignKeyHostIndex HistoryEntry HostResource +KmsSecret +KmsSecretRevision LogsExportCursor LrpTokenEntity Modification diff --git a/javatests/google/registry/export/reporting_kinds.txt b/javatests/google/registry/export/reporting_kinds.txt index 4ad987b90..466f1093a 100644 --- a/javatests/google/registry/export/reporting_kinds.txt +++ b/javatests/google/registry/export/reporting_kinds.txt @@ -8,6 +8,8 @@ ForeignKeyDomainIndex ForeignKeyHostIndex HistoryEntry HostResource +KmsSecret +KmsSecretRevision LrpTokenEntity Modification OneTime diff --git a/javatests/google/registry/keyring/kms/BUILD b/javatests/google/registry/keyring/kms/BUILD new file mode 100644 index 000000000..89a9c4189 --- /dev/null +++ b/javatests/google/registry/keyring/kms/BUILD @@ -0,0 +1,40 @@ +package( + default_testonly = 1, + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "kms", + srcs = glob(["*.java"]), + resources = [ + "pgp-private-keyring-registry.asc", + "pgp-public-keyring.asc", + ], + deps = [ + "//java/google/registry/keyring/kms", + "//java/google/registry/model", + "//javatests/google/registry/testing", + "//third_party/java/objectify:objectify-v4_1", + "@com_google_api_client", + "@com_google_apis_google_api_services_cloudkms", + "@com_google_code_findbugs_jsr305", + "@com_google_guava", + "@com_google_http_client", + "@com_google_http_client_jackson2", + "@com_google_truth", + "@junit", + "@org_bouncycastle_bcpg_jdk15on", + "@org_bouncycastle_bcpkix_jdk15on", + "@org_mockito_all", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["*Test.java"]), + deps = [":kms"], +) diff --git a/javatests/google/registry/keyring/kms/GoogleJsonResponseExceptionHelper.java b/javatests/google/registry/keyring/kms/GoogleJsonResponseExceptionHelper.java new file mode 100644 index 000000000..045d606f3 --- /dev/null +++ b/javatests/google/registry/keyring/kms/GoogleJsonResponseExceptionHelper.java @@ -0,0 +1,171 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.json.jackson2.JacksonFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** A helper to create instances of {@link GoogleJsonResponseException}. */ +public class GoogleJsonResponseExceptionHelper { + /** + * @param statusCode the status code that should be in the returned {@link + * GoogleJsonResponseException} + * @return a {@link GoogleJsonResponseException} with the status code {@code statusCode} + * @throws IOException shouldn't occur + */ + public static GoogleJsonResponseException create(int statusCode) throws IOException { + HttpResponse response = createHttpResponse(statusCode, null); + return GoogleJsonResponseException.from(new JacksonFactory(), response); + } + + public static HttpResponse createHttpResponse(int statusCode, InputStream content) + throws IOException { + FakeHttpTransport transport = new FakeHttpTransport(statusCode, content); + HttpRequestFactory factory = transport.createRequestFactory(); + HttpRequest request = + factory.buildRequest( + "foo", new GenericUrl("http://example.com/bar"), new EmptyHttpContent()); + request.setThrowExceptionOnExecuteError(false); + return request.execute(); + } + + private static class FakeHttpTransport extends HttpTransport { + private final int statusCode; + private final InputStream content; + + FakeHttpTransport(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + protected LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + return new FakeLowLevelHttpRequest(statusCode, content); + } + } + + private static class FakeLowLevelHttpRequest extends LowLevelHttpRequest { + private final int statusCode; + private final InputStream content; + + FakeLowLevelHttpRequest(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + public void addHeader(String name, String value) throws IOException { + // Nothing! + } + + @Override + public LowLevelHttpResponse execute() throws IOException { + return new FakeLowLevelHttpResponse(statusCode, content); + } + } + + private static class FakeLowLevelHttpResponse extends LowLevelHttpResponse { + private final int statusCode; + private final InputStream content; + + FakeLowLevelHttpResponse(int statusCode, InputStream content) { + this.statusCode = statusCode; + this.content = content; + } + + @Override + public InputStream getContent() throws IOException { + return content; + } + + @Override + public String getContentEncoding() throws IOException { + return null; + } + + @Override + public long getContentLength() throws IOException { + return 0; + } + + @Override + public String getContentType() throws IOException { + return "text/json"; + } + + @Override + public String getStatusLine() throws IOException { + return null; + } + + @Override + public int getStatusCode() throws IOException { + return statusCode; + } + + @Override + public String getReasonPhrase() throws IOException { + return null; + } + + @Override + public int getHeaderCount() throws IOException { + return 0; + } + + @Override + public String getHeaderName(int index) throws IOException { + return null; + } + + @Override + public String getHeaderValue(int index) throws IOException { + return null; + } + } + + private static class EmptyHttpContent implements HttpContent { + @Override + public long getLength() throws IOException { + return 0; + } + + @Override + public String getType() { + return "text/json"; + } + + @Override + public boolean retrySupported() { + return false; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + // Nothing! + } + } +} diff --git a/javatests/google/registry/keyring/kms/KmsKeyringTest.java b/javatests/google/registry/keyring/kms/KmsKeyringTest.java new file mode 100644 index 000000000..b2d5a6239 --- /dev/null +++ b/javatests/google/registry/keyring/kms/KmsKeyringTest.java @@ -0,0 +1,344 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatastoreHelper.persistResources; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.services.cloudkms.v1beta1.CloudKMS; +import com.google.api.services.cloudkms.v1beta1.model.DecryptRequest; +import com.google.api.services.cloudkms.v1beta1.model.DecryptResponse; +import com.google.common.collect.ImmutableList; +import google.registry.model.server.KmsSecret; +import google.registry.model.server.KmsSecretRevision; +import google.registry.model.server.KmsSecretRevision.Builder; +import google.registry.testing.AppEngineRule; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class KmsKeyringTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + @Mock private CloudKMS kms; + @Mock private CloudKMS.Projects kmsProjects; + @Mock private CloudKMS.Projects.Locations kmsLocations; + @Mock private CloudKMS.Projects.Locations.KeyRings kmsKeyRings; + @Mock private CloudKMS.Projects.Locations.KeyRings.CryptoKeys kmsCryptoKeys; + @Mock private CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Decrypt kmsCryptoKeysDecrypt; + + @Captor private ArgumentCaptor cryptoKeyName; + @Captor private ArgumentCaptor decryptRequest; + + private KmsKeyring keyring; + + @Before + public void setUp() throws Exception { + keyring = new KmsKeyring("foo", "bar", kms); + + when(kms.projects()).thenReturn(kmsProjects); + when(kmsProjects.locations()).thenReturn(kmsLocations); + when(kmsLocations.keyRings()).thenReturn(kmsKeyRings); + when(kmsKeyRings.cryptoKeys()).thenReturn(kmsCryptoKeys); + when(kmsCryptoKeys.decrypt(anyString(), any(DecryptRequest.class))) + .thenReturn(kmsCryptoKeysDecrypt); + } + + @Test + public void test_getRdeSigningKey() throws Exception { + persistSecret("rde-signing-private"); + persistSecret("rde-signing-public"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn( + new DecryptResponse() + .encodePlaintext(KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded())) + .thenReturn( + new DecryptResponse().encodePlaintext(KmsTestHelper.getPrivateKeyring().getEncoded())); + + PGPKeyPair rdeSigningKey = keyring.getRdeSigningKey(); + + verify(kmsCryptoKeys, times(2)).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getAllValues()) + .isEqualTo( + ImmutableList.of( + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-signing-public", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-signing-private")); + assertThat(decryptRequest.getAllValues()) + .isEqualTo( + ImmutableList.of( + new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE), + new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE))); + assertThat(rdeSigningKey.getKeyID()) + .isEqualTo(KmsTestHelper.getPublicKeyring().getPublicKey().getKeyID()); + } + + @Test + public void test_getRdeStagingEncryptionKey() throws Exception { + persistSecret("rde-staging-public"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn( + new DecryptResponse() + .encodePlaintext(KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded())); + + PGPPublicKey rdeStagingEncryptionKey = keyring.getRdeStagingEncryptionKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-staging-public"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(rdeStagingEncryptionKey.getFingerprint()) + .isEqualTo(KmsTestHelper.getPublicKeyring().getPublicKey().getFingerprint()); + } + + @Test + public void test_getRdeStagingDecryptionKey() throws Exception { + persistSecret("rde-staging-private"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn( + new DecryptResponse().encodePlaintext(KmsTestHelper.getPrivateKeyring().getEncoded())); + + PGPPrivateKey rdeStagingDecryptionKey = keyring.getRdeStagingDecryptionKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-staging-private"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(rdeStagingDecryptionKey.getKeyID()) + .isEqualTo(KmsTestHelper.getPrivateKeyring().getSecretKey().getKeyID()); + } + + @Test + public void test_getRdeReceiverKey() throws Exception { + persistSecret("rde-receiver-public"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn( + new DecryptResponse().encodePlaintext(KmsTestHelper.getPublicKeyring().getEncoded())); + + PGPPublicKey rdeReceiverKey = keyring.getRdeReceiverKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-receiver-public"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(rdeReceiverKey.getFingerprint()) + .isEqualTo(KmsTestHelper.getPublicKeyring().getPublicKey().getFingerprint()); + } + + @Test + public void test_getBrdaSigningKey() throws Exception { + persistSecret("brda-signing-public"); + persistSecret("brda-signing-private"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn( + new DecryptResponse() + .encodePlaintext(KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded())) + .thenReturn( + new DecryptResponse().encodePlaintext(KmsTestHelper.getPrivateKeyring().getEncoded())); + + PGPKeyPair brdaSigningKey = keyring.getBrdaSigningKey(); + + verify(kmsCryptoKeys, times(2)).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getAllValues()) + .isEqualTo( + ImmutableList.of( + "projects/foo/locations/global/keyRings/bar/cryptoKeys/brda-signing-public", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/brda-signing-private")); + assertThat(decryptRequest.getAllValues()) + .isEqualTo( + ImmutableList.of( + new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE), + new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE))); + assertThat(brdaSigningKey.getKeyID()) + .isEqualTo(KmsTestHelper.getPrivateKeyring().getPublicKey().getKeyID()); + } + + @Test + public void test_getBrdaReceiverKey() throws Exception { + persistSecret("brda-receiver-public"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn( + new DecryptResponse() + .encodePlaintext(KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded())); + + PGPPublicKey brdaReceiverKey = keyring.getBrdaReceiverKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/brda-receiver-public"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(brdaReceiverKey.getFingerprint()) + .isEqualTo(KmsTestHelper.getPublicKeyring().getPublicKey().getFingerprint()); + } + + @Test + public void test_getRdeSshClientPublicKey() throws Exception { + persistSecret("rde-ssh-client-public"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String rdeSshClientPublicKey = keyring.getRdeSshClientPublicKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-ssh-client-public"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(rdeSshClientPublicKey).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + @Test + public void test_getRdeSshClientPrivateKey() throws Exception { + persistSecret("rde-ssh-client-private"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String rdeSshClientPrivateKey = keyring.getRdeSshClientPrivateKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-ssh-client-private"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(rdeSshClientPrivateKey).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + @Test + public void test_getIcannReportingPassword() throws Exception { + persistSecret("icann-reporting-password"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext("icann123".getBytes(UTF_8))); + + String icannReportingPassword = keyring.getIcannReportingPassword(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo( + "projects/foo/locations/global/keyRings/bar/cryptoKeys/icann-reporting-password"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(icannReportingPassword).isEqualTo("icann123"); + } + + @Test + public void test_getMarksdbDnlLogin() throws Exception { + persistSecret("marksdb-dnl-login"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String marksdbDnlLogin = keyring.getMarksdbDnlLogin(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/marksdb-dnl-login"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(marksdbDnlLogin).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + @Test + public void test_getMarksdbLordnPassword() throws Exception { + persistSecret("marksdb-lordn-password"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String marksdbLordnPassword = keyring.getMarksdbLordnPassword(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/marksdb-lordn-password"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(marksdbLordnPassword).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + @Test + public void test_getMarksdbSmdrlLogin() throws Exception { + persistSecret("marksdb-smdrl-login"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String marksdbSmdrlLogin = keyring.getMarksdbSmdrlLogin(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/marksdb-smdrl-login"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(marksdbSmdrlLogin).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + @Test + public void test_getJsonCredential() throws Exception { + persistSecret("json-credential"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String jsonCredential = keyring.getJsonCredential(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/json-credential"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(jsonCredential).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + @Test + public void test_getBraintreePrivateKey() throws Exception { + persistSecret("braintree-private-key"); + when(kmsCryptoKeysDecrypt.execute()) + .thenReturn(new DecryptResponse().encodePlaintext(KmsTestHelper.DUMMY_KEY.getBytes(UTF_8))); + + String braintreePrivateKey = keyring.getBraintreePrivateKey(); + + verify(kmsCryptoKeys).decrypt(cryptoKeyName.capture(), decryptRequest.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/braintree-private-key"); + assertThat(decryptRequest.getValue()) + .isEqualTo(new DecryptRequest().setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + assertThat(braintreePrivateKey).isEqualTo(KmsTestHelper.DUMMY_KEY); + } + + private static void persistSecret(String secretName) { + KmsSecretRevision secretRevision = + new Builder() + .setEncryptedValue(KmsTestHelper.DUMMY_ENCRYPTED_VALUE) + .setKmsCryptoKeyVersionName(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION) + .setParent(secretName) + .build(); + KmsSecret secret = KmsSecret.create(secretName, secretRevision); + persistResources(ImmutableList.of(secretRevision, secret)); + } +} diff --git a/javatests/google/registry/keyring/kms/KmsTestHelper.java b/javatests/google/registry/keyring/kms/KmsTestHelper.java new file mode 100644 index 000000000..5865fd84c --- /dev/null +++ b/javatests/google/registry/keyring/kms/KmsTestHelper.java @@ -0,0 +1,49 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import static com.google.common.io.Resources.getResource; + +import com.google.common.io.ByteSource; +import com.google.common.io.Resources; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRing; +import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRing; + +/** Stores dummy values for test use in {@link KmsUpdaterTest} and {@link KmsKeyringTest}. */ +final class KmsTestHelper { + + static final String DUMMY_KEY = "the quick brown fox"; + static final String DUMMY_CRYPTO_KEY_VERSION = "cheeseburger"; + static final String DUMMY_ENCRYPTED_VALUE = "meow"; + + /** The contents of a dummy PGP public key stored in a file. */ + private static final ByteSource PGP_PUBLIC_KEYRING = + Resources.asByteSource(getResource(KmsTestHelper.class, "pgp-public-keyring.asc")); + + /** The contents of a dummy PGP private key stored in a file. */ + private static final ByteSource PGP_PRIVATE_KEYRING = + Resources.asByteSource(getResource(KmsTestHelper.class, "pgp-private-keyring-registry.asc")); + + static BcPGPPublicKeyRing getPublicKeyring() throws Exception { + return new BcPGPPublicKeyRing(PGPUtil.getDecoderStream(PGP_PUBLIC_KEYRING.openStream())); + } + + static BcPGPSecretKeyRing getPrivateKeyring() throws Exception { + return new BcPGPSecretKeyRing(PGPUtil.getDecoderStream(PGP_PRIVATE_KEYRING.openStream())); + } + + private KmsTestHelper() {} +} diff --git a/javatests/google/registry/keyring/kms/KmsUpdaterTest.java b/javatests/google/registry/keyring/kms/KmsUpdaterTest.java new file mode 100644 index 000000000..9163b0d92 --- /dev/null +++ b/javatests/google/registry/keyring/kms/KmsUpdaterTest.java @@ -0,0 +1,477 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.kms; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.common.EntityGroupRoot.getCrossTldKey; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.services.cloudkms.v1beta1.CloudKMS; +import com.google.api.services.cloudkms.v1beta1.model.CryptoKey; +import com.google.api.services.cloudkms.v1beta1.model.CryptoKeyVersion; +import com.google.api.services.cloudkms.v1beta1.model.EncryptRequest; +import com.google.api.services.cloudkms.v1beta1.model.EncryptResponse; +import com.google.api.services.cloudkms.v1beta1.model.KeyRing; +import com.google.common.collect.ImmutableList; +import com.googlecode.objectify.Key; +import google.registry.model.server.KmsSecret; +import google.registry.model.server.KmsSecretRevision; +import google.registry.testing.AppEngineRule; +import java.io.ByteArrayInputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class KmsUpdaterTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + @Mock private CloudKMS kms; + @Mock private CloudKMS.Projects kmsProjects; + @Mock private CloudKMS.Projects.Locations kmsLocations; + @Mock private CloudKMS.Projects.Locations.KeyRings kmsKeyRings; + @Mock private CloudKMS.Projects.Locations.KeyRings.Get kmsKeyRingsGet; + @Mock private CloudKMS.Projects.Locations.KeyRings.Create kmsKeyRingsCreate; + @Mock private CloudKMS.Projects.Locations.KeyRings.CryptoKeys kmsCryptoKeys; + @Mock private CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Get kmsCryptoKeysGet; + @Mock private CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Create kmsCryptoKeysCreate; + + @Mock + private CloudKMS.Projects.Locations.KeyRings.CryptoKeys.CryptoKeyVersions kmsCryptoKeyVersions; + + @Mock + private CloudKMS.Projects.Locations.KeyRings.CryptoKeys.CryptoKeyVersions.Create + kmsCryptoKeyVersionsCreate; + + @Mock private CloudKMS.Projects.Locations.KeyRings.CryptoKeys.Encrypt kmsCryptoKeysEncrypt; + + @Captor private ArgumentCaptor keyRing; + @Captor private ArgumentCaptor cryptoKey; + @Captor private ArgumentCaptor cryptoKeyVersion; + @Captor private ArgumentCaptor keyRingName; + @Captor private ArgumentCaptor cryptoKeyName; + @Captor private ArgumentCaptor cryptoKeyVersionName; + @Captor private ArgumentCaptor encryptRequest; + + private KmsUpdater updater; + + @Before + public void setUp() throws Exception { + when(kms.projects()).thenReturn(kmsProjects); + when(kmsProjects.locations()).thenReturn(kmsLocations); + when(kmsLocations.keyRings()).thenReturn(kmsKeyRings); + when(kmsKeyRings.get(anyString())).thenReturn(kmsKeyRingsGet); + when(kmsKeyRings.create(anyString(), any(KeyRing.class))).thenReturn(kmsKeyRingsCreate); + when(kmsKeyRings.cryptoKeys()).thenReturn(kmsCryptoKeys); + when(kmsCryptoKeys.get(anyString())).thenReturn(kmsCryptoKeysGet); + when(kmsCryptoKeys.create(anyString(), any(CryptoKey.class))).thenReturn(kmsCryptoKeysCreate); + when(kmsCryptoKeys.cryptoKeyVersions()).thenReturn(kmsCryptoKeyVersions); + when(kmsCryptoKeyVersions.create(anyString(), any(CryptoKeyVersion.class))) + .thenReturn(kmsCryptoKeyVersionsCreate); + when(kmsCryptoKeyVersionsCreate.execute()) + .thenReturn(new CryptoKeyVersion().setName(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION)); + when(kmsCryptoKeys.encrypt(anyString(), any(EncryptRequest.class))) + .thenReturn(kmsCryptoKeysEncrypt); + when(kmsCryptoKeysEncrypt.execute()) + .thenReturn( + new EncryptResponse() + .setName(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION) + .setCiphertext(KmsTestHelper.DUMMY_ENCRYPTED_VALUE)); + + updater = new KmsUpdater("foo", "bar", kms); + } + + @Test + public void test_close_createsNewKeyRing_ifNotFound() throws Exception { + when(kmsKeyRingsGet.execute()).thenThrow(createNotFoundException()); + + updater.setBraintreePrivateKey(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verify(kmsKeyRings).create(keyRingName.capture(), keyRing.capture()); + assertThat(keyRingName.getValue()).isEqualTo("global"); + assertThat(keyRing.getValue()) + .isEqualTo(new KeyRing().setName("projects/foo/locations/global/keyRings/bar")); + verify(kmsKeyRingsCreate).execute(); + } + + @Test + public void test_close_createsNewCryptoKey_ifNotFound() throws Exception { + when(kmsCryptoKeysGet.execute()).thenThrow(createNotFoundException()); + + updater.setBraintreePrivateKey(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verify(kmsCryptoKeys).create(cryptoKeyName.capture(), cryptoKey.capture()); + assertThat(cryptoKeyName.getValue()) + .isEqualTo("projects/foo/locations/global/keyRings/bar/cryptoKeys/braintree-private-key"); + assertThat(cryptoKey.getValue()) + .isEqualTo(new CryptoKey().setName("braintree-private-key").setPurpose("ENCRYPT_DECRYPT")); + verify(kmsCryptoKeysCreate).execute(); + } + + @Test + public void test_setMultipleSecrets() throws Exception { + updater + .setBraintreePrivateKey("value1") + .setIcannReportingPassword("value2") + .setJsonCredential("value3"); + updater.update(); + + verify(kmsCryptoKeys, times(3)).get(cryptoKeyName.capture()); + assertThat(cryptoKeyName.getAllValues()) + .isEqualTo( + ImmutableList.of( + "projects/foo/locations/" + "global/keyRings/bar/cryptoKeys/braintree-private-key", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/icann-reporting-password", + "projects/foo/locations/" + "global/keyRings/bar/cryptoKeys/json-credential")); + + verify(kmsCryptoKeyVersions, times(3)) + .create(cryptoKeyVersionName.capture(), cryptoKeyVersion.capture()); + assertThat(cryptoKeyVersionName.getAllValues()) + .isEqualTo( + ImmutableList.of( + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/braintree-private-key/cryptoKeyVersions", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/icann-reporting-password/cryptoKeyVersions", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/json-credential/cryptoKeyVersions")); + + verify(kmsCryptoKeys, times(3)).encrypt(cryptoKeyName.capture(), encryptRequest.capture()); + assertThat(cryptoKeyName.getValue()).isEqualTo(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION); + assertThat(encryptRequest.getAllValues()) + .isEqualTo( + ImmutableList.of( + new EncryptRequest().encodePlaintext("value1".getBytes(UTF_8)), + new EncryptRequest().encodePlaintext("value2".getBytes(UTF_8)), + new EncryptRequest().encodePlaintext("value3".getBytes(UTF_8)))); + + KmsSecret firstSecret = loadSecret("braintree-private-key"); + assertThat(firstSecret).isNotNull(); + assertThat(firstSecret.getLatestRevision()).isNotNull(); + + KmsSecret secondSecret = loadSecret("icann-reporting-password"); + assertThat(secondSecret).isNotNull(); + assertThat(secondSecret.getLatestRevision()).isNotNull(); + + KmsSecret thirdSecret = loadSecret("icann-reporting-password"); + assertThat(thirdSecret).isNotNull(); + assertThat(thirdSecret.getLatestRevision()).isNotNull(); + } + + @Test + public void test_setBraintreePrivateKey() throws Exception { + updater.setBraintreePrivateKey(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "braintree-private-key", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/braintree-private-key", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/braintree-private-key/cryptoKeyVersions"); + } + + @Test + public void test_setBrdaReceiverKey() throws Exception { + updater.setBrdaReceiverPublicKey(KmsTestHelper.getPublicKeyring().getPublicKey()); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "brda-receiver-public", + KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded(), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/brda-receiver-public", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/brda-receiver-public/cryptoKeyVersions"); + } + + @Test + public void test_setBrdaSigningKey() throws Exception { + updater.setBrdaSigningKey(KmsTestHelper.getPrivateKeyring()); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "brda-signing-private", + KmsTestHelper.getPrivateKeyring().getEncoded(), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/brda-signing-private", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/brda-signing-private/cryptoKeyVersions", + "brda-signing-public", + KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded(), + "projects/foo/locations/global/keyRings/bar/cryptoKeys/brda-signing-public", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/brda-signing-public/cryptoKeyVersions"); + } + + @Test + public void test_setIcannReportingPassword() throws Exception { + updater.setIcannReportingPassword(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "icann-reporting-password", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/icann-reporting-password", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/icann-reporting-password/cryptoKeyVersions"); + } + + @Test + public void test_setJsonCredential() throws Exception { + updater.setJsonCredential(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "json-credential", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/json-credential", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/json-credential/cryptoKeyVersions"); + } + + @Test + public void test_setMarksdbDnlLogin() throws Exception { + updater.setMarksdbDnlLogin(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "marksdb-dnl-login", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/marksdb-dnl-login", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/marksdb-dnl-login/cryptoKeyVersions"); + } + + @Test + public void test_setMarksdbLordnPassword() throws Exception { + updater.setMarksdbLordnPassword(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "marksdb-lordn-password", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/marksdb-lordn-password", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/marksdb-lordn-password/cryptoKeyVersions"); + } + + @Test + public void test_setMarksdbSmdrlLogin() throws Exception { + updater.setMarksdbSmdrlLogin(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "marksdb-smdrl-login", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/marksdb-smdrl-login", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/marksdb-smdrl-login/cryptoKeyVersions"); + } + + @Test + public void test_setRdeReceiverKey() throws Exception { + updater.setRdeReceiverPublicKey(KmsTestHelper.getPublicKeyring().getPublicKey()); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "rde-receiver-public", + KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded(), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-receiver-public", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-receiver-public/cryptoKeyVersions"); + } + + @Test + public void test_setRdeSigningKey() throws Exception { + updater.setRdeSigningKey(KmsTestHelper.getPrivateKeyring()); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "rde-signing-private", + KmsTestHelper.getPrivateKeyring().getEncoded(), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-signing-private", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-signing-private/cryptoKeyVersions", + "rde-signing-public", + KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded(), + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-signing-public", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-signing-public/cryptoKeyVersions"); + } + + @Test + public void test_setRdeSshClientPrivateKey() throws Exception { + updater.setRdeSshClientPrivateKey(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "rde-ssh-client-private", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-ssh-client-private", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-ssh-client-private/cryptoKeyVersions"); + } + + @Test + public void test_setRdeSshClientPublicKey() throws Exception { + updater.setRdeSshClientPublicKey(KmsTestHelper.DUMMY_KEY); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "rde-ssh-client-public", + KmsTestHelper.DUMMY_KEY.getBytes(UTF_8), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-ssh-client-public", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-ssh-client-public/cryptoKeyVersions"); + } + + @Test + public void test_setRdeStagingKey() throws Exception { + updater.setRdeStagingKey(KmsTestHelper.getPrivateKeyring()); + updater.update(); + + verifyKmsApiCallsAndDatastoreWrites( + "rde-staging-private", + KmsTestHelper.getPrivateKeyring().getEncoded(), + "projects/foo/locations/global/keyRings/bar", + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-staging-private", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-staging-private/cryptoKeyVersions", + "rde-staging-public", + KmsTestHelper.getPublicKeyring().getPublicKey().getEncoded(), + "projects/foo/locations/global/keyRings/bar/cryptoKeys/rde-staging-public", + "projects/foo/locations/" + + "global/keyRings/bar/cryptoKeys/rde-staging-public/cryptoKeyVersions"); + } + + private void verifyKmsApiCallsAndDatastoreWrites( + String secretName, + byte[] goldenValue, + String goldenCryptoKeyRingName, + String goldenCryptoKeyName, + String goldenCryptoKeyVersionName) + throws Exception { + verify(kmsKeyRings).get(keyRingName.capture()); + assertThat(keyRingName.getValue()).isEqualTo(goldenCryptoKeyRingName); + + verify(kmsCryptoKeys).get(cryptoKeyName.capture()); + assertThat(cryptoKeyName.getValue()).isEqualTo(goldenCryptoKeyName); + + verify(kmsCryptoKeyVersions).create(cryptoKeyVersionName.capture(), cryptoKeyVersion.capture()); + assertThat(cryptoKeyVersionName.getValue()).isEqualTo(goldenCryptoKeyVersionName); + + verify(kmsCryptoKeys).encrypt(cryptoKeyName.capture(), encryptRequest.capture()); + assertThat(cryptoKeyName.getValue()).isEqualTo(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION); + assertThat(encryptRequest.getValue()) + .isEqualTo(new EncryptRequest().encodePlaintext(goldenValue)); + + KmsSecret secret = loadSecret(secretName); + KmsSecretRevision secretRevision = ofy().load().key(secret.getLatestRevision()).now(); + assertThat(secretRevision.getKmsCryptoKeyVersionName()) + .isEqualTo(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION); + assertThat(secretRevision.getEncryptedValue()).isEqualTo(KmsTestHelper.DUMMY_ENCRYPTED_VALUE); + } + + /** Variant of {@code verifyKmsApiCallsAndDatastoreWrites} for key pairs. */ + private void verifyKmsApiCallsAndDatastoreWrites( + String firstSecretName, + byte[] firstGoldenValue, + String goldenCryptoKeyRingName, + String firstGoldenCryptoKeyName, + String firstGoldenCryptoKeyVersionName, + String secondSecretName, + byte[] secondGoldenValue, + String secondGoldenCryptoKeyName, + String secondGoldenCryptoKeyVersionName) + throws Exception { + verify(kmsKeyRings, times(1)).get(keyRingName.capture()); + assertThat(keyRingName.getValue()).isEqualTo(goldenCryptoKeyRingName); + + verify(kmsCryptoKeys, times(2)).get(cryptoKeyName.capture()); + assertThat(cryptoKeyName.getAllValues()) + .isEqualTo(ImmutableList.of(firstGoldenCryptoKeyName, secondGoldenCryptoKeyName)); + + verify(kmsCryptoKeyVersions, times(2)) + .create(cryptoKeyVersionName.capture(), cryptoKeyVersion.capture()); + assertThat(cryptoKeyVersionName.getAllValues()) + .isEqualTo( + ImmutableList.of(firstGoldenCryptoKeyVersionName, secondGoldenCryptoKeyVersionName)); + + verify(kmsCryptoKeys, times(2)).encrypt(cryptoKeyName.capture(), encryptRequest.capture()); + assertThat(cryptoKeyName.getValue()).isEqualTo(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION); + assertThat(encryptRequest.getAllValues()) + .isEqualTo( + ImmutableList.of( + new EncryptRequest().encodePlaintext(firstGoldenValue), + new EncryptRequest().encodePlaintext(secondGoldenValue))); + + KmsSecret secret = loadSecret(firstSecretName); + KmsSecretRevision secretRevision = ofy().load().key(secret.getLatestRevision()).now(); + assertThat(secretRevision.getKmsCryptoKeyVersionName()) + .isEqualTo(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION); + assertThat(secretRevision.getEncryptedValue()).isEqualTo(KmsTestHelper.DUMMY_ENCRYPTED_VALUE); + + KmsSecret secondSecret = loadSecret(secondSecretName); + KmsSecretRevision secondSecretRevision = + ofy().load().key(secondSecret.getLatestRevision()).now(); + assertThat(secondSecretRevision.getKmsCryptoKeyVersionName()) + .isEqualTo(KmsTestHelper.DUMMY_CRYPTO_KEY_VERSION); + assertThat(secondSecretRevision.getEncryptedValue()) + .isEqualTo(KmsTestHelper.DUMMY_ENCRYPTED_VALUE); + } + + private static GoogleJsonResponseException createNotFoundException() throws Exception { + ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes(UTF_8)); + HttpResponse response = GoogleJsonResponseExceptionHelper.createHttpResponse(404, inputStream); + HttpResponseException.Builder httpResponseExceptionBuilder = + new HttpResponseException.Builder(response); + httpResponseExceptionBuilder.setStatusCode(404); + httpResponseExceptionBuilder.setStatusMessage("NOT_FOUND"); + return new GoogleJsonResponseException(httpResponseExceptionBuilder, null); + } + + private static KmsSecret loadSecret(String secret) { + return ofy().load().key(Key.create(getCrossTldKey(), KmsSecret.class, secret)).now(); + } +} diff --git a/javatests/google/registry/keyring/kms/pgp-private-keyring-registry.asc b/javatests/google/registry/keyring/kms/pgp-private-keyring-registry.asc new file mode 100644 index 000000000..318ce4bc2 --- /dev/null +++ b/javatests/google/registry/keyring/kms/pgp-private-keyring-registry.asc @@ -0,0 +1,98 @@ +gpg -a --export-secret-keys --export-options export-minimal \ + rde-unittest@registry.test \ + rde-unittest-dsa@registry.test \ + >javatests/google/registry/rde/testdata/pgp-private-keyring-registry.asc + +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQOYBFUj+XwBCACwqQR0GNEJMJFq3zcWDeFDRljUpANnFooyrDafXNXdZLEwAXly +I5TtEwKui7dcl954APEi+dILQMiOpiguXnJoOFMeV/gqulwdUvcsruPRoud4ckLv +RAUokse3uHqyEtpeM+MPKP1c1OwexBiZ8lXG5gRvvcEwOLspO6pe+DYnxtD6Z8Bc +AbTgQjJlol3U36bGGwO3RjEARchZP8uGUggteeRtRSaGuOxRMvM9sbyNDD1wvQGO +6Pfaobl5C/ZduZj9eI10TjCFQyJE/m3twGlsgd8eF4+7s2NKyDoqR6syS4SmXxPR +uk/sz8nTHMqHCtSA9CElLn7jaCL8PLuY4DfjABEBAAEAB/sEcexDw5AW7Brslns7 +WdQOLknz58CSKnYFumGRdvOGo471x0O4BH8ty+moXKmbcdLOMC/hEmofkFy7giDG +seetCaXFwK7k3FWPA1Rm0mu3DaNHW5Cllo+OqsrNq52it2i5MBMJgZ2GZXEfgQ2h +agCQhZ8c9KhUv0iuS+sIgrRSIdrOLSfROrJhKknwLw5GWlAtaTU9SJt1At1UrtNX +FBg+XTmgDFfChZodUyVcowU59ZX4ffZvcUTx7j/jw3wkN5ycoWR2erv08Evo2yly +U213dnBIoo6JhjFMbN7XAeh4MDv+Y0r5bcgXwl+5u+FeHjG8EJaV4NcZt3BCgBVu +5bKZBADI8esNMohb6SEYM1rKw4qc5i9oZGbpQb3lKw5rJpot5Te2Eb5TavwfHrph +iQukBXoNVggo1ggSoFz0Lodc1elDIK9rwANLpaZGNLnY2Nlt3rRFxjczH9wY1dma +42W5914sjeFGWvZ24GKN50rbbd+1pirVjH+XmyXFrVxooWo8hQQA4Q/KKxtsDp/H +CzguCwJWWn1vTy1TmXZyaRKI7nBDDtbbQUFLjPVV7h7enTpBhJVqxDJQafgb+9zH +02qYfGP9idUF0XZRqDkmbkf9btDE3F3Y0Ojmi3r4inovKcZg2/JRcwMF5ZH4jOTr +/pUZVMjRuNFr5EkhohyNMlCH2CuaY0cEAM3MgjPFgbalSL3bITfeL4B+lesla1MR +zZ/unQtylAr38isqzsaaqmVYuAyhbhZ4vqBzzvKH2jZrQACul2alRChah+2UrM8c +k+v4mO71sjrm491EewJrFgI75189UOllZ9tCrOTan4+Z0KpqIWxSyDtMj2BqqoGH +X/PAm4Wn9nvkRAK0K0pvbmF0aGFuIFN3aWZ0IDxyZGUtdW5pdHRlc3RAcmVnaXN0 +cnkudGVzdD6JATgEEwECACIFAlUj+XwCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B +AheAAAoJEKWcEy81iaHVfToH/3hV3T6B0WQx+/c8XhVrHx3zlvG4+8sF3u4y3jPD +ER+T1NfgnlG1vkkhxj+nD1qjBR2oLwTT5V99mH1wsmbChwv4O3bZHuauNtVoyQ13 +RWZEeY6Rf46faqZeaAIsJvIZYJU/bLOwCw1vFKSLI5a4UkP2/XcB9YlYzP6NyKsS +qCuLcslU3fvqkQbTxbA3i1naXxRF+AA+3yZki/yOmG4s+3BFPms0QRQGy2ytyFjM +/I767deLqamgBIWYLDwUEhBhcyHI0L1kWnk7kZr+TpQuYwajxr/F6vlQTf60vZJ3 +3KX1fLR5RpLhxThTkPWfP7XgJjr36SEKUUSWU+XuutL/+t6dA5gEVSP5fAEIAJ55 +MsSUsvnMLAqGRRjO/auZFRitJhkqwtiFicbmRc9O+RT1UPL6iuiRp44F6CvWuAtg +5lF9aL9YL09zMfUS9PYTD0fqO5CF9WgbIAXIUugxQ4hUeczmi4JirZ7SzsOfYIqU +OsQbV+LQk8dGkWOmyDL8AaWCx5ZI6Fm2C8maxZA/Jw/SVuJbyGatKjBrOihl65KW +PDJ77RbphKRyccj1rIgjaqej1E344/8cs8DD9N/btk/SEDcw/IFc4Bp4ndOqslAb +M7xzsU8/aHOeA5dV0YsylSx8Fnw8a7zl1zUX+CeO8QihN5rKdmPNZfNF5mRB4fEA +MdwYGtZSJlrQzqFrB90AEQEAAQAH/A/RrV2uMkd5OBQ83vzg/LV6bH6wGhN9D0Ip +4lz2c5WEHp0W9+OpWuBoWb91m310y55nJ9aOnDwtg1t89kmugMFnmajmGtgMZzjj +LEknfA8ti746IIZxpQ945jfqdzVT3YE31r3otNcxVu8Xfj/iUC22tjtdWKsJ0BfU +ckzHpF0HqI5qhusaWPRiZ7af5CFpl5ZAeDXvJmqlWPJnBVFuD5RIb4NO3FmyU5YS +rusFFfjtdsO5PCOItNz4gzsrvKX3CFfE8fUYueqeCSaCDEZRp9OJBm7qmJza4iNv +VaX+I6cKsYWH8fbEjpF0PJRmqgNa1Ym+6dbaZEzxRQZaT0pk58kEAMABbtz5kXgh +T8Xb8Ay0Oh51Rdbey0W84uW3A3nQL+JMiyv82IA8PBmzIykBvYOgZC7iI1wTO37y +ON01r/BE4i0Tu0eexibVwULGi8pdKnDr4B6JWxOJT3ObWtaukXVn0nPscKYNTL86 +AddnNN0DfCVhGblaU+Cn5LfaX4cIpP9zBADTSq/3OUYfzBTWeegXQRNsEucc/uCd +k6MV5dfaStEtTQwBKXz//hEbrNq+4Z4UCRiHddJlsLjSGKySwSLYORjikqYiqfpH +HYj05YfSsAl7z7viuLB8q5Gs1D0+dVDV+aNEDBaa1pGe0qgGG93CAD2F6VZ7aWri +KTNchBGTzENnbwP7BNcDmufDgeq+aVk97pKgRYzI1vKMK6lFJtZtFEJGI3VaeWYv +P0mh6xhxike7/AFdXAVqcp8Sz4GT0OsvEwxk9ehfRd1cGYnjwxEms1DbfGPUexxQ +yLA6ErXhmqwOGpG1xf0NqcFVIloycjLLWDFEftnVCqw++E6sLyCkpl7YfqxBCIkB +HwQYAQIACQUCVSP5fAIbDAAKCRClnBMvNYmh1ZrQCACfKciHmPhZv67RznnsDeEB +G2a2y6aLkFKyGq0NgwF8iNdRVDcALd7Kwzp4E4hZpKWQJ1U1bJYu/O6hCajxY824 +UmRpN5vMlXue/5AidykBkxhr6NhlhjBN/weQcvW5utugJ/U0ZmqEmJGYsIXuXLGs +bMKjDM07F3hXVy53FqpH5JNTA62Y8psZo7dC4vWs8PNKAi9uoLIj40rp1B1UKLqE +5tCTS7FBvzMxbLyYTzQjnCuCOqv8nSyy3TTbfzSmuhqarw36rBZm5a2Ne2HZsSMY +K3R+t+o/fEGdn3/O2ZY00CVRGk58No9COM3i7PSmTLOGnxxndN8BO+lm44bYhDhC +lQNTBFUj/0gRCACN0oNK9EaKDBHu6ab7w5FtBsnmPyxMwVf+ROREB9j8V5ZnulfM +Hj5u9WFkzG5g09rQn4fgYmPdB5YzYHYaM/WatiQk+tdjrg6tfHQBD5ZUsLOqcxgX +HU+h7+wyrcPM/9+gFQtZKFznrELAi5JLqLhQy2PQBOsPt4VbTBbBnX8L81Hw56xd +FrPkos1iLeNZ4hSCoDpoPEaXIS8X+8La+fDH52KorC/LrMm/aYX/+48gdGLqfe5h +1q+vAHOxQXlPdvbdiGXj0COcAU+vd1iKC1tGYZHxb2e+I+rC4QFxJJ3PY6juJMVE +kHxazkNY+efXplQRmTjglkXndzvqkGHsV6c7AQDlwWNdwSvQGc5RL16gTA9A3T+p +VtFV62mVHdoh24/H4Qf7BJq2nL0jVgC2yQq6U/1Mbq8Stq/WrJEdn/SGAHoQG712 +ipR251t8hb7UWdtrT9LCSHNXGJsVURhL+RivQ1LzU99SRM7rRYBG45GlvnMiN3Y7 +v+RH61T0d1HBr9YXq/H64QxmUge/9svSKtQQHzzaLNvyvnX1iVWfp+ttex9Dsme2 +7BkP5zhc0vQ2NgLP96kvQoaXs2LxMwR1JPiCFo8Y5rUPrR2v2ieETMxAXYJGB8HO +1EQGqEp6Fd3KwK75bx6Q/BRVq9AyyDPWdsnxnzM+L8kcJvzRPS457RmicOcHhkE2 +FhWraW9PhV7jB5SzF2Q/msFGeQ6wrfht3lF675EbBggAh/28GoI5ryVZRbhwj2fV +7h8MmwWj2G8T/gt3lu0SEvm0/kpChJxF6hmyQLq/6tZITEVjqUCmFuta1plbANDt +Cu+8Jmw3NVRk9iTbrYZc9fytfjROi98si5eimvMEiwKv53MreIqyX7THHw1FXlFa +RDzez3zspmuOO+534APA3mVyuDtT4/3K35mBDwVqcykbQRBM8/nZhS6vrXNwkPp4 +PhkN/Q9Ethfe6y1w9uHa9UshT0/tAguzas8d8ve+UmpmXqd1G60/mbWDMiLnJfFJ +H7fCz7EAZ37r99Ss1Z+l/wF3XUaIJ4UiV/3i1T3COCZaF5rjqdv1oEy+555/r5Mv +0gABAJJg1dR1x4xjstvehkDP6Gm0G27gGXdcwEj2VCvlGgO8EVy0L0pvbmF0aGFu +IFN3aWZ0IDxyZGUtdW5pdHRlc3QtZHNhQHJlZ2lzdHJ5LnRlc3Q+iHoEExEIACIF +AlUj/0gCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJ6mQpZ89XyKn+gB +AK91twI+kyFHwBcg+8E5fsai63ZfJsusASeNvtWhMhavAQC+kNNqO1KrDdczgcLu +X3bQCJG3gOzo6iq/3zC723HPHp0CPQRVI/9IEAgA5QrYsEpUuMBdfyod0D1VPetE +FBZ+T3sT3FXW/cpijeSl2OKYJIColhnLo3Etld8zNmJ6td27onEhjOFvT2rsBMs5 +DLX4b7avXiRqTFUXi+qPLyI8iGyPaWjlmGNhED07egCVesM2t4gN+46IE0PztIDR +Uy1SdHp8bwnAEdlud5T4VZgGmFXI9s+4nrjdnm532XBLa8w03xSzDYq5B4T/5F/J +qDSz//p6aqrUonMYHr2PCp0xaHM+Q34m+jR1cqvQQJxBvcEBiNNZJuh23w1wNB/v +M2cI/LJTrCKIHVLRfy7NJVQPhqkgnJeVT4Bnzsy7LLKCzKuG7He2Ecf9L5TspwAE +DQf8DMnCuEbWss+H9KK0h/Xwi3Ftbp/tnOaynUyp7dOULGnlZ+5nioTiwvW0jRfN +AP86OIDP8zztahFXH9PuOCtQ2rR7VbUUaohafsvX3Cn6+5ITjA7kzINa+Uc1eKsi +Bn1ze6rT5E7svLUxT+9N/0d0OCcjCf3aLfJggOHYws8PwDdhbYKPeX082t64BAsM +6UViv0ZiNUL4zj0Q5PUO1tWDvVZ3x9k/gHbi1vS/2+U6sMYhP/gh7/L//du0ty/v +5mOLkBKIyKWltfLRDcvfu12pKMcZBhj3iIPoTduC22PNjIV+YXdW6lOx32pz9Yvj +tYNmeeWb/n2w6D8iZUsw1s94uwABUgKn70aB8/FKhyqC+RkhdycbGYdnl+JJX/Aj +F4QI+pjW0h5Yf1hM66q699YVzIhhBBgRCAAJBQJVI/9IAhsMAAoJEJ6mQpZ89XyK +itEA/1F7PMm1HyJxb6DXxRaBbsIL3sVm3OjK5XGuoyjSltsmAQCdIMfGn9ArSsEo +Euhilp9DguZPtN4dIl56gnM8abR0rQ== +=+pjN +-----END PGP PRIVATE KEY BLOCK----- diff --git a/javatests/google/registry/keyring/kms/pgp-public-keyring.asc b/javatests/google/registry/keyring/kms/pgp-public-keyring.asc new file mode 100644 index 000000000..4ac258a5d --- /dev/null +++ b/javatests/google/registry/keyring/kms/pgp-public-keyring.asc @@ -0,0 +1,95 @@ +gpg -a --export --export-options export-minimal \ + rde-unittest@registry.test \ + rde-unittest-dsa@registry.test \ + rde-unittest@escrow.test \ + >javatests/google/registry/rde/testdata/pgp-public-keyring.asc + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +zInfxrmYRlOExWEu5eg9EAI= +=60ZZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt index 242a9bd5f..858692955 100644 --- a/javatests/google/registry/model/schema.txt +++ b/javatests/google/registry/model/schema.txt @@ -813,6 +813,18 @@ enum google.registry.model.reporting.HistoryEntry$Type { RDE_IMPORT; SYNTHETIC; } +class google.registry.model.server.KmsSecret { + @Id java.lang.String name; + @Parent com.googlecode.objectify.Key parent; + com.googlecode.objectify.Key latestRevision; +} +class google.registry.model.server.KmsSecretRevision { + @Id long revisionKey; + @Parent com.googlecode.objectify.Key parent; + google.registry.model.CreateAutoTimestamp creationTime; + java.lang.String encryptedValue; + java.lang.String kmsCryptoKeyVersionName; +} class google.registry.model.server.Lock { @Id java.lang.String lockId; java.util.LinkedHashSet queue; diff --git a/javatests/google/registry/model/server/KmsSecretRevisionTest.java b/javatests/google/registry/model/server/KmsSecretRevisionTest.java new file mode 100644 index 000000000..6547e9133 --- /dev/null +++ b/javatests/google/registry/model/server/KmsSecretRevisionTest.java @@ -0,0 +1,66 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.model.server; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.persistResource; + +import com.google.common.base.Strings; +import google.registry.testing.AppEngineRule; +import google.registry.testing.ExceptionRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class KmsSecretRevisionTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + @Rule public final ExceptionRule thrown = new ExceptionRule(); + + private KmsSecretRevision secretRevision; + + @Before + public void setUp() { + secretRevision = + persistResource( + new KmsSecretRevision.Builder() + .setKmsCryptoKeyVersionName("foo") + .setParent("bar") + .setEncryptedValue("blah") + .build()); + } + + @Test + public void test_setEncryptedValue_tooLong_throwsException() { + thrown.expect(IllegalArgumentException.class, "Secret is greater than 67108864 bytes"); + secretRevision = + persistResource( + new KmsSecretRevision.Builder() + .setKmsCryptoKeyVersionName("foo") + .setParent("bar") + .setEncryptedValue(Strings.repeat("a", 64 * 1024 * 1024 + 1)) + .build()); + } + + @Test + public void testPersistence() { + assertThat(ofy().load().entity(secretRevision).now()).isEqualTo(secretRevision); + } +} diff --git a/javatests/google/registry/model/server/KmsSecretTest.java b/javatests/google/registry/model/server/KmsSecretTest.java new file mode 100644 index 000000000..834763cc0 --- /dev/null +++ b/javatests/google/registry/model/server/KmsSecretTest.java @@ -0,0 +1,53 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.model.server; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.DatastoreHelper.persistResource; + +import google.registry.testing.AppEngineRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class KmsSecretTest { + + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + + private KmsSecret secret; + private KmsSecretRevision secretRevision; + + @Before + public void setUp() { + secretRevision = + persistResource( + new KmsSecretRevision.Builder() + .setKmsCryptoKeyVersionName("foo") + .setParent("bar") + .setEncryptedValue("blah") + .build()); + + secret = persistResource(KmsSecret.create("someSecret", secretRevision)); + } + + @Test + public void testPersistence() { + assertThat(ofy().load().entity(secret).now()).isEqualTo(secret); + } +}