forked from opensearch-project/security
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Changes: - Refactored DefaultKeyStore into specialized subclasses, each managing a distinct responsibility. - Added missing tests for certificate loading, SSL parameter configuration, and related processes. Signed-off-by: Andrey Pleskach <ples@aiven.io>
- Loading branch information
1 parent
8d48838
commit 58074cb
Showing
22 changed files
with
3,489 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
src/main/java/org/opensearch/security/ssl/SslConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
* | ||
* Modifications Copyright OpenSearch Contributors. See | ||
* GitHub history for details. | ||
*/ | ||
|
||
package org.opensearch.security.ssl; | ||
|
||
import java.nio.file.Path; | ||
import java.security.AccessController; | ||
import java.security.PrivilegedActionException; | ||
import java.security.PrivilegedExceptionAction; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
|
||
import org.opensearch.OpenSearchException; | ||
import org.opensearch.security.ssl.config.Certificate; | ||
import org.opensearch.security.ssl.config.KeyStoreConfiguration; | ||
import org.opensearch.security.ssl.config.SslParameters; | ||
import org.opensearch.security.ssl.config.TrustStoreConfiguration; | ||
|
||
import io.netty.handler.codec.http2.Http2SecurityUtil; | ||
import io.netty.handler.ssl.ApplicationProtocolConfig; | ||
import io.netty.handler.ssl.ApplicationProtocolNames; | ||
import io.netty.handler.ssl.SslContext; | ||
import io.netty.handler.ssl.SslContextBuilder; | ||
import io.netty.handler.ssl.SupportedCipherSuiteFilter; | ||
|
||
public class SslConfiguration { | ||
|
||
private final static Logger LOGGER = LogManager.getLogger(SslConfiguration.class); | ||
|
||
private final SslParameters sslParameters; | ||
|
||
private final TrustStoreConfiguration trustStoreConfiguration; | ||
|
||
private final KeyStoreConfiguration keyStoreConfiguration; | ||
|
||
public SslConfiguration( | ||
final SslParameters sslParameters, | ||
final TrustStoreConfiguration trustStoreConfiguration, | ||
final KeyStoreConfiguration keyStoreConfiguration | ||
) { | ||
this.sslParameters = sslParameters; | ||
this.trustStoreConfiguration = trustStoreConfiguration; | ||
this.keyStoreConfiguration = keyStoreConfiguration; | ||
} | ||
|
||
public List<Path> dependentFiles() { | ||
return Stream.concat(keyStoreConfiguration.files().stream(), Stream.of(trustStoreConfiguration.file())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
public List<Certificate> certificates() { | ||
return Stream.concat(trustStoreConfiguration.certificates().stream(), keyStoreConfiguration.certificates().stream()) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
public SslParameters sslParameters() { | ||
return sslParameters; | ||
} | ||
|
||
@SuppressWarnings("removal") | ||
SslContext buildServerSslContext() { | ||
try { | ||
return AccessController.doPrivileged( | ||
(PrivilegedExceptionAction<SslContext>) () -> SslContextBuilder.forServer(keyStoreConfiguration.createKeyManagerFactory()) | ||
.sslProvider(sslParameters.provider()) | ||
.clientAuth(sslParameters.clientAuth()) | ||
.protocols(sslParameters.allowedProtocols().toArray(new String[0])) | ||
// TODO we always add all HTTP 2 ciphers, while maybe it is better to set them differently | ||
.ciphers( | ||
Stream.concat(Http2SecurityUtil.CIPHERS.stream(), sslParameters.allowedCiphers().stream()) | ||
.sorted(String::compareTo) | ||
.collect(Collectors.toList()), | ||
SupportedCipherSuiteFilter.INSTANCE | ||
) | ||
.sessionCacheSize(0) | ||
.sessionTimeout(0) | ||
.applicationProtocolConfig( | ||
new ApplicationProtocolConfig( | ||
ApplicationProtocolConfig.Protocol.ALPN, | ||
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. | ||
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, | ||
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. | ||
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, | ||
ApplicationProtocolNames.HTTP_2, | ||
ApplicationProtocolNames.HTTP_1_1 | ||
) | ||
) | ||
.trustManager(trustStoreConfiguration.createTrustManagerFactory()) | ||
.build() | ||
); | ||
} catch (PrivilegedActionException e) { | ||
throw new OpenSearchException("Filed to build server SSL context", e); | ||
} | ||
} | ||
|
||
@SuppressWarnings("removal") | ||
SslContext buildClientSslContext() { | ||
try { | ||
return AccessController.doPrivileged( | ||
(PrivilegedExceptionAction<SslContext>) () -> SslContextBuilder.forClient() | ||
.sslProvider(sslParameters.provider()) | ||
.protocols(sslParameters.allowedProtocols()) | ||
.ciphers(sslParameters.allowedCiphers()) | ||
.applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) | ||
.sessionCacheSize(0) | ||
.sessionTimeout(0) | ||
.sslProvider(sslParameters.provider()) | ||
.keyManager(keyStoreConfiguration.createKeyManagerFactory()) | ||
.trustManager(trustStoreConfiguration.createTrustManagerFactory()) | ||
.build() | ||
); | ||
} catch (PrivilegedActionException e) { | ||
throw new OpenSearchException("Filed to build client SSL context", e); | ||
} | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) return true; | ||
if (o == null || getClass() != o.getClass()) return false; | ||
SslConfiguration that = (SslConfiguration) o; | ||
return Objects.equals(sslParameters, that.sslParameters) | ||
&& Objects.equals(trustStoreConfiguration, that.trustStoreConfiguration) | ||
&& Objects.equals(keyStoreConfiguration, that.keyStoreConfiguration); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(sslParameters, trustStoreConfiguration, keyStoreConfiguration); | ||
} | ||
} |
181 changes: 181 additions & 0 deletions
181
src/main/java/org/opensearch/security/ssl/SslContextHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
/* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
* | ||
* Modifications Copyright OpenSearch Contributors. See | ||
* GitHub history for details. | ||
*/ | ||
|
||
package org.opensearch.security.ssl; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.security.cert.CertificateException; | ||
import java.security.cert.X509Certificate; | ||
import java.util.Date; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
import javax.net.ssl.SSLEngine; | ||
|
||
import org.opensearch.security.ssl.config.Certificate; | ||
import org.opensearch.transport.NettyAllocator; | ||
|
||
import io.netty.handler.ssl.SslContext; | ||
|
||
public class SslContextHandler { | ||
|
||
private SslContext sslContext; | ||
|
||
private final SslConfiguration sslConfiguration; | ||
|
||
private final List<Certificate> loadedCertificates; | ||
|
||
public SslContextHandler(final SslConfiguration sslConfiguration) { | ||
this(sslConfiguration, false); | ||
} | ||
|
||
public SslContextHandler(final SslConfiguration sslConfiguration, final boolean client) { | ||
this.sslContext = client ? sslConfiguration.buildClientSslContext() : sslConfiguration.buildServerSslContext(); | ||
this.sslConfiguration = sslConfiguration; | ||
this.loadedCertificates = sslConfiguration.certificates(); | ||
} | ||
|
||
public SSLEngine createSSLEngine() { | ||
return sslContext.newEngine(NettyAllocator.getAllocator()); | ||
} | ||
|
||
public SSLEngine createSSLEngine(final String hostname, final int port) { | ||
return sslContext.newEngine(NettyAllocator.getAllocator(), hostname, port); | ||
} | ||
|
||
public SslConfiguration sslConfiguration() { | ||
return sslConfiguration; | ||
} | ||
|
||
SslContext sslContext() { | ||
return sslContext; | ||
} | ||
|
||
Stream<Certificate> keyMaterialCertificates() { | ||
return keyMaterialCertificates(loadedCertificates); | ||
} | ||
|
||
Stream<Certificate> keyMaterialCertificates(final List<Certificate> certificates) { | ||
return certificates.stream().filter(Certificate::hasKey); | ||
} | ||
|
||
void reloadSslContext() throws CertificateException { | ||
final var newCertificates = sslConfiguration.certificates(); | ||
validateNewCertificates(newCertificates); | ||
invalidateSessions(); | ||
if (sslContext.isClient()) { | ||
sslContext = sslConfiguration.buildClientSslContext(); | ||
} else { | ||
sslContext = sslConfiguration.buildServerSslContext(); | ||
} | ||
loadedCertificates.clear(); | ||
loadedCertificates.addAll(newCertificates); | ||
} | ||
|
||
private boolean sameCertificates(final List<Certificate> newCertificates) { | ||
final Set<String> currentCertSignatureSet = keyMaterialCertificates().map(Certificate::x509Certificate) | ||
.map(X509Certificate::getSignature) | ||
.map(s -> new String(s, StandardCharsets.UTF_8)) | ||
.collect(Collectors.toSet()); | ||
final Set<String> newCertSignatureSet = keyMaterialCertificates(newCertificates).map(Certificate::x509Certificate) | ||
.map(X509Certificate::getSignature) | ||
.map(s -> new String(s, StandardCharsets.UTF_8)) | ||
.collect(Collectors.toSet()); | ||
return currentCertSignatureSet.equals(newCertSignatureSet); | ||
} | ||
|
||
private boolean hasValidExpiryDates(final List<Certificate> newCertificates) { | ||
// Get the earliest expiry date for current certificates | ||
final Date earliestExpiryDate = keyMaterialCertificates().map(Certificate::x509Certificate) | ||
.map(X509Certificate::getNotAfter) | ||
.min(Date::compareTo) | ||
.get(); | ||
// New certificates that expire before or on the same date as the current ones are invalid. | ||
boolean newCertsExpireBeforeCurrentCerts = keyMaterialCertificates(newCertificates).map(Certificate::x509Certificate) | ||
.anyMatch(c -> { | ||
Date notAfterDate = c.getNotAfter(); | ||
return notAfterDate.before(earliestExpiryDate) || notAfterDate.equals(earliestExpiryDate); | ||
}); | ||
|
||
return !newCertsExpireBeforeCurrentCerts; | ||
} | ||
|
||
private void validateSubjectDns(final List<Certificate> newCertificates) throws CertificateException { | ||
final List<String> currentSubjectDNs = keyMaterialCertificates().map(Certificate::subject).sorted().collect(Collectors.toList()); | ||
final List<String> newSubjectDNs = keyMaterialCertificates(newCertificates).map(Certificate::subject) | ||
.sorted() | ||
.collect(Collectors.toList()); | ||
if (!currentSubjectDNs.equals(newSubjectDNs)) { | ||
throw new CertificateException( | ||
"New certificates do not have valid Subject DNs. Current Subject DNs " | ||
+ currentSubjectDNs | ||
+ " new Subject DNs " | ||
+ newSubjectDNs | ||
); | ||
} | ||
} | ||
|
||
private void validateIssuerDns(final List<Certificate> newCertificates) throws CertificateException { | ||
final List<String> currentIssuerDNs = keyMaterialCertificates().map(Certificate::issuer).sorted().collect(Collectors.toList()); | ||
final List<String> newIssuerDNs = keyMaterialCertificates(newCertificates).map(Certificate::issuer) | ||
.sorted() | ||
.collect(Collectors.toList()); | ||
if (!currentIssuerDNs.equals(newIssuerDNs)) { | ||
throw new CertificateException( | ||
"New certificates do not have valid Issuer DNs. Current Issuer DNs: " | ||
+ currentIssuerDNs | ||
+ " new Issuer DNs: " | ||
+ newIssuerDNs | ||
); | ||
} | ||
} | ||
|
||
private void validateSans(final List<Certificate> newCertificates) throws CertificateException { | ||
final List<String> currentSans = keyMaterialCertificates().map(Certificate::subjectAlternativeNames) | ||
.sorted() | ||
.collect(Collectors.toList()); | ||
final List<String> newSans = keyMaterialCertificates(newCertificates).map(Certificate::subjectAlternativeNames) | ||
.sorted() | ||
.collect(Collectors.toList()); | ||
if (!currentSans.equals(newSans)) { | ||
throw new CertificateException( | ||
"New certificates do not have valid SANs. Current SANs: " + currentSans + " new SANs: " + newSans | ||
); | ||
} | ||
} | ||
|
||
private void validateNewCertificates(final List<Certificate> newCertificates) throws CertificateException { | ||
if (sameCertificates(newCertificates)) { | ||
throw new CertificateException("Existing certificates are the same as new one"); | ||
} | ||
if (!hasValidExpiryDates(newCertificates)) { | ||
throw new CertificateException("New certificates should not expire before the current ones"); | ||
} | ||
validateSubjectDns(newCertificates); | ||
validateIssuerDns(newCertificates); | ||
validateSans(newCertificates); | ||
} | ||
|
||
private void invalidateSessions() { | ||
final var sessionContext = sslContext.sessionContext(); | ||
if (sessionContext != null) { | ||
final var sessionsIds = sessionContext.getIds(); | ||
while (sessionsIds.hasMoreElements()) { | ||
final var session = sessionContext.getSession(sessionsIds.nextElement()); | ||
if (session != null) { | ||
session.invalidate(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.