blob: b30d9e53704d38fd38734714845d7b28dd5a4352 [file] [log] [blame]
/*
* 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();
}
}