summaryrefslogtreecommitdiff
path: root/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509TestContext.java
blob: 71fde1e6c8cee7bb135bd3ad0e42594e983fb7a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
/*
 * 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 static java.util.Objects.requireNonNull;
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;

/**
 * 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 File trustStorePkcs12File;

    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 File keyStorePkcs12File;

    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);
        trustStorePkcs12File = trustStorePemFile = trustStoreJksFile = null;
        keyStorePkcs12File = 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();
        case PKCS12:
            return getTrustStorePkcs12File();
        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) {
            File trustStoreJksFile = File.createTempFile(TRUST_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
            trustStoreJksFile.deleteOnExit();
            try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStoreJksFile)) {
                byte[] bytes = X509TestHelpers.certToJavaTrustStoreBytes(trustStoreCertificate, trustStorePassword);
                trustStoreOutputStream.write(bytes);
                trustStoreOutputStream.flush();
            } catch (GeneralSecurityException e) {
                throw new IOException(e);
            }
            this.trustStoreJksFile = trustStoreJksFile;
        }
        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;
    }

    private File getTrustStorePkcs12File() throws IOException {
        if (trustStorePkcs12File == null) {
            File trustStorePkcs12File = File.createTempFile(TRUST_STORE_PREFIX, KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
            trustStorePkcs12File.deleteOnExit();
            try (final FileOutputStream trustStoreOutputStream = new FileOutputStream(trustStorePkcs12File)) {
                byte[] bytes = X509TestHelpers.certToPKCS12TrustStoreBytes(trustStoreCertificate, trustStorePassword);
                trustStoreOutputStream.write(bytes);
                trustStoreOutputStream.flush();
            } catch (GeneralSecurityException e) {
                throw new IOException(e);
            }
            this.trustStorePkcs12File = trustStorePkcs12File;
        }
        return trustStorePkcs12File;
    }

    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();
        case PKCS12:
            return getKeyStorePkcs12File();
        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) {
            File keyStoreJksFile = File.createTempFile(KEY_STORE_PREFIX, KeyStoreFileType.JKS.getDefaultFileExtension(), tempDir);
            keyStoreJksFile.deleteOnExit();
            try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStoreJksFile)) {
                byte[] bytes = X509TestHelpers.certAndPrivateKeyToJavaKeyStoreBytes(keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
                keyStoreOutputStream.write(bytes);
                keyStoreOutputStream.flush();
            } catch (GeneralSecurityException e) {
                throw new IOException(e);
            }
            this.keyStoreJksFile = keyStoreJksFile;
        }
        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;
    }

    private File getKeyStorePkcs12File() throws IOException {
        if (keyStorePkcs12File == null) {
            File keyStorePkcs12File = File.createTempFile(KEY_STORE_PREFIX, KeyStoreFileType.PKCS12.getDefaultFileExtension(), tempDir);
            keyStorePkcs12File.deleteOnExit();
            try (final FileOutputStream keyStoreOutputStream = new FileOutputStream(keyStorePkcs12File)) {
                byte[] bytes = X509TestHelpers.certAndPrivateKeyToPKCS12Bytes(keyStoreCertificate, keyStoreKeyPair.getPrivate(), keyStorePassword);
                keyStoreOutputStream.write(bytes);
                keyStoreOutputStream.flush();
            } catch (GeneralSecurityException e) {
                throw new IOException(e);
            }
            this.keyStorePkcs12File = keyStorePkcs12File;
        }
        return keyStorePkcs12File;
    }

    /**
     * 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();
    }

}