Просмотр исходного кода

Add X509Certificate generator for samples

Issue gh-1558
Joe Grandja 1 год назад
Родитель
Сommit
f67b259a1b

+ 22 - 0
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"
+}

+ 177 - 0
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));
+	}
+
+}

+ 141 - 0
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) {
+	}
+
+}

+ 3 - 0
samples/x509-certificate-generator/src/main/resources/application.yml

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