Skip to content

Commit b16da37

Browse files
authored
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.
1 parent 0e39b29 commit b16da37

File tree

3 files changed

+114
-36
lines changed

3 files changed

+114
-36
lines changed

netty/src/test/java/io/grpc/netty/AdvancedTlsTest.java

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -436,16 +436,13 @@ public void onFileReloadingTrustManagerBadInitialContentTest() throws Exception
436436
}
437437

438438
@Test
439-
public void keyManagerAliasesTest() {
439+
public void keyManagerAliasesTest() throws Exception {
440440
AdvancedTlsX509KeyManager km = new AdvancedTlsX509KeyManager();
441-
assertArrayEquals(
442-
new String[] {"default"}, km.getClientAliases("", null));
443-
assertEquals(
444-
"default", km.chooseClientAlias(new String[] {"default"}, null, null));
445-
assertArrayEquals(
446-
new String[] {"default"}, km.getServerAliases("", null));
447-
assertEquals(
448-
"default", km.chooseServerAlias("default", null, null));
441+
km.updateIdentityCredentials(serverCert0, serverKey0);
442+
assertArrayEquals(new String[] {"key-1"}, km.getClientAliases("", null));
443+
assertEquals("key-1", km.chooseClientAlias(new String[] {"key-1"}, null, null));
444+
assertArrayEquals(new String[] {"key-1"}, km.getServerAliases("", null));
445+
assertEquals("key-1", km.chooseServerAlias("key-1", null, null));
449446
}
450447

451448
@Test

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

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.concurrent.ScheduledExecutorService;
3333
import java.util.concurrent.ScheduledFuture;
3434
import java.util.concurrent.TimeUnit;
35+
import java.util.concurrent.atomic.AtomicInteger;
3536
import java.util.logging.Level;
3637
import java.util.logging.Logger;
3738
import javax.net.ssl.SSLEngine;
@@ -40,59 +41,86 @@
4041
/**
4142
* AdvancedTlsX509KeyManager is an {@code X509ExtendedKeyManager} that allows users to configure
4243
* 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.
4349
*/
4450
public final class AdvancedTlsX509KeyManager extends X509ExtendedKeyManager {
4551
private static final Logger log = Logger.getLogger(AdvancedTlsX509KeyManager.class.getName());
4652
// Minimum allowed period for refreshing files with credential information.
47-
private static final int MINIMUM_REFRESH_PERIOD_IN_MINUTES = 1 ;
48-
// The credential information to be sent to peers to prove our identity.
49-
private volatile KeyInfo keyInfo;
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);
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);
61+
62+
public AdvancedTlsX509KeyManager() {}
63+
64+
private String alias() {
65+
KeyInfo curr = this.snapshot.current;
66+
return curr != null ? curr.alias : null;
67+
}
5068

5169
@Override
5270
public PrivateKey getPrivateKey(String alias) {
53-
if (alias.equals("default")) {
54-
return this.keyInfo.key;
71+
KeyInfoSnapshot snap = this.snapshot;
72+
if (snap.current != null && snap.current.alias.equals(alias)) {
73+
return snap.current.key;
74+
}
75+
if (snap.previous != null && snap.previous.alias.equals(alias)) {
76+
return snap.previous.key;
5577
}
5678
return null;
5779
}
5880

5981
@Override
6082
public X509Certificate[] getCertificateChain(String alias) {
61-
if (alias.equals("default")) {
62-
return Arrays.copyOf(this.keyInfo.certs, this.keyInfo.certs.length);
83+
KeyInfoSnapshot snap = this.snapshot;
84+
if (snap.current != null && snap.current.alias.equals(alias)) {
85+
return Arrays.copyOf(snap.current.certs, snap.current.certs.length);
86+
}
87+
if (snap.previous != null && snap.previous.alias.equals(alias)) {
88+
return Arrays.copyOf(snap.previous.certs, snap.previous.certs.length);
6389
}
6490
return null;
6591
}
6692

6793
@Override
6894
public String[] getClientAliases(String keyType, Principal[] issuers) {
69-
return new String[] {"default"};
95+
String alias = alias();
96+
return alias != null ? new String[] {alias} : null;
7097
}
7198

7299
@Override
73100
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
74-
return "default";
101+
return alias();
75102
}
76103

77104
@Override
78105
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
79-
return "default";
106+
return alias();
80107
}
81108

82109
@Override
83110
public String[] getServerAliases(String keyType, Principal[] issuers) {
84-
return new String[] {"default"};
111+
String alias = alias();
112+
return alias != null ? new String[] {alias} : null;
85113
}
86114

87115
@Override
88116
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
89-
return "default";
117+
return alias();
90118
}
91119

92120
@Override
93121
public String chooseEngineServerAlias(String keyType, Principal[] issuers,
94122
SSLEngine engine) {
95-
return "default";
123+
return alias();
96124
}
97125

98126
/**
@@ -116,7 +144,9 @@ public void updateIdentityCredentials(PrivateKey key, X509Certificate[] certs) {
116144
* @param key the private key that is going to be used
117145
*/
118146
public void updateIdentityCredentials(X509Certificate[] certs, PrivateKey key) {
119-
this.keyInfo = new KeyInfo(checkNotNull(certs, "certs"), checkNotNull(key, "key"));
147+
KeyInfo newInfo = new KeyInfo(checkNotNull(certs, "certs"), checkNotNull(key, "key"),
148+
ALIAS_PREFIX + revision.incrementAndGet());
149+
this.snapshot = new KeyInfoSnapshot(newInfo, this.snapshot.current);
120150
}
121151

122152
/**
@@ -218,10 +248,22 @@ private static class KeyInfo {
218248
// The private key and the cert chain we will use to send to peers to prove our identity.
219249
final X509Certificate[] certs;
220250
final PrivateKey key;
251+
final String alias;
221252

222-
public KeyInfo(X509Certificate[] certs, PrivateKey key) {
253+
public KeyInfo(X509Certificate[] certs, PrivateKey key, String alias) {
223254
this.certs = certs;
224255
this.key = key;
256+
this.alias = alias;
257+
}
258+
}
259+
260+
private static class KeyInfoSnapshot {
261+
final KeyInfo current;
262+
final KeyInfo previous;
263+
264+
KeyInfoSnapshot(KeyInfo current, KeyInfo previous) {
265+
this.current = current;
266+
this.previous = previous;
225267
}
226268
}
227269

@@ -309,4 +351,3 @@ public interface Closeable extends java.io.Closeable {
309351
void close();
310352
}
311353
}
312-

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

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

1919
import static org.junit.Assert.assertArrayEquals;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertNull;
2122
import static org.junit.Assert.assertThrows;
2223
import static org.junit.Assert.assertTrue;
2324
import static org.junit.Assert.fail;
@@ -48,7 +49,6 @@ public class AdvancedTlsX509KeyManagerTest {
4849
private static final String SERVER_0_PEM_FILE = "server0.pem";
4950
private static final String CLIENT_0_KEY_FILE = "client.key";
5051
private static final String CLIENT_0_PEM_FILE = "client.pem";
51-
private static final String ALIAS = "default";
5252

5353
private ScheduledExecutorService executor;
5454

@@ -79,22 +79,62 @@ public void setUp() throws Exception {
7979
public void updateTrustCredentials_replacesIssuers() throws Exception {
8080
// Overall happy path checking of public API.
8181
AdvancedTlsX509KeyManager serverKeyManager = new AdvancedTlsX509KeyManager();
82+
8283
serverKeyManager.updateIdentityCredentials(serverCert0, serverKey0);
83-
assertEquals(serverKey0, serverKeyManager.getPrivateKey(ALIAS));
84-
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(ALIAS));
84+
String alias1 = serverKeyManager.chooseEngineServerAlias(null, null, null);
85+
assertEquals(AdvancedTlsX509KeyManager.ALIAS_PREFIX + "1", alias1);
86+
assertEquals(serverKey0, serverKeyManager.getPrivateKey(alias1));
87+
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(alias1));
8588

8689
serverKeyManager.updateIdentityCredentials(clientCert0File, clientKey0File);
87-
assertEquals(clientKey0, serverKeyManager.getPrivateKey(ALIAS));
88-
assertArrayEquals(clientCert0, serverKeyManager.getCertificateChain(ALIAS));
89-
90-
serverKeyManager.updateIdentityCredentials(serverCert0File, serverKey0File,1,
90+
String alias2 = serverKeyManager.chooseEngineServerAlias(null, null, null);
91+
assertEquals(AdvancedTlsX509KeyManager.ALIAS_PREFIX + "2", alias2);
92+
assertEquals(clientKey0, serverKeyManager.getPrivateKey(alias2));
93+
assertArrayEquals(clientCert0, serverKeyManager.getCertificateChain(alias2));
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));
97+
98+
serverKeyManager.updateIdentityCredentials(serverCert0File, serverKey0File, 1,
9199
TimeUnit.MINUTES, executor);
92-
assertEquals(serverKey0, serverKeyManager.getPrivateKey(ALIAS));
93-
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(ALIAS));
100+
String alias3 = serverKeyManager.chooseEngineServerAlias(null, null, null);
101+
assertEquals(serverKey0, serverKeyManager.getPrivateKey(alias3));
102+
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(alias3));
103+
// alias1 is now two rotations back — no longer retained.
104+
assertNull(serverKeyManager.getPrivateKey(alias1));
94105

95106
serverKeyManager.updateIdentityCredentials(serverCert0, serverKey0);
96-
assertEquals(serverKey0, serverKeyManager.getPrivateKey(ALIAS));
97-
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(ALIAS));
107+
String alias4 = serverKeyManager.chooseEngineServerAlias(null, null, null);
108+
assertEquals(serverKey0, serverKeyManager.getPrivateKey(alias4));
109+
assertArrayEquals(serverCert0, serverKeyManager.getCertificateChain(alias4));
110+
}
111+
112+
@Test
113+
public void allAliasMethods_returnNullBeforeCredentialsLoaded() {
114+
AdvancedTlsX509KeyManager keyManager = new AdvancedTlsX509KeyManager();
115+
116+
assertNull(keyManager.chooseClientAlias(null, null, null));
117+
assertNull(keyManager.chooseServerAlias(null, null, null));
118+
assertNull(keyManager.chooseEngineClientAlias(null, null, null));
119+
assertNull(keyManager.chooseEngineServerAlias(null, null, null));
120+
assertNull(keyManager.getClientAliases(null, null));
121+
assertNull(keyManager.getServerAliases(null, null));
122+
assertNull(keyManager.getPrivateKey("key-1"));
123+
assertNull(keyManager.getCertificateChain("key-1"));
124+
}
125+
126+
@Test
127+
public void allAliasMethods_agreeAfterCredentialLoad() throws Exception {
128+
AdvancedTlsX509KeyManager keyManager = new AdvancedTlsX509KeyManager();
129+
keyManager.updateIdentityCredentials(serverCert0, serverKey0);
130+
131+
String expectedAlias = AdvancedTlsX509KeyManager.ALIAS_PREFIX + "1";
132+
assertEquals(expectedAlias, keyManager.chooseClientAlias(null, null, null));
133+
assertEquals(expectedAlias, keyManager.chooseServerAlias(null, null, null));
134+
assertEquals(expectedAlias, keyManager.chooseEngineClientAlias(null, null, null));
135+
assertEquals(expectedAlias, keyManager.chooseEngineServerAlias(null, null, null));
136+
assertArrayEquals(new String[]{expectedAlias}, keyManager.getClientAliases(null, null));
137+
assertArrayEquals(new String[]{expectedAlias}, keyManager.getServerAliases(null, null));
98138
}
99139

100140
@Test

0 commit comments

Comments
 (0)