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

grpc / grpc-java / #20229

27 Mar 2026 03:51PM UTC coverage: 88.72% (+0.02%) from 88.702%
#20229

push

github

web-flow
util: update AdvancedTlsX509KeyManager to support key alias for reloaded cert (#12686)

## Overview

Make the alias in `AdvancedTlsX509KeyManager` dynamic so it can be used
with Netty's `OpenSslCachingX509KeyManagerFactory` to update key
material after reload.

Fixes #12670
Fixes #12485

## Problem

When using `SslProvider.OPENSSL`, each TLS handshake must encode Java
key material into a native buffer consumed by OpenSSL, which can account
for ~8% of server CPU. Netty's `OpenSslCachingX509KeyManagerFactory`
avoids this by caching the encoded buffer keyed by alias — but the
previous implementation always returned `"default"`, so the factory
could never detect credential rotations and create a new cache entry on
cert reload.

## Details

- The alias is now set to `key-<N>` (e.g. `key-1`, `key-2`, ...) and
  incremented on every `updateIdentityCredentials` call, ensuring the
  same alias always maps to the same key material.
- One prior key value is kept to allow consistent handshaking during key
  changes.

35542 of 40061 relevant lines covered (88.72%)

0.89 hits per line

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

81.32
/../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 com.google.errorprone.annotations.InlineMe;
22
import io.grpc.ExperimentalApi;
23
import java.io.File;
24
import java.io.FileInputStream;
25
import java.io.IOException;
26
import java.net.Socket;
27
import java.security.GeneralSecurityException;
28
import java.security.Principal;
29
import java.security.PrivateKey;
30
import java.security.cert.X509Certificate;
31
import java.util.Arrays;
32
import java.util.concurrent.ScheduledExecutorService;
33
import java.util.concurrent.ScheduledFuture;
34
import java.util.concurrent.TimeUnit;
35
import java.util.concurrent.atomic.AtomicInteger;
36
import java.util.logging.Level;
37
import java.util.logging.Logger;
38
import javax.net.ssl.SSLEngine;
39
import javax.net.ssl.X509ExtendedKeyManager;
40

41
/**
42
 * AdvancedTlsX509KeyManager is an {@code X509ExtendedKeyManager} that allows users to configure
43
 * advanced TLS features, such as private key and certificate chain reloading.
44
 *
45
 * <p>The alias increments on every credential load (e.g. {@code "key-1"}, {@code "key-2"}, ...),
46
 * so the same alias always maps to the same key material. The previous alias is retained for one
47
 * rotation to allow in-progress handshakes to complete, ensuring alias-to-key-material consistency
48
 * across credential reloads.
49
 */
50
public final class AdvancedTlsX509KeyManager extends X509ExtendedKeyManager {
51
  private static final Logger log = Logger.getLogger(AdvancedTlsX509KeyManager.class.getName());
1✔
52
  // Minimum allowed period for refreshing files with credential information.
53
  private static final int MINIMUM_REFRESH_PERIOD_IN_MINUTES = 1;
54
  // Prefix for the key material alias; revision counter appended on each credential load.
55
  static final String ALIAS_PREFIX = "key-";
56

57
  private final AtomicInteger revision = new AtomicInteger(0);
1✔
58
  // Snapshot of current and previous KeyInfo; previous is retained for in-progress handshakes
59
  // after one rotation.
60
  private volatile KeyInfoSnapshot snapshot = new KeyInfoSnapshot(null, null);
1✔
61

62
  public AdvancedTlsX509KeyManager() {}
1✔
63

64
  private String alias() {
65
    KeyInfo curr = this.snapshot.current;
1✔
66
    return curr != null ? curr.alias : null;
1✔
67
  }
68

69
  @Override
70
  public PrivateKey getPrivateKey(String alias) {
71
    KeyInfoSnapshot snap = this.snapshot;
1✔
72
    if (snap.current != null && snap.current.alias.equals(alias)) {
1✔
73
      return snap.current.key;
1✔
74
    }
75
    if (snap.previous != null && snap.previous.alias.equals(alias)) {
1✔
76
      return snap.previous.key;
1✔
77
    }
78
    return null;
1✔
79
  }
80

81
  @Override
82
  public X509Certificate[] getCertificateChain(String alias) {
83
    KeyInfoSnapshot snap = this.snapshot;
1✔
84
    if (snap.current != null && snap.current.alias.equals(alias)) {
1✔
85
      return Arrays.copyOf(snap.current.certs, snap.current.certs.length);
1✔
86
    }
87
    if (snap.previous != null && snap.previous.alias.equals(alias)) {
1✔
88
      return Arrays.copyOf(snap.previous.certs, snap.previous.certs.length);
1✔
89
    }
90
    return null;
1✔
91
  }
92

93
  @Override
94
  public String[] getClientAliases(String keyType, Principal[] issuers) {
95
    String alias = alias();
1✔
96
    return alias != null ? new String[] {alias} : null;
1✔
97
  }
98

99
  @Override
100
  public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
101
    return alias();
1✔
102
  }
103

104
  @Override
105
  public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
106
    return alias();
1✔
107
  }
108

109
  @Override
110
  public String[] getServerAliases(String keyType, Principal[] issuers) {
111
    String alias = alias();
1✔
112
    return alias != null ? new String[] {alias} : null;
1✔
113
  }
114

115
  @Override
116
  public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
117
    return alias();
1✔
118
  }
119

120
  @Override
121
  public String chooseEngineServerAlias(String keyType, Principal[] issuers,
122
      SSLEngine engine) {
123
    return alias();
1✔
124
  }
125

126
  /**
127
   * Updates the current cached private key and cert chains.
128
   *
129
   * @param key  the private key that is going to be used
130
   * @param certs  the certificate chain that is going to be used
131
   * @deprecated Use {@link #updateIdentityCredentials(X509Certificate[], PrivateKey)}
132
   */
133
  @Deprecated
134
  @InlineMe(replacement = "this.updateIdentityCredentials(certs, key)")
135
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8024")
136
  public void updateIdentityCredentials(PrivateKey key, X509Certificate[] certs) {
137
    updateIdentityCredentials(certs, key);
×
138
  }
×
139

140
  /**
141
   * Updates the current cached private key and cert chains.
142
   *
143
   * @param certs  the certificate chain that is going to be used
144
   * @param key  the private key that is going to be used
145
   */
146
  public void updateIdentityCredentials(X509Certificate[] certs, PrivateKey key) {
147
    KeyInfo newInfo = new KeyInfo(checkNotNull(certs, "certs"), checkNotNull(key, "key"),
1✔
148
        ALIAS_PREFIX + revision.incrementAndGet());
1✔
149
    this.snapshot = new KeyInfoSnapshot(newInfo, this.snapshot.current);
1✔
150
  }
1✔
151

152
  /**
153
   * Schedules a {@code ScheduledExecutorService} to read certificate chains and private key from
154
   * the local file paths periodically, and update the cached identity credentials if they are both
155
   * updated. You must close the returned Closeable before calling this method again or other update
156
   * methods ({@link AdvancedTlsX509KeyManager#updateIdentityCredentials}, {@link
157
   * AdvancedTlsX509KeyManager#updateIdentityCredentials(File, File)}).
158
   * Before scheduling the task, the method synchronously executes {@code  readAndUpdate} once. The
159
   * minimum refresh period of 1 minute is enforced.
160
   *
161
   * @param certFile  the file on disk holding the certificate chain
162
   * @param keyFile  the file on disk holding the private key
163
   * @param period the period between successive read-and-update executions
164
   * @param unit the time unit of the initialDelay and period parameters
165
   * @param executor the executor service we use to read and update the credentials
166
   * @return an object that caller should close when the file refreshes are not needed
167
   */
168
  public Closeable updateIdentityCredentials(File certFile, File keyFile,
169
      long period, TimeUnit unit, ScheduledExecutorService executor) throws IOException,
170
      GeneralSecurityException {
171
    UpdateResult newResult = readAndUpdate(certFile, keyFile, 0, 0);
1✔
172
    if (!newResult.success) {
1✔
173
      throw new GeneralSecurityException(
×
174
          "Files were unmodified before their initial update. Probably a bug.");
175
    }
176
    if (checkNotNull(unit, "unit").toMinutes(period) < MINIMUM_REFRESH_PERIOD_IN_MINUTES) {
1✔
177
      log.log(Level.FINE,
1✔
178
          "Provided refresh period of {0} {1} is too small. Default value of {2} minute(s) "
179
          + "will be used.", new Object[] {period, unit.name(), MINIMUM_REFRESH_PERIOD_IN_MINUTES});
1✔
180
      period = MINIMUM_REFRESH_PERIOD_IN_MINUTES;
1✔
181
      unit = TimeUnit.MINUTES;
1✔
182
    }
183
    final ScheduledFuture<?> future =
1✔
184
        checkNotNull(executor, "executor").scheduleWithFixedDelay(
1✔
185
            new LoadFilePathExecution(certFile, keyFile), period, period, unit);
186
    return () -> future.cancel(false);
1✔
187
  }
188

189
  /**
190
   * Updates certificate chains and the private key from the local file paths.
191
   *
192
   * @param certFile  the file on disk holding the certificate chain
193
   * @param keyFile  the file on disk holding the private key
194
   */
195
  public void updateIdentityCredentials(File certFile, File keyFile) throws IOException,
196
      GeneralSecurityException {
197
    UpdateResult newResult = readAndUpdate(certFile, keyFile, 0, 0);
1✔
198
    if (!newResult.success) {
1✔
199
      throw new GeneralSecurityException(
×
200
          "Files were unmodified before their initial update. Probably a bug.");
201
    }
202
  }
1✔
203

204
  /**
205
   * Updates the private key and certificate chains from the local file paths.
206
   *
207
   * @param keyFile  the file on disk holding the private key
208
   * @param certFile  the file on disk holding the certificate chain
209
   * @deprecated Use {@link #updateIdentityCredentials(File, File)} instead.
210
   */
211
  @Deprecated
212
  @InlineMe(replacement = "this.updateIdentityCredentials(certFile, keyFile)")
213
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8024")
214
  public void updateIdentityCredentialsFromFile(File keyFile, File certFile) throws IOException,
215
      GeneralSecurityException {
216
    updateIdentityCredentials(certFile, keyFile);
×
217
  }
×
218

219
  /**
220
   * Schedules a {@code ScheduledExecutorService} to read private key and certificate chains from
221
   * the local file paths periodically, and update the cached identity credentials if they are both
222
   * updated. You must close the returned Closeable before calling this method again or other update
223
   * methods ({@link AdvancedTlsX509KeyManager#updateIdentityCredentials}, {@link
224
   * AdvancedTlsX509KeyManager#updateIdentityCredentials(File, File)}).
225
   * Before scheduling the task, the method synchronously executes {@code  readAndUpdate} once. The
226
   * minimum refresh period of 1 minute is enforced.
227
   *
228
   * @param keyFile  the file on disk holding the private key
229
   * @param certFile  the file on disk holding the certificate chain
230
   * @param period the period between successive read-and-update executions
231
   * @param unit the time unit of the initialDelay and period parameters
232
   * @param executor the executor service we use to read and update the credentials
233
   * @return an object that caller should close when the file refreshes are not needed
234
   * @deprecated Use {@link
235
   * #updateIdentityCredentials(File, File, long, TimeUnit, ScheduledExecutorService)} instead.
236
   */
237
  @Deprecated
238
  @InlineMe(replacement =
239
      "this.updateIdentityCredentials(certFile, keyFile, period, unit, executor)")
240
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/8024")
241
  public Closeable updateIdentityCredentialsFromFile(File keyFile, File certFile,
242
      long period, TimeUnit unit, ScheduledExecutorService executor) throws IOException,
243
      GeneralSecurityException {
244
    return updateIdentityCredentials(certFile, keyFile, period, unit, executor);
×
245
  }
246

247
  private static class KeyInfo {
248
    // The private key and the cert chain we will use to send to peers to prove our identity.
249
    final X509Certificate[] certs;
250
    final PrivateKey key;
251
    final String alias;
252

253
    public KeyInfo(X509Certificate[] certs, PrivateKey key, String alias) {
1✔
254
      this.certs = certs;
1✔
255
      this.key = key;
1✔
256
      this.alias = alias;
1✔
257
    }
1✔
258
  }
259

260
  private static class KeyInfoSnapshot {
261
    final KeyInfo current;
262
    final KeyInfo previous;
263

264
    KeyInfoSnapshot(KeyInfo current, KeyInfo previous) {
1✔
265
      this.current = current;
1✔
266
      this.previous = previous;
1✔
267
    }
1✔
268
  }
269

270
  private class LoadFilePathExecution implements Runnable {
271
    File keyFile;
272
    File certFile;
273
    long currentCertTime;
274
    long currentKeyTime;
275

276
    public LoadFilePathExecution(File certFile, File keyFile) {
1✔
277
      this.certFile = certFile;
1✔
278
      this.keyFile = keyFile;
1✔
279
      this.currentCertTime = 0;
1✔
280
      this.currentKeyTime = 0;
1✔
281
    }
1✔
282

283
    @Override
284
    public void run() {
285
      try {
286
        UpdateResult newResult = readAndUpdate(this.certFile, this.keyFile, this.currentKeyTime,
×
287
            this.currentCertTime);
288
        if (newResult.success) {
×
289
          this.currentCertTime = newResult.certTime;
×
290
          this.currentKeyTime = newResult.keyTime;
×
291
        }
292
      } catch (IOException | GeneralSecurityException e) {
×
293
        log.log(Level.SEVERE, String.format("Failed refreshing certificate and private key"
×
294
                + " chain from files. Using previous ones (certFile lastModified = %s, keyFile "
295
                + "lastModified = %s)", certFile.lastModified(), keyFile.lastModified()), e);
×
296
      }
×
297
    }
×
298
  }
299

300
  private static class UpdateResult {
301
    boolean success;
302
    long certTime;
303
    long keyTime;
304

305
    public UpdateResult(boolean success, long certTime, long keyTime) {
1✔
306
      this.success = success;
1✔
307
      this.certTime = certTime;
1✔
308
      this.keyTime = keyTime;
1✔
309
    }
1✔
310
  }
311

312
  /**
313
   * Reads the private key and certificates specified in the path locations. Updates {@code key} and
314
   * {@code cert} if both of their modified time changed since last read.
315
   *
316
   * @param certFile  the file on disk holding the certificate chain
317
   * @param keyFile  the file on disk holding the private key
318
   * @param oldKeyTime the time when the private key file is modified during last execution
319
   * @param oldCertTime the time when the certificate chain file is modified during last execution
320
   * @return the result of this update execution
321
   */
322
  private UpdateResult readAndUpdate(File certFile, File keyFile, long oldKeyTime, long oldCertTime)
323
      throws IOException, GeneralSecurityException {
324
    long newKeyTime = checkNotNull(keyFile, "keyFile").lastModified();
1✔
325
    long newCertTime = checkNotNull(certFile, "certFile").lastModified();
1✔
326
    // We only update when both the key and the certs are updated.
327
    if (newKeyTime != oldKeyTime && newCertTime != oldCertTime) {
1✔
328
      FileInputStream keyInputStream = new FileInputStream(keyFile);
1✔
329
      try {
330
        PrivateKey key = CertificateUtils.getPrivateKey(keyInputStream);
1✔
331
        FileInputStream certInputStream = new FileInputStream(certFile);
1✔
332
        try {
333
          X509Certificate[] certs = CertificateUtils.getX509Certificates(certInputStream);
1✔
334
          updateIdentityCredentials(certs, key);
1✔
335
          return new UpdateResult(true, newKeyTime, newCertTime);
1✔
336
        } finally {
337
          certInputStream.close();
1✔
338
        }
339
      } finally {
340
        keyInputStream.close();
1✔
341
      }
342
    }
343
    return new UpdateResult(false, oldKeyTime, oldCertTime);
×
344
  }
345

346
  /**
347
   * Mainly used to avoid throwing IO Exceptions in java.io.Closeable.
348
   */
349
  public interface Closeable extends java.io.Closeable {
350
    @Override
351
    void close();
352
  }
353
}
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