diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java
index 2dfbcbe9a4e..29c4ac58b82 100644
--- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java
+++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java
@@ -161,7 +161,7 @@ public class DomainFlowUtils {
+ "name collision.";
/** Strict validator for ascii lowercase letters, digits, and "-", allowing "." as a separator */
- private static final CharMatcher ALLOWED_CHARS =
+ public static final CharMatcher ALLOWED_CHARS =
CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('0', '9').or(CharMatcher.anyOf("-.")));
/** Default validator used to determine if an IDN name can be provisioned on a TLD. */
diff --git a/core/src/main/java/google/registry/model/registrar/Registrar.java b/core/src/main/java/google/registry/model/registrar/Registrar.java
index 8f5618eb5e0..958b279d4f1 100644
--- a/core/src/main/java/google/registry/model/registrar/Registrar.java
+++ b/core/src/main/java/google/registry/model/registrar/Registrar.java
@@ -25,6 +25,7 @@
import static com.google.common.collect.Streams.stream;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
+import static google.registry.flows.domain.DomainFlowUtils.ALLOWED_CHARS;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.tld.Tlds.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
@@ -32,6 +33,7 @@
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableSortedCopy;
import static google.registry.util.DateTimeUtils.START_INSTANT;
+import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@@ -50,6 +52,7 @@
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
+import com.google.common.net.InternetDomainName;
import com.google.gson.annotations.Expose;
import com.google.re2j.Pattern;
import google.registry.model.Buildable;
@@ -185,6 +188,12 @@ public enum State {
/** Regex for E.164 phone number format. */
private static final Pattern E164_PATTERN = Pattern.compile("\\+[0-9]{1,3}\\.[0-9]{1,14}");
+ /** Maximum number of hostnames in the EPP client certificate allowlist. */
+ public static final int MAX_EPP_CLIENT_CERTIFICATE_HOSTNAMES = 20;
+
+ /** Maximum number of revoked client certificate hashes. */
+ public static final int MAX_REVOKED_CLIENT_CERTIFICATE_HASHES = 20;
+
/** Regex for telephone support passcode (5 digit string). */
public static final Pattern PHONE_PASSCODE_PATTERN = Pattern.compile("\\d{5}");
@@ -295,6 +304,23 @@ public enum State {
/** Base64 encoded SHA256 hash of {@link #failoverClientCertificate}. */
String failoverClientCertificateHash;
+ /**
+ * Hostnames for which a client certificate issued by a publicly trusted CA is accepted at EPP
+ * login, as an alternative to the pinned {@link #clientCertificate}.
+ *
+ *
Entries are stored in canonical A-label (punycode) lowercase form without a trailing dot.
+ * They are only consulted while the {@code ALLOW_EPP_CLIENT_CERT_HOSTNAME_AUTH} feature flag is
+ * active.
+ */
+ @Expose Set allowedEppClientCertificateHostnames;
+
+ /**
+ * Base64 encoded SHA256 hashes of client certificates the registrar has revoked, in the same form
+ * as {@link #clientCertificateHash}. A certificate with one of these hashes is rejected at EPP
+ * login regardless of which mechanism would otherwise have accepted it.
+ */
+ @Expose Set revokedClientCertificateHashes;
+
/** An allow list of netmasks (in CIDR notation) which the client is allowed to connect from. */
@org.hibernate.annotations.Type(CidrBlockListUserType.class)
@Column(columnDefinition = "text[]")
@@ -540,6 +566,14 @@ public ImmutableList getIpAddressAllowList() {
return nullToEmptyImmutableCopy(ipAddressAllowList);
}
+ public ImmutableSet getAllowedEppClientCertificateHostnames() {
+ return nullToEmptyImmutableCopy(allowedEppClientCertificateHostnames);
+ }
+
+ public ImmutableSet getRevokedClientCertificateHashes() {
+ return nullToEmptyImmutableCopy(revokedClientCertificateHashes);
+ }
+
public RegistrarAddress getLocalizedAddress() {
return localizedAddress;
}
@@ -662,6 +696,9 @@ public Map toJsonMap() {
.put("clientCertificateHash", clientCertificateHash)
.put("failoverClientCertificate", failoverClientCertificate)
.put("failoverClientCertificateHash", failoverClientCertificateHash)
+ .putListOfStrings(
+ "allowedEppClientCertificateHostnames", allowedEppClientCertificateHostnames)
+ .putListOfStrings("revokedClientCertificateHashes", revokedClientCertificateHashes)
.put("localizedAddress", localizedAddress)
.put("internationalizedAddress", internationalizedAddress)
.put("phoneNumber", phoneNumber)
@@ -853,6 +890,51 @@ public Builder setLastPocVerificationDate(Instant now) {
return this;
}
+ /**
+ * Sets the hostnames for which CA-issued client certificates are accepted at EPP login.
+ *
+ * Each entry is canonicalized to lowercase A-label (punycode) form and validated as a
+ * multi-label DNS name; duplicates collapse to one entry.
+ */
+ public Builder setAllowedEppClientCertificateHostnames(Iterable hostnames) {
+ checkArgumentNotNull(hostnames, "EPP client certificate hostnames cannot be null");
+ ImmutableSet validated =
+ stream(hostnames)
+ .map(
+ hostname -> {
+ InternetDomainName domainName =
+ InternetDomainName.from(canonicalizeHostname(hostname));
+ // InternetDomainName allows underscores and single-label names; require
+ // strict ASCII LDH characters and at least two labels.
+ checkArgument(
+ ALLOWED_CHARS.matchesAllOf(domainName.toString()) && domainName.hasParent(),
+ "Not a valid fully qualified hostname: %s",
+ hostname);
+ return domainName.toString();
+ })
+ .collect(toImmutableSet());
+ checkArgument(
+ validated.size() <= MAX_EPP_CLIENT_CERTIFICATE_HOSTNAMES,
+ "At most %s EPP client certificate hostnames may be configured, got %s",
+ MAX_EPP_CLIENT_CERTIFICATE_HOSTNAMES,
+ validated.size());
+ getInstance().allowedEppClientCertificateHostnames = validated;
+ return this;
+ }
+
+ /** Sets the hashes of client certificates that are rejected at EPP login. */
+ public Builder setRevokedClientCertificateHashes(Iterable hashes) {
+ checkArgumentNotNull(hashes, "Revoked client certificate hashes cannot be null");
+ ImmutableSet validated = ImmutableSet.copyOf(hashes);
+ checkArgument(
+ validated.size() <= MAX_REVOKED_CLIENT_CERTIFICATE_HASHES,
+ "At most %s revoked client certificates may be configured, got %s",
+ MAX_REVOKED_CLIENT_CERTIFICATE_HASHES,
+ validated.size());
+ getInstance().revokedClientCertificateHashes = validated;
+ return this;
+ }
+
private static String calculateHash(String clientCertificate) {
if (clientCertificate == null) {
return null;
diff --git a/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java b/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java
index 392664269ba..63cbaaa15dd 100644
--- a/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java
+++ b/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java
@@ -96,6 +96,20 @@ private void setResponse(Registrar savedRegistrar) {
}
try {
+ if (!savedRegistrar
+ .getAllowedEppClientCertificateHostnames()
+ .equals(registrarParameter.getAllowedEppClientCertificateHostnames())) {
+ updatedRegistrarBuilder.setAllowedEppClientCertificateHostnames(
+ registrarParameter.getAllowedEppClientCertificateHostnames());
+ updates.add("EPP_CERT_HOSTNAMES_CHANGE");
+ }
+ if (!savedRegistrar
+ .getRevokedClientCertificateHashes()
+ .equals(registrarParameter.getRevokedClientCertificateHashes())) {
+ updatedRegistrarBuilder.setRevokedClientCertificateHashes(
+ registrarParameter.getRevokedClientCertificateHashes());
+ updates.add("REVOKED_CERTS_CHANGE");
+ }
if (!savedRegistrar
.getClientCertificate()
.equals(registrarParameter.getClientCertificate())) {
@@ -116,7 +130,7 @@ private void setResponse(Registrar savedRegistrar) {
updates.add("FAILOVER_SSL_CERT_CHANGE");
}
}
- } catch (InsecureCertificateException e) {
+ } catch (InsecureCertificateException | IllegalArgumentException e) {
setFailedResponse(e.getMessage(), SC_BAD_REQUEST);
return;
}
diff --git a/core/src/test/java/google/registry/model/registrar/RegistrarTest.java b/core/src/test/java/google/registry/model/registrar/RegistrarTest.java
index b92ef9cdcc3..b5698c729b8 100644
--- a/core/src/test/java/google/registry/model/registrar/RegistrarTest.java
+++ b/core/src/test/java/google/registry/model/registrar/RegistrarTest.java
@@ -802,4 +802,190 @@ void testSuccess_toJsonMap_includesExpiryAccessPeriodEnabled() {
Registrar modified = registrar.asBuilder().setExpiryAccessPeriodEnabled(true).build();
assertThat(modified.toJsonMap()).containsEntry("expiryAccessPeriodEnabled", true);
}
+
+ @Test
+ void testSuccess_eppClientCertificateHostnames_defaultEmpty() {
+ assertThat(registrar.getAllowedEppClientCertificateHostnames()).isEmpty();
+ assertThat(registrar.toJsonMap())
+ .containsEntry("allowedEppClientCertificateHostnames", ImmutableList.of());
+ }
+
+ @Test
+ void testSuccess_eppClientCertificateHostnames_persistence() {
+ persistResource(
+ registrar
+ .asBuilder()
+ .setAllowedEppClientCertificateHostnames(
+ ImmutableList.of("epp.registrar.example", "epp2.registrar.example"))
+ .build());
+ Registrar loaded = Registrar.loadByRegistrarId("registrar").get();
+ assertThat(loaded.getAllowedEppClientCertificateHostnames())
+ .containsExactly("epp.registrar.example", "epp2.registrar.example");
+ assertThat((Iterable>) loaded.toJsonMap().get("allowedEppClientCertificateHostnames"))
+ .containsExactly("epp.registrar.example", "epp2.registrar.example");
+ }
+
+ @Test
+ void testSuccess_eppClientCertificateHostnames_canonicalizedAndDeduplicated() {
+ Registrar modified =
+ registrar
+ .asBuilder()
+ .setAllowedEppClientCertificateHostnames(
+ ImmutableList.of(
+ "EPP.Example.COM",
+ "epp.example.com",
+ "Epp.Example.Com",
+ "b.example",
+ "epp.みんな"))
+ .build();
+ assertThat(modified.getAllowedEppClientCertificateHostnames())
+ .containsExactly("epp.example.com", "b.example", "epp.xn--q9jyb4c");
+ }
+
+ @Test
+ void testSuccess_eppClientCertificateHostnames_clearing() {
+ registrar =
+ registrar
+ .asBuilder()
+ .setAllowedEppClientCertificateHostnames(ImmutableList.of("epp.registrar.example"))
+ .build();
+ registrar =
+ registrar.asBuilder().setAllowedEppClientCertificateHostnames(ImmutableList.of()).build();
+ assertThat(registrar.getAllowedEppClientCertificateHostnames()).isEmpty();
+ assertThat(registrar.allowedEppClientCertificateHostnames).isNull();
+ }
+
+ @Test
+ void testSuccess_eppClientCertificateHostnames_acceptsIdnALabelAndLongNames() {
+ String longLabel = "a".repeat(63);
+ Registrar modified =
+ registrar
+ .asBuilder()
+ .setAllowedEppClientCertificateHostnames(
+ ImmutableList.of(
+ "xn--q9jyb4c.registrar.example",
+ "epp.xn--q9jyb4c",
+ longLabel + ".example",
+ "a-b.c-d.example"))
+ .build();
+ assertThat(modified.getAllowedEppClientCertificateHostnames()).hasSize(4);
+ }
+
+ @Test
+ void testFailure_eppClientCertificateHostnames_invalidNames() {
+ ImmutableList invalid =
+ ImmutableList.of(
+ "",
+ "epp",
+ "epp.",
+ "*.registrar.example",
+ "https://epp.registrar.example",
+ "epp.registrar.example:700",
+ "epp.registrar.example/path",
+ "192.0.2.1",
+ "2001:db8::1",
+ "epp_1.registrar.example",
+ "-epp.registrar.example",
+ "epp-.registrar.example",
+ "epp.registrar.ex ample",
+ "a".repeat(64) + ".example",
+ ("a".repeat(63) + ".").repeat(4) + "example");
+ for (String hostname : invalid) {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ registrar
+ .asBuilder()
+ .setAllowedEppClientCertificateHostnames(ImmutableList.of(hostname)));
+ }
+ }
+
+ @Test
+ void testFailure_eppClientCertificateHostnames_tooMany() {
+ ImmutableList.Builder hostnames = new ImmutableList.Builder<>();
+ for (int i = 0; i <= Registrar.MAX_EPP_CLIENT_CERTIFICATE_HOSTNAMES; i++) {
+ hostnames.add(String.format("epp%d.registrar.example", i));
+ }
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> registrar.asBuilder().setAllowedEppClientCertificateHostnames(hostnames.build()));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("At most 20 EPP client certificate hostnames may be configured, got 21");
+ }
+
+ @Test
+ void testFailure_eppClientCertificateHostnames_null() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> registrar.asBuilder().setAllowedEppClientCertificateHostnames(null));
+ }
+
+ @Test
+ void testSuccess_revokedClientCertificateHashes_defaultEmpty() {
+ assertThat(registrar.getRevokedClientCertificateHashes()).isEmpty();
+ assertThat(registrar.toJsonMap())
+ .containsEntry("revokedClientCertificateHashes", ImmutableList.of());
+ }
+
+ @Test
+ void testSuccess_revokedClientCertificateHashes_persistence() {
+ persistResource(
+ registrar
+ .asBuilder()
+ .setRevokedClientCertificateHashes(
+ ImmutableList.of(SAMPLE_CERT_HASH, SAMPLE_CERT2_HASH))
+ .build());
+ Registrar loaded = Registrar.loadByRegistrarId("registrar").get();
+ assertThat(loaded.getRevokedClientCertificateHashes())
+ .containsExactly(SAMPLE_CERT_HASH, SAMPLE_CERT2_HASH);
+ assertThat((Iterable>) loaded.toJsonMap().get("revokedClientCertificateHashes"))
+ .containsExactly(SAMPLE_CERT_HASH, SAMPLE_CERT2_HASH);
+ }
+
+ @Test
+ void testSuccess_revokedClientCertificateHashes_deduplicated() {
+ Registrar modified =
+ registrar
+ .asBuilder()
+ .setRevokedClientCertificateHashes(ImmutableList.of(SAMPLE_CERT_HASH, SAMPLE_CERT_HASH))
+ .build();
+ assertThat(modified.getRevokedClientCertificateHashes()).containsExactly(SAMPLE_CERT_HASH);
+ }
+
+ @Test
+ void testSuccess_revokedClientCertificateHashes_clearing() {
+ registrar =
+ registrar
+ .asBuilder()
+ .setRevokedClientCertificateHashes(ImmutableList.of(SAMPLE_CERT_HASH))
+ .build();
+ registrar = registrar.asBuilder().setRevokedClientCertificateHashes(ImmutableList.of()).build();
+ assertThat(registrar.getRevokedClientCertificateHashes()).isEmpty();
+ assertThat(registrar.revokedClientCertificateHashes).isNull();
+ }
+
+ @Test
+ void testFailure_revokedClientCertificateHashes_tooMany() {
+ ImmutableList.Builder hashes = new ImmutableList.Builder<>();
+ for (int i = 0; i <= Registrar.MAX_REVOKED_CLIENT_CERTIFICATE_HASHES; i++) {
+ // Vary the first character to produce distinct, well-formed hashes.
+ hashes.add((char) ('A' + i) + SAMPLE_CERT_HASH.substring(1));
+ }
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> registrar.asBuilder().setRevokedClientCertificateHashes(hashes.build()));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("At most 20 revoked client certificates may be configured, got 21");
+ }
+
+ @Test
+ void testFailure_revokedClientCertificateHashes_null() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> registrar.asBuilder().setRevokedClientCertificateHashes(null));
+ }
}
diff --git a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java
index f8436e12cf0..12ffaa385b3 100644
--- a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java
+++ b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java
@@ -16,14 +16,19 @@
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT2;
+import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
+import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.loadSingleton;
+import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.Mockito.when;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
@@ -163,6 +168,144 @@ void testFailure_validityPeriodTooLong_returnsSpecificError() throws IOException
assertThat(response.getPayload()).isEqualTo(expectedError);
}
+ @Test
+ void testSuccess_setAllowedEppClientCertificateHostnames() throws IOException {
+ SecurityAction action =
+ createAction(
+ testRegistrar.getRegistrarId(),
+ securityJson(
+ ImmutableMap.of(
+ "allowedEppClientCertificateHostnames",
+ ImmutableList.of("EPP.Example.COM", "epp2.example.com", "epp.example.com"))),
+ lenientChecker());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(SC_OK);
+ Registrar r = loadRegistrar(testRegistrar.getRegistrarId());
+ assertThat(r.getAllowedEppClientCertificateHostnames())
+ .containsExactly("epp.example.com", "epp2.example.com");
+ ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
+ assertThat(history.getDescription()).hasValue("registrarId|EPP_CERT_HOSTNAMES_CHANGE");
+ }
+
+ @Test
+ void testSuccess_unchangedAllowedEppClientCertificateHostnames() throws IOException {
+ persistResource(
+ testRegistrar
+ .asBuilder()
+ .setAllowedEppClientCertificateHostnames(ImmutableList.of("epp.example.com"))
+ .build());
+ SecurityAction action =
+ createAction(
+ testRegistrar.getRegistrarId(),
+ securityJson(
+ ImmutableMap.of(
+ "allowedEppClientCertificateHostnames",
+ ImmutableList.of("epp.example.com"),
+ "ipAddressAllowList",
+ ImmutableList.of("192.168.1.1/32"))),
+ lenientChecker());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(SC_OK);
+ ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
+ assertThat(history.getDescription()).hasValue("registrarId|IP_CHANGE");
+ }
+
+ @Test
+ void testFailure_invalidAllowedEppClientCertificateHostname() throws IOException {
+ SecurityAction action =
+ createAction(
+ testRegistrar.getRegistrarId(),
+ securityJson(
+ ImmutableMap.of(
+ "allowedEppClientCertificateHostnames",
+ ImmutableList.of("epp.example.com", "*.example.com"))),
+ lenientChecker());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
+ assertThat(response.getPayload()).contains("*.example.com");
+ assertThat(
+ loadRegistrar(testRegistrar.getRegistrarId()).getAllowedEppClientCertificateHostnames())
+ .isEmpty();
+ }
+
+ @Test
+ void testSuccess_setRevokedClientCertificateHashes() throws IOException {
+ SecurityAction action =
+ createAction(
+ testRegistrar.getRegistrarId(),
+ securityJson(
+ ImmutableMap.of(
+ "revokedClientCertificateHashes",
+ ImmutableList.of(SAMPLE_CERT3_HASH, SAMPLE_CERT_HASH))),
+ lenientChecker());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(SC_OK);
+ Registrar r = loadRegistrar(testRegistrar.getRegistrarId());
+ assertThat(r.getRevokedClientCertificateHashes())
+ .containsExactly(SAMPLE_CERT3_HASH, SAMPLE_CERT_HASH);
+ ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
+ assertThat(history.getDescription()).hasValue("registrarId|REVOKED_CERTS_CHANGE");
+ }
+
+ @Test
+ void testSuccess_unchangedRevokedClientCertificateHashes() throws IOException {
+ persistResource(
+ testRegistrar
+ .asBuilder()
+ .setRevokedClientCertificateHashes(ImmutableList.of(SAMPLE_CERT_HASH))
+ .build());
+ SecurityAction action =
+ createAction(
+ testRegistrar.getRegistrarId(),
+ securityJson(
+ ImmutableMap.of(
+ "revokedClientCertificateHashes",
+ ImmutableList.of(SAMPLE_CERT_HASH),
+ "ipAddressAllowList",
+ ImmutableList.of("192.168.1.1/32"))),
+ lenientChecker());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(SC_OK);
+ ConsoleUpdateHistory history = loadSingleton(ConsoleUpdateHistory.class).get();
+ assertThat(history.getDescription()).hasValue("registrarId|IP_CHANGE");
+ }
+
+ @Test
+ void testFailure_tooManyRevokedClientCertificateHashes() throws IOException {
+ ImmutableList.Builder hashes = new ImmutableList.Builder<>();
+ for (int i = 0; i <= Registrar.MAX_REVOKED_CLIENT_CERTIFICATE_HASHES; i++) {
+ hashes.add((char) ('A' + i) + SAMPLE_CERT_HASH.substring(1));
+ }
+ SecurityAction action =
+ createAction(
+ testRegistrar.getRegistrarId(),
+ securityJson(ImmutableMap.of("revokedClientCertificateHashes", hashes.build())),
+ lenientChecker());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
+ assertThat(response.getPayload())
+ .contains("At most 20 revoked client certificates may be configured, got 21");
+ assertThat(loadRegistrar(testRegistrar.getRegistrarId()).getRevokedClientCertificateHashes())
+ .isEmpty();
+ }
+
+ /** Builds the JSON body the console sends: the registrar ID plus the given security fields. */
+ private static String securityJson(ImmutableMap fields) {
+ return GSON.toJson(
+ ImmutableMap.builder().put("registrarId", "registrarId").putAll(fields).build());
+ }
+
+ private CertificateChecker lenientChecker() {
+ clock.setTo(Instant.parse("2020-11-01T00:00:00Z"));
+ return new CertificateChecker(
+ ImmutableSortedMap.of(START_INSTANT, 20825, Instant.parse("2020-09-01T00:00:00Z"), 398),
+ 30,
+ 15,
+ 2048,
+ ImmutableSet.of("secp256r1", "secp384r1"),
+ clock);
+ }
+
private SecurityAction createAction(
String registrarId, String jsonBody, CertificateChecker certificateChecker)
throws IOException {
diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated
index d10fbb5f3fd..64b0146c5e3 100644
--- a/db/src/main/resources/sql/schema/db-schema.sql.generated
+++ b/db/src/main/resources/sql/schema/db-schema.sql.generated
@@ -504,6 +504,7 @@
create table "Registrar" (
registrar_id text not null,
last_update_time timestamp(6) with time zone not null,
+ allowed_epp_client_certificate_hostnames text[],
allowed_tlds text[],
billing_account_map hstore,
block_premium_names boolean not null,
@@ -545,6 +546,7 @@
rdap_base_urls text[],
registrar_name text not null,
registry_lock_allowed boolean not null,
+ revoked_client_certificate_hashes text[],
password_salt text,
state text check ((state in ('PENDING','ACTIVE','SUSPENDED','DISABLED'))),
type text not null check ((type in ('REAL','OTE','PDT','EXTERNAL_MONITORING','INTERNAL','MONITORING','TEST'))),