Add X509Certificate generator for samples

Issue gh-1558
This commit is contained in:
Joe Grandja
2024-03-15 10:00:50 -04:00
parent 682c1f936e
commit f67b259a1b
4 changed files with 343 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
plugins {
id "org.springframework.boot" version "3.2.2"
id "io.spring.dependency-management" version "1.1.0"
id "java"
}
group = project.rootProject.group
version = project.rootProject.version
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter"
implementation "org.bouncycastle:bcpkix-jdk18on:1.77"
implementation "org.bouncycastle:bcprov-jdk18on:1.77"
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.Calendar;
import java.util.Date;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
/**
* @author Joe Grandja
* @since 1.3
*/
final class BouncyCastleUtils {
private static final String SHA256_RSA_SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final Date DEFAULT_START_DATE;
private static final Date DEFAULT_END_DATE;
static final String BC_PROVIDER = "BC";
static {
Security.addProvider(new BouncyCastleProvider());
// Setup default certificate start date to yesterday and end date for 1 year validity
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -1);
DEFAULT_START_DATE = calendar.getTime();
calendar.add(Calendar.YEAR, 1);
DEFAULT_END_DATE = calendar.getTime();
}
private BouncyCastleUtils() {
}
static KeyPair generateRSAKeyPair() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BC_PROVIDER);
keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4));
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
static X509Certificate createTrustAnchorCertificate(KeyPair keyPair, String distinguishedName) throws Exception {
X500Principal subject = new X500Principal(distinguishedName);
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
subject,
serialNum,
DEFAULT_START_DATE,
DEFAULT_END_DATE,
subject,
keyPair.getPublic());
// Add Extensions
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
certBuilder
// A BasicConstraints to mark root certificate as CA certificate
.addExtension(Extension.basicConstraints, true, new BasicConstraints(true))
.addExtension(Extension.subjectKeyIdentifier, false,
extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
.setProvider(BC_PROVIDER).build(keyPair.getPrivate());
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
return converter.getCertificate(certBuilder.build(signer));
}
static X509Certificate createCACertificate(X509Certificate signerCert, PrivateKey signerKey,
PublicKey certKey, String distinguishedName) throws Exception {
X500Principal subject = new X500Principal(distinguishedName);
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
signerCert.getSubjectX500Principal(),
serialNum,
DEFAULT_START_DATE,
DEFAULT_END_DATE,
subject,
certKey);
// Add Extensions
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
certBuilder
// A BasicConstraints to mark as CA certificate and how many CA certificates can follow it in the chain
// (with 0 meaning the chain ends with the next certificate in the chain).
.addExtension(Extension.basicConstraints, true, new BasicConstraints(0))
// KeyUsage specifies what the public key in the certificate can be used for.
// In this case, it can be used for signing other certificates and/or
// signing Certificate Revocation Lists (CRLs).
.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign))
.addExtension(Extension.authorityKeyIdentifier, false,
extensionUtils.createAuthorityKeyIdentifier(signerCert))
.addExtension(Extension.subjectKeyIdentifier, false,
extensionUtils.createSubjectKeyIdentifier(certKey));
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
.setProvider(BC_PROVIDER).build(signerKey);
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
return converter.getCertificate(certBuilder.build(signer));
}
static X509Certificate createEndEntityCertificate(X509Certificate signerCert, PrivateKey signerKey,
PublicKey certKey, String distinguishedName) throws Exception {
X500Principal subject = new X500Principal(distinguishedName);
BigInteger serialNum = new BigInteger(Long.toString(new SecureRandom().nextLong()));
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
signerCert.getSubjectX500Principal(),
serialNum,
DEFAULT_START_DATE,
DEFAULT_END_DATE,
subject,
certKey);
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
certBuilder
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature))
.addExtension(Extension.authorityKeyIdentifier, false,
extensionUtils.createAuthorityKeyIdentifier(signerCert))
.addExtension(Extension.subjectKeyIdentifier, false,
extensionUtils.createSubjectKeyIdentifier(certKey));
ContentSigner signer = new JcaContentSignerBuilder(SHA256_RSA_SIGNATURE_ALGORITHM)
.setProvider(BC_PROVIDER).build(signerKey);
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC_PROVIDER);
return converter.getCertificate(certBuilder.build(signer));
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample;
import java.io.FileOutputStream;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static sample.BouncyCastleUtils.BC_PROVIDER;
/**
* @author Joe Grandja
* @since 1.3
*/
@SpringBootApplication
public class X509CertificateGeneratorApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(X509CertificateGeneratorApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
String baseDistinguishedName = "OU=Spring Samples, O=Spring, C=US";
// Generate the Root certificate (Trust Anchor or most-trusted CA) and keystore file
String commonName = "spring-samples-trusted-ca";
String rootCommonName = commonName;
String distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
KeyPair rootKeyPair = BouncyCastleUtils.generateRSAKeyPair();
X509Certificate rootCertificate = BouncyCastleUtils.createTrustAnchorCertificate(rootKeyPair, distinguishedName);
writeCertificatePEMEncoded(rootCertificate, "./samples/x509-certificate-generator/generated/" + commonName + ".pem");
createKeystoreFile(rootKeyPair, new Certificate[] {rootCertificate}, commonName,
null, "./samples/x509-certificate-generator/generated/" + commonName + "-keystore.p12");
TrustedCertificateHolder[] rootTrustedCertificate = { new TrustedCertificateHolder(rootCertificate, rootCommonName) };
// Generate the CA (intermediary) certificate and keystore file
commonName = "spring-samples-ca";
String caCommonName = commonName;
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
KeyPair caKeyPair = BouncyCastleUtils.generateRSAKeyPair();
X509Certificate caCertificate = BouncyCastleUtils.createCACertificate(
rootCertificate, rootKeyPair.getPrivate(), caKeyPair.getPublic(), distinguishedName);
writeCertificatePEMEncoded(caCertificate, "./samples/x509-certificate-generator/generated/" + commonName + ".pem");
createKeystoreFile(caKeyPair, new Certificate[] {caCertificate, rootCertificate}, commonName,
rootTrustedCertificate, "./samples/x509-certificate-generator/generated/" + commonName + "-keystore.p12");
TrustedCertificateHolder[] caTrustedCertificate = { new TrustedCertificateHolder(caCertificate, caCommonName) };
// Generate the certificate and keystore file for the demo-client sample
commonName = "demo-client-sample";
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
KeyPair demoClientKeyPair = BouncyCastleUtils.generateRSAKeyPair();
X509Certificate demoClientCertificate = BouncyCastleUtils.createEndEntityCertificate(
caCertificate, caKeyPair.getPrivate(), demoClientKeyPair.getPublic(), distinguishedName);
demoClientCertificate.verify(caCertificate.getPublicKey(), BC_PROVIDER);
createKeystoreFile(demoClientKeyPair, new Certificate[] {demoClientCertificate, caCertificate, rootCertificate}, commonName,
caTrustedCertificate, "./samples/demo-client/src/main/resources/keystore.p12");
// Generate the certificate and keystore file for the messages-resource sample
commonName = "messages-resource-sample";
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
KeyPair messagesResourceKeyPair = BouncyCastleUtils.generateRSAKeyPair();
X509Certificate messagesResourceCertificate = BouncyCastleUtils.createEndEntityCertificate(
caCertificate, caKeyPair.getPrivate(), messagesResourceKeyPair.getPublic(), distinguishedName);
messagesResourceCertificate.verify(caCertificate.getPublicKey(), BC_PROVIDER);
createKeystoreFile(messagesResourceKeyPair, new Certificate[] {messagesResourceCertificate, caCertificate, rootCertificate}, commonName,
caTrustedCertificate, "./samples/messages-resource/src/main/resources/keystore.p12");
// Generate the certificate and keystore file for the demo-authorizationserver sample
commonName = "demo-authorizationserver-sample";
distinguishedName = "CN=" + commonName + ", " + baseDistinguishedName;
KeyPair demoAuthorizationServerKeyPair = BouncyCastleUtils.generateRSAKeyPair();
X509Certificate demoAuthorizationServerCertificate = BouncyCastleUtils.createEndEntityCertificate(
caCertificate, caKeyPair.getPrivate(), demoAuthorizationServerKeyPair.getPublic(), distinguishedName);
demoAuthorizationServerCertificate.verify(caCertificate.getPublicKey(), BC_PROVIDER);
createKeystoreFile(demoAuthorizationServerKeyPair, new Certificate[] {demoAuthorizationServerCertificate, caCertificate, rootCertificate}, commonName,
caTrustedCertificate, "./samples/demo-authorizationserver/src/main/resources/keystore.p12");
}
private static void createKeystoreFile(KeyPair keyPair, Certificate[] certificateChain, String alias,
TrustedCertificateHolder[] trustedCertificates, String fileName) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12", BC_PROVIDER);
keyStore.load(null, null);
keyStore.setKeyEntry(alias, keyPair.getPrivate(), "password".toCharArray(), certificateChain);
if (trustedCertificates != null && trustedCertificates.length > 0) {
for (TrustedCertificateHolder trustedCertificate : trustedCertificates) {
keyStore.setCertificateEntry(trustedCertificate.alias, trustedCertificate.certificate);
}
}
Path path = Paths.get(fileName);
Path parent = path.getParent();
if (parent != null && Files.notExists(parent)) {
Files.createDirectories(parent);
}
FileOutputStream fos = new FileOutputStream(fileName);
keyStore.store(fos, "password".toCharArray());
}
private static void writeCertificatePEMEncoded(Certificate certificate, String fileName) throws Exception {
StringWriter sw = new StringWriter();
try (JcaPEMWriter jpw = new JcaPEMWriter(sw)) {
jpw.writeObject(certificate);
}
String pem = sw.toString();
Path path = Paths.get(fileName);
Path parent = path.getParent();
if (parent != null && Files.notExists(parent)) {
Files.createDirectories(parent);
}
Files.write(path, pem.getBytes());
}
private record TrustedCertificateHolder(Certificate certificate, String alias) {
}
}

View File

@@ -0,0 +1,3 @@
spring:
main:
web-application-type: none