/* * * Copyright ConsenSys AG. * * 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 * * http://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. * * SPDX-License-Identifier: Apache-2.0 * */ package org.hyperledger.besu.ethereum.api.tls; import static org.hyperledger.besu.crypto.SecureRandomProvider.createSecureRandom; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.time.Instant; import java.time.Period; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.tuweni.net.tls.TLS; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.cert.CertIOException; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; /** * Utility class to generate temporary self-signed certificates in PKCS12 format for testing * purposes using BouncyCastle APIs. The generated certificate supports SAN extension for multiple * DNS and IP addresses * * <p>Note: DO NOT USE IN PRODUCTION. The generated stores and files are marked to be deleted on JVM * exit. */ public final class SelfSignedP12Certificate { private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider(); private static final String alias = "test"; private static final boolean IS_CA = true; private static final String distinguishedName = "CN=localhost"; private static final List<String> sanHostNames = List.of("localhost"); private static final List<String> sanIpAddresses = List.of("127.0.0.1"); private static final char[] password = "changeit".toCharArray(); private final Certificate certificate; private final Path keyStore; private final Path trustStore; private SelfSignedP12Certificate( final Certificate certificate, final Path keyStore, final Path trustStore) { this.certificate = certificate; this.keyStore = keyStore; this.trustStore = trustStore; } public static SelfSignedP12Certificate create() { try { final KeyPair keyPair = generateKeyPair(); final Certificate certificate = generateSelfSignedCertificate(keyPair); final Path keyStore = createKeyStore(keyPair.getPrivate(), certificate); final Path trustStore = createTrustStore(certificate); return new SelfSignedP12Certificate(certificate, keyStore, trustStore); } catch (final IOException | GeneralSecurityException | OperatorCreationException e) { throw new RuntimeException("Error creating self signed certificates", e); } } public Certificate getCertificate() { return certificate; } public Path getKeyStoreFile() { return keyStore; } public Path getTrustStoreFile() { return trustStore; } public char[] getPassword() { return password; } public String getCommonName() { try { final X500Name subject = new X509CertificateHolder(certificate.getEncoded()).getSubject(); final RDN commonNameRdn = subject.getRDNs(BCStyle.CN)[0]; return IETFUtils.valueToString(commonNameRdn.getFirst().getValue()); } catch (final IOException | CertificateEncodingException e) { throw new RuntimeException("Error extracting common name from certificate", e); } } public String getCertificateHexFingerprint() { try { return TLS.certificateHexFingerprint(certificate); } catch (CertificateEncodingException e) { throw new RuntimeException("Error extracting certificate fingerprint", e); } } private static KeyPair generateKeyPair() throws GeneralSecurityException { final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048, createSecureRandom()); return keyPairGenerator.generateKeyPair(); } @SuppressWarnings("JdkObsolete") // JcaX509v3CertificateBuilder requires java.util.Date. private static Certificate generateSelfSignedCertificate(final KeyPair keyPair) throws CertIOException, GeneralSecurityException, OperatorCreationException { final X500Name issuer = new X500Name(distinguishedName); final X500Name subject = new X500Name(distinguishedName); final BigInteger serialNumber = new BigInteger(String.valueOf(Instant.now().toEpochMilli())); final X509v3CertificateBuilder v3CertificateBuilder = new JcaX509v3CertificateBuilder( issuer, serialNumber, Date.from(Instant.now()), Date.from(Instant.now().plus(Period.ofDays(90))), subject, keyPair.getPublic()); // extensions v3CertificateBuilder.addExtension( Extension.basicConstraints, true, new BasicConstraints(IS_CA)); v3CertificateBuilder.addExtension( Extension.subjectAlternativeName, false, getSubjectAlternativeNames()); final ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); return new JcaX509CertificateConverter() .setProvider(BOUNCY_CASTLE_PROVIDER) .getCertificate(v3CertificateBuilder.build(contentSigner)); } private static GeneralNames getSubjectAlternativeNames() { final List<GeneralName> hostGeneralNames = sanHostNames.stream() .map(hostName -> new GeneralName(GeneralName.dNSName, hostName)) .collect(Collectors.toList()); final List<GeneralName> ipGeneralNames = sanIpAddresses.stream() .map(ipAddress -> new GeneralName(GeneralName.iPAddress, ipAddress)) .collect(Collectors.toList()); final GeneralName[] generalNames = Stream.of(hostGeneralNames, ipGeneralNames) .flatMap(Collection::stream) .toArray(GeneralName[]::new); return new GeneralNames(generalNames); } private static Path createKeyStore(final PrivateKey privateKey, final Certificate certificate) throws IOException, GeneralSecurityException { final KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(null); keyStore.setKeyEntry(alias, privateKey, password, new Certificate[] {certificate}); return saveStore(keyStore); } private static Path createTrustStore(final Certificate certificate) throws IOException, GeneralSecurityException { final KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(null); keyStore.setCertificateEntry(alias, certificate); return saveStore(keyStore); } private static Path saveStore(final KeyStore keyStore) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { final Path pfxPath = Files.createTempFile(alias, ".pfx"); pfxPath.toFile().deleteOnExit(); try (final FileOutputStream outputStream = new FileOutputStream(pfxPath.toFile())) { keyStore.store(outputStream, password); } return pfxPath; } }