Repository: zookeeper Updated Branches: refs/heads/master e5fc12281 -> 03286f29d
http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java new file mode 100644 index 0000000..5a86bb4 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java @@ -0,0 +1,492 @@ +/** + * 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.zookeeper.common; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import org.apache.commons.io.FileUtils; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; + +import static java.util.Objects.requireNonNull; + +/** + * This class simplifies the creation of certificates and private keys for SSL/TLS connections. + */ +public class X509TestContext { + private static final String TRUST_STORE_PREFIX = "zk_test_ca"; + private static final String KEY_STORE_PREFIX = "zk_test_key"; + + private final File tempDir; + + private final X509KeyType trustStoreKeyType; + private final KeyPair trustStoreKeyPair; + private final long trustStoreCertExpirationMillis; + private final X509Certificate trustStoreCertificate; + private final String trustStorePassword; + private File trustStoreJksFile; + private File trustStorePemFile; + + private final X509KeyType keyStoreKeyType; + private final KeyPair keyStoreKeyPair; + private final long keyStoreCertExpirationMillis; + private final X509Certificate keyStoreCertificate; + private final String keyStorePassword; + private File keyStoreJksFile; + private File keyStorePemFile; + + private final Boolean hostnameVerification; + + /** + * Constructor is intentionally private, use the Builder class instead. + * @param tempDir the directory in which key store and trust store temp files will be written. + * @param trustStoreKeyPair the key pair for the trust store. + * @param trustStoreCertExpirationMillis the expiration of the trust store cert, in milliseconds from now. + * @param trustStorePassword the password to protect a JKS trust store (ignored for PEM trust stores). + * @param keyStoreKeyPair the key pair for the key store. + * @param keyStoreCertExpirationMillis the expiration of the key store cert, in milliseconds from now. + * @param keyStorePassword the password to protect the key store private key. + * @throws IOException + * @throws GeneralSecurityException + * @throws OperatorCreationException + */ + private X509TestContext(File tempDir, + KeyPair trustStoreKeyPair, + long trustStoreCertExpirationMillis, + String trustStorePassword, + KeyPair keyStoreKeyPair, + long keyStoreCertExpirationMillis, + String keyStorePassword, + Boolean hostnameVerification) throws IOException, GeneralSecurityException, OperatorCreationException { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + throw new IllegalStateException("BC Security provider was not found"); + } + this.tempDir = requireNonNull(tempDir); + if (!tempDir.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + tempDir); + } + this.trustStoreKeyPair = requireNonNull(trustStoreKeyPair); + this.trustStoreKeyType = keyPairToType(trustStoreKeyPair); + this.trustStoreCertExpirationMillis = trustStoreCertExpirationMillis; + this.trustStorePassword = requireNonNull(trustStorePassword); + this.keyStoreKeyPair = requireNonNull(keyStoreKeyPair); + this.keyStoreKeyType = keyPairToType(keyStoreKeyPair); + this.keyStoreCertExpirationMillis = keyStoreCertExpirationMillis; + this.keyStorePassword = requireNonNull(keyStorePassword); + + X500NameBuilder caNameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + caNameBuilder.addRDN(BCStyle.CN, MethodHandles.lookup().lookupClass().getCanonicalName() + " Root CA"); + trustStoreCertificate = X509TestHelpers.newSelfSignedCACert( + caNameBuilder.build(), + trustStoreKeyPair, + trustStoreCertExpirationMillis); + + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, MethodHandles.lookup().lookupClass().getCanonicalName() + " Zookeeper Test"); + keyStoreCertificate = X509TestHelpers.newCert( + trustStoreCertificate, + trustStoreKeyPair, + nameBuilder.build(), + keyStoreKeyPair.getPublic(), + keyStoreCertExpirationMillis); + trustStorePemFile = trustStoreJksFile = keyStorePemFile = keyStoreJksFile = null; + + this.hostnameVerification = hostnameVerification; + } + + /** + * Returns the X509KeyType of the given key pair. + * @param keyPair the key pair. + * @return <code>X509KeyType.RSA</code> if given an RSA key pair, and <code>X509KeyType.EC</code> otherwise. + */ + private X509KeyType keyPairToType(KeyPair keyPair) { + if (keyPair.getPrivate().getAlgorithm().contains("RSA")) { + return X509KeyType.RSA; + } else { + return X509KeyType.EC; + } + } + + public File getTempDir() { + return tempDir; + } + + public X509KeyType getTrustStoreKeyType() { + return trustStoreKeyType; + } + + public KeyPair getTrustStoreKeyPair() { + return trustStoreKeyPair; + } + + public long getTrustStoreCertExpirationMillis() { + return trustStoreCertExpirationMillis; + } + + public X509Certificate getTrustStoreCertificate() { + return trustStoreCertificate; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + /** + * Returns the path to the trust store file in the given format (JKS or PEM). Note that the file is created lazily, + * the first time this method is called. The trust store file is temporary and will be deleted on exit. + * @param storeFileType the store file type (JKS or PEM). + * @return the path to the trust store file. + * @throws IOException if there is an error creating the trust store file. + */ + public File getTrustStoreFile(KeyStoreFileType storeFileType) throws IOException { + switch (storeFileType) { + case JKS: + return getTrustStoreJksFile(); + case PEM: + return getTrustStorePemFile(); + default: + throw new IllegalArgumentException("Invalid trust store type: " + storeFileType + ", must be one of: " + + Arrays.toString(KeyStoreFileType.values())); + } + } + + private File getTrustStoreJksFile() throws IOException { + if (trustStoreJksFile == null) { + try { + File trustStoreJksFile = File.createTempFile( + TRUST_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir); + trustStoreJksFile.deleteOnExit(); + final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreJksFile); + try { + byte[] bytes = X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword); + trustStoreOutputStream.write(bytes); + trustStoreOutputStream.flush(); + } finally { + trustStoreOutputStream.close(); + } + this.trustStoreJksFile = trustStoreJksFile; + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + return trustStoreJksFile; + } + + private File getTrustStorePemFile() throws IOException { + if (trustStorePemFile == null) { + File trustStorePemFile = File.createTempFile( + TRUST_STORE_PREFIX, KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir); + trustStorePemFile.deleteOnExit(); + FileUtils.writeStringToFile( + trustStorePemFile, + X509TestHelpers.pemEncodeX509Certificate(trustStoreCertificate), + StandardCharsets.US_ASCII, + false); + this.trustStorePemFile = trustStorePemFile; + } + return trustStorePemFile; + } + + public X509KeyType getKeyStoreKeyType() { + return keyStoreKeyType; + } + + public KeyPair getKeyStoreKeyPair() { + return keyStoreKeyPair; + } + + public long getKeyStoreCertExpirationMillis() { + return keyStoreCertExpirationMillis; + } + + public X509Certificate getKeyStoreCertificate() { + return keyStoreCertificate; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public boolean isKeyStoreEncrypted() { + return keyStorePassword.length() > 0; + } + + /** + * Returns the path to the key store file in the given format (JKS or PEM). Note that the file is created lazily, + * the first time this method is called. The key store file is temporary and will be deleted on exit. + * @param storeFileType the store file type (JKS or PEM). + * @return the path to the key store file. + * @throws IOException if there is an error creating the key store file. + */ + public File getKeyStoreFile(KeyStoreFileType storeFileType) throws IOException { + switch (storeFileType) { + case JKS: + return getKeyStoreJksFile(); + case PEM: + return getKeyStorePemFile(); + default: + throw new IllegalArgumentException("Invalid key store type: " + storeFileType + ", must be one of: " + + Arrays.toString(KeyStoreFileType.values())); + } + } + + private File getKeyStoreJksFile() throws IOException { + if (keyStoreJksFile == null) { + try { + File keyStoreJksFile = File.createTempFile( + KEY_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir); + keyStoreJksFile.deleteOnExit(); + final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreJksFile); + try { + byte[] bytes = X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes( + keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword); + keyStoreOutputStream.write(bytes); + keyStoreOutputStream.flush(); + } finally { + keyStoreOutputStream.close(); + } + this.keyStoreJksFile = keyStoreJksFile; + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + return keyStoreJksFile; + } + + private File getKeyStorePemFile() throws IOException { + if (keyStorePemFile == null) { + try { + File keyStorePemFile = File.createTempFile( + KEY_STORE_PREFIX, KeyStoreFileType.PEM.getDefaultFileExtension(), tempDir); + keyStorePemFile.deleteOnExit(); + FileUtils.writeStringToFile( + keyStorePemFile, + X509TestHelpers.pemEncodeCertAndPrivateKey( + keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword), + StandardCharsets.US_ASCII, + false); + this.keyStorePemFile = keyStorePemFile; + } catch (OperatorCreationException e) { + throw new IOException(e); + } + } + return keyStorePemFile; + } + + /** + * Sets the SSL system properties such that the given X509Util object can be used to create SSL Contexts that + * will use the trust store and key store files created by this test context. Example usage: + * <pre> + * X509TestContext testContext = ...; // create the test context + * X509Util x509Util = new QuorumX509Util(); + * testContext.setSystemProperties(x509Util, KeyStoreFileType.JKS, KeyStoreFileType.JKS); + * // The returned context will use the key store and trust store created by the test context. + * SSLContext ctx = x509Util.getDefaultSSLContext(); + * </pre> + * @param x509Util the X509Util. + * @param keyStoreFileType the store file type to use for the key store (JKS or PEM). + * @param trustStoreFileType the store file type to use for the trust store (JKS or PEM). + * @throws IOException if there is an error creating the key store file or trust store file. + */ + public void setSystemProperties(X509Util x509Util, + KeyStoreFileType keyStoreFileType, + KeyStoreFileType trustStoreFileType) throws IOException { + System.setProperty( + x509Util.getSslKeystoreLocationProperty(), + this.getKeyStoreFile(keyStoreFileType).getAbsolutePath()); + System.setProperty(x509Util.getSslKeystorePasswdProperty(), this.getKeyStorePassword()); + System.setProperty(x509Util.getSslKeystoreTypeProperty(), keyStoreFileType.getPropertyValue()); + System.setProperty( + x509Util.getSslTruststoreLocationProperty(), + this.getTrustStoreFile(trustStoreFileType).getAbsolutePath()); + System.setProperty(x509Util.getSslTruststorePasswdProperty(), this.getTrustStorePassword()); + System.setProperty(x509Util.getSslTruststoreTypeProperty(), trustStoreFileType.getPropertyValue()); + if (hostnameVerification != null) { + System.setProperty(x509Util.getSslHostnameVerificationEnabledProperty(), hostnameVerification.toString()); + } else { + System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty()); + } + } + + /** + * Clears system properties set by + * {@link #setSystemProperties(X509Util, KeyStoreFileType, KeyStoreFileType)}. + * @param x509Util the X509Util to read property keys from. + */ + public void clearSystemProperties(X509Util x509Util) { + System.clearProperty(x509Util.getSslKeystoreLocationProperty()); + System.clearProperty(x509Util.getSslKeystorePasswdProperty()); + System.clearProperty(x509Util.getSslKeystoreTypeProperty()); + System.clearProperty(x509Util.getSslTruststoreLocationProperty()); + System.clearProperty(x509Util.getSslTruststorePasswdProperty()); + System.clearProperty(x509Util.getSslTruststoreTypeProperty()); + System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty()); + } + + /** + * Builder class, used for creating new instances of X509TestContext. + */ + public static class Builder { + public static final long DEFAULT_CERT_EXPIRATION_MILLIS = 1000L * 60 * 60 * 24; // 1 day + private File tempDir; + private X509KeyType trustStoreKeyType; + private String trustStorePassword; + private long trustStoreCertExpirationMillis; + private X509KeyType keyStoreKeyType; + private String keyStorePassword; + private long keyStoreCertExpirationMillis; + private Boolean hostnameVerification; + + /** + * Creates an empty builder. + */ + public Builder() { + trustStoreKeyType = X509KeyType.EC; + trustStorePassword = ""; + trustStoreCertExpirationMillis = DEFAULT_CERT_EXPIRATION_MILLIS; + keyStoreKeyType = X509KeyType.EC; + keyStorePassword = ""; + keyStoreCertExpirationMillis = DEFAULT_CERT_EXPIRATION_MILLIS; + hostnameVerification = null; + } + + /** + * Builds a new X509TestContext from this builder. + * @return a new X509TestContext + * @throws IOException + * @throws GeneralSecurityException + * @throws OperatorCreationException + */ + public X509TestContext build() throws IOException, GeneralSecurityException, OperatorCreationException { + KeyPair trustStoreKeyPair = X509TestHelpers.generateKeyPair(trustStoreKeyType); + KeyPair keyStoreKeyPair = X509TestHelpers.generateKeyPair(keyStoreKeyType); + return new X509TestContext( + tempDir, + trustStoreKeyPair, + trustStoreCertExpirationMillis, + trustStorePassword, + keyStoreKeyPair, + keyStoreCertExpirationMillis, + keyStorePassword, + hostnameVerification); + } + + /** + * Sets the temporary directory. Certificate and private key files will be created in this directory. + * @param tempDir the temp directory. + * @return this Builder. + */ + public Builder setTempDir(File tempDir) { + this.tempDir = tempDir; + return this; + } + + /** + * Sets the trust store key type. The CA key generated for the test context will be of this type. + * @param keyType the key type. + * @return this Builder. + */ + public Builder setTrustStoreKeyType(X509KeyType keyType) { + trustStoreKeyType = keyType; + return this; + } + + /** + * Sets the trust store password. Ignored for PEM trust stores, JKS trust stores will be encrypted with this + * password. + * @param password the password. + * @return this Builder. + */ + public Builder setTrustStorePassword(String password) { + trustStorePassword = password; + return this; + } + + /** + * Sets the trust store certificate's expiration, in milliseconds from when <code>build()</code> is called. + * @param expirationMillis expiration in milliseconds. + * @return this Builder. + */ + public Builder setTrustStoreCertExpirationMillis(long expirationMillis) { + trustStoreCertExpirationMillis = expirationMillis; + return this; + } + + /** + * Sets the key store key type. The private key generated for the test context will be of this type. + * @param keyType the key type. + * @return this Builder. + */ + public Builder setKeyStoreKeyType(X509KeyType keyType) { + keyStoreKeyType = keyType; + return this; + } + + /** + * Sets the key store password. The private key (PEM, JKS) and certificate (JKS only) will be encrypted with + * this password. + * @param password the password. + * @return this Builder. + */ + public Builder setKeyStorePassword(String password) { + keyStorePassword = password; + return this; + } + + /** + * Sets the key store certificate's expiration, in milliseconds from when <code>build()</code> is called. + * @param expirationMillis expiration in milliseconds. + * @return this Builder. + */ + public Builder setKeyStoreCertExpirationMillis(long expirationMillis) { + keyStoreCertExpirationMillis = expirationMillis; + return this; + } + + /** + * Sets the hostname verification behavior. If null is provided, reverts the behavior to the default, otherwise + * explicitly sets hostname verification to true or false. + * @param hostnameVerification new value for the hostname verification setting. + * @return this Builder. + */ + public Builder setHostnameVerification(Boolean hostnameVerification) { + this.hostnameVerification = hostnameVerification; + return this; + } + } + + /** + * Returns a new default-constructed Builder. + * @return a new Builder. + */ + public static Builder newBuilder() { + return new Builder(); + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java new file mode 100644 index 0000000..59b7634 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestHelpers.java @@ -0,0 +1,402 @@ +/** + * 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.zookeeper.common; + +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +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.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +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.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.operator.bc.BcContentSignerBuilder; +import org.bouncycastle.operator.bc.BcECContentSignerBuilder; +import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.Date; + +/** + * This class contains helper methods for creating X509 certificates and key pairs, and for serializing them + * to JKS or PEM files. + */ +public class X509TestHelpers { + private static final Logger LOG = LoggerFactory.getLogger(X509TestHelpers.class); + + private static final SecureRandom PRNG = new SecureRandom(); + private static final int DEFAULT_RSA_KEY_SIZE_BITS = 2048; + private static final BigInteger DEFAULT_RSA_PUB_EXPONENT = RSAKeyGenParameterSpec.F4; // 65537 + private static final String DEFAULT_ELLIPTIC_CURVE_NAME = "secp256r1"; + // Per RFC 5280 section 4.1.2.2, X509 certificates can use up to 20 bytes == 160 bits for serial numbers. + private static final int SERIAL_NUMBER_MAX_BITS = 20 * Byte.SIZE; + + /** + * Uses the private key of the given key pair to create a self-signed CA certificate with the public half of the + * key pair and the given subject and expiration. The issuer of the new cert will be equal to the subject. + * Returns the new certificate. + * The returned certificate should be used as the trust store. The private key of the input key pair should be + * used to sign certificates that are used by test peers to establish TLS connections to each other. + * @param subject the subject of the new certificate being created. + * @param keyPair the key pair to use. The public key will be embedded in the new certificate, and the private key + * will be used to self-sign the certificate. + * @param expirationMillis expiration of the new certificate, in milliseconds from now. + * @return a new self-signed CA certificate. + * @throws IOException + * @throws OperatorCreationException + * @throws GeneralSecurityException + */ + public static X509Certificate newSelfSignedCACert( + X500Name subject, + KeyPair keyPair, + long expirationMillis) throws IOException, OperatorCreationException, GeneralSecurityException { + Date now = new Date(); + X509v3CertificateBuilder builder = initCertBuilder( + subject, // for self-signed certs, issuer == subject + now, + new Date(now.getTime() + expirationMillis), + subject, + keyPair.getPublic()); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); // is a CA + builder.addExtension( + Extension.keyUsage, + true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)); + return buildAndSignCertificate(keyPair.getPrivate(), builder); + } + + /** + * Using the private key of the given CA key pair and the Subject of the given CA cert as the Issuer, issues a + * new cert with the given subject and public key. The returned certificate, combined with the private key half + * of the <code>certPublicKey</code>, should be used as the key store. + * @param caCert the certificate of the CA that's doing the signing. + * @param caKeyPair the key pair of the CA. The private key will be used to sign. The public key must match the + * public key in the <code>caCert</code>. + * @param certSubject the subject field of the new cert being issued. + * @param certPublicKey the public key of the new cert being issued. + * @param expirationMillis the expiration of the cert being issued, in milliseconds from now. + * @return a new certificate signed by the CA's private key. + * @throws IOException + * @throws OperatorCreationException + * @throws GeneralSecurityException + */ + public static X509Certificate newCert( + X509Certificate caCert, + KeyPair caKeyPair, + X500Name certSubject, + PublicKey certPublicKey, + long expirationMillis) throws IOException, OperatorCreationException, GeneralSecurityException { + if (!caKeyPair.getPublic().equals(caCert.getPublicKey())) { + throw new IllegalArgumentException("CA private key does not match the public key in the CA cert"); + } + Date now = new Date(); + X509v3CertificateBuilder builder = initCertBuilder( + new X500Name(caCert.getIssuerDN().getName()), + now, + new Date(now.getTime() + expirationMillis), + certSubject, + certPublicKey); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // not a CA + builder.addExtension( + Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyAgreement)); + builder.addExtension( + Extension.extendedKeyUsage, + true, + new ExtendedKeyUsage(new KeyPurposeId[] { KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth })); + + builder.addExtension( + Extension.subjectAlternativeName, + false, + getLocalhostSubjectAltNames()); + return buildAndSignCertificate(caKeyPair.getPrivate(), builder); + } + + /** + * Returns subject alternative names for "localhost". + * @return the subject alternative names for "localhost". + */ + private static GeneralNames getLocalhostSubjectAltNames() throws UnknownHostException { + InetAddress[] localAddresses = InetAddress.getAllByName("localhost"); + GeneralName[] generalNames = new GeneralName[localAddresses.length + 1]; + for (int i = 0; i < localAddresses.length; i++) { + generalNames[i] = new GeneralName(GeneralName.iPAddress, new DEROctetString(localAddresses[i].getAddress())); + } + generalNames[generalNames.length - 1] = new GeneralName(GeneralName.dNSName, new DERIA5String("localhost")); + return new GeneralNames(generalNames); + } + + /** + * Helper method for newSelfSignedCACert() and newCert(). Initializes a X509v3CertificateBuilder with + * logic that's common to both methods. + * @param issuer Issuer field of the new cert. + * @param notBefore date before which the new cert is not valid. + * @param notAfter date after which the new cert is not valid. + * @param subject Subject field of the new cert. + * @param subjectPublicKey public key to store in the new cert. + * @return a X509v3CertificateBuilder that can be further customized to finish creating the new cert. + */ + private static X509v3CertificateBuilder initCertBuilder( + X500Name issuer, + Date notBefore, + Date notAfter, + X500Name subject, + PublicKey subjectPublicKey) { + return new X509v3CertificateBuilder( + issuer, + new BigInteger(SERIAL_NUMBER_MAX_BITS, PRNG), + notBefore, + notAfter, + subject, + SubjectPublicKeyInfo.getInstance(subjectPublicKey.getEncoded())); + } + + /** + * Signs the certificate being built by the given builder using the given private key and returns the certificate. + * @param privateKey the private key to sign the certificate with. + * @param builder the cert builder that contains the certificate data. + * @return the signed certificate. + * @throws IOException + * @throws OperatorCreationException + * @throws CertificateException + */ + private static X509Certificate buildAndSignCertificate( + PrivateKey privateKey, + X509v3CertificateBuilder builder) throws IOException, OperatorCreationException, CertificateException { + BcContentSignerBuilder signerBuilder; + if (privateKey.getAlgorithm().contains("RSA")) { // a little hacky way to detect key type, but it works + AlgorithmIdentifier signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().find( + "SHA256WithRSAEncryption"); + AlgorithmIdentifier digestAlgorithm = new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm); + signerBuilder = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm); + } else { // if not RSA, assume EC + AlgorithmIdentifier signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().find( + "SHA256withECDSA"); + AlgorithmIdentifier digestAlgorithm = new DefaultDigestAlgorithmIdentifierFinder().find(signatureAlgorithm); + signerBuilder = new BcECContentSignerBuilder(signatureAlgorithm, digestAlgorithm); + } + AsymmetricKeyParameter privateKeyParam = PrivateKeyFactory.createKey(privateKey.getEncoded()); + ContentSigner signer = signerBuilder.build(privateKeyParam); + return toX509Cert(builder.build(signer)); + } + + /** + * Generates a new asymmetric key pair of the given type. + * @param keyType the type of key pair to generate. + * @return the new key pair. + * @throws GeneralSecurityException if your java crypto providers are messed up. + */ + public static KeyPair generateKeyPair(X509KeyType keyType) throws GeneralSecurityException { + switch (keyType) { + case RSA: + return generateRSAKeyPair(); + case EC: + return generateECKeyPair(); + default: + throw new IllegalArgumentException("Invalid X509KeyType"); + } + } + + /** + * Generates an RSA key pair with a 2048-bit private key and F4 (65537) as the public exponent. + * @return the key pair. + */ + public static KeyPair generateRSAKeyPair() throws GeneralSecurityException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + RSAKeyGenParameterSpec keyGenSpec = new RSAKeyGenParameterSpec( + DEFAULT_RSA_KEY_SIZE_BITS, DEFAULT_RSA_PUB_EXPONENT); + keyGen.initialize(keyGenSpec, PRNG); + return keyGen.generateKeyPair(); + } + + /** + * Generates an elliptic curve key pair using the "secp256r1" aka "prime256v1" aka "NIST P-256" curve. + * @return the key pair. + */ + public static KeyPair generateECKeyPair() throws GeneralSecurityException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); + keyGen.initialize(new ECGenParameterSpec(DEFAULT_ELLIPTIC_CURVE_NAME), PRNG); + return keyGen.generateKeyPair(); + } + + /** + * PEM-encodes the given X509 certificate and private key (compatible with OpenSSL), optionally protecting the + * private key with a password. Concatenates them both and returns the result as a single string. + * This creates the PEM encoding of a key store. + * @param cert the X509 certificate to PEM-encode. + * @param privateKey the private key to PEM-encode. + * @param keyPassword an optional key password. If empty or null, the private key will not be encrypted. + * @return a String containing the PEM encodings of the certificate and private key. + * @throws IOException if converting the certificate or private key to PEM format fails. + * @throws OperatorCreationException if constructing the encryptor from the given password fails. + */ + public static String pemEncodeCertAndPrivateKey( + X509Certificate cert, + PrivateKey privateKey, + String keyPassword) throws IOException, OperatorCreationException { + return pemEncodeX509Certificate(cert) + + "\n" + + pemEncodePrivateKey(privateKey, keyPassword); + } + + /** + * PEM-encodes the given private key (compatible with OpenSSL), optionally protecting it with a password, and + * returns the result as a String. + * @param key the private key. + * @param password an optional key password. If empty or null, the private key will not be encrypted. + * @return a String containing the PEM encoding of the private key. + * @throws IOException if converting the key to PEM format fails. + * @throws OperatorCreationException if constructing the encryptor from the given password fails. + */ + public static String pemEncodePrivateKey( + PrivateKey key, + String password) throws IOException, OperatorCreationException { + StringWriter stringWriter = new StringWriter(); + JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); + OutputEncryptor encryptor = null; + if (password != null && password.length() > 0) { + encryptor = new JceOpenSSLPKCS8EncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC) + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .setRandom(PRNG) + .setPasssword(password.toCharArray()) + .build(); + } + pemWriter.writeObject(new JcaPKCS8Generator(key, encryptor)); + pemWriter.close(); + return stringWriter.toString(); + } + + /** + * PEM-encodes the given X509 certificate (compatible with OpenSSL) and returns the result as a String. + * @param cert the certificate. + * @return a String containing the PEM encoding of the certificate. + * @throws IOException if converting the certificate to PEM format fails. + */ + public static String pemEncodeX509Certificate(X509Certificate cert) throws IOException { + StringWriter stringWriter = new StringWriter(); + JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); + pemWriter.writeObject(cert); + pemWriter.close(); + return stringWriter.toString(); + } + + /** + * Encodes the given X509Certificate as a JKS TrustStore, optionally protecting the cert with a password (though + * it's unclear why one would do this since certificates only contain public information and do not need to be + * kept secret). Returns the byte array encoding of the trust store, which may be written to a file and loaded to + * instantiate the trust store at a later point or in another process. + * @param cert the certificate to serialize. + * @param keyPassword an optional password to encrypt the trust store. If empty or null, the cert will not be encrypted. + * @return the serialized bytes of the JKS trust store. + * @throws IOException + * @throws GeneralSecurityException + */ + public static byte[] certToJavaTrustStoreBytes( + X509Certificate cert, + String keyPassword) throws IOException, GeneralSecurityException { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray(); + trustStore.load(null, keyPasswordChars); + trustStore.setCertificateEntry(cert.getSubjectDN().toString(), cert); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + trustStore.store(outputStream, keyPasswordChars); + outputStream.flush(); + byte[] result = outputStream.toByteArray(); + outputStream.close(); + return result; + } + + /** + * Encodes the given X509Certificate and private key as a JKS KeyStore, optionally protecting the private key + * (and possibly the cert?) with a password. Returns the byte array encoding of the key store, which may be written + * to a file and loaded to instantiate the key store at a later point or in another process. + * @param cert the X509 certificate to serialize. + * @param privateKey the private key to serialize. + * @param keyPassword an optional key password. If empty or null, the private key will not be encrypted. + * @return the serialized bytes of the JKS key store. + * @throws IOException + * @throws GeneralSecurityException + */ + public static byte[] certAndPrivateKeyToJavaKeyStoreBytes( + X509Certificate cert, + PrivateKey privateKey, + String keyPassword) throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + char[] keyPasswordChars = keyPassword == null ? new char[0] : keyPassword.toCharArray(); + keyStore.load(null, keyPasswordChars); + keyStore.setKeyEntry( + "key", + privateKey, + keyPasswordChars, + new Certificate[] { cert }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + keyStore.store(outputStream, keyPasswordChars); + outputStream.flush(); + byte[] result = outputStream.toByteArray(); + outputStream.close(); + return result; + } + + /** + * Convenience method to convert a bouncycastle X509CertificateHolder to a java X509Certificate. + * @param certHolder a bouncycastle X509CertificateHolder. + * @return a java X509Certificate + * @throws CertificateException if the conversion fails. + */ + public static X509Certificate toX509Cert(X509CertificateHolder certHolder) throws CertificateException { + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certHolder); + } +} http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java index e726caa..6b343c3 100644 --- a/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java @@ -17,150 +17,78 @@ */ package org.apache.zookeeper.common; +import java.security.Security; +import java.util.Collection; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + import org.apache.zookeeper.PortAssignment; -import org.apache.zookeeper.ZKTestCase; import org.apache.zookeeper.client.ZKClientConfig; import org.apache.zookeeper.server.ServerCnxnFactory; -import org.bouncycastle.asn1.x500.X500NameBuilder; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -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.ContentSigner; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.junit.After; -import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.SSLSocket; -import java.io.FileOutputStream; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.Calendar; -import java.util.Date; -import java.util.Random; - -import static org.apache.zookeeper.test.ClientBase.createTmpDir; - -public class X509UtilTest extends ZKTestCase { - - private static final char[] PASSWORD = "password".toCharArray(); - private X509Certificate rootCertificate; - - private String truststorePath; - private String keystorePath; - private static KeyPair rootKeyPair; - +@RunWith(Parameterized.class) +public class X509UtilTest extends BaseX509ParameterizedTestCase { private X509Util x509Util; - private String[] customCipherSuites = new String[]{"SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"}; - - @BeforeClass - public static void createKeyPair() throws Exception { - Security.addProvider(new BouncyCastleProvider()); - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); - keyPairGenerator.initialize(4096); - rootKeyPair = keyPairGenerator.genKeyPair(); + private static final String[] customCipherSuites = new String[]{ + "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"}; + + @Parameterized.Parameters + public static Collection<Object[]> params() { + return BaseX509ParameterizedTestCase.defaultParams(); } - @AfterClass - public static void removeBouncyCastleProvider() throws Exception { - Security.removeProvider("BC"); + public X509UtilTest( + X509KeyType caKeyType, + X509KeyType certKeyType, + String keyPassword, + Integer paramIndex) { + super(paramIndex, () -> { + try { + return X509TestContext.newBuilder() + .setTempDir(tempDir) + .setKeyStorePassword(keyPassword) + .setKeyStoreKeyType(certKeyType) + .setTrustStorePassword(keyPassword) + .setTrustStoreKeyType(caKeyType) + .build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } @Before public void setUp() throws Exception { - rootCertificate = createSelfSignedCertifcate(rootKeyPair); - - String tmpDir = createTmpDir().getAbsolutePath(); - truststorePath = tmpDir + "/truststore.jks"; - keystorePath = tmpDir + "/keystore.jks"; - - x509Util = new ClientX509Util(); - - writeKeystore(rootCertificate, rootKeyPair, keystorePath); - + x509TestContext.setSystemProperties(new ClientX509Util(), KeyStoreFileType.JKS, KeyStoreFileType.JKS); System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory"); System.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, "org.apache.zookeeper.ClientCnxnSocketNetty"); - System.setProperty(x509Util.getSslKeystoreLocationProperty(), keystorePath); - System.setProperty(x509Util.getSslKeystorePasswdProperty(), new String(PASSWORD)); - System.setProperty(x509Util.getSslTruststoreLocationProperty(), truststorePath); - System.setProperty(x509Util.getSslTruststorePasswdProperty(), new String(PASSWORD)); - System.setProperty(x509Util.getSslHostnameVerificationEnabledProperty(), "false"); - - writeTrustStore(PASSWORD); - } - - private void writeKeystore(X509Certificate certificate, KeyPair keyPair, String path) throws Exception { - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null, PASSWORD); - keyStore.setKeyEntry("alias", keyPair.getPrivate(), PASSWORD, new Certificate[] { certificate }); - FileOutputStream outputStream = new FileOutputStream(path); - keyStore.store(outputStream, PASSWORD); - outputStream.flush(); - outputStream.close(); - } - - private void writeTrustStore(char[] password) throws Exception { - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(null, password); - trustStore.setCertificateEntry(rootCertificate.getSubjectDN().toString(), rootCertificate); - FileOutputStream outputStream = new FileOutputStream(truststorePath); - if (password == null) { - trustStore.store(outputStream, new char[0]); - } else { - trustStore.store(outputStream, password); - } - outputStream.flush(); - outputStream.close(); - } - - private X509Certificate createSelfSignedCertifcate(KeyPair keyPair) throws Exception { - X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); - nameBuilder.addRDN(BCStyle.CN, "localhost"); - Date notBefore = new Date(); - Calendar cal = Calendar.getInstance(); - cal.setTime(notBefore); - cal.add(Calendar.YEAR, 1); - Date notAfter = cal.getTime(); - BigInteger serialNumber = new BigInteger(128, new Random()); - - X509v3CertificateBuilder certificateBuilder = - new JcaX509v3CertificateBuilder(nameBuilder.build(), serialNumber, notBefore, notAfter, nameBuilder.build(), keyPair.getPublic()) - .addExtension(Extension.basicConstraints, true, new BasicConstraints(0)) - .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)); - - ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate()); - - return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)); + x509Util = new ClientX509Util(); } @After - public void cleanUp() throws Exception { - System.clearProperty(x509Util.getSslKeystoreLocationProperty()); - System.clearProperty(x509Util.getSslKeystorePasswdProperty()); - System.clearProperty(x509Util.getSslTruststoreLocationProperty()); - System.clearProperty(x509Util.getSslTruststorePasswdProperty()); - System.clearProperty(x509Util.getSslHostnameVerificationEnabledProperty()); + public void cleanUp() { + x509TestContext.clearSystemProperties(x509Util); System.clearProperty(x509Util.getSslOcspEnabledProperty()); System.clearProperty(x509Util.getSslCrlEnabledProperty()); System.clearProperty(x509Util.getCipherSuitesProperty()); + System.clearProperty(x509Util.getSslProtocolProperty()); System.clearProperty("com.sun.net.ssl.checkRevocation"); System.clearProperty("com.sun.security.enableCRLDP"); - Security.setProperty("com.sun.security.enableCRLDP", "false"); + Security.setProperty("ocsp.enable", Boolean.FALSE.toString()); + Security.setProperty("com.sun.security.enableCRLDP", Boolean.FALSE.toString()); + System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY); + System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET); } @Test(timeout = 5000) @@ -178,13 +106,6 @@ public class X509UtilTest extends ZKTestCase { } @Test(timeout = 5000) - public void testCreateSSLContextWithoutTrustStorePassword() throws Exception { - writeTrustStore(null); - System.clearProperty(x509Util.getSslTruststorePasswdProperty()); - x509Util.getDefaultSSLContext(); - } - - @Test(timeout = 5000, expected = X509Exception.SSLContextException.class) public void testCreateSSLContextWithoutKeyStoreLocation() throws Exception { System.clearProperty(x509Util.getSslKeystoreLocationProperty()); x509Util.getDefaultSSLContext(); @@ -192,6 +113,9 @@ public class X509UtilTest extends ZKTestCase { @Test(timeout = 5000, expected = X509Exception.SSLContextException.class) public void testCreateSSLContextWithoutKeyStorePassword() throws Exception { + if (!x509TestContext.isKeyStoreEncrypted()) { + throw new X509Exception.SSLContextException(""); + } System.clearProperty(x509Util.getSslKeystorePasswdProperty()); x509Util.getDefaultSSLContext(); } @@ -256,6 +180,182 @@ public class X509UtilTest extends ZKTestCase { Assert.assertTrue(sslServerSocket.getNeedClientAuth()); } + @Test + public void testLoadPEMKeyStore() throws Exception { + // Make sure we can instantiate a key manager from the PEM file on disk + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + x509TestContext.getKeyStorePassword(), + KeyStoreFileType.PEM.getPropertyValue()); + } + + @Test + public void testLoadPEMKeyStoreNullPassword() throws Exception { + if (!x509TestContext.getKeyStorePassword().isEmpty()) { + return; + } + // Make sure that empty password and null password are treated the same + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + null, + KeyStoreFileType.PEM.getPropertyValue()); + } + + @Test + public void testLoadPEMKeyStoreAutodetectStoreFileType() throws Exception { + // Make sure we can instantiate a key manager from the PEM file on disk + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + x509TestContext.getKeyStorePassword(), + null /* null StoreFileType means 'autodetect from file extension' */); + } + + @Test(expected = X509Exception.KeyManagerException.class) + public void testLoadPEMKeyStoreWithWrongPassword() throws Exception { + // Attempting to load with the wrong key password should fail + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + "wrong password", // intentionally use the wrong password + KeyStoreFileType.PEM.getPropertyValue()); + } + + @Test + public void testLoadPEMTrustStore() throws Exception { + // Make sure we can instantiate a trust manager from the PEM file on disk + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + x509TestContext.getTrustStorePassword(), + KeyStoreFileType.PEM.getPropertyValue(), + false, + false, + true, + true); + } + + @Test + public void testLoadPEMTrustStoreNullPassword() throws Exception { + if (!x509TestContext.getTrustStorePassword().isEmpty()) { + return; + } + // Make sure that empty password and null password are treated the same + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + null, + KeyStoreFileType.PEM.getPropertyValue(), + false, + false, + true, + true); + + } + + @Test + public void testLoadPEMTrustStoreAutodetectStoreFileType() throws Exception { + // Make sure we can instantiate a trust manager from the PEM file on disk + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), + x509TestContext.getTrustStorePassword(), + null, // null StoreFileType means 'autodetect from file extension' + false, + false, + true, + true); + } + + @Test + public void testLoadJKSKeyStore() throws Exception { + // Make sure we can instantiate a key manager from the JKS file on disk + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + x509TestContext.getKeyStorePassword(), + KeyStoreFileType.JKS.getPropertyValue()); + } + + @Test + public void testLoadJKSKeyStoreNullPassword() throws Exception { + if (!x509TestContext.getKeyStorePassword().isEmpty()) { + return; + } + // Make sure that empty password and null password are treated the same + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + null, + KeyStoreFileType.JKS.getPropertyValue()); + } + + @Test + public void testLoadJKSKeyStoreAutodetectStoreFileType() throws Exception { + // Make sure we can instantiate a key manager from the JKS file on disk + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + x509TestContext.getKeyStorePassword(), + null /* null StoreFileType means 'autodetect from file extension' */); + } + + @Test(expected = X509Exception.KeyManagerException.class) + public void testLoadJKSKeyStoreWithWrongPassword() throws Exception { + // Attempting to load with the wrong key password should fail + X509KeyManager km = X509Util.createKeyManager( + x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + "wrong password", + KeyStoreFileType.JKS.getPropertyValue()); + } + + @Test + public void testLoadJKSTrustStore() throws Exception { + // Make sure we can instantiate a trust manager from the JKS file on disk + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + x509TestContext.getTrustStorePassword(), + KeyStoreFileType.JKS.getPropertyValue(), + true, + true, + true, + true); + } + + @Test + public void testLoadJKSTrustStoreNullPassword() throws Exception { + if (!x509TestContext.getTrustStorePassword().isEmpty()) { + return; + } + // Make sure that empty password and null password are treated the same + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + null, + KeyStoreFileType.JKS.getPropertyValue(), + false, + false, + true, + true); + } + + @Test + public void testLoadJKSTrustStoreAutodetectStoreFileType() throws Exception { + // Make sure we can instantiate a trust manager from the JKS file on disk + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + x509TestContext.getTrustStorePassword(), + null, // null StoreFileType means 'autodetect from file extension' + true, + true, + true, + true); + } + + @Test(expected = X509Exception.TrustManagerException.class) + public void testLoadJKSTrustStoreWithWrongPassword() throws Exception { + // Attempting to load with the wrong key password should fail + X509TrustManager tm = X509Util.createTrustManager( + x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), + "wrong password", + KeyStoreFileType.JKS.getPropertyValue(), + true, + true, + true, + true); + } + // Warning: this will reset the x509Util private void setCustomCipherSuites() { System.setProperty(x509Util.getCipherSuitesProperty(), customCipherSuites[0] + "," + customCipherSuites[1]); http://git-wip-us.apache.org/repos/asf/zookeeper/blob/03286f29/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java ---------------------------------------------------------------------- diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java new file mode 100644 index 0000000..d0d3dc7 --- /dev/null +++ b/zookeeper-server/src/test/java/org/apache/zookeeper/util/PemReaderTest.java @@ -0,0 +1,137 @@ +/** + * 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.zookeeper.util; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.apache.zookeeper.common.BaseX509ParameterizedTestCase; +import org.apache.zookeeper.common.KeyStoreFileType; +import org.apache.zookeeper.common.X509KeyType; +import org.apache.zookeeper.common.X509TestContext; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class PemReaderTest extends BaseX509ParameterizedTestCase { + + @Parameterized.Parameters + public static Collection<Object[]> params() { + return BaseX509ParameterizedTestCase.defaultParams(); + } + + public PemReaderTest( + X509KeyType caKeyType, + X509KeyType certKeyType, + String keyPassword, + Integer paramIndex) { + super(paramIndex, () -> { + try { + return X509TestContext.newBuilder() + .setTempDir(tempDir) + .setKeyStorePassword(keyPassword) + .setKeyStoreKeyType(certKeyType) + .setTrustStorePassword(keyPassword) + .setTrustStoreKeyType(caKeyType) + .build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + public void testLoadPrivateKeyFromKeyStore() throws IOException, GeneralSecurityException { + Optional<String> optPassword = x509TestContext.getKeyStorePassword().length() > 0 + ? Optional.of(x509TestContext.getKeyStorePassword()) + : Optional.empty(); + PrivateKey privateKey = PemReader.loadPrivateKey( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM), optPassword); + Assert.assertEquals(x509TestContext.getKeyStoreKeyPair().getPrivate(), privateKey); + } + + // Try to load a password-protected private key without providing a password + @Test(expected = GeneralSecurityException.class) + public void testLoadEncryptedPrivateKeyFromKeyStoreWithoutPassword() throws GeneralSecurityException, IOException { + if (!x509TestContext.isKeyStoreEncrypted()) { + throw new GeneralSecurityException(); // this case is not tested so throw the expected exception + } + PemReader.loadPrivateKey(x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM), Optional.empty()); + } + + // Try to load a password-protected private key with the wrong password + @Test(expected = GeneralSecurityException.class) + public void testLoadEncryptedPrivateKeyFromKeyStoreWithWrongPassword() throws GeneralSecurityException, IOException { + if (!x509TestContext.isKeyStoreEncrypted()) { + throw new GeneralSecurityException(); // this case is not tested so throw the expected exception + } + PemReader.loadPrivateKey( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM), + Optional.of("wrong password")); + } + + // Try to load a non-protected private key while providing a password + @Test(expected = IOException.class) + public void testLoadUnencryptedPrivateKeyFromKeyStoreWithWrongPassword() throws GeneralSecurityException, IOException { + if (x509TestContext.isKeyStoreEncrypted()) { + throw new IOException(); + } + PemReader.loadPrivateKey( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM), + Optional.of("wrong password")); + } + + // Expect this to fail, the trust store does not contain a private key + @Test(expected = KeyStoreException.class) + public void testLoadPrivateKeyFromTrustStore() throws IOException, GeneralSecurityException { + PemReader.loadPrivateKey( + x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM), Optional.empty()); + } + + // Expect this to fail, the trust store does not contain a private key + @Test(expected = KeyStoreException.class) + public void testLoadPrivateKeyFromTrustStoreWithPassword() throws IOException, GeneralSecurityException { + PemReader.loadPrivateKey( + x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM), Optional.of("foobar")); + } + + @Test + public void testLoadCertificateFromKeyStore() throws IOException, GeneralSecurityException { + List<X509Certificate> certs = PemReader.readCertificateChain( + x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM)); + Assert.assertEquals(1, certs.size()); + Assert.assertEquals(x509TestContext.getKeyStoreCertificate(), certs.get(0)); + } + + @Test + public void testLoadCertificateFromTrustStore() throws IOException, GeneralSecurityException { + List<X509Certificate> certs = PemReader.readCertificateChain( + x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM)); + Assert.assertEquals(1, certs.size()); + Assert.assertEquals(x509TestContext.getTrustStoreCertificate(), certs.get(0)); + } +}