• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #19256

30 May 2024 05:54PM UTC coverage: 88.313% (+0.02%) from 88.298%
#19256

push

github

web-flow
security: Stabilize AdvancedTlsX509KeyManager. (#11139)

* Clean up and de-experimentalization of KeyManager

* Unit tests for API validity.

32047 of 36288 relevant lines covered (88.31%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.1
/../util/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java
1
/*
2
 * Copyright 2021 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 com.google.common.base.Preconditions.checkNotNull;
20

21
import java.io.File;
22
import java.io.FileInputStream;
23
import java.io.IOException;
24
import java.net.Socket;
25
import java.security.GeneralSecurityException;
26
import java.security.Principal;
27
import java.security.PrivateKey;
28
import java.security.cert.X509Certificate;
29
import java.util.Arrays;
30
import java.util.concurrent.ScheduledExecutorService;
31
import java.util.concurrent.ScheduledFuture;
32
import java.util.concurrent.TimeUnit;
33
import java.util.logging.Level;
34
import java.util.logging.Logger;
35
import javax.net.ssl.SSLEngine;
36
import javax.net.ssl.X509ExtendedKeyManager;
37

38
/**
39
 * AdvancedTlsX509KeyManager is an {@code X509ExtendedKeyManager} that allows users to configure
40
 * advanced TLS features, such as private key and certificate chain reloading.
41
 */
42
public final class AdvancedTlsX509KeyManager extends X509ExtendedKeyManager {
1✔
43
  private static final Logger log = Logger.getLogger(AdvancedTlsX509KeyManager.class.getName());
1✔
44
  // Minimum allowed period for refreshing files with credential information.
45
  private static final int MINIMUM_REFRESH_PERIOD_IN_MINUTES = 1 ;
46
  // The credential information to be sent to peers to prove our identity.
47
  private volatile KeyInfo keyInfo;
48

49
  @Override
50
  public PrivateKey getPrivateKey(String alias) {
51
    if (alias.equals("default")) {
1✔
52
      return this.keyInfo.key;
1✔
53
    }
54
    return null;
×
55
  }
56

57
  @Override
58
  public X509Certificate[] getCertificateChain(String alias) {
59
    if (alias.equals("default")) {
1✔
60
      return Arrays.copyOf(this.keyInfo.certs, this.keyInfo.certs.length);
1✔
61
    }
62
    return null;
×
63
  }
64

65
  @Override
66
  public String[] getClientAliases(String keyType, Principal[] issuers) {
67
    return new String[] {"default"};
1✔
68
  }
69

70
  @Override
71
  public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
72
    return "default";
1✔
73
  }
74

75
  @Override
76
  public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
77
    return "default";
1✔
78
  }
79

80
  @Override
81
  public String[] getServerAliases(String keyType, Principal[] issuers) {
82
    return new String[] {"default"};
1✔
83
  }
84

85
  @Override
86
  public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
87
    return "default";
1✔
88
  }
89

90
  @Override
91
  public String chooseEngineServerAlias(String keyType, Principal[] issuers,
92
      SSLEngine engine) {
93
    return "default";
1✔
94
  }
95

96
  /**
97
   * Updates the current cached private key and cert chains.
98
   *
99
   * @param key  the private key that is going to be used
100
   * @param certs  the certificate chain that is going to be used
101
   */
102
  public void updateIdentityCredentials(PrivateKey key, X509Certificate[] certs) {
103
    this.keyInfo = new KeyInfo(checkNotNull(key, "key"), checkNotNull(certs, "certs"));
1✔
104
  }
1✔
105

106
  /**
107
   * Schedules a {@code ScheduledExecutorService} to read private key and certificate chains from
108
   * the local file paths periodically, and update the cached identity credentials if they are both
109
   * updated. You must close the returned Closeable before calling this method again or other update
110
   * methods ({@link AdvancedTlsX509KeyManager#updateIdentityCredentials}, {@link
111
   * AdvancedTlsX509KeyManager#updateIdentityCredentialsFromFile(File, File)}).
112
   * Before scheduling the task, the method synchronously executes {@code  readAndUpdate} once. The
113
   * minimum refresh period of 1 minute is enforced.
114
   *
115
   * @param keyFile  the file on disk holding the private key
116
   * @param certFile  the file on disk holding the certificate chain
117
   * @param period the period between successive read-and-update executions
118
   * @param unit the time unit of the initialDelay and period parameters
119
   * @param executor the execute service we use to read and update the credentials
120
   * @return an object that caller should close when the file refreshes are not needed
121
   */
122
  public Closeable updateIdentityCredentialsFromFile(File keyFile, File certFile,
123
      long period, TimeUnit unit, ScheduledExecutorService executor) throws IOException,
124
      GeneralSecurityException {
125
    UpdateResult newResult = readAndUpdate(keyFile, certFile, 0, 0);
1✔
126
    if (!newResult.success) {
1✔
127
      throw new GeneralSecurityException(
×
128
          "Files were unmodified before their initial update. Probably a bug.");
129
    }
130
    if (checkNotNull(unit, "unit").toMinutes(period) < MINIMUM_REFRESH_PERIOD_IN_MINUTES) {
1✔
131
      log.log(Level.FINE,
1✔
132
          "Provided refresh period of {0} {1} is too small. Default value of {2} minute(s) "
133
          + "will be used.", new Object[] {period, unit.name(), MINIMUM_REFRESH_PERIOD_IN_MINUTES});
1✔
134
      period = MINIMUM_REFRESH_PERIOD_IN_MINUTES;
1✔
135
      unit = TimeUnit.MINUTES;
1✔
136
    }
137
    final ScheduledFuture<?> future =
1✔
138
        checkNotNull(executor, "executor").scheduleWithFixedDelay(
1✔
139
            new LoadFilePathExecution(keyFile, certFile), period, period, unit);
140
    return () -> future.cancel(false);
1✔
141
  }
142

143
  /**
144
   * Updates the private key and certificate chains from the local file paths.
145
   *
146
   * @param keyFile  the file on disk holding the private key
147
   * @param certFile  the file on disk holding the certificate chain
148
   */
149
  public void updateIdentityCredentialsFromFile(File keyFile, File certFile) throws IOException,
150
      GeneralSecurityException {
151
    UpdateResult newResult = readAndUpdate(keyFile, certFile, 0, 0);
1✔
152
    if (!newResult.success) {
1✔
153
      throw new GeneralSecurityException(
×
154
          "Files were unmodified before their initial update. Probably a bug.");
155
    }
156
  }
1✔
157

158
  private static class KeyInfo {
159
    // The private key and the cert chain we will use to send to peers to prove our identity.
160
    final PrivateKey key;
161
    final X509Certificate[] certs;
162

163
    public KeyInfo(PrivateKey key, X509Certificate[] certs) {
1✔
164
      this.key = key;
1✔
165
      this.certs = certs;
1✔
166
    }
1✔
167
  }
168

169
  private class LoadFilePathExecution implements Runnable {
170
    File keyFile;
171
    File certFile;
172
    long currentKeyTime;
173
    long currentCertTime;
174

175
    public LoadFilePathExecution(File keyFile, File certFile) {
1✔
176
      this.keyFile = keyFile;
1✔
177
      this.certFile = certFile;
1✔
178
      this.currentKeyTime = 0;
1✔
179
      this.currentCertTime = 0;
1✔
180
    }
1✔
181

182
    @Override
183
    public void run() {
184
      try {
185
        UpdateResult newResult = readAndUpdate(this.keyFile, this.certFile, this.currentKeyTime,
×
186
            this.currentCertTime);
187
        if (newResult.success) {
×
188
          this.currentKeyTime = newResult.keyTime;
×
189
          this.currentCertTime = newResult.certTime;
×
190
        }
191
      } catch (IOException | GeneralSecurityException e) {
×
192
        log.log(Level.SEVERE, e, () -> String.format("Failed refreshing private key and certificate"
×
193
                + " chain from files. Using previous ones (keyFile lastModified = %s, certFile "
194
                + "lastModified = %s)", keyFile.lastModified(), certFile.lastModified()));
×
195
      }
×
196
    }
×
197
  }
198

199
  private static class UpdateResult {
200
    boolean success;
201
    long keyTime;
202
    long certTime;
203

204
    public UpdateResult(boolean success, long keyTime, long certTime) {
1✔
205
      this.success = success;
1✔
206
      this.keyTime = keyTime;
1✔
207
      this.certTime = certTime;
1✔
208
    }
1✔
209
  }
210

211
  /**
212
   * Reads the private key and certificates specified in the path locations. Updates {@code key} and
213
   * {@code cert} if both of their modified time changed since last read.
214
   *
215
   * @param keyFile  the file on disk holding the private key
216
   * @param certFile  the file on disk holding the certificate chain
217
   * @param oldKeyTime the time when the private key file is modified during last execution
218
   * @param oldCertTime the time when the certificate chain file is modified during last execution
219
   * @return the result of this update execution
220
   */
221
  private UpdateResult readAndUpdate(File keyFile, File certFile, long oldKeyTime, long oldCertTime)
222
      throws IOException, GeneralSecurityException {
223
    long newKeyTime = checkNotNull(keyFile, "keyFile").lastModified();
1✔
224
    long newCertTime = checkNotNull(certFile, "certFile").lastModified();
1✔
225
    // We only update when both the key and the certs are updated.
226
    if (newKeyTime != oldKeyTime && newCertTime != oldCertTime) {
1✔
227
      FileInputStream keyInputStream = new FileInputStream(keyFile);
1✔
228
      try {
229
        PrivateKey key = CertificateUtils.getPrivateKey(keyInputStream);
1✔
230
        FileInputStream certInputStream = new FileInputStream(certFile);
1✔
231
        try {
232
          X509Certificate[] certs = CertificateUtils.getX509Certificates(certInputStream);
1✔
233
          updateIdentityCredentials(key, certs);
1✔
234
          return new UpdateResult(true, newKeyTime, newCertTime);
1✔
235
        } finally {
236
          certInputStream.close();
1✔
237
        }
238
      } finally {
239
        keyInputStream.close();
1✔
240
      }
241
    }
242
    return new UpdateResult(false, oldKeyTime, oldCertTime);
×
243
  }
244

245
  /**
246
   * Mainly used to avoid throwing IO Exceptions in java.io.Closeable.
247
   */
248
  public interface Closeable extends java.io.Closeable {
249
    @Override
250
    void close();
251
  }
252
}
253

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc