xds: SdsX509TrustManager implementation for XDS and SDS (#6254)

This commit is contained in:
sanjaypujare 2019-10-09 14:52:09 -07:00 committed by GitHub
parent 024a46bd11
commit a633b53f95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 603 additions and 0 deletions

View File

@ -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<? extends Certificate> certs = factory.generateCertificates(bis);
return certs.toArray(new X509Certificate[0]);
} finally {
bis.close();
}
}
}

View File

@ -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<String> 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<String> verifySanList) {
for (String sanToVerify : verifySanList) {
if (sanToVerify.equalsIgnoreCase(stringFromCert)) {
return true;
}
}
return false;
}
private static boolean verifyOneSanInList(List<?> entry, List<String> 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<String> verifyList)
throws CertificateException {
Collection<List<?>> 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<String> 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();
}
}

View File

@ -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-----

View File

@ -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-----

View File

@ -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");
}
}
}