Browse Source

KAFKA-10279; Allow dynamic update of certificates with additional SubjectAltNames (#9044)

Reviewers: Manikumar Reddy <manikumar.reddy@gmail.com>
pull/9049/head
Rajini Sivaram 4 years ago committed by GitHub
parent
commit
6162a15326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      clients/src/main/java/org/apache/kafka/common/security/ssl/SslFactory.java
  2. 2
      clients/src/test/java/org/apache/kafka/common/network/CertStores.java
  3. 50
      clients/src/test/java/org/apache/kafka/common/network/SslTransportLayerTest.java
  4. 8
      clients/src/test/java/org/apache/kafka/test/TestSslUtils.java

35
clients/src/main/java/org/apache/kafka/common/security/ssl/SslFactory.java

@ -162,10 +162,8 @@ public class SslFactory implements Reconfigurable, Closeable { @@ -162,10 +162,8 @@ public class SslFactory implements Reconfigurable, Closeable {
throw new ConfigException("Cannot remove the SSL keystore from an existing listener for " +
"which a keystore was configured.");
}
if (!CertificateEntries.create(sslEngineFactory.keystore()).equals(
CertificateEntries.create(newSslEngineFactory.keystore()))) {
throw new ConfigException("Keystore DistinguishedName or SubjectAltNames do not match");
}
CertificateEntries.ensureCompatible(newSslEngineFactory.keystore(), sslEngineFactory.keystore());
}
if (sslEngineFactory.truststore() == null && newSslEngineFactory.truststore() != null) {
throw new ConfigException("Cannot add SSL truststore to an existing listener for which no " +
@ -238,6 +236,7 @@ public class SslFactory implements Reconfigurable, Closeable { @@ -238,6 +236,7 @@ public class SslFactory implements Reconfigurable, Closeable {
}
static class CertificateEntries {
private final String alias;
private final Principal subjectPrincipal;
private final Set<List<?>> subjectAltNames;
@ -248,12 +247,36 @@ public class SslFactory implements Reconfigurable, Closeable { @@ -248,12 +247,36 @@ public class SslFactory implements Reconfigurable, Closeable {
String alias = aliases.nextElement();
Certificate cert = keystore.getCertificate(alias);
if (cert instanceof X509Certificate)
entries.add(new CertificateEntries((X509Certificate) cert));
entries.add(new CertificateEntries(alias, (X509Certificate) cert));
}
return entries;
}
CertificateEntries(X509Certificate cert) throws GeneralSecurityException {
static void ensureCompatible(KeyStore newKeystore, KeyStore oldKeystore) throws GeneralSecurityException {
List<CertificateEntries> newEntries = CertificateEntries.create(newKeystore);
List<CertificateEntries> oldEntries = CertificateEntries.create(oldKeystore);
if (newEntries.size() != oldEntries.size()) {
throw new ConfigException(String.format("Keystore entries do not match, existing store contains %d entries, new store contains %d entries",
oldEntries.size(), newEntries.size()));
}
for (int i = 0; i < newEntries.size(); i++) {
CertificateEntries newEntry = newEntries.get(i);
CertificateEntries oldEntry = oldEntries.get(i);
if (!Objects.equals(newEntry.subjectPrincipal, oldEntry.subjectPrincipal)) {
throw new ConfigException(String.format("Keystore DistinguishedName does not match: " +
" existing={alias=%s, DN=%s}, new={alias=%s, DN=%s}",
oldEntry.alias, oldEntry.subjectPrincipal, newEntry.alias, newEntry.subjectPrincipal));
}
if (!newEntry.subjectAltNames.containsAll(oldEntry.subjectAltNames)) {
throw new ConfigException(String.format("Keystore SubjectAltNames do not match: " +
" existing={alias=%s, SAN=%s}, new={alias=%s, SAN=%s}",
oldEntry.alias, oldEntry.subjectAltNames, newEntry.alias, newEntry.subjectAltNames));
}
}
}
CertificateEntries(String alias, X509Certificate cert) throws GeneralSecurityException {
this.alias = alias;
this.subjectPrincipal = cert.getSubjectX500Principal();
Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
// use a set for comparison

2
clients/src/test/java/org/apache/kafka/common/network/CertStores.java

@ -46,7 +46,7 @@ public class CertStores { @@ -46,7 +46,7 @@ public class CertStores {
}
public CertStores(boolean server, String commonName, String sanHostName) throws Exception {
this(server, commonName, new TestSslUtils.CertificateBuilder().sanDnsName(sanHostName));
this(server, commonName, new TestSslUtils.CertificateBuilder().sanDnsNames(sanHostName));
}
public CertStores(boolean server, String commonName, InetAddress hostAddress) throws Exception {

50
clients/src/test/java/org/apache/kafka/common/network/SslTransportLayerTest.java

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
*/
package org.apache.kafka.common.network;
import java.io.File;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.config.internals.BrokerSecurityConfigs;
@ -1058,6 +1059,55 @@ public class SslTransportLayerTest { @@ -1058,6 +1059,55 @@ public class SslTransportLayerTest {
NetworkTestUtils.checkClientConnection(newClientSelector, "3", 100, 10);
}
@Test
public void testServerKeystoreDynamicUpdateWithNewSubjectAltName() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SSL;
TestSecurityConfig config = new TestSecurityConfig(sslServerConfigs);
ListenerName listenerName = ListenerName.forSecurityProtocol(securityProtocol);
ChannelBuilder serverChannelBuilder = ChannelBuilders.serverChannelBuilder(listenerName,
false, securityProtocol, config, null, null, time, new LogContext());
server = new NioEchoServer(listenerName, securityProtocol, config,
"localhost", serverChannelBuilder, null, time);
server.start();
InetSocketAddress addr = new InetSocketAddress("localhost", server.port());
Selector selector = createSelector(sslClientConfigs);
String node1 = "1";
selector.connect(node1, addr, BUFFER_SIZE, BUFFER_SIZE);
NetworkTestUtils.checkClientConnection(selector, node1, 100, 10);
selector.close();
TestSslUtils.CertificateBuilder certBuilder = new TestSslUtils.CertificateBuilder().sanDnsNames("localhost", "*.example.com");
File truststoreFile = new File((String) sslClientConfigs.get(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG));
Map<String, Object> newConfigs = TestSslUtils.createSslConfig(false, true, Mode.SERVER, truststoreFile, "server", "server", certBuilder);
Map<String, Object> newKeystoreConfigs = new HashMap<>();
for (String propName : CertStores.KEYSTORE_PROPS) {
newKeystoreConfigs.put(propName, newConfigs.get(propName));
}
ListenerReconfigurable reconfigurableBuilder = (ListenerReconfigurable) serverChannelBuilder;
reconfigurableBuilder.validateReconfiguration(newKeystoreConfigs);
reconfigurableBuilder.reconfigure(newKeystoreConfigs);
for (String propName : CertStores.TRUSTSTORE_PROPS) {
sslClientConfigs.put(propName, newConfigs.get(propName));
}
selector = createSelector(sslClientConfigs);
String node2 = "2";
selector.connect(node2, addr, BUFFER_SIZE, BUFFER_SIZE);
NetworkTestUtils.checkClientConnection(selector, node2, 100, 10);
TestSslUtils.CertificateBuilder invalidBuilder = new TestSslUtils.CertificateBuilder().sanDnsNames("localhost");
Map<String, Object> invalidConfig = TestSslUtils.createSslConfig(false, false, Mode.SERVER, truststoreFile, "server", "server", invalidBuilder);
Map<String, Object> invalidKeystoreConfigs = new HashMap<>();
for (String propName : CertStores.KEYSTORE_PROPS) {
invalidKeystoreConfigs.put(propName, invalidConfig.get(propName));
}
verifyInvalidReconfigure(reconfigurableBuilder, invalidKeystoreConfigs, "keystore without existing SubjectAltName");
String node3 = "3";
selector.connect(node3, addr, BUFFER_SIZE, BUFFER_SIZE);
NetworkTestUtils.checkClientConnection(selector, node3, 100, 10);
}
/**
* Tests reconfiguration of server truststore. Verifies that existing connections continue
* to work with old truststore and new connections work with new truststore.

8
clients/src/test/java/org/apache/kafka/test/TestSslUtils.java

@ -48,6 +48,7 @@ import org.apache.kafka.common.config.types.Password; @@ -48,6 +48,7 @@ import org.apache.kafka.common.config.types.Password;
import org.apache.kafka.common.security.auth.SslEngineFactory;
import org.apache.kafka.common.security.ssl.DefaultSslEngineFactory;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Extension;
@ -212,8 +213,11 @@ public class TestSslUtils { @@ -212,8 +213,11 @@ public class TestSslUtils {
this.algorithm = algorithm;
}
public CertificateBuilder sanDnsName(String hostName) throws IOException {
subjectAltName = new GeneralNames(new GeneralName(GeneralName.dNSName, hostName)).getEncoded();
public CertificateBuilder sanDnsNames(String... hostNames) throws IOException {
GeneralName[] altNames = new GeneralName[hostNames.length];
for (int i = 0; i < hostNames.length; i++)
altNames[i] = new GeneralName(GeneralName.dNSName, hostNames[i]);
subjectAltName = GeneralNames.getInstance(new DERSequence(altNames)).getEncoded();
return this;
}

Loading…
Cancel
Save