Add certificate checks for create and update registrar commands (#837)

* Add certificatechecks for create and update registrar commands

* Add CertificateCheckerModule

* Remove commented out code

* Still tring to get dependency injection to work

* Get this actually working

* Add tests for multiple violations

* Small formatting fixes

* Rename configs and fix collectors

* Add checks for failover client certificate

* Fix formatting
This commit is contained in:
sarahcaseybot 2020-10-22 11:43:22 -04:00 committed by GitHub
parent 0b73e9032c
commit 93d922af6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 463 additions and 19 deletions

View file

@ -0,0 +1,44 @@
// Copyright 2020 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.config;
import com.google.common.collect.ImmutableSortedMap;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.CertificateChecker;
import google.registry.util.Clock;
import javax.inject.Singleton;
import org.joda.time.DateTime;
/** Dagger module that provides the {@link CertificateChecker} used in the application. */
// TODO(sarahbot@): Move this module to a better location. Possibly flows/. If we decide to move
// CertificateChecker.java to core/ delete this file and inject the CertificateChecker constructor
// instead.
@Module
public abstract class CertificateCheckerModule {
@Provides
@Singleton
static CertificateChecker provideCertificateChecker(
@Config("maxValidityDaysSchedule") ImmutableSortedMap<DateTime, Integer> validityDaysMap,
@Config("expirationWarningDays") int daysToExpiration,
@Config("minimumRsaKeyLength") int minimumRsaKeyLength,
Clock clock) {
return new CertificateChecker(validityDaysMap, daysToExpiration, minimumRsaKeyLength, clock);
}
private CertificateCheckerModule() {}
}

View file

@ -16,9 +16,12 @@ package google.registry.config;
import static com.google.common.base.Suppliers.memoize; import static com.google.common.base.Suppliers.memoize;
import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static google.registry.config.ConfigUtils.makeUrl; import static google.registry.config.ConfigUtils.makeUrl;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8; import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Comparator.naturalOrder;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii; import com.google.common.base.Ascii;
@ -27,6 +30,7 @@ import com.google.common.base.Strings;
import com.google.common.base.Supplier; import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import google.registry.util.TaskQueueUtils; import google.registry.util.TaskQueueUtils;
@ -46,6 +50,7 @@ import javax.inject.Qualifier;
import javax.inject.Singleton; import javax.inject.Singleton;
import javax.mail.internet.AddressException; import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants; import org.joda.time.DateTimeConstants;
import org.joda.time.Duration; import org.joda.time.Duration;
@ -1345,6 +1350,33 @@ public final class RegistryConfig {
public static String provideRdapTosStaticUrl(RegistryConfigSettings config) { public static String provideRdapTosStaticUrl(RegistryConfigSettings config) {
return config.registryPolicy.rdapTosStaticUrl; return config.registryPolicy.rdapTosStaticUrl;
} }
@Provides
@Config("maxValidityDaysSchedule")
public static ImmutableSortedMap<DateTime, Integer> provideValidityDaysMap(
RegistryConfigSettings config) {
return config.sslCertificateValidation.maxValidityDaysSchedule.entrySet().stream()
.collect(
toImmutableSortedMap(
naturalOrder(),
e ->
e.getKey().equals("START_OF_TIME")
? START_OF_TIME
: DateTime.parse(e.getKey()),
e -> e.getValue()));
}
@Provides
@Config("expirationWarningDays")
public static int provideDaysToExpiration(RegistryConfigSettings config) {
return config.sslCertificateValidation.expirationWarningDays;
}
@Provides
@Config("minimumRsaKeyLength")
public static int provideMinimumRsaKeyLength(RegistryConfigSettings config) {
return config.sslCertificateValidation.minimumRsaKeyLength;
}
} }
/** Returns the App Engine project ID, which is based off the environment name. */ /** Returns the App Engine project ID, which is based off the environment name. */

View file

@ -15,6 +15,7 @@
package google.registry.config; package google.registry.config;
import java.util.List; import java.util.List;
import java.util.Map;
/** The POJO that YAML config files are deserialized into. */ /** The POJO that YAML config files are deserialized into. */
public class RegistryConfigSettings { public class RegistryConfigSettings {
@ -38,6 +39,7 @@ public class RegistryConfigSettings {
public Beam beam; public Beam beam;
public Keyring keyring; public Keyring keyring;
public RegistryTool registryTool; public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
/** Configuration options that apply to the entire App Engine project. */ /** Configuration options that apply to the entire App Engine project. */
public static class AppEngine { public static class AppEngine {
@ -218,4 +220,11 @@ public class RegistryConfigSettings {
public String clientSecret; public String clientSecret;
public String username; public String username;
} }
/** Configuration for the certificate checker. */
public static class SslCertificateValidation {
public Map<String, Integer> maxValidityDaysSchedule;
public int expirationWarningDays;
public int minimumRsaKeyLength;
}
} }

View file

@ -446,3 +446,17 @@ registryTool:
# OAuth client secret used by the tool. # OAuth client secret used by the tool.
clientSecret: YOUR_CLIENT_SECRET clientSecret: YOUR_CLIENT_SECRET
username: toolusername username: toolusername
# Configuration options for checking SSL certificates.
sslCertificateValidation:
# A map specifying the maximum amount of days the certificate can be valid.
# The entry key is the date closest before the date the certificate was issued
# and the entry value is the applicable maximum validity days for that certificate.
maxValidityDaysSchedule:
"START_OF_TIME": 825
"2020-09-01T00:00:00Z": 398
# The number of days before a certificate expires that indicates the
# certificate is nearing expiration and warnings should be sent.
expirationWarningDays: 30
# The minimum number of bits an RSA key must contain
minimumRsaKeyLength: 2048

View file

@ -22,6 +22,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import static google.registry.util.RegistrarUtils.normalizeRegistrarName; import static google.registry.util.RegistrarUtils.normalizeRegistrarName;
import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameter;
@ -38,9 +39,15 @@ import google.registry.tools.params.OptionalLongParameter;
import google.registry.tools.params.OptionalPhoneNumberParameter; import google.registry.tools.params.OptionalPhoneNumberParameter;
import google.registry.tools.params.OptionalStringParameter; import google.registry.tools.params.OptionalStringParameter;
import google.registry.tools.params.PathParameter; import google.registry.tools.params.PathParameter;
import google.registry.util.CertificateChecker;
import google.registry.util.CertificateChecker.CertificateViolation;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import java.io.ByteArrayInputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -48,7 +55,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -57,9 +66,9 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
static final FluentLogger logger = FluentLogger.forEnclosingClass(); static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Parameter( @Inject CertificateChecker certificateChecker;
description = "Client identifier of the registrar account",
required = true) @Parameter(description = "Client identifier of the registrar account", required = true)
List<String> mainParameters; List<String> mainParameters;
@Parameter( @Parameter(
@ -356,11 +365,21 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
} }
if (clientCertificateFilename != null) { if (clientCertificateFilename != null) {
String asciiCert = new String(Files.readAllBytes(clientCertificateFilename), US_ASCII); String asciiCert = new String(Files.readAllBytes(clientCertificateFilename), US_ASCII);
// An empty certificate file is allowed in order to provide a functionality for removing an
// existing certificate without providing a replacement. An uploaded empty certificate file
// will prevent the registrar from being able to establish EPP connections.
if (!asciiCert.equals("")) {
verifyCertificate(asciiCert);
}
builder.setClientCertificate(asciiCert, now); builder.setClientCertificate(asciiCert, now);
} }
if (failoverClientCertificateFilename != null) { if (failoverClientCertificateFilename != null) {
String asciiCert = String asciiCert =
new String(Files.readAllBytes(failoverClientCertificateFilename), US_ASCII); new String(Files.readAllBytes(failoverClientCertificateFilename), US_ASCII);
if (!asciiCert.equals("")) {
verifyCertificate(asciiCert);
}
builder.setFailoverClientCertificate(asciiCert, now); builder.setFailoverClientCertificate(asciiCert, now);
} }
if (!isNullOrEmpty(clientCertificateHash)) { if (!isNullOrEmpty(clientCertificateHash)) {
@ -463,6 +482,22 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand {
} }
} }
private void verifyCertificate(String certificateString) throws CertificateException {
X509Certificate certificate =
(X509Certificate)
CertificateFactory.getInstance("X509")
.generateCertificate(new ByteArrayInputStream(certificateString.getBytes(UTF_8)));
ImmutableSet<CertificateViolation> violations =
certificateChecker.checkCertificate(certificate);
if (!violations.isEmpty()) {
String displayMessages =
violations.stream()
.map(violation -> violation.getDisplayMessage(certificateChecker))
.collect(Collectors.joining("\n"));
throw new CertificateException(displayMessages);
}
}
@Override @Override
protected String execute() throws Exception { protected String execute() throws Exception {
// Save registrar to Datastore and output its response // Save registrar to Datastore and output its response

View file

@ -20,6 +20,7 @@ import dagger.Lazy;
import google.registry.batch.BatchModule; import google.registry.batch.BatchModule;
import google.registry.beam.initsql.BeamJpaModule; import google.registry.beam.initsql.BeamJpaModule;
import google.registry.bigquery.BigqueryModule; import google.registry.bigquery.BigqueryModule;
import google.registry.config.CertificateCheckerModule;
import google.registry.config.CredentialModule.LocalCredentialJson; import google.registry.config.CredentialModule.LocalCredentialJson;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule; import google.registry.config.RegistryConfig.ConfigModule;
@ -60,6 +61,7 @@ import javax.inject.Singleton;
BatchModule.class, BatchModule.class,
BeamJpaModule.class, BeamJpaModule.class,
BigqueryModule.class, BigqueryModule.class,
CertificateCheckerModule.class,
ConfigModule.class, ConfigModule.class,
CloudDnsWriterModule.class, CloudDnsWriterModule.class,
DatastoreAdminModule.class, DatastoreAdminModule.class,
@ -83,42 +85,83 @@ import javax.inject.Singleton;
}) })
interface RegistryToolComponent { interface RegistryToolComponent {
void inject(AckPollMessagesCommand command); void inject(AckPollMessagesCommand command);
void inject(CheckDomainClaimsCommand command); void inject(CheckDomainClaimsCommand command);
void inject(CheckDomainCommand command); void inject(CheckDomainCommand command);
void inject(CountDomainsCommand command); void inject(CountDomainsCommand command);
void inject(CreateAnchorTenantCommand command); void inject(CreateAnchorTenantCommand command);
void inject(CreateCdnsTld command); void inject(CreateCdnsTld command);
void inject(CreateContactCommand command); void inject(CreateContactCommand command);
void inject(CreateDomainCommand command); void inject(CreateDomainCommand command);
void inject(CreateRegistrarCommand command);
void inject(CreateTldCommand command); void inject(CreateTldCommand command);
void inject(DeployInvoicingPipelineCommand command); void inject(DeployInvoicingPipelineCommand command);
void inject(DeploySpec11PipelineCommand command); void inject(DeploySpec11PipelineCommand command);
void inject(EncryptEscrowDepositCommand command); void inject(EncryptEscrowDepositCommand command);
void inject(GenerateAllocationTokensCommand command); void inject(GenerateAllocationTokensCommand command);
void inject(GenerateDnsReportCommand command); void inject(GenerateDnsReportCommand command);
void inject(GenerateEscrowDepositCommand command); void inject(GenerateEscrowDepositCommand command);
void inject(GetKeyringSecretCommand command); void inject(GetKeyringSecretCommand command);
void inject(GetOperationStatusCommand command); void inject(GetOperationStatusCommand command);
void inject(GhostrydeCommand command); void inject(GhostrydeCommand command);
void inject(ImportDatastoreCommand command); void inject(ImportDatastoreCommand command);
void inject(ListCursorsCommand command); void inject(ListCursorsCommand command);
void inject(ListDatastoreOperationsCommand command); void inject(ListDatastoreOperationsCommand command);
void inject(LoadSnapshotCommand command); void inject(LoadSnapshotCommand command);
void inject(LockDomainCommand command); void inject(LockDomainCommand command);
void inject(LoginCommand command); void inject(LoginCommand command);
void inject(LogoutCommand command); void inject(LogoutCommand command);
void inject(PendingEscrowCommand command); void inject(PendingEscrowCommand command);
void inject(RenewDomainCommand command); void inject(RenewDomainCommand command);
void inject(SendEscrowReportToIcannCommand command); void inject(SendEscrowReportToIcannCommand command);
void inject(SetNumInstancesCommand command); void inject(SetNumInstancesCommand command);
void inject(SetupOteCommand command); void inject(SetupOteCommand command);
void inject(UnlockDomainCommand command); void inject(UnlockDomainCommand command);
void inject(UnrenewDomainCommand command); void inject(UnrenewDomainCommand command);
void inject(UpdateCursorsCommand command); void inject(UpdateCursorsCommand command);
void inject(UpdateDomainCommand command); void inject(UpdateDomainCommand command);
void inject(UpdateKmsKeyringCommand command); void inject(UpdateKmsKeyringCommand command);
void inject(UpdateRegistrarCommand command);
void inject(UpdateTldCommand command); void inject(UpdateTldCommand command);
void inject(ValidateEscrowDepositCommand command); void inject(ValidateEscrowDepositCommand command);
void inject(WhoisQueryCommand command); void inject(WhoisQueryCommand command);
AppEngineConnection appEngineConnection(); AppEngineConnection appEngineConnection();

View file

@ -88,5 +88,40 @@ public final class CertificateSamples {
*/ */
public static final String SAMPLE_CERT2_HASH = "GNd6ZP8/n91t9UTnpxR8aH7aAW4+CpvufYx9ViGbcMY"; public static final String SAMPLE_CERT2_HASH = "GNd6ZP8/n91t9UTnpxR8aH7aAW4+CpvufYx9ViGbcMY";
/*
* openssl req -new -nodes -x509 -days 200 -newkey rsa:2048 -keyout client1.key -out client1.crt
* -subj "/C=US/ST=New York/L=New York/O=Google/OU=domain-registry-test/CN=client1"
*/
public static final String SAMPLE_CERT3 =
"-----BEGIN CERTIFICATE-----\n"
+ "MIIDyzCCArOgAwIBAgIUJnhiVrxAxgwkLJzHPm1w/lBoNs4wDQYJKoZIhvcNAQEL\n"
+ "BQAwdTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhO\n"
+ "ZXcgWW9yazEPMA0GA1UECgwGR29vZ2xlMR0wGwYDVQQLDBRkb21haW4tcmVnaXN0\n"
+ "cnktdGVzdDEQMA4GA1UEAwwHY2xpZW50MTAeFw0yMDEwMTIxNzU5NDFaFw0yMTA0\n"
+ "MzAxNzU5NDFaMHUxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8G\n"
+ "A1UEBwwITmV3IFlvcmsxDzANBgNVBAoMBkdvb2dsZTEdMBsGA1UECwwUZG9tYWlu\n"
+ "LXJlZ2lzdHJ5LXRlc3QxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEB\n"
+ "AQUAA4IBDwAwggEKAoIBAQC0msirO7kXyGEC93stsNYGc02Z77Q2qfHFwaGYkUG8\n"
+ "QvOF5SWN+jwTo5Td6Jj26A26a8MLCtK45TCBuMRNcUsHhajhT19ocphO20iY3zhi\n"
+ "ycwV1id0iwME4kPd1m57BELRE9tUPOxF81/JQXdR1fwT5KRVHYRDWZhaZ5aBmlZY\n"
+ "3t/H9Ly0RBYyApkMaGs3nlb94OOug6SouUfRt02S59ja3wsE2SVF/Eui647OXP7O\n"
+ "QdYXofxuqLoNkE8EnAdl43/enGLiCIVd0G2lABibFF+gbxTtfgbg7YtfUZJdL+Mb\n"
+ "RAcAtuLXEamNQ9H63JgVF16PlQVCDz2XyI3uCfPpDDiBAgMBAAGjUzBRMB0GA1Ud\n"
+ "DgQWBBQ26bWk8qfEBjXs/xZ4m8JZyalnITAfBgNVHSMEGDAWgBQ26bWk8qfEBjXs\n"
+ "/xZ4m8JZyalnITAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAZ\n"
+ "VcsgslBKanKOieJ5ik2d9qzOMXKfBuWPRFWbkC3t9i5awhHqnGAaj6nICnnMZIyt\n"
+ "rdx5lZW5aaQyf0EP/90JAA8Xmty4A6MXmEjQAMiCOpP3A7eeS6Xglgi8IOZl4/bg\n"
+ "LonW62TUkilo5IiFt/QklFTeHIjXB+OvA8+2Quqyd+zp7v6KnhXjvaomim78DhwE\n"
+ "0PIUnjmiRpGpHfTVioTdfhPHZ2Y93Y8K7juL93sQog9aBu5m9XRJCY6wGyWPE83i\n"
+ "kmLfGzjcnaJ6kqCd9xQRFZ0JwHmGlkAQvFoeengbNUqSyjyVgsOoNkEsrWwe/JFO\n"
+ "iqBvjEhJlvRoefvkdR98\n"
+ "-----END CERTIFICATE-----\n";
/*
* python -c "import sys;print sys.argv[1].decode('hex').encode('base64').strip('\n=')" $(openssl
* x509 -fingerprint -sha256 -in client1.crt | grep -Po '(?<=Fingerprint=).*' | sed s/://g)
*/
public static final String SAMPLE_CERT3_HASH = "GM2tYFuzdpDXN0lqpUXlsvrqk8OdMayryV+4/DOFZ0M";
private CertificateSamples() {} private CertificateSamples() {}
} }

View file

@ -175,7 +175,12 @@ public abstract class CommandTestCase<C extends Command> {
/** Returns a path to a known good certificate file. */ /** Returns a path to a known good certificate file. */
String getCertFilename() throws IOException { String getCertFilename() throws IOException {
return writeToNamedTmpFile("cert.pem", CertificateSamples.SAMPLE_CERT); return getCertFilename(CertificateSamples.SAMPLE_CERT);
}
/** Returns a path to a specified certificate file. */
String getCertFilename(String certificateFile) throws IOException {
return writeToNamedTmpFile("cert.pem", certificateFile);
} }
/** Reloads the given resource from Datastore. */ /** Reloads the given resource from Datastore. */

View file

@ -19,9 +19,12 @@ import static com.google.common.truth.Truth8.assertThat;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT; import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH; import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatastoreHelper.createTlds; import static google.registry.testing.DatastoreHelper.createTlds;
import static google.registry.testing.DatastoreHelper.persistNewRegistrar; import static google.registry.testing.DatastoreHelper.persistNewRegistrar;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
@ -31,11 +34,13 @@ import static org.mockito.Mockito.when;
import com.beust.jcommander.ParameterException; import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Range; import com.google.common.collect.Range;
import com.google.common.net.MediaType; import com.google.common.net.MediaType;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.testing.CertificateSamples; import google.registry.util.CertificateChecker;
import java.io.IOException; import java.io.IOException;
import java.security.cert.CertificateException;
import java.util.Optional; import java.util.Optional;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -52,6 +57,12 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
command.setConnection(connection); command.setConnection(connection);
command.certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
fakeClock);
} }
@Test @Test
@ -354,12 +365,13 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test @Test
void testSuccess_clientCertFileFlag() throws Exception { void testSuccess_clientCertFileFlag() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
runCommandForced( runCommandForced(
"--name=blobio", "--name=blobio",
"--password=some_password", "--password=some_password",
"--registrar_type=REAL", "--registrar_type=REAL",
"--iana_id=8", "--iana_id=8",
"--cert_file=" + getCertFilename(), "--cert_file=" + getCertFilename(SAMPLE_CERT3),
"--passcode=01234", "--passcode=01234",
"--icann_referral_email=foo@bar.test", "--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"", "--street=\"123 Fake St\"",
@ -371,8 +383,67 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
Optional<Registrar> registrar = Registrar.loadByClientId("clientz"); Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isPresent(); assertThat(registrar).isPresent();
assertThat(registrar.get().getClientCertificateHash()) assertThat(registrar.get().getClientCertificateHash()).isEqualTo(SAMPLE_CERT3_HASH);
.isEqualTo(CertificateSamples.SAMPLE_CERT_HASH); }
@Test
void testFail_clientCertFileFlagWithViolation() throws Exception {
fakeClock.setTo(DateTime.parse("2020-10-01T00:00:00Z"));
CertificateException thrown =
assertThrows(
CertificateException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--cert_file=" + getCertFilename(SAMPLE_CERT),
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate validity period is too long; it must be less than or equal to 398"
+ " days.");
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isEmpty();
}
@Test
void testFail_clientCertFileFlagWithMultipleViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
CertificateException thrown =
assertThrows(
CertificateException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--cert_file=" + getCertFilename(SAMPLE_CERT),
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate is expired.\nCertificate validity period is too long; it must be less"
+ " than or equal to 398 days.");
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isEmpty();
} }
@Test @Test
@ -400,12 +471,13 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test @Test
void testSuccess_failoverClientCertFileFlag() throws Exception { void testSuccess_failoverClientCertFileFlag() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
runCommandForced( runCommandForced(
"--name=blobio", "--name=blobio",
"--password=some_password", "--password=some_password",
"--registrar_type=REAL", "--registrar_type=REAL",
"--iana_id=8", "--iana_id=8",
"--failover_cert_file=" + getCertFilename(), "--failover_cert_file=" + getCertFilename(SAMPLE_CERT3),
"--passcode=01234", "--passcode=01234",
"--icann_referral_email=foo@bar.test", "--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"", "--street=\"123 Fake St\"",
@ -420,8 +492,68 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
Registrar registrar = registrarOptional.get(); Registrar registrar = registrarOptional.get();
assertThat(registrar.getClientCertificate()).isNull(); assertThat(registrar.getClientCertificate()).isNull();
assertThat(registrar.getClientCertificateHash()).isNull(); assertThat(registrar.getClientCertificateHash()).isNull();
assertThat(registrar.getFailoverClientCertificate()).isEqualTo(SAMPLE_CERT); assertThat(registrar.getFailoverClientCertificate()).isEqualTo(SAMPLE_CERT3);
assertThat(registrar.getFailoverClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH); assertThat(registrar.getFailoverClientCertificateHash()).isEqualTo(SAMPLE_CERT3_HASH);
}
@Test
void testFail_failoverClientCertFileFlagWithViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
CertificateException thrown =
assertThrows(
CertificateException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--failover_cert_file=" + getCertFilename(SAMPLE_CERT),
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate validity period is too long; it must be less than or equal to 398"
+ " days.");
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isEmpty();
}
@Test
void testFail_failoverClientCertFileFlagWithMultipleViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2055-11-01T00:00:00Z"));
CertificateException thrown =
assertThrows(
CertificateException.class,
() ->
runCommandForced(
"--name=blobio",
"--password=some_password",
"--registrar_type=REAL",
"--iana_id=8",
"--failover_cert_file=" + getCertFilename(SAMPLE_CERT),
"--passcode=01234",
"--icann_referral_email=foo@bar.test",
"--street=\"123 Fake St\"",
"--city Fakington",
"--state MA",
"--zip 00351",
"--cc US",
"clientz"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate is expired.\nCertificate validity period is too long; it must be less"
+ " than or equal to 398 days.");
Optional<Registrar> registrar = Registrar.loadByClientId("clientz");
assertThat(registrar).isEmpty();
} }
@Test @Test
@ -1052,7 +1184,7 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test @Test
void testFailure_invalidCertFileContents() { void testFailure_invalidCertFileContents() {
assertThrows( assertThrows(
Exception.class, CertificateException.class,
() -> () ->
runCommandForced( runCommandForced(
"--name=blobio", "--name=blobio",
@ -1073,7 +1205,7 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test @Test
void testFailure_invalidFailoverCertFileContents() { void testFailure_invalidFailoverCertFileContents() {
assertThrows( assertThrows(
IllegalArgumentException.class, CertificateException.class,
() -> () ->
runCommandForced( runCommandForced(
"--name=blobio", "--name=blobio",
@ -1094,7 +1226,7 @@ class CreateRegistrarCommandTest extends CommandTestCase<CreateRegistrarCommand>
@Test @Test
void testFailure_certHashAndCertFile() { void testFailure_certHashAndCertFile() {
assertThrows( assertThrows(
IllegalArgumentException.class, CertificateException.class,
() -> () ->
runCommandForced( runCommandForced(
"--name=blobio", "--name=blobio",

View file

@ -19,10 +19,13 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.Truth8.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT; import static google.registry.testing.CertificateSamples.SAMPLE_CERT;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT3_HASH;
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH; import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
import static google.registry.testing.DatastoreHelper.createTlds; import static google.registry.testing.DatastoreHelper.createTlds;
import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.loadRegistrar;
import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC; import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -30,20 +33,34 @@ import com.beust.jcommander.ParameterException;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.registrar.Registrar; import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State; import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.Registrar.Type; import google.registry.model.registrar.Registrar.Type;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.util.CertificateChecker;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import java.security.cert.CertificateException;
import java.util.Optional; import java.util.Optional;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** Unit tests for {@link UpdateRegistrarCommand}. */ /** Unit tests for {@link UpdateRegistrarCommand}. */
class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand> { class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand> {
@BeforeEach
void beforeEach() {
command.certificateChecker =
new CertificateChecker(
ImmutableSortedMap.of(START_OF_TIME, 825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
30,
2048,
fakeClock);
}
@Test @Test
void testSuccess_alsoUpdateInCloudSql() throws Exception { void testSuccess_alsoUpdateInCloudSql() throws Exception {
assertThat(loadRegistrar("NewRegistrar").verifyPassword("some_password")).isFalse(); assertThat(loadRegistrar("NewRegistrar").verifyPassword("some_password")).isFalse();
@ -232,15 +249,94 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
@Test @Test
void testSuccess_certFile() throws Exception { void testSuccess_certFile() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar"); Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getClientCertificate()).isNull(); assertThat(registrar.getClientCertificate()).isNull();
assertThat(registrar.getClientCertificateHash()).isNull(); assertThat(registrar.getClientCertificateHash()).isNull();
runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"); runCommand("--cert_file=" + getCertFilename(SAMPLE_CERT3), "--force", "NewRegistrar");
registrar = loadRegistrar("NewRegistrar"); registrar = loadRegistrar("NewRegistrar");
// NB: Hash was computed manually using 'openssl x509 -fingerprint -sha256 -in ...' and then // NB: Hash was computed manually using 'openssl x509 -fingerprint -sha256 -in ...' and then
// converting the result from a hex string to non-padded base64 encoded string. // converting the result from a hex string to non-padded base64 encoded string.
assertThat(registrar.getClientCertificate()).isEqualTo(SAMPLE_CERT); assertThat(registrar.getClientCertificate()).isEqualTo(SAMPLE_CERT3);
assertThat(registrar.getClientCertificateHash()).isEqualTo(SAMPLE_CERT_HASH); assertThat(registrar.getClientCertificateHash()).isEqualTo(SAMPLE_CERT3_HASH);
}
@Test
void testFail_certFileWithViolation() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getClientCertificate()).isNull();
assertThat(registrar.getClientCertificateHash()).isNull();
CertificateException thrown =
assertThrows(
CertificateException.class,
() -> runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate validity period is too long; it must be less than or equal to 398"
+ " days.");
assertThat(registrar.getClientCertificate()).isNull();
}
@Test
void testFail_certFileWithMultipleViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getClientCertificate()).isNull();
assertThat(registrar.getClientCertificateHash()).isNull();
CertificateException thrown =
assertThrows(
CertificateException.class,
() -> runCommand("--cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate is expired.\nCertificate validity period is too long; it must be less"
+ " than or equal to 398 days.");
assertThat(registrar.getClientCertificate()).isNull();
}
@Test
void testFail_failoverCertFileWithViolation() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getFailoverClientCertificate()).isNull();
CertificateException thrown =
assertThrows(
CertificateException.class,
() ->
runCommand("--failover_cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate validity period is too long; it must be less than or equal to 398"
+ " days.");
assertThat(registrar.getFailoverClientCertificate()).isNull();
}
@Test
void testFail_failoverCertFileWithMultipleViolations() throws Exception {
fakeClock.setTo(DateTime.parse("2055-10-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getFailoverClientCertificate()).isNull();
CertificateException thrown =
assertThrows(
CertificateException.class,
() ->
runCommand("--failover_cert_file=" + getCertFilename(), "--force", "NewRegistrar"));
assertThat(thrown.getMessage())
.isEqualTo(
"Certificate is expired.\nCertificate validity period is too long; it must be less"
+ " than or equal to 398 days.");
assertThat(registrar.getFailoverClientCertificate()).isNull();
}
@Test
void testSuccess_failoverCertFile() throws Exception {
fakeClock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
Registrar registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getFailoverClientCertificate()).isNull();
runCommand("--failover_cert_file=" + getCertFilename(SAMPLE_CERT3), "--force", "NewRegistrar");
registrar = loadRegistrar("NewRegistrar");
assertThat(registrar.getFailoverClientCertificate()).isEqualTo(SAMPLE_CERT3);
} }
@Test @Test
@ -672,7 +768,7 @@ class UpdateRegistrarCommandTest extends CommandTestCase<UpdateRegistrarCommand>
IllegalArgumentException.class, IllegalArgumentException.class,
() -> () ->
runCommand( runCommand(
"--cert_file=" + getCertFilename(), "--cert_file=" + getCertFilename(SAMPLE_CERT3),
"--cert_hash=ABCDEF", "--cert_hash=ABCDEF",
"--force", "--force",
"NewRegistrar")); "NewRegistrar"));

View file

@ -53,7 +53,6 @@ public class CertificateChecker {
* ); * );
* </pre> * </pre>
*/ */
// TODO(sarahbot): Inject this.
public CertificateChecker( public CertificateChecker(
ImmutableSortedMap<DateTime, Integer> maxValidityLengthSchedule, ImmutableSortedMap<DateTime, Integer> maxValidityLengthSchedule,
int daysToExpiration, int daysToExpiration,