From 279f65b6cf9926d680019f21794f9042d25ca458 Mon Sep 17 00:00:00 2001 From: sarahcaseybot Date: Fri, 29 Jan 2021 16:59:57 -0500 Subject: [PATCH] Convert Strings to X509 Certificates before validating (#948) * Convert certificate strings to certificates * Format fixes * Revert "Format fixes" This reverts commit 26f88bd3135d4106e51b138d0d07ccf4719a432d. * Revert "Convert certificate strings to certificates" This reverts commit 6d47ed2861feca30447c042cb2fb4d28c6ec2e6d. * Convert strings to certs for validation * Add clarification comments * Add test to verify endoded cert from proxy * Add some helper methods * add tests for PEM with metadata * small changes * replace .com with .test --- .../google/registry/flows/TlsCredentials.java | 44 ++++++++- .../flows/certs/CertificateChecker.java | 14 ++- .../ValidateLoginCredentialsCommand.java | 10 ++- .../registry/flows/EppLoginTlsTest.java | 90 ++++++++++++++++--- .../flows/session/LoginFlowViaTlsTest.java | 61 +++++++++++-- .../proxy/handler/EppServiceHandlerTest.java | 19 ++++ .../java/google/registry/util/X509Utils.java | 17 ++++ 7 files changed, 229 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/google/registry/flows/TlsCredentials.java b/core/src/main/java/google/registry/flows/TlsCredentials.java index 48f64bdff..4c23c799c 100644 --- a/core/src/main/java/google/registry/flows/TlsCredentials.java +++ b/core/src/main/java/google/registry/flows/TlsCredentials.java @@ -16,6 +16,7 @@ package google.registry.flows; import static com.google.common.base.MoreObjects.toStringHelper; import static google.registry.request.RequestParameters.extractOptionalHeader; +import static google.registry.util.X509Utils.loadCertificate; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -32,7 +33,11 @@ import google.registry.flows.certs.CertificateChecker.InsecureCertificateExcepti import google.registry.model.registrar.Registrar; import google.registry.request.Header; import google.registry.util.CidrAddressBlock; +import java.io.ByteArrayInputStream; import java.net.InetAddress; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; import java.util.Optional; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; @@ -149,14 +154,31 @@ public class TlsCredentials implements TransportCredentials { "Request from registrar %s did not include X-SSL-Full-Certificate.", registrar.getClientId()); } else { + X509Certificate passedCert; + Optional storedCert; + Optional storedFailoverCert; + + try { + storedCert = deserializePemCert(registrar.getClientCertificate()); + storedFailoverCert = deserializePemCert(registrar.getFailoverClientCertificate()); + passedCert = decodeCertString(clientCertificate.get()); + } catch (Exception e) { + // TODO(Sarahbot@): remove this catch once we know it's working + logger.atWarning().log( + "Error converting certificate string to certificate for %s: %s", + registrar.getClientId(), e); + validateCertificateHash(registrar); + return; + } + // Check if the certificate is equal to the one on file for the registrar. - if (clientCertificate.equals(registrar.getClientCertificate()) - || clientCertificate.equals(registrar.getFailoverClientCertificate())) { + if (passedCert.equals(storedCert.orElse(null)) + || passedCert.equals(storedFailoverCert.orElse(null))) { // Check certificate for any requirement violations // TODO(Sarahbot@): Throw exceptions instead of just logging once requirement enforcement // begins try { - certificateChecker.validateCertificate(clientCertificate.get()); + certificateChecker.validateCertificate(passedCert); } catch (InsecureCertificateException e) { // throw exception in unit tests and Sandbox if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST) @@ -220,6 +242,22 @@ public class TlsCredentials implements TransportCredentials { } } + // Converts a PEM formatted certificate string into an X509Certificate + private Optional deserializePemCert(Optional certificateString) + throws CertificateException { + if (certificateString.isPresent()) { + return Optional.of(loadCertificate(certificateString.get())); + } + return Optional.empty(); + } + + // Decodes the string representation of an encoded certificate back into an X509Certificate + private X509Certificate decodeCertString(String encodedCertString) throws CertificateException { + byte decodedCert[] = Base64.getDecoder().decode(encodedCertString); + ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedCert); + return loadCertificate(inputStream); + } + @Override public String toString() { return toStringHelper(getClass()) diff --git a/core/src/main/java/google/registry/flows/certs/CertificateChecker.java b/core/src/main/java/google/registry/flows/certs/CertificateChecker.java index aeef1356b..404ec1f3c 100644 --- a/core/src/main/java/google/registry/flows/certs/CertificateChecker.java +++ b/core/src/main/java/google/registry/flows/certs/CertificateChecker.java @@ -90,7 +90,19 @@ public class CertificateChecker { * exist. */ public void validateCertificate(String certificateString) throws InsecureCertificateException { - ImmutableSet violations = checkCertificate(certificateString); + handleCertViolations(checkCertificate(certificateString)); + } + + /** + * Checks the given certificate string for violations and throws an exception if any violations + * exist. + */ + public void validateCertificate(X509Certificate certificate) throws InsecureCertificateException { + handleCertViolations(checkCertificate(certificate)); + } + + private void handleCertViolations(ImmutableSet violations) + throws InsecureCertificateException { if (!violations.isEmpty()) { String displayMessages = violations.stream() diff --git a/core/src/main/java/google/registry/tools/ValidateLoginCredentialsCommand.java b/core/src/main/java/google/registry/tools/ValidateLoginCredentialsCommand.java index 07aa9868a..fcd5ec040 100644 --- a/core/src/main/java/google/registry/tools/ValidateLoginCredentialsCommand.java +++ b/core/src/main/java/google/registry/tools/ValidateLoginCredentialsCommand.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Strings.isNullOrEmpty; import static google.registry.util.PreconditionsUtils.checkArgumentPresent; +import static google.registry.util.X509Utils.encodeX509CertificateFromPemString; import static google.registry.util.X509Utils.getCertificateHash; import static google.registry.util.X509Utils.loadCertificate; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -77,10 +78,11 @@ final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi { checkArgument( clientCertificatePath == null || isNullOrEmpty(clientCertificateHash), "Can't specify both --cert_hash and --cert_file"); - String clientCertificate = ""; + String encodedCertificate = ""; if (clientCertificatePath != null) { - clientCertificate = new String(Files.readAllBytes(clientCertificatePath), US_ASCII); - clientCertificateHash = getCertificateHash(loadCertificate(clientCertificate)); + String certificateString = new String(Files.readAllBytes(clientCertificatePath), US_ASCII); + encodedCertificate = encodeX509CertificateFromPemString(certificateString); + clientCertificateHash = getCertificateHash(loadCertificate(clientCertificatePath)); } Registrar registrar = checkArgumentPresent( @@ -88,7 +90,7 @@ final class ValidateLoginCredentialsCommand implements CommandWithRemoteApi { new TlsCredentials( true, Optional.ofNullable(clientCertificateHash), - Optional.ofNullable(clientCertificate), + Optional.ofNullable(encodedCertificate), Optional.ofNullable(clientIpAddress), certificateChecker) .validate(registrar, password); diff --git a/core/src/test/java/google/registry/flows/EppLoginTlsTest.java b/core/src/test/java/google/registry/flows/EppLoginTlsTest.java index 40bb60764..2f2aa97ed 100644 --- a/core/src/test/java/google/registry/flows/EppLoginTlsTest.java +++ b/core/src/test/java/google/registry/flows/EppLoginTlsTest.java @@ -18,6 +18,8 @@ import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.LogsSubject.assertAboutLogs; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static google.registry.util.X509Utils.encodeX509Certificate; +import static google.registry.util.X509Utils.encodeX509CertificateFromPemString; import static org.joda.time.DateTimeZone.UTC; import com.google.common.collect.ImmutableMap; @@ -31,6 +33,7 @@ import google.registry.testing.CertificateSamples; import google.registry.testing.SystemPropertyExtension; import google.registry.util.SelfSignedCaCertificate; import java.io.StringWriter; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Optional; import java.util.logging.Level; @@ -67,6 +70,8 @@ class EppLoginTlsTest extends EppTestCase { Logger.getLogger(TlsCredentials.class.getCanonicalName()); private final TestLogHandler handler = new TestLogHandler(); + private String encodedCertString; + void setCredentials(String clientCertificateHash, String clientCertificate) { setTransportCredentials( new TlsCredentials( @@ -78,7 +83,7 @@ class EppLoginTlsTest extends EppTestCase { } @BeforeEach - void beforeEach() { + void beforeEach() throws CertificateException { persistResource( loadRegistrar("NewRegistrar") .asBuilder() @@ -91,18 +96,19 @@ class EppLoginTlsTest extends EppTestCase { .setClientCertificate(CertificateSamples.SAMPLE_CERT2, DateTime.now(UTC)) .build()); loggerToIntercept.addHandler(handler); + encodedCertString = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT3); } @Test void testLoginLogout() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, CertificateSamples.SAMPLE_CERT3); + setCredentials(null, encodedCertString); assertThatLoginSucceeds("NewRegistrar", "foo-BAR2"); assertThatLogoutSucceeds(); } @Test void testLogin_wrongPasswordFails() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, CertificateSamples.SAMPLE_CERT3); + setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString); // For TLS login, we also check the epp xml password. assertThatLogin("NewRegistrar", "incorrect") .hasResponse( @@ -112,7 +118,7 @@ class EppLoginTlsTest extends EppTestCase { @Test void testMultiLogin() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, CertificateSamples.SAMPLE_CERT3); + setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString); assertThatLoginSucceeds("NewRegistrar", "foo-BAR2"); assertThatLogoutSucceeds(); assertThatLoginSucceeds("NewRegistrar", "foo-BAR2"); @@ -126,7 +132,7 @@ class EppLoginTlsTest extends EppTestCase { @Test void testNonAuthedLogin_fails() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT_HASH, CertificateSamples.SAMPLE_CERT); + setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, encodedCertString); assertThatLogin("TheRegistrar", "password2") .hasResponse( "response_error.xml", @@ -155,7 +161,7 @@ class EppLoginTlsTest extends EppTestCase { @Test void testGoodPrimaryCertificate() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, CertificateSamples.SAMPLE_CERT3); + setCredentials(null, encodedCertString); DateTime now = DateTime.now(UTC); persistResource( loadRegistrar("NewRegistrar") @@ -168,7 +174,7 @@ class EppLoginTlsTest extends EppTestCase { @Test void testGoodFailoverCertificate() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, CertificateSamples.SAMPLE_CERT3); + setCredentials(null, encodedCertString); DateTime now = DateTime.now(UTC); persistResource( loadRegistrar("NewRegistrar") @@ -181,7 +187,7 @@ class EppLoginTlsTest extends EppTestCase { @Test void testMissingPrimaryCertificateButHasFailover_usesFailover() throws Exception { - setCredentials(CertificateSamples.SAMPLE_CERT3_HASH, CertificateSamples.SAMPLE_CERT3); + setCredentials(null, encodedCertString); DateTime now = DateTime.now(UTC); persistResource( loadRegistrar("NewRegistrar") @@ -210,8 +216,9 @@ class EppLoginTlsTest extends EppTestCase { @Test void testCertificateDoesNotMeetRequirements_fails() throws Exception { + String proxyEncoded = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT); // SAMPLE_CERT has a validity period that is too long - setCredentials(CertificateSamples.SAMPLE_CERT_HASH, CertificateSamples.SAMPLE_CERT); + setCredentials(CertificateSamples.SAMPLE_CERT_HASH, proxyEncoded); persistResource( loadRegistrar("NewRegistrar") .asBuilder() @@ -232,7 +239,6 @@ class EppLoginTlsTest extends EppTestCase { @Test void testCertificateDoesNotMeetMultipleRequirements_fails() throws Exception { - X509Certificate certificate = SelfSignedCaCertificate.create( "test", clock.nowUtc().plusDays(100), clock.nowUtc().plusDays(5000)) @@ -244,8 +250,10 @@ class EppLoginTlsTest extends EppTestCase { pw.writeObject(generator); } + String proxyEncoded = encodeX509Certificate(certificate); + // SAMPLE_CERT has a validity period that is too long - setCredentials(CertificateSamples.SAMPLE_CERT_HASH, sw.toString()); + setCredentials(null, proxyEncoded); persistResource( loadRegistrar("NewRegistrar") .asBuilder() @@ -270,7 +278,8 @@ class EppLoginTlsTest extends EppTestCase { void testCertificateDoesNotMeetRequirementsInProduction_succeeds() throws Exception { RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension); // SAMPLE_CERT has a validity period that is too long - setCredentials(CertificateSamples.SAMPLE_CERT_HASH, CertificateSamples.SAMPLE_CERT); + String proxyEncoded = encodeX509CertificateFromPemString(CertificateSamples.SAMPLE_CERT); + setCredentials(null, proxyEncoded); persistResource( loadRegistrar("NewRegistrar") .asBuilder() @@ -288,4 +297,61 @@ class EppLoginTlsTest extends EppTestCase { + " Certificate validity period is too long; it must be less than or equal to 398" + " days."); } + + @Test + void testRegistrarCertificateContainsExtraMetadata_succeeds() throws Exception { + String certPem = + String.format( + "Bag Attributes\n" + + " localKeyID: 1F 1C 3A 3A 4C 03 EC C4 BC 7A C3 21 A9 F2 13 66 21 B8 7B 26 \n" + + "subject=/C=US/ST=New York/L=New" + + " York/O=Test/OU=ABC/CN=tester.test/emailAddress=test-certificate@test.test\n" + + "issuer=/C=US/ST=NY/L=NYC/O=ABC/OU=TEST CA/CN=TEST" + + " CA/emailAddress=testing@test.test\n" + + "%s", + CertificateSamples.SAMPLE_CERT3); + + setCredentials(null, encodeX509CertificateFromPemString(certPem)); + DateTime now = DateTime.now(UTC); + persistResource( + loadRegistrar("NewRegistrar") + .asBuilder() + .setClientCertificate(certPem, now) + .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now) + .build()); + assertThatLoginSucceeds("NewRegistrar", "foo-BAR2"); + } + + @Test + void testRegistrarCertificateContainsExtraMetadataAndViolations_fails() throws Exception { + String certPem = + String.format( + "Bag Attributes\n" + + " localKeyID: 1F 1C 3A 3A 4C 03 EC C4 BC 7A C3 21 A9 F2 13 66 21 B8 7B 26 \n" + + "subject=/C=US/ST=New York/L=New" + + " York/O=Test/OU=ABC/CN=tester.test/emailAddress=test-certificate@test.test\n" + + "issuer=/C=US/ST=NY/L=NYC/O=ABC/OU=TEST CA/CN=TEST" + + " CA/emailAddress=testing@test.test\n" + + "%s", + CertificateSamples.SAMPLE_CERT); + + setCredentials(null, encodeX509CertificateFromPemString(certPem)); + DateTime now = DateTime.now(UTC); + persistResource( + loadRegistrar("NewRegistrar") + .asBuilder() + .setClientCertificate(certPem, now) + .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now) + .build()); + assertThatLogin("NewRegistrar", "foo-BAR2") + .hasResponse( + "response_error.xml", + ImmutableMap.of( + "CODE", + "2200", + "MSG", + "Registrar certificate contains the following security violations:\n" + + "Certificate validity period is too long; it must be less than or equal to" + + " 398 days.")); + } } diff --git a/core/src/test/java/google/registry/flows/session/LoginFlowViaTlsTest.java b/core/src/test/java/google/registry/flows/session/LoginFlowViaTlsTest.java index 2b0f6b4cd..e4245efde 100644 --- a/core/src/test/java/google/registry/flows/session/LoginFlowViaTlsTest.java +++ b/core/src/test/java/google/registry/flows/session/LoginFlowViaTlsTest.java @@ -16,6 +16,7 @@ package google.registry.flows.session; import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static google.registry.util.X509Utils.encodeX509CertificateFromPemString; import static org.joda.time.DateTimeZone.UTC; import com.google.common.collect.ImmutableList; @@ -30,9 +31,18 @@ import google.registry.flows.certs.CertificateChecker; import google.registry.model.registrar.Registrar; import google.registry.testing.CertificateSamples; import google.registry.util.CidrAddressBlock; +import google.registry.util.SelfSignedCaCertificate; +import java.io.StringWriter; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; import java.util.Optional; import org.joda.time.DateTime; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemObjectGenerator; +import org.testcontainers.shaded.org.bouncycastle.util.io.pem.PemWriter; /** Unit tests for {@link LoginFlow} when accessed via a TLS transport. */ public class LoginFlowViaTlsTest extends LoginFlowTestCase { @@ -54,6 +64,12 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { 2048, ImmutableSet.of("secp256r1", "secp384r1"), clock); + private Optional encodedCertString; + + @BeforeEach + void beforeEach() throws CertificateException { + encodedCertString = Optional.of(encodeX509CertificateFromPemString(GOOD_CERT.get())); + } @Override protected Registrar.Builder getRegistrarBuilder() { @@ -66,7 +82,36 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { @Test void testSuccess_withGoodCredentials() throws Exception { persistResource(getRegistrarBuilder().build()); - credentials = new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, GOOD_IP, certificateChecker); + credentials = + new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IP, certificateChecker); + doSuccessfulTest("login_valid.xml"); + } + + @Test + void testSuccess_withNewlyConstructedCertificate() throws Exception { + X509Certificate certificate = + SelfSignedCaCertificate.create( + "test", clock.nowUtc().minusDays(100), clock.nowUtc().plusDays(150)) + .cert(); + + StringWriter sw = new StringWriter(); + try (PemWriter pw = new PemWriter(sw)) { + PemObjectGenerator generator = new JcaMiscPEMGenerator(certificate); + pw.writeObject(generator); + } + + persistResource( + super.getRegistrarBuilder() + .setClientCertificate(sw.toString(), DateTime.now(UTC)) + .setIpAddressAllowList( + ImmutableList.of( + CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32))) + .build()); + + String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded()); + credentials = + new TlsCredentials( + true, Optional.empty(), Optional.of(encodedCertificate), GOOD_IP, certificateChecker); doSuccessfulTest("login_valid.xml"); } @@ -78,7 +123,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { ImmutableList.of(CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32"))) .build()); credentials = - new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, GOOD_IPV6, certificateChecker); + new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IPV6, certificateChecker); doSuccessfulTest("login_valid.xml"); } @@ -90,7 +135,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { ImmutableList.of(CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32"))) .build()); credentials = - new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, GOOD_IPV6, certificateChecker); + new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IPV6, certificateChecker); doSuccessfulTest("login_valid.xml"); } @@ -100,14 +145,18 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { getRegistrarBuilder() .setIpAddressAllowList(ImmutableList.of(CidrAddressBlock.create("192.168.1.255/24"))) .build()); - credentials = new TlsCredentials(true, GOOD_CERT_HASH, GOOD_CERT, GOOD_IP, certificateChecker); + credentials = + new TlsCredentials(true, GOOD_CERT_HASH, encodedCertString, GOOD_IP, certificateChecker); doSuccessfulTest("login_valid.xml"); } @Test - void testFailure_incorrectClientCertificateHash() { + void testFailure_incorrectClientCertificateHash() throws Exception { persistResource(getRegistrarBuilder().build()); - credentials = new TlsCredentials(true, BAD_CERT_HASH, BAD_CERT, GOOD_IP, certificateChecker); + String proxyEncoded = encodeX509CertificateFromPemString(BAD_CERT.get()); + credentials = + new TlsCredentials( + true, BAD_CERT_HASH, Optional.of(proxyEncoded), GOOD_IP, certificateChecker); doFailingTest("login_valid.xml", BadRegistrarCertificateException.class); } diff --git a/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java b/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java index ab389d14c..1869cafe5 100644 --- a/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java +++ b/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java @@ -21,6 +21,7 @@ import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent; import static google.registry.proxy.TestUtils.makeEppHttpResponse; import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY; import static google.registry.util.X509Utils.getCertificateHash; +import static google.registry.util.X509Utils.loadCertificate; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; @@ -47,6 +48,7 @@ import io.netty.util.concurrent.Promise; import java.io.ByteArrayInputStream; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.Base64; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -239,6 +241,23 @@ class EppServiceHandlerTest { assertThat(channel.isActive()).isTrue(); } + @Test + void testSuccess_requestContainsEncodedCertificate() throws Exception { + setHandshakeSuccess(); + // First inbound message is hello. + channel.readInbound(); + String content = "stuff"; + channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8))); + FullHttpRequest request = channel.readInbound(); + assertThat(request).isEqualTo(makeEppHttpRequestWithCertificate(content)); + String encodedCert = request.headers().get("X-SSL-Full-Certificate"); + assertThat(encodedCert).isNotEqualTo(SAMPLE_CERT); + X509Certificate decodedCert = + loadCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(encodedCert))); + X509Certificate pemCert = loadCertificate(SAMPLE_CERT); + assertThat(decodedCert).isEqualTo(pemCert); + } + @Test void testSuccess_sendCertificateOnlyBeforeLogin() throws Exception { setHandshakeSuccess(); diff --git a/util/src/main/java/google/registry/util/X509Utils.java b/util/src/main/java/google/registry/util/X509Utils.java index f51e6ba08..ecd4250f3 100644 --- a/util/src/main/java/google/registry/util/X509Utils.java +++ b/util/src/main/java/google/registry/util/X509Utils.java @@ -32,6 +32,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CRLException; import java.security.cert.CRLReason; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateParsingException; @@ -39,6 +40,7 @@ import java.security.cert.CertificateRevokedException; import java.security.cert.X509CRL; import java.security.cert.X509CRLEntry; import java.security.cert.X509Certificate; +import java.util.Base64; import java.util.Date; import java.util.NoSuchElementException; import java.util.Optional; @@ -177,5 +179,20 @@ public final class X509Utils { newCrl.verify(rootCert.getPublicKey()); } + /** Constructs an X.509 certificate from a PEM string and encodes it. */ + public static String encodeX509CertificateFromPemString(String certificateString) + throws CertificateException { + return encodeX509Certificate(loadCertificate(certificateString)); + } + + /** + * Encodes an X.509 certificate in the same form that the proxy encodes a certificate before + * passing it via an HTTP header. + */ + public static String encodeX509Certificate(X509Certificate certificate) + throws CertificateEncodingException { + return Base64.getEncoder().encodeToString(certificate.getEncoded()); + } + private X509Utils() {} }