001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.http.ssl; 019 020import java.io.File; 021import java.io.FileOutputStream; 022import java.io.IOException; 023import java.io.Writer; 024import java.math.BigInteger; 025import java.net.URL; 026import java.nio.charset.StandardCharsets; 027import java.security.GeneralSecurityException; 028import java.security.InvalidKeyException; 029import java.security.Key; 030import java.security.KeyPair; 031import java.security.KeyPairGenerator; 032import java.security.KeyStore; 033import java.security.NoSuchAlgorithmException; 034import java.security.NoSuchProviderException; 035import java.security.SecureRandom; 036import java.security.SignatureException; 037import java.security.cert.Certificate; 038import java.security.cert.CertificateEncodingException; 039import java.security.cert.X509Certificate; 040import java.util.Date; 041import java.util.HashMap; 042import java.util.Map; 043import javax.security.auth.x500.X500Principal; 044import org.apache.hadoop.conf.Configuration; 045import org.apache.hadoop.hbase.HBaseCommonTestingUtil; 046import org.apache.hadoop.security.ssl.FileBasedKeyStoresFactory; 047import org.apache.hadoop.security.ssl.SSLFactory; 048import org.bouncycastle.x509.X509V1CertificateGenerator; 049 050import org.apache.hbase.thirdparty.com.google.common.io.Files; 051 052public final class KeyStoreTestUtil { 053 private KeyStoreTestUtil() { 054 } 055 056 public static String getClasspathDir(Class<?> klass) throws Exception { 057 String file = klass.getName(); 058 file = file.replace('.', '/') + ".class"; 059 URL url = Thread.currentThread().getContextClassLoader().getResource(file); 060 String baseDir = url.toURI().getPath(); 061 baseDir = baseDir.substring(0, baseDir.length() - file.length() - 1); 062 return baseDir; 063 } 064 065 /** 066 * Create a self-signed X.509 Certificate. 067 * @param dn the X.509 Distinguished Name, eg "CN=Test, L=London, C=GB" 068 * @param pair the KeyPair 069 * @param days how many days from now the Certificate is valid for 070 * @param algorithm the signing algorithm, eg "SHA1withRSA" 071 * @return the self-signed certificate 072 */ 073 @SuppressWarnings("JavaUtilDate") 074 public static X509Certificate generateCertificate(String dn, KeyPair pair, int days, 075 String algorithm) throws CertificateEncodingException, InvalidKeyException, 076 IllegalStateException, NoSuchProviderException, NoSuchAlgorithmException, SignatureException { 077 Date from = new Date(); 078 Date to = new Date(from.getTime() + days * 86400000L); 079 BigInteger sn = new BigInteger(64, new SecureRandom()); 080 KeyPair keyPair = pair; 081 X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); 082 X500Principal dnName = new X500Principal(dn); 083 084 certGen.setSerialNumber(sn); 085 certGen.setIssuerDN(dnName); 086 certGen.setNotBefore(from); 087 certGen.setNotAfter(to); 088 certGen.setSubjectDN(dnName); 089 certGen.setPublicKey(keyPair.getPublic()); 090 certGen.setSignatureAlgorithm(algorithm); 091 X509Certificate cert = certGen.generate(pair.getPrivate()); 092 return cert; 093 } 094 095 public static KeyPair generateKeyPair(String algorithm) throws NoSuchAlgorithmException { 096 KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm); 097 keyGen.initialize(1024); 098 return keyGen.genKeyPair(); 099 } 100 101 private static KeyStore createEmptyKeyStore() throws GeneralSecurityException, IOException { 102 return createEmptyKeyStore("jks"); 103 } 104 105 private static KeyStore createEmptyKeyStore(String keyStoreType) 106 throws GeneralSecurityException, IOException { 107 KeyStore ks = KeyStore.getInstance(keyStoreType); 108 ks.load(null, null); // initialize 109 return ks; 110 } 111 112 private static void saveKeyStore(KeyStore ks, String filename, String password) 113 throws GeneralSecurityException, IOException { 114 FileOutputStream out = new FileOutputStream(filename); 115 try { 116 ks.store(out, password.toCharArray()); 117 } finally { 118 out.close(); 119 } 120 } 121 122 /** 123 * Creates a keystore with a single key and saves it to a file. This method will use the same 124 * password for the keystore and for the key. This method will always generate a keystore file in 125 * JKS format. 126 * @param filename String file to save 127 * @param password String store password to set on keystore 128 * @param alias String alias to use for the key 129 * @param privateKey Key to save in keystore 130 * @param cert Certificate to use as certificate chain associated to key 131 * @throws GeneralSecurityException for any error with the security APIs 132 * @throws IOException if there is an I/O error saving the file 133 */ 134 public static void createKeyStore(String filename, String password, String alias, Key privateKey, 135 Certificate cert) throws GeneralSecurityException, IOException { 136 createKeyStore(filename, password, password, alias, privateKey, cert); 137 } 138 139 /** 140 * Creates a keystore with a single key and saves it to a file. This method will always generate a 141 * keystore file in JKS format. 142 * @param filename String file to save 143 * @param password String store password to set on keystore 144 * @param keyPassword String key password to set on key 145 * @param alias String alias to use for the key 146 * @param privateKey Key to save in keystore 147 * @param cert Certificate to use as certificate chain associated to key 148 * @throws GeneralSecurityException for any error with the security APIs 149 * @throws IOException if there is an I/O error saving the file 150 */ 151 public static void createKeyStore(String filename, String password, String keyPassword, 152 String alias, Key privateKey, Certificate cert) throws GeneralSecurityException, IOException { 153 createKeyStore(filename, password, keyPassword, alias, privateKey, cert, "JKS"); 154 } 155 156 /** 157 * Creates a keystore with a single key and saves it to a file. 158 * @param filename String file to save 159 * @param password String store password to set on keystore 160 * @param keyPassword String key password to set on key 161 * @param alias String alias to use for the key 162 * @param privateKey Key to save in keystore 163 * @param cert Certificate to use as certificate chain associated to key 164 * @param keystoreType String keystore file type (e.g. "JKS") 165 * @throws GeneralSecurityException for any error with the security APIs 166 * @throws IOException if there is an I/O error saving the file 167 */ 168 public static void createKeyStore(String filename, String password, String keyPassword, 169 String alias, Key privateKey, Certificate cert, String keystoreType) 170 throws GeneralSecurityException, IOException { 171 KeyStore ks = createEmptyKeyStore(keystoreType); 172 ks.setKeyEntry(alias, privateKey, keyPassword.toCharArray(), new Certificate[] { cert }); 173 saveKeyStore(ks, filename, password); 174 } 175 176 /** 177 * Creates a truststore with a single certificate and saves it to a file. This method uses the 178 * default JKS truststore type. 179 * @param filename String file to save 180 * @param password String store password to set on truststore 181 * @param alias String alias to use for the certificate 182 * @param cert Certificate to add 183 * @throws GeneralSecurityException for any error with the security APIs 184 * @throws IOException if there is an I/O error saving the file 185 */ 186 public static void createTrustStore(String filename, String password, String alias, 187 Certificate cert) throws GeneralSecurityException, IOException { 188 createTrustStore(filename, password, alias, cert, "JKS"); 189 } 190 191 /** 192 * Creates a truststore with a single certificate and saves it to a file. 193 * @param filename String file to save 194 * @param password String store password to set on truststore 195 * @param alias String alias to use for the certificate 196 * @param cert Certificate to add 197 * @param trustStoreType String keystore file type (e.g. "JKS") 198 * @throws GeneralSecurityException for any error with the security APIs 199 * @throws IOException if there is an I/O error saving the file 200 */ 201 public static void createTrustStore(String filename, String password, String alias, 202 Certificate cert, String trustStoreType) throws GeneralSecurityException, IOException { 203 KeyStore ks = createEmptyKeyStore(trustStoreType); 204 ks.setCertificateEntry(alias, cert); 205 saveKeyStore(ks, filename, password); 206 } 207 208 public static <T extends Certificate> void createTrustStore(String filename, String password, 209 Map<String, T> certs) throws GeneralSecurityException, IOException { 210 KeyStore ks = createEmptyKeyStore(); 211 for (Map.Entry<String, T> cert : certs.entrySet()) { 212 ks.setCertificateEntry(cert.getKey(), cert.getValue()); 213 } 214 saveKeyStore(ks, filename, password); 215 } 216 217 public static void cleanupSSLConfig(Configuration conf) throws Exception { 218 File f = new File(conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.SERVER, 219 FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY))); 220 f.delete(); 221 f = new File(conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.SERVER, 222 FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY))); 223 f.delete(); 224 225 String clientKeyStore = 226 conf.get(FileBasedKeyStoresFactory.resolvePropertyName(SSLFactory.Mode.CLIENT, 227 FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY)); 228 if (clientKeyStore != null) { 229 f = new File(clientKeyStore); 230 f.delete(); 231 } 232 f = new File(KeyStoreTestUtil.getClasspathDir(KeyStoreTestUtil.class) + "/" 233 + conf.get(SSLFactory.SSL_CLIENT_CONF_KEY)); 234 f.delete(); 235 f = new File(KeyStoreTestUtil.getClasspathDir(KeyStoreTestUtil.class) + "/" 236 + conf.get(SSLFactory.SSL_SERVER_CONF_KEY)); 237 f.delete(); 238 } 239 240 /** 241 * Performs complete setup of SSL configuration in preparation for testing an SSLFactory. This 242 * includes keys, certs, keystores, truststores, the server SSL configuration file, the client SSL 243 * configuration file, and the master configuration file read by the SSLFactory. 244 * @param keystoresDir String directory to save keystores 245 * @param sslConfDir String directory to save SSL configuration files 246 * @param conf Configuration master configuration to be used by an SSLFactory, which will 247 * be mutated by this method 248 * @param useClientCert boolean true to make the client present a cert in the SSL handshake 249 */ 250 public static void setupSSLConfig(String keystoresDir, String sslConfDir, Configuration conf, 251 boolean useClientCert) throws Exception { 252 String clientKS = keystoresDir + "/clientKS.jks"; 253 String clientPassword = "clientP"; 254 String serverKS = keystoresDir + "/serverKS.jks"; 255 String serverPassword = "serverP"; 256 String trustKS = keystoresDir + "/trustKS.jks"; 257 String trustPassword = "trustP"; 258 259 File sslClientConfFile = new File(sslConfDir + "/ssl-client-" + System.nanoTime() + "-" 260 + HBaseCommonTestingUtil.getRandomUUID() + ".xml"); 261 File sslServerConfFile = new File(sslConfDir + "/ssl-server-" + System.nanoTime() + "-" 262 + HBaseCommonTestingUtil.getRandomUUID() + ".xml"); 263 264 Map<String, X509Certificate> certs = new HashMap<>(); 265 266 if (useClientCert) { 267 KeyPair cKP = KeyStoreTestUtil.generateKeyPair("RSA"); 268 X509Certificate cCert = 269 KeyStoreTestUtil.generateCertificate("CN=localhost, O=client", cKP, 30, "SHA1withRSA"); 270 KeyStoreTestUtil.createKeyStore(clientKS, clientPassword, "client", cKP.getPrivate(), cCert); 271 certs.put("client", cCert); 272 } 273 274 KeyPair sKP = KeyStoreTestUtil.generateKeyPair("RSA"); 275 X509Certificate sCert = 276 KeyStoreTestUtil.generateCertificate("CN=localhost, O=server", sKP, 30, "SHA1withRSA"); 277 KeyStoreTestUtil.createKeyStore(serverKS, serverPassword, "server", sKP.getPrivate(), sCert); 278 certs.put("server", sCert); 279 280 KeyStoreTestUtil.createTrustStore(trustKS, trustPassword, certs); 281 282 Configuration clientSSLConf = 283 createClientSSLConfig(clientKS, clientPassword, clientPassword, trustKS); 284 Configuration serverSSLConf = 285 createServerSSLConfig(serverKS, serverPassword, serverPassword, trustKS); 286 287 saveConfig(sslClientConfFile, clientSSLConf); 288 saveConfig(sslServerConfFile, serverSSLConf); 289 290 conf.set(SSLFactory.SSL_HOSTNAME_VERIFIER_KEY, "ALLOW_ALL"); 291 conf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile.getName()); 292 conf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile.getName()); 293 conf.set("dfs.https.server.keystore.resource", sslServerConfFile.getName()); 294 295 conf.setBoolean(SSLFactory.SSL_REQUIRE_CLIENT_CERT_KEY, useClientCert); 296 } 297 298 /** 299 * Creates SSL configuration for a client. 300 * @param clientKS String client keystore file 301 * @param password String store password, or null to avoid setting store password 302 * @param keyPassword String key password, or null to avoid setting key password 303 * @param trustKS String truststore file 304 * @return Configuration for client SSL 305 */ 306 public static Configuration createClientSSLConfig(String clientKS, String password, 307 String keyPassword, String trustKS) { 308 Configuration clientSSLConf = 309 createSSLConfig(SSLFactory.Mode.CLIENT, clientKS, password, keyPassword, trustKS); 310 return clientSSLConf; 311 } 312 313 /** 314 * Creates SSL configuration for a server. 315 * @param serverKS String server keystore file 316 * @param password String store password, or null to avoid setting store password 317 * @param keyPassword String key password, or null to avoid setting key password 318 * @param trustKS String truststore file 319 * @return Configuration for server SSL 320 */ 321 public static Configuration createServerSSLConfig(String serverKS, String password, 322 String keyPassword, String trustKS) throws IOException { 323 Configuration serverSSLConf = 324 createSSLConfig(SSLFactory.Mode.SERVER, serverKS, password, keyPassword, trustKS); 325 return serverSSLConf; 326 } 327 328 /** 329 * Creates SSL configuration. 330 * @param mode SSLFactory.Mode mode to configure 331 * @param keystore String keystore file 332 * @param password String store password, or null to avoid setting store password 333 * @param keyPassword String key password, or null to avoid setting key password 334 * @param trustKS String truststore file 335 * @return Configuration for SSL 336 */ 337 private static Configuration createSSLConfig(SSLFactory.Mode mode, String keystore, 338 String password, String keyPassword, String trustKS) { 339 String trustPassword = "trustP"; 340 341 Configuration sslConf = new Configuration(false); 342 if (keystore != null) { 343 sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode, 344 FileBasedKeyStoresFactory.SSL_KEYSTORE_LOCATION_TPL_KEY), keystore); 345 } 346 if (password != null) { 347 sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode, 348 FileBasedKeyStoresFactory.SSL_KEYSTORE_PASSWORD_TPL_KEY), password); 349 } 350 if (keyPassword != null) { 351 sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode, 352 FileBasedKeyStoresFactory.SSL_KEYSTORE_KEYPASSWORD_TPL_KEY), keyPassword); 353 } 354 if (trustKS != null) { 355 sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode, 356 FileBasedKeyStoresFactory.SSL_TRUSTSTORE_LOCATION_TPL_KEY), trustKS); 357 } 358 if (trustPassword != null) { 359 sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode, 360 FileBasedKeyStoresFactory.SSL_TRUSTSTORE_PASSWORD_TPL_KEY), trustPassword); 361 } 362 sslConf.set(FileBasedKeyStoresFactory.resolvePropertyName(mode, 363 FileBasedKeyStoresFactory.SSL_TRUSTSTORE_RELOAD_INTERVAL_TPL_KEY), "1000"); 364 365 return sslConf; 366 } 367 368 /** 369 * Saves configuration to a file. 370 * @param file File to save 371 * @param conf Configuration contents to write to file 372 * @throws IOException if there is an I/O error saving the file 373 */ 374 public static void saveConfig(File file, Configuration conf) throws IOException { 375 try (Writer writer = Files.newWriter(file, StandardCharsets.UTF_8)) { 376 conf.writeXml(writer); 377 } 378 } 379}