Skip to content

Commit

Permalink
util: Stabilize AdvancedTlsX509TrustManager
Browse files Browse the repository at this point in the history
  • Loading branch information
erm-g committed Jul 11, 2024
1 parent b181e49 commit 658cbf6
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 45 deletions.
132 changes: 87 additions & 45 deletions util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package io.grpc.util;

import io.grpc.ExperimentalApi;
import static com.google.common.base.Preconditions.checkNotNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
Expand All @@ -42,31 +43,36 @@

/**
* AdvancedTlsX509TrustManager is an {@code X509ExtendedTrustManager} that allows users to configure
* advanced TLS features, such as root certificate reloading, peer cert custom verification, etc.
* For Android users: this class is only supported in API level 24 and above.
* advanced TLS features, such as root certificate reloading and peer cert custom verification.
* The basic instantiation pattern is
* <code>new Builder().build().useSystemDefaultTrustCerts();</code>
*
* <p>For Android users: this class is only supported in API level 24 and above.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8024")
@IgnoreJRERequirement
public final class AdvancedTlsX509TrustManager extends X509ExtendedTrustManager {
private static final Logger log = Logger.getLogger(AdvancedTlsX509TrustManager.class.getName());

// Minimum allowed period for refreshing files with credential information.
private static final int MINIMUM_REFRESH_PERIOD_IN_MINUTES = 1;
private static final String NOT_ENOUGH_INFO_MESSAGE =
"Not enough information to validate peer. SSLEngine or Socket required.";
private final Verification verification;
private final SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier;

// The delegated trust manager used to perform traditional certificate verification.
private volatile X509ExtendedTrustManager delegateManager = null;

private AdvancedTlsX509TrustManager(Verification verification,
SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier) throws CertificateException {
SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier) {
this.verification = verification;
this.socketAndEnginePeerVerifier = socketAndEnginePeerVerifier;
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
throw new CertificateException(
"Not enough information to validate peer. SSLEngine or Socket required.");
throw new CertificateException(NOT_ENOUGH_INFO_MESSAGE);
}

@Override
Expand All @@ -90,8 +96,7 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEng
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
throw new CertificateException(
"Not enough information to validate peer. SSLEngine or Socket required.");
throw new CertificateException(NOT_ENOUGH_INFO_MESSAGE);
}

@Override
Expand Down Expand Up @@ -127,6 +132,7 @@ public void useSystemDefaultTrustCerts() throws CertificateException, KeyStoreEx
*/
public void updateTrustCredentials(X509Certificate[] trustCerts) throws IOException,
GeneralSecurityException {
checkNotNull(trustCerts, "trustCerts");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
int i = 1;
Expand All @@ -135,8 +141,7 @@ public void updateTrustCredentials(X509Certificate[] trustCerts) throws IOExcept
keyStore.setCertificateEntry(alias, cert);
i++;
}
X509ExtendedTrustManager newDelegateManager = createDelegateTrustManager(keyStore);
this.delegateManager = newDelegateManager;
this.delegateManager = createDelegateTrustManager(keyStore);
}

private static X509ExtendedTrustManager createDelegateTrustManager(KeyStore keyStore)
Expand All @@ -148,9 +153,9 @@ private static X509ExtendedTrustManager createDelegateTrustManager(KeyStore keyS
TrustManager[] tms = tmf.getTrustManagers();
// Iterate over the returned trust managers, looking for an instance of X509TrustManager.
// If found, use that as the delegate trust manager.
for (int j = 0; j < tms.length; j++) {
if (tms[j] instanceof X509ExtendedTrustManager) {
delegateManager = (X509ExtendedTrustManager) tms[j];
for (TrustManager tm : tms) {
if (tm instanceof X509ExtendedTrustManager) {
delegateManager = (X509ExtendedTrustManager) tm;
break;
}
}
Expand All @@ -169,8 +174,7 @@ private void checkTrusted(X509Certificate[] chain, String authType, SSLEngine ss
"Want certificate verification but got null or empty certificates");
}
if (sslEngine == null && socket == null) {
throw new CertificateException(
"Not enough information to validate peer. SSLEngine or Socket required.");
throw new CertificateException(NOT_ENOUGH_INFO_MESSAGE);
}
if (this.verification != Verification.INSECURELY_SKIP_ALL_VERIFICATION) {
X509ExtendedTrustManager currentDelegateManager = this.delegateManager;
Expand Down Expand Up @@ -211,12 +215,18 @@ private void checkTrusted(X509Certificate[] chain, String authType, SSLEngine ss

/**
* Schedules a {@code ScheduledExecutorService} to read trust certificates from a local file path
* periodically, and update the cached trust certs if there is an update.
* periodically, and updates the cached trust certs if there is an update. You must close the
* returned Closeable before calling this method again or other update methods
* ({@link AdvancedTlsX509TrustManager#useSystemDefaultTrustCerts()},
* {@link AdvancedTlsX509TrustManager#updateTrustCredentials(X509Certificate[])},
* {@link AdvancedTlsX509TrustManager#updateTrustCredentialsFromFile(File)}).
* Before scheduling the task, the method synchronously reads and updates trust certificates once.
* If the provided period is less than 1 minute, it is automatically adjusted to 1 minute.
*
* @param trustCertFile the file on disk holding the trust certificates
* @param period the period between successive read-and-update executions
* @param unit the time unit of the initialDelay and period parameters
* @param executor the execute service we use to read and update the credentials
* @param executor the executor service we use to read and update the credentials
* @return an object that caller should close when the file refreshes are not needed
*/
public Closeable updateTrustCredentialsFromFile(File trustCertFile, long period, TimeUnit unit,
Expand All @@ -226,14 +236,17 @@ public Closeable updateTrustCredentialsFromFile(File trustCertFile, long period,
throw new GeneralSecurityException(
"Files were unmodified before their initial update. Probably a bug.");
}
if (checkNotNull(unit, "unit").toMinutes(period) < MINIMUM_REFRESH_PERIOD_IN_MINUTES) {
log.log(Level.FINE,
"Provided refresh period of {0} {1} is too small. Default value of {2} minute(s) "
+ "will be used.", new Object[] {period, unit.name(), MINIMUM_REFRESH_PERIOD_IN_MINUTES});
period = MINIMUM_REFRESH_PERIOD_IN_MINUTES;
unit = TimeUnit.MINUTES;
}
final ScheduledFuture<?> future =
executor.scheduleWithFixedDelay(
checkNotNull(executor, "executor").scheduleWithFixedDelay(
new LoadFilePathExecution(trustCertFile), period, period, unit);
return new Closeable() {
@Override public void close() {
future.cancel(false);
}
};
return () -> future.cancel(false);
}

/**
Expand Down Expand Up @@ -264,13 +277,14 @@ public void run() {
try {
this.currentTime = readAndUpdate(this.file, this.currentTime);
} catch (IOException | GeneralSecurityException e) {
log.log(Level.SEVERE, "Failed refreshing trust CAs from file. Using previous CAs", e);
log.log(Level.SEVERE, String.format("Failed refreshing trust CAs from file. Using "
+ "previous CAs (file lastModified = %s)", file.lastModified()), e);
}
}
}

/**
* Reads the trust certificates specified in the path location, and update the key store if the
* Reads the trust certificates specified in the path location, and updates the key store if the
* modified time has changed since last read.
*
* @param trustCertFile the file on disk holding the trust certificates
Expand All @@ -279,7 +293,7 @@ public void run() {
*/
private long readAndUpdate(File trustCertFile, long oldTime)
throws IOException, GeneralSecurityException {
long newTime = trustCertFile.lastModified();
long newTime = checkNotNull(trustCertFile, "trustCertFile").lastModified();
if (newTime == oldTime) {
return oldTime;
}
Expand All @@ -303,27 +317,32 @@ public static Builder newBuilder() {
return new Builder();
}

// The verification mode when authenticating the peer certificate.
/**
* The verification mode when authenticating the peer certificate.
*/
public enum Verification {
// This is the DEFAULT and RECOMMENDED mode for most applications.
// Setting this on the client side will do the certificate and hostname verification, while
// setting this on the server side will only do the certificate verification.
/**
* This is the DEFAULT and RECOMMENDED mode for most applications.
* Setting this on the client side performs both certificate and hostname verification, while
* setting it on the server side only performs certificate verification.
*/
CERTIFICATE_AND_HOST_NAME_VERIFICATION,
// This SHOULD be chosen only when you know what the implication this will bring, and have a
// basic understanding about TLS.
// It SHOULD be accompanied with proper additional peer identity checks set through
// {@code PeerVerifier}(nit: why this @code not working?). Failing to do so will leave
// applications to MITM attack.
// Also note that this will only take effect if the underlying SDK implementation invokes
// checkClientTrusted/checkServerTrusted with the {@code SSLEngine} parameter while doing
// verification.
// Setting this on either side will only do the certificate verification.
/**
* DANGEROUS: Use trusted credentials to verify the certificate, but clients will not verify the
* certificate is for the expected host. This setting is only appropriate when accompanied by
* proper additional peer identity checks set through SslSocketAndEnginePeerVerifier. Failing to
* do so will leave your applications vulnerable to MITM attacks.
* This setting has the same behavior on server-side as CERTIFICATE_AND_HOST_NAME_VERIFICATION.
*/
CERTIFICATE_ONLY_VERIFICATION,
// Setting is very DANGEROUS. Please try to avoid this in a real production environment, unless
// you are a super advanced user intended to re-implement the whole verification logic on your
// own. A secure verification might include:
// 1. proper verification on the peer certificate chain
// 2. proper checks on the identity of the peer certificate
/**
* DANGEROUS: This SHOULD be used by advanced user intended to implement the entire verification
* logic themselves {@link SslSocketAndEnginePeerVerifier}) themselves. This includes: <br>
* 1. Proper verification of the peer certificate chain <br>
* 2. Proper checks of the identity of the peer certificate <br>
* Failing to do so will leave your application without any TLS-related protection. Keep in mind
* that any loaded trust certificates will be ignored when using this mode.
*/
INSECURELY_SKIP_ALL_VERIFICATION,
}

Expand Down Expand Up @@ -356,18 +375,41 @@ void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, SSL
throws CertificateException;
}

/**
* Builds a new {@link AdvancedTlsX509TrustManager}. By default, no trust certificates are loaded
* after the build. To load them, use one of the following methods: {@link
* AdvancedTlsX509TrustManager#updateTrustCredentials(X509Certificate[])}, {@link
* AdvancedTlsX509TrustManager#updateTrustCredentialsFromFile(File, long, TimeUnit,
* ScheduledExecutorService)}, {@link AdvancedTlsX509TrustManager#updateTrustCredentialsFromFile
* (File, long, TimeUnit, ScheduledExecutorService)}.
*/
public static final class Builder {

private Verification verification = Verification.CERTIFICATE_AND_HOST_NAME_VERIFICATION;
private SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier;

private Builder() {}

/**
* Sets {@link Verification}, mode when authenticating the peer certificate. By default, {@link
* Verification#CERTIFICATE_AND_HOST_NAME_VERIFICATION} value is used.
*
* @param verification Verification mode used for the current AdvancedTlsX509TrustManager
* @return Builder with set verification
*/
public Builder setVerification(Verification verification) {
this.verification = verification;
return this;
}

/**
* Sets {@link SslSocketAndEnginePeerVerifier}, which methods will be called in addition to
* verifying certificates.
*
* @param verifier SslSocketAndEnginePeerVerifier used for the current
* AdvancedTlsX509TrustManager
* @return Builder with set verifier
*/
public Builder setSslSocketAndEnginePeerVerifier(SslSocketAndEnginePeerVerifier verifier) {
this.socketAndEnginePeerVerifier = verifier;
return this;
Expand Down
Loading

0 comments on commit 658cbf6

Please sign in to comment.