Skip to content

Commit 43be82b

Browse files
committed
maintain the previous key after rotation
1 parent 9456de4 commit 43be82b

2 files changed

Lines changed: 43 additions & 83 deletions

File tree

util/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
* AdvancedTlsX509KeyManager is an {@code X509ExtendedKeyManager} that allows users to configure
4343
* advanced TLS features, such as private key and certificate chain reloading.
4444
*
45-
* <p>The key material alias increments on every credential load (e.g. {@code "key-1"},
46-
* {@code "key-2"}, ...), ensuring the same alias always maps to the same key material. This is
47-
* required by Netty's {@code OpenSslCachingX509KeyManagerFactory} to correctly cache key
48-
* material and create a new cache entry on cert reload.
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. This is required by Netty's
48+
* {@code OpenSslCachingX509KeyManagerFactory} to cache key material across cert reloads.
4949
*
5050
* <p>When using {@code SslProvider.OPENSSL}, wrap this key manager in Netty's
5151
* {@code OpenSslCachingX509KeyManagerFactory} to avoid per-handshake key material encoding
@@ -61,44 +61,39 @@ public final class AdvancedTlsX509KeyManager extends X509ExtendedKeyManager {
6161
static final String ALIAS_PREFIX = "key-";
6262

6363
private final AtomicInteger revision = new AtomicInteger(0);
64-
private final int revisionWarningThreshold;
65-
// The credential information to be sent to peers to prove our identity.
66-
private volatile KeyInfo keyInfo;
64+
// Snapshot of current and previous KeyInfo; previous is retained for in-progress handshakes
65+
// after one rotation.
66+
private volatile KeyInfoSnapshot snapshot = new KeyInfoSnapshot(null, null);
6767

68-
public AdvancedTlsX509KeyManager() {
69-
// Netty's default OpenSslCachingX509KeyManagerFactory maxCachedEntries.
70-
this(1024);
71-
}
72-
73-
/**
74-
* Creates a key manager with a custom revision warning threshold.
75-
* @param revisionWarningThreshold the number of credential loads after which a warning is logged.
76-
* Only relevant when using {@code SslProvider.OPENSSL} with
77-
* {@code OpenSslCachingX509KeyManagerFactory}.
78-
*/
79-
public AdvancedTlsX509KeyManager(int revisionWarningThreshold) {
80-
this.revisionWarningThreshold = revisionWarningThreshold;
81-
}
68+
public AdvancedTlsX509KeyManager() {}
8269

8370
private String alias() {
84-
KeyInfo info = this.keyInfo;
85-
if (info == null) {
86-
return null;
87-
}
88-
return info.alias;
71+
KeyInfo curr = this.snapshot.current;
72+
return curr != null ? curr.alias : null;
8973
}
9074

9175
@Override
9276
public PrivateKey getPrivateKey(String alias) {
93-
KeyInfo info = this.keyInfo;
94-
return info != null && info.alias.equals(alias) ? info.key : null;
77+
KeyInfoSnapshot snap = this.snapshot;
78+
if (snap.current != null && snap.current.alias.equals(alias)) {
79+
return snap.current.key;
80+
}
81+
if (snap.previous != null && snap.previous.alias.equals(alias)) {
82+
return snap.previous.key;
83+
}
84+
return null;
9585
}
9686

9787
@Override
9888
public X509Certificate[] getCertificateChain(String alias) {
99-
KeyInfo info = this.keyInfo;
100-
return info != null && info.alias.equals(alias)
101-
? Arrays.copyOf(info.certs, info.certs.length) : null;
89+
KeyInfoSnapshot snap = this.snapshot;
90+
if (snap.current != null && snap.current.alias.equals(alias)) {
91+
return Arrays.copyOf(snap.current.certs, snap.current.certs.length);
92+
}
93+
if (snap.previous != null && snap.previous.alias.equals(alias)) {
94+
return Arrays.copyOf(snap.previous.certs, snap.previous.certs.length);
95+
}
96+
return null;
10297
}
10398

10499
@Override
@@ -155,15 +150,9 @@ public void updateIdentityCredentials(PrivateKey key, X509Certificate[] certs) {
155150
* @param key the private key that is going to be used
156151
*/
157152
public void updateIdentityCredentials(X509Certificate[] certs, PrivateKey key) {
158-
// When using SslProvider.OPENSSL with OpenSslCachingX509KeyManagerFactory, its cache stops
159-
// accepting new aliases once maxCachedEntries is reached (default: 1024). Beyond this,
160-
// handshakes still succeed but per-handshake re-encoding overhead resumes.
161-
if (revision.get() >= revisionWarningThreshold) {
162-
log.warning("AdvancedTlsX509KeyManager: revision counter has reached "
163-
+ revisionWarningThreshold);
164-
}
165-
this.keyInfo = new KeyInfo(checkNotNull(certs, "certs"), checkNotNull(key, "key"),
153+
KeyInfo newInfo = new KeyInfo(checkNotNull(certs, "certs"), checkNotNull(key, "key"),
166154
ALIAS_PREFIX + revision.incrementAndGet());
155+
this.snapshot = new KeyInfoSnapshot(newInfo, this.snapshot.current);
167156
}
168157

169158
/**
@@ -262,7 +251,6 @@ public Closeable updateIdentityCredentialsFromFile(File keyFile, File certFile,
262251
}
263252

264253
private static class KeyInfo {
265-
// The private key and the cert chain we will use to send to peers to prove our identity.
266254
final X509Certificate[] certs;
267255
final PrivateKey key;
268256
final String alias;
@@ -274,6 +262,16 @@ public KeyInfo(X509Certificate[] certs, PrivateKey key, String alias) {
274262
}
275263
}
276264

265+
private static class KeyInfoSnapshot {
266+
final KeyInfo current;
267+
final KeyInfo previous;
268+
269+
KeyInfoSnapshot(KeyInfo current, KeyInfo previous) {
270+
this.current = current;
271+
this.previous = previous;
272+
}
273+
}
274+
277275
private class LoadFilePathExecution implements Runnable {
278276
File keyFile;
279277
File certFile;

util/src/test/java/io/grpc/util/AdvancedTlsX509KeyManagerTest.java

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import static org.junit.Assert.assertArrayEquals;
2020
import static org.junit.Assert.assertEquals;
21-
import static org.junit.Assert.assertFalse;
2221
import static org.junit.Assert.assertNull;
2322
import static org.junit.Assert.assertThrows;
2423
import static org.junit.Assert.assertTrue;
@@ -92,14 +91,17 @@ public void updateTrustCredentials_replacesIssuers() throws Exception {
9291
assertEquals(AdvancedTlsX509KeyManager.ALIAS_PREFIX + "2", alias2);
9392
assertEquals(clientKey0, serverKeyManager.getPrivateKey(alias2));
9493
assertArrayEquals(clientCert0, serverKeyManager.getCertificateChain(alias2));
95-
// Old alias no longer resolves — ensures alias stability contract is enforced.
96-
assertNull(serverKeyManager.getPrivateKey(alias1));
94+
// Previous alias still resolves — retained to allow in-progress handshakes to complete.
95+
assertEquals(serverKey0, serverKeyManager.getPrivateKey(alias1));
96+
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(alias1));
9797

9898
serverKeyManager.updateIdentityCredentials(serverCert0File, serverKey0File, 1,
9999
TimeUnit.MINUTES, executor);
100100
String alias3 = serverKeyManager.chooseEngineServerAlias(null, null, null);
101101
assertEquals(serverKey0, serverKeyManager.getPrivateKey(alias3));
102102
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(alias3));
103+
// alias1 is now two rotations back — no longer retained.
104+
assertNull(serverKeyManager.getPrivateKey(alias1));
103105

104106
serverKeyManager.updateIdentityCredentials(serverCert0, serverKey0);
105107
String alias4 = serverKeyManager.chooseEngineServerAlias(null, null, null);
@@ -135,46 +137,6 @@ public void allAliasMethods_agreeAfterCredentialLoad() throws Exception {
135137
assertArrayEquals(new String[]{expectedAlias}, keyManager.getServerAliases(null, null));
136138
}
137139

138-
@Test
139-
public void revisionWarningThreshold_logsWarningAtThreshold() throws Exception {
140-
Logger log = Logger.getLogger(AdvancedTlsX509KeyManager.class.getName());
141-
TestHandler handler = new TestHandler();
142-
log.addHandler(handler);
143-
log.setUseParentHandlers(false);
144-
log.setLevel(Level.ALL);
145-
146-
try {
147-
// Custom threshold: warning when revision reaches threshold.
148-
int threshold = 3;
149-
AdvancedTlsX509KeyManager customKeyManager = new AdvancedTlsX509KeyManager(threshold);
150-
for (int i = 0; i < threshold; i++) {
151-
customKeyManager.updateIdentityCredentials(serverCert0, serverKey0);
152-
}
153-
assertFalse(hasRevisionWarning(handler));
154-
customKeyManager.updateIdentityCredentials(serverCert0, serverKey0);
155-
assertTrue(hasRevisionWarning(handler));
156-
157-
// Key manager must still provide credentials correctly after soft threshold is exceeded.
158-
String alias = customKeyManager.chooseEngineServerAlias(null, null, null);
159-
assertEquals(serverKey0, customKeyManager.getPrivateKey(alias));
160-
assertArrayEquals(serverCert0, customKeyManager.getCertificateChain(alias));
161-
162-
// Further credential updates must also work.
163-
customKeyManager.updateIdentityCredentials(clientCert0File, clientKey0File);
164-
String newAlias = customKeyManager.chooseEngineServerAlias(null, null, null);
165-
assertEquals(clientKey0, customKeyManager.getPrivateKey(newAlias));
166-
assertArrayEquals(clientCert0, customKeyManager.getCertificateChain(newAlias));
167-
} finally {
168-
log.removeHandler(handler);
169-
}
170-
}
171-
172-
private static boolean hasRevisionWarning(TestHandler handler) {
173-
return handler.getRecords().stream()
174-
.anyMatch(r -> Level.WARNING.equals(r.getLevel())
175-
&& r.getMessage().contains("revision counter has reached"));
176-
}
177-
178140
@Test
179141
public void credentialSettingParameterValidity() throws Exception {
180142
// Checking edge cases of public API parameter setting.

0 commit comments

Comments
 (0)