Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Certificate validation improvements #4017

Open
wants to merge 2 commits into
base: v3.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("squid:S106") //ignoring the System.out System.err warinings
@SuppressWarnings("squid:S106") //ignoring the System.out System.err warnings
public class Analyser {

public static void main(String[] args) {
public static int mainWithExitCode(String[] args) {
try {
ApimlConf conf = new ApimlConf();
CommandLine cmd = new CommandLine(conf);
cmd.parseArgs(args);
if (conf.isHelpRequested()) {
cmd.printVersionHelp(System.out);
CommandLine.usage(new ApimlConf(), System.out);
return;
return 8;
}

Stores stores = new Stores(conf);
Expand All @@ -51,15 +51,19 @@ public static void main(String[] args) {
verifiers.add(new LocalHandshake(sslContextFactory, client));
}
if (conf.getKeyStore() != null) {
verifiers.add(new LocalVerifier(stores));
verifiers.add(new LocalVerifier(stores, conf.getRequiredHostNames()));
}
verifiers.forEach(Verifier::verify);

boolean valid = verifiers.stream().map(Verifier::verify).min(Boolean::compareTo).orElse(false);
return valid ? 0 : 4;
} catch (Exception e) {
System.err.println(e.getMessage());
}

return 4;
}

public static final void main(String[] args) {
System.exit(mainWithExitCode(args));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class ApimlConf implements Config {
private boolean helpRequested = false;
@Option(names = {"-c", "--clientcert"}, description = "Add client certificate to HTTPS request")
private boolean clientCertAuth;
@Option(names = {"-d", "--hostnames"}, split = ",", description = "All hostnames that should match with the server certificate separated by comma")
private String[] requiredHostNames;

public String getKeyStore() {
return keyStore;
Expand Down Expand Up @@ -89,6 +91,10 @@ public boolean isClientCertAuth() {
private String defaultValue(String value, String defaultVal) {
return value != null ? value : defaultVal;
}

public String[] getRequiredHostNames() {
return requiredHostNames;
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public LocalHandshake(SSLContextFactory sslContextFactory, HttpClient client) {
}

@Override
public void verify() {
public boolean verify() {
try { //NOSONAR
SSLServerSocket listener = (SSLServerSocket) sslContextFactory.getSslContextWithKeystore().getServerSocketFactory().createServerSocket(0);
// start listening on socket to do a SSL handshake
Expand All @@ -48,6 +48,8 @@ public void verify() {
client.executeCall(new URL(address));
System.out.println("Handshake was successful. Certificate stored under alias \"" + keyAlias + "\" is trusted by truststore \"" + trustStore
+ "\".");

return true;
} catch (SSLHandshakeException e) {
System.out.println("Handshake failed. Certificate stored under alias \"" + keyAlias + "\" is not trusted by truststore \"" + trustStore
+ "\". Error message: " + e.getMessage());
Expand All @@ -57,5 +59,7 @@ public void verify() {
} catch (KeyStoreException e) {
System.err.println("Failed when loading key alias. " + e.getMessage());
}

return false;
}
}
131 changes: 119 additions & 12 deletions certificate-analyser/src/main/java/org/zowe/apiml/LocalVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings("squid:S106") //ignoring the System.out System.err warinings
public class LocalVerifier implements Verifier {

private Stores stores;
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss. SSSZ");

public LocalVerifier(Stores stores) {
private final Stores stores;
private final String[] requiredHostnames;

public LocalVerifier(Stores stores, String[] requiredHostnames) {
this.stores = stores;
this.requiredHostnames = requiredHostnames;
}

public void verify() {
public boolean verify() {
System.out.println("=============");
System.out.println("Verifying keystore: " + stores.getConf().getKeyStore() +
" against truststore: " + stores.getConf().getTrustStore());
Expand All @@ -43,9 +50,7 @@ public void verify() {
System.out.println("Trusted certificate is stored under alias: " + cert.getKey());
System.out.println("Certificate authority: " + trustedCA.getSubjectDN());
System.out.println("Details about valid certificate:");
printDetails(alias);

return;
return verifyCertificate(alias);
}
} catch (Exception e) {
// this means that cert is not valid, intentionally ignore
Expand All @@ -57,17 +62,101 @@ public void verify() {
System.err.println("Error loading secret from keystore" + e.getMessage());
}

return false;
}

void printDetails(String keyAlias) throws KeyStoreException {
Certificate[] certificate = stores.getKeyStore().getCertificateChain(keyAlias);
X509Certificate serverCert = (X509Certificate) certificate[0];
boolean verifyExpiration(X509Certificate serverCert) {
Date expiration = serverCert.getNotAfter();
boolean expired = expiration.before(new Date());

System.out.println("++++++++");
System.out.println("Expiration data: " + DATE_TIME_FORMAT.format(expiration));
if (expired) {
System.out.println("The certificate is expired");
}
System.out.println("++++++++");

return !expired;
}

boolean isMatching(String hostname, String cn, List<String> alternativeNames) {
if (cn.startsWith("*.")) {
int firstDot = hostname.indexOf('.');
if ((firstDot > 0) && cn.substring(1).equalsIgnoreCase(hostname.substring(firstDot))) {
return true;
}
}

return alternativeNames.stream().anyMatch(hostname::equalsIgnoreCase);
}

boolean verifyHostnames(X509Certificate serverCert) {
String commonName;
List<String> alternativeNames;
try {
Pattern pattern = Pattern.compile("CN=(.*?)(?:,|$)");
Matcher matcher = pattern.matcher(serverCert.getSubjectX500Principal().getName());
commonName = matcher.find() ? matcher.group(1) : "";
alternativeNames = serverCert.getSubjectAlternativeNames().stream()
.flatMap(Collection::stream)
.map(String::valueOf)
.distinct()
.sorted()
.toList();
} catch (CertificateParsingException e) {
System.err.println(e.getMessage());
return false;
}

System.out.println("++++++++");
System.out.println("Possible hostname values:");
System.out.println("CN: " + commonName);
System.out.println("alternative names:");
alternativeNames.forEach(System.out::println);
System.out.println("++++++++");

List<String> notMatching = Arrays.stream(requiredHostnames)
.filter(requiredHostname -> !isMatching(requiredHostname, commonName, alternativeNames))
.distinct()
.sorted()
.toList();
if (notMatching.isEmpty()) {
System.out.println("All required hostnames are matched with the certificate");
} else {
System.out.println("Not matched hostnames with the certificate:");
notMatching.forEach(System.out::println);
}
System.out.println("++++++++");

return notMatching.isEmpty();
}

boolean verifyServer(X509Certificate serverCert) {
try {
boolean serverAuth = serverCert.getExtendedKeyUsage().contains("1.3.6.1.5.5.7.3.1");

System.out.println("++++++++");
if (serverAuth) {
System.out.println("Certificate can be used for web server.");
} else {
System.out.println("Certificate can't be used for web server. " +
"Provide certificate with extended key usage: 1.3.6.1.5.5.7.3.1");
}
System.out.println("++++++++");
System.out.println("Possible hostname values:");
serverCert.getSubjectAlternativeNames().forEach(System.out::println);

return serverAuth;
} catch (CertificateParsingException e) {
System.err.println(e.getMessage());
}

return false;
}

boolean verifyX509(X509Certificate serverCert) {
try {
boolean clientAuth = serverCert.getExtendedKeyUsage().contains("1.3.6.1.5.5.7.3.2");

System.out.println("++++++++");
if (clientAuth) {
System.out.println("Certificate can be used for client authentication.");
} else {
Expand All @@ -76,9 +165,27 @@ void printDetails(String keyAlias) throws KeyStoreException {
}
System.out.println("++++++++");

return clientAuth;
} catch (CertificateParsingException e) {
System.err.println(e.getMessage());
}

return false;
}

boolean verifyCertificate(String keyAlias) throws KeyStoreException {
Certificate[] certificate = stores.getKeyStore().getCertificateChain(keyAlias);
X509Certificate serverCert = (X509Certificate) certificate[0];

boolean expirationCheck = verifyExpiration(serverCert);
boolean hostNameCheck = true;
if (requiredHostnames != null) {
hostNameCheck = verifyHostnames(serverCert);
}
boolean serverCheck = verifyServer(serverCert);
boolean x509Check = verifyX509(serverCert);

return expirationCheck && hostNameCheck && serverCheck && x509Check;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public RemoteHandshake(SSLContextFactory sslContextFactory, HttpClient httpClien
this.httpClient = httpClient;
}

public void verify() {
@Override
public boolean verify() {
String serviceAddress = sslContextFactory.getStores().getConf().getRemoteUrl();
String trustStore = sslContextFactory.getStores().getConf().getTrustStore();

Expand All @@ -35,6 +36,7 @@ public void verify() {
httpClient.executeCall(url);
System.out.println("Handshake was successful. Service \"" + serviceAddress + "\" is trusted by truststore \"" + trustStore
+ "\".");
return true;
} catch (MalformedURLException e) {
System.out.println("Incorrect url \"" + serviceAddress + "\". Error message: " + e.getMessage());
} catch (SSLHandshakeException e) {
Expand All @@ -43,7 +45,7 @@ public void verify() {
} catch (Exception e) {
System.out.println("Failed when calling url: \"" + serviceAddress + "\" Error message: " + e.getMessage());
}
return false;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@

public interface Verifier {

void verify();
boolean verify();

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class AnalyserTest {
Expand All @@ -41,7 +42,7 @@ void providedCorrectInputs_certificateIsVerified() {
"--keypasswd", "password",
"--keyalias", "localhost",
"-l"};
Analyser.main(args);
assertEquals(0, Analyser.mainWithExitCode(args));
assertTrue(outputStream.toString().contains("Trusted certificate is stored under alias:"));
}
}
Loading
Loading