diff --git a/xds/src/main/java/io/grpc/xds/sds/trust/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/sds/trust/CertificateUtils.java new file mode 100644 index 0000000000..481abd8e63 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/sds/trust/CertificateUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 The gRPC Authors + * + * 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 io.grpc.xds.sds.trust; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; + +/** + * Contains certificate utility method(s). + */ +final class CertificateUtils { + + private static CertificateFactory factory; + + private static synchronized void initInstance() throws CertificateException { + if (factory == null) { + factory = CertificateFactory.getInstance("X.509"); + } + } + + static synchronized X509Certificate[] toX509Certificates(String fileName) + throws CertificateException, IOException { + initInstance(); + FileInputStream fis = new FileInputStream(fileName); + BufferedInputStream bis = new BufferedInputStream(fis); + try { + Collection certs = factory.generateCertificates(bis); + return certs.toArray(new X509Certificate[0]); + } finally { + bis.close(); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/sds/trust/SdsX509TrustManager.java b/xds/src/main/java/io/grpc/xds/sds/trust/SdsX509TrustManager.java new file mode 100644 index 0000000000..a1ae0d70db --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/sds/trust/SdsX509TrustManager.java @@ -0,0 +1,280 @@ +/* + * Copyright 2019 The gRPC Authors + * + * 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 io.grpc.xds.sds.trust; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import javax.annotation.Nullable; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Extension of {@link X509ExtendedTrustManager} that implements verification of + * SANs (subject-alternate-names) against the list in CertificateValidationContext. + */ +final class SdsX509TrustManager extends X509ExtendedTrustManager implements X509TrustManager { + + // ref: io.grpc.okhttp.internal.OkHostnameVerifier and + // sun.security.x509.GeneralNameInterface + private static final int ALT_DNS_NAME = 2; + private static final int ALT_URI_NAME = 6; + private static final int ALT_IPA_NAME = 7; + + private final X509ExtendedTrustManager delegate; + private final CertificateValidationContext certContext; + + SdsX509TrustManager(@Nullable CertificateValidationContext certContext, + X509ExtendedTrustManager delegate) { + checkNotNull(delegate, "delegate"); + this.certContext = certContext; + this.delegate = delegate; + } + + // Copied from OkHostnameVerifier.verifyHostName(). + private static boolean verifyDnsNameInPattern(String pattern, String sanToVerify) { + // Basic sanity checks + // Check length == 0 instead of .isEmpty() to support Java 5. + if (sanToVerify == null + || sanToVerify.length() == 0 + || sanToVerify.startsWith(".") + || sanToVerify.endsWith("..")) { + // Invalid domain name + return false; + } + if (pattern == null + || pattern.length() == 0 + || pattern.startsWith(".") + || pattern.endsWith("..")) { + // Invalid pattern/domain name + return false; + } + + // Normalize sanToVerify and pattern by turning them into absolute domain names if they are not + // yet absolute. This is needed because server certificates do not normally contain absolute + // names or patterns, but they should be treated as absolute. At the same time, any sanToVerify + // presented to this method should also be treated as absolute for the purposes of matching + // to the server certificate. + // www.android.com matches www.android.com + // www.android.com matches www.android.com. + // www.android.com. matches www.android.com. + // www.android.com. matches www.android.com + if (!sanToVerify.endsWith(".")) { + sanToVerify += '.'; + } + if (!pattern.endsWith(".")) { + pattern += '.'; + } + // sanToVerify and pattern are now absolute domain names. + + pattern = pattern.toLowerCase(Locale.US); + // sanToVerify and pattern are now in lower case -- domain names are case-insensitive. + + if (!pattern.contains("*")) { + // Not a wildcard pattern -- sanToVerify and pattern must match exactly. + return sanToVerify.equals(pattern); + } + // Wildcard pattern + + // WILDCARD PATTERN RULES: + // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the + // only character in that label (i.e., must match the whole left-most label). + // For example, *.example.com is permitted, while *a.example.com, a*.example.com, + // a*b.example.com, a.*.example.com are not permitted. + // 2. Asterisk (*) cannot match across domain name labels. + // For example, *.example.com matches test.example.com but does not match + // sub.test.example.com. + // 3. Wildcard patterns for single-label domain names are not permitted. + + if (!pattern.startsWith("*.") || pattern.indexOf('*', 1) != -1) { + // Asterisk (*) is only permitted in the left-most domain name label and must be the only + // character in that label + return false; + } + + // Optimization: check whether sanToVerify is too short to match the pattern. sanToVerify must + // be at + // least as long as the pattern because asterisk must match the whole left-most label and + // sanToVerify starts with a non-empty label. Thus, asterisk has to match one or more + // characters. + if (sanToVerify.length() < pattern.length()) { + // sanToVerify too short to match the pattern. + return false; + } + + if ("*.".equals(pattern)) { + // Wildcard pattern for single-label domain name -- not permitted. + return false; + } + + // sanToVerify must end with the region of pattern following the asterisk. + String suffix = pattern.substring(1); + if (!sanToVerify.endsWith(suffix)) { + // sanToVerify does not end with the suffix + return false; + } + + // Check that asterisk did not match across domain name labels. + int suffixStartIndexInHostName = sanToVerify.length() - suffix.length(); + // Asterisk is matching across domain name labels -- not permitted. + return suffixStartIndexInHostName <= 0 + || sanToVerify.lastIndexOf('.', suffixStartIndexInHostName - 1) == -1; + + // sanToVerify matches pattern + } + + private static boolean verifyDnsNameInSanList(String altNameFromCert, + List verifySanList) { + for (String verifySan : verifySanList) { + if (verifyDnsNameInPattern(altNameFromCert, verifySan)) { + return true; + } + } + return false; + } + + /** + * helper function for verifying URI or IP address. For now we compare IP addresses as strings + * without any regard to IPv4 vs IPv6. + * + * @param stringFromCert either URI or IP address + * @param verifySanList list of SANs from certificate context + * @return true if there is a match + */ + private static boolean verifyStringInSanList(String stringFromCert, List verifySanList) { + for (String sanToVerify : verifySanList) { + if (sanToVerify.equalsIgnoreCase(stringFromCert)) { + return true; + } + } + return false; + } + + private static boolean verifyOneSanInList(List entry, List verifySanList) + throws CertificateParsingException { + // from OkHostnameVerifier.getSubjectAltNames + if (entry == null || entry.size() < 2) { + throw new CertificateParsingException("Invalid SAN entry"); + } + Integer altNameType = (Integer) entry.get(0); + if (altNameType == null) { + throw new CertificateParsingException("Invalid SAN entry: null altNameType"); + } + String altNameFromCert = (String) entry.get(1); + switch (altNameType) { + case ALT_DNS_NAME: + return verifyDnsNameInSanList(altNameFromCert, verifySanList); + case ALT_URI_NAME: + case ALT_IPA_NAME: + return verifyStringInSanList(altNameFromCert, verifySanList); + default: + throw new CertificateParsingException("Unsupported altNameType: " + altNameType); + } + } + + // logic from Envoy::Extensions::TransportSockets::Tls::ContextImpl::verifySubjectAltName + private static void verifySubjectAltNameInLeaf(X509Certificate cert, List verifyList) + throws CertificateException { + Collection> names = cert.getSubjectAlternativeNames(); + if (names == null || names.size() == 0) { + throw new CertificateException("Peer certificate SAN check failed"); + } + for (List name : names) { + if (verifyOneSanInList(name, verifyList)) { + return; + } + } + // at this point there's no match + throw new CertificateException("Peer certificate SAN check failed"); + } + + /** + * Verifies SANs in the peer cert chain against verify_subject_alt_name in the certContext. + * This is called from various check*Trusted methods. + */ + @VisibleForTesting + void verifySubjectAltNameInChain(X509Certificate[] peerCertChain) throws CertificateException { + if (certContext == null) { + return; + } + List verifyList = certContext.getVerifySubjectAltNameList(); + if (verifyList == null || verifyList.isEmpty()) { + return; + } + if (peerCertChain == null || peerCertChain.length < 1) { + throw new CertificateException("Peer certificate(s) missing"); + } + // verify SANs only in the top cert (leaf cert) + verifySubjectAltNameInLeaf(peerCertChain[0], verifyList); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + delegate.checkClientTrusted(chain, authType, socket); + verifySubjectAltNameInChain(chain); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) + throws CertificateException { + delegate.checkClientTrusted(chain, authType, sslEngine); + verifySubjectAltNameInChain(chain); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + delegate.checkClientTrusted(chain, authType); + verifySubjectAltNameInChain(chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + delegate.checkServerTrusted(chain, authType, socket); + verifySubjectAltNameInChain(chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine) + throws CertificateException { + delegate.checkServerTrusted(chain, authType, sslEngine); + verifySubjectAltNameInChain(chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + delegate.checkServerTrusted(chain, authType); + verifySubjectAltNameInChain(chain); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return delegate.getAcceptedIssuers(); + } +} diff --git a/xds/src/test/certs/client.pem b/xds/src/test/certs/client.pem new file mode 100644 index 0000000000..913649b97f --- /dev/null +++ b/xds/src/test/certs/client.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6TCCAlKgAwIBAgIBCjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTEwMDEwOTU4WhcNMjUxMTA3 +MDEwOTU4WjBaMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8G +A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRMwEQYDVQQDDAp0ZXN0Y2xp +ZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDsVEfbob4W3lVCDLOVmx9K +cdJnoZdvurGaTY87xNiopmaR8zCR7pFR9BX5L4bNG/PkuVLfVTVAKndyDCQggBBr +UTaEITNbfWK9swHJEr20WnKfhS/wo/Xg5sqNNCrFRmnnnwOA4eDlvmYZEzSnJXV6 +pEro9bBH9uOCWWLqmaev7QIDAQABo4HCMIG/MAkGA1UdEwQCMAAwCwYDVR0PBAQD +AgXgMB0GA1UdDgQWBBQAdbW5Vml/CnYwqdP3mOHDARU+8zBwBgNVHSMEaTBnoVqk +WDBWMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMY +SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2GCCQCRxhke +HRoqBzAJBgNVHREEAjAAMAkGA1UdEgQCMAAwDQYJKoZIhvcNAQELBQADgYEAf4MM +k+sdzd720DfrQ0PF2gDauR3M9uBubozDuMuF6ufAuQBJSKGQEGibXbUelrwHmnql +UjTyfolVcxEBVaF4VFHmn7u6vP7S1NexIDdNUHcULqxIb7Tzl8JYq8OOHD2rQy4H +s8BXaVIzw4YcaCGAMS0iDX052Sy7e2JhP8Noxvo= +-----END CERTIFICATE----- diff --git a/xds/src/test/certs/server1.pem b/xds/src/test/certs/server1.pem new file mode 100644 index 0000000000..f3d43fcc5b --- /dev/null +++ b/xds/src/test/certs/server1.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx +MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV +BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 +ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco +LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg +zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd +9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy +em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G +CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 +hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh +y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +-----END CERTIFICATE----- diff --git a/xds/src/test/java/io/grpc/xds/sds/trust/SdsX509TrustManagerTest.java b/xds/src/test/java/io/grpc/xds/sds/trust/SdsX509TrustManagerTest.java new file mode 100644 index 0000000000..d118c7eb2d --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/sds/trust/SdsX509TrustManagerTest.java @@ -0,0 +1,236 @@ +/* + * Copyright 2019 The gRPC Authors + * + * 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 io.grpc.xds.sds.trust; + +import static com.google.common.truth.Truth.assertThat; + +import io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.X509ExtendedTrustManager; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Unit tests for {@link SdsX509TrustManager}. + */ +@RunWith(JUnit4.class) +public class SdsX509TrustManagerTest { + /** + * server1 has 4 SANs. + */ + private static final String SERVER_1_PEM_FILE = "src/test/certs/server1.pem"; + + /** + * client has no SANs. + */ + private static final String CLIENT_PEM_FILE = "src/test/certs/client.pem"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private X509ExtendedTrustManager mockDelegate; + + @Test + public void nullCertContextTest() throws CertificateException, IOException { + SdsX509TrustManager trustManager = new SdsX509TrustManager(null, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void emptySanListContextTest() throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void missingPeerCerts() throws CertificateException, FileNotFoundException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("foo.com") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + try { + trustManager.verifySubjectAltNameInChain(null); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Peer certificate(s) missing"); + } + } + + @Test + public void emptyArrayPeerCerts() throws CertificateException, FileNotFoundException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("foo.com") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + try { + trustManager.verifySubjectAltNameInChain(new X509Certificate[0]); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Peer certificate(s) missing"); + } + } + + @Test + public void noSansInPeerCerts() throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("foo.com") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(CLIENT_PEM_FILE); + try { + trustManager.verifySubjectAltNameInChain(certs); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Peer certificate SAN check failed"); + } + } + + @Test + public void oneSanInPeerCertsVerifies() throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("waterzooi.test.google.be") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void oneSanInPeerCertsVerifiesMultipleVerifySans() + throws CertificateException, IOException { + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder() + .addVerifySubjectAltName("x.foo.com") + .addVerifySubjectAltName("waterzooi.test.google.be") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void oneSanInPeerCertsNotFoundException() + throws CertificateException, IOException { + CertificateValidationContext certContext = + CertificateValidationContext.newBuilder().addVerifySubjectAltName("x.foo.com").build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + try { + trustManager.verifySubjectAltNameInChain(certs); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Peer certificate SAN check failed"); + } + } + + @Test + public void wildcardSanInPeerCertsVerifiesMultipleVerifySans() + throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("x.foo.com") + .addVerifySubjectAltName("abc.test.youtube.com") // should match *.test.youtube.com + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void wildcardSanInPeerCertsVerifiesMultipleVerifySans1() + throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("x.foo.com") + .addVerifySubjectAltName("abc.test.google.fr") // should match *.test.google.fr + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void wildcardSanInPeerCertsSubdomainMismatch() + throws CertificateException, IOException { + // 2. Asterisk (*) cannot match across domain name labels. + // For example, *.example.com matches test.example.com but does not match + // sub.test.example.com. + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("sub.abc.test.youtube.com") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + try { + trustManager.verifySubjectAltNameInChain(certs); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Peer certificate SAN check failed"); + } + } + + @Test + public void oneIpAddressInPeerCertsVerifies() throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("x.foo.com") + .addVerifySubjectAltName("192.168.1.3") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + trustManager.verifySubjectAltNameInChain(certs); + } + + @Test + public void oneIpAddressInPeerCertsMismatch() throws CertificateException, IOException { + CertificateValidationContext certContext = CertificateValidationContext + .newBuilder() + .addVerifySubjectAltName("x.foo.com") + .addVerifySubjectAltName("192.168.2.3") + .build(); + SdsX509TrustManager trustManager = new SdsX509TrustManager(certContext, mockDelegate); + X509Certificate[] certs = CertificateUtils.toX509Certificates(SERVER_1_PEM_FILE); + try { + trustManager.verifySubjectAltNameInChain(certs); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Peer certificate SAN check failed"); + } + } +}