2014-05-17 19:26:01 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2014 The Netty Project
|
|
|
|
*
|
|
|
|
* The Netty Project 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 io.netty.handler.ssl.util;
|
|
|
|
|
|
|
|
import io.netty.buffer.Unpooled;
|
|
|
|
import io.netty.handler.codec.base64.Base64;
|
|
|
|
import io.netty.util.CharsetUtil;
|
2015-09-22 20:25:48 +02:00
|
|
|
import io.netty.util.internal.SystemPropertyUtil;
|
2014-05-17 19:26:01 +02:00
|
|
|
import io.netty.util.internal.logging.InternalLogger;
|
|
|
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
|
|
|
|
|
|
|
import java.io.File;
|
2015-07-15 14:13:47 +02:00
|
|
|
import java.io.FileInputStream;
|
2014-05-17 19:26:01 +02:00
|
|
|
import java.io.FileOutputStream;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.OutputStream;
|
|
|
|
import java.security.KeyPair;
|
|
|
|
import java.security.KeyPairGenerator;
|
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
import java.security.PrivateKey;
|
|
|
|
import java.security.SecureRandom;
|
|
|
|
import java.security.cert.CertificateEncodingException;
|
|
|
|
import java.security.cert.CertificateException;
|
2015-07-15 14:13:47 +02:00
|
|
|
import java.security.cert.CertificateFactory;
|
2014-05-17 19:26:01 +02:00
|
|
|
import java.security.cert.X509Certificate;
|
|
|
|
import java.util.Date;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a temporary self-signed certificate for testing purposes.
|
|
|
|
* <p>
|
|
|
|
* <strong>NOTE:</strong>
|
|
|
|
* Never use the certificate and private key generated by this class in production.
|
|
|
|
* It is purely for testing purposes, and thus it is very insecure.
|
|
|
|
* It even uses an insecure pseudo-random generator for faster generation internally.
|
|
|
|
* </p><p>
|
|
|
|
* A X.509 certificate file and a RSA private key file are generated in a system's temporary directory using
|
|
|
|
* {@link java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using
|
|
|
|
* {@link java.io.File#deleteOnExit()}.
|
|
|
|
* </p><p>
|
|
|
|
* At first, this method tries to use OpenJDK's X.509 implementation (the {@code sun.security.x509} package).
|
|
|
|
* If it fails, it tries to use <a href="http://www.bouncycastle.org/">Bouncy Castle</a> as a fallback.
|
|
|
|
* </p>
|
|
|
|
*/
|
|
|
|
public final class SelfSignedCertificate {
|
|
|
|
|
|
|
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(SelfSignedCertificate.class);
|
|
|
|
|
|
|
|
/** Current time minus 1 year, just in case software clock goes back due to time synchronization */
|
2015-09-22 20:25:48 +02:00
|
|
|
private static final Date DEFAULT_NOT_BEFORE = new Date(SystemPropertyUtil.getLong(
|
|
|
|
"io.netty.selfSignedCertificate.defaultNotBefore", System.currentTimeMillis() - 86400000L * 365));
|
2014-05-17 19:26:01 +02:00
|
|
|
/** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */
|
2015-09-22 20:25:48 +02:00
|
|
|
private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong(
|
|
|
|
"io.netty.selfSignedCertificate.defaultNotAfter", 253402300799000L));
|
2014-05-17 19:26:01 +02:00
|
|
|
|
|
|
|
private final File certificate;
|
|
|
|
private final File privateKey;
|
2015-07-15 14:13:47 +02:00
|
|
|
private final X509Certificate cert;
|
|
|
|
private final PrivateKey key;
|
2014-05-17 19:26:01 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
*/
|
|
|
|
public SelfSignedCertificate() throws CertificateException {
|
2015-09-22 20:25:48 +02:00
|
|
|
this(DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
* @param notBefore Certificate is not valid before this time
|
|
|
|
* @param notAfter Certificate is not valid after this time
|
|
|
|
*/
|
|
|
|
public SelfSignedCertificate(Date notBefore, Date notAfter) throws CertificateException {
|
2015-09-25 02:37:33 +02:00
|
|
|
this("example.com", notBefore, notAfter);
|
2014-05-17 19:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
*
|
|
|
|
* @param fqdn a fully qualified domain name
|
|
|
|
*/
|
|
|
|
public SelfSignedCertificate(String fqdn) throws CertificateException {
|
2015-09-22 20:25:48 +02:00
|
|
|
this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
*
|
|
|
|
* @param fqdn a fully qualified domain name
|
|
|
|
* @param notBefore Certificate is not valid before this time
|
|
|
|
* @param notAfter Certificate is not valid after this time
|
|
|
|
*/
|
|
|
|
public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
|
2014-05-17 19:26:01 +02:00
|
|
|
// Bypass entrophy collection by using insecure random generator.
|
|
|
|
// We just want to generate it without any delay because it's for testing purposes only.
|
2015-09-22 20:25:48 +02:00
|
|
|
this(fqdn, ThreadLocalInsecureRandom.current(), 1024, notBefore, notAfter);
|
2014-05-17 19:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
*
|
|
|
|
* @param fqdn a fully qualified domain name
|
|
|
|
* @param random the {@link java.security.SecureRandom} to use
|
|
|
|
* @param bits the number of bits of the generated private key
|
|
|
|
*/
|
|
|
|
public SelfSignedCertificate(String fqdn, SecureRandom random, int bits) throws CertificateException {
|
2015-09-22 20:25:48 +02:00
|
|
|
this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance.
|
|
|
|
*
|
|
|
|
* @param fqdn a fully qualified domain name
|
|
|
|
* @param random the {@link java.security.SecureRandom} to use
|
|
|
|
* @param bits the number of bits of the generated private key
|
|
|
|
* @param notBefore Certificate is not valid before this time
|
|
|
|
* @param notAfter Certificate is not valid after this time
|
|
|
|
*/
|
|
|
|
public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
|
|
|
|
throws CertificateException {
|
2014-05-17 19:26:01 +02:00
|
|
|
// Generate an RSA key pair.
|
|
|
|
final KeyPair keypair;
|
|
|
|
try {
|
|
|
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
|
|
|
keyGen.initialize(bits, random);
|
|
|
|
keypair = keyGen.generateKeyPair();
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
|
|
// Should not reach here because every Java implementation must have RSA key pair generator.
|
|
|
|
throw new Error(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
String[] paths;
|
|
|
|
try {
|
|
|
|
// Try the OpenJDK's proprietary implementation.
|
2015-09-22 20:25:48 +02:00
|
|
|
paths = OpenJdkSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
|
2014-05-17 19:26:01 +02:00
|
|
|
} catch (Throwable t) {
|
|
|
|
logger.debug("Failed to generate a self-signed X.509 certificate using sun.security.x509:", t);
|
|
|
|
try {
|
|
|
|
// Try Bouncy Castle if the current JVM didn't have sun.security.x509.
|
2015-09-22 20:25:48 +02:00
|
|
|
paths = BouncyCastleSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
|
2014-05-17 19:26:01 +02:00
|
|
|
} catch (Throwable t2) {
|
|
|
|
logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
|
|
|
|
throw new CertificateException(
|
|
|
|
"No provider succeeded to generate a self-signed certificate. " +
|
|
|
|
"See debug log for the root cause.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
certificate = new File(paths[0]);
|
|
|
|
privateKey = new File(paths[1]);
|
2015-07-15 14:13:47 +02:00
|
|
|
key = keypair.getPrivate();
|
2015-12-23 01:28:03 +01:00
|
|
|
FileInputStream certificateInput = null;
|
2015-07-15 14:13:47 +02:00
|
|
|
try {
|
2015-12-23 01:28:03 +01:00
|
|
|
certificateInput = new FileInputStream(certificate);
|
|
|
|
cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
|
2015-07-15 14:13:47 +02:00
|
|
|
} catch (Exception e) {
|
|
|
|
throw new CertificateEncodingException(e);
|
2015-12-23 01:28:03 +01:00
|
|
|
} finally {
|
|
|
|
if (certificateInput != null) {
|
|
|
|
try {
|
|
|
|
certificateInput.close();
|
|
|
|
} catch (IOException e) {
|
|
|
|
logger.warn("Failed to close a file: " + certificate, e);
|
|
|
|
}
|
|
|
|
}
|
2015-07-15 14:13:47 +02:00
|
|
|
}
|
2014-05-17 19:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the generated X.509 certificate file in PEM format.
|
|
|
|
*/
|
|
|
|
public File certificate() {
|
|
|
|
return certificate;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the generated RSA private key file in PEM format.
|
|
|
|
*/
|
|
|
|
public File privateKey() {
|
|
|
|
return privateKey;
|
|
|
|
}
|
|
|
|
|
2015-07-15 14:13:47 +02:00
|
|
|
/**
|
|
|
|
* Returns the generated X.509 certificate.
|
|
|
|
*/
|
|
|
|
public X509Certificate cert() {
|
|
|
|
return cert;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the generated RSA private key.
|
|
|
|
*/
|
|
|
|
public PrivateKey key() {
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
|
2014-05-17 19:26:01 +02:00
|
|
|
/**
|
|
|
|
* Deletes the generated X.509 certificate file and RSA private key file.
|
|
|
|
*/
|
|
|
|
public void delete() {
|
|
|
|
safeDelete(certificate);
|
|
|
|
safeDelete(privateKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
static String[] newSelfSignedCertificate(
|
|
|
|
String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
|
|
|
|
|
|
|
|
// Encode the private key into a file.
|
|
|
|
String keyText = "-----BEGIN PRIVATE KEY-----\n" +
|
|
|
|
Base64.encode(Unpooled.wrappedBuffer(key.getEncoded()), true).toString(CharsetUtil.US_ASCII) +
|
|
|
|
"\n-----END PRIVATE KEY-----\n";
|
|
|
|
|
|
|
|
File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
|
|
|
|
keyFile.deleteOnExit();
|
|
|
|
|
|
|
|
OutputStream keyOut = new FileOutputStream(keyFile);
|
|
|
|
try {
|
|
|
|
keyOut.write(keyText.getBytes(CharsetUtil.US_ASCII));
|
|
|
|
keyOut.close();
|
|
|
|
keyOut = null;
|
|
|
|
} finally {
|
|
|
|
if (keyOut != null) {
|
|
|
|
safeClose(keyFile, keyOut);
|
|
|
|
safeDelete(keyFile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encode the certificate into a CRT file.
|
|
|
|
String certText = "-----BEGIN CERTIFICATE-----\n" +
|
|
|
|
Base64.encode(Unpooled.wrappedBuffer(cert.getEncoded()), true).toString(CharsetUtil.US_ASCII) +
|
|
|
|
"\n-----END CERTIFICATE-----\n";
|
|
|
|
|
|
|
|
File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
|
|
|
|
certFile.deleteOnExit();
|
|
|
|
|
|
|
|
OutputStream certOut = new FileOutputStream(certFile);
|
|
|
|
try {
|
|
|
|
certOut.write(certText.getBytes(CharsetUtil.US_ASCII));
|
|
|
|
certOut.close();
|
|
|
|
certOut = null;
|
|
|
|
} finally {
|
|
|
|
if (certOut != null) {
|
|
|
|
safeClose(certFile, certOut);
|
|
|
|
safeDelete(certFile);
|
|
|
|
safeDelete(keyFile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new String[] { certFile.getPath(), keyFile.getPath() };
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void safeDelete(File certFile) {
|
|
|
|
if (!certFile.delete()) {
|
|
|
|
logger.warn("Failed to delete a file: " + certFile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void safeClose(File keyFile, OutputStream keyOut) {
|
|
|
|
try {
|
|
|
|
keyOut.close();
|
|
|
|
} catch (IOException e) {
|
|
|
|
logger.warn("Failed to close a file: " + keyFile, e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|