mirror of https://github.com/grpc/grpc-java.git
xds: implementation of SslContextSecretVolumeSecretProvider (#6251)
This commit is contained in:
parent
4fc41bd0e1
commit
8d1ee9cfc7
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
project(':grpc-testing-proto'),
|
project(':grpc-testing-proto'),
|
||||||
libraries.guava_testlib
|
libraries.guava_testlib
|
||||||
signature "org.codehaus.mojo.signature:java17:1.0@signature"
|
signature "org.codehaus.mojo.signature:java17:1.0@signature"
|
||||||
|
testRuntime libraries.netty_tcnative
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,16 @@ import java.util.concurrent.Executor;
|
||||||
public interface SecretProvider<T> {
|
public interface SecretProvider<T> {
|
||||||
|
|
||||||
interface Callback<T> {
|
interface Callback<T> {
|
||||||
|
/** Informs callee of new/updated secret. */
|
||||||
void updateSecret(T secret);
|
void updateSecret(T secret);
|
||||||
|
|
||||||
|
/** Informs callee of an exception that was generated. */
|
||||||
|
void onException(Throwable throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a callback on the given executor. The callback will run when secret becomes
|
* Registers a callback on the given executor. The callback will run when secret becomes available
|
||||||
* available or immediately if the result is already available.
|
* or immediately if the result is already available.
|
||||||
*/
|
*/
|
||||||
void addCallback(Callback<T> callback, Executor executor);
|
void addCallback(Callback<T> callback, Executor executor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext;
|
||||||
|
import io.envoyproxy.envoy.api.v2.auth.TlsCertificate;
|
||||||
|
import io.envoyproxy.envoy.api.v2.core.DataSource.SpecifierCase;
|
||||||
|
import io.grpc.netty.GrpcSslContexts;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An SslContext provider that uses file-based secrets (secret volume). Used for both server and
|
||||||
|
* client SslContexts
|
||||||
|
*/
|
||||||
|
final class SslContextSecretVolumeSecretProvider implements SecretProvider<SslContext> {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
Logger.getLogger(SslContextSecretVolumeSecretProvider.class.getName());
|
||||||
|
|
||||||
|
private final boolean server;
|
||||||
|
@Nullable private final String privateKey;
|
||||||
|
@Nullable private final String privateKeyPassword;
|
||||||
|
@Nullable private final String certificateChain;
|
||||||
|
@Nullable private final String trustedCa;
|
||||||
|
|
||||||
|
private SslContextSecretVolumeSecretProvider(
|
||||||
|
@Nullable String privateKey,
|
||||||
|
@Nullable String privateKeyPassword,
|
||||||
|
@Nullable String certificateChain,
|
||||||
|
@Nullable String trustedCa,
|
||||||
|
boolean server) {
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
this.privateKeyPassword = privateKeyPassword;
|
||||||
|
this.certificateChain = certificateChain;
|
||||||
|
this.trustedCa = trustedCa;
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
static CertificateValidationContext validateCertificateContext(
|
||||||
|
@Nullable CertificateValidationContext certContext, boolean optional) {
|
||||||
|
if (certContext == null || !certContext.hasTrustedCa()) {
|
||||||
|
checkArgument(optional, "certContext is required");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
checkArgument(
|
||||||
|
certContext.getTrustedCa().getSpecifierCase() == SpecifierCase.FILENAME,
|
||||||
|
"filename expected");
|
||||||
|
return certContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
static TlsCertificate validateTlsCertificate(
|
||||||
|
@Nullable TlsCertificate tlsCertificate, boolean optional) {
|
||||||
|
if (tlsCertificate == null) {
|
||||||
|
checkArgument(optional, "tlsCertificate is required");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (optional
|
||||||
|
&& (tlsCertificate.getPrivateKey().getSpecifierCase() == SpecifierCase.SPECIFIER_NOT_SET)
|
||||||
|
&& (tlsCertificate.getCertificateChain().getSpecifierCase()
|
||||||
|
== SpecifierCase.SPECIFIER_NOT_SET)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
checkArgument(
|
||||||
|
tlsCertificate.getPrivateKey().getSpecifierCase() == SpecifierCase.FILENAME,
|
||||||
|
"filename expected");
|
||||||
|
checkArgument(
|
||||||
|
tlsCertificate.getCertificateChain().getSpecifierCase() == SpecifierCase.FILENAME,
|
||||||
|
"filename expected");
|
||||||
|
return tlsCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SslContextSecretVolumeSecretProvider getProviderForServer(
|
||||||
|
TlsCertificate tlsCertificate, @Nullable CertificateValidationContext certContext) {
|
||||||
|
// first validate
|
||||||
|
validateTlsCertificate(tlsCertificate, /* optional= */ false);
|
||||||
|
// certContext exists in case of mTLS, else null for a server
|
||||||
|
if (certContext != null) {
|
||||||
|
certContext = validateCertificateContext(certContext, /* optional= */ true);
|
||||||
|
}
|
||||||
|
String privateKeyPassword =
|
||||||
|
tlsCertificate.hasPassword() ? tlsCertificate.getPassword().getInlineString() : null;
|
||||||
|
String trustedCa = certContext != null ? certContext.getTrustedCa().getFilename() : null;
|
||||||
|
return new SslContextSecretVolumeSecretProvider(
|
||||||
|
tlsCertificate.getPrivateKey().getFilename(),
|
||||||
|
privateKeyPassword,
|
||||||
|
tlsCertificate.getCertificateChain().getFilename(),
|
||||||
|
trustedCa,
|
||||||
|
/* server= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SslContextSecretVolumeSecretProvider getProviderForClient(
|
||||||
|
@Nullable TlsCertificate tlsCertificate, CertificateValidationContext certContext) {
|
||||||
|
// first validate
|
||||||
|
validateCertificateContext(certContext, /* optional= */ false);
|
||||||
|
// tlsCertificate exists in case of mTLS, else null for a client
|
||||||
|
if (tlsCertificate != null) {
|
||||||
|
tlsCertificate = validateTlsCertificate(tlsCertificate, /* optional= */ true);
|
||||||
|
}
|
||||||
|
String privateKey = null;
|
||||||
|
String privateKeyPassword = null;
|
||||||
|
String certificateChain = null;
|
||||||
|
if (tlsCertificate != null) {
|
||||||
|
privateKey = tlsCertificate.getPrivateKey().getFilename();
|
||||||
|
if (tlsCertificate.hasPassword()) {
|
||||||
|
privateKeyPassword = tlsCertificate.getPassword().getInlineString();
|
||||||
|
}
|
||||||
|
certificateChain = tlsCertificate.getCertificateChain().getFilename();
|
||||||
|
}
|
||||||
|
return new SslContextSecretVolumeSecretProvider(
|
||||||
|
privateKey,
|
||||||
|
privateKeyPassword,
|
||||||
|
certificateChain,
|
||||||
|
certContext.getTrustedCa().getFilename(),
|
||||||
|
/* server= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCallback(final Callback<SslContext> callback, Executor executor) {
|
||||||
|
checkNotNull(callback, "callback");
|
||||||
|
checkNotNull(executor, "executor");
|
||||||
|
executor.execute(
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// as per the contract we will read the current secrets on disk
|
||||||
|
// this involves I/O which can potentially block the executor or event loop
|
||||||
|
SslContext sslContext = null;
|
||||||
|
try {
|
||||||
|
sslContext = buildSslContextFromSecrets();
|
||||||
|
try {
|
||||||
|
callback.updateSecret(sslContext);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.log(Level.SEVERE, "Exception from callback.updateSecret", t);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.log(Level.SEVERE, "Exception from buildSslContextFromSecrets", e);
|
||||||
|
callback.onException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
SslContext buildSslContextFromSecrets() throws SSLException {
|
||||||
|
SslContextBuilder sslContextBuilder;
|
||||||
|
if (server) {
|
||||||
|
sslContextBuilder =
|
||||||
|
GrpcSslContexts.forServer(
|
||||||
|
new File(certificateChain), new File(privateKey), privateKeyPassword);
|
||||||
|
if (trustedCa != null) {
|
||||||
|
sslContextBuilder.trustManager(new File(trustedCa));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sslContextBuilder = GrpcSslContexts.forClient().trustManager(new File(trustedCa));
|
||||||
|
if (privateKey != null && certificateChain != null) {
|
||||||
|
sslContextBuilder.keyManager(
|
||||||
|
new File(certificateChain), new File(privateKey), privateKeyPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sslContextBuilder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,523 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import io.envoyproxy.envoy.api.v2.auth.CertificateValidationContext;
|
||||||
|
import io.envoyproxy.envoy.api.v2.auth.TlsCertificate;
|
||||||
|
import io.envoyproxy.envoy.api.v2.core.DataSource;
|
||||||
|
import io.grpc.internal.testing.TestUtils;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link SslContextSecretVolumeSecretProvider}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class SslContextSecretVolumeSecretProviderTest {
|
||||||
|
|
||||||
|
private static final String SERVER_1_PEM_FILE = "server1.pem";
|
||||||
|
private static final String SERVER_1_KEY_FILE = "server1.key";
|
||||||
|
private static final String CLIENT_PEM_FILE = "client.pem";
|
||||||
|
private static final String CLIENT_KEY_FILE = "client.key";
|
||||||
|
private static final String CA_PEM_FILE = "ca.pem";
|
||||||
|
|
||||||
|
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateCertificateContext_nullAndNotOptional_throwsException() {
|
||||||
|
// expect exception when certContext is null and not optional
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateCertificateContext(
|
||||||
|
/* certContext= */ null, /* optional= */ false);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("certContext is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateCertificateContext_missingTrustCa_throwsException() {
|
||||||
|
// expect exception when certContext has no CA and not optional
|
||||||
|
CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateCertificateContext(
|
||||||
|
certContext, /* optional= */ false);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("certContext is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateCertificateContext_nullAndOptional() {
|
||||||
|
// certContext argument can be null when optional
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
SslContextSecretVolumeSecretProvider.validateCertificateContext(
|
||||||
|
/* certContext= */ null, /* optional= */ true);
|
||||||
|
assertThat(certContext).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateCertificateContext_missingTrustCaOptional() {
|
||||||
|
// certContext argument can have missing CA when optional
|
||||||
|
CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance();
|
||||||
|
assertThat(
|
||||||
|
SslContextSecretVolumeSecretProvider.validateCertificateContext(
|
||||||
|
certContext, /* optional= */ true))
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateCertificateContext_inlineString_throwsException() {
|
||||||
|
// expect exception when certContext doesn't use filename (inline string)
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
CertificateValidationContext.newBuilder()
|
||||||
|
.setTrustedCa(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateCertificateContext(
|
||||||
|
certContext, /* optional= */ false);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateCertificateContext_filename() {
|
||||||
|
// validation succeeds and returns same instance when filename provided
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
CertificateValidationContext.newBuilder()
|
||||||
|
.setTrustedCa(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
assertThat(
|
||||||
|
SslContextSecretVolumeSecretProvider.validateCertificateContext(
|
||||||
|
certContext, /* optional= */ false))
|
||||||
|
.isSameInstanceAs(certContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_nullAndNotOptional_throwsException() {
|
||||||
|
// expect exception when tlsCertificate is null and not optional
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(
|
||||||
|
/* tlsCertificate= */ null, /* optional= */ false);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("tlsCertificate is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_nullOptional() {
|
||||||
|
assertThat(
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(
|
||||||
|
/* tlsCertificate= */ null, /* optional= */ true))
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_defaultInstance_returnsNull() {
|
||||||
|
// tlsCertificate is not null but has no value (default instance): expect null
|
||||||
|
TlsCertificate tlsCert = TlsCertificate.getDefaultInstance();
|
||||||
|
assertThat(
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(
|
||||||
|
tlsCert, /* optional= */ true))
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_missingCertChainNotOptional_throwsException() {
|
||||||
|
// expect exception when tlsCertificate has missing certChain and not optional
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ false);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_missingCertChainOptional_throwsException() {
|
||||||
|
// expect exception when tlsCertificate has missing certChain even if optional
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_missingPrivateKeyNotOptional_throwsException() {
|
||||||
|
// expect exception when tlsCertificate has missing private key and not optional
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ false);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_missingPrivateKeyOptional_throwsException() {
|
||||||
|
// expect exception when tlsCertificate has missing private key even if optional
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_optional_returnsSameInstance() {
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setFilename("foo"))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
assertThat(
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(
|
||||||
|
tlsCert, /* optional= */ true))
|
||||||
|
.isSameInstanceAs(tlsCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_notOptional_returnsSameInstance() {
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setFilename("foo"))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
assertThat(
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(
|
||||||
|
tlsCert, /* optional= */ false))
|
||||||
|
.isSameInstanceAs(tlsCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_certChainInlineString_throwsException() {
|
||||||
|
// expect exception when tlsCertificate has certChain as inline string
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateTlsCertificate_privateKeyInlineString_throwsException() {
|
||||||
|
// expect exception when tlsCertificate has private key as inline string
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.validateTlsCertificate(tlsCert, /* optional= */ true);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer_nullTlsCertificate_throwsException() {
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForServer(
|
||||||
|
/* tlsCertificate= */ null, /* certContext= */ null);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("tlsCertificate is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer_defaultTlsCertificate_throwsException() {
|
||||||
|
TlsCertificate tlsCert = TlsCertificate.getDefaultInstance();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForServer(tlsCert, /* certContext= */ null);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer_certContextWithInlineString_throwsException() {
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setFilename("foo"))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
CertificateValidationContext.newBuilder()
|
||||||
|
.setTrustedCa(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForServer(tlsCert, certContext);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_nullCertContext_throwsException() {
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForClient(
|
||||||
|
/* tlsCertificate= */ null, /* certContext= */ null);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("certContext is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_defaultCertContext_throwsException() {
|
||||||
|
CertificateValidationContext certContext = CertificateValidationContext.getDefaultInstance();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForClient(
|
||||||
|
/* tlsCertificate= */ null, certContext);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("certContext is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_certWithPrivateKeyInlineString_throwsException() {
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setFilename("foo"))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setInlineString("bar"))
|
||||||
|
.build();
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
CertificateValidationContext.newBuilder()
|
||||||
|
.setTrustedCa(DataSource.newBuilder().setFilename("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForClient(tlsCert, certContext);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_certWithCertChainInlineString_throwsException() {
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setInlineString("foo"))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setFilename("bar"))
|
||||||
|
.build();
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
CertificateValidationContext.newBuilder()
|
||||||
|
.setTrustedCa(DataSource.newBuilder().setFilename("foo"))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
SslContextSecretVolumeSecretProvider.getProviderForClient(tlsCert, certContext);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).isEqualTo("filename expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTempFileNameForResourcesFile(String resFile) throws IOException {
|
||||||
|
return TestUtils.loadCert(resFile).getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper method to build SslContextSecretVolumeSecretProvider from given files. */
|
||||||
|
private static SslContextSecretVolumeSecretProvider getSslContextSecretVolumeSecretProvider(
|
||||||
|
boolean server, String certChainFilename, String privateKeyFilename, String trustedCaFilename)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// get temp file for each file
|
||||||
|
if (certChainFilename != null) {
|
||||||
|
certChainFilename = getTempFileNameForResourcesFile(certChainFilename);
|
||||||
|
}
|
||||||
|
if (privateKeyFilename != null) {
|
||||||
|
privateKeyFilename = getTempFileNameForResourcesFile(privateKeyFilename);
|
||||||
|
}
|
||||||
|
if (trustedCaFilename != null) {
|
||||||
|
trustedCaFilename = getTempFileNameForResourcesFile(trustedCaFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
TlsCertificate tlsCert =
|
||||||
|
(certChainFilename == null && privateKeyFilename == null)
|
||||||
|
? null
|
||||||
|
: TlsCertificate.newBuilder()
|
||||||
|
.setCertificateChain(DataSource.newBuilder().setFilename(certChainFilename))
|
||||||
|
.setPrivateKey(DataSource.newBuilder().setFilename(privateKeyFilename))
|
||||||
|
.build();
|
||||||
|
CertificateValidationContext certContext =
|
||||||
|
trustedCaFilename == null
|
||||||
|
? null
|
||||||
|
: CertificateValidationContext.newBuilder()
|
||||||
|
.setTrustedCa(DataSource.newBuilder().setFilename(trustedCaFilename))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return server
|
||||||
|
? SslContextSecretVolumeSecretProvider.getProviderForServer(tlsCert, certContext)
|
||||||
|
: SslContextSecretVolumeSecretProvider.getProviderForClient(tlsCert, certContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to build SslContextSecretVolumeSecretProvider, call buildSslContext on it and
|
||||||
|
* check returned SslContext.
|
||||||
|
*/
|
||||||
|
private void sslContextForEitherWithBothCertAndTrust(
|
||||||
|
boolean server, String pemFile, String keyFile, String caFile) throws IOException {
|
||||||
|
SslContextSecretVolumeSecretProvider provider =
|
||||||
|
getSslContextSecretVolumeSecretProvider(server, pemFile, keyFile, caFile);
|
||||||
|
|
||||||
|
SslContext sslContext = provider.buildSslContextFromSecrets();
|
||||||
|
doChecksOnSslContext(server, sslContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doChecksOnSslContext(boolean server, SslContext sslContext) {
|
||||||
|
if (server) {
|
||||||
|
assertThat(sslContext.isServer()).isTrue();
|
||||||
|
} else {
|
||||||
|
assertThat(sslContext.isClient()).isTrue();
|
||||||
|
}
|
||||||
|
List<String> apnProtos = sslContext.applicationProtocolNegotiator().protocols();
|
||||||
|
assertThat(apnProtos).isNotNull();
|
||||||
|
assertThat(apnProtos).contains("h2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer() throws IOException {
|
||||||
|
sslContextForEitherWithBothCertAndTrust(
|
||||||
|
true, SERVER_1_PEM_FILE, SERVER_1_KEY_FILE, CA_PEM_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient() throws IOException {
|
||||||
|
sslContextForEitherWithBothCertAndTrust(false, CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer_onlyCert() throws IOException {
|
||||||
|
sslContextForEitherWithBothCertAndTrust(true, SERVER_1_PEM_FILE, SERVER_1_KEY_FILE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_onlyTrust() throws IOException {
|
||||||
|
sslContextForEitherWithBothCertAndTrust(false, null, null, CA_PEM_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer_badFile_throwsException() throws IOException {
|
||||||
|
try {
|
||||||
|
sslContextForEitherWithBothCertAndTrust(true, SERVER_1_PEM_FILE, SERVER_1_PEM_FILE, null);
|
||||||
|
Assert.fail("no exception thrown");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
assertThat(expected.getMessage()).contains("File does not contain valid private key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestCallback<T> implements SecretProvider.Callback<T> {
|
||||||
|
|
||||||
|
T updatedSecret;
|
||||||
|
Throwable updatedThrowable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateSecret(T secret) {
|
||||||
|
updatedSecret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
updatedThrowable = throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get the value thru directExecutor callback. Because of directExecutor this is
|
||||||
|
* a synchronous callback - so need to provide a listener.
|
||||||
|
*/
|
||||||
|
private static TestCallback<SslContext> getValueThruCallback(
|
||||||
|
SecretProvider<SslContext> provider) {
|
||||||
|
TestCallback<SslContext> testCallback = new TestCallback<>();
|
||||||
|
provider.addCallback(testCallback, MoreExecutors.directExecutor());
|
||||||
|
return testCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForServer_both_callsback() throws IOException {
|
||||||
|
SslContextSecretVolumeSecretProvider provider =
|
||||||
|
getSslContextSecretVolumeSecretProvider(
|
||||||
|
true, SERVER_1_PEM_FILE, SERVER_1_KEY_FILE, CA_PEM_FILE);
|
||||||
|
|
||||||
|
TestCallback<SslContext> testCallback = getValueThruCallback(provider);
|
||||||
|
doChecksOnSslContext(true, testCallback.updatedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_both_callsback() throws IOException {
|
||||||
|
SslContextSecretVolumeSecretProvider provider =
|
||||||
|
getSslContextSecretVolumeSecretProvider(
|
||||||
|
false, CLIENT_PEM_FILE, CLIENT_KEY_FILE, CA_PEM_FILE);
|
||||||
|
|
||||||
|
TestCallback<SslContext> testCallback = getValueThruCallback(provider);
|
||||||
|
doChecksOnSslContext(false, testCallback.updatedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getProviderForClient_both_callsback_setException() throws IOException {
|
||||||
|
SslContextSecretVolumeSecretProvider provider =
|
||||||
|
getSslContextSecretVolumeSecretProvider(
|
||||||
|
false, CLIENT_PEM_FILE, CLIENT_PEM_FILE, CA_PEM_FILE);
|
||||||
|
TestCallback<SslContext> testCallback = getValueThruCallback(provider);
|
||||||
|
assertThat(testCallback.updatedSecret).isNull();
|
||||||
|
assertThat(testCallback.updatedThrowable).isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThat(testCallback.updatedThrowable.getMessage())
|
||||||
|
.contains("File does not contain valid private key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,27 +58,16 @@ public class TlsCertificateSecretProviderMapTest {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
static class TestCallback implements
|
|
||||||
SecretProvider.Callback<TlsCertificateStore> {
|
|
||||||
|
|
||||||
TlsCertificateStore updatedSecret;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateSecret(TlsCertificateStore secret) {
|
|
||||||
updatedSecret = secret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to get the value thru directExecutore callback. Used by other classes.
|
* Helper method to get the value thru directExecutore callback. Used by other classes.
|
||||||
*/
|
*/
|
||||||
static TlsCertificateStore getValueThruCallback(SecretProvider<TlsCertificateStore> provider) {
|
static TlsCertificateStore getValueThruCallback(SecretProvider<TlsCertificateStore> provider) {
|
||||||
TestCallback testCallback = new TestCallback();
|
SslContextSecretVolumeSecretProviderTest.TestCallback<TlsCertificateStore> testCallback
|
||||||
|
= new SslContextSecretVolumeSecretProviderTest.TestCallback<>();
|
||||||
provider.addCallback(testCallback, MoreExecutors.directExecutor());
|
provider.addCallback(testCallback, MoreExecutors.directExecutor());
|
||||||
return testCallback.updatedSecret;
|
return testCallback.updatedSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void createTest() throws IOException, ExecutionException, InterruptedException {
|
public void createTest() throws IOException, ExecutionException, InterruptedException {
|
||||||
ConfigSource configSource = createFileAndConfigSource(temporaryFolder);
|
ConfigSource configSource = createFileAndConfigSource(temporaryFolder);
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ public class TlsCertificateSecretVolumeSecretProviderTest {
|
||||||
public void updateSecret(TlsCertificateStore secret) {
|
public void updateSecret(TlsCertificateStore secret) {
|
||||||
listenerRun = true;
|
listenerRun = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
|
||||||
|
}
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
assertThat(listenerRun).isTrue();
|
assertThat(listenerRun).isTrue();
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +136,11 @@ public class TlsCertificateSecretVolumeSecretProviderTest {
|
||||||
public void updateSecret(TlsCertificateStore secret) {
|
public void updateSecret(TlsCertificateStore secret) {
|
||||||
listenerRun = true;
|
listenerRun = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
|
||||||
|
}
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
assertThat(listenerRun).isFalse();
|
assertThat(listenerRun).isFalse();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue