diff --git a/xds/src/main/java/io/grpc/xds/sds/trust/SdsTrustManagerFactory.java b/xds/src/main/java/io/grpc/xds/sds/trust/SdsTrustManagerFactory.java new file mode 100644 index 0000000000..0b19721beb --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/sds/trust/SdsTrustManagerFactory.java @@ -0,0 +1,115 @@ +/* + * 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 io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext; +import io.grpc.Internal; +import io.netty.handler.ssl.util.SimpleTrustManagerFactory; +import java.io.File; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertStoreException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.ManagerFactoryParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * Factory class used by providers of {@link io.grpc.xds.sds.TlsContextManager} to provide a {@link + * SdsX509TrustManager} for trust and SAN checks. + */ +@Internal +public final class SdsTrustManagerFactory extends SimpleTrustManagerFactory { + + private static final Logger logger = Logger.getLogger(SdsTrustManagerFactory.class.getName()); + private SdsX509TrustManager sdsX509TrustManager; + + /** Constructor that loads certs from a file. */ + public SdsTrustManagerFactory( + String certsFile, @Nullable CertificateValidationContext certContext) + throws CertificateException, IOException, CertStoreException { + this(CertificateUtils.toX509Certificates(new File(certsFile)), certContext); + } + + /** Constructor that takes array of certs. */ + public SdsTrustManagerFactory( + X509Certificate[] certs, @Nullable CertificateValidationContext certContext) + throws CertStoreException { + checkNotNull(certs, "certs"); + createSdsX509TrustManager(certs, certContext); + } + + private void createSdsX509TrustManager( + X509Certificate[] certs, CertificateValidationContext certContext) throws CertStoreException { + TrustManagerFactory tmf = null; + try { + tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + KeyStore ks = KeyStore.getInstance("PKCS12"); + // perform a load to initialize KeyStore + ks.load(/* stream= */ null, /* password= */ null); + int i = 1; + for (X509Certificate cert : certs) { + // note: alias lookup uses toLowerCase(Locale.ENGLISH) + // so our alias needs to be all lower-case and unique + ks.setCertificateEntry("alias" + Integer.toString(i), cert); + i++; + } + tmf.init(ks); + } catch (NoSuchAlgorithmException | KeyStoreException | IOException | CertificateException e) { + logger.log(Level.SEVERE, "createSdsX509TrustManager", e); + throw new CertStoreException(e); + } + TrustManager[] tms = tmf.getTrustManagers(); + X509ExtendedTrustManager myDelegate = null; + if (tms != null) { + for (TrustManager tm : tms) { + if (tm instanceof X509ExtendedTrustManager) { + myDelegate = (X509ExtendedTrustManager) tm; + break; + } + } + } + if (myDelegate == null) { + throw new CertStoreException("Native X509 TrustManager not found."); + } + sdsX509TrustManager = new SdsX509TrustManager(certContext, myDelegate); + } + + @Override + protected void engineInit(KeyStore keyStore) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + protected TrustManager[] engineGetTrustManagers() { + return new TrustManager[] {sdsX509TrustManager}; + } +} diff --git a/xds/src/test/java/io/grpc/xds/sds/trust/SdsTrustManagerFactoryTest.java b/xds/src/test/java/io/grpc/xds/sds/trust/SdsTrustManagerFactoryTest.java new file mode 100644 index 0000000000..e6eabcbb55 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/sds/trust/SdsTrustManagerFactoryTest.java @@ -0,0 +1,132 @@ +/* + * 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.grpc.internal.testing.TestUtils; +import java.io.IOException; +import java.security.cert.CertStoreException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.TrustManager; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link SdsTrustManagerFactory}. */ +@RunWith(JUnit4.class) +public class SdsTrustManagerFactoryTest { + + /** Trust store cert. */ + private static final String CA_PEM_FILE = "ca.pem"; + + /** server cert. */ + private static final String SERVER_1_PEM_FILE = "server1.pem"; + + /** client cert. */ + private static final String CLIENT_PEM_FILE = "client.pem"; + + /** bad server cert. */ + private static final String BAD_SERVER_PEM_FILE = "badserver.pem"; + + /** bad client cert. */ + private static final String BAD_CLIENT_PEM_FILE = "badclient.pem"; + + @Test + public void constructor_fromFile() throws CertificateException, IOException, CertStoreException { + SdsTrustManagerFactory factory = + new SdsTrustManagerFactory( + TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(), /* certContext= */ null); + assertThat(factory).isNotNull(); + TrustManager[] tms = factory.getTrustManagers(); + assertThat(tms).isNotNull(); + assertThat(tms).hasLength(1); + TrustManager myTm = tms[0]; + assertThat(myTm).isInstanceOf(SdsX509TrustManager.class); + SdsX509TrustManager sdsX509TrustManager = (SdsX509TrustManager) myTm; + X509Certificate[] acceptedIssuers = sdsX509TrustManager.getAcceptedIssuers(); + assertThat(acceptedIssuers).isNotNull(); + assertThat(acceptedIssuers).hasLength(1); + X509Certificate caCert = acceptedIssuers[0]; + assertThat(caCert) + .isEqualTo(CertificateUtils.toX509Certificates(TestUtils.loadCert(CA_PEM_FILE))[0]); + } + + @Test + public void checkServerTrusted_goodCert() + throws CertificateException, IOException, CertStoreException { + SdsTrustManagerFactory factory = + new SdsTrustManagerFactory( + TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(), /* certContext= */ null); + SdsX509TrustManager sdsX509TrustManager = (SdsX509TrustManager) factory.getTrustManagers()[0]; + X509Certificate[] serverChain = + CertificateUtils.toX509Certificates(TestUtils.loadCert(SERVER_1_PEM_FILE)); + sdsX509TrustManager.checkServerTrusted(serverChain, "RSA"); + } + + @Test + public void checkClientTrusted_goodCert() + throws CertificateException, IOException, CertStoreException { + SdsTrustManagerFactory factory = + new SdsTrustManagerFactory( + TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(), /* certContext= */ null); + SdsX509TrustManager sdsX509TrustManager = (SdsX509TrustManager) factory.getTrustManagers()[0]; + X509Certificate[] clientChain = + CertificateUtils.toX509Certificates(TestUtils.loadCert(CLIENT_PEM_FILE)); + sdsX509TrustManager.checkClientTrusted(clientChain, "RSA"); + } + + @Test + public void checkServerTrusted_badCert_throwsException() + throws CertificateException, IOException, CertStoreException { + SdsTrustManagerFactory factory = + new SdsTrustManagerFactory( + TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(), /* certContext= */ null); + SdsX509TrustManager sdsX509TrustManager = (SdsX509TrustManager) factory.getTrustManagers()[0]; + X509Certificate[] serverChain = + CertificateUtils.toX509Certificates(TestUtils.loadCert(BAD_SERVER_PEM_FILE)); + try { + sdsX509TrustManager.checkServerTrusted(serverChain, "RSA"); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected) + .hasMessageThat() + .contains("unable to find valid certification path to requested target"); + } + } + + @Test + public void checkClientTrusted_badCert_throwsException() + throws CertificateException, IOException, CertStoreException { + SdsTrustManagerFactory factory = + new SdsTrustManagerFactory( + TestUtils.loadCert(CA_PEM_FILE).getAbsolutePath(), /* certContext= */ null); + SdsX509TrustManager sdsX509TrustManager = (SdsX509TrustManager) factory.getTrustManagers()[0]; + X509Certificate[] clientChain = + CertificateUtils.toX509Certificates(TestUtils.loadCert(BAD_CLIENT_PEM_FILE)); + try { + sdsX509TrustManager.checkClientTrusted(clientChain, "RSA"); + Assert.fail("no exception thrown"); + } catch (CertificateException expected) { + assertThat(expected) + .hasMessageThat() + .contains("unable to find valid certification path to requested target"); + } + } +}