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
+
+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
+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);
+ }
+}