Skip to content

Commit 981eb9c

Browse files
authored
replace KeyToolCertificateGenerator by CertificateBuilder from netty (kroxylicious#544)
* replace KeyToolCertificateGenerator by CertificateBuilder from netty Signed-off-by: Francisco Vila <[email protected]> * remove useless variable Signed-off-by: Francisco Vila <[email protected]> * sort imports Signed-off-by: Francisco Vila <[email protected]> * added suggested keystore manager Signed-off-by: Francisco Vila <[email protected]> * sort imports Signed-off-by: Francisco Vila <[email protected]> * reformat Signed-off-by: Francisco Vila <[email protected]> * add copyright header Signed-off-by: Francisco Vila <[email protected]> * refactor Signed-off-by: Francisco Vila <[email protected]> * remove password creation Signed-off-by: Francisco Vila <[email protected]> * throw exceptions Signed-off-by: Francisco Vila <[email protected]> * deleted interface, add new method to generate the certificate file and password Signed-off-by: Francisco Vila <[email protected]> * sort imports Signed-off-by: Francisco Vila <[email protected]> * update method doc Signed-off-by: Francisco Vila <[email protected]> * comments addressed Signed-off-by: Francisco Vila <[email protected]> * fix format Signed-off-by: Francisco Vila <[email protected]> * remove addDnsNames method Signed-off-by: Francisco Vila <[email protected]> * remove commented code Signed-off-by: Francisco Vila <[email protected]> --------- Signed-off-by: Francisco Vila <[email protected]>
1 parent 08d11a9 commit 981eb9c

File tree

5 files changed

+322
-0
lines changed

5 files changed

+322
-0
lines changed

impl/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@
131131
<groupId>org.bouncycastle</groupId>
132132
<artifactId>bcutil-jdk18on</artifactId>
133133
</dependency>
134+
<dependency>
135+
<groupId>io.netty</groupId>
136+
<artifactId>netty-pkitesting</artifactId>
137+
</dependency>
134138
</dependencies>
135139

136140

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package io.kroxylicious.testing.kafka.common;
7+
8+
import java.io.FileOutputStream;
9+
import java.io.IOException;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.Paths;
13+
import java.nio.file.attribute.FileAttribute;
14+
import java.nio.file.attribute.PosixFilePermission;
15+
import java.nio.file.attribute.PosixFilePermissions;
16+
import java.security.KeyStore;
17+
import java.security.KeyStoreException;
18+
import java.security.NoSuchAlgorithmException;
19+
import java.security.cert.CertificateException;
20+
import java.util.Set;
21+
import java.util.UUID;
22+
import java.util.concurrent.ConcurrentHashMap;
23+
import java.util.concurrent.ConcurrentMap;
24+
25+
import io.netty.pkitesting.CertificateBuilder;
26+
import io.netty.pkitesting.X509Bundle;
27+
28+
public class KeystoreManager {
29+
private final ConcurrentMap<Path, String> passwords = new ConcurrentHashMap<>();
30+
31+
/**
32+
* Creates a CertificateBuilder with the appropriate default values for kroxylicious test usage.
33+
* @param distinguishedName the distinguished name. See {@link KeystoreManager#buildDistinguishedName(String, String, String, String, String, String, String)}
34+
* for generating a distinguished name
35+
* @return The partially populated certificate builder.
36+
*/
37+
public CertificateBuilder newCertificateBuilder(String distinguishedName) {
38+
return new CertificateBuilder()
39+
.rsa2048()
40+
.subject(distinguishedName);
41+
}
42+
43+
/**
44+
* Builds and adds provided certificate builder as a self-signed certificate
45+
* @param certificateBuilder the builder configuring the certificate
46+
* @return the self-signed certificate.
47+
*/
48+
public X509Bundle createSelfSignedCertificate(CertificateBuilder certificateBuilder) throws Exception {
49+
return certificateBuilder.copy()
50+
.setIsCertificateAuthority(true)
51+
.buildSelfSigned();
52+
}
53+
54+
/**
55+
* Optional we don't need this today!
56+
* Builds and adds provided certificate builder as a certificate signed by the provided issuer.
57+
* @param certificateBuilder the builder configuring the certificate
58+
* @return the signed certificate.
59+
*/
60+
public X509Bundle createSignedCertificate(X509Bundle issuer, CertificateBuilder certificateBuilder) throws Exception {
61+
return certificateBuilder.copy()
62+
.buildIssuedBy(issuer);
63+
}
64+
65+
/**
66+
* Formats the provided fields into a RFC5280 compliant form
67+
* @param email the email
68+
* @param domain the domain
69+
* @param organizationUnit the organization unit
70+
* @param organization the organization
71+
* @param city the city
72+
* @param state the state
73+
* @param country the country
74+
* @return the distinguished name
75+
*/
76+
public String buildDistinguishedName(String email, String domain, String organizationUnit, String organization, String city, String state, String country) {
77+
return "CN=" + domain + ", OU=" + organizationUnit + ", O=" + organization + ", L=" + city + ", ST=" + state + ", C=" + country + ", EMAILADDRESS=" + email;
78+
}
79+
80+
/**
81+
* Gets password.
82+
*
83+
* @return the password
84+
*/
85+
public String getPassword(Path keystorePath) {
86+
// hyphen is removed to make our debugging easier: copy-pasting with hyphens not always copy the whole password
87+
return this.passwords.computeIfAbsent(keystorePath, path -> UUID.randomUUID().toString().replace("-", ""));
88+
}
89+
90+
/**
91+
* Generate a keystore at returned path. It contains both certificates, subject's (key certs) and issuer's (trust CA certs)
92+
* Use {@link KeystoreManager#getPassword(Path)} for getting the keystore password.
93+
*
94+
* @param bundle the bundle
95+
* @return the path of the generated certificate file
96+
* @throws KeyStoreException the key store exception
97+
* @throws IOException the io exception
98+
* @throws CertificateException the certificate exception
99+
* @throws NoSuchAlgorithmException the no such algorithm exception
100+
*/
101+
public Path generateCertificateFile(X509Bundle bundle) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
102+
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
103+
Path certsDirectory = Files.createTempDirectory("kroxylicious", attr);
104+
Path keyStoreFilePath = Paths.get(certsDirectory.toAbsolutePath().toString(), "keystore.jks");
105+
KeyStore keyStore = bundle.toKeyStore(getPassword(keyStoreFilePath).toCharArray());
106+
try (FileOutputStream stream = new FileOutputStream(keyStoreFilePath.toFile())) {
107+
keyStore.store(stream, getPassword(keyStoreFilePath).toCharArray());
108+
}
109+
keyStoreFilePath.toFile().deleteOnExit();
110+
certsDirectory.toFile().deleteOnExit();
111+
112+
return keyStoreFilePath;
113+
}
114+
}

impl/src/main/java/io/kroxylicious/testing/kafka/common/KeytoolCertificateGenerator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
/**
3232
* Used to configure and manage test certificates using the JDK's keytool.
3333
*/
34+
@Deprecated(since = "0.13.0")
3435
@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "Requires ability to write test key material to file-system.")
3536
public class KeytoolCertificateGenerator {
3637
private static final String PKCS12_KEYSTORE_TYPE = "PKCS12";
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.testing.kafka.common;
8+
9+
import java.nio.file.Path;
10+
import java.security.KeyStore;
11+
import java.security.KeyStoreException;
12+
import java.security.cert.X509Certificate;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
16+
import org.assertj.core.api.InstanceOfAssertFactories;
17+
import org.junit.jupiter.api.Test;
18+
19+
import io.netty.pkitesting.CertificateBuilder;
20+
import io.netty.pkitesting.X509Bundle;
21+
22+
import edu.umd.cs.findbugs.annotations.NonNull;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.junit.jupiter.api.Assertions.assertAll;
26+
27+
class KeystoreTest {
28+
private static final int ASN_GENERAL_NAME_IP_ADDRESS = 7;
29+
private static final int ASN_GENERAL_NAME_DNS = 2;
30+
31+
@Test
32+
void generateSelfSignedKeyStore() throws Exception {
33+
var keystore = new KeystoreManager();
34+
CertificateBuilder certificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", "localhost", "Dev",
35+
"Kroxylicious.io", null, null, "US"));
36+
X509Bundle bundle = keystore.createSelfSignedCertificate(certificateBuilder);
37+
38+
assertAll(() -> {
39+
assertThat(bundle.getCertificate()).isNotNull();
40+
assertThat(bundle.isSelfSigned()).isTrue();
41+
assertThat(bundle.isCertificateAuthority()).isTrue();
42+
});
43+
}
44+
45+
@Test
46+
void generateSignedKeyStore() throws Exception {
47+
var keystore = new KeystoreManager();
48+
CertificateBuilder issuerCertificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", "localhost", "TestCA",
49+
"Issuer", null, null, "US"));
50+
51+
CertificateBuilder certificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", "localhost", "Dev",
52+
"Kroxylicious.io", null, null, "US"));
53+
54+
X509Bundle issuer = keystore.createSelfSignedCertificate(issuerCertificateBuilder);
55+
X509Bundle signed = keystore.createSignedCertificate(issuer, certificateBuilder);
56+
57+
assertAll(() -> {
58+
assertThat(signed.getCertificate()).isNotNull();
59+
assertThat(signed.getCertificate().getIssuerX500Principal()).isEqualTo(issuer.getCertificate().getSubjectX500Principal());
60+
assertThat(signed.isSelfSigned()).isFalse();
61+
assertThat(signed.isCertificateAuthority()).isFalse();
62+
});
63+
}
64+
65+
@Test
66+
void generateKeyStoreWithIPDomain() throws Exception {
67+
String domainIP = "127.0.0.1";
68+
var keystore = new KeystoreManager();
69+
CertificateBuilder certificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", "localhost", "Dev",
70+
"Kroxylicious.io", null, null, "US"))
71+
.addSanIpAddress(domainIP);
72+
X509Bundle bundle = keystore.createSelfSignedCertificate(certificateBuilder);
73+
74+
List<?> expectedIp = List.of(KeystoreTest.ASN_GENERAL_NAME_IP_ADDRESS, domainIP);
75+
76+
assertThat(bundle.getCertificate())
77+
.asInstanceOf(InstanceOfAssertFactories.type(X509Certificate.class))
78+
.satisfies(c -> {
79+
var names = c.getSubjectAlternativeNames();
80+
assertThat(names)
81+
.singleElement()
82+
.isEqualTo(expectedIp);
83+
});
84+
}
85+
86+
@Test
87+
void generateKeyStoreWithDnsSAN() throws Exception {
88+
String domain = "localhost";
89+
var keystore = new KeystoreManager();
90+
CertificateBuilder certificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", "localhost", "Dev",
91+
"Kroxylicious.io", null, null, "US"))
92+
.addSanDnsName(domain);
93+
X509Bundle bundle = keystore.createSelfSignedCertificate(certificateBuilder);
94+
95+
List<?> expectedDns = List.of(KeystoreTest.ASN_GENERAL_NAME_DNS, domain);
96+
97+
assertThat(bundle.getCertificate())
98+
.asInstanceOf(InstanceOfAssertFactories.type(X509Certificate.class))
99+
.satisfies(c -> {
100+
var names = c.getSubjectAlternativeNames();
101+
assertThat(names)
102+
.singleElement()
103+
.isEqualTo(expectedDns);
104+
});
105+
}
106+
107+
@Test
108+
void generatesKeyStoreWithIPAndDnsSAN() throws Exception {
109+
String domain = "localhost";
110+
String domainIP = "127.0.0.1";
111+
var keystore = new KeystoreManager();
112+
CertificateBuilder certificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", "localhost", "Dev",
113+
"Kroxylicious.io", null, null, "US"))
114+
.addSanDnsName(domain)
115+
.addSanIpAddress(domainIP);
116+
117+
X509Bundle bundle = keystore.createSelfSignedCertificate(certificateBuilder);
118+
119+
List<?> expectedDns = List.of(KeystoreTest.ASN_GENERAL_NAME_DNS, domain);
120+
List<?> expectedIp = List.of(KeystoreTest.ASN_GENERAL_NAME_IP_ADDRESS, domainIP);
121+
122+
assertThat(bundle.getCertificate())
123+
.asInstanceOf(InstanceOfAssertFactories.type(X509Certificate.class))
124+
.satisfies(c -> {
125+
var names = c.getSubjectAlternativeNames();
126+
assertThat(names)
127+
.anyMatch(x -> x.equals(expectedDns))
128+
.anyMatch(x -> x.equals(expectedIp));
129+
});
130+
}
131+
132+
@Test
133+
void generateKeyStoreFile() throws Exception {
134+
String domain = "localhost";
135+
var keystore = new KeystoreManager();
136+
CertificateBuilder issuerCertificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", domain, "TestCA",
137+
"Issuer", null, null, "US"));
138+
139+
CertificateBuilder certificateBuilder = keystore.newCertificateBuilder(keystore.buildDistinguishedName("[email protected]", domain, "Dev",
140+
"Kroxylicious.io", null, null, "US"));
141+
142+
X509Bundle issuer = keystore.createSelfSignedCertificate(issuerCertificateBuilder);
143+
X509Bundle signed = keystore.createSignedCertificate(issuer, certificateBuilder);
144+
145+
Path keyStoreFilePath = keystore.generateCertificateFile(signed);
146+
String password = keystore.getPassword(keyStoreFilePath);
147+
assertThat(keyStoreFilePath.toFile()).exists();
148+
var ks = KeyStore.getInstance(keyStoreFilePath.toFile(), password.toCharArray());
149+
var keyAliases = keyAliasList(ks);
150+
assertThat(keyAliases).hasSize(1);
151+
var keyAlias = keyAliases.get(0);
152+
153+
var certAliases = certificateAliasList(ks);
154+
var certAlias = certAliases.get(0);
155+
assertAll(() -> {
156+
assertThat(certAliases).hasSize(1);
157+
assertThat(certAlias).isNotEqualTo(keyAlias);
158+
});
159+
160+
assertAll(() -> {
161+
assertThat(ks.getCertificate(keyAlias)).isEqualTo(signed.getCertificate());
162+
assertThat(ks.getCertificate(certAlias)).isEqualTo(issuer.getCertificate());
163+
assertThat(ks.getType()).isEqualTo(signed.toKeyStore(password.toCharArray()).getType());
164+
});
165+
}
166+
167+
@NonNull
168+
private List<String> aliasList(KeyStore ks) throws KeyStoreException {
169+
List<String> aliases = new ArrayList<>();
170+
ks.aliases().asIterator().forEachRemaining(aliases::add);
171+
return aliases;
172+
}
173+
174+
@NonNull
175+
private List<String> keyAliasList(KeyStore ks) throws KeyStoreException {
176+
return aliasList(ks).stream().filter(a -> {
177+
try {
178+
return ks.isKeyEntry(a);
179+
}
180+
catch (KeyStoreException e) {
181+
throw new RuntimeException(e);
182+
}
183+
}).toList();
184+
}
185+
186+
@NonNull
187+
private List<String> certificateAliasList(KeyStore ks) throws KeyStoreException {
188+
return aliasList(ks).stream().filter(a -> {
189+
try {
190+
return ks.isCertificateEntry(a);
191+
}
192+
catch (KeyStoreException e) {
193+
throw new RuntimeException(e);
194+
}
195+
}).toList();
196+
}
197+
}

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
<moby-names-generator.version>20.10.1-r0</moby-names-generator.version>
100100
<jacoco-maven-plugin.version>0.8.14</jacoco-maven-plugin.version>
101101
<bouncycastle.version>1.82</bouncycastle.version>
102+
<netty.version>4.2.7.Final</netty.version>
102103

103104
<!-- Avoids issues with ryuk when running with podman https://github.com/containers/podman/issues/7927#issuecomment-731525556 -->
104105
<testcontainers.ryuk.disabled>true</testcontainers.ryuk.disabled>
@@ -253,6 +254,11 @@
253254
<artifactId>bcutil-jdk18on</artifactId>
254255
<version>${bouncycastle.version}</version>
255256
</dependency>
257+
<dependency>
258+
<groupId>io.netty</groupId>
259+
<artifactId>netty-pkitesting</artifactId>
260+
<version>${netty.version}</version>
261+
</dependency>
256262
</dependencies>
257263
</dependencyManagement>
258264

0 commit comments

Comments
 (0)