| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE |
| * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file |
| * to You 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. |
| */ |
| package org.apache.tuweni.net.tls; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static java.nio.file.Files.createDirectories; |
| import static org.apache.tuweni.crypto.Hash.sha2_256; |
| |
| import org.apache.tuweni.bytes.Bytes; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.IOException; |
| import java.math.BigInteger; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.StandardCopyOption; |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.CertificateException; |
| import java.security.cert.X509Certificate; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.UUID; |
| |
| import com.google.common.base.Strings; |
| import org.bouncycastle.asn1.x500.X500Name; |
| import org.bouncycastle.cert.X509v3CertificateBuilder; |
| import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; |
| import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; |
| import org.bouncycastle.operator.ContentSigner; |
| import org.bouncycastle.operator.OperatorCreationException; |
| import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; |
| import org.bouncycastle.util.io.pem.PemObject; |
| import org.bouncycastle.util.io.pem.PemReader; |
| import org.bouncycastle.util.io.pem.PemWriter; |
| |
| /** |
| * Common utilities for TLS. |
| * |
| * <p> |
| * This class depends upon the BouncyCastle library being available and added as a {@link java.security.Provider}. See |
| * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation. |
| * |
| * <p> |
| * BouncyCastle can be included using the gradle dependencies <code>org.bouncycastle:bcprov-jdk15on</code> and |
| * <code>org.bouncycastle:bcpkix-jdk15on</code>. |
| */ |
| public final class TLS { |
| private TLS() {} |
| |
| /** |
| * Create a self-signed certificate, if it is not already present. |
| * |
| * <p> |
| * If both the key or the certificate file are missing, they will be re-created as a self-signed certificate. |
| * |
| * @param key The key path. |
| * @param certificate The certificate path. |
| * @return {@code true} if a self-signed certificate was created. |
| * @throws IOException If an IO error occurs creating the certificate. |
| */ |
| public static boolean createSelfSignedCertificateIfMissing(Path key, Path certificate) throws IOException { |
| return createSelfSignedCertificateIfMissing(key, certificate, null); |
| } |
| |
| /** |
| * Create a self-signed certificate, if it is not already present. |
| * |
| * <p> |
| * If both the key or the certificate file are missing, they will be re-created as a self-signed certificate. |
| * |
| * @param key The key path. |
| * @param certificate The certificate path. |
| * @param commonName the name to use for the CN attribute of the certificate. If null or empty, a random value is |
| * used. |
| * @return {@code true} if a self-signed certificate was created. |
| * @throws IOException If an IO error occurs creating the certificate. |
| */ |
| public static boolean createSelfSignedCertificateIfMissing(Path key, Path certificate, String commonName) |
| throws IOException { |
| if (Files.exists(certificate) || Files.exists(key)) { |
| return false; |
| } |
| |
| createDirectories(certificate.getParent()); |
| createDirectories(key.getParent()); |
| |
| Path keyFile = Files.createTempFile(key.getParent(), "client-key", ".tmp"); |
| Path certFile = Files.createTempFile(certificate.getParent(), "client-cert", ".tmp"); |
| |
| try { |
| createSelfSignedCertificate(new Date(), keyFile, certFile, commonName); |
| } catch (CertificateException | NoSuchAlgorithmException | OperatorCreationException e) { |
| throw new TLSEnvironmentException("Could not generate certificate: " + e.getMessage(), e); |
| } |
| |
| Files.move(keyFile, key, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); |
| Files.move(certFile, certificate, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); |
| return true; |
| } |
| |
| private static void createSelfSignedCertificate(Date now, Path key, Path certificate, String commonName) |
| throws NoSuchAlgorithmException, |
| IOException, |
| OperatorCreationException, |
| CertificateException { |
| KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); |
| rsa.initialize(2048, new SecureRandom()); |
| |
| KeyPair keyPair = rsa.generateKeyPair(); |
| |
| Calendar cal = Calendar.getInstance(); |
| cal.setTime(now); |
| cal.add(Calendar.YEAR, 1); |
| Date yearFromNow = cal.getTime(); |
| |
| if (Strings.isNullOrEmpty(commonName)) { |
| commonName = UUID.randomUUID().toString() + ".com"; |
| } |
| |
| X500Name dn = new X500Name("CN=" + commonName); |
| |
| X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( |
| dn, |
| new BigInteger(64, new SecureRandom()), |
| now, |
| yearFromNow, |
| dn, |
| keyPair.getPublic()); |
| |
| ContentSigner signer = |
| new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider("BC").build(keyPair.getPrivate()); |
| X509Certificate x509Certificate = |
| new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer)); |
| |
| try (BufferedWriter writer = Files.newBufferedWriter(key, UTF_8); PemWriter pemWriter = new PemWriter(writer)) { |
| pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded())); |
| } |
| |
| try (BufferedWriter writer = Files.newBufferedWriter(certificate, UTF_8); |
| PemWriter pemWriter = new PemWriter(writer)) { |
| pemWriter.writeObject(new PemObject("CERTIFICATE", x509Certificate.getEncoded())); |
| } |
| } |
| |
| /** |
| * Read a PEM-encoded file. |
| * |
| * @param certificate The path to a PEM-encoded file. |
| * @return The bytes for the PEM content. |
| * @throws IOException If an IO error occurs. |
| */ |
| public static byte[] readPemFile(Path certificate) throws IOException { |
| try (BufferedReader reader = Files.newBufferedReader(certificate, UTF_8); |
| PemReader pemReader = new PemReader(reader)) { |
| PemObject pemObject = pemReader.readPemObject(); |
| return pemObject.getContent(); |
| } |
| } |
| |
| /** |
| * Calculate the fingerprint for a PEM-encoded certificate. |
| * |
| * @param certificate The path to a PEM-encoded certificate. |
| * @return The fingerprint bytes for the certificate. |
| * @throws IOException If an IO error occurs. |
| */ |
| public static byte[] certificateFingerprint(Path certificate) throws IOException { |
| return sha2_256(readPemFile(certificate)); |
| } |
| |
| /** |
| * Calculate the fingerprint for a PEM-encoded certificate. |
| * |
| * @param certificate The path to a PEM-encoded certificate. |
| * @return The fingerprint hex-string for the certificate. |
| * @throws IOException If an IO error occurs. |
| */ |
| public static String certificateHexFingerprint(Path certificate) throws IOException { |
| return Bytes.wrap(certificateFingerprint(certificate)).toHexString().substring(2).toLowerCase(); |
| } |
| |
| /** |
| * Calculate the fingerprint for certificate. |
| * |
| * @param certificate The certificate. |
| * @return The fingerprint bytes for the certificate. |
| * @throws CertificateEncodingException If the certificate cannot be encoded. |
| */ |
| public static byte[] certificateFingerprint(Certificate certificate) throws CertificateEncodingException { |
| return sha2_256(certificate.getEncoded()); |
| } |
| |
| /** |
| * Calculate the fingerprint for certificate. |
| * |
| * @param certificate The certificate. |
| * @return The fingerprint hex-string for the certificate. |
| * @throws CertificateEncodingException If the certificate cannot be encoded. |
| */ |
| public static String certificateHexFingerprint(Certificate certificate) throws CertificateEncodingException { |
| return Bytes.wrap(certificateFingerprint(certificate)).toHexString().substring(2).toLowerCase(); |
| } |
| } |