Skip to content

Commit f67b259

Browse files
committed
Add X509Certificate generator for samples
Issue gh-1558
1 parent 682c1f9 commit f67b259

File tree

4 files changed

+343
-0
lines changed

4 files changed

+343
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
id "org.springframework.boot" version "3.2.2"
3+
id "io.spring.dependency-management" version "1.1.0"
4+
id "java"
5+
}
6+
7+
group = project.rootProject.group
8+
version = project.rootProject.version
9+
10+
java {
11+
sourceCompatibility = JavaVersion.VERSION_17
12+
}
13+
14+
repositories {
15+
mavenCentral()
16+
}
17+
18+
dependencies {
19+
implementation "org.springframework.boot:spring-boot-starter"
20+
implementation "org.bouncycastle:bcpkix-jdk18on:1.77"
21+
implementation "org.bouncycastle:bcprov-jdk18on:1.77"
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2020-2024 the original author or 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+
* https://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+
package sample;
17+
18+
import java.math.BigInteger;
19+
import java.security.KeyPair;
20+
import java.security.KeyPairGenerator;
21+
import java.security.PrivateKey;
22+
import java.security.PublicKey;
23+
import java.security.SecureRandom;
24+
import java.security.Security;
25+
import java.security.cert.X509Certificate;
26+
import java.security.spec.RSAKeyGenParameterSpec;
27+
import java.util.Calendar;
28+
import java.util.Date;
29+
30+
import javax.security.auth.x500.X500Principal;
31+
32+
import org.bouncycastle.asn1.x509.BasicConstraints;
33+
import org.bouncycastle.asn1.x509.Extension;
34+
import org.bouncycastle.asn1.x509.KeyUsage;
35+
import org.bouncycastle.cert.X509v3CertificateBuilder;
36+
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
37+
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
38+
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
39+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
40+
import org.bouncycastle.operator.ContentSigner;
41+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
42+
43+
/**
44+
* @author Joe Grandja
45+
* @since 1.3
46+
*/
47+
final class BouncyCastleUtils {
48+
private static final String SHA256_RSA_SIGNATURE_ALGORITHM = "SHA256withRSA";
49+
private static final Date DEFAULT_START_DATE;
50+
private static final Date DEFAULT_END_DATE;
51+
static final String BC_PROVIDER = "BC";
52+
53+
static {
54+
Security.addProvider(new BouncyCastleProvider());
55+
56+
// Setup default certificate start date to yesterday and end date for 1 year validity
57+
Calendar calendar = Calendar.getInstance();
58+
calendar.add(Calendar.DATE, -1);
59+
DEFAULT_START_DATE = calendar.getTime();
60+
calendar.add(Calendar.YEAR, 1);
61+
DEFAULT_END_DATE = calendar.getTime();
62+
}
63+
64+
private BouncyCastleUtils() {
65+
}
66+
67+
static KeyPair generateRSAKeyPair() {
68+
KeyPair keyPair;
69+
try {
70+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BC_PROVIDER);
71+
keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4));
72+
keyPair = keyPairGenerator.generateKeyPair();
73+
} catch (Exception ex) {
74+
throw new IllegalStateException(ex);
75+
}
76+
return keyPair;
77+
}
78+
79+
static X509Certificate createTrustAnchorCertificate(KeyPair keyPair, String distinguishedName) throws Exception {
80+
X500Principal subject = new X500Principal(distinguishedName);
81+
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
82+
83+
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
84+
subject,
85+
serialNum,
86+
DEFAULT_START_DATE,
87+
DEFAULT_END_DATE,
88+
subject,
89+
keyPair.getPublic());
90+
91+
// Add Extensions
92+
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
93+
certBuilder
94+
// A BasicConstraints to mark root certificate as CA certificate
95+
.addExtension(Extension.basicConstraints, true, new BasicConstraints(true))
96+
.addExtension(Extension.subjectKeyIdentifier, false,
97+
extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
98+
99+
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
100+
.setProvider(BC_PROVIDER).build(keyPair.getPrivate());
101+
102+
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
103+
104+
return converter.getCertificate(certBuilder.build(signer));
105+
}
106+
107+
static X509Certificate createCACertificate(X509Certificate signerCert, PrivateKey signerKey,
108+
PublicKey certKey, String distinguishedName) throws Exception {
109+
110+
X500Principal subject = new X500Principal(distinguishedName);
111+
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
112+
113+
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
114+
signerCert.getSubjectX500Principal(),
115+
serialNum,
116+
DEFAULT_START_DATE,
117+
DEFAULT_END_DATE,
118+
subject,
119+
certKey);
120+
121+
// Add Extensions
122+
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
123+
certBuilder
124+
// A BasicConstraints to mark as CA certificate and how many CA certificates can follow it in the chain
125+
// (with 0 meaning the chain ends with the next certificate in the chain).
126+
.addExtension(Extension.basicConstraints, true, new BasicConstraints(0))
127+
// KeyUsage specifies what the public key in the certificate can be used for.
128+
// In this case, it can be used for signing other certificates and/or
129+
// signing Certificate Revocation Lists (CRLs).
130+
.addExtension(Extension.keyUsage, true,
131+
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign))
132+
.addExtension(Extension.authorityKeyIdentifier, false,
133+
extensionUtils.createAuthorityKeyIdentifier(signerCert))
134+
.addExtension(Extension.subjectKeyIdentifier, false,
135+
extensionUtils.createSubjectKeyIdentifier(certKey));
136+
137+
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
138+
.setProvider(BC_PROVIDER).build(signerKey);
139+
140+
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
141+
142+
return converter.getCertificate(certBuilder.build(signer));
143+
}
144+
145+
static X509Certificate createEndEntityCertificate(X509Certificate signerCert, PrivateKey signerKey,
146+
PublicKey certKey, String distinguishedName) throws Exception {
147+
148+
X500Principal subject = new X500Principal(distinguishedName);
149+
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
150+
151+
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
152+
signerCert.getSubjectX500Principal(),
153+
serialNum,
154+
DEFAULT_START_DATE,
155+
DEFAULT_END_DATE,
156+
subject,
157+
certKey);
158+
159+
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
160+
certBuilder
161+
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
162+
.addExtension(Extension.keyUsage, true,
163+
new KeyUsage(KeyUsage.digitalSignature))
164+
.addExtension(Extension.authorityKeyIdentifier, false,
165+
extensionUtils.createAuthorityKeyIdentifier(signerCert))
166+
.addExtension(Extension.subjectKeyIdentifier, false,
167+
extensionUtils.createSubjectKeyIdentifier(certKey));
168+
169+
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
170+
.setProvider(BC_PROVIDER).build(signerKey);
171+
172+
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
173+
174+
return converter.getCertificate(certBuilder.build(signer));
175+
}
176+
177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2020-2024 the original author or 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+
* https://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+
package sample;
17+
18+
import java.io.FileOutputStream;
19+
import java.io.StringWriter;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
23+
import java.security.KeyPair;
24+
import java.security.KeyStore;
25+
import java.security.cert.Certificate;
26+
import java.security.cert.X509Certificate;
27+
28+
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
29+
30+
import org.springframework.boot.CommandLineRunner;
31+
import org.springframework.boot.SpringApplication;
32+
import org.springframework.boot.autoconfigure.SpringBootApplication;
33+
34+
import static sample.BouncyCastleUtils.BC_PROVIDER;
35+
36+
/**
37+
* @author Joe Grandja
38+
* @since 1.3
39+
*/
40+
@SpringBootApplication
41+
public class X509CertificateGeneratorApplication implements CommandLineRunner {
42+
43+
public static void main(String[] args) {
44+
SpringApplication.run(X509CertificateGeneratorApplication.class, args);
45+
}
46+
@Override
47+
public void run(String... args) throws Exception {
48+
String baseDistinguishedName = "OU=Spring Samples, O=Spring, C=US";
49+
50+
// Generate the Root certificate (Trust Anchor or most-trusted CA) and keystore file
51+
String commonName = "spring-samples-trusted-ca";
52+
String rootCommonName = commonName;
53+
String distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
54+
KeyPair rootKeyPair = BouncyCastleUtils.generateRSAKeyPair();
55+
X509Certificate rootCertificate = BouncyCastleUtils.createTrustAnchorCertificate(rootKeyPair, distinguishedName);
56+
writeCertificatePEMEncoded(rootCertificate, "./samples/x509-certificate-generator/generated/" + commonName + ".pem");
57+
createKeystoreFile(rootKeyPair, new Certificate[] {rootCertificate}, commonName,
58+
null, "./samples/x509-certificate-generator/generated/" + commonName + "-keystore.p12");
59+
TrustedCertificateHolder[] rootTrustedCertificate = { new TrustedCertificateHolder(rootCertificate, rootCommonName) };
60+
61+
// Generate the CA (intermediary) certificate and keystore file
62+
commonName = "spring-samples-ca";
63+
String caCommonName = commonName;
64+
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
65+
KeyPair caKeyPair = BouncyCastleUtils.generateRSAKeyPair();
66+
X509Certificate caCertificate = BouncyCastleUtils.createCACertificate(
67+
rootCertificate, rootKeyPair.getPrivate(), caKeyPair.getPublic(), distinguishedName);
68+
writeCertificatePEMEncoded(caCertificate, "./samples/x509-certificate-generator/generated/" + commonName + ".pem");
69+
createKeystoreFile(caKeyPair, new Certificate[] {caCertificate, rootCertificate}, commonName,
70+
rootTrustedCertificate, "./samples/x509-certificate-generator/generated/" + commonName + "-keystore.p12");
71+
TrustedCertificateHolder[] caTrustedCertificate = { new TrustedCertificateHolder(caCertificate, caCommonName) };
72+
73+
// Generate the certificate and keystore file for the demo-client sample
74+
commonName = "demo-client-sample";
75+
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
76+
KeyPair demoClientKeyPair = BouncyCastleUtils.generateRSAKeyPair();
77+
X509Certificate demoClientCertificate = BouncyCastleUtils.createEndEntityCertificate(
78+
caCertificate, caKeyPair.getPrivate(), demoClientKeyPair.getPublic(), distinguishedName);
79+
demoClientCertificate.verify(caCertificate.getPublicKey(), BC_PROVIDER);
80+
createKeystoreFile(demoClientKeyPair, new Certificate[] {demoClientCertificate, caCertificate, rootCertificate}, commonName,
81+
caTrustedCertificate, "./samples/demo-client/src/main/resources/keystore.p12");
82+
83+
// Generate the certificate and keystore file for the messages-resource sample
84+
commonName = "messages-resource-sample";
85+
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
86+
KeyPair messagesResourceKeyPair = BouncyCastleUtils.generateRSAKeyPair();
87+
X509Certificate messagesResourceCertificate = BouncyCastleUtils.createEndEntityCertificate(
88+
caCertificate, caKeyPair.getPrivate(), messagesResourceKeyPair.getPublic(), distinguishedName);
89+
messagesResourceCertificate.verify(caCertificate.getPublicKey(), BC_PROVIDER);
90+
createKeystoreFile(messagesResourceKeyPair, new Certificate[] {messagesResourceCertificate, caCertificate, rootCertificate}, commonName,
91+
caTrustedCertificate, "./samples/messages-resource/src/main/resources/keystore.p12");
92+
93+
// Generate the certificate and keystore file for the demo-authorizationserver sample
94+
commonName = "demo-authorizationserver-sample";
95+
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
96+
KeyPair demoAuthorizationServerKeyPair = BouncyCastleUtils.generateRSAKeyPair();
97+
X509Certificate demoAuthorizationServerCertificate = BouncyCastleUtils.createEndEntityCertificate(
98+
caCertificate, caKeyPair.getPrivate(), demoAuthorizationServerKeyPair.getPublic(), distinguishedName);
99+
demoAuthorizationServerCertificate.verify(caCertificate.getPublicKey(), BC_PROVIDER);
100+
createKeystoreFile(demoAuthorizationServerKeyPair, new Certificate[] {demoAuthorizationServerCertificate, caCertificate, rootCertificate}, commonName,
101+
caTrustedCertificate, "./samples/demo-authorizationserver/src/main/resources/keystore.p12");
102+
}
103+
104+
private static void createKeystoreFile(KeyPair keyPair, Certificate[] certificateChain, String alias,
105+
TrustedCertificateHolder[] trustedCertificates, String fileName) throws Exception {
106+
107+
KeyStore keyStore = KeyStore.getInstance("PKCS12", BC_PROVIDER);
108+
keyStore.load(null, null);
109+
keyStore.setKeyEntry(alias, keyPair.getPrivate(), "password".toCharArray(), certificateChain);
110+
if (trustedCertificates != null && trustedCertificates.length > 0) {
111+
for (TrustedCertificateHolder trustedCertificate : trustedCertificates) {
112+
keyStore.setCertificateEntry(trustedCertificate.alias, trustedCertificate.certificate);
113+
}
114+
}
115+
Path path = Paths.get(fileName);
116+
Path parent = path.getParent();
117+
if (parent != null && Files.notExists(parent)) {
118+
Files.createDirectories(parent);
119+
}
120+
FileOutputStream fos = new FileOutputStream(fileName);
121+
keyStore.store(fos, "password".toCharArray());
122+
}
123+
124+
private static void writeCertificatePEMEncoded(Certificate certificate, String fileName) throws Exception {
125+
StringWriter sw = new StringWriter();
126+
try (JcaPEMWriter jpw = new JcaPEMWriter(sw)) {
127+
jpw.writeObject(certificate);
128+
}
129+
String pem = sw.toString();
130+
Path path = Paths.get(fileName);
131+
Path parent = path.getParent();
132+
if (parent != null && Files.notExists(parent)) {
133+
Files.createDirectories(parent);
134+
}
135+
Files.write(path, pem.getBytes());
136+
}
137+
138+
private record TrustedCertificateHolder(Certificate certificate, String alias) {
139+
}
140+
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
spring:
2+
main:
3+
web-application-type: none

0 commit comments

Comments
 (0)