Skip to content

Commit

Permalink
Use cert-pinning in test cluster wait-for-health (#92657)
Browse files Browse the repository at this point in the history
The previous model relied on treating the server's certificate
configuration as a trust anchor. This isn't guaranteed to work,
which lead to needing to support "certificate_authorities" as an
alternative, which in turn polluted the node's config with settings
that only existed to enable tests to run.

The new model ties the "wait-for-health" HTTP client to the leaf
certificates themselves. This means that it will always connect to a
node that has the exact certificates it expects, and doesn't rely on
knowing the issuer of the node's certificate.
  • Loading branch information
tvernum committed Feb 6, 2023
1 parent 14cca12 commit 6e402ad
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1696,20 +1696,17 @@ public boolean isHttpSslEnabled() {
}

void configureHttpWait(WaitForHttpResource wait) {
if (settings.containsKey("xpack.security.http.ssl.certificate_authorities")) {
wait.setCertificateAuthorities(
getConfigDir().resolve(settings.get("xpack.security.http.ssl.certificate_authorities").toString()).toFile()
);
}
if (settings.containsKey("xpack.security.http.ssl.certificate")) {
wait.setCertificateAuthorities(getConfigDir().resolve(settings.get("xpack.security.http.ssl.certificate").toString()).toFile());
}
if (settings.containsKey("xpack.security.http.ssl.keystore.path")
&& settings.containsKey("xpack.security.http.ssl.certificate_authorities") == false) { // Can not set both trust stores and CA
wait.setTrustStoreFile(getConfigDir().resolve(settings.get("xpack.security.http.ssl.keystore.path").toString()).toFile());
}
if (keystoreSettings.containsKey("xpack.security.http.ssl.keystore.secure_password")) {
wait.setTrustStorePassword(keystoreSettings.get("xpack.security.http.ssl.keystore.secure_password").toString());
wait.setServerCertificate(getConfigDir().resolve(settings.get("xpack.security.http.ssl.certificate").toString()).toFile());
} else {
if (settings.containsKey("xpack.security.http.ssl.keystore.path")) {
wait.setServerKeystoreFile(
getConfigDir().resolve(settings.get("xpack.security.http.ssl.keystore.path").toString()).toFile()
);
}
if (keystoreSettings.containsKey("xpack.security.http.ssl.keystore.secure_password")) {
wait.setServerKeystorePassword(keystoreSettings.get("xpack.security.http.ssl.keystore.secure_password").toString());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.gradle.testclusters;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

class SslTrustResolver {
private Set<File> certificateAuthorities;
private File trustStoreFile;
private String trustStorePassword;
private File serverCertificate;
private File serverKeyStoreFile;
private String serverKeyStorePassword;

public void setCertificateAuthorities(File... certificateAuthorities) {
this.certificateAuthorities = new HashSet<>(Arrays.asList(certificateAuthorities));
}

public void setTrustStoreFile(File trustStoreFile) {
this.trustStoreFile = trustStoreFile;
}

public void setTrustStorePassword(String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}

public void setServerCertificate(File serverCertificate) {
this.serverCertificate = serverCertificate;
}

public void setServerKeystoreFile(File keyStoreFile) {
this.serverKeyStoreFile = keyStoreFile;
}

public void setServerKeystorePassword(String keyStorePassword) {
this.serverKeyStorePassword = keyStorePassword;
}

public SSLContext getSslContext() throws GeneralSecurityException, IOException {
final TrustManager[] trustManagers = buildTrustManagers();
if (trustManagers != null) {
return createSslContext(trustManagers);
} else {
return null;
}
}

TrustManager[] buildTrustManagers() throws GeneralSecurityException, IOException {
var configurationCount = Stream.of(
this.certificateAuthorities,
this.trustStoreFile,
this.serverCertificate,
this.serverKeyStoreFile
).filter(Objects::nonNull).count();
if (configurationCount == 0) {
return null;
} else if (configurationCount > 1) {
throw new IllegalStateException(
String.format(
Locale.ROOT,
"Cannot specify more than one trust method (CA=%s, trustStore=%s, serverCert=%s, serverKeyStore=%s)",
certificateAuthorities,
trustStoreFile,
serverCertificate,
serverKeyStoreFile
)
);
}
if (this.certificateAuthorities != null) {
return getTrustManagers(buildTrustStoreFromCA(certificateAuthorities));
} else if (this.trustStoreFile != null) {
return getTrustManagers(readKeyStoreFromFile(trustStoreFile, trustStorePassword));
} else if (this.serverCertificate != null) {
return buildTrustManagerFromLeafCertificates(head(readCertificates(serverCertificate)));
} else if (this.serverKeyStoreFile != null) {
return buildTrustManagerFromLeafCertificates(readCertificatesFromKeystore(serverKeyStoreFile, serverKeyStorePassword));
} else {
// Cannot get here unless the code gets out of sync with the 'configurationCount == 0' check above
throw new IllegalStateException("Expected to configure trust, but all configuration values are null");
}
}

private SSLContext createSslContext(TrustManager[] trustManagers) throws GeneralSecurityException {
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(new KeyManager[0], trustManagers, new SecureRandom());
return sslContext;
}

private TrustManager[] getTrustManagers(KeyStore trustStore) throws GeneralSecurityException {
checkForTrustEntry(trustStore);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
return tmf.getTrustManagers();
}

private void checkForTrustEntry(KeyStore trustStore) throws KeyStoreException {
Enumeration<String> enumeration = trustStore.aliases();
while (enumeration.hasMoreElements()) {
if (trustStore.isCertificateEntry(enumeration.nextElement())) {
// found trusted cert entry
return;
}
}
throw new IllegalStateException("Trust-store does not contain any trusted certificate entries");
}

private static KeyStore buildTrustStoreFromCA(Set<File> files) throws GeneralSecurityException, IOException {
final KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
store.load(null, null);
int counter = 0;
for (File ca : files) {
for (Certificate certificate : readCertificates(ca)) {
store.setCertificateEntry("cert-" + counter, certificate);
counter++;
}
}
return store;
}

private static TrustManager[] buildTrustManagerFromLeafCertificates(Collection<? extends Certificate> certificates) {
final Set<X509Certificate> trusted = certificates.stream()
.filter(X509Certificate.class::isInstance)
.map(X509Certificate.class::cast)
.collect(Collectors.toUnmodifiableSet());

var trustManager = new X509TrustManager() {
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
final X509Certificate leaf = chain[0];
if (trusted.contains(leaf) == false) {
throw new CertificateException("Untrusted leaf certificate: " + leaf.getSubjectX500Principal());
}
}

@Override
public X509Certificate[] getAcceptedIssuers() {
// This doesn't apply when trusting leaf certs, and is only really needed for server trust managers anyways
return new X509Certificate[0];
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throw new CertificateException("This trust manager is for client use only and cannot trust other clients");
}

};
return new TrustManager[] { trustManager };
}

private static Collection<Certificate> readCertificatesFromKeystore(File file, String password) throws GeneralSecurityException,
IOException {
var keyStore = readKeyStoreFromFile(file, password);
final Set<Certificate> certificates = new HashSet<>(keyStore.size());
var enumeration = keyStore.aliases();
while (enumeration.hasMoreElements()) {
var alias = enumeration.nextElement();
if (keyStore.isKeyEntry(alias)) {
certificates.add(keyStore.getCertificate(alias));
}
}
return certificates;
}

private static KeyStore readKeyStoreFromFile(File file, String password) throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance(file.getName().endsWith(".jks") ? "JKS" : "PKCS12");
try (InputStream input = new FileInputStream(file)) {
keyStore.load(input, password == null ? null : password.toCharArray());
}
return keyStore;
}

private static Collection<? extends Certificate> readCertificates(File pemFile) throws GeneralSecurityException, IOException {
final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
try (InputStream input = new FileInputStream(pemFile)) {
return certFactory.generateCertificates(input);
}
}

private Collection<? extends Certificate> head(Collection<? extends Certificate> certificates) {
if (certificates.isEmpty()) {
return certificates;
} else {
return List.of(certificates.iterator().next());
}
}
}

0 comments on commit 6e402ad

Please sign in to comment.