Skip to content

Commit 658cbf6

Browse files
authoredJul 11, 2024··
util: Stabilize AdvancedTlsX509TrustManager
1 parent b181e49 commit 658cbf6

File tree

2 files changed

+249
-45
lines changed

2 files changed

+249
-45
lines changed
 

‎util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java

+87-45
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
package io.grpc.util;
1818

19-
import io.grpc.ExperimentalApi;
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
2021
import java.io.File;
2122
import java.io.FileInputStream;
2223
import java.io.IOException;
@@ -42,31 +43,36 @@
4243

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

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

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

5966
private AdvancedTlsX509TrustManager(Verification verification,
60-
SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier) throws CertificateException {
67+
SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier) {
6168
this.verification = verification;
6269
this.socketAndEnginePeerVerifier = socketAndEnginePeerVerifier;
6370
}
6471

6572
@Override
6673
public void checkClientTrusted(X509Certificate[] chain, String authType)
6774
throws CertificateException {
68-
throw new CertificateException(
69-
"Not enough information to validate peer. SSLEngine or Socket required.");
75+
throw new CertificateException(NOT_ENOUGH_INFO_MESSAGE);
7076
}
7177

7278
@Override
@@ -90,8 +96,7 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEng
9096
@Override
9197
public void checkServerTrusted(X509Certificate[] chain, String authType)
9298
throws CertificateException {
93-
throw new CertificateException(
94-
"Not enough information to validate peer. SSLEngine or Socket required.");
99+
throw new CertificateException(NOT_ENOUGH_INFO_MESSAGE);
95100
}
96101

97102
@Override
@@ -127,6 +132,7 @@ public void useSystemDefaultTrustCerts() throws CertificateException, KeyStoreEx
127132
*/
128133
public void updateTrustCredentials(X509Certificate[] trustCerts) throws IOException,
129134
GeneralSecurityException {
135+
checkNotNull(trustCerts, "trustCerts");
130136
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
131137
keyStore.load(null, null);
132138
int i = 1;
@@ -135,8 +141,7 @@ public void updateTrustCredentials(X509Certificate[] trustCerts) throws IOExcept
135141
keyStore.setCertificateEntry(alias, cert);
136142
i++;
137143
}
138-
X509ExtendedTrustManager newDelegateManager = createDelegateTrustManager(keyStore);
139-
this.delegateManager = newDelegateManager;
144+
this.delegateManager = createDelegateTrustManager(keyStore);
140145
}
141146

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

212216
/**
213217
* Schedules a {@code ScheduledExecutorService} to read trust certificates from a local file path
214-
* periodically, and update the cached trust certs if there is an update.
218+
* periodically, and updates the cached trust certs if there is an update. You must close the
219+
* returned Closeable before calling this method again or other update methods
220+
* ({@link AdvancedTlsX509TrustManager#useSystemDefaultTrustCerts()},
221+
* {@link AdvancedTlsX509TrustManager#updateTrustCredentials(X509Certificate[])},
222+
* {@link AdvancedTlsX509TrustManager#updateTrustCredentialsFromFile(File)}).
223+
* Before scheduling the task, the method synchronously reads and updates trust certificates once.
224+
* If the provided period is less than 1 minute, it is automatically adjusted to 1 minute.
215225
*
216226
* @param trustCertFile the file on disk holding the trust certificates
217227
* @param period the period between successive read-and-update executions
218228
* @param unit the time unit of the initialDelay and period parameters
219-
* @param executor the execute service we use to read and update the credentials
229+
* @param executor the executor service we use to read and update the credentials
220230
* @return an object that caller should close when the file refreshes are not needed
221231
*/
222232
public Closeable updateTrustCredentialsFromFile(File trustCertFile, long period, TimeUnit unit,
@@ -226,14 +236,17 @@ public Closeable updateTrustCredentialsFromFile(File trustCertFile, long period,
226236
throw new GeneralSecurityException(
227237
"Files were unmodified before their initial update. Probably a bug.");
228238
}
239+
if (checkNotNull(unit, "unit").toMinutes(period) < MINIMUM_REFRESH_PERIOD_IN_MINUTES) {
240+
log.log(Level.FINE,
241+
"Provided refresh period of {0} {1} is too small. Default value of {2} minute(s) "
242+
+ "will be used.", new Object[] {period, unit.name(), MINIMUM_REFRESH_PERIOD_IN_MINUTES});
243+
period = MINIMUM_REFRESH_PERIOD_IN_MINUTES;
244+
unit = TimeUnit.MINUTES;
245+
}
229246
final ScheduledFuture<?> future =
230-
executor.scheduleWithFixedDelay(
247+
checkNotNull(executor, "executor").scheduleWithFixedDelay(
231248
new LoadFilePathExecution(trustCertFile), period, period, unit);
232-
return new Closeable() {
233-
@Override public void close() {
234-
future.cancel(false);
235-
}
236-
};
249+
return () -> future.cancel(false);
237250
}
238251

239252
/**
@@ -264,13 +277,14 @@ public void run() {
264277
try {
265278
this.currentTime = readAndUpdate(this.file, this.currentTime);
266279
} catch (IOException | GeneralSecurityException e) {
267-
log.log(Level.SEVERE, "Failed refreshing trust CAs from file. Using previous CAs", e);
280+
log.log(Level.SEVERE, String.format("Failed refreshing trust CAs from file. Using "
281+
+ "previous CAs (file lastModified = %s)", file.lastModified()), e);
268282
}
269283
}
270284
}
271285

272286
/**
273-
* Reads the trust certificates specified in the path location, and update the key store if the
287+
* Reads the trust certificates specified in the path location, and updates the key store if the
274288
* modified time has changed since last read.
275289
*
276290
* @param trustCertFile the file on disk holding the trust certificates
@@ -279,7 +293,7 @@ public void run() {
279293
*/
280294
private long readAndUpdate(File trustCertFile, long oldTime)
281295
throws IOException, GeneralSecurityException {
282-
long newTime = trustCertFile.lastModified();
296+
long newTime = checkNotNull(trustCertFile, "trustCertFile").lastModified();
283297
if (newTime == oldTime) {
284298
return oldTime;
285299
}
@@ -303,27 +317,32 @@ public static Builder newBuilder() {
303317
return new Builder();
304318
}
305319

306-
// The verification mode when authenticating the peer certificate.
320+
/**
321+
* The verification mode when authenticating the peer certificate.
322+
*/
307323
public enum Verification {
308-
// This is the DEFAULT and RECOMMENDED mode for most applications.
309-
// Setting this on the client side will do the certificate and hostname verification, while
310-
// setting this on the server side will only do the certificate verification.
324+
/**
325+
* This is the DEFAULT and RECOMMENDED mode for most applications.
326+
* Setting this on the client side performs both certificate and hostname verification, while
327+
* setting it on the server side only performs certificate verification.
328+
*/
311329
CERTIFICATE_AND_HOST_NAME_VERIFICATION,
312-
// This SHOULD be chosen only when you know what the implication this will bring, and have a
313-
// basic understanding about TLS.
314-
// It SHOULD be accompanied with proper additional peer identity checks set through
315-
// {@code PeerVerifier}(nit: why this @code not working?). Failing to do so will leave
316-
// applications to MITM attack.
317-
// Also note that this will only take effect if the underlying SDK implementation invokes
318-
// checkClientTrusted/checkServerTrusted with the {@code SSLEngine} parameter while doing
319-
// verification.
320-
// Setting this on either side will only do the certificate verification.
330+
/**
331+
* DANGEROUS: Use trusted credentials to verify the certificate, but clients will not verify the
332+
* certificate is for the expected host. This setting is only appropriate when accompanied by
333+
* proper additional peer identity checks set through SslSocketAndEnginePeerVerifier. Failing to
334+
* do so will leave your applications vulnerable to MITM attacks.
335+
* This setting has the same behavior on server-side as CERTIFICATE_AND_HOST_NAME_VERIFICATION.
336+
*/
321337
CERTIFICATE_ONLY_VERIFICATION,
322-
// Setting is very DANGEROUS. Please try to avoid this in a real production environment, unless
323-
// you are a super advanced user intended to re-implement the whole verification logic on your
324-
// own. A secure verification might include:
325-
// 1. proper verification on the peer certificate chain
326-
// 2. proper checks on the identity of the peer certificate
338+
/**
339+
* DANGEROUS: This SHOULD be used by advanced user intended to implement the entire verification
340+
* logic themselves {@link SslSocketAndEnginePeerVerifier}) themselves. This includes: <br>
341+
* 1. Proper verification of the peer certificate chain <br>
342+
* 2. Proper checks of the identity of the peer certificate <br>
343+
* Failing to do so will leave your application without any TLS-related protection. Keep in mind
344+
* that any loaded trust certificates will be ignored when using this mode.
345+
*/
327346
INSECURELY_SKIP_ALL_VERIFICATION,
328347
}
329348

@@ -356,18 +375,41 @@ void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, SSL
356375
throws CertificateException;
357376
}
358377

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

361388
private Verification verification = Verification.CERTIFICATE_AND_HOST_NAME_VERIFICATION;
362389
private SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier;
363390

364391
private Builder() {}
365392

393+
/**
394+
* Sets {@link Verification}, mode when authenticating the peer certificate. By default, {@link
395+
* Verification#CERTIFICATE_AND_HOST_NAME_VERIFICATION} value is used.
396+
*
397+
* @param verification Verification mode used for the current AdvancedTlsX509TrustManager
398+
* @return Builder with set verification
399+
*/
366400
public Builder setVerification(Verification verification) {
367401
this.verification = verification;
368402
return this;
369403
}
370404

405+
/**
406+
* Sets {@link SslSocketAndEnginePeerVerifier}, which methods will be called in addition to
407+
* verifying certificates.
408+
*
409+
* @param verifier SslSocketAndEnginePeerVerifier used for the current
410+
* AdvancedTlsX509TrustManager
411+
* @return Builder with set verifier
412+
*/
371413
public Builder setSslSocketAndEnginePeerVerifier(SslSocketAndEnginePeerVerifier verifier) {
372414
this.socketAndEnginePeerVerifier = verifier;
373415
return this;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.util;
18+
19+
import static org.junit.Assert.assertArrayEquals;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertThrows;
23+
24+
import com.google.common.collect.Iterables;
25+
import io.grpc.internal.FakeClock;
26+
import io.grpc.internal.testing.TestUtils;
27+
import io.grpc.testing.TlsTesting;
28+
import java.io.File;
29+
import java.io.IOException;
30+
import java.net.Socket;
31+
import java.security.GeneralSecurityException;
32+
import java.security.cert.CertificateException;
33+
import java.security.cert.X509Certificate;
34+
import java.util.ArrayList;
35+
import java.util.List;
36+
import java.util.NoSuchElementException;
37+
import java.util.concurrent.ScheduledExecutorService;
38+
import java.util.concurrent.TimeUnit;
39+
import java.util.logging.Handler;
40+
import java.util.logging.Level;
41+
import java.util.logging.LogRecord;
42+
import java.util.logging.Logger;
43+
import org.junit.Before;
44+
import org.junit.Test;
45+
import org.junit.runner.RunWith;
46+
import org.junit.runners.JUnit4;
47+
48+
/** Unit tests for {@link AdvancedTlsX509TrustManager}. */
49+
@RunWith(JUnit4.class)
50+
public class AdvancedTlsX509TrustManagerTest {
51+
52+
private static final String CA_PEM_FILE = "ca.pem";
53+
private static final String SERVER_0_PEM_FILE = "server0.pem";
54+
private File caCertFile;
55+
private File serverCert0File;
56+
57+
private X509Certificate[] caCert;
58+
private X509Certificate[] serverCert0;
59+
60+
private ScheduledExecutorService executor;
61+
62+
@Before
63+
public void setUp() throws IOException, GeneralSecurityException {
64+
executor = new FakeClock().getScheduledExecutorService();
65+
caCertFile = TestUtils.loadCert(CA_PEM_FILE);
66+
caCert = CertificateUtils.getX509Certificates(TlsTesting.loadCert(CA_PEM_FILE));
67+
serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE);
68+
serverCert0 = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SERVER_0_PEM_FILE));
69+
}
70+
71+
@Test
72+
public void updateTrustCredentials_replacesIssuers() throws Exception {
73+
// Overall happy path checking of public API.
74+
AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build();
75+
trustManager.updateTrustCredentialsFromFile(serverCert0File);
76+
assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers());
77+
78+
trustManager.updateTrustCredentials(caCert);
79+
assertArrayEquals(caCert, trustManager.getAcceptedIssuers());
80+
81+
trustManager.updateTrustCredentialsFromFile(serverCert0File, 1, TimeUnit.MINUTES,
82+
executor);
83+
assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers());
84+
85+
trustManager.updateTrustCredentialsFromFile(serverCert0File);
86+
assertArrayEquals(serverCert0, trustManager.getAcceptedIssuers());
87+
}
88+
89+
@Test
90+
public void systemDefaultDelegateManagerInstantiation() throws Exception {
91+
AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build();
92+
trustManager.useSystemDefaultTrustCerts();
93+
CertificateException ce = assertThrows(CertificateException.class, () -> trustManager
94+
.checkServerTrusted(serverCert0, "RSA", new Socket()));
95+
assertEquals("socket is not a type of SSLSocket", ce.getMessage());
96+
}
97+
98+
@Test
99+
public void credentialSettingParameterValidity() throws Exception {
100+
// Checking edge cases of public API parameter setting.
101+
AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build();
102+
103+
NullPointerException npe = assertThrows(NullPointerException.class, () -> trustManager
104+
.updateTrustCredentials(null));
105+
assertEquals("trustCerts", npe.getMessage());
106+
107+
npe = assertThrows(NullPointerException.class, () -> trustManager
108+
.updateTrustCredentialsFromFile(null));
109+
assertEquals("trustCertFile", npe.getMessage());
110+
111+
npe = assertThrows(NullPointerException.class, () -> trustManager
112+
.updateTrustCredentialsFromFile(null, 1, null, null));
113+
assertEquals("trustCertFile", npe.getMessage());
114+
115+
npe = assertThrows(NullPointerException.class, () -> trustManager
116+
.updateTrustCredentialsFromFile(caCertFile, 1, null, null));
117+
assertEquals("unit", npe.getMessage());
118+
119+
npe = assertThrows(NullPointerException.class, () -> trustManager
120+
.updateTrustCredentialsFromFile(caCertFile, 1, TimeUnit.MINUTES, null));
121+
assertEquals("executor", npe.getMessage());
122+
123+
Logger log = Logger.getLogger(AdvancedTlsX509TrustManager.class.getName());
124+
TestHandler handler = new TestHandler();
125+
log.addHandler(handler);
126+
log.setUseParentHandlers(false);
127+
log.setLevel(Level.FINE);
128+
trustManager.updateTrustCredentialsFromFile(serverCert0File, -1, TimeUnit.SECONDS,
129+
executor);
130+
log.removeHandler(handler);
131+
try {
132+
LogRecord logRecord = Iterables.find(handler.getRecords(),
133+
record -> record.getMessage().contains("Default value of "));
134+
assertNotNull(logRecord);
135+
} catch (NoSuchElementException e) {
136+
throw new AssertionError("Log message related to setting default values not found");
137+
}
138+
}
139+
140+
141+
private static class TestHandler extends Handler {
142+
private final List<LogRecord> records = new ArrayList<>();
143+
144+
@Override
145+
public void publish(LogRecord record) {
146+
records.add(record);
147+
}
148+
149+
@Override
150+
public void flush() {
151+
}
152+
153+
@Override
154+
public void close() throws SecurityException {
155+
}
156+
157+
public List<LogRecord> getRecords() {
158+
return records;
159+
}
160+
}
161+
162+
}

0 commit comments

Comments
 (0)
Please sign in to comment.