From 3318922008ea03242a830d591c95c273dc628ada Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 26 Mar 2026 16:31:49 +0530 Subject: [PATCH 01/10] JWT pre-flight check code Signed-off-by: hrishikesh-nalawade --- build.gradle | 1 + pre-flight-check/build.gradle | 27 ++++ .../org/zowe/apiml/HttpClientWrapper.java | 65 ++++++++ .../org/zowe/apiml/JwkEndpointChecker.java | 100 ++++++++++++ .../java/org/zowe/apiml/PreFlightCheck.java | 70 +++++++++ .../org/zowe/apiml/PreFlightCheckConf.java | 106 +++++++++++++ .../org/zowe/apiml/PreFlightCheckConfig.java | 34 ++++ .../org/zowe/apiml/SSLContextFactory.java | 52 ++++++ .../src/main/java/org/zowe/apiml/Stores.java | 128 +++++++++++++++ .../apiml/StoresNotInitializeException.java | 18 +++ .../zowe/apiml/JwkEndpointCheckerTest.java | 148 ++++++++++++++++++ .../org/zowe/apiml/PreFlightCheckTest.java | 90 +++++++++++ settings.gradle | 1 + 13 files changed, 840 insertions(+) create mode 100644 pre-flight-check/build.gradle create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/Stores.java create mode 100644 pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java create mode 100644 pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java create mode 100644 pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java diff --git a/build.gradle b/build.gradle index e8eb874ccb..c88fe4b4fe 100644 --- a/build.gradle +++ b/build.gradle @@ -183,6 +183,7 @@ ext.javaLibraries = [ 'apiml-security-common', 'apiml-tomcat-common', 'certificate-analyser', + 'pre-flight-check', 'common-service-core', 'security-service-client-spring', 'apiml-sample-extension', diff --git a/pre-flight-check/build.gradle b/pre-flight-check/build.gradle new file mode 100644 index 0000000000..ca98a4c3d4 --- /dev/null +++ b/pre-flight-check/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' +} + +dependencies { + implementation libs.picocli + annotationProcessor libs.picocli.codegen + + testImplementation libs.mockito.core + testImplementation libs.hamcrest +} + +compileJava { + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + +jar { + manifest { + attributes( + 'Main-Class': 'org.zowe.apiml.PreFlightCheck' + ) + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java new file mode 100644 index 0000000000..a8a091d9ad --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java @@ -0,0 +1,65 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; + +@SuppressWarnings("squid:S106") +public class HttpClientWrapper { + + private static final int CONNECT_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; + + private final SSLContext sslContext; + private final boolean useHttps; + + public HttpClientWrapper(SSLContext sslContext) { + this.sslContext = sslContext; + this.useHttps = true; + } + + public HttpClientWrapper() { + this.sslContext = null; + this.useHttps = false; + } + + public int executeCall(URL url, Map headers) throws IOException { + HttpURLConnection con; + if (useHttps) { + HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); + httpsCon.setSSLSocketFactory(sslContext.getSocketFactory()); + con = httpsCon; + } else { + con = (HttpURLConnection) url.openConnection(); + } + + con.setRequestMethod("GET"); + con.setConnectTimeout(CONNECT_TIMEOUT); + con.setReadTimeout(READ_TIMEOUT); + + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + + try { + return con.getResponseCode(); + } finally { + con.disconnect(); + } + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java b/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java new file mode 100644 index 0000000000..c9301b2084 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java @@ -0,0 +1,100 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import javax.net.ssl.SSLHandshakeException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("squid:S106") +public class JwkEndpointChecker { + + static final String JWK_ENDPOINT_PATH = "/jwt/ibm/api/zOSMFBuilder/jwk"; + private static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; + + private final HttpClientWrapper httpClient; + private final PreFlightCheckConfig conf; + + public JwkEndpointChecker(HttpClientWrapper httpClient, PreFlightCheckConfig conf) { + this.httpClient = httpClient; + this.conf = conf; + } + + public boolean check() { + String urlString = conf.getScheme() + "://" + conf.getZosmfHost() + ":" + conf.getZosmfPort() + JWK_ENDPOINT_PATH; + + Map headers = new HashMap<>(); + headers.put(ZOSMF_CSRF_HEADER, ""); + + try { + URL url = new URL(urlString); + System.out.println("Checking z/OSMF JWK endpoint: " + urlString); + + int responseCode = httpClient.executeCall(url, headers); + return evaluateResponseCode(responseCode, urlString); + } catch (SSLHandshakeException e) { + System.err.println("FAILURE: SSL handshake failed when connecting to " + urlString + "."); + System.err.println("Verify that the truststore contains the z/OSMF server certificate."); + System.err.println("Details: " + e.getMessage()); + return false; + } catch (ConnectException e) { + System.err.println("FAILURE: Cannot connect to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); + System.err.println("Verify the host and port are correct and z/OSMF is running."); + System.err.println("Details: " + e.getMessage()); + return false; + } catch (SocketTimeoutException e) { + System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); + System.err.println("Details: " + e.getMessage()); + return false; + } catch (Exception e) { + System.err.println("FAILURE: Unexpected error when calling " + urlString + "."); + System.err.println("Details: " + e.getMessage()); + return false; + } + } + + private boolean evaluateResponseCode(int responseCode, String urlString) { + if (responseCode >= 200 && responseCode < 300) { + System.out.println("SUCCESS: z/OSMF JWK endpoint is reachable and responding. HTTP " + responseCode); + return true; + } + + if (responseCode == 401) { + System.out.println("SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401"); + return true; + } + + if (responseCode == 404) { + System.err.println("FAILURE: z/OSMF JWK endpoint not found. HTTP 404"); + System.err.println("Try configuring the jwtAutoConfiguration to LTPA"); + return false; + } + + if (responseCode >= 400 && responseCode < 500) { + System.err.println("FAILURE: z/OSMF JWK endpoint returned unexpected client error. HTTP " + responseCode); + System.err.println("URL: " + urlString); + return false; + } + + if (responseCode >= 500) { + System.err.println("FAILURE: z/OSMF JWK endpoint returned server error. HTTP " + responseCode); + System.err.println("URL: " + urlString); + return false; + } + + System.err.println("FAILURE: z/OSMF JWK endpoint returned unexpected response code. HTTP " + responseCode); + System.err.println("URL: " + urlString); + return false; + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java new file mode 100644 index 0000000000..0d4e30be85 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java @@ -0,0 +1,70 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import picocli.CommandLine; + +@SuppressWarnings("squid:S106") +public class PreFlightCheck { + + public static int mainWithExitCode(String[] args) { + try { + PreFlightCheckConf conf = new PreFlightCheckConf(); + CommandLine cmd = new CommandLine(conf); + cmd.parseArgs(args); + + if (conf.isHelpRequested()) { + cmd.printVersionHelp(System.out); + CommandLine.usage(new PreFlightCheckConf(), System.out); + return 8; + } + + validateConfig(conf); + + HttpClientWrapper httpClient; + if ("https".equalsIgnoreCase(conf.getScheme())) { + Stores stores = new Stores(conf); + SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); + httpClient = new HttpClientWrapper(sslContextFactory.getSslContext()); + } else { + httpClient = new HttpClientWrapper(); + } + + JwkEndpointChecker checker = new JwkEndpointChecker(httpClient, conf); + boolean success = checker.check(); + return success ? 0 : 4; + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + return 4; + } + } + + static void validateConfig(PreFlightCheckConf conf) { + String scheme = conf.getScheme(); + if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { + throw new IllegalArgumentException("--scheme must be 'http' or 'https', got: " + scheme); + } + + if ("https".equalsIgnoreCase(scheme)) { + if (conf.getTrustStore() == null) { + throw new IllegalArgumentException("--truststore is required when --scheme=https. " + + "Provide the path to the truststore containing the z/OSMF server certificate."); + } + if (conf.getTrustStorePassword() == null) { + throw new IllegalArgumentException("--truststore-password is required when --scheme=https."); + } + } + } + + public static void main(String[] args) { + System.exit(mainWithExitCode(args)); + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java new file mode 100644 index 0000000000..2887736bb3 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java @@ -0,0 +1,106 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import picocli.CommandLine; +import picocli.CommandLine.Option; + +@CommandLine.Command( + name = "pre-flight-check", + version = { + "Pre-Flight Check 1.0", + "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", + "OS: ${os.name} ${os.version} ${os.arch}" + }, + description = "Performs a pre-flight connectivity check against the z/OSMF JWK endpoint." +) +public class PreFlightCheckConf implements PreFlightCheckConfig { + + @Option(names = {"--zosmf-host"}, required = true, description = "Hostname or IP of the z/OSMF server") + private String zosmfHost; + + @Option(names = {"--zosmf-port"}, required = true, description = "Port of the z/OSMF server") + private int zosmfPort; + + @Option(names = {"--scheme"}, description = "http or https (default: ${DEFAULT-VALUE})") + private String scheme = "https"; + + @Option(names = {"--keystore"}, description = "Path to the keystore file (for HTTPS mutual TLS)") + private String keyStore; + + @Option(names = {"--keystore-password"}, arity = "0..1", interactive = true, description = "Password for the keystore") + private String keyStorePassword; + + @Option(names = {"--keystore-type"}, description = "Type of keystore (default: ${DEFAULT-VALUE})") + private String keyStoreType = "PKCS12"; + + @Option(names = {"--truststore"}, description = "Path to the truststore file (for HTTPS)") + private String trustStore; + + @Option(names = {"--truststore-password"}, arity = "0..1", interactive = true, description = "Password for the truststore") + private String trustStorePassword; + + @Option(names = {"--truststore-type"}, description = "Type of truststore (default: ${DEFAULT-VALUE})") + private String trustStoreType = "PKCS12"; + + @Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message") + private boolean helpRequested = false; + + @Override + public String getZosmfHost() { + return zosmfHost; + } + + @Override + public int getZosmfPort() { + return zosmfPort; + } + + @Override + public String getScheme() { + return scheme; + } + + @Override + public String getKeyStore() { + return keyStore; + } + + @Override + public String getKeyStorePassword() { + return keyStorePassword; + } + + @Override + public String getKeyStoreType() { + return keyStoreType; + } + + @Override + public String getTrustStore() { + return trustStore; + } + + @Override + public String getTrustStorePassword() { + return trustStorePassword; + } + + @Override + public String getTrustStoreType() { + return trustStoreType; + } + + @Override + public boolean isHelpRequested() { + return helpRequested; + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java new file mode 100644 index 0000000000..aa9ee16040 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java @@ -0,0 +1,34 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +public interface PreFlightCheckConfig { + + String getZosmfHost(); + + int getZosmfPort(); + + String getScheme(); + + String getKeyStore(); + + String getKeyStorePassword(); + + String getKeyStoreType(); + + String getTrustStore(); + + String getTrustStorePassword(); + + String getTrustStoreType(); + + boolean isHelpRequested(); +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java new file mode 100644 index 0000000000..c11b148343 --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -0,0 +1,52 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; + +public class SSLContextFactory { + + private final Stores stores; + private SSLContext sslContext; + + private SSLContextFactory(Stores stores) { + this.stores = stores; + } + + public SSLContext getSslContext() { + return sslContext; + } + + public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { + SSLContextFactory factory = new SSLContextFactory(stores); + + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustFactory.init(stores.getTrustStore()); + + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (stores.getKeyStore() != null) { + keyFactory.init(stores.getKeyStore(), stores.getConf().getKeyStorePassword().toCharArray()); + } else { + KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); + emptyKeystore.load(null, null); + keyFactory.init(emptyKeystore, null); + } + + factory.sslContext = SSLContext.getInstance("TLSv1.2"); + factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); + return factory; + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java b/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java new file mode 100644 index 0000000000..04041403fb --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java @@ -0,0 +1,128 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("squid:S106") +public class Stores { + + private static final Pattern KEYRING_PATTERN = Pattern.compile("^(safkeyring[^:]*):/{2,4}([^/]+)/([^/]+)$"); + + private KeyStore keyStore; + private KeyStore trustStore; + private final PreFlightCheckConfig conf; + + public Stores(PreFlightCheckConfig conf) { + this.conf = conf; + init(); + } + + public static boolean isKeyring(String input) { + if (input == null) return false; + Matcher matcher = KEYRING_PATTERN.matcher(input); + return matcher.matches(); + } + + public static String formatKeyringUrl(String input) { + if (input == null) return null; + Matcher matcher = KEYRING_PATTERN.matcher(input); + if (matcher.matches()) { + return matcher.group(1) + "://" + matcher.group(2) + "/" + matcher.group(3); + } + return input; + } + + void init() { + try { + initKeystore(); + if (trustStore == null) { + initTruststore(); + } + } catch (FileNotFoundException e) { + throw new StoresNotInitializeException("Error while loading keystore file. Error message: " + e.getMessage() + "\n" + + "Possible solution: Verify correct path to the keystore. Change owner or permission to the keystore file."); + } catch (Exception e) { + throw new StoresNotInitializeException(e.getMessage()); + } + } + + private void initTruststore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + if (conf.getTrustStore() == null) { + System.out.println("No truststore specified, will use empty."); + try { + this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + this.trustStore.load(null, null); + } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { + System.err.println(e.getMessage()); + } + return; + } + try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { + this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + } + } + + private void initKeystore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { + if (conf.getKeyStore() == null) { + return; + } + if (isKeyring(conf.getKeyStore())) { + try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { + this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + this.trustStore = this.keyStore; + } catch (Exception e) { + throw new StoresNotInitializeException(e.getMessage()); + } + } else { + try (InputStream keyStoreIStream = new FileInputStream(conf.getKeyStore())) { + this.keyStore = readKeyStore(keyStoreIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + } + } + } + + public static KeyStore readKeyStore(InputStream is, char[] pass, String type) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + KeyStore keyStore = KeyStore.getInstance(type); + keyStore.load(is, pass); + return keyStore; + } + + public KeyStore getKeyStore() { + return keyStore; + } + + public KeyStore getTrustStore() { + return trustStore; + } + + public PreFlightCheckConfig getConf() { + return conf; + } + + public static URL keyRingUrl(String uri) throws MalformedURLException { + if (!isKeyring(uri)) { + throw new StoresNotInitializeException("Incorrect key ring format: " + uri + + ". Make sure you use format safkeyring://userId/keyRing"); + } + return new URL(formatKeyringUrl(uri)); + } +} diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java b/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java new file mode 100644 index 0000000000..e5c77f773c --- /dev/null +++ b/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java @@ -0,0 +1,18 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +public class StoresNotInitializeException extends RuntimeException { + + public StoresNotInitializeException(String message) { + super(message); + } +} diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java b/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java new file mode 100644 index 0000000000..642f4d507c --- /dev/null +++ b/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java @@ -0,0 +1,148 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLHandshakeException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.net.ConnectException; +import java.net.SocketTimeoutException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JwkEndpointCheckerTest { + + private final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private HttpClientWrapper mockClient; + private PreFlightCheckConfig mockConf; + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outStream)); + System.setErr(new PrintStream(errStream)); + + mockClient = mock(HttpClientWrapper.class); + mockConf = mock(PreFlightCheckConfig.class); + when(mockConf.getScheme()).thenReturn("https"); + when(mockConf.getZosmfHost()).thenReturn("zosmf.example.com"); + when(mockConf.getZosmfPort()).thenReturn(443); + } + + @AfterEach + void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Nested + class SuccessResponses { + + @Test + void response200IsSuccess() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(200); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertTrue(checker.check()); + assertTrue(outStream.toString().contains("SUCCESS")); + assertTrue(outStream.toString().contains("200")); + } + + @Test + void response401IsSuccess() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(401); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertTrue(checker.check()); + assertTrue(outStream.toString().contains("SUCCESS")); + assertTrue(outStream.toString().contains("401")); + } + } + + @Nested + class FailureResponses { + + @Test + void response404IsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(404); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("FAILURE")); + assertTrue(errStream.toString().contains("404")); + } + + @Test + void response500IsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(500); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("FAILURE")); + assertTrue(errStream.toString().contains("server error")); + } + + @Test + void response403IsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(403); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("FAILURE")); + assertTrue(errStream.toString().contains("client error")); + } + } + + @Nested + class ExceptionHandling { + + @Test + void sslHandshakeExceptionReportsCertificateError() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new SSLHandshakeException("certificate unknown")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("SSL handshake failed")); + assertTrue(errStream.toString().contains("truststore")); + } + + @Test + void connectExceptionReportsUnreachable() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new ConnectException("Connection refused")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("Cannot connect")); + } + + @Test + void socketTimeoutExceptionReportsTimeout() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new SocketTimeoutException("Read timed out")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("timed out")); + } + + @Test + void unexpectedExceptionIsHandled() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new IOException("unexpected")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("Unexpected error")); + } + } +} diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java b/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java new file mode 100644 index 0000000000..a361a75819 --- /dev/null +++ b/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java @@ -0,0 +1,90 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.*; + +class PreFlightCheckTest { + + private final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + @BeforeEach + void setupStreams() { + System.setOut(new PrintStream(outStream)); + System.setErr(new PrintStream(errStream)); + } + + @AfterEach + void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + void helpFlagReturnsExitCode8() { + String[] args = {"--help"}; + assertEquals(8, PreFlightCheck.mainWithExitCode(args)); + assertTrue(outStream.toString().contains("Pre-Flight Check")); + } + + @Test + void missingRequiredArgsReturnsExitCode4() { + String[] args = {}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + } + + @Nested + class ValidationTests { + + @Test + void invalidSchemeIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "ftp"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--scheme must be 'http' or 'https'")); + } + + @Test + void httpsWithoutTruststoreIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--truststore is required")); + } + + @Test + void httpsWithoutTruststorePasswordIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", + "--truststore", "some/path.p12"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--truststore-password is required")); + } + + @Test + void httpDoesNotRequireTruststore() { + // This will fail to connect but should not fail validation + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "http"}; + int exitCode = PreFlightCheck.mainWithExitCode(args); + // Should be 4 (connection failure) not a validation error + assertEquals(4, exitCode); + assertFalse(errStream.toString().contains("--truststore is required")); + } + } +} diff --git a/settings.gradle b/settings.gradle index 2fadb0f914..8c4a32051f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -54,6 +54,7 @@ include 'onboarding-enabler-python' include 'zaas-client' include 'mock-services' include 'certificate-analyser' +include 'pre-flight-check' include 'apiml-tomcat-common' include 'apiml-sample-extension' include 'apiml-sample-extension-package' From 2fafc3caa304a301c5db960743e7adbc810cf4cc Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Fri, 27 Mar 2026 16:12:09 +0530 Subject: [PATCH 02/10] handling verifyCertificate Scenarios Signed-off-by: hrishikesh-nalawade --- pre-flight-check/README.md | 331 ++++++++++++++++++ .../org/zowe/apiml/HttpClientWrapper.java | 9 +- .../java/org/zowe/apiml/PreFlightCheck.java | 39 ++- .../org/zowe/apiml/PreFlightCheckConf.java | 8 + .../org/zowe/apiml/PreFlightCheckConfig.java | 2 + .../org/zowe/apiml/SSLContextFactory.java | 34 ++ .../org/zowe/apiml/PreFlightCheckTest.java | 30 +- 7 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 pre-flight-check/README.md diff --git a/pre-flight-check/README.md b/pre-flight-check/README.md new file mode 100644 index 0000000000..b84b00b87c --- /dev/null +++ b/pre-flight-check/README.md @@ -0,0 +1,331 @@ +# Pre-Flight Check Tool + +A Java utility that verifies connectivity to the z/OSMF JWK endpoint **before/after** starting the Zowe API Mediation Layer. This tool helps diagnose configuration issues early such as incorrect hostnames, unreachable ports, missing certificates, or misconfigured z/OSMF by performing a lightweight HTTP(S) call to the z/OSMF JWK endpoint at `/jwt/ibm/api/zOSMFBuilder/jwk`. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Building](#building) +- [Usage](#usage) +- [CLI Flags Reference](#cli-flags-reference) +- [Certificate Verification Modes](#certificate-verification-modes) +- [Exit Codes](#exit-codes) +- [Response Interpretation](#response-interpretation) +- [Testing Scenarios](#testing-scenarios) + - [1. Quick Test — DISABLED Mode](#1-quick-test--disabled-mode-no-truststore-needed) + - [2. STRICT Mode — Full Verification](#2-strict-mode--full-certificate-and-hostname-verification) + - [3. NONSTRICT Mode — Skip Hostname Check](#3-nonstrict-mode--certificate-chain-verified-hostname-check-skipped) + - [4. HTTP Mode (No SSL)](#4-http-mode-no-ssl) + - [5. Validation Error Tests](#5-validation-error-tests) +- [SAF Keyrings](#saf-keyrings) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +When Zowe API ML starts, it attempts to reach z/OSMF to obtain public keys for JWT token validation. If z/OSMF is unreachable or misconfigured, the startup fails with errors that can be difficult to diagnose. This pre-flight check tool isolates that connectivity test into a simple, standalone JAR that can be run before Zowe startup. + +**What it checks:** + +- TCP connectivity to the z/OSMF host and port +- SSL/TLS handshake (when using HTTPS) +- Certificate trust chain validation (STRICT/NONSTRICT modes) +- Hostname verification (STRICT mode) +- HTTP response from the JWK endpoint (`/jwt/ibm/api/zOSMFBuilder/jwk`) + +## Prerequisites + +- **Java 17 or higher** (Java 17, 21, or any later version) +- Network access to the z/OSMF server +- A truststore containing the z/OSMF server's CA certificate (required for STRICT and NONSTRICT modes) + +## Building + +From the root of the `api-layer` repository: + +```bash +./gradlew :pre-flight-check:build +``` + +On Windows: + +```powershell +.\gradlew :pre-flight-check:build +``` + +The fat JAR (with all dependencies bundled) will be generated at: + +``` +pre-flight-check/build/libs/pre-flight-check-.jar +``` + +For example: `pre-flight-check/build/libs/pre-flight-check-3.5.12-SNAPSHOT.jar` + +## Usage + +```bash +java -jar pre-flight-check-.jar --zosmf-host --zosmf-port [options] +``` + +**Minimal example (DISABLED mode — quickest way to test):** + +```bash +java -jar pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --verify-certificates DISABLED +``` + +**Full example (STRICT mode with truststore):** + +```bash +java -jar pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore /path/to/truststore.p12 \ + --truststore-password changeit +``` + +**Display help:** + +```bash +java -jar pre-flight-check-.jar --help +``` + +## CLI Flags Reference + +### Required Flags + +| Flag | Description | Example | +|------|-------------|---------| +| `--zosmf-host` | Hostname or IP address of the z/OSMF server | `--zosmf-host myzosmf.example.com` | +| `--zosmf-port` | Port number of the z/OSMF server | `--zosmf-port 11443` | + +> **Note:** If `--zosmf-host` or `--zosmf-port` are omitted, picocli will display: +> `Missing required option: '--zosmf-host='` + +### Conditionally Required Flags + +These flags are required when `--scheme=https` (the default) and `--verify-certificates` is **not** `DISABLED`: + +| Flag | Description | Error when missing | +|------|-------------|-------------------| +| `--truststore` | Path to the truststore file containing the z/OSMF CA certificate | `ERROR: --truststore is required when --scheme=https and verification is not DISABLED.` | +| `--truststore-password` | Password for the truststore. If specified without a value, you will be prompted interactively. | `ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED.` | + +### Optional Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--scheme` | `https` | Protocol to use: `http` or `https` | +| `--verify-certificates` | `STRICT` | Certificate verification mode: `STRICT`, `NONSTRICT`, or `DISABLED` | +| `--truststore-type` | `PKCS12` | Format of the truststore file (e.g., `PKCS12`, `JKS`, `JCERACFKS`) | +| `--keystore` | *(none)* | Path to keystore file (only needed for mutual TLS / client certificate authentication) | +| `--keystore-password` | *(none)* | Password for the keystore. If specified without a value, you will be prompted interactively. | +| `--keystore-type` | `PKCS12` | Format of the keystore file | +| `-h`, `--help` | | Display usage help and exit | + +## Certificate Verification Modes + +The `--verify-certificates` flag controls how SSL/TLS certificates are validated when connecting over HTTPS. This mirrors the `zowe.verifyCertificates` setting in the Zowe configuration (`zowe.yaml`). + +### STRICT (Default) + +```bash +--verify-certificates STRICT +``` + +- **Certificate chain**: Fully validated against the truststore +- **Hostname verification**: The server certificate's CN/SAN must match the `--zosmf-host` value +- **Truststore**: Required +- **Use case**: Production environments — maximum security + +### NONSTRICT + +```bash +--verify-certificates NONSTRICT +``` + +- **Certificate chain**: Fully validated against the truststore +- **Hostname verification**: Skipped — the server certificate does not need to match the hostname +- **Truststore**: Required +- **Use case**: Environments where the z/OSMF certificate is issued for a different hostname (e.g., accessing via IP address when the cert has a DNS name) + +### DISABLED + +```bash +--verify-certificates DISABLED +``` + +- **Certificate chain**: Not validated — all certificates are trusted +- **Hostname verification**: Skipped +- **Truststore**: Not required +- **Use case**: Development/testing environments, or initial connectivity debugging +- **Warning**: Prints `WARNING: SSL certificate verification is DISABLED. All certificates will be trusted.` + +> **Security Note:** `DISABLED` mode should **never** be used in production. It is vulnerable to man-in-the-middle attacks. + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | **Success** — z/OSMF JWK endpoint is reachable and responding | +| `4` | **Failure** — connection failed, SSL error, endpoint not found, or configuration error | +| `8` | **Help** — help/version was displayed; no check was performed | + +## Response Interpretation + +The tool interprets HTTP response codes from the z/OSMF JWK endpoint as follows: + +| HTTP Code | Result | Message | +|-----------|--------|---------| +| 200-299 | **SUCCESS** | `z/OSMF JWK endpoint is reachable and responding. HTTP ` | +| 401 | **SUCCESS** | `z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401` | +| 404 | **FAILURE** | `z/OSMF JWK endpoint not found. HTTP 404` — Consider configuring `jwtAutoConfiguration` to LTPA | +| 4xx (other) | **FAILURE** | `z/OSMF JWK endpoint returned unexpected client error. HTTP ` | +| 5xx | **FAILURE** | `z/OSMF JWK endpoint returned server error. HTTP ` | + +**Note:** A `401 Unauthorized` is treated as **success** because the tool does not send authentication credentials. A 401 confirms the endpoint exists and z/OSMF is processing requests. + +### Connection-Level Errors + +| Error | Message | +|-------|---------| +| SSL handshake failure | `FAILURE: SSL handshake failed. Verify that the truststore contains the z/OSMF server certificate.` | +| Connection refused | `FAILURE: Cannot connect to :. Verify the host and port are correct and z/OSMF is running.` | +| Connection timeout | `FAILURE: Connection timed out to :.` | + +## Testing Scenarios + +Below are step-by-step commands for testing all modes. Replace `` with your actual JAR version (e.g., `3.5.12-SNAPSHOT`) and adjust the host/port for your environment. + +### 1. Quick Test — DISABLED Mode (No Truststore Needed) + +The fastest way to verify basic TCP + HTTP connectivity: + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --verify-certificates DISABLED +``` + +**Expected output (success):** + +``` +WARNING: SSL certificate verification is DISABLED. All certificates will be trusted. +Checking z/OSMF JWK endpoint: https://myzosmf.example.com:11443/jwt/ibm/api/zOSMFBuilder/jwk +SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401 +``` + +### 2. STRICT Mode — Full Certificate and Hostname Verification + +Requires a truststore containing the z/OSMF server's CA certificate (see [Creating a Truststore](#creating-a-truststore)): + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore /path/to/zosmf-truststore.p12 \ + --truststore-password password +``` + +**Expected output (success):** + +``` +Checking z/OSMF JWK endpoint: https://myzosmf.example.com:11443/jwt/ibm/api/zOSMFBuilder/jwk +SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401 +``` + +**Expected output (SSL failure — wrong truststore):** + +``` +FAILURE: SSL handshake failed when connecting to https://myzosmf.example.com:11443/jwt/ibm/api/zOSMFBuilder/jwk. +Verify that the truststore contains the z/OSMF server certificate. +Details: PKIX path building failed: ...unable to find valid certification path to requested target +``` + +### 3. NONSTRICT Mode — Certificate Chain Verified, Hostname Check Skipped + +Useful when connecting via IP address but the certificate has a DNS name: + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host 10.0.0.50 \ + --zosmf-port 11443 \ + --truststore /path/to/zosmf-truststore.p12 \ + --truststore-password password \ + --verify-certificates NONSTRICT +``` + +**Expected output (success):** + +``` +INFO: Hostname verification is disabled (NONSTRICT mode). +Checking z/OSMF JWK endpoint: https://10.0.0.50:11443/jwt/ibm/api/zOSMFBuilder/jwk +SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401 +``` + +### 4. HTTP Mode (No SSL) + +For z/OSMF instances running on plain HTTP (uncommon): + +```bash +java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 80 \ + --scheme http +``` + +### 5. Validation Error Tests + +**Missing required flags:** + +```bash +# No arguments at all +java -jar pre-flight-check-.jar +# Output: Missing required options: '--zosmf-host=', '--zosmf-port=' + +# Missing truststore in STRICT mode +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 +# Output: ERROR: --truststore is required when --scheme=https and verification is not DISABLED. + +# Missing truststore password +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --truststore my.p12 +# Output: ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED. +``` + +**Invalid values:** + +```bash +# Invalid scheme +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --scheme ftp +# Output: ERROR: --scheme must be 'http' or 'https', got: ftp + +# Invalid verify mode +java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --verify-certificates INVALID +# Output: ERROR: --verify-certificates must be STRICT, NONSTRICT, or DISABLED, got: INVALID +``` + +**Unreachable host:** + +```bash +java -jar pre-flight-check-.jar --zosmf-host nonexistent.host --zosmf-port 443 --verify-certificates DISABLED +# Output: FAILURE: Cannot connect to nonexistent.host:443. +``` + +## SAF Keyrings + +On z/OS, if you are using SAF keyrings instead of file-based keystores/truststores, provide the keyring path in the `safkeyring://` format and add the JVM protocol handler: + +```bash +java -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -jar pre-flight-check-.jar \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore safkeyring://IZUSVR/ZoweKeyring \ + --truststore-password password \ + --truststore-type JCERACFKS +``` diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java index a8a091d9ad..df3fa0d5e5 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java @@ -10,6 +10,7 @@ package org.zowe.apiml; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import java.io.IOException; @@ -25,15 +26,18 @@ public class HttpClientWrapper { private final SSLContext sslContext; private final boolean useHttps; + private final HostnameVerifier hostnameVerifier; - public HttpClientWrapper(SSLContext sslContext) { + public HttpClientWrapper(SSLContext sslContext, HostnameVerifier hostnameVerifier) { this.sslContext = sslContext; this.useHttps = true; + this.hostnameVerifier = hostnameVerifier; } public HttpClientWrapper() { this.sslContext = null; this.useHttps = false; + this.hostnameVerifier = null; } public int executeCall(URL url, Map headers) throws IOException { @@ -41,6 +45,9 @@ public int executeCall(URL url, Map headers) throws IOException if (useHttps) { HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); httpsCon.setSSLSocketFactory(sslContext.getSocketFactory()); + if (hostnameVerifier != null) { + httpsCon.setHostnameVerifier(hostnameVerifier); + } con = httpsCon; } else { con = (HttpURLConnection) url.openConnection(); diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java index 0d4e30be85..034860b9f1 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java @@ -12,9 +12,15 @@ import picocli.CommandLine; +import javax.net.ssl.HostnameVerifier; + @SuppressWarnings("squid:S106") public class PreFlightCheck { + static final String VERIFY_STRICT = "STRICT"; + static final String VERIFY_NONSTRICT = "NONSTRICT"; + static final String VERIFY_DISABLED = "DISABLED"; + public static int mainWithExitCode(String[] args) { try { PreFlightCheckConf conf = new PreFlightCheckConf(); @@ -31,9 +37,25 @@ public static int mainWithExitCode(String[] args) { HttpClientWrapper httpClient; if ("https".equalsIgnoreCase(conf.getScheme())) { - Stores stores = new Stores(conf); - SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); - httpClient = new HttpClientWrapper(sslContextFactory.getSslContext()); + String verifyMode = conf.getVerifyCertificates().toUpperCase(); + + if (VERIFY_DISABLED.equals(verifyMode)) { + SSLContextFactory sslContextFactory = SSLContextFactory.initTrustAllSSLContext(); + HostnameVerifier noopVerifier = (hostname, session) -> true; + httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); + } else { + Stores stores = new Stores(conf); + SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); + + HostnameVerifier hostnameVerifier; + if (VERIFY_NONSTRICT.equals(verifyMode)) { + hostnameVerifier = (hostname, session) -> true; + System.out.println("INFO: Hostname verification is disabled (NONSTRICT mode)."); + } else { + hostnameVerifier = null; // use default JDK hostname verifier + } + httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), hostnameVerifier); + } } else { httpClient = new HttpClientWrapper(); } @@ -53,13 +75,18 @@ static void validateConfig(PreFlightCheckConf conf) { throw new IllegalArgumentException("--scheme must be 'http' or 'https', got: " + scheme); } - if ("https".equalsIgnoreCase(scheme)) { + String verifyMode = conf.getVerifyCertificates().toUpperCase(); + if (!VERIFY_STRICT.equals(verifyMode) && !VERIFY_NONSTRICT.equals(verifyMode) && !VERIFY_DISABLED.equals(verifyMode)) { + throw new IllegalArgumentException("--verify-certificates must be STRICT, NONSTRICT, or DISABLED, got: " + conf.getVerifyCertificates()); + } + + if ("https".equalsIgnoreCase(scheme) && !VERIFY_DISABLED.equals(verifyMode)) { if (conf.getTrustStore() == null) { - throw new IllegalArgumentException("--truststore is required when --scheme=https. " + + throw new IllegalArgumentException("--truststore is required when --scheme=https and verification is not DISABLED. " + "Provide the path to the truststore containing the z/OSMF server certificate."); } if (conf.getTrustStorePassword() == null) { - throw new IllegalArgumentException("--truststore-password is required when --scheme=https."); + throw new IllegalArgumentException("--truststore-password is required when --scheme=https and verification is not DISABLED."); } } } diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java index 2887736bb3..106c4c664c 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java @@ -51,6 +51,9 @@ public class PreFlightCheckConf implements PreFlightCheckConfig { @Option(names = {"--truststore-type"}, description = "Type of truststore (default: ${DEFAULT-VALUE})") private String trustStoreType = "PKCS12"; + @Option(names = {"--verify-certificates"}, description = "Certificate verification mode: STRICT, NONSTRICT, or DISABLED (default: ${DEFAULT-VALUE})") + private String verifyCertificates = "STRICT"; + @Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message") private boolean helpRequested = false; @@ -99,6 +102,11 @@ public String getTrustStoreType() { return trustStoreType; } + @Override + public String getVerifyCertificates() { + return verifyCertificates; + } + @Override public boolean isHelpRequested() { return helpRequested; diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java index aa9ee16040..0f0bb1e215 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java @@ -30,5 +30,7 @@ public interface PreFlightCheckConfig { String getTrustStoreType(); + String getVerifyCertificates(); + boolean isHelpRequested(); } diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index c11b148343..480d6f03e8 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -12,11 +12,15 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.security.*; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +@SuppressWarnings("squid:S106") public class SSLContextFactory { private final Stores stores; @@ -49,4 +53,34 @@ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgor factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); return factory; } + + public static SSLContextFactory initTrustAllSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException { + SSLContextFactory factory = new SSLContextFactory(null); + + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // trust all + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // trust all + } + } + }; + + KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); + emptyKeystore.load(null, null); + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyFactory.init(emptyKeystore, null); + + factory.sslContext = SSLContext.getInstance("TLSv1.2"); + factory.sslContext.init(keyFactory.getKeyManagers(), trustAllCerts, new SecureRandom()); + System.out.println("WARNING: SSL certificate verification is DISABLED. All certificates will be trusted."); + return factory; + } } diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java b/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java index a361a75819..762a5c4a5d 100644 --- a/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java +++ b/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java @@ -63,12 +63,29 @@ void invalidSchemeIsRejected() { } @Test - void httpsWithoutTruststoreIsRejected() { + void invalidVerifyCertificatesIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", + "--truststore", "some/path.p12", "--truststore-password", "pass", + "--verify-certificates", "INVALID"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--verify-certificates must be STRICT, NONSTRICT, or DISABLED")); + } + + @Test + void httpsStrictWithoutTruststoreIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https"}; assertEquals(4, PreFlightCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--truststore is required")); } + @Test + void httpsNonstrictWithoutTruststoreIsRejected() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", + "--verify-certificates", "NONSTRICT"}; + assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertTrue(errStream.toString().contains("--truststore is required")); + } + @Test void httpsWithoutTruststorePasswordIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", @@ -77,6 +94,17 @@ void httpsWithoutTruststorePasswordIsRejected() { assertTrue(errStream.toString().contains("--truststore-password is required")); } + @Test + void httpsDisabledDoesNotRequireTruststore() { + // DISABLED mode skips certificate verification entirely — no truststore needed + // Will fail to connect to a non-existent server, but should pass validation + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "https", + "--verify-certificates", "DISABLED"}; + int exitCode = PreFlightCheck.mainWithExitCode(args); + assertEquals(4, exitCode); + assertFalse(errStream.toString().contains("--truststore is required")); + } + @Test void httpDoesNotRequireTruststore() { // This will fail to connect but should not fail validation From 33a4b1e2d9555e8df835bbf20baa1ea481cf21e2 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Sat, 28 Mar 2026 01:19:21 +0530 Subject: [PATCH 03/10] config change for publishing SDK Signed-off-by: hrishikesh-nalawade --- gradle/publish.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 607f189068..8f481a156d 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -11,6 +11,7 @@ ext.javaLibraries = [ 'apiml-security-common', 'apiml-tomcat-common', 'certificate-analyser', + 'pre-flight-check', 'common-service-core', 'security-service-client-spring', 'apiml-sample-extension', From 41f8345ea6241a0731aaa587a1ce3509ab77aa89 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Tue, 31 Mar 2026 23:33:19 +0530 Subject: [PATCH 04/10] adding Java Doc Signed-off-by: hrishikesh-nalawade --- .../java/org/zowe/apiml/HttpClientWrapper.java | 5 +++++ .../org/zowe/apiml/JwkEndpointChecker.java | 4 ++++ .../java/org/zowe/apiml/PreFlightCheck.java | 7 +++++++ .../org/zowe/apiml/PreFlightCheckConf.java | 5 +++++ .../org/zowe/apiml/PreFlightCheckConfig.java | 5 +++++ .../java/org/zowe/apiml/SSLContextFactory.java | 18 ++++++++++++++++++ .../src/main/java/org/zowe/apiml/Stores.java | 16 ++++++++++++++++ .../apiml/StoresNotInitializeException.java | 4 ++++ 8 files changed, 64 insertions(+) diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java index df3fa0d5e5..490f94f194 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java @@ -18,6 +18,11 @@ import java.net.URL; import java.util.Map; +/** + * HTTP/HTTPS client wrapping {@link java.net.HttpURLConnection}. + * Supports custom {@link SSLContext} and {@link HostnameVerifier} for + * STRICT, NONSTRICT, and DISABLED certificate verification modes. + */ @SuppressWarnings("squid:S106") public class HttpClientWrapper { diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java b/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java index c9301b2084..fcd68d119d 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java @@ -17,6 +17,10 @@ import java.util.HashMap; import java.util.Map; +/** + * Checks z/OSMF JWK endpoint availability at {@code /jwt/ibm/api/zOSMFBuilder/jwk}. + * Interprets the HTTP response code to determine if the endpoint is functional + */ @SuppressWarnings("squid:S106") public class JwkEndpointChecker { diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java index 034860b9f1..b968d53a4f 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java @@ -14,6 +14,13 @@ import javax.net.ssl.HostnameVerifier; +/** + * Entry point and orchestrator for the pre-flight check tool. + * Parses CLI arguments, validates configuration, builds the appropriate + * SSL context and HTTP client, then delegates to {@link JwkEndpointChecker}. + * + *

Exit codes: 0 = success, 4 = failure/error, 8 = help displayed.

+ */ @SuppressWarnings("squid:S106") public class PreFlightCheck { diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java index 106c4c664c..5d3284f5c4 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java @@ -13,6 +13,11 @@ import picocli.CommandLine; import picocli.CommandLine.Option; +/** + * CLI argument parser backed by picocli. + * Maps command-line flags (e.g. {@code --zosmf-host}, {@code --verify-certificates}) + * to configuration properties exposed via {@link PreFlightCheckConfig}. + */ @CommandLine.Command( name = "pre-flight-check", version = { diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java index 0f0bb1e215..d42bbaaedb 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java @@ -10,6 +10,11 @@ package org.zowe.apiml; +/** + * Configuration contract for the pre-flight check tool. + * Exposes all user-supplied settings such as z/OSMF host, port, scheme, + * keystore/truststore paths, and certificate verification mode. + */ public interface PreFlightCheckConfig { String getZosmfHost(); diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index 480d6f03e8..8e4f8cedaf 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -20,6 +20,13 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +/** + * Builds a TLSv1.2 {@link SSLContext} in two modes: + *
    + *
  • {@link #initSSLContext(Stores)} — normal mode using real truststore/keystore
  • + *
  • {@link #initTrustAllSSLContext()} — trust-all mode for DISABLED verification
  • + *
+ */ @SuppressWarnings("squid:S106") public class SSLContextFactory { @@ -34,6 +41,12 @@ public SSLContext getSslContext() { return sslContext; } + /** + * Creates an SSLContext using the provided keystore and truststore. + * + * @param stores loaded keystore/truststore pair + * @return factory holding the initialized SSLContext + */ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { SSLContextFactory factory = new SSLContextFactory(stores); @@ -54,6 +67,11 @@ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgor return factory; } + /** + * Creates an SSLContext that trusts all certificates. Use only when verification is DISABLED. + * + * @return factory holding the trust-all SSLContext + */ public static SSLContextFactory initTrustAllSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException { SSLContextFactory factory = new SSLContextFactory(null); diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java b/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java index 04041403fb..53cc962b27 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java @@ -23,6 +23,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Loads Java {@link java.security.KeyStore} instances from the filesystem + * or z/OS SAF keyrings. Supports PKCS12, JKS, and {@code safkeyring://} URIs. + */ @SuppressWarnings("squid:S106") public class Stores { @@ -37,12 +41,24 @@ public Stores(PreFlightCheckConfig conf) { init(); } + /** + * Checks whether the given path is a SAF keyring URI. + * + * @param input store path to check + * @return {@code true} if the path matches the keyring pattern + */ public static boolean isKeyring(String input) { if (input == null) return false; Matcher matcher = KEYRING_PATTERN.matcher(input); return matcher.matches(); } + /** + * Normalizes a keyring URI to the canonical {@code safkeyring://userId/keyRing} format. + * + * @param input raw keyring URI + * @return normalized URI, or the original input if not a keyring + */ public static String formatKeyringUrl(String input) { if (input == null) return null; Matcher matcher = KEYRING_PATTERN.matcher(input); diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java b/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java index e5c77f773c..c6b5231cf2 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java +++ b/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java @@ -10,6 +10,10 @@ package org.zowe.apiml; +/** + * Thrown when keystore or truststore initialization fails + * (e.g. missing file, wrong password, invalid keyring format). + */ public class StoresNotInitializeException extends RuntimeException { public StoresNotInitializeException(String message) { From 138c47defea1c84856a43fcf2fa3ae7c82c70750 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 1 Apr 2026 17:29:25 +0530 Subject: [PATCH 05/10] renaming pre-flight-check to zosmf-jwt-check Signed-off-by: hrishikesh-nalawade --- build.gradle | 2 +- gradle/publish.gradle | 2 +- settings.gradle | 2 +- .../README.md | 58 +++++++------------ .../build.gradle | 2 +- .../org/zowe/apiml/HttpClientWrapper.java | 0 .../org/zowe/apiml/JwkEndpointChecker.java | 16 +++-- .../org/zowe/apiml/SSLContextFactory.java | 0 .../src/main/java/org/zowe/apiml/Stores.java | 6 +- .../apiml/StoresNotInitializeException.java | 0 .../java/org/zowe/apiml/ZosmfJwtCheck.java | 10 ++-- .../org/zowe/apiml/ZosmfJwtCheckConf.java | 10 ++-- .../org/zowe/apiml/ZosmfJwtCheckConfig.java | 4 +- .../zowe/apiml/JwkEndpointCheckerTest.java | 6 +- .../org/zowe/apiml/ZosmfJwtCheckTest.java | 22 +++---- 15 files changed, 66 insertions(+), 74 deletions(-) rename {pre-flight-check => zosmf-jwt-check}/README.md (81%) rename {pre-flight-check => zosmf-jwt-check}/build.gradle (89%) rename {pre-flight-check => zosmf-jwt-check}/src/main/java/org/zowe/apiml/HttpClientWrapper.java (100%) rename {pre-flight-check => zosmf-jwt-check}/src/main/java/org/zowe/apiml/JwkEndpointChecker.java (81%) rename {pre-flight-check => zosmf-jwt-check}/src/main/java/org/zowe/apiml/SSLContextFactory.java (100%) rename {pre-flight-check => zosmf-jwt-check}/src/main/java/org/zowe/apiml/Stores.java (97%) rename {pre-flight-check => zosmf-jwt-check}/src/main/java/org/zowe/apiml/StoresNotInitializeException.java (100%) rename pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java => zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java (93%) rename pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java => zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java (91%) rename pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java => zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java (89%) rename {pre-flight-check => zosmf-jwt-check}/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java (96%) rename pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java => zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java (85%) diff --git a/build.gradle b/build.gradle index c88fe4b4fe..9d8ed86a6e 100644 --- a/build.gradle +++ b/build.gradle @@ -183,7 +183,7 @@ ext.javaLibraries = [ 'apiml-security-common', 'apiml-tomcat-common', 'certificate-analyser', - 'pre-flight-check', + 'zosmf-jwt-check', 'common-service-core', 'security-service-client-spring', 'apiml-sample-extension', diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 8f481a156d..7a6982ce50 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -11,7 +11,7 @@ ext.javaLibraries = [ 'apiml-security-common', 'apiml-tomcat-common', 'certificate-analyser', - 'pre-flight-check', + 'zosmf-jwt-check', 'common-service-core', 'security-service-client-spring', 'apiml-sample-extension', diff --git a/settings.gradle b/settings.gradle index 8c4a32051f..aeb34a82e7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -54,7 +54,7 @@ include 'onboarding-enabler-python' include 'zaas-client' include 'mock-services' include 'certificate-analyser' -include 'pre-flight-check' +include 'zosmf-jwt-check' include 'apiml-tomcat-common' include 'apiml-sample-extension' include 'apiml-sample-extension-package' diff --git a/pre-flight-check/README.md b/zosmf-jwt-check/README.md similarity index 81% rename from pre-flight-check/README.md rename to zosmf-jwt-check/README.md index b84b00b87c..be134af7ed 100644 --- a/pre-flight-check/README.md +++ b/zosmf-jwt-check/README.md @@ -1,10 +1,9 @@ -# Pre-Flight Check Tool +# z/OSMF JWT Check Tool -A Java utility that verifies connectivity to the z/OSMF JWK endpoint **before/after** starting the Zowe API Mediation Layer. This tool helps diagnose configuration issues early such as incorrect hostnames, unreachable ports, missing certificates, or misconfigured z/OSMF by performing a lightweight HTTP(S) call to the z/OSMF JWK endpoint at `/jwt/ibm/api/zOSMFBuilder/jwk`. +A Java utility that verifies connectivity to the z/OSMF JWK endpoint. This tool helps diagnose configuration issues early such as incorrect hostnames, unreachable ports, missing certificates, or misconfigured z/OSMF by performing a lightweight HTTP(S) call to the z/OSMF JWK endpoint at `/jwt/ibm/api/zOSMFBuilder/jwk`. ## Table of Contents -- [Overview](#overview) - [Prerequisites](#prerequisites) - [Building](#building) - [Usage](#usage) @@ -22,19 +21,6 @@ A Java utility that verifies connectivity to the z/OSMF JWK endpoint **before/af - [Troubleshooting](#troubleshooting) --- - -## Overview - -When Zowe API ML starts, it attempts to reach z/OSMF to obtain public keys for JWT token validation. If z/OSMF is unreachable or misconfigured, the startup fails with errors that can be difficult to diagnose. This pre-flight check tool isolates that connectivity test into a simple, standalone JAR that can be run before Zowe startup. - -**What it checks:** - -- TCP connectivity to the z/OSMF host and port -- SSL/TLS handshake (when using HTTPS) -- Certificate trust chain validation (STRICT/NONSTRICT modes) -- Hostname verification (STRICT mode) -- HTTP response from the JWK endpoint (`/jwt/ibm/api/zOSMFBuilder/jwk`) - ## Prerequisites - **Java 17 or higher** (Java 17, 21, or any later version) @@ -46,33 +32,33 @@ When Zowe API ML starts, it attempts to reach z/OSMF to obtain public keys for J From the root of the `api-layer` repository: ```bash -./gradlew :pre-flight-check:build +./gradlew :zosmf-jwt-check:build ``` On Windows: ```powershell -.\gradlew :pre-flight-check:build +.\gradlew :zosmf-jwt-check:build ``` The fat JAR (with all dependencies bundled) will be generated at: ``` -pre-flight-check/build/libs/pre-flight-check-.jar +zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar ``` -For example: `pre-flight-check/build/libs/pre-flight-check-3.5.12-SNAPSHOT.jar` +For example: `zosmf-jwt-check/build/libs/zosmf-jwt-check-3.5.12-SNAPSHOT.jar` ## Usage ```bash -java -jar pre-flight-check-.jar --zosmf-host --zosmf-port [options] +java -jar zosmf-jwt-check-.jar --zosmf-host --zosmf-port [options] ``` -**Minimal example (DISABLED mode — quickest way to test):** +**Minimal example (DISABLED mode, quickest way to test):** ```bash -java -jar pre-flight-check-.jar \ +java -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --verify-certificates DISABLED @@ -81,7 +67,7 @@ java -jar pre-flight-check-.jar \ **Full example (STRICT mode with truststore):** ```bash -java -jar pre-flight-check-.jar \ +java -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --truststore /path/to/truststore.p12 \ @@ -91,7 +77,7 @@ java -jar pre-flight-check-.jar \ **Display help:** ```bash -java -jar pre-flight-check-.jar --help +java -jar zosmf-jwt-check-.jar --help ``` ## CLI Flags Reference @@ -206,7 +192,7 @@ Below are step-by-step commands for testing all modes. Replace `` with The fastest way to verify basic TCP + HTTP connectivity: ```bash -java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ +java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --verify-certificates DISABLED @@ -225,7 +211,7 @@ SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected with Requires a truststore containing the z/OSMF server's CA certificate (see [Creating a Truststore](#creating-a-truststore)): ```bash -java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ +java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --truststore /path/to/zosmf-truststore.p12 \ @@ -252,7 +238,7 @@ Details: PKIX path building failed: ...unable to find valid certification path t Useful when connecting via IP address but the certificate has a DNS name: ```bash -java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ +java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ --zosmf-host 10.0.0.50 \ --zosmf-port 11443 \ --truststore /path/to/zosmf-truststore.p12 \ @@ -273,7 +259,7 @@ SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected with For z/OSMF instances running on plain HTTP (uncommon): ```bash -java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ +java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 80 \ --scheme http @@ -285,15 +271,15 @@ java -jar pre-flight-check/build/libs/pre-flight-check-.jar \ ```bash # No arguments at all -java -jar pre-flight-check-.jar +java -jar zosmf-jwt-check-.jar # Output: Missing required options: '--zosmf-host=', '--zosmf-port=' # Missing truststore in STRICT mode -java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 +java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 # Output: ERROR: --truststore is required when --scheme=https and verification is not DISABLED. # Missing truststore password -java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --truststore my.p12 +java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 --truststore my.p12 # Output: ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED. ``` @@ -301,18 +287,18 @@ java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 -- ```bash # Invalid scheme -java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --scheme ftp +java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 --scheme ftp # Output: ERROR: --scheme must be 'http' or 'https', got: ftp # Invalid verify mode -java -jar pre-flight-check-.jar --zosmf-host myhost --zosmf-port 443 --verify-certificates INVALID +java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 --verify-certificates INVALID # Output: ERROR: --verify-certificates must be STRICT, NONSTRICT, or DISABLED, got: INVALID ``` **Unreachable host:** ```bash -java -jar pre-flight-check-.jar --zosmf-host nonexistent.host --zosmf-port 443 --verify-certificates DISABLED +java -jar zosmf-jwt-check-.jar --zosmf-host nonexistent.host --zosmf-port 443 --verify-certificates DISABLED # Output: FAILURE: Cannot connect to nonexistent.host:443. ``` @@ -322,7 +308,7 @@ On z/OS, if you are using SAF keyrings instead of file-based keystores/truststor ```bash java -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ - -jar pre-flight-check-.jar \ + -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --truststore safkeyring://IZUSVR/ZoweKeyring \ diff --git a/pre-flight-check/build.gradle b/zosmf-jwt-check/build.gradle similarity index 89% rename from pre-flight-check/build.gradle rename to zosmf-jwt-check/build.gradle index ca98a4c3d4..e95188ccdf 100644 --- a/pre-flight-check/build.gradle +++ b/zosmf-jwt-check/build.gradle @@ -17,7 +17,7 @@ compileJava { jar { manifest { attributes( - 'Main-Class': 'org.zowe.apiml.PreFlightCheck' + 'Main-Class': 'org.zowe.apiml.ZosmfJwtCheck' ) } duplicatesStrategy = DuplicatesStrategy.EXCLUDE diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java similarity index 100% rename from pre-flight-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java similarity index 81% rename from pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java index fcd68d119d..648099e2e9 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java @@ -13,6 +13,7 @@ import javax.net.ssl.SSLHandshakeException; import java.net.ConnectException; import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -28,9 +29,9 @@ public class JwkEndpointChecker { private static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; private final HttpClientWrapper httpClient; - private final PreFlightCheckConfig conf; + private final ZosmfJwtCheckConfig conf; - public JwkEndpointChecker(HttpClientWrapper httpClient, PreFlightCheckConfig conf) { + public JwkEndpointChecker(HttpClientWrapper httpClient, ZosmfJwtCheckConfig conf) { this.httpClient = httpClient; this.conf = conf; } @@ -59,10 +60,15 @@ public boolean check() { return false; } catch (SocketTimeoutException e) { System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); - System.err.println("Details: " + e.getMessage()); + System.err.println("This is commonly caused by an incorrect host/port or a firewall blocking the connection."); + System.err.println("Verify the z/OSMF host and port are correct and that no firewall is blocking access."); + return false; + } catch (UnknownHostException e) { + System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); + System.err.println("The host '" + conf.getZosmfHost() + "' could not be resolved."); return false; } catch (Exception e) { - System.err.println("FAILURE: Unexpected error when calling " + urlString + "."); + System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); System.err.println("Details: " + e.getMessage()); return false; } @@ -81,7 +87,7 @@ private boolean evaluateResponseCode(int responseCode, String urlString) { if (responseCode == 404) { System.err.println("FAILURE: z/OSMF JWK endpoint not found. HTTP 404"); - System.err.println("Try configuring the jwtAutoConfiguration to LTPA"); + System.err.println("JWT support not found, may not be configured. LTPA may be used as an alternative"); return false; } diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java similarity index 100% rename from pre-flight-check/src/main/java/org/zowe/apiml/SSLContextFactory.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java similarity index 97% rename from pre-flight-check/src/main/java/org/zowe/apiml/Stores.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java index 53cc962b27..ef7b527e67 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/Stores.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java @@ -34,9 +34,9 @@ public class Stores { private KeyStore keyStore; private KeyStore trustStore; - private final PreFlightCheckConfig conf; + private final ZosmfJwtCheckConfig conf; - public Stores(PreFlightCheckConfig conf) { + public Stores(ZosmfJwtCheckConfig conf) { this.conf = conf; init(); } @@ -130,7 +130,7 @@ public KeyStore getTrustStore() { return trustStore; } - public PreFlightCheckConfig getConf() { + public ZosmfJwtCheckConfig getConf() { return conf; } diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java similarity index 100% rename from pre-flight-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java similarity index 93% rename from pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java index b968d53a4f..dbadbd0ca7 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheck.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java @@ -15,14 +15,14 @@ import javax.net.ssl.HostnameVerifier; /** - * Entry point and orchestrator for the pre-flight check tool. + * Entry point and orchestrator for the z/OSMF JWT check tool. * Parses CLI arguments, validates configuration, builds the appropriate * SSL context and HTTP client, then delegates to {@link JwkEndpointChecker}. * *

Exit codes: 0 = success, 4 = failure/error, 8 = help displayed.

*/ @SuppressWarnings("squid:S106") -public class PreFlightCheck { +public class ZosmfJwtCheck { static final String VERIFY_STRICT = "STRICT"; static final String VERIFY_NONSTRICT = "NONSTRICT"; @@ -30,13 +30,13 @@ public class PreFlightCheck { public static int mainWithExitCode(String[] args) { try { - PreFlightCheckConf conf = new PreFlightCheckConf(); + ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); cmd.parseArgs(args); if (conf.isHelpRequested()) { cmd.printVersionHelp(System.out); - CommandLine.usage(new PreFlightCheckConf(), System.out); + CommandLine.usage(new ZosmfJwtCheckConf(), System.out); return 8; } @@ -76,7 +76,7 @@ public static int mainWithExitCode(String[] args) { } } - static void validateConfig(PreFlightCheckConf conf) { + static void validateConfig(ZosmfJwtCheckConf conf) { String scheme = conf.getScheme(); if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { throw new IllegalArgumentException("--scheme must be 'http' or 'https', got: " + scheme); diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java similarity index 91% rename from pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java index 5d3284f5c4..9fc3cb1017 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConf.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java @@ -16,18 +16,18 @@ /** * CLI argument parser backed by picocli. * Maps command-line flags (e.g. {@code --zosmf-host}, {@code --verify-certificates}) - * to configuration properties exposed via {@link PreFlightCheckConfig}. + * to configuration properties exposed via {@link ZosmfJwtCheckConfig}. */ @CommandLine.Command( - name = "pre-flight-check", + name = "zosmf-jwt-check", version = { - "Pre-Flight Check 1.0", + "z/OSMF JWT Check 1.0", "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", "OS: ${os.name} ${os.version} ${os.arch}" }, - description = "Performs a pre-flight connectivity check against the z/OSMF JWK endpoint." + description = "Checks connectivity to the z/OSMF JWK endpoint." ) -public class PreFlightCheckConf implements PreFlightCheckConfig { +public class ZosmfJwtCheckConf implements ZosmfJwtCheckConfig { @Option(names = {"--zosmf-host"}, required = true, description = "Hostname or IP of the z/OSMF server") private String zosmfHost; diff --git a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java similarity index 89% rename from pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java index d42bbaaedb..f94d10fc40 100644 --- a/pre-flight-check/src/main/java/org/zowe/apiml/PreFlightCheckConfig.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java @@ -11,11 +11,11 @@ package org.zowe.apiml; /** - * Configuration contract for the pre-flight check tool. + * Configuration contract for the z/OSMF JWT check tool. * Exposes all user-supplied settings such as z/OSMF host, port, scheme, * keystore/truststore paths, and certificate verification mode. */ -public interface PreFlightCheckConfig { +public interface ZosmfJwtCheckConfig { String getZosmfHost(); diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java similarity index 96% rename from pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java rename to zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java index 642f4d507c..09370fe7d0 100644 --- a/pre-flight-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java @@ -36,7 +36,7 @@ class JwkEndpointCheckerTest { private final PrintStream originalErr = System.err; private HttpClientWrapper mockClient; - private PreFlightCheckConfig mockConf; + private ZosmfJwtCheckConfig mockConf; @BeforeEach void setUp() { @@ -44,7 +44,7 @@ void setUp() { System.setErr(new PrintStream(errStream)); mockClient = mock(HttpClientWrapper.class); - mockConf = mock(PreFlightCheckConfig.class); + mockConf = mock(ZosmfJwtCheckConfig.class); when(mockConf.getScheme()).thenReturn("https"); when(mockConf.getZosmfHost()).thenReturn("zosmf.example.com"); when(mockConf.getZosmfPort()).thenReturn(443); @@ -142,7 +142,7 @@ void unexpectedExceptionIsHandled() throws IOException { when(mockClient.executeCall(any(), anyMap())).thenThrow(new IOException("unexpected")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertFalse(checker.check()); - assertTrue(errStream.toString().contains("Unexpected error")); + assertTrue(errStream.toString().contains("verify hostname and port")); } } } diff --git a/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java similarity index 85% rename from pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java rename to zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java index 762a5c4a5d..c4a96eb1b9 100644 --- a/pre-flight-check/src/test/java/org/zowe/apiml/PreFlightCheckTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java @@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.*; -class PreFlightCheckTest { +class ZosmfJwtCheckTest { private final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); private final ByteArrayOutputStream errStream = new ByteArrayOutputStream(); @@ -42,14 +42,14 @@ void restoreStreams() { @Test void helpFlagReturnsExitCode8() { String[] args = {"--help"}; - assertEquals(8, PreFlightCheck.mainWithExitCode(args)); - assertTrue(outStream.toString().contains("Pre-Flight Check")); + assertEquals(8, ZosmfJwtCheck.mainWithExitCode(args)); + assertTrue(outStream.toString().contains("z/OSMF JWT Check")); } @Test void missingRequiredArgsReturnsExitCode4() { String[] args = {}; - assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); } @Nested @@ -58,7 +58,7 @@ class ValidationTests { @Test void invalidSchemeIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "ftp"}; - assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--scheme must be 'http' or 'https'")); } @@ -67,14 +67,14 @@ void invalidVerifyCertificatesIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", "--truststore", "some/path.p12", "--truststore-password", "pass", "--verify-certificates", "INVALID"}; - assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--verify-certificates must be STRICT, NONSTRICT, or DISABLED")); } @Test void httpsStrictWithoutTruststoreIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https"}; - assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--truststore is required")); } @@ -82,7 +82,7 @@ void httpsStrictWithoutTruststoreIsRejected() { void httpsNonstrictWithoutTruststoreIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", "--verify-certificates", "NONSTRICT"}; - assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--truststore is required")); } @@ -90,7 +90,7 @@ void httpsNonstrictWithoutTruststoreIsRejected() { void httpsWithoutTruststorePasswordIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", "--truststore", "some/path.p12"}; - assertEquals(4, PreFlightCheck.mainWithExitCode(args)); + assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--truststore-password is required")); } @@ -100,7 +100,7 @@ void httpsDisabledDoesNotRequireTruststore() { // Will fail to connect to a non-existent server, but should pass validation String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "https", "--verify-certificates", "DISABLED"}; - int exitCode = PreFlightCheck.mainWithExitCode(args); + int exitCode = ZosmfJwtCheck.mainWithExitCode(args); assertEquals(4, exitCode); assertFalse(errStream.toString().contains("--truststore is required")); } @@ -109,7 +109,7 @@ void httpsDisabledDoesNotRequireTruststore() { void httpDoesNotRequireTruststore() { // This will fail to connect but should not fail validation String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "http"}; - int exitCode = PreFlightCheck.mainWithExitCode(args); + int exitCode = ZosmfJwtCheck.mainWithExitCode(args); // Should be 4 (connection failure) not a validation error assertEquals(4, exitCode); assertFalse(errStream.toString().contains("--truststore is required")); From 0b1f19624f9102d527e49307dba2ba650da96fc7 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 1 Apr 2026 19:17:28 +0530 Subject: [PATCH 06/10] Added verbose mode (-v) support Signed-off-by: hrishikesh-nalawade --- .../org/zowe/apiml/HttpClientWrapper.java | 35 +++++++++++++++++-- .../org/zowe/apiml/JwkEndpointChecker.java | 9 +++-- .../org/zowe/apiml/ZosmfJwtCheckConf.java | 8 +++++ .../org/zowe/apiml/ZosmfJwtCheckConfig.java | 2 ++ .../zowe/apiml/JwkEndpointCheckerTest.java | 11 +++--- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java index 490f94f194..8faca04157 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/HttpClientWrapper.java @@ -13,10 +13,15 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.stream.Collectors; /** * HTTP/HTTPS client wrapping {@link java.net.HttpURLConnection}. @@ -29,6 +34,19 @@ public class HttpClientWrapper { private static final int CONNECT_TIMEOUT = 5000; private static final int READ_TIMEOUT = 5000; + public static class Response { + private final int statusCode; + private final String body; + + public Response(int statusCode, String body) { + this.statusCode = statusCode; + this.body = body; + } + + public int getStatusCode() { return statusCode; } + public String getBody() { return body; } + } + private final SSLContext sslContext; private final boolean useHttps; private final HostnameVerifier hostnameVerifier; @@ -45,7 +63,7 @@ public HttpClientWrapper() { this.hostnameVerifier = null; } - public int executeCall(URL url, Map headers) throws IOException { + public Response executeCall(URL url, Map headers) throws IOException { HttpURLConnection con; if (useHttps) { HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); @@ -69,9 +87,22 @@ public int executeCall(URL url, Map headers) throws IOException } try { - return con.getResponseCode(); + int responseCode = con.getResponseCode(); + String body = readBody(con); + return new Response(responseCode, body); } finally { con.disconnect(); } } + + private String readBody(HttpURLConnection con) { + try { + InputStream is = con.getErrorStream() != null ? con.getErrorStream() : con.getInputStream(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } catch (Exception e) { + return null; + } + } } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java index 648099e2e9..3eaeea9e11 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java @@ -46,8 +46,11 @@ public boolean check() { URL url = new URL(urlString); System.out.println("Checking z/OSMF JWK endpoint: " + urlString); - int responseCode = httpClient.executeCall(url, headers); - return evaluateResponseCode(responseCode, urlString); + HttpClientWrapper.Response response = httpClient.executeCall(url, headers); + if (conf.isVerbose() && response.getBody() != null) { + System.out.println("Response body:\n" + response.getBody()); + } + return evaluateResponseCode(response.getStatusCode(), urlString); } catch (SSLHandshakeException e) { System.err.println("FAILURE: SSL handshake failed when connecting to " + urlString + "."); System.err.println("Verify that the truststore contains the z/OSMF server certificate."); @@ -81,7 +84,7 @@ private boolean evaluateResponseCode(int responseCode, String urlString) { } if (responseCode == 401) { - System.out.println("SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected without credentials). HTTP 401"); + System.out.println("SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized, expected without credentials). HTTP 401"); return true; } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java index 9fc3cb1017..8296ce0572 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java @@ -59,6 +59,9 @@ public class ZosmfJwtCheckConf implements ZosmfJwtCheckConfig { @Option(names = {"--verify-certificates"}, description = "Certificate verification mode: STRICT, NONSTRICT, or DISABLED (default: ${DEFAULT-VALUE})") private String verifyCertificates = "STRICT"; + @Option(names = {"-v", "--verbose"}, description = "Print the response body from the endpoint") + private boolean verbose = false; + @Option(names = {"-h", "--help"}, usageHelp = true, description = "Display a help message") private boolean helpRequested = false; @@ -112,6 +115,11 @@ public String getVerifyCertificates() { return verifyCertificates; } + @Override + public boolean isVerbose() { + return verbose; + } + @Override public boolean isHelpRequested() { return helpRequested; diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java index f94d10fc40..f52f7832e1 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConfig.java @@ -37,5 +37,7 @@ public interface ZosmfJwtCheckConfig { String getVerifyCertificates(); + boolean isVerbose(); + boolean isHelpRequested(); } diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java index 09370fe7d0..8e3d813388 100644 --- a/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java @@ -48,6 +48,7 @@ void setUp() { when(mockConf.getScheme()).thenReturn("https"); when(mockConf.getZosmfHost()).thenReturn("zosmf.example.com"); when(mockConf.getZosmfPort()).thenReturn(443); + when(mockConf.isVerbose()).thenReturn(false); } @AfterEach @@ -61,7 +62,7 @@ class SuccessResponses { @Test void response200IsSuccess() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(200); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertTrue(checker.check()); assertTrue(outStream.toString().contains("SUCCESS")); @@ -70,7 +71,7 @@ void response200IsSuccess() throws IOException { @Test void response401IsSuccess() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(401); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(401, "")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertTrue(checker.check()); assertTrue(outStream.toString().contains("SUCCESS")); @@ -83,7 +84,7 @@ class FailureResponses { @Test void response404IsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(404); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(404, "")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertFalse(checker.check()); assertTrue(errStream.toString().contains("FAILURE")); @@ -92,7 +93,7 @@ void response404IsFailure() throws IOException { @Test void response500IsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(500); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(500, "")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertFalse(checker.check()); assertTrue(errStream.toString().contains("FAILURE")); @@ -101,7 +102,7 @@ void response500IsFailure() throws IOException { @Test void response403IsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(403); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(403, "")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertFalse(checker.check()); assertTrue(errStream.toString().contains("FAILURE")); From 5b2d30c305b70f9cb0d589c497d4408fb367911f Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 1 Apr 2026 19:57:03 +0530 Subject: [PATCH 07/10] empty response body handling Signed-off-by: hrishikesh-nalawade --- .../org/zowe/apiml/JwkEndpointChecker.java | 37 ++++++++++++++++++- .../zowe/apiml/JwkEndpointCheckerTest.java | 10 ++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java index 3eaeea9e11..06f5b0681a 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/JwkEndpointChecker.java @@ -17,6 +17,8 @@ import java.net.URL; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Checks z/OSMF JWK endpoint availability at {@code /jwt/ibm/api/zOSMFBuilder/jwk}. @@ -27,6 +29,7 @@ public class JwkEndpointChecker { static final String JWK_ENDPOINT_PATH = "/jwt/ibm/api/zOSMFBuilder/jwk"; private static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; + private static final Pattern N_VALUE_PATTERN = Pattern.compile("\"n\"\\s*:\\s*\"([^\"]*)\""); private final HttpClientWrapper httpClient; private final ZosmfJwtCheckConfig conf; @@ -50,7 +53,7 @@ public boolean check() { if (conf.isVerbose() && response.getBody() != null) { System.out.println("Response body:\n" + response.getBody()); } - return evaluateResponseCode(response.getStatusCode(), urlString); + return evaluateResponseCode(response.getStatusCode(), response.getBody(), urlString); } catch (SSLHandshakeException e) { System.err.println("FAILURE: SSL handshake failed when connecting to " + urlString + "."); System.err.println("Verify that the truststore contains the z/OSMF server certificate."); @@ -77,8 +80,11 @@ public boolean check() { } } - private boolean evaluateResponseCode(int responseCode, String urlString) { + boolean evaluateResponseCode(int responseCode, String body, String urlString) { if (responseCode >= 200 && responseCode < 300) { + if (!validateJwkBody(body)) { + return false; + } System.out.println("SUCCESS: z/OSMF JWK endpoint is reachable and responding. HTTP " + responseCode); return true; } @@ -110,4 +116,31 @@ private boolean evaluateResponseCode(int responseCode, String urlString) { System.err.println("URL: " + urlString); return false; } + + boolean validateJwkBody(String body) { + if (body == null || body.isEmpty()) { + System.err.println("WARNING: z/OSMF JWK endpoint returned an empty response body."); + System.err.println("Response body: " + (body == null ? "" : "")); + return false; + } + + Matcher matcher = N_VALUE_PATTERN.matcher(body); + if (!matcher.find()) { + System.err.println("WARNING: JWK response does not contain an RSA modulus (\"n\" key)."); + System.err.println("The z/OSMF JWK endpoint may not be properly configured."); + System.err.println("Response body: " + body); + return false; + } + + String nValue = matcher.group(1); + if (nValue == null || nValue.trim().isEmpty()) { + System.err.println("FAILURE: JWK response contains an empty RSA modulus (\"n\" key is empty)."); + System.err.println("The z/OSMF server returned a key that cannot be used for JWT verification."); + System.err.println("Check z/OSMF JWT configuration and ensure the signing key is properly generated."); + System.err.println("Response body: " + body); + return false; + } + + return true; + } } diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java index 8e3d813388..7e30a847d8 100644 --- a/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/JwkEndpointCheckerTest.java @@ -62,7 +62,7 @@ class SuccessResponses { @Test void response200IsSuccess() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "")); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"validModulusValue\"}]}")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertTrue(checker.check()); assertTrue(outStream.toString().contains("SUCCESS")); @@ -77,6 +77,14 @@ void response401IsSuccess() throws IOException { assertTrue(outStream.toString().contains("SUCCESS")); assertTrue(outStream.toString().contains("401")); } + + @Test + void response200WithEmptyModulusIsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"\"}]}")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("empty RSA modulus")); + } } @Nested From 074a84f763fff9d49ac94cd92988ea5fc85e80ab Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 2 Apr 2026 16:41:28 +0530 Subject: [PATCH 08/10] updating --truststore to --truststore-file Signed-off-by: hrishikesh-nalawade --- zosmf-jwt-check/README.md | 16 ++++++++-------- .../main/java/org/zowe/apiml/ZosmfJwtCheck.java | 2 +- .../java/org/zowe/apiml/ZosmfJwtCheckConf.java | 4 ++-- .../java/org/zowe/apiml/ZosmfJwtCheckTest.java | 12 ++++++------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/zosmf-jwt-check/README.md b/zosmf-jwt-check/README.md index be134af7ed..7536e4aa71 100644 --- a/zosmf-jwt-check/README.md +++ b/zosmf-jwt-check/README.md @@ -70,7 +70,7 @@ java -jar zosmf-jwt-check-.jar \ java -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ - --truststore /path/to/truststore.p12 \ + --truststore-file /path/to/truststore.p12 \ --truststore-password changeit ``` @@ -98,7 +98,7 @@ These flags are required when `--scheme=https` (the default) and `--verify-certi | Flag | Description | Error when missing | |------|-------------|-------------------| -| `--truststore` | Path to the truststore file containing the z/OSMF CA certificate | `ERROR: --truststore is required when --scheme=https and verification is not DISABLED.` | +| `--truststore-file` | Path to the truststore file containing the z/OSMF CA certificate | `ERROR: --truststore-file is required when --scheme=https and verification is not DISABLED.` | | `--truststore-password` | Password for the truststore. If specified without a value, you will be prompted interactively. | `ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED.` | ### Optional Flags @@ -108,7 +108,7 @@ These flags are required when `--scheme=https` (the default) and `--verify-certi | `--scheme` | `https` | Protocol to use: `http` or `https` | | `--verify-certificates` | `STRICT` | Certificate verification mode: `STRICT`, `NONSTRICT`, or `DISABLED` | | `--truststore-type` | `PKCS12` | Format of the truststore file (e.g., `PKCS12`, `JKS`, `JCERACFKS`) | -| `--keystore` | *(none)* | Path to keystore file (only needed for mutual TLS / client certificate authentication) | +| `--keystore-file` | *(none)* | Path to keystore file (only needed for mutual TLS / client certificate authentication) | | `--keystore-password` | *(none)* | Password for the keystore. If specified without a value, you will be prompted interactively. | | `--keystore-type` | `PKCS12` | Format of the keystore file | | `-h`, `--help` | | Display usage help and exit | @@ -214,7 +214,7 @@ Requires a truststore containing the z/OSMF server's CA certificate (see [Creati java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ - --truststore /path/to/zosmf-truststore.p12 \ + --truststore-file /path/to/zosmf-truststore.p12 \ --truststore-password password ``` @@ -241,7 +241,7 @@ Useful when connecting via IP address but the certificate has a DNS name: java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ --zosmf-host 10.0.0.50 \ --zosmf-port 11443 \ - --truststore /path/to/zosmf-truststore.p12 \ + --truststore-file /path/to/zosmf-truststore.p12 \ --truststore-password password \ --verify-certificates NONSTRICT ``` @@ -276,10 +276,10 @@ java -jar zosmf-jwt-check-.jar # Missing truststore in STRICT mode java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 -# Output: ERROR: --truststore is required when --scheme=https and verification is not DISABLED. +# Output: ERROR: --truststore-file is required when --scheme=https and verification is not DISABLED. # Missing truststore password -java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 --truststore my.p12 +java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 --truststore-file my.p12 # Output: ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED. ``` @@ -311,7 +311,7 @@ java -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ - --truststore safkeyring://IZUSVR/ZoweKeyring \ + --truststore-file safkeyring://IZUSVR/ZoweKeyring \ --truststore-password password \ --truststore-type JCERACFKS ``` diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java index dbadbd0ca7..bb13cc57bc 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java @@ -89,7 +89,7 @@ static void validateConfig(ZosmfJwtCheckConf conf) { if ("https".equalsIgnoreCase(scheme) && !VERIFY_DISABLED.equals(verifyMode)) { if (conf.getTrustStore() == null) { - throw new IllegalArgumentException("--truststore is required when --scheme=https and verification is not DISABLED. " + + throw new IllegalArgumentException("--truststore-file is required when --scheme=https and verification is not DISABLED. " + "Provide the path to the truststore containing the z/OSMF server certificate."); } if (conf.getTrustStorePassword() == null) { diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java index 8296ce0572..9bb9096ecc 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheckConf.java @@ -38,7 +38,7 @@ public class ZosmfJwtCheckConf implements ZosmfJwtCheckConfig { @Option(names = {"--scheme"}, description = "http or https (default: ${DEFAULT-VALUE})") private String scheme = "https"; - @Option(names = {"--keystore"}, description = "Path to the keystore file (for HTTPS mutual TLS)") + @Option(names = {"--keystore-file"}, description = "Path to the keystore file (for HTTPS mutual TLS)") private String keyStore; @Option(names = {"--keystore-password"}, arity = "0..1", interactive = true, description = "Password for the keystore") @@ -47,7 +47,7 @@ public class ZosmfJwtCheckConf implements ZosmfJwtCheckConfig { @Option(names = {"--keystore-type"}, description = "Type of keystore (default: ${DEFAULT-VALUE})") private String keyStoreType = "PKCS12"; - @Option(names = {"--truststore"}, description = "Path to the truststore file (for HTTPS)") + @Option(names = {"--truststore-file"}, description = "Path to the truststore file (for HTTPS)") private String trustStore; @Option(names = {"--truststore-password"}, arity = "0..1", interactive = true, description = "Password for the truststore") diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java index c4a96eb1b9..9da86cbcea 100644 --- a/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfJwtCheckTest.java @@ -65,7 +65,7 @@ void invalidSchemeIsRejected() { @Test void invalidVerifyCertificatesIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", - "--truststore", "some/path.p12", "--truststore-password", "pass", + "--truststore-file", "some/path.p12", "--truststore-password", "pass", "--verify-certificates", "INVALID"}; assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--verify-certificates must be STRICT, NONSTRICT, or DISABLED")); @@ -75,7 +75,7 @@ void invalidVerifyCertificatesIsRejected() { void httpsStrictWithoutTruststoreIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https"}; assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); - assertTrue(errStream.toString().contains("--truststore is required")); + assertTrue(errStream.toString().contains("--truststore-file is required")); } @Test @@ -83,13 +83,13 @@ void httpsNonstrictWithoutTruststoreIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", "--verify-certificates", "NONSTRICT"}; assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); - assertTrue(errStream.toString().contains("--truststore is required")); + assertTrue(errStream.toString().contains("--truststore-file is required")); } @Test void httpsWithoutTruststorePasswordIsRejected() { String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "443", "--scheme", "https", - "--truststore", "some/path.p12"}; + "--truststore-file", "some/path.p12"}; assertEquals(4, ZosmfJwtCheck.mainWithExitCode(args)); assertTrue(errStream.toString().contains("--truststore-password is required")); } @@ -102,7 +102,7 @@ void httpsDisabledDoesNotRequireTruststore() { "--verify-certificates", "DISABLED"}; int exitCode = ZosmfJwtCheck.mainWithExitCode(args); assertEquals(4, exitCode); - assertFalse(errStream.toString().contains("--truststore is required")); + assertFalse(errStream.toString().contains("--truststore-file is required")); } @Test @@ -112,7 +112,7 @@ void httpDoesNotRequireTruststore() { int exitCode = ZosmfJwtCheck.mainWithExitCode(args); // Should be 4 (connection failure) not a validation error assertEquals(4, exitCode); - assertFalse(errStream.toString().contains("--truststore is required")); + assertFalse(errStream.toString().contains("--truststore-file is required")); } } } From 4bb3b8e91af76bf14d11532931850ad50cba43a4 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 23 Apr 2026 20:02:51 +0530 Subject: [PATCH 09/10] safkeyring truststore bug fix Signed-off-by: hrishikesh-nalawade --- .../src/main/java/org/zowe/apiml/Stores.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java index ef7b527e67..9a93584375 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java @@ -93,8 +93,14 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } return; } - try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { - this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + if (isKeyring(conf.getTrustStore())) { + try (InputStream trustStoreIStream = keyRingUrl(conf.getTrustStore()).openStream()) { + this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + } + } else { + try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { + this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + } } } From e2d0058a2890b35ff983176e1cbf528ab1e4de0e Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Sat, 25 Apr 2026 00:59:55 +0530 Subject: [PATCH 10/10] safkeyring truststore bug fix Signed-off-by: hrishikesh-nalawade --- .../main/java/org/zowe/apiml/ZosmfJwtCheck.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java index bb13cc57bc..e95093a748 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfJwtCheck.java @@ -29,6 +29,7 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { + ensureSafkeyringHandler(); try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); @@ -98,6 +99,20 @@ static void validateConfig(ZosmfJwtCheckConf conf) { } } + /** + * Registers the IBM SAF keyring URL protocol handler so that + * {@code new URL("safkeyring://...")} works on z/OS without requiring the + * caller to pass {@code -Djava.protocol.handler.pkgs=com.ibm.crypto.provider}. + * On non-z/OS platforms the handler class is simply not found and is ignored. + */ + static void ensureSafkeyringHandler() { + String existing = System.getProperty("java.protocol.handler.pkgs", ""); + if (!existing.contains("com.ibm.crypto.provider")) { + System.setProperty("java.protocol.handler.pkgs", + existing.isEmpty() ? "com.ibm.crypto.provider" : existing + "|com.ibm.crypto.provider"); + } + } + public static void main(String[] args) { System.exit(mainWithExitCode(args)); }