org.neo4j.test.ssl.CertificateChainFactory Maven / Gradle / Ivy
Show all versions of test-utils Show documentation
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.neo4j.test.ssl;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
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.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Random;
import java.util.Set;
/**
* Utility for generating a 3 certificate chain with embedded ocsp revocation checking URIs
*/
public final class CertificateChainFactory
{
/**
* Current time minus 1 year, just in case software clock goes back due to time synchronization
*/
private static final Date NOT_BEFORE = new Date( System.currentTimeMillis() - 86_400_000L * 365 );
/**
* The maximum possible value in X.509 specification: 9999-12-31 23:59:59
*/
private static final Date NOT_AFTER = new Date( 253_402_300_799_000L );
private static volatile boolean cleanupRequired = true;
private CertificateChainFactory()
{
}
public static void createCertificateChain( Path endUserCertPath, Path endUserPrivateKeyPath, Path intCertPath, Path intPrivateKeyPath,
Path rootCertPath, Path rootPrivateKeyPath, int ocspServerPortNo, BouncyCastleProvider bouncyCastleProvider )
throws Exception
{
Security.addProvider( bouncyCastleProvider );
installCleanupHook( endUserCertPath, endUserPrivateKeyPath, intCertPath, intPrivateKeyPath, rootCertPath, rootPrivateKeyPath, bouncyCastleProvider );
String ocspBaseURL = "http://localhost:" + ocspServerPortNo;
KeyPair rootCertKeyPair = generateKeyPair();
KeyPair intCertKeyPair = generateKeyPair();
KeyPair endUserCertKeyPair = generateKeyPair();
X509Certificate rootCert = generateCertificate( null, null, rootCertKeyPair, "rootCA",
ocspBaseURL, rootCertPath, rootPrivateKeyPath, bouncyCastleProvider );
X509Certificate intCert = generateCertificate( rootCert, rootCertKeyPair.getPrivate(), intCertKeyPair, "intCA",
ocspBaseURL, intCertPath, intPrivateKeyPath, bouncyCastleProvider );
X509Certificate endUserCert = generateCertificate( intCert, intCertKeyPair.getPrivate(), endUserCertKeyPair, "endUserCA",
ocspBaseURL, endUserCertPath, endUserPrivateKeyPath, bouncyCastleProvider );
// for the end user certificate we overwrite the single cert for entire chain
writePem( "CERTIFICATE", endUserCert.getEncoded(), intCert.getEncoded(), rootCert.getEncoded(), endUserCertPath );
writePem( "PRIVATE KEY", endUserCertKeyPair.getPrivate().getEncoded(), endUserPrivateKeyPath );
// Mark as done so we don't clean up certificates
cleanupRequired = false;
}
private static KeyPair generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException
{
KeyPairGenerator kpGen = KeyPairGenerator.getInstance( "RSA", "BC" );
kpGen.initialize( 2048, new SecureRandom() );
return kpGen.generateKeyPair();
}
private static X509Certificate generateCertificate( X509Certificate issuingCert, PrivateKey issuingPrivateKey, KeyPair certKeyPair, String certName,
String ocspURL, Path certificatePath, Path keyPath,
BouncyCastleProvider bouncyCastleProvider ) throws Exception
{
X509v3CertificateBuilder builder;
if ( issuingCert == null )
{
builder = new JcaX509v3CertificateBuilder(
new X500Name( "CN=" + certName ), // issuer authority
BigInteger.valueOf( new Random().nextInt() ), //serial number of certificate
NOT_BEFORE, // start of validity
NOT_AFTER, //end of certificate validity
new X500Name( "CN=" + certName ), // subject name of certificate
certKeyPair.getPublic() ); // public key of certificate
}
else
{
builder = new JcaX509v3CertificateBuilder(
issuingCert, // issuer authority
BigInteger.valueOf( new Random().nextInt() ), //serial number of certificate
NOT_BEFORE, // start of validity
NOT_AFTER, //end of certificate validity
new X500Name( "CN=" + certName ), // subject name of certificate
certKeyPair.getPublic() ); // public key of certificate
}
// key usage restrictions
builder.addExtension( Extension.keyUsage, true, new KeyUsage( KeyUsage.keyCertSign | KeyUsage.digitalSignature ) );
builder.addExtension( Extension.extendedKeyUsage, true, new ExtendedKeyUsage( KeyPurposeId.anyExtendedKeyUsage ) );
builder.addExtension( Extension.basicConstraints, false, new BasicConstraints( true ) );
// embed ocsp URI
builder.addExtension(
Extension.authorityInfoAccess, false, new AuthorityInformationAccess( new AccessDescription( AccessDescription.id_ad_ocsp,
new GeneralName(
GeneralName.uniformResourceIdentifier,
ocspURL + "/" + certName ) ) ) );
X509Certificate certificate =
new JcaX509CertificateConverter().getCertificate( builder.build( new JcaContentSignerBuilder( "SHA1withRSA" )
.setProvider( bouncyCastleProvider ).
build( issuingPrivateKey == null ? certKeyPair.getPrivate() : issuingPrivateKey ) ) ); // self sign if root cert
writePem( "CERTIFICATE", certificate.getEncoded(), certificatePath );
writePem( "PRIVATE KEY", certKeyPair.getPrivate().getEncoded(), keyPath );
return certificate;
}
/**
* Makes sure to delete partially generated certificates and reset the security context.
* Does nothing if both certificate and private key have been generated successfully.
*
* The hook should only be installed prior to generation of the certificate chain, and not if certificates already exist.
*/
private static void installCleanupHook( final Path endUserCertPath, final Path endUserPrivateKeyPath,
final Path intCertPath, final Path intPrivateKeyPath,
final Path rootCertPath, final Path rootPrivateKeyPath,
BouncyCastleProvider bouncyCastleProvider )
{
Runtime.getRuntime().addShutdownHook( new Thread( () ->
{
if ( cleanupRequired )
{
System.err.println( "Cleaning up partially generated self-signed certificate..." );
try
{
Security.removeProvider( bouncyCastleProvider.getName() );
Files.deleteIfExists( endUserCertPath );
Files.deleteIfExists( endUserPrivateKeyPath );
Files.deleteIfExists( intCertPath );
Files.deleteIfExists( intPrivateKeyPath );
Files.deleteIfExists( rootCertPath );
Files.deleteIfExists( rootPrivateKeyPath );
}
catch ( IOException e )
{
System.err.println( "Error cleaning up" );
e.printStackTrace( System.err );
}
}
} ) );
}
private static void writePem( String type, byte[] encodedContent, Path path ) throws IOException
{
Files.createDirectories( path.getParent() );
try ( PemWriter writer = new PemWriter( Files.newBufferedWriter( path, StandardCharsets.UTF_8 ) ) )
{
writer.writeObject( new PemObject( type, encodedContent ) );
writer.flush();
}
try
{
Files.setPosixFilePermissions( path, Set.of( PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_READ ) );
}
catch ( UnsupportedOperationException ignore )
{
// Fallback for windows
File file = path.toFile();
file.setReadable( false, false );
file.setWritable( false, false );
file.setReadable( true );
file.setWritable( true );
}
}
private static void writePem( String type, byte[] certA, byte[] certB, byte[] certC, Path path ) throws IOException
{
Files.createDirectories( path.getParent() );
try ( PemWriter writer = new PemWriter( Files.newBufferedWriter( path, StandardCharsets.UTF_8 ) ) )
{
writer.writeObject( new PemObject( type, certA ) );
writer.writeObject( new PemObject( type, certB ) );
writer.writeObject( new PemObject( type, certC ) );
writer.flush();
}
try
{
Files.setPosixFilePermissions( path, Set.of( PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_READ ) );
}
catch ( UnsupportedOperationException ignore )
{
// Fallback for windows
File file = path.toFile();
file.setReadable( false, false );
file.setWritable( false, false );
file.setReadable( true );
file.setWritable( true );
}
}
}