accepted_protocols, String algorithm, String store_type,
String require_client_auth, boolean require_endpoint_verification, Boolean enabled,
- Boolean optional)
+ Boolean optional, DurationSpec.IntMinutesBound max_certificate_validity_period,
+ DurationSpec.IntMinutesBound certificate_validity_warn_threshold)
{
this.ssl_context_factory = ssl_context_factory;
this.keystore = keystore;
@@ -222,6 +231,8 @@ public class EncryptionOptions
this.require_endpoint_verification = require_endpoint_verification;
this.enabled = enabled;
this.optional = optional;
+ this.max_certificate_validity_period = max_certificate_validity_period;
+ this.certificate_validity_warn_threshold = certificate_validity_warn_threshold;
}
public EncryptionOptions(EncryptionOptions options)
@@ -239,7 +250,9 @@ public class EncryptionOptions
require_client_auth = options.require_client_auth;
require_endpoint_verification = options.require_endpoint_verification;
enabled = options.enabled;
- this.optional = options.optional;
+ optional = options.optional;
+ max_certificate_validity_period = options.max_certificate_validity_period;
+ certificate_validity_warn_threshold = options.certificate_validity_warn_threshold;
}
/* Computes enabled and optional before use. Because the configuration can be loaded
@@ -318,6 +331,7 @@ public class EncryptionOptions
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.REQUIRE_ENDPOINT_VERIFICATION, this.require_endpoint_verification);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ENABLED, this.enabled);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.OPTIONAL, this.optional);
+ putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.MAX_CERTIFICATE_VALIDITY_PERIOD, this.max_certificate_validity_period);
}
private void initializeSslContextFactory()
@@ -371,6 +385,7 @@ public class EncryptionOptions
* Sets if encryption should be enabled for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating enabled state
* is probably a bad idea.
+ *
* @param enabled value to set
*/
public void setEnabled(Boolean enabled)
@@ -384,8 +399,9 @@ public class EncryptionOptions
* Explicitly providing a value in the configuration take precedent.
* If no optional value is set and !isEnabled(), then optional connections are allowed
* if a keystore exists. Without it, it would be impossible to establish the connections.
- *
+ *
* Return type is Boolean even though it can never be null so that snakeyaml can find it
+ *
* @return if the channel may be encrypted
*/
public Boolean getOptional()
@@ -398,6 +414,7 @@ public class EncryptionOptions
* Sets if encryption should be optional for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating enabled state
* is probably a bad idea.
+ *
* @param optional value to set
*/
public void setOptional(Boolean optional)
@@ -410,6 +427,7 @@ public class EncryptionOptions
* Sets accepted TLS protocol for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating protocol state
* is probably a bad idea.
+ *
* @param protocol value to set
*/
@VisibleForTesting
@@ -427,6 +445,7 @@ public class EncryptionOptions
* Sets accepted TLS protocols for this channel. Note that this should only be called by
* the configuration parser or tests. It is public only for that purpose, mutating protocol state
* is probably a bad idea. The function casing is required for snakeyaml to find this setter for the protected field.
+ *
* @param accepted_protocols value to set
*/
public void setAcceptedProtocols(List accepted_protocols)
@@ -474,25 +493,25 @@ public class EncryptionOptions
public EncryptionOptions withSslContextFactory(ParameterizedClass sslContextFactoryClass)
{
return new EncryptionOptions(sslContextFactoryClass, keystore, keystore_password, truststore,
- truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
- store_type, require_client_auth, require_endpoint_verification,enabled,
- optional).applyConfig();
+ truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
+ store_type, require_client_auth, require_endpoint_verification, enabled,
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withKeyStore(String keystore)
{
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
- truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
+ truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withKeyStorePassword(String keystore_password)
{
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
- truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
+ truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withTrustStore(String truststore)
@@ -500,7 +519,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withTrustStorePassword(String truststore_password)
@@ -508,7 +527,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withCipherSuites(List cipher_suites)
@@ -516,15 +535,16 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
- public EncryptionOptions withCipherSuites(String ... cipher_suites)
+ public EncryptionOptions withCipherSuites(String... cipher_suites)
{
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, ImmutableList.copyOf(cipher_suites), protocol,
accepted_protocols, algorithm, store_type, require_client_auth,
- require_endpoint_verification, enabled, optional).applyConfig();
+ require_endpoint_verification, enabled, optional, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withProtocol(String protocol)
@@ -532,17 +552,17 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withAcceptedProtocols(List accepted_protocols)
{
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
- truststore_password, cipher_suites,protocol, accepted_protocols == null ? null :
- ImmutableList.copyOf(accepted_protocols),
+ truststore_password, cipher_suites, protocol, accepted_protocols == null ? null :
+ ImmutableList.copyOf(accepted_protocols),
algorithm, store_type, require_client_auth, require_endpoint_verification,
- enabled, optional).applyConfig();
+ enabled, optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
@@ -551,7 +571,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withStoreType(String store_type)
@@ -559,7 +579,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withRequireClientAuth(ClientAuth require_client_auth)
@@ -567,7 +587,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth.value, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
@@ -575,7 +595,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withEnabled(boolean enabled)
@@ -583,7 +603,7 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
}
public EncryptionOptions withOptional(Boolean optional)
@@ -591,7 +611,23 @@ public class EncryptionOptions
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
- optional).applyConfig();
+ optional, max_certificate_validity_period, max_certificate_validity_period).applyConfig();
+ }
+
+ public EncryptionOptions withMaxCertificateValidityPeriod(DurationSpec.IntMinutesBound maxCertificateValidityPeriod)
+ {
+ return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
+ truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
+ store_type, require_client_auth, require_endpoint_verification, enabled,
+ optional, maxCertificateValidityPeriod, certificate_validity_warn_threshold).applyConfig();
+ }
+
+ public EncryptionOptions withCertificateValidityWarnThreshold(DurationSpec.IntMinutesBound certificateValidityWarnThreshold)
+ {
+ return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
+ truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
+ store_type, require_client_auth, require_endpoint_verification, enabled,
+ optional, max_certificate_validity_period, certificateValidityWarnThreshold).applyConfig();
}
/**
@@ -676,11 +712,13 @@ public class EncryptionOptions
List cipher_suites, String protocol, List accepted_protocols,
String algorithm, String store_type, String require_client_auth,
boolean require_endpoint_verification, Boolean optional,
- InternodeEncryption internode_encryption, boolean legacy_ssl_storage_port_enabled)
+ InternodeEncryption internode_encryption, boolean legacy_ssl_storage_port_enabled,
+ DurationSpec.IntMinutesBound maxCertificateAgeMinutes,
+ DurationSpec.IntMinutesBound certificateValidityWarnThreshold)
{
super(sslContextFactoryClass, keystore, keystore_password, truststore, truststore_password, cipher_suites,
- protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
- null, optional);
+ protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
+ null, optional, maxCertificateAgeMinutes, certificateValidityWarnThreshold);
this.internode_encryption = internode_encryption;
this.legacy_ssl_storage_port_enabled = legacy_ssl_storage_port_enabled;
this.outbound_keystore = outbound_keystore;
@@ -807,6 +845,7 @@ public class EncryptionOptions
return result;
}
+ @Override
public ServerEncryptionOptions withSslContextFactory(ParameterizedClass sslContextFactoryClass)
{
return new ServerEncryptionOptions(sslContextFactoryClass, keystore, keystore_password,
@@ -814,9 +853,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withKeyStore(String keystore)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -824,9 +865,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withKeyStorePassword(String keystore_password)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -834,9 +877,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withTrustStore(String truststore)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -844,9 +889,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withTrustStorePassword(String truststore_password)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -854,9 +901,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withCipherSuites(List cipher_suites)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -864,9 +913,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withCipherSuites(String... cipher_suites)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -874,9 +925,11 @@ public class EncryptionOptions
truststore_password, Arrays.asList(cipher_suites), protocol,
accepted_protocols, algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withProtocol(String protocol)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -884,9 +937,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withAcceptedProtocols(List accepted_protocols)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -894,9 +949,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withAlgorithm(String algorithm)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -904,9 +961,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withStoreType(String store_type)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -914,9 +973,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withRequireClientAuth(ClientAuth require_client_auth)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -924,9 +985,11 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth.value,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
+ @Override
public ServerEncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
{
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
@@ -934,7 +997,8 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
public ServerEncryptionOptions withOptional(boolean optional)
@@ -944,7 +1008,8 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
public ServerEncryptionOptions withInternodeEncryption(InternodeEncryption internode_encryption)
@@ -954,7 +1019,8 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
public ServerEncryptionOptions withLegacySslStoragePort(boolean enable_legacy_ssl_storage_port)
@@ -964,7 +1030,8 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- enable_legacy_ssl_storage_port).applyConfigInternal();
+ enable_legacy_ssl_storage_port, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
public ServerEncryptionOptions withOutboundKeystore(String outboundKeystore)
@@ -974,7 +1041,8 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
}
public ServerEncryptionOptions withOutboundKeystorePassword(String outboundKeystorePassword)
@@ -984,7 +1052,20 @@ public class EncryptionOptions
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
- legacy_ssl_storage_port_enabled).applyConfigInternal();
+ legacy_ssl_storage_port_enabled, max_certificate_validity_period,
+ max_certificate_validity_period).applyConfigInternal();
+ }
+
+ @Override
+ public ServerEncryptionOptions withMaxCertificateValidityPeriod(DurationSpec.IntMinutesBound maxCertificateValidityPeriod)
+ {
+ return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password,
+ outbound_keystore, outbound_keystore_password, truststore,
+ truststore_password, cipher_suites, protocol, accepted_protocols,
+ algorithm, store_type, require_client_auth,
+ require_endpoint_verification, optional, internode_encryption,
+ legacy_ssl_storage_port_enabled, maxCertificateValidityPeriod,
+ certificate_validity_warn_threshold).applyConfigInternal();
}
}
}
diff --git a/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java b/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java
new file mode 100644
index 0000000000..bb32f0416f
--- /dev/null
+++ b/src/java/org/apache/cassandra/metrics/MutualTlsMetrics.java
@@ -0,0 +1,50 @@
+/*
+ * 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.cassandra.metrics;
+
+import com.codahale.metrics.Histogram;
+
+import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
+
+/**
+ * Captures metrics related to Mutual TLS certificate expiration and certificate age for client
+ * and internode connections
+ */
+public class MutualTlsMetrics
+{
+ private static final MetricNameFactory factory = new DefaultNameFactory("MutualTls");
+
+ public final static MutualTlsMetrics instance = new MutualTlsMetrics();
+
+ /**
+ * Histogram of expiration days for client certificates
+ */
+ public final Histogram clientCertificateExpirationDays;
+
+ /**
+ * Histogram of expiration days for internode certificates
+ */
+ public final Histogram internodeCertificateExpirationDays;
+
+ public MutualTlsMetrics()
+ {
+ clientCertificateExpirationDays = Metrics.histogram(factory.createMetricName("ClientCertificateExpirationDays"), true);
+ internodeCertificateExpirationDays = Metrics.histogram(factory.createMetricName("InternodeCertificateExpirationDays"), true);
+ }
+}
diff --git a/test/data/config/version=5.0-alpha1.yml b/test/data/config/version=5.0-alpha1.yml
index 01a5f90ea6..ce1da70bd9 100644
--- a/test/data/config/version=5.0-alpha1.yml
+++ b/test/data/config/version=5.0-alpha1.yml
@@ -388,6 +388,8 @@ server_encryption_options:
keystore: "java.lang.String"
truststore: "java.lang.String"
algorithm: "java.lang.String"
+ max_certificate_validity_period: "org.apache.cassandra.config.DurationSpec.IntMinutesBound"
+ certificate_validity_warn_threshold: "org.apache.cassandra.config.DurationSpec.IntMinutesBound"
partition_tombstones_fail_threshold: "java.lang.Long"
traverse_auth_from_root: "java.lang.Boolean"
denylist_refresh: "org.apache.cassandra.config.DurationSpec.IntSecondsBound"
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java
index bed9bc6f39..3d3b9f3958 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java
+++ b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java
@@ -1295,6 +1295,15 @@ public class ClusterUtils
return address.getAddress().getHostAddress() + ":" + address.getPort();
}
+ /**
+ * @return the native address in host:port format (ex. 127.0.0.1:9042)
+ */
+ public static InetSocketAddress getNativeInetSocketAddress(IInstance target)
+ {
+ return new InetSocketAddress(target.config().broadcastAddress().getAddress(),
+ getIntConfig(target.config(), "native_transport_port", 9042));
+ }
+
/**
* Get the broadcast address InetAddess string (ex. localhost/127.0.0.1 or /127.0.0.1)
*/
@@ -1303,6 +1312,27 @@ public class ClusterUtils
return target.config().broadcastAddress().getAddress().toString();
}
+ /**
+ * Tries to return the integer configuration from the {@code config}, fallsback to {@code defaultValue}
+ * when it fails to retrieve the value.
+ *
+ * @param config the config instance
+ * @param configName the name of the configuration
+ * @param defaultValue the default value
+ * @return the integer value from the configuration, or the default value when it fails to retrieve it
+ */
+ public static int getIntConfig(IInstanceConfig config, String configName, int defaultValue)
+ {
+ try
+ {
+ return config.getInt(configName);
+ }
+ catch (NullPointerException npe)
+ {
+ return defaultValue;
+ }
+ }
+
public static final class RingInstanceDetails
{
private final String address;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/JavaDriverUtils.java b/test/distributed/org/apache/cassandra/distributed/test/JavaDriverUtils.java
index c7c478b478..ae25731458 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/JavaDriverUtils.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/JavaDriverUtils.java
@@ -18,10 +18,16 @@
package org.apache.cassandra.distributed.test;
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
import com.datastax.driver.core.ProtocolVersion;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.ICluster;
import org.apache.cassandra.distributed.api.IInstance;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
public final class JavaDriverUtils
{
@@ -31,10 +37,16 @@ public final class JavaDriverUtils
public static com.datastax.driver.core.Cluster create(ICluster extends IInstance> dtest)
{
- return create(dtest, null);
+ return create(dtest, null, null);
}
public static com.datastax.driver.core.Cluster create(ICluster extends IInstance> dtest, ProtocolVersion version)
+ {
+ return create(dtest, version, null);
+ }
+
+ public static com.datastax.driver.core.Cluster create(ICluster extends IInstance> dtest, ProtocolVersion version,
+ Consumer overrideBuilder)
{
if (dtest.size() == 0)
throw new IllegalArgumentException("Attempted to open java driver for empty cluster");
@@ -47,13 +59,22 @@ public final class JavaDriverUtils
com.datastax.driver.core.Cluster.Builder builder = com.datastax.driver.core.Cluster.builder();
- //TODO support port
- //TODO support auth
- dtest.stream().forEach(i -> builder.addContactPoint(i.broadcastAddress().getAddress().getHostAddress()));
+ List contactPoints = buildContactPoints(dtest);
+ builder.addContactPointsWithPorts(contactPoints);
if (version != null)
builder.withProtocolVersion(version);
+ if (overrideBuilder != null)
+ overrideBuilder.accept(builder);
+
return builder.build();
}
+
+ public static List buildContactPoints(ICluster extends IInstance> dtest)
+ {
+ return dtest.stream()
+ .map(ClusterUtils::getNativeInetSocketAddress)
+ .collect(Collectors.toList());
+ }
}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/auth/MutualTlsCertificateValidityPeriodTest.java b/test/distributed/org/apache/cassandra/distributed/test/auth/MutualTlsCertificateValidityPeriodTest.java
new file mode 100644
index 0000000000..4f65b7ad75
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/auth/MutualTlsCertificateValidityPeriodTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.cassandra.distributed.test.auth;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.StreamSupport;
+import javax.net.ssl.SSLException;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.codahale.metrics.Histogram;
+import com.datastax.driver.core.PlainTextAuthProvider;
+import com.datastax.driver.core.RemoteEndpointAwareJdkSSLOptions;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.Row;
+import com.datastax.driver.core.SSLOptions;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.SimpleStatement;
+import com.datastax.driver.core.policies.LoadBalancingPolicy;
+import org.apache.cassandra.config.EncryptionOptions;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.ICluster;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
+import org.apache.cassandra.distributed.test.JavaDriverUtils;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+import org.apache.cassandra.distributed.util.Auth;
+import org.apache.cassandra.distributed.util.SingleHostLoadBalancingPolicy;
+import org.apache.cassandra.metrics.ClearableHistogram;
+import org.apache.cassandra.metrics.MutualTlsMetrics;
+import org.apache.cassandra.security.ISslContextFactory;
+import org.apache.cassandra.transport.SimpleClientSslContextFactory;
+import org.apache.cassandra.utils.tls.CertificateBuilder;
+import org.apache.cassandra.utils.tls.CertificateBundle;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.InstanceOfAssertFactories;
+
+import static org.apache.cassandra.auth.CassandraRoleManager.DEFAULT_SUPERUSER_NAME;
+import static org.apache.cassandra.auth.CassandraRoleManager.DEFAULT_SUPERUSER_PASSWORD;
+import static org.apache.cassandra.transport.TlsTestUtils.CLIENT_TRUSTSTORE_PASSWORD;
+import static org.apache.cassandra.transport.TlsTestUtils.SERVER_KEYSTORE_PASSWORD;
+import static org.apache.cassandra.transport.TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD;
+import static org.assertj.core.api.Assertions.as;
+import static org.assertj.core.api.Assertions.fail;
+
+/**
+ * Tests mTLS certificate validity period functionality
+ */
+public class MutualTlsCertificateValidityPeriodTest extends TestBaseImpl
+{
+ private static final String IDENTITY = "spiffe://test.cassandra.apache.org/dTest/mtls";
+ private static ICluster CLUSTER;
+ private static final char[] KEYSTORE_PASSWORD = "cassandra".toCharArray();
+
+ @ClassRule
+ public static TemporaryFolder tempFolder = new TemporaryFolder();
+
+ static CertificateBundle CA;
+ static Path truststorePath;
+
+ @BeforeClass
+ public static void setupClass() throws Exception
+ {
+ Cluster.Builder builder = Cluster.build(1);
+
+ CA = new CertificateBuilder().subject("CN=Apache Cassandra Root CA, OU=Certification Authority, O=Unknown, C=Unknown")
+ .alias("fakerootca")
+ .isCertificateAuthority(true)
+ .buildSelfSigned();
+
+ truststorePath = CA.toTempKeyStorePath(tempFolder.getRoot().toPath(),
+ SERVER_TRUSTSTORE_PASSWORD.toCharArray(),
+ SERVER_TRUSTSTORE_PASSWORD.toCharArray());
+
+
+ CertificateBundle keystore = new CertificateBuilder().subject("CN=Apache Cassandra, OU=ssl_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
+ .addSanDnsName(InetAddress.getLocalHost().getCanonicalHostName())
+ .addSanDnsName(InetAddress.getLocalHost().getHostName())
+ .buildIssuedBy(CA);
+
+ Path serverKeystorePath = keystore.toTempKeyStorePath(tempFolder.getRoot().toPath(),
+ SERVER_KEYSTORE_PASSWORD.toCharArray(),
+ SERVER_KEYSTORE_PASSWORD.toCharArray());
+
+ builder.withConfig(c -> c.set("authenticator.class_name", "org.apache.cassandra.auth.MutualTlsWithPasswordFallbackAuthenticator")
+ .set("authenticator.parameters", Collections.singletonMap("validator_class_name", "org.apache.cassandra.auth.SpiffeCertificateValidator"))
+ .set("role_manager", "CassandraRoleManager")
+ .set("authorizer", "CassandraAuthorizer")
+ .set("client_encryption_options.enabled", "true")
+ .set("client_encryption_options.require_client_auth", "optional")
+ .set("client_encryption_options.keystore", serverKeystorePath.toString())
+ .set("client_encryption_options.keystore_password", SERVER_KEYSTORE_PASSWORD)
+ .set("client_encryption_options.truststore", truststorePath.toString())
+ .set("client_encryption_options.truststore_password", SERVER_TRUSTSTORE_PASSWORD)
+ .set("client_encryption_options.require_endpoint_verification", "false")
+ .set("client_encryption_options.max_certificate_validity_period", "30d")
+ .set("client_encryption_options.certificate_validity_warn_threshold", "5d")
+ .with(Feature.NATIVE_PROTOCOL, Feature.GOSSIP));
+ CLUSTER = builder.start();
+
+ configureIdentity();
+ }
+
+ @AfterClass
+ public static void teardown() throws Exception
+ {
+ if (CLUSTER != null)
+ CLUSTER.close();
+ }
+
+ @After
+ public void afterEach()
+ {
+ // reset metrics
+ CLUSTER.get(1).runOnInstance(() -> {
+ Histogram client = MutualTlsMetrics.instance.clientCertificateExpirationDays;
+ Histogram internode = MutualTlsMetrics.instance.internodeCertificateExpirationDays;
+
+ if (client instanceof ClearableHistogram)
+ {
+ ((ClearableHistogram) client).clear();
+ }
+
+ if (internode instanceof ClearableHistogram)
+ {
+ ((ClearableHistogram) internode).clear();
+ }
+ });
+ }
+
+ @Test
+ public void testExpiringCertificate() throws Exception
+ {
+ Path clientKeystorePath = generateClientCertificate(null);
+
+ com.datastax.driver.core.Cluster driver = JavaDriverUtils.create(CLUSTER, null, b -> b.withSSL(getSSLOptions(clientKeystorePath)));
+
+ testWithDriver(driver, (Session session) -> {
+ ResultSet clientView = session.execute(new SimpleStatement("SELECT * FROM system_views.clients"));
+ Assertions.assertThat(clientView).isNotNull().isNotEmpty();
+
+ Optional thisClient = StreamSupport.stream(clientView.spliterator(), false)
+ .filter(row -> "cassandra_ssl_test".equals(row.getString("username")))
+ .findFirst();
+
+ Assertions.assertThat(thisClient).isPresent();
+ Row row = thisClient.get();
+ Map authenticationMetadata = row.getMap("authentication_metadata", String.class, String.class);
+
+ Assertions.assertThat(authenticationMetadata).isNotNull().hasSize(1)
+ .containsKey("identity")
+ .extractingByKey("identity", as(InstanceOfAssertFactories.STRING)).isEqualTo(IDENTITY);
+ Assertions.assertThat(row.getString("authentication_mode")).isEqualTo("MutualTls");
+ Assertions.assertThat(CLUSTER.get(1).logs().grep("Certificate with identity '" + IDENTITY + "' will expire").getResult())
+ .isNotEmpty();
+ CLUSTER.get(1).runOnInstance(() -> Assertions.assertThat(MutualTlsMetrics.instance.clientCertificateExpirationDays.getCount()).isEqualTo(2));
+ });
+ }
+
+ @Test
+ public void testCertificateReachingMaxValidityPeriod() throws Exception
+ {
+ Path clientKeystorePath = generateClientCertificate(b -> b.notBefore(Instant.now().minus(26, ChronoUnit.DAYS))
+ .notAfter(Instant.now().plus(4, ChronoUnit.DAYS).minus(1, ChronoUnit.MINUTES)));
+
+ com.datastax.driver.core.Cluster driver = JavaDriverUtils.create(CLUSTER, null, b -> b.withSSL(getSSLOptions(clientKeystorePath)));
+
+ testWithDriver(driver, (Session session) -> {
+ ResultSet clientView = session.execute(new SimpleStatement("SELECT * FROM system_views.clients"));
+ Assertions.assertThat(clientView).isNotNull().isNotEmpty();
+
+ Optional thisClient = StreamSupport.stream(clientView.spliterator(), false)
+ .filter(row -> "cassandra_ssl_test".equals(row.getString("username")))
+ .findFirst();
+
+ Assertions.assertThat(thisClient).isPresent();
+ Row row = thisClient.get();
+ Map authenticationMetadata = row.getMap("authentication_metadata", String.class, String.class);
+
+ Assertions.assertThat(authenticationMetadata).isNotNull().hasSize(1)
+ .containsKey("identity")
+ .extractingByKey("identity", as(InstanceOfAssertFactories.STRING)).isEqualTo(IDENTITY);
+ Assertions.assertThat(row.getString("authentication_mode")).isEqualTo("MutualTls");
+ Assertions.assertThat(CLUSTER.get(1).logs().grep("Certificate with identity '" + IDENTITY + "' will expire").getResult())
+ .isNotEmpty();
+ CLUSTER.get(1).runOnInstance(() -> Assertions.assertThat(MutualTlsMetrics.instance.clientCertificateExpirationDays.getCount()).isGreaterThanOrEqualTo(2));
+ });
+ }
+
+ @Test
+ public void testFailsWhenCertificateExceedsMaxAllowedValidityPeriod() throws Exception
+ {
+ Path clientKeystorePath = generateClientCertificate(b -> b.notAfter(Instant.now().plus(365, ChronoUnit.DAYS)));
+
+ com.datastax.driver.core.Cluster driver = JavaDriverUtils.create(CLUSTER, null, b -> b.withSSL(getSSLOptions(clientKeystorePath)));
+
+ try
+ {
+ testWithDriver(driver, null);
+ fail("Should not be able to connect when the certificate exceeds the maximum allowed validity period");
+ }
+ catch (com.datastax.driver.core.exceptions.NoHostAvailableException exception)
+ {
+ Assertions.assertThat(exception)
+ .hasMessageContaining("The validity period of the provided certificate (366 days) exceeds the maximum allowed validity period of 30 days");
+ }
+ }
+
+ @Test
+ public void testFailsWhenCertificateIsExpired() throws Exception
+ {
+ Path clientKeystorePath = generateClientCertificate(b -> b.notBefore(Instant.now().minus(30, ChronoUnit.DAYS))
+ .notAfter(Instant.now().minus(10, ChronoUnit.DAYS)));
+
+ com.datastax.driver.core.Cluster driver = JavaDriverUtils.create(CLUSTER, null, b -> b.withSSL(getSSLOptions(clientKeystorePath)));
+
+ try
+ {
+ testWithDriver(driver,
+ session -> CLUSTER.get(1).runOnInstance(() -> Assertions.assertThat(MutualTlsMetrics.instance.clientCertificateExpirationDays.getCount()).isZero()));
+ fail("Should not be able to connect when the certificate is expired");
+ }
+ catch (com.datastax.driver.core.exceptions.NoHostAvailableException exception)
+ {
+ Assertions.assertThat(exception).hasMessageContaining("Channel has been closed");
+ }
+ }
+
+ private void testWithDriver(com.datastax.driver.core.Cluster providedDriver, Consumer consumer)
+ {
+ try (com.datastax.driver.core.Cluster driver = providedDriver;
+ Session session = driver.connect())
+ {
+ if (consumer != null)
+ {
+ consumer.accept(session);
+ }
+ }
+ }
+
+ public static SSLOptions getSSLOptions(Path keystorePath) throws RuntimeException
+ {
+ try
+ {
+ return RemoteEndpointAwareJdkSSLOptions.builder()
+ .withSSLContext(getClientSslContextFactory(keystorePath)
+ .createJSSESslContext(EncryptionOptions.ClientAuth.OPTIONAL))
+ .build();
+ }
+ catch (SSLException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ISslContextFactory getClientSslContextFactory(Path keystorePath)
+ {
+ ImmutableMap.Builder params = ImmutableMap.builder()
+ .put("truststore", truststorePath.toString())
+ .put("truststore_password", CLIENT_TRUSTSTORE_PASSWORD);
+
+ if (keystorePath != null)
+ {
+ params.put("keystore", keystorePath.toString())
+ .put("keystore_password", "cassandra");
+ }
+
+ return new SimpleClientSslContextFactory(params.build());
+ }
+
+ private static void configureIdentity()
+ {
+ withAuthenticatedSession(CLUSTER.get(1), DEFAULT_SUPERUSER_NAME, DEFAULT_SUPERUSER_PASSWORD, session -> {
+ session.execute("CREATE ROLE cassandra_ssl_test WITH LOGIN = true");
+ session.execute(String.format("ADD IDENTITY '%s' TO ROLE 'cassandra_ssl_test'", IDENTITY));
+ // GRANT select to cassandra_ssl_test to be able to query the system_views.clients virtual table
+ session.execute("GRANT SELECT ON ALL KEYSPACES to cassandra_ssl_test");
+ });
+ }
+
+ static void withAuthenticatedSession(IInvokableInstance instance, String username, String password, Consumer consumer)
+ {
+ // wait for existing roles
+ Auth.waitForExistingRoles(instance);
+
+ InetSocketAddress nativeInetSocketAddress = ClusterUtils.getNativeInetSocketAddress(instance);
+ InetAddress address = nativeInetSocketAddress.getAddress();
+ LoadBalancingPolicy lbc = new SingleHostLoadBalancingPolicy(address);
+
+ com.datastax.driver.core.Cluster.Builder builder = com.datastax.driver.core.Cluster.builder()
+ .withLoadBalancingPolicy(lbc)
+ .withSSL(getSSLOptions(null))
+ .withAuthProvider(new PlainTextAuthProvider(username, password))
+ .addContactPoint(address.getHostAddress())
+ .withPort(nativeInetSocketAddress.getPort());
+
+ try (com.datastax.driver.core.Cluster c = builder.build(); Session session = c.connect())
+ {
+ consumer.accept(session);
+ }
+ }
+
+ private Path generateClientCertificate(Function customizeCertificate) throws Exception
+ {
+
+ CertificateBuilder builder = new CertificateBuilder().subject("CN=Apache Cassandra, OU=ssl_test, O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
+ .notBefore(Instant.now().minus(1, ChronoUnit.DAYS))
+ .notAfter(Instant.now().plus(1, ChronoUnit.DAYS))
+ .alias("spiffecert")
+ .addSanUriName(IDENTITY)
+ .rsa2048Algorithm();
+ if (customizeCertificate != null)
+ {
+ builder = customizeCertificate.apply(builder);
+ }
+ CertificateBundle ssc = builder.buildIssuedBy(CA);
+ return ssc.toTempKeyStorePath(tempFolder.getRoot().toPath(), KEYSTORE_PASSWORD, KEYSTORE_PASSWORD);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsCertificateValidityPeriodValidatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsCertificateValidityPeriodValidatorTest.java
new file mode 100644
index 0000000000..7170d3a9e9
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/MutualTlsCertificateValidityPeriodValidatorTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.cassandra.auth;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.apache.cassandra.config.DurationSpec;
+import org.assertj.core.api.Assertions;
+
+import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain;
+import static org.apache.cassandra.auth.SpiffeCertificateValidatorTest.CERTIFICATE_PATH;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Unit tests for {@link MutualTlsCertificateValidityPeriodValidator}
+ */
+public class MutualTlsCertificateValidityPeriodValidatorTest
+{
+ @Test
+ public void testValidator() throws CertificateException
+ {
+ // Create a validator that allows certificates to be issued for 4000 days or less
+ MutualTlsCertificateValidityPeriodValidator validator = new MutualTlsCertificateValidityPeriodValidator(new DurationSpec.IntMinutesBound("4000d"));
+ Certificate[] chain = loadCertificateChain(CERTIFICATE_PATH);
+ Assertions.assertThat(validator.validate(chain))
+ .isNotNull()
+ .isGreaterThan(0)
+ .isLessThan((int) TimeUnit.DAYS.toMinutes(3650));
+ }
+
+ @Test
+ public void testValidatorWithNullInput()
+ {
+ MutualTlsCertificateValidityPeriodValidator validator = new MutualTlsCertificateValidityPeriodValidator(null);
+ Assertions.assertThat(validator.validate(null)).isEqualTo(-1);
+ }
+
+ @Test
+ public void testValidatorWithNoX509Certs()
+ {
+ Certificate[] chain = { mock(Certificate.class) };
+ MutualTlsCertificateValidityPeriodValidator validator = new MutualTlsCertificateValidityPeriodValidator(null);
+ Assertions.assertThat(validator.validate(chain)).isEqualTo(-1);
+ }
+
+ @Test
+ public void testValidatorWithoutMaxCertificateAge() throws CertificateException
+ {
+ // Create a validator that does not validate for certificate age
+ MutualTlsCertificateValidityPeriodValidator validator = new MutualTlsCertificateValidityPeriodValidator(null);
+ Certificate[] chain = loadCertificateChain(CERTIFICATE_PATH);
+ Assertions.assertThat(validator.validate(chain))
+ .isGreaterThan(0)
+ .isLessThan((int) TimeUnit.DAYS.toMinutes(3650));
+ }
+
+ @Test
+ public void testThrowsWhenAgeExceedsMaximumAllowedAge() throws CertificateException
+ {
+ // Create a validator that allows certificates to be issued for 30 days or less
+ MutualTlsCertificateValidityPeriodValidator validator = new MutualTlsCertificateValidityPeriodValidator(new DurationSpec.IntMinutesBound("30d"));
+ Certificate[] chain = loadCertificateChain(CERTIFICATE_PATH);
+ Assertions.assertThatThrownBy(() -> validator.validate(chain))
+ .hasMessageContaining("The validity period of the provided certificate (3650 days) exceeds the maximum allowed validity period of 30 days");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsUtilTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsUtilTest.java
new file mode 100644
index 0000000000..033fa033ed
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/MutualTlsUtilTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.cassandra.auth;
+
+import org.junit.Test;
+
+import org.assertj.core.api.Assertions;
+
+import static org.apache.cassandra.auth.MutualTlsUtil.toHumanReadableCertificateExpiration;
+
+/**
+ * Unit tests for {@link MutualTlsUtil}
+ */
+public class MutualTlsUtilTest
+{
+ @Test
+ public void testToHumanReadableCertificateExpiration()
+ {
+ Assertions.assertThat(toHumanReadableCertificateExpiration(0)).isEqualTo("0 minutes");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(1)).isEqualTo("1 minute");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(2)).isEqualTo("2 minutes");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(10)).isEqualTo("10 minutes");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(60)).isEqualTo("1 hour");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(61)).isEqualTo("1 hour 1 minute");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(80)).isEqualTo("1 hour 20 minutes");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(240)).isEqualTo("4 hours");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(1440)).isEqualTo("1 day");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(1501)).isEqualTo("1 day 1 hour 1 minute");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(1740)).isEqualTo("1 day 5 hours");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(2880)).isEqualTo("2 days");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(3180)).isEqualTo("2 days 5 hours");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(525600)).isEqualTo("365 days");
+ Assertions.assertThat(toHumanReadableCertificateExpiration(Integer.MAX_VALUE)).isEqualTo("1491308 days 2 hours 7 minutes");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java b/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java
index fb1083c3f0..981bb99435 100644
--- a/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java
+++ b/test/unit/org/apache/cassandra/auth/SpiffeCertificateValidatorTest.java
@@ -32,7 +32,7 @@ import static org.junit.Assert.assertEquals;
public class SpiffeCertificateValidatorTest
{
- private static final String CERTIFICATE_PATH = "auth/SampleMtlsClientCertificate.pem";
+ static final String CERTIFICATE_PATH = "auth/SampleMtlsClientCertificate.pem";
@Rule
public ExpectedException expectedException = ExpectedException.none();
diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java
index f09527067f..cbfea947fe 100644
--- a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java
+++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java
@@ -29,6 +29,7 @@ import org.junit.Test;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.assertj.core.api.Assertions;
+import org.yaml.snakeyaml.constructor.ConstructorException;
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.all;
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.dc;
@@ -37,6 +38,7 @@ import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOpti
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.ENCRYPTED;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.OPTIONAL;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -61,7 +63,7 @@ public class EncryptionOptionsTest
new HashMap<>()),
keystorePath, "dummypass",
"dummytruststore", "dummypass",
- Collections.emptyList(), null, null, null, "JKS", "false", false, enabled, optional)
+ Collections.emptyList(), null, null, null, "JKS", "false", false, enabled, optional, null, null)
.applyConfig(),
expected,
String.format("optional=%s keystore=%s enabled=%s", optional, keystorePath, enabled));
@@ -75,7 +77,7 @@ public class EncryptionOptionsTest
customSslContextFactoryParams),
keystorePath, "dummypass",
"dummytruststore", "dummypass",
- Collections.emptyList(), null, null, null, "JKS", "false", false, enabled, optional)
+ Collections.emptyList(), null, null, null, "JKS", "false", false, enabled, optional, null, null)
.applyConfig(),
expected,
String.format("optional=%s keystore=%s enabled=%s", optional, keystorePath, enabled));
@@ -126,7 +128,7 @@ public class EncryptionOptionsTest
{
return new ServerEncryptionOptionsTestCase(new EncryptionOptions.ServerEncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
new HashMap<>()), keystorePath, "dummypass", keystorePath, "dummypass", "dummytruststore", "dummypass",
- Collections.emptyList(), null, null, null, "JKS", "false", false, optional, internodeEncryption, false)
+ Collections.emptyList(), null, null, null, "JKS", "false", false, optional, internodeEncryption, false, null, null)
.applyConfig(),
expected,
String.format("optional=%s keystore=%s internode=%s", optional, keystorePath, internodeEncryption));
@@ -161,6 +163,51 @@ public class EncryptionOptionsTest
.hasMessage("Invalid yaml. Please remove properties [isOptional] from your cassandra.yaml");
}
+ @Test
+ public void testMaxCertificateValidityPeriod()
+ {
+ Map yaml = ImmutableMap.of(
+ "server_encryption_options", ImmutableMap.of(
+ "max_certificate_validity_period", "2d"
+ ),
+ "client_encryption_options", ImmutableMap.of(
+ "max_certificate_validity_period", "10d"
+ )
+ );
+
+ Config config = YamlConfigurationLoader.fromMap(yaml, Config.class);
+ assertEquals(new DurationSpec.IntMinutesBound("2d"), config.server_encryption_options.max_certificate_validity_period);
+ assertEquals(new DurationSpec.IntMinutesBound("10d"), config.client_encryption_options.max_certificate_validity_period);
+ }
+
+ @Test
+ public void testFailsToParseInvalidMaxCertificateValidityPeriodValue()
+ {
+ Map yaml = ImmutableMap.of(
+ "server_encryption_options", ImmutableMap.of(
+ "max_certificate_validity_period", "not-a-valid-input"
+ )
+ );
+
+ Assertions.assertThatThrownBy(() -> YamlConfigurationLoader.fromMap(yaml, Config.class))
+ .isInstanceOf(ConstructorException.class)
+ .hasMessageContaining("Cannot create property=server_encryption_options for JavaBean=org.apache.cassandra.config.Config@");
+ }
+
+ @Test
+ public void testFailsToParseNegativeMaxCertificateValidityPeriod()
+ {
+ Map yaml = ImmutableMap.of(
+ "server_encryption_options", ImmutableMap.of(
+ "max_certificate_validity_period", "-2d"
+ )
+ );
+
+ Assertions.assertThatThrownBy(() -> YamlConfigurationLoader.fromMap(yaml, Config.class))
+ .isInstanceOf(ConstructorException.class)
+ .hasMessageContaining("Cannot create property=server_encryption_options for JavaBean=org.apache.cassandra.config.Config@");
+ }
+
final ServerEncryptionOptionsTestCase[] serverEncryptionOptionTestCases = {
// Optional Keystore Internode Expected
diff --git a/test/unit/org/apache/cassandra/utils/tls/CertificateBuilder.java b/test/unit/org/apache/cassandra/utils/tls/CertificateBuilder.java
new file mode 100644
index 0000000000..3f8a785f2d
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/tls/CertificateBuilder.java
@@ -0,0 +1,234 @@
+/*
+ * 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.cassandra.utils.tls;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import javax.security.auth.x500.X500Principal;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+/**
+ * A utility class to generate certificates for tests
+ */
+public class CertificateBuilder
+{
+ private static final GeneralName[] EMPTY_SAN = {};
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private boolean isCertificateAuthority;
+ private String alias;
+ private X500Name subject;
+ private SecureRandom random;
+ private Date notBefore = Date.from(Instant.now().minus(1, ChronoUnit.DAYS));
+ private Date notAfter = Date.from(Instant.now().plus(1, ChronoUnit.DAYS));
+ private String algorithm;
+ private AlgorithmParameterSpec algorithmParameterSpec;
+ private String signatureAlgorithm;
+ private BigInteger serial;
+ private final List subjectAlternativeNames = new ArrayList<>();
+
+ public CertificateBuilder()
+ {
+ ecp256Algorithm();
+ }
+
+ public CertificateBuilder isCertificateAuthority(boolean isCertificateAuthority)
+ {
+ this.isCertificateAuthority = isCertificateAuthority;
+ return this;
+ }
+
+ public CertificateBuilder subject(String subject)
+ {
+ this.subject = new X500Name(Objects.requireNonNull(subject));
+ return this;
+ }
+
+ public CertificateBuilder notBefore(Instant notBefore)
+ {
+ return notBefore(Date.from(Objects.requireNonNull(notBefore)));
+ }
+
+ private CertificateBuilder notBefore(Date notBefore)
+ {
+ this.notBefore = Objects.requireNonNull(notBefore);
+ return this;
+ }
+
+ public CertificateBuilder notAfter(Instant notAfter)
+ {
+ return notAfter(Date.from(Objects.requireNonNull(notAfter)));
+ }
+
+ private CertificateBuilder notAfter(Date notAfter)
+ {
+ this.notAfter = Objects.requireNonNull(notAfter);
+ return this;
+ }
+
+ public CertificateBuilder addSanUriName(String uri)
+ {
+ subjectAlternativeNames.add(new GeneralName(GeneralName.uniformResourceIdentifier, uri));
+ return this;
+ }
+
+ public CertificateBuilder addSanDnsName(String dnsName)
+ {
+ subjectAlternativeNames.add(new GeneralName(GeneralName.dNSName, dnsName));
+ return this;
+ }
+
+ public CertificateBuilder secureRandom(SecureRandom secureRandom)
+ {
+ this.random = Objects.requireNonNull(secureRandom);
+ return this;
+ }
+
+ public CertificateBuilder alias(String alias)
+ {
+ this.alias = Objects.requireNonNull(alias);
+ return this;
+ }
+
+ public CertificateBuilder serial(BigInteger serial)
+ {
+ this.serial = serial;
+ return this;
+ }
+
+ public CertificateBuilder ecp256Algorithm()
+ {
+ this.algorithm = "EC";
+ this.algorithmParameterSpec = new ECGenParameterSpec("secp256r1");
+ this.signatureAlgorithm = "SHA256WITHECDSA";
+ return this;
+ }
+
+ public CertificateBuilder rsa2048Algorithm()
+ {
+ this.algorithm = "RSA";
+ this.algorithmParameterSpec = new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4);
+ this.signatureAlgorithm = "SHA256WITHRSA";
+ return this;
+ }
+
+ public CertificateBundle buildSelfSigned() throws Exception
+ {
+ KeyPair keyPair = generateKeyPair();
+
+ JcaX509v3CertificateBuilder builder = createCertBuilder(subject, subject, keyPair);
+ addExtensions(builder);
+
+ ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate());
+ X509CertificateHolder holder = builder.build(signer);
+ X509Certificate root = new JcaX509CertificateConverter().getCertificate(holder);
+ return new CertificateBundle(signatureAlgorithm, new X509Certificate[]{ root }, root, keyPair, alias);
+ }
+
+ public CertificateBundle buildIssuedBy(CertificateBundle issuer) throws Exception
+ {
+ String issuerSignAlgorithm = issuer.signatureAlgorithm();
+ return buildIssuedBy(issuer, issuerSignAlgorithm);
+ }
+
+ public CertificateBundle buildIssuedBy(CertificateBundle issuer, String issuerSignAlgorithm) throws Exception
+ {
+ KeyPair keyPair = generateKeyPair();
+
+ X500Principal issuerPrincipal = issuer.certificate().getSubjectX500Principal();
+ X500Name issuerName = X500Name.getInstance(issuerPrincipal.getEncoded());
+ JcaX509v3CertificateBuilder builder = createCertBuilder(issuerName, subject, keyPair);
+
+ addExtensions(builder);
+
+ PrivateKey issuerPrivateKey = issuer.keyPair().getPrivate();
+ if (issuerPrivateKey == null)
+ {
+ throw new IllegalArgumentException("Cannot sign certificate with issuer that does not have a private key.");
+ }
+ ContentSigner signer = new JcaContentSignerBuilder(issuerSignAlgorithm).build(issuerPrivateKey);
+ X509CertificateHolder holder = builder.build(signer);
+ X509Certificate cert = new JcaX509CertificateConverter().getCertificate(holder);
+ X509Certificate[] issuerPath = issuer.certificatePath();
+ X509Certificate[] path = new X509Certificate[issuerPath.length + 1];
+ path[0] = cert;
+ System.arraycopy(issuerPath, 0, path, 1, issuerPath.length);
+ return new CertificateBundle(signatureAlgorithm, path, issuer.rootCertificate(), keyPair, alias);
+ }
+
+ private SecureRandom secureRandom()
+ {
+ return Objects.requireNonNullElse(random, SECURE_RANDOM);
+ }
+
+ private KeyPair generateKeyPair() throws GeneralSecurityException
+ {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
+ keyGen.initialize(algorithmParameterSpec, secureRandom());
+ return keyGen.generateKeyPair();
+ }
+
+ private JcaX509v3CertificateBuilder createCertBuilder(X500Name issuer, X500Name subject, KeyPair keyPair)
+ {
+ BigInteger serial = this.serial != null ? this.serial : new BigInteger(159, secureRandom());
+ PublicKey pubKey = keyPair.getPublic();
+ return new JcaX509v3CertificateBuilder(issuer, serial, notBefore, notAfter, subject, pubKey);
+ }
+
+ private void addExtensions(JcaX509v3CertificateBuilder builder) throws IOException
+ {
+ if (isCertificateAuthority)
+ {
+ builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
+ }
+
+ boolean criticality = false;
+ if (!subjectAlternativeNames.isEmpty())
+ {
+ builder.addExtension(Extension.subjectAlternativeName, criticality,
+ new GeneralNames(subjectAlternativeNames.toArray(EMPTY_SAN)));
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/utils/tls/CertificateBundle.java b/test/unit/org/apache/cassandra/utils/tls/CertificateBundle.java
new file mode 100644
index 0000000000..f6af56e210
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/tls/CertificateBundle.java
@@ -0,0 +1,109 @@
+/*
+ * 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.cassandra.utils.tls;
+
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.X509Certificate;
+import java.util.Objects;
+
+public class CertificateBundle
+{
+ private final String signatureAlgorithm;
+ private final X509Certificate[] chain;
+ private final X509Certificate root;
+ private final KeyPair keyPair;
+ private final String alias;
+
+ public CertificateBundle(String signatureAlgorithm, X509Certificate[] chain,
+ X509Certificate root, KeyPair keyPair, String alias)
+ {
+ this.signatureAlgorithm = Objects.requireNonNull(signatureAlgorithm);
+ this.chain = chain;
+ this.root = root;
+ this.keyPair = keyPair;
+ this.alias = Objects.requireNonNullElse(alias, "1");
+ }
+
+ public KeyStore toKeyStore(char[] keyEntryPassword) throws KeyStoreException
+ {
+ KeyStore keyStore;
+ try
+ {
+ keyStore = KeyStore.getInstance("PKCS12");
+ keyStore.load(null, null);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("Failed to initialize PKCS#12 KeyStore.", e);
+ }
+ keyStore.setCertificateEntry("1", root);
+ if (!isCertificateAuthority())
+ {
+ keyStore.setKeyEntry(alias, keyPair.getPrivate(), keyEntryPassword, chain);
+ }
+ return keyStore;
+ }
+
+ public Path toTempKeyStorePath(Path baseDir, char[] pkcs12Password, char[] keyEntryPassword) throws Exception
+ {
+ KeyStore keyStore = toKeyStore(keyEntryPassword);
+ Path tempFile = Files.createTempFile(baseDir, "ks", ".p12");
+ try (OutputStream out = Files.newOutputStream(tempFile, StandardOpenOption.WRITE))
+ {
+ keyStore.store(out, pkcs12Password);
+ }
+ return tempFile;
+ }
+
+ public boolean isCertificateAuthority()
+ {
+ return chain[0].getBasicConstraints() != -1;
+ }
+
+ public X509Certificate certificate()
+ {
+ return chain[0];
+ }
+
+ public KeyPair keyPair()
+ {
+ return keyPair;
+ }
+
+ public X509Certificate[] certificatePath()
+ {
+ return chain.clone();
+ }
+
+ public X509Certificate rootCertificate()
+ {
+ return root;
+ }
+
+ public String signatureAlgorithm()
+ {
+ return signatureAlgorithm;
+ }
+}