mirror of https://github.com/grpc/grpc-java.git
xds: SdsX509TrustManager implementation for XDS and SDS (#6254)
This commit is contained in:
parent
024a46bd11
commit
a633b53f95
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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-----
|
||||||
|
|
@ -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-----
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue