/*
* Copyright 2017 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package com.github.ambry.commons;
import com.github.ambry.config.VerifiableProperties;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v1CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.junit.Assert;
public class TestSSLUtils {
private final static String SSL_CONTEXT_PROTOCOL = "TLS";
private final static String SSL_CONTEXT_PROVIDER = "SunJSSE";
private final static String SSL_ENABLED_PROTOCOLS = "TLSv1.2";
private final static String ENDPOINT_IDENTIFICATION_ALGORITHM = "HTTPS";
private final static String SSL_CIPHER_SUITES = "TLS_RSA_WITH_AES_128_CBC_SHA";
private final static String TRUSTSTORE_PASSWORD = "UnitTestTrustStorePassword";
private final static String CLIENT_AUTHENTICATION = "required";
private final static String KEYMANAGER_ALGORITHM = "PKIX";
private final static String TRUSTMANAGER_ALGORITHM = "PKIX";
private final static String KEYSTORE_TYPE = "JKS";
private final static String TRUSTSTORE_TYPE = "JKS";
/**
* Create a self-signed X.509 Certificate.
* From http://bfo.com/blog/2011/03/08/odds_and_ends_creating_a_new_x_509_certificate.html.
*
* @param dn the X.509 Distinguished Name, eg "CN(commonName)=Test, O(organizationName)=Org"
* @param pair the KeyPair
* @param days how many days from now the Certificate is valid for
* @param algorithm the signing algorithm, eg "SHA1withRSA"
* @return the self-signed certificate
* @throws java.security.cert.CertificateException thrown if a security error or an IO error ocurred.
*/
public static X509Certificate generateCertificate(String dn, KeyPair pair, int days, String algorithm)
throws CertificateException {
try {
Security.addProvider(new BouncyCastleProvider());
AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(algorithm);
AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
AsymmetricKeyParameter privateKeyAsymKeyParam = PrivateKeyFactory.createKey(pair.getPrivate().getEncoded());
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(pair.getPublic().getEncoded());
ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(privateKeyAsymKeyParam);
X500Name name = new X500Name(dn);
Date from = new Date();
Date to = new Date(from.getTime() + days * 86400000L);
BigInteger sn = new BigInteger(64, new SecureRandom());
X509v1CertificateBuilder v1CertGen = new X509v1CertificateBuilder(name, sn, from, to, name, subPubKeyInfo);
X509CertificateHolder certificateHolder = v1CertGen.build(sigGen);
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder);
} catch (CertificateException ce) {
throw ce;
} catch (Exception e) {
throw new CertificateException(e);
}
}
public static KeyPair generateKeyPair(String algorithm) throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
keyGen.initialize(1024);
return keyGen.genKeyPair();
}
private static KeyStore createEmptyKeyStore() throws GeneralSecurityException, IOException {
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(null, null); // initialize
return ks;
}
private static void saveKeyStore(KeyStore ks, String filename, String password)
throws GeneralSecurityException, IOException {
FileOutputStream out = new FileOutputStream(filename);
try {
ks.store(out, password.toCharArray());
} finally {
out.close();
}
}
/**
* Creates a keystore with a single key and saves it to a file.
*
* @param filename String file to save
* @param password String store password to set on keystore
* @param keyPassword String key password to set on key
* @param alias String alias to use for the key
* @param privateKey Key to save in keystore
* @param cert Certificate to use as certificate chain associated to key
* @throws GeneralSecurityException for any error with the security APIs
* @throws IOException if there is an I/O error saving the file
*/
public static void createKeyStore(String filename, String password, String keyPassword, String alias, Key privateKey,
Certificate cert) throws GeneralSecurityException, IOException {
KeyStore ks = createEmptyKeyStore();
ks.setKeyEntry(alias, privateKey, keyPassword.toCharArray(), new Certificate[]{cert});
saveKeyStore(ks, filename, password);
}
public static <T extends Certificate> void createTrustStore(String filename, String password, Map<String, T> certs)
throws GeneralSecurityException, IOException {
KeyStore ks = KeyStore.getInstance("JKS");
try {
FileInputStream in = new FileInputStream(filename);
ks.load(in, password.toCharArray());
in.close();
} catch (EOFException e) {
ks = createEmptyKeyStore();
}
for (Map.Entry<String, T> cert : certs.entrySet()) {
ks.setCertificateEntry(cert.getKey(), cert.getValue());
}
saveKeyStore(ks, filename, password);
}
public static void addSSLProperties(Properties props, String sslEnabledDatacenters, SSLFactory.Mode mode,
File trustStoreFile, String certAlias) throws IOException, GeneralSecurityException {
Map<String, X509Certificate> certs = new HashMap<String, X509Certificate>();
File keyStoreFile;
String password;
if (mode == SSLFactory.Mode.CLIENT) {
password = "UnitTestClientKeyStorePassword";
keyStoreFile = File.createTempFile("selfsigned-keystore-client", ".jks");
KeyPair cKP = generateKeyPair("RSA");
X509Certificate cCert = generateCertificate("CN=localhost, O=client", cKP, 30, "SHA1withRSA");
createKeyStore(keyStoreFile.getPath(), password, password, certAlias, cKP.getPrivate(), cCert);
certs.put(certAlias, cCert);
} else {
password = "UnitTestServerKeyStorePassword";
keyStoreFile = File.createTempFile("selfsigned-keystore-server", ".jks");
KeyPair sKP = generateKeyPair("RSA");
X509Certificate sCert = generateCertificate("CN=localhost, O=server", sKP, 30, "SHA1withRSA");
createKeyStore(keyStoreFile.getPath(), password, password, certAlias, sKP.getPrivate(), sCert);
certs.put(certAlias, sCert);
}
createTrustStore(trustStoreFile.getPath(), TRUSTSTORE_PASSWORD, certs);
props.put("ssl.context.protocol", SSL_CONTEXT_PROTOCOL);
props.put("ssl.context.provider", SSL_CONTEXT_PROVIDER);
props.put("ssl.enabled.protocols", SSL_ENABLED_PROTOCOLS);
props.put("ssl.endpoint.identification.algorithm", ENDPOINT_IDENTIFICATION_ALGORITHM);
props.put("ssl.client.authentication", CLIENT_AUTHENTICATION);
props.put("ssl.keymanager.algorithm", KEYMANAGER_ALGORITHM);
props.put("ssl.trustmanager.algorithm", TRUSTMANAGER_ALGORITHM);
props.put("ssl.keystore.type", KEYSTORE_TYPE);
props.put("ssl.keystore.path", keyStoreFile.getPath());
props.put("ssl.keystore.password", password);
props.put("ssl.key.password", password);
props.put("ssl.truststore.type", TRUSTSTORE_TYPE);
props.put("ssl.truststore.path", trustStoreFile.getPath());
props.put("ssl.truststore.password", TRUSTSTORE_PASSWORD);
props.put("ssl.cipher.suites", SSL_CIPHER_SUITES);
props.put("clustermap.ssl.enabled.datacenters", sslEnabledDatacenters);
}
/**
* Creates VerifiableProperties with SSL related configs based on the values passed and few other
* pre-populated values
* @param sslEnabledDatacenters Comma separated list of datacenters against which ssl connections should be
* established
* @param mode Represents if the caller is a client or server
* @param trustStoreFile File path of the truststore file
* @param certAlias alias used for the certificate
* @return {@link VerifiableProperties} with all the required values populated
* @throws IOException
* @throws GeneralSecurityException
*/
public static VerifiableProperties createSslProps(String sslEnabledDatacenters, SSLFactory.Mode mode,
File trustStoreFile, String certAlias) throws IOException, GeneralSecurityException {
Properties props = new Properties();
addSSLProperties(props, sslEnabledDatacenters, mode, trustStoreFile, certAlias);
props.setProperty("clustermap.cluster.name", "test");
props.setProperty("clustermap.datacenter.name", "dc1");
props.setProperty("clustermap.host.name", "localhost");
return new VerifiableProperties(props);
}
public static void verifySSLConfig(SSLContext sslContext, SSLEngine sslEngine, boolean isClient) {
// SSLContext verify
Assert.assertEquals(sslContext.getProtocol(), SSL_CONTEXT_PROTOCOL);
Assert.assertEquals(sslContext.getProvider().getName(), SSL_CONTEXT_PROVIDER);
// SSLEngine verify
String[] enabledProtocols = sslEngine.getEnabledProtocols();
Assert.assertEquals(enabledProtocols.length, 1);
Assert.assertEquals(enabledProtocols[0], SSL_ENABLED_PROTOCOLS);
String[] enabledCipherSuite = sslEngine.getEnabledCipherSuites();
Assert.assertEquals(enabledCipherSuite.length, 1);
Assert.assertEquals(enabledCipherSuite[0], SSL_CIPHER_SUITES);
Assert.assertEquals(sslEngine.getWantClientAuth(), false);
if (isClient) {
Assert.assertEquals(sslEngine.getSSLParameters().getEndpointIdentificationAlgorithm(),
ENDPOINT_IDENTIFICATION_ALGORITHM);
Assert.assertEquals(sslEngine.getNeedClientAuth(), false);
Assert.assertEquals(sslEngine.getUseClientMode(), true);
} else {
Assert.assertEquals(sslEngine.getSSLParameters().getEndpointIdentificationAlgorithm(), null);
Assert.assertEquals(sslEngine.getNeedClientAuth(), true);
Assert.assertEquals(sslEngine.getUseClientMode(), false);
}
}
}