From f67b259a1b02000b4ace8ca0d4ee3aed5dfd247c Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 15 Mar 2024 10:00:50 -0400 Subject: [PATCH] Add X509Certificate generator for samples Issue gh-1558 --- .../samples-x509-certificate-generator.gradle | 22 +++ .../main/java/sample/BouncyCastleUtils.java | 177 ++++++++++++++++++ .../X509CertificateGeneratorApplication.java | 141 ++++++++++++++ .../src/main/resources/application.yml | 3 + 4 files changed, 343 insertions(+) create mode 100644 samples/x509-certificate-generator/samples-x509-certificate-generator.gradle create mode 100644 samples/x509-certificate-generator/src/main/java/sample/BouncyCastleUtils.java create mode 100644 samples/x509-certificate-generator/src/main/java/sample/X509CertificateGeneratorApplication.java create mode 100644 samples/x509-certificate-generator/src/main/resources/application.yml diff --git a/samples/x509-certificate-generator/samples-x509-certificate-generator.gradle b/samples/x509-certificate-generator/samples-x509-certificate-generator.gradle new file mode 100644 index 00000000..5fc2874e --- /dev/null +++ b/samples/x509-certificate-generator/samples-x509-certificate-generator.gradle @@ -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" +} diff --git a/samples/x509-certificate-generator/src/main/java/sample/BouncyCastleUtils.java b/samples/x509-certificate-generator/src/main/java/sample/BouncyCastleUtils.java new file mode 100644 index 00000000..f38e4c3a --- /dev/null +++ b/samples/x509-certificate-generator/src/main/java/sample/BouncyCastleUtils.java @@ -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)); + } + +} diff --git a/samples/x509-certificate-generator/src/main/java/sample/X509CertificateGeneratorApplication.java b/samples/x509-certificate-generator/src/main/java/sample/X509CertificateGeneratorApplication.java new file mode 100644 index 00000000..3e372a42 --- /dev/null +++ b/samples/x509-certificate-generator/src/main/java/sample/X509CertificateGeneratorApplication.java @@ -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) { + } + +} diff --git a/samples/x509-certificate-generator/src/main/resources/application.yml b/samples/x509-certificate-generator/src/main/resources/application.yml new file mode 100644 index 00000000..c594d756 --- /dev/null +++ b/samples/x509-certificate-generator/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + main: + web-application-type: none