From 3318922008ea03242a830d591c95c273dc628ada Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 26 Mar 2026 16:31:49 +0530 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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)); } From 2088e48998e9bd0d0f8aae917382dc7d05957c6b Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 13 May 2026 17:08:58 +0530 Subject: [PATCH 11/28] Debug Logs Signed-off-by: hrishikesh-nalawade --- .../org/zowe/apiml/HttpClientWrapper.java | 14 +++++ .../org/zowe/apiml/JwkEndpointChecker.java | 19 +++++++ .../org/zowe/apiml/SSLContextFactory.java | 6 +++ .../src/main/java/org/zowe/apiml/Stores.java | 52 +++++++++++++++++-- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 30 +++++++++++ 5 files changed, 118 insertions(+), 3 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 8faca04157..8feecfa6e3 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 @@ -28,6 +28,7 @@ * Supports custom {@link SSLContext} and {@link HostnameVerifier} for * STRICT, NONSTRICT, and DISABLED certificate verification modes. */ +// TODO: REMOVE all "DEBUG [HttpClientWrapper]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class HttpClientWrapper { @@ -64,6 +65,11 @@ public HttpClientWrapper() { } public Response executeCall(URL url, Map headers) throws IOException { + System.out.println("DEBUG [HttpClientWrapper] executeCall() URL=" + url); + System.out.println("DEBUG [HttpClientWrapper] useHttps=" + useHttps); + System.out.println("DEBUG [HttpClientWrapper] sslContext=" + (sslContext != null ? sslContext.getProtocol() : "")); + System.out.println("DEBUG [HttpClientWrapper] hostnameVerifier=" + (hostnameVerifier != null ? hostnameVerifier.getClass().getName() : "")); + HttpURLConnection con; if (useHttps) { HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); @@ -72,8 +78,10 @@ public Response executeCall(URL url, Map headers) throws IOExcep httpsCon.setHostnameVerifier(hostnameVerifier); } con = httpsCon; + System.out.println("DEBUG [HttpClientWrapper] HTTPS connection opened"); } else { con = (HttpURLConnection) url.openConnection(); + System.out.println("DEBUG [HttpClientWrapper] HTTP connection opened"); } con.setRequestMethod("GET"); @@ -86,10 +94,16 @@ public Response executeCall(URL url, Map headers) throws IOExcep } } + System.out.println("DEBUG [HttpClientWrapper] Sending GET request (connectTimeout=" + CONNECT_TIMEOUT + "ms, readTimeout=" + READ_TIMEOUT + "ms)..."); try { int responseCode = con.getResponseCode(); + System.out.println("DEBUG [HttpClientWrapper] Response code=" + responseCode); String body = readBody(con); + System.out.println("DEBUG [HttpClientWrapper] Response body read, length=" + (body != null ? body.length() : "")); return new Response(responseCode, body); + } catch (IOException e) { + System.err.println("DEBUG [HttpClientWrapper] IOException during request: " + e.getClass().getName() + ": " + e.getMessage()); + throw e; } finally { con.disconnect(); } 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 06f5b0681a..1eedc7e8d3 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 @@ -24,6 +24,7 @@ * 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 */ +// TODO: REMOVE all "DEBUG [JwkEndpointChecker]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class JwkEndpointChecker { @@ -45,11 +46,19 @@ public boolean check() { Map headers = new HashMap<>(); headers.put(ZOSMF_CSRF_HEADER, ""); + System.out.println("DEBUG [JwkEndpointChecker] check() called"); + System.out.println("DEBUG [JwkEndpointChecker] target URL=" + urlString); + System.out.println("DEBUG [JwkEndpointChecker] headers=" + headers); + try { URL url = new URL(urlString); System.out.println("Checking z/OSMF JWK endpoint: " + urlString); + System.out.println("DEBUG [JwkEndpointChecker] Calling httpClient.executeCall()..."); HttpClientWrapper.Response response = httpClient.executeCall(url, headers); + + System.out.println("DEBUG [JwkEndpointChecker] Response received: HTTP " + response.getStatusCode()); + System.out.println("DEBUG [JwkEndpointChecker] Response body length=" + (response.getBody() != null ? response.getBody().length() : "")); if (conf.isVerbose() && response.getBody() != null) { System.out.println("Response body:\n" + response.getBody()); } @@ -58,24 +67,34 @@ public boolean check() { 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()); + System.err.println("DEBUG [JwkEndpointChecker] SSLHandshakeException stack trace:"); + e.printStackTrace(System.err); 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()); + System.err.println("DEBUG [JwkEndpointChecker] ConnectException stack trace:"); + e.printStackTrace(System.err); return false; } catch (SocketTimeoutException e) { System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); 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."); + System.err.println("DEBUG [JwkEndpointChecker] SocketTimeoutException stack trace:"); + e.printStackTrace(System.err); 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."); + System.err.println("DEBUG [JwkEndpointChecker] UnknownHostException stack trace:"); + e.printStackTrace(System.err); return false; } catch (Exception e) { System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); System.err.println("Details: " + e.getMessage()); + System.err.println("DEBUG [JwkEndpointChecker] Exception class=" + e.getClass().getName()); + e.printStackTrace(System.err); return false; } } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index 8e4f8cedaf..c3e1b034a0 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -27,6 +27,7 @@ *
  • {@link #initTrustAllSSLContext()} — trust-all mode for DISABLED verification
  • * */ +// TODO: REMOVE all "DEBUG [SSLContextFactory]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class SSLContextFactory { @@ -48,15 +49,19 @@ public SSLContext getSslContext() { * @return factory holding the initialized SSLContext */ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { + System.out.println("DEBUG [SSLContextFactory] initSSLContext() called"); SSLContextFactory factory = new SSLContextFactory(stores); + System.out.println("DEBUG [SSLContextFactory] Initializing TrustManagerFactory with trustStore (type=" + stores.getTrustStore().getType() + ", size=" + stores.getTrustStore().size() + ")"); TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustFactory.init(stores.getTrustStore()); KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); if (stores.getKeyStore() != null) { + System.out.println("DEBUG [SSLContextFactory] Initializing KeyManagerFactory with keyStore (type=" + stores.getKeyStore().getType() + ", size=" + stores.getKeyStore().size() + ")"); keyFactory.init(stores.getKeyStore(), stores.getConf().getKeyStorePassword().toCharArray()); } else { + System.out.println("DEBUG [SSLContextFactory] No keyStore, using empty keystore for KeyManagerFactory"); KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); emptyKeystore.load(null, null); keyFactory.init(emptyKeystore, null); @@ -64,6 +69,7 @@ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgor factory.sslContext = SSLContext.getInstance("TLSv1.2"); factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); + System.out.println("DEBUG [SSLContextFactory] SSLContext created successfully (protocol=" + factory.sslContext.getProtocol() + ")"); return factory; } 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 9a93584375..4ff78fefcc 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 @@ -27,6 +27,7 @@ * Loads Java {@link java.security.KeyStore} instances from the filesystem * or z/OS SAF keyrings. Supports PKCS12, JKS, and {@code safkeyring://} URIs. */ +// TODO: REMOVE all "DEBUG [Stores]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class Stores { @@ -69,22 +70,38 @@ public static String formatKeyringUrl(String input) { } void init() { + System.out.println("DEBUG [Stores] init() called"); + System.out.println("DEBUG [Stores] trustStore path=" + conf.getTrustStore()); + System.out.println("DEBUG [Stores] trustStore type=" + conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] trustStore isKeyring=" + isKeyring(conf.getTrustStore())); + System.out.println("DEBUG [Stores] keyStore path=" + conf.getKeyStore()); + System.out.println("DEBUG [Stores] keyStore type=" + (conf.getKeyStore() != null ? conf.getKeyStoreType() : "")); + System.out.println("DEBUG [Stores] keyStore isKeyring=" + isKeyring(conf.getKeyStore())); + System.out.println("DEBUG [Stores] java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); try { + System.out.println("DEBUG [Stores] Calling initKeystore()..."); initKeystore(); + System.out.println("DEBUG [Stores] initKeystore() completed. trustStore set by keystore=" + (trustStore != null)); if (trustStore == null) { + System.out.println("DEBUG [Stores] Calling initTruststore()..."); initTruststore(); + System.out.println("DEBUG [Stores] initTruststore() completed successfully."); } } catch (FileNotFoundException e) { + System.err.println("DEBUG [Stores] FileNotFoundException in init(): " + e.getMessage()); + e.printStackTrace(System.err); 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) { + System.err.println("DEBUG [Stores] Exception in init(): " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); 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."); + System.out.println("DEBUG [Stores] initTruststore: No truststore specified, will use empty."); try { this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); this.trustStore.load(null, null); @@ -94,30 +111,48 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl return; } if (isKeyring(conf.getTrustStore())) { - try (InputStream trustStoreIStream = keyRingUrl(conf.getTrustStore()).openStream()) { + System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); + String formatted = formatKeyringUrl(conf.getTrustStore()); + System.out.println("DEBUG [Stores] initTruststore: Formatted keyring URL: " + formatted); + System.out.println("DEBUG [Stores] initTruststore: Calling keyRingUrl()..."); + URL url = keyRingUrl(conf.getTrustStore()); + System.out.println("DEBUG [Stores] initTruststore: URL object created: " + url + " (protocol=" + url.getProtocol() + ")"); + System.out.println("DEBUG [Stores] initTruststore: Calling url.openStream()..."); + try (InputStream trustStoreIStream = url.openStream()) { + System.out.println("DEBUG [Stores] initTruststore: openStream() succeeded. Loading keystore type=" + conf.getTrustStoreType()); this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } } else { + System.out.println("DEBUG [Stores] initTruststore: Loading from file: " + conf.getTrustStore()); try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from file. Aliases count=" + trustStore.size()); } } } private void initKeystore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if (conf.getKeyStore() == null) { + System.out.println("DEBUG [Stores] initKeystore: No keystore specified, skipping."); return; } if (isKeyring(conf.getKeyStore())) { + System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; + System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); } catch (Exception e) { + System.err.println("DEBUG [Stores] initKeystore: Exception loading keyring: " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); throw new StoresNotInitializeException(e.getMessage()); } } else { + System.out.println("DEBUG [Stores] initKeystore: Loading from file: " + conf.getKeyStore()); try (InputStream keyStoreIStream = new FileInputStream(conf.getKeyStore())) { this.keyStore = readKeyStore(keyStoreIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from file. Aliases count=" + keyStore.size()); } } } @@ -145,6 +180,17 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { throw new StoresNotInitializeException("Incorrect key ring format: " + uri + ". Make sure you use format safkeyring://userId/keyRing"); } - return new URL(formatKeyringUrl(uri)); + String formatted = formatKeyringUrl(uri); + System.out.println("DEBUG [Stores] keyRingUrl: Creating URL from: " + formatted); + try { + URL url = new URL(formatted); + System.out.println("DEBUG [Stores] keyRingUrl: URL created successfully. protocol=" + url.getProtocol() + + " host=" + url.getHost() + " path=" + url.getPath()); + return url; + } catch (MalformedURLException e) { + System.err.println("DEBUG [Stores] keyRingUrl: MalformedURLException for '" + formatted + "': " + e.getMessage()); + System.err.println("DEBUG [Stores] keyRingUrl: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); + throw e; + } } } 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 e95093a748..1144755085 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 @@ -21,6 +21,7 @@ * *

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

    */ +// TODO: REMOVE all "DEBUG [ZosmfJwtCheck]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class ZosmfJwtCheck { @@ -29,7 +30,18 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { + System.out.println("DEBUG [ZosmfJwtCheck] ===== zosmf-jwt-check starting ====="); + System.out.println("DEBUG [ZosmfJwtCheck] Java version: " + System.getProperty("java.version")); + System.out.println("DEBUG [ZosmfJwtCheck] Java vendor: " + System.getProperty("java.vendor")); + System.out.println("DEBUG [ZosmfJwtCheck] OS name: " + System.getProperty("os.name")); + System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (BEFORE ensureSafkeyringHandler): " + + System.getProperty("java.protocol.handler.pkgs", "")); + ensureSafkeyringHandler(); + + System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (AFTER ensureSafkeyringHandler): " + + System.getProperty("java.protocol.handler.pkgs", "")); + try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); @@ -41,6 +53,17 @@ public static int mainWithExitCode(String[] args) { return 8; } + System.out.println("DEBUG [ZosmfJwtCheck] Parsed config:"); + System.out.println("DEBUG [ZosmfJwtCheck] zosmf-host=" + conf.getZosmfHost()); + System.out.println("DEBUG [ZosmfJwtCheck] zosmf-port=" + conf.getZosmfPort()); + System.out.println("DEBUG [ZosmfJwtCheck] scheme=" + conf.getScheme()); + System.out.println("DEBUG [ZosmfJwtCheck] verify-certificates=" + conf.getVerifyCertificates()); + System.out.println("DEBUG [ZosmfJwtCheck] truststore-file=" + conf.getTrustStore()); + System.out.println("DEBUG [ZosmfJwtCheck] truststore-type=" + conf.getTrustStoreType()); + System.out.println("DEBUG [ZosmfJwtCheck] truststore-isKeyring=" + Stores.isKeyring(conf.getTrustStore())); + System.out.println("DEBUG [ZosmfJwtCheck] keystore-file=" + conf.getKeyStore()); + System.out.println("DEBUG [ZosmfJwtCheck] keystore-type=" + conf.getKeyStoreType()); + validateConfig(conf); HttpClientWrapper httpClient; @@ -52,8 +75,11 @@ public static int mainWithExitCode(String[] args) { HostnameVerifier noopVerifier = (hostname, session) -> true; httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); } else { + System.out.println("DEBUG [ZosmfJwtCheck] Creating Stores (truststore/keystore)..."); Stores stores = new Stores(conf); + System.out.println("DEBUG [ZosmfJwtCheck] Stores created successfully. Building SSLContext..."); SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); + System.out.println("DEBUG [ZosmfJwtCheck] SSLContext created successfully."); HostnameVerifier hostnameVerifier; if (VERIFY_NONSTRICT.equals(verifyMode)) { @@ -69,10 +95,14 @@ public static int mainWithExitCode(String[] args) { } JwkEndpointChecker checker = new JwkEndpointChecker(httpClient, conf); + System.out.println("DEBUG [ZosmfJwtCheck] Calling JwkEndpointChecker.check()..."); boolean success = checker.check(); + System.out.println("DEBUG [ZosmfJwtCheck] JwkEndpointChecker.check() returned: " + success); return success ? 0 : 4; } catch (Exception e) { System.err.println("ERROR: " + e.getMessage()); + System.err.println("DEBUG [ZosmfJwtCheck] Exception class: " + e.getClass().getName()); + e.printStackTrace(System.err); return 4; } } From fc8c14f65b704344d700b7ca5991b0f3a0389ca4 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 14 May 2026 00:41:11 +0530 Subject: [PATCH 12/28] further fix Signed-off-by: hrishikesh-nalawade --- .../src/main/java/org/zowe/apiml/Stores.java | 85 +++++++++++++++++-- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 59 ++++++++++++- 2 files changed, 133 insertions(+), 11 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 4ff78fefcc..8dad9b614d 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 @@ -14,6 +14,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Constructor; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyStore; @@ -112,14 +113,9 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } if (isKeyring(conf.getTrustStore())) { System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); - String formatted = formatKeyringUrl(conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: Formatted keyring URL: " + formatted); - System.out.println("DEBUG [Stores] initTruststore: Calling keyRingUrl()..."); - URL url = keyRingUrl(conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: URL object created: " + url + " (protocol=" + url.getProtocol() + ")"); - System.out.println("DEBUG [Stores] initTruststore: Calling url.openStream()..."); - try (InputStream trustStoreIStream = url.openStream()) { - System.out.println("DEBUG [Stores] initTruststore: openStream() succeeded. Loading keystore type=" + conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Opening keyring stream (URL with RACFInputStream fallback)..."); + try (InputStream trustStoreIStream = openKeyringStream(conf.getTrustStore(), conf.getTrustStorePassword().toCharArray())) { + System.out.println("DEBUG [Stores] initTruststore: Stream opened. Loading keystore type=" + conf.getTrustStoreType()); this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } @@ -139,7 +135,7 @@ private void initKeystore() throws IOException, CertificateException, NoSuchAlgo } if (isKeyring(conf.getKeyStore())) { System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); - try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { + try (InputStream keyringIStream = openKeyringStream(conf.getKeyStore(), conf.getKeyStorePassword().toCharArray())) { this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); @@ -193,4 +189,75 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { throw e; } } + + /** + * Opens an InputStream to a SAF keyring. Tries the standard URL-based approach first, + * then falls back to IBM's RACFInputStream (loaded via reflection) if the URL protocol + * handler is not available. + */ + // TODO: REMOVE debug logging after SAF keyring issue is resolved + @SuppressWarnings("squid:S3011") + static InputStream openKeyringStream(String uri, char[] password) throws IOException { + String formatted = formatKeyringUrl(uri); + System.out.println("DEBUG [Stores] openKeyringStream: uri='" + uri + "' formatted='" + formatted + "'"); + System.out.println("DEBUG [Stores] openKeyringStream: java.protocol.handler.pkgs=" + + System.getProperty("java.protocol.handler.pkgs", "")); + + // Try standard URL-based approach first (same as main API ML services) + try { + System.out.println("DEBUG [Stores] openKeyringStream: Attempting new URL('" + formatted + "')..."); + URL url = new URL(formatted); + System.out.println("DEBUG [Stores] openKeyringStream: URL created. protocol=" + url.getProtocol() + + " host=" + url.getHost() + " path=" + url.getPath() + + " class=" + url.getClass().getName()); + System.out.println("DEBUG [Stores] openKeyringStream: Calling url.openStream()..."); + InputStream is = url.openStream(); + System.out.println("DEBUG [Stores] openKeyringStream: URL.openStream() SUCCEEDED (stream class=" + is.getClass().getName() + ")"); + return is; + } catch (MalformedURLException urlEx) { + System.err.println("DEBUG [Stores] openKeyringStream: URL approach FAILED with MalformedURLException: " + urlEx.getMessage()); + } catch (IOException ioEx) { + System.err.println("DEBUG [Stores] openKeyringStream: URL.openStream() FAILED with IOException: " + + ioEx.getClass().getName() + ": " + ioEx.getMessage()); + ioEx.printStackTrace(System.err); + // Re-throw IO errors from openStream — the URL was valid but the stream failed + throw ioEx; + } + + // Fallback: use com.ibm.crypto.zsecurity.provider.RACFInputStream via reflection + // This class is available on z/OS IBM JDKs and bypasses the URL protocol handler + System.out.println("DEBUG [Stores] openKeyringStream: URL handler not available, attempting RACFInputStream fallback..."); + + Matcher matcher = KEYRING_PATTERN.matcher(uri); + if (!matcher.matches()) { + matcher = KEYRING_PATTERN.matcher(formatted); + if (!matcher.matches()) { + throw new IOException("Cannot open keyring: invalid URI format: " + uri); + } + } + + String userId = matcher.group(2); + String ringName = matcher.group(3); + System.out.println("DEBUG [Stores] openKeyringStream: Parsed userId='" + userId + "' ringName='" + ringName + "'"); + + try { + System.out.println("DEBUG [Stores] openKeyringStream: Loading class com.ibm.crypto.zsecurity.provider.RACFInputStream..."); + Class racfClass = Class.forName("com.ibm.crypto.zsecurity.provider.RACFInputStream"); + System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream class loaded (classLoader=" + racfClass.getClassLoader() + ")"); + Constructor ctor = racfClass.getConstructor(String.class, String.class, char[].class); + System.out.println("DEBUG [Stores] openKeyringStream: Invoking RACFInputStream('" + userId + "', '" + ringName + "', )..."); + InputStream is = (InputStream) ctor.newInstance(userId, ringName, password); + System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream SUCCEEDED (stream class=" + is.getClass().getName() + ")"); + return is; + } catch (ClassNotFoundException cnfe) { + System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream class NOT FOUND: " + cnfe.getMessage()); + throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + + "and RACFInputStream class not found. Ensure running on z/OS with IBM JDK.", cnfe); + } catch (Exception e) { + System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream FAILED: " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); + throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + + "and RACFInputStream fallback failed: " + e.getMessage(), e); + } + } } 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 1144755085..45a92ed472 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 @@ -33,7 +33,13 @@ public static int mainWithExitCode(String[] args) { System.out.println("DEBUG [ZosmfJwtCheck] ===== zosmf-jwt-check starting ====="); System.out.println("DEBUG [ZosmfJwtCheck] Java version: " + System.getProperty("java.version")); System.out.println("DEBUG [ZosmfJwtCheck] Java vendor: " + System.getProperty("java.vendor")); + System.out.println("DEBUG [ZosmfJwtCheck] Java home: " + System.getProperty("java.home")); System.out.println("DEBUG [ZosmfJwtCheck] OS name: " + System.getProperty("os.name")); + System.out.println("DEBUG [ZosmfJwtCheck] java.class.path: " + System.getProperty("java.class.path", "")); + System.out.println("DEBUG [ZosmfJwtCheck] java.ext.dirs: " + System.getProperty("java.ext.dirs", "")); + System.out.println("DEBUG [ZosmfJwtCheck] Classloader chain: app=" + ZosmfJwtCheck.class.getClassLoader() + + " parent=" + (ZosmfJwtCheck.class.getClassLoader() != null ? ZosmfJwtCheck.class.getClassLoader().getParent() : "null") + + " threadCtx=" + Thread.currentThread().getContextClassLoader()); System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (BEFORE ensureSafkeyringHandler): " + System.getProperty("java.protocol.handler.pkgs", "")); @@ -137,9 +143,58 @@ static void validateConfig(ZosmfJwtCheckConf conf) { */ static void ensureSafkeyringHandler() { String existing = System.getProperty("java.protocol.handler.pkgs", ""); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: current value='" + existing + "'"); if (!existing.contains("com.ibm.crypto.provider")) { - System.setProperty("java.protocol.handler.pkgs", - existing.isEmpty() ? "com.ibm.crypto.provider" : existing + "|com.ibm.crypto.provider"); + String newValue = existing.isEmpty() ? "com.ibm.crypto.provider" : existing + "|com.ibm.crypto.provider"; + System.setProperty("java.protocol.handler.pkgs", newValue); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: SET to '" + newValue + "'"); + } else { + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: already contains com.ibm.crypto.provider, no change"); + } + + // TODO: REMOVE diagnostic checks after SAF keyring issue is resolved + // Check which IBM z/OS keyring-related classes are available on the classpath + String[] classesToCheck = { + "com.ibm.crypto.provider.safkeyring.Handler", + "com.ibm.crypto.provider.safkeyringjce.Handler", + "com.ibm.crypto.provider.safkeyringjcecca.Handler", + "com.ibm.crypto.provider.safkeyringjceccaracfks.Handler", + "com.ibm.crypto.provider.safkeyringjcehybrid.Handler", + "com.ibm.crypto.zsecurity.provider.RACFInputStream", + "com.ibm.jsse2.IBMJSSEProvider2", + }; + for (String className : classesToCheck) { + try { + Class cls = Class.forName(className); + System.out.println("DEBUG [ZosmfJwtCheck] Class FOUND: " + className + + " (classLoader=" + cls.getClassLoader() + ")"); + } catch (ClassNotFoundException e) { + System.out.println("DEBUG [ZosmfJwtCheck] Class NOT FOUND: " + className); + } + } + + // List installed security providers + System.out.println("DEBUG [ZosmfJwtCheck] Installed security providers:"); + for (java.security.Provider p : java.security.Security.getProviders()) { + System.out.println("DEBUG [ZosmfJwtCheck] " + p.getName() + " v" + p.getVersionStr() + + " (" + p.getClass().getName() + ")"); + } + + // Test URL creation inline to capture exact failure point + System.out.println("DEBUG [ZosmfJwtCheck] Testing URL creation for 'safkeyring://test/test'..."); + try { + java.net.URL testUrl = new java.net.URL("safkeyring://test/test"); + System.out.println("DEBUG [ZosmfJwtCheck] URL creation SUCCEEDED: " + testUrl); + } catch (java.net.MalformedURLException e) { + System.out.println("DEBUG [ZosmfJwtCheck] URL creation FAILED: " + e.getMessage()); + // If the handler class was FOUND above but URL creation FAILED, that confirms + // the bootstrap classloader theory: Class.forName from our code can find it, + // but java.net.URL (loaded by bootstrap classloader) cannot. + System.out.println("DEBUG [ZosmfJwtCheck] URL class classLoader: " + java.net.URL.class.getClassLoader()); + System.out.println("DEBUG [ZosmfJwtCheck] This class classLoader: " + ZosmfJwtCheck.class.getClassLoader()); + System.out.println("DEBUG [ZosmfJwtCheck] If handler class was FOUND above but URL creation FAILED,"); + System.out.println("DEBUG [ZosmfJwtCheck] this indicates a classloader visibility issue."); + System.out.println("DEBUG [ZosmfJwtCheck] The RACFInputStream fallback in openKeyringStream() will be used instead."); } } From 2b566bd4b4a44daee58998cd7b970606b353bb1e Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 14 May 2026 02:23:09 +0530 Subject: [PATCH 13/28] further fix Signed-off-by: hrishikesh-nalawade --- .../src/main/java/org/zowe/apiml/Stores.java | 195 ++++++++++++------ 1 file changed, 137 insertions(+), 58 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 8dad9b614d..5dcc868a41 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 @@ -15,11 +15,14 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; import java.security.cert.CertificateException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -113,12 +116,9 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } if (isKeyring(conf.getTrustStore())) { System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: Opening keyring stream (URL with RACFInputStream fallback)..."); - try (InputStream trustStoreIStream = openKeyringStream(conf.getTrustStore(), conf.getTrustStorePassword().toCharArray())) { - System.out.println("DEBUG [Stores] initTruststore: Stream opened. Loading keystore type=" + conf.getTrustStoreType()); - this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); - } + System.out.println("DEBUG [Stores] initTruststore: Loading keyring truststore (multi-strategy)..."); + this.trustStore = loadKeyringKeyStore(conf.getTrustStore(), conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } else { System.out.println("DEBUG [Stores] initTruststore: Loading from file: " + conf.getTrustStore()); try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { @@ -135,8 +135,8 @@ private void initKeystore() throws IOException, CertificateException, NoSuchAlgo } if (isKeyring(conf.getKeyStore())) { System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); - try (InputStream keyringIStream = openKeyringStream(conf.getKeyStore(), conf.getKeyStorePassword().toCharArray())) { - this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + try { + this.keyStore = loadKeyringKeyStore(conf.getKeyStore(), conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); } catch (Exception e) { @@ -191,73 +191,152 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { } /** - * Opens an InputStream to a SAF keyring. Tries the standard URL-based approach first, - * then falls back to IBM's RACFInputStream (loaded via reflection) if the URL protocol - * handler is not available. + * Loads a KeyStore from a SAF keyring using multiple strategies in order: + *
      + *
    1. Standard URL approach: {@code new URL("safkeyring://...").openStream()}
    2. + *
    3. RACFInputStream via reflection (bypasses URL handler)
    4. + *
    5. Direct JCERACFKS load via IBMZSecurity provider (no InputStream needed)
    6. + *
    */ // TODO: REMOVE debug logging after SAF keyring issue is resolved @SuppressWarnings("squid:S3011") - static InputStream openKeyringStream(String uri, char[] password) throws IOException { + static KeyStore loadKeyringKeyStore(String uri, char[] password, String type) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { String formatted = formatKeyringUrl(uri); - System.out.println("DEBUG [Stores] openKeyringStream: uri='" + uri + "' formatted='" + formatted + "'"); - System.out.println("DEBUG [Stores] openKeyringStream: java.protocol.handler.pkgs=" + System.out.println("DEBUG [Stores] loadKeyringKeyStore: uri='" + uri + "' formatted='" + formatted + "' type=" + type); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); - // Try standard URL-based approach first (same as main API ML services) + Matcher matcher = KEYRING_PATTERN.matcher(uri); + if (!matcher.matches()) { + matcher = KEYRING_PATTERN.matcher(formatted); + } + String userId = matcher.matches() ? matcher.group(2) : null; + String ringName = matcher.matches() ? matcher.group(3) : null; + + // Strategy 1: Standard URL-based approach (same as main API ML services) try { - System.out.println("DEBUG [Stores] openKeyringStream: Attempting new URL('" + formatted + "')..."); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 - URL approach..."); URL url = new URL(formatted); - System.out.println("DEBUG [Stores] openKeyringStream: URL created. protocol=" + url.getProtocol() - + " host=" + url.getHost() + " path=" + url.getPath() - + " class=" + url.getClass().getName()); - System.out.println("DEBUG [Stores] openKeyringStream: Calling url.openStream()..."); - InputStream is = url.openStream(); - System.out.println("DEBUG [Stores] openKeyringStream: URL.openStream() SUCCEEDED (stream class=" + is.getClass().getName() + ")"); - return is; + System.out.println("DEBUG [Stores] loadKeyringKeyStore: URL created. protocol=" + url.getProtocol()); + try (InputStream is = url.openStream()) { + System.out.println("DEBUG [Stores] loadKeyringKeyStore: URL.openStream() SUCCEEDED (stream class=" + is.getClass().getName() + ")"); + KeyStore ks = KeyStore.getInstance(type); + ks.load(is, password); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 SUCCEEDED. Aliases count=" + ks.size()); + return ks; + } } catch (MalformedURLException urlEx) { - System.err.println("DEBUG [Stores] openKeyringStream: URL approach FAILED with MalformedURLException: " + urlEx.getMessage()); + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 FAILED (MalformedURLException): " + urlEx.getMessage()); } catch (IOException ioEx) { - System.err.println("DEBUG [Stores] openKeyringStream: URL.openStream() FAILED with IOException: " + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 FAILED (IOException on openStream): " + ioEx.getClass().getName() + ": " + ioEx.getMessage()); - ioEx.printStackTrace(System.err); - // Re-throw IO errors from openStream — the URL was valid but the stream failed - throw ioEx; } - // Fallback: use com.ibm.crypto.zsecurity.provider.RACFInputStream via reflection - // This class is available on z/OS IBM JDKs and bypasses the URL protocol handler - System.out.println("DEBUG [Stores] openKeyringStream: URL handler not available, attempting RACFInputStream fallback..."); - - Matcher matcher = KEYRING_PATTERN.matcher(uri); - if (!matcher.matches()) { - matcher = KEYRING_PATTERN.matcher(formatted); - if (!matcher.matches()) { - throw new IOException("Cannot open keyring: invalid URI format: " + uri); + // Strategy 2: RACFInputStream via reflection + if (userId != null && ringName != null) { + try { + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 - RACFInputStream('" + userId + "', '" + ringName + "')..."); + Class racfClass = Class.forName("com.ibm.crypto.zsecurity.provider.RACFInputStream"); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: RACFInputStream class loaded (classLoader=" + racfClass.getClassLoader() + ")"); + Constructor ctor = racfClass.getConstructor(String.class, String.class, char[].class); + InputStream is = (InputStream) ctor.newInstance(userId, ringName, password); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: RACFInputStream created. Loading KeyStore..."); + KeyStore ks = KeyStore.getInstance(type); + ks.load(is, password); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 SUCCEEDED. Aliases count=" + ks.size()); + return ks; + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + String causeMsg = cause != null ? cause.getMessage() : ite.getMessage(); + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 FAILED (RACFInputStream constructor threw): " + causeMsg); + if (cause != null) { + cause.printStackTrace(System.err); + } + // If the error is about private key access, try Strategy 3 + if (cause instanceof IOException && causeMsg != null && causeMsg.contains("private key")) { + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Private key permission error detected." + + " This likely means the running user does not have RACF authority to access" + + " private keys in keyring " + userId + "/" + ringName + "." + + " Trying Strategy 3 (direct JCERACFKS load)..."); + } else { + // Non-private-key error — still try Strategy 3 but log the original error + System.err.println("DEBUG [Stores] loadKeyringKeyStore: RACFInputStream failed with non-private-key error. Trying Strategy 3..."); + } + } catch (ClassNotFoundException cnfe) { + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 FAILED (class not found): " + cnfe.getMessage()); + } catch (Exception e) { + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 FAILED: " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); } } - String userId = matcher.group(2); - String ringName = matcher.group(3); - System.out.println("DEBUG [Stores] openKeyringStream: Parsed userId='" + userId + "' ringName='" + ringName + "'"); + // Strategy 3: Direct JCERACFKS KeyStore load using IBMZSecurity provider + // On IBM z/OS JDK, the JCERACFKS KeyStore SPI can load keyrings directly + // when the keyring URL is provided as the password/loadStoreParameter. + // This approach does NOT use RACFInputStream and may handle inaccessible + // private keys gracefully (loading only certificates the user can access). + Provider ibmZSecurityProvider = Security.getProvider("IBMZSecurity"); + if (ibmZSecurityProvider != null) { + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3 - Direct JCERACFKS via IBMZSecurity provider..."); + try { + KeyStore ks = KeyStore.getInstance(type, ibmZSecurityProvider); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: KeyStore.getInstance('" + type + "', IBMZSecurity) succeeded."); - try { - System.out.println("DEBUG [Stores] openKeyringStream: Loading class com.ibm.crypto.zsecurity.provider.RACFInputStream..."); - Class racfClass = Class.forName("com.ibm.crypto.zsecurity.provider.RACFInputStream"); - System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream class loaded (classLoader=" + racfClass.getClassLoader() + ")"); - Constructor ctor = racfClass.getConstructor(String.class, String.class, char[].class); - System.out.println("DEBUG [Stores] openKeyringStream: Invoking RACFInputStream('" + userId + "', '" + ringName + "', )..."); - InputStream is = (InputStream) ctor.newInstance(userId, ringName, password); - System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream SUCCEEDED (stream class=" + is.getClass().getName() + ")"); - return is; - } catch (ClassNotFoundException cnfe) { - System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream class NOT FOUND: " + cnfe.getMessage()); - throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + - "and RACFInputStream class not found. Ensure running on z/OS with IBM JDK.", cnfe); - } catch (Exception e) { - System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream FAILED: " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); - throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + - "and RACFInputStream fallback failed: " + e.getMessage(), e); + // Try loading with the keyring URL as the password + // Some IBM implementations accept safkeyring://userId/ring as load parameter + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3a - load(null, ringUrl as password)..."); + try { + ks.load(null, formatted.toCharArray()); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3a SUCCEEDED. Aliases count=" + ks.size()); + return ks; + } catch (Exception e3a) { + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3a FAILED: " + e3a.getClass().getName() + ": " + e3a.getMessage()); + } + + // Try loading with a DomainLoadStoreParameter-style approach + // Pass the ring name in format "userId/ringName" as password + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3b - load(null, userId/ringName as password)..."); + try { + ks = KeyStore.getInstance(type, ibmZSecurityProvider); + ks.load(null, (userId + "/" + ringName).toCharArray()); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3b SUCCEEDED. Aliases count=" + ks.size()); + return ks; + } catch (Exception e3b) { + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3b FAILED: " + e3b.getClass().getName() + ": " + e3b.getMessage()); + } + + // Try with standard password and null stream + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c - load(null, 'password')..."); + try { + ks = KeyStore.getInstance(type, ibmZSecurityProvider); + ks.load(null, password); + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c SUCCEEDED. Aliases count=" + ks.size()); + if (ks.size() > 0) { + return ks; + } + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c returned empty keystore, not useful."); + } catch (Exception e3c) { + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c FAILED: " + e3c.getClass().getName() + ": " + e3c.getMessage()); + } + } catch (Exception e3) { + System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3 setup FAILED: " + e3.getClass().getName() + ": " + e3.getMessage()); + e3.printStackTrace(System.err); + } + } else { + System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3 SKIPPED (IBMZSecurity provider not available)"); } + + // All strategies failed + String errorMsg = "Cannot load keyring '" + uri + "': all strategies exhausted.\n" + + " - URL handler: safkeyring protocol handler not found in this JDK.\n" + + " - RACFInputStream: likely failed due to RACF authority.\n" + + "Ensure the user running 'zwe validate' has READ access to the keyring.\n" + + "RACF commands to verify/grant access:\n" + + " RLIST FACILITY IRR.DIGTCERT.LISTRING ALL\n" + + " PERMIT IRR.DIGTCERT.LISTRING CLASS(FACILITY) ID() ACCESS(READ)\n" + + " SETROPTS RACLIST(FACILITY) REFRESH"; + System.err.println("DEBUG [Stores] loadKeyringKeyStore: ALL STRATEGIES FAILED."); + System.err.println(errorMsg); + throw new IOException(errorMsg); } } From 06dfd59cf442b2b056af2c2f6cc5aafefbb987f2 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 21 May 2026 13:43:31 +0530 Subject: [PATCH 14/28] Revert "further fix" This reverts commit 2b566bd4b4a44daee58998cd7b970606b353bb1e. --- .../src/main/java/org/zowe/apiml/Stores.java | 195 ++++++------------ 1 file changed, 58 insertions(+), 137 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 5dcc868a41..8dad9b614d 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 @@ -15,14 +15,11 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.Provider; -import java.security.Security; import java.security.cert.CertificateException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -116,9 +113,12 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } if (isKeyring(conf.getTrustStore())) { System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: Loading keyring truststore (multi-strategy)..."); - this.trustStore = loadKeyringKeyStore(conf.getTrustStore(), conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); + System.out.println("DEBUG [Stores] initTruststore: Opening keyring stream (URL with RACFInputStream fallback)..."); + try (InputStream trustStoreIStream = openKeyringStream(conf.getTrustStore(), conf.getTrustStorePassword().toCharArray())) { + System.out.println("DEBUG [Stores] initTruststore: Stream opened. Loading keystore type=" + conf.getTrustStoreType()); + this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); + } } else { System.out.println("DEBUG [Stores] initTruststore: Loading from file: " + conf.getTrustStore()); try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { @@ -135,8 +135,8 @@ private void initKeystore() throws IOException, CertificateException, NoSuchAlgo } if (isKeyring(conf.getKeyStore())) { System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); - try { - this.keyStore = loadKeyringKeyStore(conf.getKeyStore(), conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + try (InputStream keyringIStream = openKeyringStream(conf.getKeyStore(), conf.getKeyStorePassword().toCharArray())) { + this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); } catch (Exception e) { @@ -191,152 +191,73 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { } /** - * Loads a KeyStore from a SAF keyring using multiple strategies in order: - *
      - *
    1. Standard URL approach: {@code new URL("safkeyring://...").openStream()}
    2. - *
    3. RACFInputStream via reflection (bypasses URL handler)
    4. - *
    5. Direct JCERACFKS load via IBMZSecurity provider (no InputStream needed)
    6. - *
    + * Opens an InputStream to a SAF keyring. Tries the standard URL-based approach first, + * then falls back to IBM's RACFInputStream (loaded via reflection) if the URL protocol + * handler is not available. */ // TODO: REMOVE debug logging after SAF keyring issue is resolved @SuppressWarnings("squid:S3011") - static KeyStore loadKeyringKeyStore(String uri, char[] password, String type) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + static InputStream openKeyringStream(String uri, char[] password) throws IOException { String formatted = formatKeyringUrl(uri); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: uri='" + uri + "' formatted='" + formatted + "' type=" + type); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: java.protocol.handler.pkgs=" + System.out.println("DEBUG [Stores] openKeyringStream: uri='" + uri + "' formatted='" + formatted + "'"); + System.out.println("DEBUG [Stores] openKeyringStream: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); - Matcher matcher = KEYRING_PATTERN.matcher(uri); - if (!matcher.matches()) { - matcher = KEYRING_PATTERN.matcher(formatted); - } - String userId = matcher.matches() ? matcher.group(2) : null; - String ringName = matcher.matches() ? matcher.group(3) : null; - - // Strategy 1: Standard URL-based approach (same as main API ML services) + // Try standard URL-based approach first (same as main API ML services) try { - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 - URL approach..."); + System.out.println("DEBUG [Stores] openKeyringStream: Attempting new URL('" + formatted + "')..."); URL url = new URL(formatted); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: URL created. protocol=" + url.getProtocol()); - try (InputStream is = url.openStream()) { - System.out.println("DEBUG [Stores] loadKeyringKeyStore: URL.openStream() SUCCEEDED (stream class=" + is.getClass().getName() + ")"); - KeyStore ks = KeyStore.getInstance(type); - ks.load(is, password); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 SUCCEEDED. Aliases count=" + ks.size()); - return ks; - } + System.out.println("DEBUG [Stores] openKeyringStream: URL created. protocol=" + url.getProtocol() + + " host=" + url.getHost() + " path=" + url.getPath() + + " class=" + url.getClass().getName()); + System.out.println("DEBUG [Stores] openKeyringStream: Calling url.openStream()..."); + InputStream is = url.openStream(); + System.out.println("DEBUG [Stores] openKeyringStream: URL.openStream() SUCCEEDED (stream class=" + is.getClass().getName() + ")"); + return is; } catch (MalformedURLException urlEx) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 FAILED (MalformedURLException): " + urlEx.getMessage()); + System.err.println("DEBUG [Stores] openKeyringStream: URL approach FAILED with MalformedURLException: " + urlEx.getMessage()); } catch (IOException ioEx) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 1 FAILED (IOException on openStream): " + System.err.println("DEBUG [Stores] openKeyringStream: URL.openStream() FAILED with IOException: " + ioEx.getClass().getName() + ": " + ioEx.getMessage()); + ioEx.printStackTrace(System.err); + // Re-throw IO errors from openStream — the URL was valid but the stream failed + throw ioEx; } - // Strategy 2: RACFInputStream via reflection - if (userId != null && ringName != null) { - try { - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 - RACFInputStream('" + userId + "', '" + ringName + "')..."); - Class racfClass = Class.forName("com.ibm.crypto.zsecurity.provider.RACFInputStream"); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: RACFInputStream class loaded (classLoader=" + racfClass.getClassLoader() + ")"); - Constructor ctor = racfClass.getConstructor(String.class, String.class, char[].class); - InputStream is = (InputStream) ctor.newInstance(userId, ringName, password); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: RACFInputStream created. Loading KeyStore..."); - KeyStore ks = KeyStore.getInstance(type); - ks.load(is, password); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 SUCCEEDED. Aliases count=" + ks.size()); - return ks; - } catch (InvocationTargetException ite) { - Throwable cause = ite.getCause(); - String causeMsg = cause != null ? cause.getMessage() : ite.getMessage(); - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 FAILED (RACFInputStream constructor threw): " + causeMsg); - if (cause != null) { - cause.printStackTrace(System.err); - } - // If the error is about private key access, try Strategy 3 - if (cause instanceof IOException && causeMsg != null && causeMsg.contains("private key")) { - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Private key permission error detected." - + " This likely means the running user does not have RACF authority to access" - + " private keys in keyring " + userId + "/" + ringName + "." - + " Trying Strategy 3 (direct JCERACFKS load)..."); - } else { - // Non-private-key error — still try Strategy 3 but log the original error - System.err.println("DEBUG [Stores] loadKeyringKeyStore: RACFInputStream failed with non-private-key error. Trying Strategy 3..."); - } - } catch (ClassNotFoundException cnfe) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 FAILED (class not found): " + cnfe.getMessage()); - } catch (Exception e) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 2 FAILED: " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); + // Fallback: use com.ibm.crypto.zsecurity.provider.RACFInputStream via reflection + // This class is available on z/OS IBM JDKs and bypasses the URL protocol handler + System.out.println("DEBUG [Stores] openKeyringStream: URL handler not available, attempting RACFInputStream fallback..."); + + Matcher matcher = KEYRING_PATTERN.matcher(uri); + if (!matcher.matches()) { + matcher = KEYRING_PATTERN.matcher(formatted); + if (!matcher.matches()) { + throw new IOException("Cannot open keyring: invalid URI format: " + uri); } } - // Strategy 3: Direct JCERACFKS KeyStore load using IBMZSecurity provider - // On IBM z/OS JDK, the JCERACFKS KeyStore SPI can load keyrings directly - // when the keyring URL is provided as the password/loadStoreParameter. - // This approach does NOT use RACFInputStream and may handle inaccessible - // private keys gracefully (loading only certificates the user can access). - Provider ibmZSecurityProvider = Security.getProvider("IBMZSecurity"); - if (ibmZSecurityProvider != null) { - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3 - Direct JCERACFKS via IBMZSecurity provider..."); - try { - KeyStore ks = KeyStore.getInstance(type, ibmZSecurityProvider); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: KeyStore.getInstance('" + type + "', IBMZSecurity) succeeded."); - - // Try loading with the keyring URL as the password - // Some IBM implementations accept safkeyring://userId/ring as load parameter - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3a - load(null, ringUrl as password)..."); - try { - ks.load(null, formatted.toCharArray()); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3a SUCCEEDED. Aliases count=" + ks.size()); - return ks; - } catch (Exception e3a) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3a FAILED: " + e3a.getClass().getName() + ": " + e3a.getMessage()); - } - - // Try loading with a DomainLoadStoreParameter-style approach - // Pass the ring name in format "userId/ringName" as password - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3b - load(null, userId/ringName as password)..."); - try { - ks = KeyStore.getInstance(type, ibmZSecurityProvider); - ks.load(null, (userId + "/" + ringName).toCharArray()); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3b SUCCEEDED. Aliases count=" + ks.size()); - return ks; - } catch (Exception e3b) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3b FAILED: " + e3b.getClass().getName() + ": " + e3b.getMessage()); - } + String userId = matcher.group(2); + String ringName = matcher.group(3); + System.out.println("DEBUG [Stores] openKeyringStream: Parsed userId='" + userId + "' ringName='" + ringName + "'"); - // Try with standard password and null stream - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c - load(null, 'password')..."); - try { - ks = KeyStore.getInstance(type, ibmZSecurityProvider); - ks.load(null, password); - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c SUCCEEDED. Aliases count=" + ks.size()); - if (ks.size() > 0) { - return ks; - } - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c returned empty keystore, not useful."); - } catch (Exception e3c) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3c FAILED: " + e3c.getClass().getName() + ": " + e3c.getMessage()); - } - } catch (Exception e3) { - System.err.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3 setup FAILED: " + e3.getClass().getName() + ": " + e3.getMessage()); - e3.printStackTrace(System.err); - } - } else { - System.out.println("DEBUG [Stores] loadKeyringKeyStore: Strategy 3 SKIPPED (IBMZSecurity provider not available)"); + try { + System.out.println("DEBUG [Stores] openKeyringStream: Loading class com.ibm.crypto.zsecurity.provider.RACFInputStream..."); + Class racfClass = Class.forName("com.ibm.crypto.zsecurity.provider.RACFInputStream"); + System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream class loaded (classLoader=" + racfClass.getClassLoader() + ")"); + Constructor ctor = racfClass.getConstructor(String.class, String.class, char[].class); + System.out.println("DEBUG [Stores] openKeyringStream: Invoking RACFInputStream('" + userId + "', '" + ringName + "', )..."); + InputStream is = (InputStream) ctor.newInstance(userId, ringName, password); + System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream SUCCEEDED (stream class=" + is.getClass().getName() + ")"); + return is; + } catch (ClassNotFoundException cnfe) { + System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream class NOT FOUND: " + cnfe.getMessage()); + throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + + "and RACFInputStream class not found. Ensure running on z/OS with IBM JDK.", cnfe); + } catch (Exception e) { + System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream FAILED: " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); + throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + + "and RACFInputStream fallback failed: " + e.getMessage(), e); } - - // All strategies failed - String errorMsg = "Cannot load keyring '" + uri + "': all strategies exhausted.\n" - + " - URL handler: safkeyring protocol handler not found in this JDK.\n" - + " - RACFInputStream: likely failed due to RACF authority.\n" - + "Ensure the user running 'zwe validate' has READ access to the keyring.\n" - + "RACF commands to verify/grant access:\n" - + " RLIST FACILITY IRR.DIGTCERT.LISTRING ALL\n" - + " PERMIT IRR.DIGTCERT.LISTRING CLASS(FACILITY) ID() ACCESS(READ)\n" - + " SETROPTS RACLIST(FACILITY) REFRESH"; - System.err.println("DEBUG [Stores] loadKeyringKeyStore: ALL STRATEGIES FAILED."); - System.err.println(errorMsg); - throw new IOException(errorMsg); } } From 2e9ed3a536e82f99b5cc592889bf274ad089a8c6 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 21 May 2026 13:46:07 +0530 Subject: [PATCH 15/28] Revert "further fix" This reverts commit fc8c14f65b704344d700b7ca5991b0f3a0389ca4. --- .../src/main/java/org/zowe/apiml/Stores.java | 85 ++----------------- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 59 +------------ 2 files changed, 11 insertions(+), 133 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 8dad9b614d..4ff78fefcc 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 @@ -14,7 +14,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Constructor; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyStore; @@ -113,9 +112,14 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } if (isKeyring(conf.getTrustStore())) { System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: Opening keyring stream (URL with RACFInputStream fallback)..."); - try (InputStream trustStoreIStream = openKeyringStream(conf.getTrustStore(), conf.getTrustStorePassword().toCharArray())) { - System.out.println("DEBUG [Stores] initTruststore: Stream opened. Loading keystore type=" + conf.getTrustStoreType()); + String formatted = formatKeyringUrl(conf.getTrustStore()); + System.out.println("DEBUG [Stores] initTruststore: Formatted keyring URL: " + formatted); + System.out.println("DEBUG [Stores] initTruststore: Calling keyRingUrl()..."); + URL url = keyRingUrl(conf.getTrustStore()); + System.out.println("DEBUG [Stores] initTruststore: URL object created: " + url + " (protocol=" + url.getProtocol() + ")"); + System.out.println("DEBUG [Stores] initTruststore: Calling url.openStream()..."); + try (InputStream trustStoreIStream = url.openStream()) { + System.out.println("DEBUG [Stores] initTruststore: openStream() succeeded. Loading keystore type=" + conf.getTrustStoreType()); this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } @@ -135,7 +139,7 @@ private void initKeystore() throws IOException, CertificateException, NoSuchAlgo } if (isKeyring(conf.getKeyStore())) { System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); - try (InputStream keyringIStream = openKeyringStream(conf.getKeyStore(), conf.getKeyStorePassword().toCharArray())) { + try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); @@ -189,75 +193,4 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { throw e; } } - - /** - * Opens an InputStream to a SAF keyring. Tries the standard URL-based approach first, - * then falls back to IBM's RACFInputStream (loaded via reflection) if the URL protocol - * handler is not available. - */ - // TODO: REMOVE debug logging after SAF keyring issue is resolved - @SuppressWarnings("squid:S3011") - static InputStream openKeyringStream(String uri, char[] password) throws IOException { - String formatted = formatKeyringUrl(uri); - System.out.println("DEBUG [Stores] openKeyringStream: uri='" + uri + "' formatted='" + formatted + "'"); - System.out.println("DEBUG [Stores] openKeyringStream: java.protocol.handler.pkgs=" - + System.getProperty("java.protocol.handler.pkgs", "")); - - // Try standard URL-based approach first (same as main API ML services) - try { - System.out.println("DEBUG [Stores] openKeyringStream: Attempting new URL('" + formatted + "')..."); - URL url = new URL(formatted); - System.out.println("DEBUG [Stores] openKeyringStream: URL created. protocol=" + url.getProtocol() - + " host=" + url.getHost() + " path=" + url.getPath() - + " class=" + url.getClass().getName()); - System.out.println("DEBUG [Stores] openKeyringStream: Calling url.openStream()..."); - InputStream is = url.openStream(); - System.out.println("DEBUG [Stores] openKeyringStream: URL.openStream() SUCCEEDED (stream class=" + is.getClass().getName() + ")"); - return is; - } catch (MalformedURLException urlEx) { - System.err.println("DEBUG [Stores] openKeyringStream: URL approach FAILED with MalformedURLException: " + urlEx.getMessage()); - } catch (IOException ioEx) { - System.err.println("DEBUG [Stores] openKeyringStream: URL.openStream() FAILED with IOException: " - + ioEx.getClass().getName() + ": " + ioEx.getMessage()); - ioEx.printStackTrace(System.err); - // Re-throw IO errors from openStream — the URL was valid but the stream failed - throw ioEx; - } - - // Fallback: use com.ibm.crypto.zsecurity.provider.RACFInputStream via reflection - // This class is available on z/OS IBM JDKs and bypasses the URL protocol handler - System.out.println("DEBUG [Stores] openKeyringStream: URL handler not available, attempting RACFInputStream fallback..."); - - Matcher matcher = KEYRING_PATTERN.matcher(uri); - if (!matcher.matches()) { - matcher = KEYRING_PATTERN.matcher(formatted); - if (!matcher.matches()) { - throw new IOException("Cannot open keyring: invalid URI format: " + uri); - } - } - - String userId = matcher.group(2); - String ringName = matcher.group(3); - System.out.println("DEBUG [Stores] openKeyringStream: Parsed userId='" + userId + "' ringName='" + ringName + "'"); - - try { - System.out.println("DEBUG [Stores] openKeyringStream: Loading class com.ibm.crypto.zsecurity.provider.RACFInputStream..."); - Class racfClass = Class.forName("com.ibm.crypto.zsecurity.provider.RACFInputStream"); - System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream class loaded (classLoader=" + racfClass.getClassLoader() + ")"); - Constructor ctor = racfClass.getConstructor(String.class, String.class, char[].class); - System.out.println("DEBUG [Stores] openKeyringStream: Invoking RACFInputStream('" + userId + "', '" + ringName + "', )..."); - InputStream is = (InputStream) ctor.newInstance(userId, ringName, password); - System.out.println("DEBUG [Stores] openKeyringStream: RACFInputStream SUCCEEDED (stream class=" + is.getClass().getName() + ")"); - return is; - } catch (ClassNotFoundException cnfe) { - System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream class NOT FOUND: " + cnfe.getMessage()); - throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + - "and RACFInputStream class not found. Ensure running on z/OS with IBM JDK.", cnfe); - } catch (Exception e) { - System.err.println("DEBUG [Stores] openKeyringStream: RACFInputStream FAILED: " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); - throw new IOException("Cannot open keyring '" + uri + "': URL protocol handler not available " + - "and RACFInputStream fallback failed: " + e.getMessage(), e); - } - } } 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 45a92ed472..1144755085 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 @@ -33,13 +33,7 @@ public static int mainWithExitCode(String[] args) { System.out.println("DEBUG [ZosmfJwtCheck] ===== zosmf-jwt-check starting ====="); System.out.println("DEBUG [ZosmfJwtCheck] Java version: " + System.getProperty("java.version")); System.out.println("DEBUG [ZosmfJwtCheck] Java vendor: " + System.getProperty("java.vendor")); - System.out.println("DEBUG [ZosmfJwtCheck] Java home: " + System.getProperty("java.home")); System.out.println("DEBUG [ZosmfJwtCheck] OS name: " + System.getProperty("os.name")); - System.out.println("DEBUG [ZosmfJwtCheck] java.class.path: " + System.getProperty("java.class.path", "")); - System.out.println("DEBUG [ZosmfJwtCheck] java.ext.dirs: " + System.getProperty("java.ext.dirs", "")); - System.out.println("DEBUG [ZosmfJwtCheck] Classloader chain: app=" + ZosmfJwtCheck.class.getClassLoader() - + " parent=" + (ZosmfJwtCheck.class.getClassLoader() != null ? ZosmfJwtCheck.class.getClassLoader().getParent() : "null") - + " threadCtx=" + Thread.currentThread().getContextClassLoader()); System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (BEFORE ensureSafkeyringHandler): " + System.getProperty("java.protocol.handler.pkgs", "")); @@ -143,58 +137,9 @@ static void validateConfig(ZosmfJwtCheckConf conf) { */ static void ensureSafkeyringHandler() { String existing = System.getProperty("java.protocol.handler.pkgs", ""); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: current value='" + existing + "'"); if (!existing.contains("com.ibm.crypto.provider")) { - String newValue = existing.isEmpty() ? "com.ibm.crypto.provider" : existing + "|com.ibm.crypto.provider"; - System.setProperty("java.protocol.handler.pkgs", newValue); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: SET to '" + newValue + "'"); - } else { - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: already contains com.ibm.crypto.provider, no change"); - } - - // TODO: REMOVE diagnostic checks after SAF keyring issue is resolved - // Check which IBM z/OS keyring-related classes are available on the classpath - String[] classesToCheck = { - "com.ibm.crypto.provider.safkeyring.Handler", - "com.ibm.crypto.provider.safkeyringjce.Handler", - "com.ibm.crypto.provider.safkeyringjcecca.Handler", - "com.ibm.crypto.provider.safkeyringjceccaracfks.Handler", - "com.ibm.crypto.provider.safkeyringjcehybrid.Handler", - "com.ibm.crypto.zsecurity.provider.RACFInputStream", - "com.ibm.jsse2.IBMJSSEProvider2", - }; - for (String className : classesToCheck) { - try { - Class cls = Class.forName(className); - System.out.println("DEBUG [ZosmfJwtCheck] Class FOUND: " + className - + " (classLoader=" + cls.getClassLoader() + ")"); - } catch (ClassNotFoundException e) { - System.out.println("DEBUG [ZosmfJwtCheck] Class NOT FOUND: " + className); - } - } - - // List installed security providers - System.out.println("DEBUG [ZosmfJwtCheck] Installed security providers:"); - for (java.security.Provider p : java.security.Security.getProviders()) { - System.out.println("DEBUG [ZosmfJwtCheck] " + p.getName() + " v" + p.getVersionStr() - + " (" + p.getClass().getName() + ")"); - } - - // Test URL creation inline to capture exact failure point - System.out.println("DEBUG [ZosmfJwtCheck] Testing URL creation for 'safkeyring://test/test'..."); - try { - java.net.URL testUrl = new java.net.URL("safkeyring://test/test"); - System.out.println("DEBUG [ZosmfJwtCheck] URL creation SUCCEEDED: " + testUrl); - } catch (java.net.MalformedURLException e) { - System.out.println("DEBUG [ZosmfJwtCheck] URL creation FAILED: " + e.getMessage()); - // If the handler class was FOUND above but URL creation FAILED, that confirms - // the bootstrap classloader theory: Class.forName from our code can find it, - // but java.net.URL (loaded by bootstrap classloader) cannot. - System.out.println("DEBUG [ZosmfJwtCheck] URL class classLoader: " + java.net.URL.class.getClassLoader()); - System.out.println("DEBUG [ZosmfJwtCheck] This class classLoader: " + ZosmfJwtCheck.class.getClassLoader()); - System.out.println("DEBUG [ZosmfJwtCheck] If handler class was FOUND above but URL creation FAILED,"); - System.out.println("DEBUG [ZosmfJwtCheck] this indicates a classloader visibility issue."); - System.out.println("DEBUG [ZosmfJwtCheck] The RACFInputStream fallback in openKeyringStream() will be used instead."); + System.setProperty("java.protocol.handler.pkgs", + existing.isEmpty() ? "com.ibm.crypto.provider" : existing + "|com.ibm.crypto.provider"); } } From b7947a109958acca6921268b9c25900c836c8ca4 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 21 May 2026 13:49:51 +0530 Subject: [PATCH 16/28] Revert "Debug Logs" This reverts commit 2088e48998e9bd0d0f8aae917382dc7d05957c6b. --- .../org/zowe/apiml/HttpClientWrapper.java | 14 ----- .../org/zowe/apiml/JwkEndpointChecker.java | 19 ------- .../org/zowe/apiml/SSLContextFactory.java | 6 --- .../src/main/java/org/zowe/apiml/Stores.java | 52 ++----------------- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 30 ----------- 5 files changed, 3 insertions(+), 118 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 8feecfa6e3..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 @@ -28,7 +28,6 @@ * Supports custom {@link SSLContext} and {@link HostnameVerifier} for * STRICT, NONSTRICT, and DISABLED certificate verification modes. */ -// TODO: REMOVE all "DEBUG [HttpClientWrapper]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class HttpClientWrapper { @@ -65,11 +64,6 @@ public HttpClientWrapper() { } public Response executeCall(URL url, Map headers) throws IOException { - System.out.println("DEBUG [HttpClientWrapper] executeCall() URL=" + url); - System.out.println("DEBUG [HttpClientWrapper] useHttps=" + useHttps); - System.out.println("DEBUG [HttpClientWrapper] sslContext=" + (sslContext != null ? sslContext.getProtocol() : "")); - System.out.println("DEBUG [HttpClientWrapper] hostnameVerifier=" + (hostnameVerifier != null ? hostnameVerifier.getClass().getName() : "")); - HttpURLConnection con; if (useHttps) { HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); @@ -78,10 +72,8 @@ public Response executeCall(URL url, Map headers) throws IOExcep httpsCon.setHostnameVerifier(hostnameVerifier); } con = httpsCon; - System.out.println("DEBUG [HttpClientWrapper] HTTPS connection opened"); } else { con = (HttpURLConnection) url.openConnection(); - System.out.println("DEBUG [HttpClientWrapper] HTTP connection opened"); } con.setRequestMethod("GET"); @@ -94,16 +86,10 @@ public Response executeCall(URL url, Map headers) throws IOExcep } } - System.out.println("DEBUG [HttpClientWrapper] Sending GET request (connectTimeout=" + CONNECT_TIMEOUT + "ms, readTimeout=" + READ_TIMEOUT + "ms)..."); try { int responseCode = con.getResponseCode(); - System.out.println("DEBUG [HttpClientWrapper] Response code=" + responseCode); String body = readBody(con); - System.out.println("DEBUG [HttpClientWrapper] Response body read, length=" + (body != null ? body.length() : "")); return new Response(responseCode, body); - } catch (IOException e) { - System.err.println("DEBUG [HttpClientWrapper] IOException during request: " + e.getClass().getName() + ": " + e.getMessage()); - throw e; } finally { con.disconnect(); } 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 1eedc7e8d3..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 @@ -24,7 +24,6 @@ * 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 */ -// TODO: REMOVE all "DEBUG [JwkEndpointChecker]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class JwkEndpointChecker { @@ -46,19 +45,11 @@ public boolean check() { Map headers = new HashMap<>(); headers.put(ZOSMF_CSRF_HEADER, ""); - System.out.println("DEBUG [JwkEndpointChecker] check() called"); - System.out.println("DEBUG [JwkEndpointChecker] target URL=" + urlString); - System.out.println("DEBUG [JwkEndpointChecker] headers=" + headers); - try { URL url = new URL(urlString); System.out.println("Checking z/OSMF JWK endpoint: " + urlString); - System.out.println("DEBUG [JwkEndpointChecker] Calling httpClient.executeCall()..."); HttpClientWrapper.Response response = httpClient.executeCall(url, headers); - - System.out.println("DEBUG [JwkEndpointChecker] Response received: HTTP " + response.getStatusCode()); - System.out.println("DEBUG [JwkEndpointChecker] Response body length=" + (response.getBody() != null ? response.getBody().length() : "")); if (conf.isVerbose() && response.getBody() != null) { System.out.println("Response body:\n" + response.getBody()); } @@ -67,34 +58,24 @@ public boolean check() { 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()); - System.err.println("DEBUG [JwkEndpointChecker] SSLHandshakeException stack trace:"); - e.printStackTrace(System.err); 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()); - System.err.println("DEBUG [JwkEndpointChecker] ConnectException stack trace:"); - e.printStackTrace(System.err); return false; } catch (SocketTimeoutException e) { System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); 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."); - System.err.println("DEBUG [JwkEndpointChecker] SocketTimeoutException stack trace:"); - e.printStackTrace(System.err); 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."); - System.err.println("DEBUG [JwkEndpointChecker] UnknownHostException stack trace:"); - e.printStackTrace(System.err); return false; } catch (Exception e) { System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); System.err.println("Details: " + e.getMessage()); - System.err.println("DEBUG [JwkEndpointChecker] Exception class=" + e.getClass().getName()); - e.printStackTrace(System.err); return false; } } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index c3e1b034a0..8e4f8cedaf 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -27,7 +27,6 @@ *
  • {@link #initTrustAllSSLContext()} — trust-all mode for DISABLED verification
  • * */ -// TODO: REMOVE all "DEBUG [SSLContextFactory]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class SSLContextFactory { @@ -49,19 +48,15 @@ public SSLContext getSslContext() { * @return factory holding the initialized SSLContext */ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { - System.out.println("DEBUG [SSLContextFactory] initSSLContext() called"); SSLContextFactory factory = new SSLContextFactory(stores); - System.out.println("DEBUG [SSLContextFactory] Initializing TrustManagerFactory with trustStore (type=" + stores.getTrustStore().getType() + ", size=" + stores.getTrustStore().size() + ")"); TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustFactory.init(stores.getTrustStore()); KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); if (stores.getKeyStore() != null) { - System.out.println("DEBUG [SSLContextFactory] Initializing KeyManagerFactory with keyStore (type=" + stores.getKeyStore().getType() + ", size=" + stores.getKeyStore().size() + ")"); keyFactory.init(stores.getKeyStore(), stores.getConf().getKeyStorePassword().toCharArray()); } else { - System.out.println("DEBUG [SSLContextFactory] No keyStore, using empty keystore for KeyManagerFactory"); KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); emptyKeystore.load(null, null); keyFactory.init(emptyKeystore, null); @@ -69,7 +64,6 @@ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgor factory.sslContext = SSLContext.getInstance("TLSv1.2"); factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); - System.out.println("DEBUG [SSLContextFactory] SSLContext created successfully (protocol=" + factory.sslContext.getProtocol() + ")"); return factory; } 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 4ff78fefcc..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 @@ -27,7 +27,6 @@ * Loads Java {@link java.security.KeyStore} instances from the filesystem * or z/OS SAF keyrings. Supports PKCS12, JKS, and {@code safkeyring://} URIs. */ -// TODO: REMOVE all "DEBUG [Stores]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class Stores { @@ -70,38 +69,22 @@ public static String formatKeyringUrl(String input) { } void init() { - System.out.println("DEBUG [Stores] init() called"); - System.out.println("DEBUG [Stores] trustStore path=" + conf.getTrustStore()); - System.out.println("DEBUG [Stores] trustStore type=" + conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] trustStore isKeyring=" + isKeyring(conf.getTrustStore())); - System.out.println("DEBUG [Stores] keyStore path=" + conf.getKeyStore()); - System.out.println("DEBUG [Stores] keyStore type=" + (conf.getKeyStore() != null ? conf.getKeyStoreType() : "")); - System.out.println("DEBUG [Stores] keyStore isKeyring=" + isKeyring(conf.getKeyStore())); - System.out.println("DEBUG [Stores] java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); try { - System.out.println("DEBUG [Stores] Calling initKeystore()..."); initKeystore(); - System.out.println("DEBUG [Stores] initKeystore() completed. trustStore set by keystore=" + (trustStore != null)); if (trustStore == null) { - System.out.println("DEBUG [Stores] Calling initTruststore()..."); initTruststore(); - System.out.println("DEBUG [Stores] initTruststore() completed successfully."); } } catch (FileNotFoundException e) { - System.err.println("DEBUG [Stores] FileNotFoundException in init(): " + e.getMessage()); - e.printStackTrace(System.err); 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) { - System.err.println("DEBUG [Stores] Exception in init(): " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); throw new StoresNotInitializeException(e.getMessage()); } } private void initTruststore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if (conf.getTrustStore() == null) { - System.out.println("DEBUG [Stores] initTruststore: No truststore specified, will use empty."); + System.out.println("No truststore specified, will use empty."); try { this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); this.trustStore.load(null, null); @@ -111,48 +94,30 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl return; } if (isKeyring(conf.getTrustStore())) { - System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); - String formatted = formatKeyringUrl(conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: Formatted keyring URL: " + formatted); - System.out.println("DEBUG [Stores] initTruststore: Calling keyRingUrl()..."); - URL url = keyRingUrl(conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: URL object created: " + url + " (protocol=" + url.getProtocol() + ")"); - System.out.println("DEBUG [Stores] initTruststore: Calling url.openStream()..."); - try (InputStream trustStoreIStream = url.openStream()) { - System.out.println("DEBUG [Stores] initTruststore: openStream() succeeded. Loading keystore type=" + conf.getTrustStoreType()); + try (InputStream trustStoreIStream = keyRingUrl(conf.getTrustStore()).openStream()) { this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } } else { - System.out.println("DEBUG [Stores] initTruststore: Loading from file: " + conf.getTrustStore()); try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from file. Aliases count=" + trustStore.size()); } } } private void initKeystore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if (conf.getKeyStore() == null) { - System.out.println("DEBUG [Stores] initKeystore: No keystore specified, skipping."); return; } if (isKeyring(conf.getKeyStore())) { - System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; - System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); } catch (Exception e) { - System.err.println("DEBUG [Stores] initKeystore: Exception loading keyring: " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); throw new StoresNotInitializeException(e.getMessage()); } } else { - System.out.println("DEBUG [Stores] initKeystore: Loading from file: " + conf.getKeyStore()); try (InputStream keyStoreIStream = new FileInputStream(conf.getKeyStore())) { this.keyStore = readKeyStore(keyStoreIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); - System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from file. Aliases count=" + keyStore.size()); } } } @@ -180,17 +145,6 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { throw new StoresNotInitializeException("Incorrect key ring format: " + uri + ". Make sure you use format safkeyring://userId/keyRing"); } - String formatted = formatKeyringUrl(uri); - System.out.println("DEBUG [Stores] keyRingUrl: Creating URL from: " + formatted); - try { - URL url = new URL(formatted); - System.out.println("DEBUG [Stores] keyRingUrl: URL created successfully. protocol=" + url.getProtocol() - + " host=" + url.getHost() + " path=" + url.getPath()); - return url; - } catch (MalformedURLException e) { - System.err.println("DEBUG [Stores] keyRingUrl: MalformedURLException for '" + formatted + "': " + e.getMessage()); - System.err.println("DEBUG [Stores] keyRingUrl: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); - throw e; - } + return new URL(formatKeyringUrl(uri)); } } 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 1144755085..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 @@ -21,7 +21,6 @@ * *

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

    */ -// TODO: REMOVE all "DEBUG [ZosmfJwtCheck]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class ZosmfJwtCheck { @@ -30,18 +29,7 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { - System.out.println("DEBUG [ZosmfJwtCheck] ===== zosmf-jwt-check starting ====="); - System.out.println("DEBUG [ZosmfJwtCheck] Java version: " + System.getProperty("java.version")); - System.out.println("DEBUG [ZosmfJwtCheck] Java vendor: " + System.getProperty("java.vendor")); - System.out.println("DEBUG [ZosmfJwtCheck] OS name: " + System.getProperty("os.name")); - System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (BEFORE ensureSafkeyringHandler): " - + System.getProperty("java.protocol.handler.pkgs", "")); - ensureSafkeyringHandler(); - - System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (AFTER ensureSafkeyringHandler): " - + System.getProperty("java.protocol.handler.pkgs", "")); - try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); @@ -53,17 +41,6 @@ public static int mainWithExitCode(String[] args) { return 8; } - System.out.println("DEBUG [ZosmfJwtCheck] Parsed config:"); - System.out.println("DEBUG [ZosmfJwtCheck] zosmf-host=" + conf.getZosmfHost()); - System.out.println("DEBUG [ZosmfJwtCheck] zosmf-port=" + conf.getZosmfPort()); - System.out.println("DEBUG [ZosmfJwtCheck] scheme=" + conf.getScheme()); - System.out.println("DEBUG [ZosmfJwtCheck] verify-certificates=" + conf.getVerifyCertificates()); - System.out.println("DEBUG [ZosmfJwtCheck] truststore-file=" + conf.getTrustStore()); - System.out.println("DEBUG [ZosmfJwtCheck] truststore-type=" + conf.getTrustStoreType()); - System.out.println("DEBUG [ZosmfJwtCheck] truststore-isKeyring=" + Stores.isKeyring(conf.getTrustStore())); - System.out.println("DEBUG [ZosmfJwtCheck] keystore-file=" + conf.getKeyStore()); - System.out.println("DEBUG [ZosmfJwtCheck] keystore-type=" + conf.getKeyStoreType()); - validateConfig(conf); HttpClientWrapper httpClient; @@ -75,11 +52,8 @@ public static int mainWithExitCode(String[] args) { HostnameVerifier noopVerifier = (hostname, session) -> true; httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); } else { - System.out.println("DEBUG [ZosmfJwtCheck] Creating Stores (truststore/keystore)..."); Stores stores = new Stores(conf); - System.out.println("DEBUG [ZosmfJwtCheck] Stores created successfully. Building SSLContext..."); SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); - System.out.println("DEBUG [ZosmfJwtCheck] SSLContext created successfully."); HostnameVerifier hostnameVerifier; if (VERIFY_NONSTRICT.equals(verifyMode)) { @@ -95,14 +69,10 @@ public static int mainWithExitCode(String[] args) { } JwkEndpointChecker checker = new JwkEndpointChecker(httpClient, conf); - System.out.println("DEBUG [ZosmfJwtCheck] Calling JwkEndpointChecker.check()..."); boolean success = checker.check(); - System.out.println("DEBUG [ZosmfJwtCheck] JwkEndpointChecker.check() returned: " + success); return success ? 0 : 4; } catch (Exception e) { System.err.println("ERROR: " + e.getMessage()); - System.err.println("DEBUG [ZosmfJwtCheck] Exception class: " + e.getClass().getName()); - e.printStackTrace(System.err); return 4; } } From df3f10f2dbff82e0af8b9584c2c74255982122b1 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Thu, 21 May 2026 13:57:48 +0530 Subject: [PATCH 17/28] Revert "safkeyring truststore bug fix" This reverts commit e2d0058a2890b35ff983176e1cbf528ab1e4de0e. --- .../main/java/org/zowe/apiml/ZosmfJwtCheck.java | 15 --------------- 1 file changed, 15 deletions(-) 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 e95093a748..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 @@ -29,7 +29,6 @@ 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); @@ -99,20 +98,6 @@ 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)); } From 4ed5b49a9622e8c839a9d6983a5bfd2e9ebb5933 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Mon, 25 May 2026 10:28:32 +0530 Subject: [PATCH 18/28] debug logs for safkeyring issue Signed-off-by: hrishikesh-nalawade --- .../org/zowe/apiml/HttpClientWrapper.java | 14 +++++ .../org/zowe/apiml/JwkEndpointChecker.java | 19 +++++++ .../org/zowe/apiml/SSLContextFactory.java | 6 +++ .../src/main/java/org/zowe/apiml/Stores.java | 52 +++++++++++++++++-- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 45 ++++++++++++++++ 5 files changed, 133 insertions(+), 3 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 8faca04157..8feecfa6e3 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 @@ -28,6 +28,7 @@ * Supports custom {@link SSLContext} and {@link HostnameVerifier} for * STRICT, NONSTRICT, and DISABLED certificate verification modes. */ +// TODO: REMOVE all "DEBUG [HttpClientWrapper]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class HttpClientWrapper { @@ -64,6 +65,11 @@ public HttpClientWrapper() { } public Response executeCall(URL url, Map headers) throws IOException { + System.out.println("DEBUG [HttpClientWrapper] executeCall() URL=" + url); + System.out.println("DEBUG [HttpClientWrapper] useHttps=" + useHttps); + System.out.println("DEBUG [HttpClientWrapper] sslContext=" + (sslContext != null ? sslContext.getProtocol() : "")); + System.out.println("DEBUG [HttpClientWrapper] hostnameVerifier=" + (hostnameVerifier != null ? hostnameVerifier.getClass().getName() : "")); + HttpURLConnection con; if (useHttps) { HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); @@ -72,8 +78,10 @@ public Response executeCall(URL url, Map headers) throws IOExcep httpsCon.setHostnameVerifier(hostnameVerifier); } con = httpsCon; + System.out.println("DEBUG [HttpClientWrapper] HTTPS connection opened"); } else { con = (HttpURLConnection) url.openConnection(); + System.out.println("DEBUG [HttpClientWrapper] HTTP connection opened"); } con.setRequestMethod("GET"); @@ -86,10 +94,16 @@ public Response executeCall(URL url, Map headers) throws IOExcep } } + System.out.println("DEBUG [HttpClientWrapper] Sending GET request (connectTimeout=" + CONNECT_TIMEOUT + "ms, readTimeout=" + READ_TIMEOUT + "ms)..."); try { int responseCode = con.getResponseCode(); + System.out.println("DEBUG [HttpClientWrapper] Response code=" + responseCode); String body = readBody(con); + System.out.println("DEBUG [HttpClientWrapper] Response body read, length=" + (body != null ? body.length() : "")); return new Response(responseCode, body); + } catch (IOException e) { + System.err.println("DEBUG [HttpClientWrapper] IOException during request: " + e.getClass().getName() + ": " + e.getMessage()); + throw e; } finally { con.disconnect(); } 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 06f5b0681a..1eedc7e8d3 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 @@ -24,6 +24,7 @@ * 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 */ +// TODO: REMOVE all "DEBUG [JwkEndpointChecker]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class JwkEndpointChecker { @@ -45,11 +46,19 @@ public boolean check() { Map headers = new HashMap<>(); headers.put(ZOSMF_CSRF_HEADER, ""); + System.out.println("DEBUG [JwkEndpointChecker] check() called"); + System.out.println("DEBUG [JwkEndpointChecker] target URL=" + urlString); + System.out.println("DEBUG [JwkEndpointChecker] headers=" + headers); + try { URL url = new URL(urlString); System.out.println("Checking z/OSMF JWK endpoint: " + urlString); + System.out.println("DEBUG [JwkEndpointChecker] Calling httpClient.executeCall()..."); HttpClientWrapper.Response response = httpClient.executeCall(url, headers); + + System.out.println("DEBUG [JwkEndpointChecker] Response received: HTTP " + response.getStatusCode()); + System.out.println("DEBUG [JwkEndpointChecker] Response body length=" + (response.getBody() != null ? response.getBody().length() : "")); if (conf.isVerbose() && response.getBody() != null) { System.out.println("Response body:\n" + response.getBody()); } @@ -58,24 +67,34 @@ public boolean check() { 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()); + System.err.println("DEBUG [JwkEndpointChecker] SSLHandshakeException stack trace:"); + e.printStackTrace(System.err); 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()); + System.err.println("DEBUG [JwkEndpointChecker] ConnectException stack trace:"); + e.printStackTrace(System.err); return false; } catch (SocketTimeoutException e) { System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); 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."); + System.err.println("DEBUG [JwkEndpointChecker] SocketTimeoutException stack trace:"); + e.printStackTrace(System.err); 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."); + System.err.println("DEBUG [JwkEndpointChecker] UnknownHostException stack trace:"); + e.printStackTrace(System.err); return false; } catch (Exception e) { System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); System.err.println("Details: " + e.getMessage()); + System.err.println("DEBUG [JwkEndpointChecker] Exception class=" + e.getClass().getName()); + e.printStackTrace(System.err); return false; } } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index 8e4f8cedaf..c3e1b034a0 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -27,6 +27,7 @@ *
  • {@link #initTrustAllSSLContext()} — trust-all mode for DISABLED verification
  • * */ +// TODO: REMOVE all "DEBUG [SSLContextFactory]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class SSLContextFactory { @@ -48,15 +49,19 @@ public SSLContext getSslContext() { * @return factory holding the initialized SSLContext */ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { + System.out.println("DEBUG [SSLContextFactory] initSSLContext() called"); SSLContextFactory factory = new SSLContextFactory(stores); + System.out.println("DEBUG [SSLContextFactory] Initializing TrustManagerFactory with trustStore (type=" + stores.getTrustStore().getType() + ", size=" + stores.getTrustStore().size() + ")"); TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustFactory.init(stores.getTrustStore()); KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); if (stores.getKeyStore() != null) { + System.out.println("DEBUG [SSLContextFactory] Initializing KeyManagerFactory with keyStore (type=" + stores.getKeyStore().getType() + ", size=" + stores.getKeyStore().size() + ")"); keyFactory.init(stores.getKeyStore(), stores.getConf().getKeyStorePassword().toCharArray()); } else { + System.out.println("DEBUG [SSLContextFactory] No keyStore, using empty keystore for KeyManagerFactory"); KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); emptyKeystore.load(null, null); keyFactory.init(emptyKeystore, null); @@ -64,6 +69,7 @@ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgor factory.sslContext = SSLContext.getInstance("TLSv1.2"); factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); + System.out.println("DEBUG [SSLContextFactory] SSLContext created successfully (protocol=" + factory.sslContext.getProtocol() + ")"); return factory; } 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 9a93584375..4ff78fefcc 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 @@ -27,6 +27,7 @@ * Loads Java {@link java.security.KeyStore} instances from the filesystem * or z/OS SAF keyrings. Supports PKCS12, JKS, and {@code safkeyring://} URIs. */ +// TODO: REMOVE all "DEBUG [Stores]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class Stores { @@ -69,22 +70,38 @@ public static String formatKeyringUrl(String input) { } void init() { + System.out.println("DEBUG [Stores] init() called"); + System.out.println("DEBUG [Stores] trustStore path=" + conf.getTrustStore()); + System.out.println("DEBUG [Stores] trustStore type=" + conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] trustStore isKeyring=" + isKeyring(conf.getTrustStore())); + System.out.println("DEBUG [Stores] keyStore path=" + conf.getKeyStore()); + System.out.println("DEBUG [Stores] keyStore type=" + (conf.getKeyStore() != null ? conf.getKeyStoreType() : "")); + System.out.println("DEBUG [Stores] keyStore isKeyring=" + isKeyring(conf.getKeyStore())); + System.out.println("DEBUG [Stores] java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); try { + System.out.println("DEBUG [Stores] Calling initKeystore()..."); initKeystore(); + System.out.println("DEBUG [Stores] initKeystore() completed. trustStore set by keystore=" + (trustStore != null)); if (trustStore == null) { + System.out.println("DEBUG [Stores] Calling initTruststore()..."); initTruststore(); + System.out.println("DEBUG [Stores] initTruststore() completed successfully."); } } catch (FileNotFoundException e) { + System.err.println("DEBUG [Stores] FileNotFoundException in init(): " + e.getMessage()); + e.printStackTrace(System.err); 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) { + System.err.println("DEBUG [Stores] Exception in init(): " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); 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."); + System.out.println("DEBUG [Stores] initTruststore: No truststore specified, will use empty."); try { this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); this.trustStore.load(null, null); @@ -94,30 +111,48 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl return; } if (isKeyring(conf.getTrustStore())) { - try (InputStream trustStoreIStream = keyRingUrl(conf.getTrustStore()).openStream()) { + System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); + String formatted = formatKeyringUrl(conf.getTrustStore()); + System.out.println("DEBUG [Stores] initTruststore: Formatted keyring URL: " + formatted); + System.out.println("DEBUG [Stores] initTruststore: Calling keyRingUrl()..."); + URL url = keyRingUrl(conf.getTrustStore()); + System.out.println("DEBUG [Stores] initTruststore: URL object created: " + url + " (protocol=" + url.getProtocol() + ")"); + System.out.println("DEBUG [Stores] initTruststore: Calling url.openStream()..."); + try (InputStream trustStoreIStream = url.openStream()) { + System.out.println("DEBUG [Stores] initTruststore: openStream() succeeded. Loading keystore type=" + conf.getTrustStoreType()); this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } } else { + System.out.println("DEBUG [Stores] initTruststore: Loading from file: " + conf.getTrustStore()); try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from file. Aliases count=" + trustStore.size()); } } } private void initKeystore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if (conf.getKeyStore() == null) { + System.out.println("DEBUG [Stores] initKeystore: No keystore specified, skipping."); return; } if (isKeyring(conf.getKeyStore())) { + System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; + System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); } catch (Exception e) { + System.err.println("DEBUG [Stores] initKeystore: Exception loading keyring: " + e.getClass().getName() + ": " + e.getMessage()); + e.printStackTrace(System.err); throw new StoresNotInitializeException(e.getMessage()); } } else { + System.out.println("DEBUG [Stores] initKeystore: Loading from file: " + conf.getKeyStore()); try (InputStream keyStoreIStream = new FileInputStream(conf.getKeyStore())) { this.keyStore = readKeyStore(keyStoreIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); + System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from file. Aliases count=" + keyStore.size()); } } } @@ -145,6 +180,17 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { throw new StoresNotInitializeException("Incorrect key ring format: " + uri + ". Make sure you use format safkeyring://userId/keyRing"); } - return new URL(formatKeyringUrl(uri)); + String formatted = formatKeyringUrl(uri); + System.out.println("DEBUG [Stores] keyRingUrl: Creating URL from: " + formatted); + try { + URL url = new URL(formatted); + System.out.println("DEBUG [Stores] keyRingUrl: URL created successfully. protocol=" + url.getProtocol() + + " host=" + url.getHost() + " path=" + url.getPath()); + return url; + } catch (MalformedURLException e) { + System.err.println("DEBUG [Stores] keyRingUrl: MalformedURLException for '" + formatted + "': " + e.getMessage()); + System.err.println("DEBUG [Stores] keyRingUrl: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); + throw e; + } } } 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..1144755085 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 @@ -21,6 +21,7 @@ * *

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

    */ +// TODO: REMOVE all "DEBUG [ZosmfJwtCheck]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class ZosmfJwtCheck { @@ -29,6 +30,18 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { + System.out.println("DEBUG [ZosmfJwtCheck] ===== zosmf-jwt-check starting ====="); + System.out.println("DEBUG [ZosmfJwtCheck] Java version: " + System.getProperty("java.version")); + System.out.println("DEBUG [ZosmfJwtCheck] Java vendor: " + System.getProperty("java.vendor")); + System.out.println("DEBUG [ZosmfJwtCheck] OS name: " + System.getProperty("os.name")); + System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (BEFORE ensureSafkeyringHandler): " + + System.getProperty("java.protocol.handler.pkgs", "")); + + ensureSafkeyringHandler(); + + System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (AFTER ensureSafkeyringHandler): " + + System.getProperty("java.protocol.handler.pkgs", "")); + try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); @@ -40,6 +53,17 @@ public static int mainWithExitCode(String[] args) { return 8; } + System.out.println("DEBUG [ZosmfJwtCheck] Parsed config:"); + System.out.println("DEBUG [ZosmfJwtCheck] zosmf-host=" + conf.getZosmfHost()); + System.out.println("DEBUG [ZosmfJwtCheck] zosmf-port=" + conf.getZosmfPort()); + System.out.println("DEBUG [ZosmfJwtCheck] scheme=" + conf.getScheme()); + System.out.println("DEBUG [ZosmfJwtCheck] verify-certificates=" + conf.getVerifyCertificates()); + System.out.println("DEBUG [ZosmfJwtCheck] truststore-file=" + conf.getTrustStore()); + System.out.println("DEBUG [ZosmfJwtCheck] truststore-type=" + conf.getTrustStoreType()); + System.out.println("DEBUG [ZosmfJwtCheck] truststore-isKeyring=" + Stores.isKeyring(conf.getTrustStore())); + System.out.println("DEBUG [ZosmfJwtCheck] keystore-file=" + conf.getKeyStore()); + System.out.println("DEBUG [ZosmfJwtCheck] keystore-type=" + conf.getKeyStoreType()); + validateConfig(conf); HttpClientWrapper httpClient; @@ -51,8 +75,11 @@ public static int mainWithExitCode(String[] args) { HostnameVerifier noopVerifier = (hostname, session) -> true; httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); } else { + System.out.println("DEBUG [ZosmfJwtCheck] Creating Stores (truststore/keystore)..."); Stores stores = new Stores(conf); + System.out.println("DEBUG [ZosmfJwtCheck] Stores created successfully. Building SSLContext..."); SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); + System.out.println("DEBUG [ZosmfJwtCheck] SSLContext created successfully."); HostnameVerifier hostnameVerifier; if (VERIFY_NONSTRICT.equals(verifyMode)) { @@ -68,10 +95,14 @@ public static int mainWithExitCode(String[] args) { } JwkEndpointChecker checker = new JwkEndpointChecker(httpClient, conf); + System.out.println("DEBUG [ZosmfJwtCheck] Calling JwkEndpointChecker.check()..."); boolean success = checker.check(); + System.out.println("DEBUG [ZosmfJwtCheck] JwkEndpointChecker.check() returned: " + success); return success ? 0 : 4; } catch (Exception e) { System.err.println("ERROR: " + e.getMessage()); + System.err.println("DEBUG [ZosmfJwtCheck] Exception class: " + e.getClass().getName()); + e.printStackTrace(System.err); return 4; } } @@ -98,6 +129,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)); } From ab021b3dbccef7f6aba7f91ac970a1304b441b0a Mon Sep 17 00:00:00 2001 From: "Signed-off-by: hrishikesh-nalawade" Date: Mon, 25 May 2026 15:11:32 +0530 Subject: [PATCH 19/28] new protocol handler code Signed-off-by: hrishikesh-nalawade --- .../src/main/java/org/zowe/apiml/Stores.java | 99 +++++++++++++++++++ .../java/org/zowe/apiml/ZosmfJwtCheck.java | 36 +++++-- 2 files changed, 128 insertions(+), 7 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 4ff78fefcc..a47ec2ef2e 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 @@ -188,9 +188,108 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { + " host=" + url.getHost() + " path=" + url.getPath()); return url; } catch (MalformedURLException e) { + System.out.println("DEBUG [Stores] keyRingUrl: Standard URL creation failed, trying explicit handler lookup..."); + URL fallbackUrl = createUrlWithExplicitHandler(formatted); + if (fallbackUrl != null) { + System.out.println("DEBUG [Stores] keyRingUrl: URL created via explicit handler. protocol=" + fallbackUrl.getProtocol()); + return fallbackUrl; + } System.err.println("DEBUG [Stores] keyRingUrl: MalformedURLException for '" + formatted + "': " + e.getMessage()); System.err.println("DEBUG [Stores] keyRingUrl: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); throw e; } } + + /** + * Fallback for IBM Java 17/21+ on z/OS where the safkeyring handler is registered + * via the {@code java.net.spi.URLStreamHandlerProvider} SPI mechanism in modules + * {@code ibm.crypto.zsecurity} and {@code ibm.crypto.hdwrcca}. + * + *

    If the modules are resolved (via {@code --add-modules}), the standard + * {@code new URL()} works. This fallback handles the case where the module IS + * resolved but the ServiceLoader lookup failed, or where we can reflectively + * instantiate the provider and obtain a handler.

    + */ + private static URL createUrlWithExplicitHandler(String formatted) { + // Extract protocol from the formatted URI (e.g., "safkeyring", "safkeyringjce", etc.) + String protocol = formatted.substring(0, formatted.indexOf(':')); + System.out.println("DEBUG [Stores] createUrlWithExplicitHandler: protocol='" + protocol + "' formatted='" + formatted + "'"); + + // Try 1: Use the URLStreamHandlerProvider SPI classes directly (Java 17/21 on z/OS) + // These are the actual provider classes found in ibm.crypto.zsecurity and ibm.crypto.hdwrcca + String[] spiProviderClasses = { + "com.ibm.crypto.zsecurity.provider.safkeyring.Provider", + "com.ibm.crypto.hdwrCCA.provider.safkeyring.Provider" + }; + + ClassLoader[] loaders = { + ClassLoader.getSystemClassLoader(), + ClassLoader.getPlatformClassLoader(), + Thread.currentThread().getContextClassLoader(), + Stores.class.getClassLoader() + }; + String[] loaderNames = {"SystemClassLoader", "PlatformClassLoader", "ContextClassLoader", "StoresClassLoader"}; + + System.out.println("DEBUG [Stores] createUrlWithExplicitHandler: Trying SPI providers..."); + for (String providerClassName : spiProviderClasses) { + for (int i = 0; i < loaders.length; i++) { + ClassLoader loader = loaders[i]; + if (loader == null) { + System.out.println("DEBUG [Stores] Skipping null loader: " + loaderNames[i]); + continue; + } + try { + System.out.println("DEBUG [Stores] Trying SPI class=" + providerClassName + " loader=" + loaderNames[i] + " (" + loader.getClass().getName() + ")"); + Class cls = Class.forName(providerClassName, true, loader); + System.out.println("DEBUG [Stores] Class loaded successfully: " + cls.getName()); + java.net.spi.URLStreamHandlerProvider provider = + (java.net.spi.URLStreamHandlerProvider) cls.getDeclaredConstructor().newInstance(); + System.out.println("DEBUG [Stores] Provider instantiated: " + provider.getClass().getName()); + java.net.URLStreamHandler handler = provider.createURLStreamHandler(protocol); + if (handler != null) { + System.out.println("DEBUG [Stores] SUCCESS: Handler obtained for protocol '" + protocol + "' from " + providerClassName + " via " + loaderNames[i]); + return new URL(null, formatted, handler); + } else { + System.out.println("DEBUG [Stores] Provider returned null handler for protocol '" + protocol + "' (not supported by this provider)"); + } + } catch (ClassNotFoundException e) { + System.out.println("DEBUG [Stores] ClassNotFoundException: " + providerClassName + " not found via " + loaderNames[i]); + } catch (Exception e) { + System.out.println("DEBUG [Stores] Exception: " + e.getClass().getName() + ": " + e.getMessage()); + } + } + } + + // Try 2: Legacy approach - look for Handler class directly (Java 8 style) + System.out.println("DEBUG [Stores] createUrlWithExplicitHandler: SPI providers not found. Trying legacy Handler classes..."); + String[] handlerPackages = { + "com.ibm.crypto.provider", + "com.ibm.crypto.zsecurity.provider", + "com.ibm.crypto.hdwrCCA.provider" + }; + + for (String pkg : handlerPackages) { + String className = pkg + "." + protocol + ".Handler"; + for (int i = 0; i < loaders.length; i++) { + ClassLoader loader = loaders[i]; + if (loader == null) continue; + try { + System.out.println("DEBUG [Stores] Trying Handler class=" + className + " loader=" + loaderNames[i]); + Class cls = Class.forName(className, true, loader); + java.net.URLStreamHandler handler = (java.net.URLStreamHandler) cls.getDeclaredConstructor().newInstance(); + System.out.println("DEBUG [Stores] SUCCESS: Handler instantiated: " + className + " via " + loaderNames[i]); + return new URL(null, formatted, handler); + } catch (ClassNotFoundException e) { + System.out.println("DEBUG [Stores] ClassNotFoundException: " + className + " not found via " + loaderNames[i]); + } catch (Exception e) { + System.out.println("DEBUG [Stores] Exception: " + e.getClass().getName() + ": " + e.getMessage()); + } + } + } + + System.err.println("DEBUG [Stores] createUrlWithExplicitHandler: FAILED - Could not find handler for protocol '" + protocol + "' in any known location."); + System.err.println("DEBUG [Stores] createUrlWithExplicitHandler: Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); + System.err.println("DEBUG [Stores] createUrlWithExplicitHandler: Resolved modules can be checked with: java --show-module-resolution --add-modules ibm.crypto.zsecurity -version"); + return null; + } } 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 1144755085..84f6380145 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 @@ -130,17 +130,39 @@ 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. + * Attempts to register the IBM SAF keyring URL protocol handler via the legacy + * {@code java.protocol.handler.pkgs} system property. + * + *

    Note: On IBM Java 17/21 (z/OS), the safkeyring handler is registered + * via the {@code java.net.spi.URLStreamHandlerProvider} SPI in module + * {@code ibm.crypto.zsecurity}, so this property has no effect. The real fix is + * to launch the JVM with {@code --add-modules ibm.crypto.zsecurity}. + * This method is retained only as a best-effort fallback for IBM Java 8.

    */ static void ensureSafkeyringHandler() { + String[] packagePrefixes = { + "com.ibm.crypto.zsecurity.provider", + "com.ibm.crypto.hdwrCCA.provider" + }; 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"); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: existing java.protocol.handler.pkgs='" + existing + "'"); + StringBuilder sb = new StringBuilder(existing); + for (String prefix : packagePrefixes) { + if (!existing.contains(prefix)) { + if (sb.length() > 0) { + sb.append('|'); + } + sb.append(prefix); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: Adding package prefix: " + prefix); + } else { + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: Already present: " + prefix); + } } + String finalValue = sb.toString(); + System.setProperty("java.protocol.handler.pkgs", finalValue); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: Final java.protocol.handler.pkgs='" + finalValue + "'"); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: NOTE - On Java 17/21, this property has no effect."); + System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: The safkeyring handler is loaded via SPI from module ibm.crypto.zsecurity (requires --add-modules)."); } public static void main(String[] args) { From 75ad634a12c4e9b585ad332fa7c65e3f17873f46 Mon Sep 17 00:00:00 2001 From: "Signed-off-by: hrishikesh-nalawade" Date: Mon, 25 May 2026 17:15:15 +0530 Subject: [PATCH 20/28] reverting fallback fixes and debug logs Signed-off-by: hrishikesh-nalawade --- zosmf-jwt-check/README.md | 9 +- .../org/zowe/apiml/HttpClientWrapper.java | 15 --- .../org/zowe/apiml/JwkEndpointChecker.java | 18 --- .../org/zowe/apiml/SSLContextFactory.java | 6 - .../src/main/java/org/zowe/apiml/Stores.java | 110 ++---------------- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 69 +---------- 6 files changed, 18 insertions(+), 209 deletions(-) diff --git a/zosmf-jwt-check/README.md b/zosmf-jwt-check/README.md index 7536e4aa71..ae7a9cbcb0 100644 --- a/zosmf-jwt-check/README.md +++ b/zosmf-jwt-check/README.md @@ -304,10 +304,10 @@ java -jar zosmf-jwt-check-.jar --zosmf-host nonexistent.host --zosmf-po ## 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: +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 IBM crypto modules to the JVM module graph: ```bash -java -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ +java --add-modules ibm.crypto.zsecurity \ -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ @@ -315,3 +315,8 @@ java -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ --truststore-password password \ --truststore-type JCERACFKS ``` + +> **Note:** On IBM Java 17/21, the safkeyring URL protocol handler is provided via the +> `java.net.spi.URLStreamHandlerProvider` SPI in module `ibm.crypto.zsecurity`. +> The legacy `-Djava.protocol.handler.pkgs=com.ibm.crypto.provider` property +> has no effect on Java 17+ and should not be used. 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 8feecfa6e3..0b2a05d35a 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 @@ -28,8 +28,6 @@ * Supports custom {@link SSLContext} and {@link HostnameVerifier} for * STRICT, NONSTRICT, and DISABLED certificate verification modes. */ -// TODO: REMOVE all "DEBUG [HttpClientWrapper]" logging lines after SAF keyring issue is resolved -@SuppressWarnings("squid:S106") public class HttpClientWrapper { private static final int CONNECT_TIMEOUT = 5000; @@ -65,11 +63,6 @@ public HttpClientWrapper() { } public Response executeCall(URL url, Map headers) throws IOException { - System.out.println("DEBUG [HttpClientWrapper] executeCall() URL=" + url); - System.out.println("DEBUG [HttpClientWrapper] useHttps=" + useHttps); - System.out.println("DEBUG [HttpClientWrapper] sslContext=" + (sslContext != null ? sslContext.getProtocol() : "")); - System.out.println("DEBUG [HttpClientWrapper] hostnameVerifier=" + (hostnameVerifier != null ? hostnameVerifier.getClass().getName() : "")); - HttpURLConnection con; if (useHttps) { HttpsURLConnection httpsCon = (HttpsURLConnection) url.openConnection(); @@ -78,10 +71,8 @@ public Response executeCall(URL url, Map headers) throws IOExcep httpsCon.setHostnameVerifier(hostnameVerifier); } con = httpsCon; - System.out.println("DEBUG [HttpClientWrapper] HTTPS connection opened"); } else { con = (HttpURLConnection) url.openConnection(); - System.out.println("DEBUG [HttpClientWrapper] HTTP connection opened"); } con.setRequestMethod("GET"); @@ -94,16 +85,10 @@ public Response executeCall(URL url, Map headers) throws IOExcep } } - System.out.println("DEBUG [HttpClientWrapper] Sending GET request (connectTimeout=" + CONNECT_TIMEOUT + "ms, readTimeout=" + READ_TIMEOUT + "ms)..."); try { int responseCode = con.getResponseCode(); - System.out.println("DEBUG [HttpClientWrapper] Response code=" + responseCode); String body = readBody(con); - System.out.println("DEBUG [HttpClientWrapper] Response body read, length=" + (body != null ? body.length() : "")); return new Response(responseCode, body); - } catch (IOException e) { - System.err.println("DEBUG [HttpClientWrapper] IOException during request: " + e.getClass().getName() + ": " + e.getMessage()); - throw e; } finally { con.disconnect(); } 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 1eedc7e8d3..40a1afd56d 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 @@ -24,7 +24,6 @@ * 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 */ -// TODO: REMOVE all "DEBUG [JwkEndpointChecker]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class JwkEndpointChecker { @@ -46,19 +45,12 @@ public boolean check() { Map headers = new HashMap<>(); headers.put(ZOSMF_CSRF_HEADER, ""); - System.out.println("DEBUG [JwkEndpointChecker] check() called"); - System.out.println("DEBUG [JwkEndpointChecker] target URL=" + urlString); - System.out.println("DEBUG [JwkEndpointChecker] headers=" + headers); - try { URL url = new URL(urlString); System.out.println("Checking z/OSMF JWK endpoint: " + urlString); - System.out.println("DEBUG [JwkEndpointChecker] Calling httpClient.executeCall()..."); HttpClientWrapper.Response response = httpClient.executeCall(url, headers); - System.out.println("DEBUG [JwkEndpointChecker] Response received: HTTP " + response.getStatusCode()); - System.out.println("DEBUG [JwkEndpointChecker] Response body length=" + (response.getBody() != null ? response.getBody().length() : "")); if (conf.isVerbose() && response.getBody() != null) { System.out.println("Response body:\n" + response.getBody()); } @@ -67,34 +59,24 @@ public boolean check() { 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()); - System.err.println("DEBUG [JwkEndpointChecker] SSLHandshakeException stack trace:"); - e.printStackTrace(System.err); 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()); - System.err.println("DEBUG [JwkEndpointChecker] ConnectException stack trace:"); - e.printStackTrace(System.err); return false; } catch (SocketTimeoutException e) { System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); 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."); - System.err.println("DEBUG [JwkEndpointChecker] SocketTimeoutException stack trace:"); - e.printStackTrace(System.err); 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."); - System.err.println("DEBUG [JwkEndpointChecker] UnknownHostException stack trace:"); - e.printStackTrace(System.err); return false; } catch (Exception e) { System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); System.err.println("Details: " + e.getMessage()); - System.err.println("DEBUG [JwkEndpointChecker] Exception class=" + e.getClass().getName()); - e.printStackTrace(System.err); return false; } } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index c3e1b034a0..8e4f8cedaf 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -27,7 +27,6 @@ *
  • {@link #initTrustAllSSLContext()} — trust-all mode for DISABLED verification
  • * */ -// TODO: REMOVE all "DEBUG [SSLContextFactory]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class SSLContextFactory { @@ -49,19 +48,15 @@ public SSLContext getSslContext() { * @return factory holding the initialized SSLContext */ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { - System.out.println("DEBUG [SSLContextFactory] initSSLContext() called"); SSLContextFactory factory = new SSLContextFactory(stores); - System.out.println("DEBUG [SSLContextFactory] Initializing TrustManagerFactory with trustStore (type=" + stores.getTrustStore().getType() + ", size=" + stores.getTrustStore().size() + ")"); TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustFactory.init(stores.getTrustStore()); KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); if (stores.getKeyStore() != null) { - System.out.println("DEBUG [SSLContextFactory] Initializing KeyManagerFactory with keyStore (type=" + stores.getKeyStore().getType() + ", size=" + stores.getKeyStore().size() + ")"); keyFactory.init(stores.getKeyStore(), stores.getConf().getKeyStorePassword().toCharArray()); } else { - System.out.println("DEBUG [SSLContextFactory] No keyStore, using empty keystore for KeyManagerFactory"); KeyStore emptyKeystore = KeyStore.getInstance(KeyStore.getDefaultType()); emptyKeystore.load(null, null); keyFactory.init(emptyKeystore, null); @@ -69,7 +64,6 @@ public static SSLContextFactory initSSLContext(Stores stores) throws NoSuchAlgor factory.sslContext = SSLContext.getInstance("TLSv1.2"); factory.sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom()); - System.out.println("DEBUG [SSLContextFactory] SSLContext created successfully (protocol=" + factory.sslContext.getProtocol() + ")"); return factory; } 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 a47ec2ef2e..90a3a401f5 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 @@ -27,7 +27,6 @@ * Loads Java {@link java.security.KeyStore} instances from the filesystem * or z/OS SAF keyrings. Supports PKCS12, JKS, and {@code safkeyring://} URIs. */ -// TODO: REMOVE all "DEBUG [Stores]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class Stores { @@ -70,38 +69,21 @@ public static String formatKeyringUrl(String input) { } void init() { - System.out.println("DEBUG [Stores] init() called"); - System.out.println("DEBUG [Stores] trustStore path=" + conf.getTrustStore()); - System.out.println("DEBUG [Stores] trustStore type=" + conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] trustStore isKeyring=" + isKeyring(conf.getTrustStore())); - System.out.println("DEBUG [Stores] keyStore path=" + conf.getKeyStore()); - System.out.println("DEBUG [Stores] keyStore type=" + (conf.getKeyStore() != null ? conf.getKeyStoreType() : "")); - System.out.println("DEBUG [Stores] keyStore isKeyring=" + isKeyring(conf.getKeyStore())); - System.out.println("DEBUG [Stores] java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); try { - System.out.println("DEBUG [Stores] Calling initKeystore()..."); initKeystore(); - System.out.println("DEBUG [Stores] initKeystore() completed. trustStore set by keystore=" + (trustStore != null)); if (trustStore == null) { - System.out.println("DEBUG [Stores] Calling initTruststore()..."); initTruststore(); - System.out.println("DEBUG [Stores] initTruststore() completed successfully."); } } catch (FileNotFoundException e) { - System.err.println("DEBUG [Stores] FileNotFoundException in init(): " + e.getMessage()); - e.printStackTrace(System.err); 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) { - System.err.println("DEBUG [Stores] Exception in init(): " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); throw new StoresNotInitializeException(e.getMessage()); } } private void initTruststore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if (conf.getTrustStore() == null) { - System.out.println("DEBUG [Stores] initTruststore: No truststore specified, will use empty."); try { this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); this.trustStore.load(null, null); @@ -111,48 +93,31 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl return; } if (isKeyring(conf.getTrustStore())) { - System.out.println("DEBUG [Stores] initTruststore: Detected SAF keyring URI: " + conf.getTrustStore()); - String formatted = formatKeyringUrl(conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: Formatted keyring URL: " + formatted); - System.out.println("DEBUG [Stores] initTruststore: Calling keyRingUrl()..."); URL url = keyRingUrl(conf.getTrustStore()); - System.out.println("DEBUG [Stores] initTruststore: URL object created: " + url + " (protocol=" + url.getProtocol() + ")"); - System.out.println("DEBUG [Stores] initTruststore: Calling url.openStream()..."); try (InputStream trustStoreIStream = url.openStream()) { - System.out.println("DEBUG [Stores] initTruststore: openStream() succeeded. Loading keystore type=" + conf.getTrustStoreType()); this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from keyring. Aliases count=" + trustStore.size()); } } else { - System.out.println("DEBUG [Stores] initTruststore: Loading from file: " + conf.getTrustStore()); try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); - System.out.println("DEBUG [Stores] initTruststore: Truststore loaded from file. Aliases count=" + trustStore.size()); } } } private void initKeystore() throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { if (conf.getKeyStore() == null) { - System.out.println("DEBUG [Stores] initKeystore: No keystore specified, skipping."); return; } if (isKeyring(conf.getKeyStore())) { - System.out.println("DEBUG [Stores] initKeystore: Detected SAF keyring URI: " + conf.getKeyStore()); try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { this.keyStore = readKeyStore(keyringIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); this.trustStore = this.keyStore; - System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from keyring. Aliases count=" + keyStore.size()); } catch (Exception e) { - System.err.println("DEBUG [Stores] initKeystore: Exception loading keyring: " + e.getClass().getName() + ": " + e.getMessage()); - e.printStackTrace(System.err); throw new StoresNotInitializeException(e.getMessage()); } } else { - System.out.println("DEBUG [Stores] initKeystore: Loading from file: " + conf.getKeyStore()); try (InputStream keyStoreIStream = new FileInputStream(conf.getKeyStore())) { this.keyStore = readKeyStore(keyStoreIStream, conf.getKeyStorePassword().toCharArray(), conf.getKeyStoreType()); - System.out.println("DEBUG [Stores] initKeystore: Keystore loaded from file. Aliases count=" + keyStore.size()); } } } @@ -181,21 +146,15 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { + ". Make sure you use format safkeyring://userId/keyRing"); } String formatted = formatKeyringUrl(uri); - System.out.println("DEBUG [Stores] keyRingUrl: Creating URL from: " + formatted); try { - URL url = new URL(formatted); - System.out.println("DEBUG [Stores] keyRingUrl: URL created successfully. protocol=" + url.getProtocol() - + " host=" + url.getHost() + " path=" + url.getPath()); - return url; + return new URL(formatted); } catch (MalformedURLException e) { - System.out.println("DEBUG [Stores] keyRingUrl: Standard URL creation failed, trying explicit handler lookup..."); URL fallbackUrl = createUrlWithExplicitHandler(formatted); if (fallbackUrl != null) { - System.out.println("DEBUG [Stores] keyRingUrl: URL created via explicit handler. protocol=" + fallbackUrl.getProtocol()); return fallbackUrl; } - System.err.println("DEBUG [Stores] keyRingUrl: MalformedURLException for '" + formatted + "': " + e.getMessage()); - System.err.println("DEBUG [Stores] keyRingUrl: java.protocol.handler.pkgs=" + System.getProperty("java.protocol.handler.pkgs", "")); + System.err.println("ERROR: Unknown protocol in '" + formatted + "': " + e.getMessage()); + System.err.println("Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); throw e; } } @@ -206,17 +165,13 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { * {@code ibm.crypto.zsecurity} and {@code ibm.crypto.hdwrcca}. * *

    If the modules are resolved (via {@code --add-modules}), the standard - * {@code new URL()} works. This fallback handles the case where the module IS - * resolved but the ServiceLoader lookup failed, or where we can reflectively - * instantiate the provider and obtain a handler.

    + * {@code new URL()} constructor works via ServiceLoader. This fallback handles + * the unlikely case where the module IS resolved but the automatic ServiceLoader + * discovery failed, by reflectively instantiating the SPI provider directly.

    */ private static URL createUrlWithExplicitHandler(String formatted) { - // Extract protocol from the formatted URI (e.g., "safkeyring", "safkeyringjce", etc.) String protocol = formatted.substring(0, formatted.indexOf(':')); - System.out.println("DEBUG [Stores] createUrlWithExplicitHandler: protocol='" + protocol + "' formatted='" + formatted + "'"); - // Try 1: Use the URLStreamHandlerProvider SPI classes directly (Java 17/21 on z/OS) - // These are the actual provider classes found in ibm.crypto.zsecurity and ibm.crypto.hdwrcca String[] spiProviderClasses = { "com.ibm.crypto.zsecurity.provider.safkeyring.Provider", "com.ibm.crypto.hdwrCCA.provider.safkeyring.Provider" @@ -228,68 +183,23 @@ private static URL createUrlWithExplicitHandler(String formatted) { Thread.currentThread().getContextClassLoader(), Stores.class.getClassLoader() }; - String[] loaderNames = {"SystemClassLoader", "PlatformClassLoader", "ContextClassLoader", "StoresClassLoader"}; - System.out.println("DEBUG [Stores] createUrlWithExplicitHandler: Trying SPI providers..."); for (String providerClassName : spiProviderClasses) { - for (int i = 0; i < loaders.length; i++) { - ClassLoader loader = loaders[i]; - if (loader == null) { - System.out.println("DEBUG [Stores] Skipping null loader: " + loaderNames[i]); - continue; - } + for (ClassLoader loader : loaders) { + if (loader == null) continue; try { - System.out.println("DEBUG [Stores] Trying SPI class=" + providerClassName + " loader=" + loaderNames[i] + " (" + loader.getClass().getName() + ")"); Class cls = Class.forName(providerClassName, true, loader); - System.out.println("DEBUG [Stores] Class loaded successfully: " + cls.getName()); java.net.spi.URLStreamHandlerProvider provider = (java.net.spi.URLStreamHandlerProvider) cls.getDeclaredConstructor().newInstance(); - System.out.println("DEBUG [Stores] Provider instantiated: " + provider.getClass().getName()); java.net.URLStreamHandler handler = provider.createURLStreamHandler(protocol); if (handler != null) { - System.out.println("DEBUG [Stores] SUCCESS: Handler obtained for protocol '" + protocol + "' from " + providerClassName + " via " + loaderNames[i]); return new URL(null, formatted, handler); - } else { - System.out.println("DEBUG [Stores] Provider returned null handler for protocol '" + protocol + "' (not supported by this provider)"); } - } catch (ClassNotFoundException e) { - System.out.println("DEBUG [Stores] ClassNotFoundException: " + providerClassName + " not found via " + loaderNames[i]); - } catch (Exception e) { - System.out.println("DEBUG [Stores] Exception: " + e.getClass().getName() + ": " + e.getMessage()); + } catch (Exception ignored) { + // Try next combination } } } - - // Try 2: Legacy approach - look for Handler class directly (Java 8 style) - System.out.println("DEBUG [Stores] createUrlWithExplicitHandler: SPI providers not found. Trying legacy Handler classes..."); - String[] handlerPackages = { - "com.ibm.crypto.provider", - "com.ibm.crypto.zsecurity.provider", - "com.ibm.crypto.hdwrCCA.provider" - }; - - for (String pkg : handlerPackages) { - String className = pkg + "." + protocol + ".Handler"; - for (int i = 0; i < loaders.length; i++) { - ClassLoader loader = loaders[i]; - if (loader == null) continue; - try { - System.out.println("DEBUG [Stores] Trying Handler class=" + className + " loader=" + loaderNames[i]); - Class cls = Class.forName(className, true, loader); - java.net.URLStreamHandler handler = (java.net.URLStreamHandler) cls.getDeclaredConstructor().newInstance(); - System.out.println("DEBUG [Stores] SUCCESS: Handler instantiated: " + className + " via " + loaderNames[i]); - return new URL(null, formatted, handler); - } catch (ClassNotFoundException e) { - System.out.println("DEBUG [Stores] ClassNotFoundException: " + className + " not found via " + loaderNames[i]); - } catch (Exception e) { - System.out.println("DEBUG [Stores] Exception: " + e.getClass().getName() + ": " + e.getMessage()); - } - } - } - - System.err.println("DEBUG [Stores] createUrlWithExplicitHandler: FAILED - Could not find handler for protocol '" + protocol + "' in any known location."); - System.err.println("DEBUG [Stores] createUrlWithExplicitHandler: Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); - System.err.println("DEBUG [Stores] createUrlWithExplicitHandler: Resolved modules can be checked with: java --show-module-resolution --add-modules ibm.crypto.zsecurity -version"); return null; } } 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 84f6380145..24c04d59fe 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 @@ -21,7 +21,6 @@ * *

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

    */ -// TODO: REMOVE all "DEBUG [ZosmfJwtCheck]" logging lines after SAF keyring issue is resolved @SuppressWarnings("squid:S106") public class ZosmfJwtCheck { @@ -30,18 +29,6 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { - System.out.println("DEBUG [ZosmfJwtCheck] ===== zosmf-jwt-check starting ====="); - System.out.println("DEBUG [ZosmfJwtCheck] Java version: " + System.getProperty("java.version")); - System.out.println("DEBUG [ZosmfJwtCheck] Java vendor: " + System.getProperty("java.vendor")); - System.out.println("DEBUG [ZosmfJwtCheck] OS name: " + System.getProperty("os.name")); - System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (BEFORE ensureSafkeyringHandler): " - + System.getProperty("java.protocol.handler.pkgs", "")); - - ensureSafkeyringHandler(); - - System.out.println("DEBUG [ZosmfJwtCheck] java.protocol.handler.pkgs (AFTER ensureSafkeyringHandler): " - + System.getProperty("java.protocol.handler.pkgs", "")); - try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); @@ -53,17 +40,6 @@ public static int mainWithExitCode(String[] args) { return 8; } - System.out.println("DEBUG [ZosmfJwtCheck] Parsed config:"); - System.out.println("DEBUG [ZosmfJwtCheck] zosmf-host=" + conf.getZosmfHost()); - System.out.println("DEBUG [ZosmfJwtCheck] zosmf-port=" + conf.getZosmfPort()); - System.out.println("DEBUG [ZosmfJwtCheck] scheme=" + conf.getScheme()); - System.out.println("DEBUG [ZosmfJwtCheck] verify-certificates=" + conf.getVerifyCertificates()); - System.out.println("DEBUG [ZosmfJwtCheck] truststore-file=" + conf.getTrustStore()); - System.out.println("DEBUG [ZosmfJwtCheck] truststore-type=" + conf.getTrustStoreType()); - System.out.println("DEBUG [ZosmfJwtCheck] truststore-isKeyring=" + Stores.isKeyring(conf.getTrustStore())); - System.out.println("DEBUG [ZosmfJwtCheck] keystore-file=" + conf.getKeyStore()); - System.out.println("DEBUG [ZosmfJwtCheck] keystore-type=" + conf.getKeyStoreType()); - validateConfig(conf); HttpClientWrapper httpClient; @@ -75,11 +51,8 @@ public static int mainWithExitCode(String[] args) { HostnameVerifier noopVerifier = (hostname, session) -> true; httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); } else { - System.out.println("DEBUG [ZosmfJwtCheck] Creating Stores (truststore/keystore)..."); Stores stores = new Stores(conf); - System.out.println("DEBUG [ZosmfJwtCheck] Stores created successfully. Building SSLContext..."); SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); - System.out.println("DEBUG [ZosmfJwtCheck] SSLContext created successfully."); HostnameVerifier hostnameVerifier; if (VERIFY_NONSTRICT.equals(verifyMode)) { @@ -95,13 +68,9 @@ public static int mainWithExitCode(String[] args) { } JwkEndpointChecker checker = new JwkEndpointChecker(httpClient, conf); - System.out.println("DEBUG [ZosmfJwtCheck] Calling JwkEndpointChecker.check()..."); - boolean success = checker.check(); - System.out.println("DEBUG [ZosmfJwtCheck] JwkEndpointChecker.check() returned: " + success); - return success ? 0 : 4; + return checker.check() ? 0 : 4; } catch (Exception e) { System.err.println("ERROR: " + e.getMessage()); - System.err.println("DEBUG [ZosmfJwtCheck] Exception class: " + e.getClass().getName()); e.printStackTrace(System.err); return 4; } @@ -129,42 +98,6 @@ static void validateConfig(ZosmfJwtCheckConf conf) { } } - /** - * Attempts to register the IBM SAF keyring URL protocol handler via the legacy - * {@code java.protocol.handler.pkgs} system property. - * - *

    Note: On IBM Java 17/21 (z/OS), the safkeyring handler is registered - * via the {@code java.net.spi.URLStreamHandlerProvider} SPI in module - * {@code ibm.crypto.zsecurity}, so this property has no effect. The real fix is - * to launch the JVM with {@code --add-modules ibm.crypto.zsecurity}. - * This method is retained only as a best-effort fallback for IBM Java 8.

    - */ - static void ensureSafkeyringHandler() { - String[] packagePrefixes = { - "com.ibm.crypto.zsecurity.provider", - "com.ibm.crypto.hdwrCCA.provider" - }; - String existing = System.getProperty("java.protocol.handler.pkgs", ""); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: existing java.protocol.handler.pkgs='" + existing + "'"); - StringBuilder sb = new StringBuilder(existing); - for (String prefix : packagePrefixes) { - if (!existing.contains(prefix)) { - if (sb.length() > 0) { - sb.append('|'); - } - sb.append(prefix); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: Adding package prefix: " + prefix); - } else { - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: Already present: " + prefix); - } - } - String finalValue = sb.toString(); - System.setProperty("java.protocol.handler.pkgs", finalValue); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: Final java.protocol.handler.pkgs='" + finalValue + "'"); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: NOTE - On Java 17/21, this property has no effect."); - System.out.println("DEBUG [ZosmfJwtCheck] ensureSafkeyringHandler: The safkeyring handler is loaded via SPI from module ibm.crypto.zsecurity (requires --add-modules)."); - } - public static void main(String[] args) { System.exit(mainWithExitCode(args)); } From 246af1a9f972e8b50821653a99c4b87036e38d8a Mon Sep 17 00:00:00 2001 From: "Signed-off-by: hrishikesh-nalawade" Date: Mon, 25 May 2026 23:51:42 +0530 Subject: [PATCH 21/28] fallback with diagnostics Signed-off-by: hrishikesh-nalawade --- .../src/main/java/org/zowe/apiml/Stores.java | 60 +++++++++++++++---- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 43 +++++++++++++ 2 files changed, 93 insertions(+), 10 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 90a3a401f5..64d3de869f 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 @@ -147,10 +147,15 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { } String formatted = formatKeyringUrl(uri); try { - return new URL(formatted); + URL url = new URL(formatted); + System.out.println("DIAG: new URL('" + formatted + "') succeeded via java.protocol.handler.pkgs"); + return url; } catch (MalformedURLException e) { + System.out.println("DIAG: new URL('" + formatted + "') failed: " + e.getMessage()); + System.out.println("DIAG: Attempting explicit handler fallback..."); URL fallbackUrl = createUrlWithExplicitHandler(formatted); if (fallbackUrl != null) { + System.out.println("DIAG: Fallback succeeded for '" + formatted + "'"); return fallbackUrl; } System.err.println("ERROR: Unknown protocol in '" + formatted + "': " + e.getMessage()); @@ -160,23 +165,23 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { } /** - * Fallback for IBM Java 17/21+ on z/OS where the safkeyring handler is registered - * via the {@code java.net.spi.URLStreamHandlerProvider} SPI mechanism in modules + * Fallback for IBM Java 17/21+ on z/OS where the safkeyring handler may be + * registered via the {@code java.net.spi.URLStreamHandlerProvider} SPI or + * via a legacy {@code ..Handler} class in modules * {@code ibm.crypto.zsecurity} and {@code ibm.crypto.hdwrcca}. * - *

    If the modules are resolved (via {@code --add-modules}), the standard - * {@code new URL()} constructor works via ServiceLoader. This fallback handles - * the unlikely case where the module IS resolved but the automatic ServiceLoader - * discovery failed, by reflectively instantiating the SPI provider directly.

    + *

    Requires the modules to be resolved via {@code --add-modules}.

    */ private static URL createUrlWithExplicitHandler(String formatted) { String protocol = formatted.substring(0, formatted.indexOf(':')); + // Try 1: SPI URLStreamHandlerProvider classes String[] spiProviderClasses = { "com.ibm.crypto.zsecurity.provider.safkeyring.Provider", "com.ibm.crypto.hdwrCCA.provider.safkeyring.Provider" }; + String[] loaderNames = {"SystemCL", "PlatformCL", "ContextCL", "StoresCL"}; ClassLoader[] loaders = { ClassLoader.getSystemClassLoader(), ClassLoader.getPlatformClassLoader(), @@ -184,8 +189,11 @@ private static URL createUrlWithExplicitHandler(String formatted) { Stores.class.getClassLoader() }; + System.out.println("DIAG: createUrlWithExplicitHandler: protocol='" + protocol + "'"); + System.out.println("DIAG: Trying SPI providers..."); for (String providerClassName : spiProviderClasses) { - for (ClassLoader loader : loaders) { + for (int i = 0; i < loaders.length; i++) { + ClassLoader loader = loaders[i]; if (loader == null) continue; try { Class cls = Class.forName(providerClassName, true, loader); @@ -193,13 +201,45 @@ private static URL createUrlWithExplicitHandler(String formatted) { (java.net.spi.URLStreamHandlerProvider) cls.getDeclaredConstructor().newInstance(); java.net.URLStreamHandler handler = provider.createURLStreamHandler(protocol); if (handler != null) { + System.out.println("DIAG: SUCCESS via SPI: " + providerClassName + " [" + loaderNames[i] + "]"); return new URL(null, formatted, handler); + } else { + System.out.println("DIAG: SPI " + providerClassName + " [" + loaderNames[i] + "] returned null handler"); } - } catch (Exception ignored) { - // Try next combination + } catch (ClassNotFoundException e) { + System.out.println("DIAG: SPI " + providerClassName + " [" + loaderNames[i] + "] ClassNotFound"); + } catch (Exception e) { + System.out.println("DIAG: SPI " + providerClassName + " [" + loaderNames[i] + "] " + e.getClass().getSimpleName() + ": " + e.getMessage()); } } } + + // Try 2: Legacy Handler classes (e.g. .safkeyring.Handler) + String[] handlerPackages = { + "com.ibm.crypto.zsecurity.provider", + "com.ibm.crypto.hdwrCCA.provider" + }; + + System.out.println("DIAG: Trying legacy Handler classes..."); + for (String pkg : handlerPackages) { + String className = pkg + "." + protocol + ".Handler"; + for (int i = 0; i < loaders.length; i++) { + ClassLoader loader = loaders[i]; + if (loader == null) continue; + try { + Class cls = Class.forName(className, true, loader); + java.net.URLStreamHandler handler = (java.net.URLStreamHandler) cls.getDeclaredConstructor().newInstance(); + System.out.println("DIAG: SUCCESS via Handler: " + className + " [" + loaderNames[i] + "]"); + return new URL(null, formatted, handler); + } catch (ClassNotFoundException e) { + System.out.println("DIAG: Handler " + className + " [" + loaderNames[i] + "] ClassNotFound"); + } catch (Exception e) { + System.out.println("DIAG: Handler " + className + " [" + loaderNames[i] + "] " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + } + + System.out.println("DIAG: createUrlWithExplicitHandler: ALL attempts failed"); return null; } } 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 24c04d59fe..c13a61e1e7 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,21 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { + System.out.println("DIAG: Java=" + System.getProperty("java.version") + " Vendor=" + System.getProperty("java.vendor")); + System.out.println("DIAG: java.protocol.handler.pkgs (before)=" + System.getProperty("java.protocol.handler.pkgs", "")); + + ensureSafkeyringHandler(); + + System.out.println("DIAG: java.protocol.handler.pkgs (after)=" + System.getProperty("java.protocol.handler.pkgs", "")); + + // Check if ibm.crypto.zsecurity module is resolved (confirms --add-modules worked) + try { + Class testClass = Class.forName("com.ibm.crypto.zsecurity.provider.safkeyring.Provider"); + System.out.println("DIAG: Module ibm.crypto.zsecurity IS resolved (class found: " + testClass.getName() + ")"); + } catch (ClassNotFoundException e) { + System.out.println("DIAG: Module ibm.crypto.zsecurity NOT resolved (ClassNotFoundException). --add-modules may be missing."); + } + try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); @@ -98,6 +113,34 @@ static void validateConfig(ZosmfJwtCheckConf conf) { } } + /** + * Registers IBM SAF keyring URL protocol handler packages via the + * {@code java.protocol.handler.pkgs} system property. + * + *

    On IBM Java 17/21 (z/OS), this property works in conjunction with + * {@code --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca} to enable + * the {@code safkeyring://} URL protocol. The {@code --add-modules} flag + * resolves the module (making classes accessible), while this property tells + * the {@link java.net.URL} class which packages to search for the handler.

    + */ + static void ensureSafkeyringHandler() { + String[] packagePrefixes = { + "com.ibm.crypto.zsecurity.provider", + "com.ibm.crypto.hdwrCCA.provider" + }; + String existing = System.getProperty("java.protocol.handler.pkgs", ""); + StringBuilder sb = new StringBuilder(existing); + for (String prefix : packagePrefixes) { + if (!existing.contains(prefix)) { + if (sb.length() > 0) { + sb.append('|'); + } + sb.append(prefix); + } + } + System.setProperty("java.protocol.handler.pkgs", sb.toString()); + } + public static void main(String[] args) { System.exit(mainWithExitCode(args)); } From 5538f73a4d5c236ef83a8e65fa4c8f9e1c55b01d Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Tue, 26 May 2026 01:44:14 +0530 Subject: [PATCH 22/28] Fix after testing fallbacks Signed-off-by: hrishikesh-nalawade --- zosmf-jwt-check/README.md | 10 +- .../src/main/java/org/zowe/apiml/Stores.java | 91 +------------------ .../java/org/zowe/apiml/ZosmfJwtCheck.java | 13 --- 3 files changed, 7 insertions(+), 107 deletions(-) diff --git a/zosmf-jwt-check/README.md b/zosmf-jwt-check/README.md index ae7a9cbcb0..b72ce55b2c 100644 --- a/zosmf-jwt-check/README.md +++ b/zosmf-jwt-check/README.md @@ -307,7 +307,7 @@ java -jar zosmf-jwt-check-.jar --zosmf-host nonexistent.host --zosmf-po 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 IBM crypto modules to the JVM module graph: ```bash -java --add-modules ibm.crypto.zsecurity \ +java --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca \ -jar zosmf-jwt-check-.jar \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ @@ -316,7 +316,7 @@ java --add-modules ibm.crypto.zsecurity \ --truststore-type JCERACFKS ``` -> **Note:** On IBM Java 17/21, the safkeyring URL protocol handler is provided via the -> `java.net.spi.URLStreamHandlerProvider` SPI in module `ibm.crypto.zsecurity`. -> The legacy `-Djava.protocol.handler.pkgs=com.ibm.crypto.provider` property -> has no effect on Java 17+ and should not be used. +> **Note:** On IBM Java 17/21, the `--add-modules` flag resolves the IBM crypto modules, +> making the safkeyring URL protocol handler classes accessible. The tool also sets +> `java.protocol.handler.pkgs` internally to register the handler packages with +> the `URL` class. Both mechanisms work together to enable `safkeyring://` URLs. 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 64d3de869f..6d78e27f49 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 @@ -147,99 +147,12 @@ public static URL keyRingUrl(String uri) throws MalformedURLException { } String formatted = formatKeyringUrl(uri); try { - URL url = new URL(formatted); - System.out.println("DIAG: new URL('" + formatted + "') succeeded via java.protocol.handler.pkgs"); - return url; + return new URL(formatted); } catch (MalformedURLException e) { - System.out.println("DIAG: new URL('" + formatted + "') failed: " + e.getMessage()); - System.out.println("DIAG: Attempting explicit handler fallback..."); - URL fallbackUrl = createUrlWithExplicitHandler(formatted); - if (fallbackUrl != null) { - System.out.println("DIAG: Fallback succeeded for '" + formatted + "'"); - return fallbackUrl; - } System.err.println("ERROR: Unknown protocol in '" + formatted + "': " + e.getMessage()); System.err.println("Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); + System.err.println("And that ensureSafkeyringHandler() has been called to set java.protocol.handler.pkgs"); throw e; } } - - /** - * Fallback for IBM Java 17/21+ on z/OS where the safkeyring handler may be - * registered via the {@code java.net.spi.URLStreamHandlerProvider} SPI or - * via a legacy {@code ..Handler} class in modules - * {@code ibm.crypto.zsecurity} and {@code ibm.crypto.hdwrcca}. - * - *

    Requires the modules to be resolved via {@code --add-modules}.

    - */ - private static URL createUrlWithExplicitHandler(String formatted) { - String protocol = formatted.substring(0, formatted.indexOf(':')); - - // Try 1: SPI URLStreamHandlerProvider classes - String[] spiProviderClasses = { - "com.ibm.crypto.zsecurity.provider.safkeyring.Provider", - "com.ibm.crypto.hdwrCCA.provider.safkeyring.Provider" - }; - - String[] loaderNames = {"SystemCL", "PlatformCL", "ContextCL", "StoresCL"}; - ClassLoader[] loaders = { - ClassLoader.getSystemClassLoader(), - ClassLoader.getPlatformClassLoader(), - Thread.currentThread().getContextClassLoader(), - Stores.class.getClassLoader() - }; - - System.out.println("DIAG: createUrlWithExplicitHandler: protocol='" + protocol + "'"); - System.out.println("DIAG: Trying SPI providers..."); - for (String providerClassName : spiProviderClasses) { - for (int i = 0; i < loaders.length; i++) { - ClassLoader loader = loaders[i]; - if (loader == null) continue; - try { - Class cls = Class.forName(providerClassName, true, loader); - java.net.spi.URLStreamHandlerProvider provider = - (java.net.spi.URLStreamHandlerProvider) cls.getDeclaredConstructor().newInstance(); - java.net.URLStreamHandler handler = provider.createURLStreamHandler(protocol); - if (handler != null) { - System.out.println("DIAG: SUCCESS via SPI: " + providerClassName + " [" + loaderNames[i] + "]"); - return new URL(null, formatted, handler); - } else { - System.out.println("DIAG: SPI " + providerClassName + " [" + loaderNames[i] + "] returned null handler"); - } - } catch (ClassNotFoundException e) { - System.out.println("DIAG: SPI " + providerClassName + " [" + loaderNames[i] + "] ClassNotFound"); - } catch (Exception e) { - System.out.println("DIAG: SPI " + providerClassName + " [" + loaderNames[i] + "] " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - } - } - - // Try 2: Legacy Handler classes (e.g. .safkeyring.Handler) - String[] handlerPackages = { - "com.ibm.crypto.zsecurity.provider", - "com.ibm.crypto.hdwrCCA.provider" - }; - - System.out.println("DIAG: Trying legacy Handler classes..."); - for (String pkg : handlerPackages) { - String className = pkg + "." + protocol + ".Handler"; - for (int i = 0; i < loaders.length; i++) { - ClassLoader loader = loaders[i]; - if (loader == null) continue; - try { - Class cls = Class.forName(className, true, loader); - java.net.URLStreamHandler handler = (java.net.URLStreamHandler) cls.getDeclaredConstructor().newInstance(); - System.out.println("DIAG: SUCCESS via Handler: " + className + " [" + loaderNames[i] + "]"); - return new URL(null, formatted, handler); - } catch (ClassNotFoundException e) { - System.out.println("DIAG: Handler " + className + " [" + loaderNames[i] + "] ClassNotFound"); - } catch (Exception e) { - System.out.println("DIAG: Handler " + className + " [" + loaderNames[i] + "] " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - } - } - - System.out.println("DIAG: createUrlWithExplicitHandler: ALL attempts failed"); - return null; - } } 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 c13a61e1e7..6fb4619f3a 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,21 +29,8 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { - System.out.println("DIAG: Java=" + System.getProperty("java.version") + " Vendor=" + System.getProperty("java.vendor")); - System.out.println("DIAG: java.protocol.handler.pkgs (before)=" + System.getProperty("java.protocol.handler.pkgs", "")); - ensureSafkeyringHandler(); - System.out.println("DIAG: java.protocol.handler.pkgs (after)=" + System.getProperty("java.protocol.handler.pkgs", "")); - - // Check if ibm.crypto.zsecurity module is resolved (confirms --add-modules worked) - try { - Class testClass = Class.forName("com.ibm.crypto.zsecurity.provider.safkeyring.Provider"); - System.out.println("DIAG: Module ibm.crypto.zsecurity IS resolved (class found: " + testClass.getName() + ")"); - } catch (ClassNotFoundException e) { - System.out.println("DIAG: Module ibm.crypto.zsecurity NOT resolved (ClassNotFoundException). --add-modules may be missing."); - } - try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); CommandLine cmd = new CommandLine(conf); From f1a5b2d223328ed8bcbb9af7b857b2b67fda2bf4 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Fri, 29 May 2026 01:18:55 +0530 Subject: [PATCH 23/28] Single Preflight check JAR code Signed-off-by: hrishikesh-nalawade --- certificate-analyser/README.md | 154 ++++++++++++++++++ certificate-analyser/build.gradle | 2 + .../main/java/org/zowe/apiml/Analyser.java | 9 + .../java/org/zowe/apiml/AnalyserTest.java | 15 +- zosmf-jwt-check/README.md | 85 ++++------ zosmf-jwt-check/build.gradle | 11 +- .../org/zowe/apiml/SSLContextFactory.java | 8 +- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 2 +- .../apiml/{Stores.java => ZosmfStores.java} | 4 +- 9 files changed, 218 insertions(+), 72 deletions(-) rename zosmf-jwt-check/src/main/java/org/zowe/apiml/{Stores.java => ZosmfStores.java} (98%) diff --git a/certificate-analyser/README.md b/certificate-analyser/README.md index bec9673a7e..691fefb0e3 100644 --- a/certificate-analyser/README.md +++ b/certificate-analyser/README.md @@ -1,5 +1,40 @@ ## Certificate validation tool +### Build + +From the root of the `api-layer` repository: + +```bash +./gradlew :certificate-analyser:build +``` + +On Windows: + +```powershell +.\gradlew :certificate-analyser:build +``` + +The fat JAR (with all dependencies bundled, including z/OSMF JWT Check) will be generated at: + +``` +certificate-analyser/build/libs/certificate-analyser-.jar +``` + +#### Build Architecture + +The `certificate-analyser` module produces a **single unified fat JAR** that bundles: + +1. All certificate-analyser classes (certificate validation, local/remote handshake, local verification) +2. All `zosmf-jwt-check` classes (z/OSMF JWK endpoint connectivity check) +3. All shared dependencies (picocli) + +This is achieved through the following Gradle configuration: + +- `certificate-analyser/build.gradle` declares `implementation project(':zosmf-jwt-check')` as a dependency +- The `jar` task uses `from { configurations.runtimeClasspath.collect { ... } }` to bundle all runtime dependencies into the fat JAR +- `zosmf-jwt-check/build.gradle` produces a thin JAR (no `Main-Class` manifest, no fat-jar bundling) that is consumed only as a build dependency +- The `zosmf-jwt-check` module **does not produce a standalone runnable JAR** — its functionality is accessed exclusively through the certificate-analyser JAR via the `--zosmf-jwt-check` CLI flag + ### Usage java -jar certificate-analyser-.jar --help @@ -61,3 +96,122 @@ java --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca \ ### Possible issues Keystore/truststore is owned by different user - permission error. Temporarily Change read permission to all. + +--- + +## z/OSMF JWT Check (embedded) + +This JAR also includes the **z/OSMF JWT Check** tool, which verifies connectivity to the z/OSMF JWK endpoint (`/jwt/ibm/api/zOSMFBuilder/jwk`). It helps diagnose configuration issues such as incorrect hostnames, unreachable ports, missing certificates, or misconfigured z/OSMF. + +To use the z/OSMF JWT check functionality, pass `--zosmf-jwt-check` as the **first** argument. All subsequent arguments are passed to the z/OSMF JWT check tool. + +### z/OSMF JWT Check — Help + +```bash +java -jar certificate-analyser-.jar --zosmf-jwt-check --help +``` + +### z/OSMF JWT Check — CLI Flags + +#### 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` | + +#### Conditionally Required Flags + +These flags are required when `--scheme=https` (the default) and `--verify-certificates` is **not** `DISABLED`: + +| Flag | Description | +|------|-------------| +| `--truststore-file` | Path to the truststore file containing the z/OSMF CA certificate | +| `--truststore-password` | Password for the truststore. If specified without a value, you will be prompted interactively | + +#### 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-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 | +| `-v`, `--verbose` | | Print the response body from the endpoint | +| `-h`, `--help` | | Display usage help and exit | + +### Certificate Verification Modes + +- **STRICT** (default) — Full certificate chain validation + hostname verification. Truststore required. +- **NONSTRICT** — Certificate chain validated, hostname verification skipped. Truststore required. +- **DISABLED** — No certificate validation. No truststore required. **Do not use in production.** + +### 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 | + +### z/OSMF JWT Check — Examples + +**Quick test (DISABLED mode — no truststore needed):** + +```bash +java -jar certificate-analyser-.jar --zosmf-jwt-check \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --verify-certificates DISABLED +``` + +**STRICT mode (full certificate verification):** + +```bash +java -jar certificate-analyser-.jar --zosmf-jwt-check \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore-file /path/to/truststore.p12 \ + --truststore-password changeit +``` + +**NONSTRICT mode (skip hostname check):** + +```bash +java -jar certificate-analyser-.jar --zosmf-jwt-check \ + --zosmf-host 10.0.0.50 \ + --zosmf-port 11443 \ + --truststore-file /path/to/truststore.p12 \ + --truststore-password password \ + --verify-certificates NONSTRICT +``` + +**HTTP mode (no SSL):** + +```bash +java -jar certificate-analyser-.jar --zosmf-jwt-check \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 80 \ + --scheme http +``` + +### SAF Keyrings (z/OSMF JWT Check) + +On z/OS, if you are using SAF keyrings, provide the keyring path in `safkeyring://` format and add the JVM protocol handler: + +```bash +java --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca \ + -jar certificate-analyser-.jar --zosmf-jwt-check \ + --zosmf-host myzosmf.example.com \ + --zosmf-port 11443 \ + --truststore-file safkeyring://IZUSVR/ZoweKeyring \ + --truststore-password password \ + --truststore-type JCERACFKS +``` + +> **Note:** On IBM Java 17/21, the `--add-modules` flag resolves the IBM crypto modules, +> making the safkeyring URL protocol handler classes accessible. The tool also sets +> `java.protocol.handler.pkgs` internally to register the handler packages with +> the `URL` class. Both mechanisms work together to enable `safkeyring://` URLs. diff --git a/certificate-analyser/build.gradle b/certificate-analyser/build.gradle index 7080e138c0..d68085d549 100644 --- a/certificate-analyser/build.gradle +++ b/certificate-analyser/build.gradle @@ -3,6 +3,7 @@ plugins { } dependencies { + implementation project(':zosmf-jwt-check') implementation libs.picocli annotationProcessor libs.picocli.codegen @@ -16,6 +17,7 @@ compileJava { } jar { + dependsOn ':zosmf-jwt-check:jar' manifest { attributes( 'Main-Class': 'org.zowe.apiml.Analyser' diff --git a/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java b/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java index 60c5f96c8c..6a35869898 100644 --- a/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java +++ b/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java @@ -13,13 +13,22 @@ import picocli.CommandLine; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @SuppressWarnings("squid:S106") //ignoring the System.out System.err warnings public class Analyser { + private static final String ZOSMF_JWT_CHECK_FLAG = "--zosmf-jwt-check"; + public static int mainWithExitCode(String[] args) { ensureSafkeyringHandler(); + + if (args.length > 0 && ZOSMF_JWT_CHECK_FLAG.equals(args[0])) { + String[] remainingArgs = Arrays.copyOfRange(args, 1, args.length); + return ZosmfJwtCheck.mainWithExitCode(remainingArgs); + } + try { ApimlConf conf = new ApimlConf(); CommandLine cmd = new CommandLine(conf); diff --git a/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java b/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java index 64d33b251c..96e36e0a89 100644 --- a/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java +++ b/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java @@ -59,6 +59,19 @@ void whenHelpRequested_thenHelpIsPrintedAndExitCodeIs8() { assertTrue(outputStream.toString().contains("Display a help message")); } + @Test + void whenZosmfJwtCheckFlagPassed_thenDelegatesToZosmfJwtCheck() { + String[] args = {"--zosmf-jwt-check", "--help"}; + assertEquals(8, Analyser.mainWithExitCode(args)); + assertTrue(outputStream.toString().contains("z/OSMF JWT Check")); + } + + @Test + void whenZosmfJwtCheckFlagWithNoArgs_thenReturnsExitCode4() { + String[] args = {"--zosmf-jwt-check"}; + assertEquals(4, Analyser.mainWithExitCode(args)); + } + @Test void whenNoRemoteUrlProvided_thenMessageIsPrinted() { String[] args = {}; @@ -149,4 +162,4 @@ void whenCalledViaMainWithExitCode_thenPropertyIsSet() { assertTrue(value.contains("com.ibm.crypto.hdwrCCA.provider")); } } -} \ No newline at end of file +} diff --git a/zosmf-jwt-check/README.md b/zosmf-jwt-check/README.md index b72ce55b2c..54a6516488 100644 --- a/zosmf-jwt-check/README.md +++ b/zosmf-jwt-check/README.md @@ -2,11 +2,13 @@ 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`. +> **Build Note:** This module no longer produces a standalone runnable JAR. Its functionality is bundled into the **certificate-analyser** fat JAR and accessed via the `--zosmf-jwt-check` CLI flag. See [Running via certificate-analyser](#running-via-certificate-analyser) below. + ## Table of Contents - [Prerequisites](#prerequisites) - [Building](#building) -- [Usage](#usage) +- [Running via certificate-analyser](#running-via-certificate-analyser) - [CLI Flags Reference](#cli-flags-reference) - [Certificate Verification Modes](#certificate-verification-modes) - [Exit Codes](#exit-codes) @@ -29,36 +31,46 @@ A Java utility that verifies connectivity to the z/OSMF JWK endpoint. This tool ## Building -From the root of the `api-layer` repository: +From the root of the `api-layer` repository, build the unified certificate-analyser JAR (which includes this module): ```bash -./gradlew :zosmf-jwt-check:build +./gradlew :certificate-analyser:build ``` On Windows: ```powershell -.\gradlew :zosmf-jwt-check:build +.\gradlew :certificate-analyser:build ``` -The fat JAR (with all dependencies bundled) will be generated at: +The fat JAR will be generated at: ``` -zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar +certificate-analyser/build/libs/certificate-analyser-.jar +``` + +To compile and test this module independently (without producing a runnable JAR): + +```bash +./gradlew :zosmf-jwt-check:test ``` -For example: `zosmf-jwt-check/build/libs/zosmf-jwt-check-3.5.12-SNAPSHOT.jar` +### Build Architecture + +This module produces a **thin JAR** (no `Main-Class`, no bundled dependencies) that is consumed as a compile/runtime dependency by `certificate-analyser`. The `certificate-analyser` fat JAR bundles all classes from this module and exposes the z/OSMF JWT check functionality via the `--zosmf-jwt-check` CLI flag. -## Usage +## Running via certificate-analyser ```bash -java -jar zosmf-jwt-check-.jar --zosmf-host --zosmf-port [options] +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check [options] ``` -**Minimal example (DISABLED mode, quickest way to test):** +All arguments after `--zosmf-jwt-check` are passed directly to the z/OSMF JWT check tool. + +**Minimal example (DISABLED mode, quickest way to test):** ```bash -java -jar zosmf-jwt-check-.jar \ +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --verify-certificates DISABLED @@ -67,7 +79,7 @@ java -jar zosmf-jwt-check-.jar \ **Full example (STRICT mode with truststore):** ```bash -java -jar zosmf-jwt-check-.jar \ +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --truststore-file /path/to/truststore.p12 \ @@ -77,7 +89,7 @@ java -jar zosmf-jwt-check-.jar \ **Display help:** ```bash -java -jar zosmf-jwt-check-.jar --help +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check --help ``` ## CLI Flags Reference @@ -192,7 +204,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 zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --verify-certificates DISABLED @@ -211,7 +223,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 zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --truststore-file /path/to/zosmf-truststore.p12 \ @@ -238,7 +250,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 zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host 10.0.0.50 \ --zosmf-port 11443 \ --truststore-file /path/to/zosmf-truststore.p12 \ @@ -259,56 +271,19 @@ SUCCESS: z/OSMF JWK endpoint exists (returned 401 Unauthorized — expected with For z/OSMF instances running on plain HTTP (uncommon): ```bash -java -jar zosmf-jwt-check/build/libs/zosmf-jwt-check-.jar \ +java -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host myzosmf.example.com \ --zosmf-port 80 \ --scheme http ``` -### 5. Validation Error Tests - -**Missing required flags:** - -```bash -# No arguments at all -java -jar zosmf-jwt-check-.jar -# Output: Missing required options: '--zosmf-host=', '--zosmf-port=' - -# Missing truststore in STRICT mode -java -jar zosmf-jwt-check-.jar --zosmf-host myhost --zosmf-port 443 -# 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-file my.p12 -# Output: ERROR: --truststore-password is required when --scheme=https and verification is not DISABLED. -``` - -**Invalid values:** - -```bash -# Invalid scheme -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 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 zosmf-jwt-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 IBM crypto modules to the JVM module graph: ```bash java --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca \ - -jar zosmf-jwt-check-.jar \ + -jar certificate-analyser/build/libs/certificate-analyser-.jar --zosmf-jwt-check \ --zosmf-host myzosmf.example.com \ --zosmf-port 11443 \ --truststore-file safkeyring://IZUSVR/ZoweKeyring \ diff --git a/zosmf-jwt-check/build.gradle b/zosmf-jwt-check/build.gradle index e95188ccdf..8a6d9a333d 100644 --- a/zosmf-jwt-check/build.gradle +++ b/zosmf-jwt-check/build.gradle @@ -15,13 +15,6 @@ compileJava { } jar { - manifest { - attributes( - 'Main-Class': 'org.zowe.apiml.ZosmfJwtCheck' - ) - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } + // Thin JAR — consumed as a dependency by certificate-analyser's fat JAR. + // No Main-Class manifest; not intended to be run standalone. } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index 8e4f8cedaf..889ac23147 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -23,17 +23,17 @@ /** * Builds a TLSv1.2 {@link SSLContext} in two modes: *
      - *
    • {@link #initSSLContext(Stores)} — normal mode using real truststore/keystore
    • + *
    • {@link #initSSLContext(ZosmfStores)} — normal mode using real truststore/keystore
    • *
    • {@link #initTrustAllSSLContext()} — trust-all mode for DISABLED verification
    • *
    */ @SuppressWarnings("squid:S106") public class SSLContextFactory { - private final Stores stores; + private final ZosmfStores stores; private SSLContext sslContext; - private SSLContextFactory(Stores stores) { + private SSLContextFactory(ZosmfStores stores) { this.stores = stores; } @@ -47,7 +47,7 @@ public SSLContext getSslContext() { * @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 { + public static SSLContextFactory initSSLContext(ZosmfStores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { SSLContextFactory factory = new SSLContextFactory(stores); TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 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 6fb4619f3a..69728b01c4 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 @@ -53,7 +53,7 @@ public static int mainWithExitCode(String[] args) { HostnameVerifier noopVerifier = (hostname, session) -> true; httpClient = new HttpClientWrapper(sslContextFactory.getSslContext(), noopVerifier); } else { - Stores stores = new Stores(conf); + ZosmfStores stores = new ZosmfStores(conf); SSLContextFactory sslContextFactory = SSLContextFactory.initSSLContext(stores); HostnameVerifier hostnameVerifier; diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java similarity index 98% rename from zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java rename to zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java index 6d78e27f49..d393a5d395 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/Stores.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java @@ -28,7 +28,7 @@ * or z/OS SAF keyrings. Supports PKCS12, JKS, and {@code safkeyring://} URIs. */ @SuppressWarnings("squid:S106") -public class Stores { +public class ZosmfStores { private static final Pattern KEYRING_PATTERN = Pattern.compile("^(safkeyring[^:]*):/{2,4}([^/]+)/([^/]+)$"); @@ -36,7 +36,7 @@ public class Stores { private KeyStore trustStore; private final ZosmfJwtCheckConfig conf; - public Stores(ZosmfJwtCheckConfig conf) { + public ZosmfStores(ZosmfJwtCheckConfig conf) { this.conf = conf; init(); } From 3a513f3eb2498d5a5d1ee33451e3dedb00db25f4 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Tue, 2 Jun 2026 15:36:11 +0530 Subject: [PATCH 24/28] refinements Signed-off-by: hrishikesh-nalawade --- .../org/zowe/apiml/JwkEndpointChecker.java | 21 +++++++----- .../org/zowe/apiml/SSLContextFactory.java | 8 ++--- .../java/org/zowe/apiml/ZosmfJwtCheck.java | 9 ++--- .../zowe/apiml/JwkEndpointCheckerTest.java | 33 +++++++------------ 4 files changed, 31 insertions(+), 40 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 40a1afd56d..84fa29c635 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 @@ -29,6 +29,9 @@ 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 String PREFIX_RESPONSE_BODY = "Response body: "; + private static final String PREFIX_URL = "URL: "; + private static final String PREFIX_DETAILS = "Details: "; private static final Pattern N_VALUE_PATTERN = Pattern.compile("\"n\"\\s*:\\s*\"([^\"]*)\""); private final HttpClientWrapper httpClient; @@ -58,12 +61,12 @@ public boolean check() { } 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()); + System.err.println(PREFIX_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()); + System.err.println(PREFIX_DETAILS + e.getMessage()); return false; } catch (SocketTimeoutException e) { System.err.println("FAILURE: Connection timed out to " + conf.getZosmfHost() + ":" + conf.getZosmfPort() + "."); @@ -76,7 +79,7 @@ public boolean check() { return false; } catch (Exception e) { System.err.println("FAILURE: Error when calling " + urlString + " verify hostname and port."); - System.err.println("Details: " + e.getMessage()); + System.err.println(PREFIX_DETAILS + e.getMessage()); return false; } } @@ -103,25 +106,25 @@ boolean evaluateResponseCode(int responseCode, String body, String urlString) { if (responseCode >= 400 && responseCode < 500) { System.err.println("FAILURE: z/OSMF JWK endpoint returned unexpected client error. HTTP " + responseCode); - System.err.println("URL: " + urlString); + System.err.println(PREFIX_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); + System.err.println(PREFIX_URL + urlString); return false; } System.err.println("FAILURE: z/OSMF JWK endpoint returned unexpected response code. HTTP " + responseCode); - System.err.println("URL: " + urlString); + System.err.println(PREFIX_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 ? "" : "")); + System.err.println(PREFIX_RESPONSE_BODY + (body == null ? "" : "")); return false; } @@ -129,7 +132,7 @@ boolean validateJwkBody(String 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); + System.err.println(PREFIX_RESPONSE_BODY + body); return false; } @@ -138,7 +141,7 @@ boolean validateJwkBody(String body) { 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); + System.err.println(PREFIX_RESPONSE_BODY + body); return false; } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java index 889ac23147..1044672d2f 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/SSLContextFactory.java @@ -30,11 +30,9 @@ @SuppressWarnings("squid:S106") public class SSLContextFactory { - private final ZosmfStores stores; private SSLContext sslContext; - private SSLContextFactory(ZosmfStores stores) { - this.stores = stores; + private SSLContextFactory() { } public SSLContext getSslContext() { @@ -48,7 +46,7 @@ public SSLContext getSslContext() { * @return factory holding the initialized SSLContext */ public static SSLContextFactory initSSLContext(ZosmfStores stores) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException, CertificateException, IOException { - SSLContextFactory factory = new SSLContextFactory(stores); + SSLContextFactory factory = new SSLContextFactory(); TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustFactory.init(stores.getTrustStore()); @@ -73,7 +71,7 @@ public static SSLContextFactory initSSLContext(ZosmfStores stores) throws NoSuch * @return factory holding the trust-all SSLContext */ public static SSLContextFactory initTrustAllSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException { - SSLContextFactory factory = new SSLContextFactory(null); + SSLContextFactory factory = new SSLContextFactory(); TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { 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 69728b01c4..e4bbcf3963 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 @@ -24,6 +24,7 @@ @SuppressWarnings("squid:S106") public class ZosmfJwtCheck { + static final String SCHEME_HTTPS = "https"; static final String VERIFY_STRICT = "STRICT"; static final String VERIFY_NONSTRICT = "NONSTRICT"; static final String VERIFY_DISABLED = "DISABLED"; @@ -45,7 +46,7 @@ public static int mainWithExitCode(String[] args) { validateConfig(conf); HttpClientWrapper httpClient; - if ("https".equalsIgnoreCase(conf.getScheme())) { + if (SCHEME_HTTPS.equalsIgnoreCase(conf.getScheme())) { String verifyMode = conf.getVerifyCertificates().toUpperCase(); if (VERIFY_DISABLED.equals(verifyMode)) { @@ -80,7 +81,7 @@ public static int mainWithExitCode(String[] args) { static void validateConfig(ZosmfJwtCheckConf conf) { String scheme = conf.getScheme(); - if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) { + if (!"http".equalsIgnoreCase(scheme) && !SCHEME_HTTPS.equalsIgnoreCase(scheme)) { throw new IllegalArgumentException("--scheme must be 'http' or 'https', got: " + scheme); } @@ -89,7 +90,7 @@ static void validateConfig(ZosmfJwtCheckConf conf) { throw new IllegalArgumentException("--verify-certificates must be STRICT, NONSTRICT, or DISABLED, got: " + conf.getVerifyCertificates()); } - if ("https".equalsIgnoreCase(scheme) && !VERIFY_DISABLED.equals(verifyMode)) { + if (SCHEME_HTTPS.equalsIgnoreCase(scheme) && !VERIFY_DISABLED.equals(verifyMode)) { if (conf.getTrustStore() == null) { 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."); @@ -119,7 +120,7 @@ static void ensureSafkeyringHandler() { StringBuilder sb = new StringBuilder(existing); for (String prefix : packagePrefixes) { if (!existing.contains(prefix)) { - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append('|'); } sb.append(prefix); 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 7e30a847d8..8e18228afb 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 @@ -14,6 +14,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import javax.net.ssl.SSLHandshakeException; import java.io.ByteArrayOutputStream; @@ -90,31 +92,18 @@ void response200WithEmptyModulusIsFailure() throws IOException { @Nested class FailureResponses { - @Test - void response404IsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(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(new HttpClientWrapper.Response(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(new HttpClientWrapper.Response(403, "")); + @ParameterizedTest + @CsvSource({ + "404, 404", + "500, server error", + "403, client error" + }) + void failureResponseCodesAreReported(int statusCode, String expectedMessage) throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(statusCode, "")); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertFalse(checker.check()); assertTrue(errStream.toString().contains("FAILURE")); - assertTrue(errStream.toString().contains("client error")); + assertTrue(errStream.toString().contains(expectedMessage)); } } From f603478801adb670e646f6d2a0ac034ea0c19a44 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 3 Jun 2026 16:06:07 +0530 Subject: [PATCH 25/28] Removed code duplication and added tests Signed-off-by: hrishikesh-nalawade --- certificate-analyser/build.gradle | 3 +- .../main/java/org/zowe/apiml/Analyser.java | 30 +-- .../main/java/org/zowe/apiml/ApimlConf.java | 7 +- .../src/main/java/org/zowe/apiml/Stores.java | 62 +---- .../apiml/StoresNotInitializeException.java | 18 -- .../java/org/zowe/apiml/AnalyserTest.java | 6 +- .../test/java/org/zowe/apiml/StoresTest.java | 26 +- certificate-common/build.gradle | 8 + .../org/zowe/apiml/common/KeyringUtils.java | 129 ++++++++++ .../common}/StoresNotInitializeException.java | 2 +- .../zowe/apiml/common/KeyringUtilsTest.java | 224 ++++++++++++++++++ settings.gradle | 1 + zosmf-jwt-check/build.gradle | 1 + .../java/org/zowe/apiml/ZosmfJwtCheck.java | 30 +-- .../main/java/org/zowe/apiml/ZosmfStores.java | 73 +----- .../org/zowe/apiml/HttpClientWrapperTest.java | 162 +++++++++++++ .../org/zowe/apiml/SSLContextFactoryTest.java | 112 +++++++++ .../java/org/zowe/apiml/ZosmfStoresTest.java | 183 ++++++++++++++ 18 files changed, 870 insertions(+), 207 deletions(-) delete mode 100644 certificate-analyser/src/main/java/org/zowe/apiml/StoresNotInitializeException.java create mode 100644 certificate-common/build.gradle create mode 100644 certificate-common/src/main/java/org/zowe/apiml/common/KeyringUtils.java rename {zosmf-jwt-check/src/main/java/org/zowe/apiml => certificate-common/src/main/java/org/zowe/apiml/common}/StoresNotInitializeException.java (95%) create mode 100644 certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java create mode 100644 zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java create mode 100644 zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java create mode 100644 zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfStoresTest.java diff --git a/certificate-analyser/build.gradle b/certificate-analyser/build.gradle index d68085d549..82ca902b79 100644 --- a/certificate-analyser/build.gradle +++ b/certificate-analyser/build.gradle @@ -3,6 +3,7 @@ plugins { } dependencies { + implementation project(':certificate-common') implementation project(':zosmf-jwt-check') implementation libs.picocli annotationProcessor libs.picocli.codegen @@ -17,7 +18,7 @@ compileJava { } jar { - dependsOn ':zosmf-jwt-check:jar' + dependsOn ':certificate-common:jar', ':zosmf-jwt-check:jar' manifest { attributes( 'Main-Class': 'org.zowe.apiml.Analyser' diff --git a/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java b/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java index 6a35869898..89dbda5612 100644 --- a/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java +++ b/certificate-analyser/src/main/java/org/zowe/apiml/Analyser.java @@ -22,7 +22,7 @@ public class Analyser { private static final String ZOSMF_JWT_CHECK_FLAG = "--zosmf-jwt-check"; public static int mainWithExitCode(String[] args) { - ensureSafkeyringHandler(); + org.zowe.apiml.common.KeyringUtils.ensureSafkeyringHandler(); if (args.length > 0 && ZOSMF_JWT_CHECK_FLAG.equals(args[0])) { String[] remainingArgs = Arrays.copyOfRange(args, 1, args.length); @@ -72,34 +72,6 @@ public static int mainWithExitCode(String[] args) { return 4; } - /** - * Registers IBM SAF keyring URL protocol handler packages via the - * {@code java.protocol.handler.pkgs} system property. - * - *

    On IBM Java 17/21 (z/OS), this property works in conjunction with - * {@code --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca} to enable - * the {@code safkeyring://} URL protocol. The {@code --add-modules} flag - * resolves the module (making classes accessible), while this property tells - * the {@link java.net.URL} class which packages to search for the handler.

    - */ - static void ensureSafkeyringHandler() { - String[] packagePrefixes = { - "com.ibm.crypto.zsecurity.provider", - "com.ibm.crypto.hdwrCCA.provider" - }; - String existing = System.getProperty("java.protocol.handler.pkgs", ""); - StringBuilder sb = new StringBuilder(existing); - for (String prefix : packagePrefixes) { - if (!existing.contains(prefix)) { - if (!sb.isEmpty()) { - sb.append('|'); - } - sb.append(prefix); - } - } - System.setProperty("java.protocol.handler.pkgs", sb.toString()); - } - public static final void main(String[] args) { System.exit(mainWithExitCode(args)); } diff --git a/certificate-analyser/src/main/java/org/zowe/apiml/ApimlConf.java b/certificate-analyser/src/main/java/org/zowe/apiml/ApimlConf.java index ceafb37fdc..65885decb0 100644 --- a/certificate-analyser/src/main/java/org/zowe/apiml/ApimlConf.java +++ b/certificate-analyser/src/main/java/org/zowe/apiml/ApimlConf.java @@ -16,7 +16,12 @@ @CommandLine.Command(version = { "Versioned Command 1.0", "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", - "OS: ${os.name} ${os.version} ${os.arch}"}) + "OS: ${os.name} ${os.version} ${os.arch}"}, + footer = { + "", + "Additional commands:", + " --zosmf-jwt-check Run the z/OSMF JWT validation check.", + " Use --zosmf-jwt-check --help for details."}) public class ApimlConf implements Config { @Option(names = {"-k", "--keystore"}, description = "Path to keystore file or keyring. When using keyring, pass -Djava.protocol.handler.pkgs=com.ibm.crypto.provider in command line.") diff --git a/certificate-analyser/src/main/java/org/zowe/apiml/Stores.java b/certificate-analyser/src/main/java/org/zowe/apiml/Stores.java index 3cc4afd108..7f5003daaf 100644 --- a/certificate-analyser/src/main/java/org/zowe/apiml/Stores.java +++ b/certificate-analyser/src/main/java/org/zowe/apiml/Stores.java @@ -10,12 +10,13 @@ package org.zowe.apiml; +import org.zowe.apiml.common.KeyringUtils; +import org.zowe.apiml.common.StoresNotInitializeException; + 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; @@ -25,14 +26,10 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; -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 Config conf; @@ -43,21 +40,6 @@ public Stores(Config 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(); @@ -82,13 +64,13 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } return; } - if (isKeyring(conf.getTrustStore())) { - try (InputStream trustStoreIStream = keyRingUrl(conf.getTrustStore()).openStream()) { - this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustPasswd().toCharArray(), conf.getTrustStoreType()); + if (KeyringUtils.isKeyring(conf.getTrustStore())) { + try (InputStream trustStoreIStream = KeyringUtils.keyRingUrl(conf.getTrustStore()).openStream()) { + this.trustStore = KeyringUtils.readKeyStore(trustStoreIStream, conf.getTrustPasswd().toCharArray(), conf.getTrustStoreType()); } } else { try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { - this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustPasswd().toCharArray(), conf.getTrustStoreType()); + this.trustStore = KeyringUtils.readKeyStore(trustStoreIStream, conf.getTrustPasswd().toCharArray(), conf.getTrustStoreType()); } } @@ -104,16 +86,16 @@ private void initKeystore() throws IOException, CertificateException, NoSuchAlgo } return; } - if (isKeyring(conf.getKeyStore())) { - try (InputStream keyringIStream = keyRingUrl(conf.getKeyStore()).openStream()) { - this.keyStore = readKeyStore(keyringIStream, conf.getKeyPasswd().toCharArray(), conf.getKeyStoreType()); + if (KeyringUtils.isKeyring(conf.getKeyStore())) { + try (InputStream keyringIStream = KeyringUtils.keyRingUrl(conf.getKeyStore()).openStream()) { + this.keyStore = KeyringUtils.readKeyStore(keyringIStream, conf.getKeyPasswd().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.getKeyPasswd().toCharArray(), conf.getKeyStoreType()); + this.keyStore = KeyringUtils.readKeyStore(keyStoreIStream, conf.getKeyPasswd().toCharArray(), conf.getKeyStoreType()); } } } @@ -149,12 +131,6 @@ public Certificate[] getServerCertificateChain(String alias) throws KeyStoreExce return keyStore.getCertificateChain(alias); } - 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; } @@ -166,20 +142,4 @@ public KeyStore getTrustStore() { public Config 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"); - } - String formatted = formatKeyringUrl(uri); - try { - return new URL(formatted); - } catch (MalformedURLException e) { - System.err.println("ERROR: Unknown protocol in '" + formatted + "': " + e.getMessage()); - System.err.println("Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); - System.err.println("And that ensureSafkeyringHandler() has been called to set java.protocol.handler.pkgs"); - throw e; - } - } } diff --git a/certificate-analyser/src/main/java/org/zowe/apiml/StoresNotInitializeException.java b/certificate-analyser/src/main/java/org/zowe/apiml/StoresNotInitializeException.java deleted file mode 100644 index e5c77f773c..0000000000 --- a/certificate-analyser/src/main/java/org/zowe/apiml/StoresNotInitializeException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java b/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java index 96e36e0a89..e667aee414 100644 --- a/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java +++ b/certificate-analyser/src/test/java/org/zowe/apiml/AnalyserTest.java @@ -128,7 +128,7 @@ void restoreProperty() { @Test void whenPropertyNotSet_thenBothPackagesAreRegistered() { System.clearProperty("java.protocol.handler.pkgs"); - Analyser.ensureSafkeyringHandler(); + org.zowe.apiml.common.KeyringUtils.ensureSafkeyringHandler(); String value = System.getProperty("java.protocol.handler.pkgs"); assertTrue(value.contains("com.ibm.crypto.zsecurity.provider")); assertTrue(value.contains("com.ibm.crypto.hdwrCCA.provider")); @@ -137,7 +137,7 @@ void whenPropertyNotSet_thenBothPackagesAreRegistered() { @Test void whenPropertyAlreadyHasOtherPackages_thenNewPackagesAreAppended() { System.setProperty("java.protocol.handler.pkgs", "com.example.custom"); - Analyser.ensureSafkeyringHandler(); + org.zowe.apiml.common.KeyringUtils.ensureSafkeyringHandler(); String value = System.getProperty("java.protocol.handler.pkgs"); assertTrue(value.startsWith("com.example.custom|")); assertTrue(value.contains("com.ibm.crypto.zsecurity.provider")); @@ -148,7 +148,7 @@ void whenPropertyAlreadyHasOtherPackages_thenNewPackagesAreAppended() { void whenPropertyAlreadyContainsPackages_thenNoDuplicatesAdded() { System.setProperty("java.protocol.handler.pkgs", "com.ibm.crypto.zsecurity.provider|com.ibm.crypto.hdwrCCA.provider"); - Analyser.ensureSafkeyringHandler(); + org.zowe.apiml.common.KeyringUtils.ensureSafkeyringHandler(); String value = System.getProperty("java.protocol.handler.pkgs"); assertEquals("com.ibm.crypto.zsecurity.provider|com.ibm.crypto.hdwrCCA.provider", value); } diff --git a/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java b/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java index 9cd6468196..da5abc3bea 100644 --- a/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java +++ b/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.zowe.apiml.common.KeyringUtils; +import org.zowe.apiml.common.StoresNotInitializeException; import picocli.CommandLine; import java.net.MalformedURLException; @@ -95,40 +97,40 @@ void whenTruststoreIsKeyring_thenKeyRingUrlIsUsed() { @Test void givenValidKeyringUri_whenProtocolHandlerNotAvailable_thenThrowsMalformedURLException() { - assertThrows(MalformedURLException.class, () -> Stores.keyRingUrl("safkeyring://userId/keyRing")); + assertThrows(MalformedURLException.class, () -> KeyringUtils.keyRingUrl("safkeyring://userId/keyRing")); } @Test void whenKeyRingUrlCalledWithInvalidFormat_thenThrowsStoresNotInitializeException() { StoresNotInitializeException e = assertThrows(StoresNotInitializeException.class, - () -> Stores.keyRingUrl("notakeyring://bad/format/extra")); + () -> KeyringUtils.keyRingUrl("notakeyring://bad/format/extra")); assertTrue(e.getMessage().contains("Incorrect key ring format")); } @Test void givenIsKeyring_whenValidSafkeyringUri_thenReturnsTrue() { - assertTrue(Stores.isKeyring("safkeyring://userId/keyRing")); - assertTrue(Stores.isKeyring("safkeyring:////userId/keyRing")); - assertTrue(Stores.isKeyring("safkeyringce://userId/keyRing")); + assertTrue(KeyringUtils.isKeyring("safkeyring://userId/keyRing")); + assertTrue(KeyringUtils.isKeyring("safkeyring:////userId/keyRing")); + assertTrue(KeyringUtils.isKeyring("safkeyringce://userId/keyRing")); } @Test void givenIsKeyring_whenInvalidUri_thenReturnsFalse() { - assertFalse(Stores.isKeyring(null)); - assertFalse(Stores.isKeyring("")); - assertFalse(Stores.isKeyring("/path/to/file.p12")); - assertFalse(Stores.isKeyring("keyring://userId/keyRing")); + assertFalse(KeyringUtils.isKeyring(null)); + assertFalse(KeyringUtils.isKeyring("")); + assertFalse(KeyringUtils.isKeyring("/path/to/file.p12")); + assertFalse(KeyringUtils.isKeyring("keyring://userId/keyRing")); } @Test void givenFormatKeyringUrl_whenUriHasExtraSlashes_thenNormalized() { - assertEquals("safkeyring://userId/keyRing", Stores.formatKeyringUrl("safkeyring:////userId/keyRing")); - assertEquals("safkeyring://userId/keyRing", Stores.formatKeyringUrl("safkeyring://userId/keyRing")); + assertEquals("safkeyring://userId/keyRing", KeyringUtils.formatKeyringUrl("safkeyring:////userId/keyRing")); + assertEquals("safkeyring://userId/keyRing", KeyringUtils.formatKeyringUrl("safkeyring://userId/keyRing")); } @Test void givenFormatKeyringUrl_whenNull_thenReturnsNull() { - assertNull(Stores.formatKeyringUrl(null)); + assertNull(KeyringUtils.formatKeyringUrl(null)); } } diff --git a/certificate-common/build.gradle b/certificate-common/build.gradle new file mode 100644 index 0000000000..d5aeef933c --- /dev/null +++ b/certificate-common/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java' +} + +dependencies { + testImplementation libs.mockito.core + testImplementation libs.hamcrest +} diff --git a/certificate-common/src/main/java/org/zowe/apiml/common/KeyringUtils.java b/certificate-common/src/main/java/org/zowe/apiml/common/KeyringUtils.java new file mode 100644 index 0000000000..52b7a1f12f --- /dev/null +++ b/certificate-common/src/main/java/org/zowe/apiml/common/KeyringUtils.java @@ -0,0 +1,129 @@ +/* + * 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.common; + +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; + +/** + * Shared utilities for SAF keyring URI handling and KeyStore loading. + * Used by both {@code certificate-analyser} and {@code zosmf-jwt-check}. + */ +@SuppressWarnings("squid:S106") +public final class KeyringUtils { + + private static final Pattern KEYRING_PATTERN = Pattern.compile("^(safkeyring[^:]*):/{2,4}([^/]+)/([^/]+)$"); + + private KeyringUtils() { + // utility class + } + + /** + * 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); + if (matcher.matches()) { + return matcher.group(1) + "://" + matcher.group(2) + "/" + matcher.group(3); + } + return input; + } + + /** + * Converts a keyring URI string to a {@link URL} suitable for opening as a stream. + * + * @param uri the keyring URI + * @return URL object + * @throws MalformedURLException if the URI is invalid + * @throws StoresNotInitializeException if the URI does not match keyring format + */ + 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"); + } + String formatted = formatKeyringUrl(uri); + try { + return new URL(formatted); + } catch (MalformedURLException e) { + System.err.println("ERROR: Unknown protocol in '" + formatted + "': " + e.getMessage()); + System.err.println("Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); + System.err.println("And that ensureSafkeyringHandler() has been called to set java.protocol.handler.pkgs"); + throw e; + } + } + + /** + * Loads a {@link KeyStore} from an input stream. + * + * @param is input stream to read from + * @param pass keystore password + * @param type keystore type (e.g. PKCS12, JKS) + * @return loaded KeyStore instance + */ + 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; + } + + /** + * Registers IBM SAF keyring URL protocol handler packages via the + * {@code java.protocol.handler.pkgs} system property. + * + *

    On IBM Java 17/21 (z/OS), this property works in conjunction with + * {@code --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca} to enable + * the {@code safkeyring://} URL protocol. The {@code --add-modules} flag + * resolves the module (making classes accessible), while this property tells + * the {@link java.net.URL} class which packages to search for the handler.

    + */ + public static void ensureSafkeyringHandler() { + String[] packagePrefixes = { + "com.ibm.crypto.zsecurity.provider", + "com.ibm.crypto.hdwrCCA.provider" + }; + String existing = System.getProperty("java.protocol.handler.pkgs", ""); + StringBuilder sb = new StringBuilder(existing); + for (String prefix : packagePrefixes) { + if (!existing.contains(prefix)) { + if (!sb.isEmpty()) { + sb.append('|'); + } + sb.append(prefix); + } + } + System.setProperty("java.protocol.handler.pkgs", sb.toString()); + } +} diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java b/certificate-common/src/main/java/org/zowe/apiml/common/StoresNotInitializeException.java similarity index 95% rename from zosmf-jwt-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java rename to certificate-common/src/main/java/org/zowe/apiml/common/StoresNotInitializeException.java index c6b5231cf2..28fbba09b3 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/StoresNotInitializeException.java +++ b/certificate-common/src/main/java/org/zowe/apiml/common/StoresNotInitializeException.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml; +package org.zowe.apiml.common; /** * Thrown when keystore or truststore initialization fails diff --git a/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java b/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java new file mode 100644 index 0000000000..6fb3eff9ec --- /dev/null +++ b/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java @@ -0,0 +1,224 @@ +/* + * 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.common; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +class KeyringUtilsTest { + + @Nested + class IsKeyring { + + @ParameterizedTest + @ValueSource(strings = { + "safkeyring://userId/keyRing", + "safkeyring:////userId/keyRing", + "safkeyringjce://userId/keyRing", + "safkeyringjcecca://userId/keyRing", + "safkeyringjcehybrid://user1/ring2" + }) + void givenValidKeyringUri_thenReturnsTrue(String input) { + assertTrue(KeyringUtils.isKeyring(input)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { + "/path/to/keystore.p12", + "file:///some/path", + "safkeyring:/missing-slash/ring", + "safkeyring://", + "safkeyring://userOnly" + }) + void givenNonKeyringInput_thenReturnsFalse(String input) { + assertFalse(KeyringUtils.isKeyring(input)); + } + } + + @Nested + class FormatKeyringUrl { + + @ParameterizedTest + @CsvSource({ + "safkeyring://userId/keyRing, safkeyring://userId/keyRing", + "safkeyring:////userId/keyRing, safkeyring://userId/keyRing", + "safkeyringjce://user/ring, safkeyringjce://user/ring", + "safkeyringjcecca:///user1/ring2, safkeyringjcecca://user1/ring2" + }) + void givenKeyringUri_thenNormalizesToTwoSlashes(String input, String expected) { + assertEquals(expected, KeyringUtils.formatKeyringUrl(input)); + } + + @Test + void givenNonKeyringPath_thenReturnsUnchanged() { + String path = "/path/to/keystore.p12"; + assertEquals(path, KeyringUtils.formatKeyringUrl(path)); + } + + @Test + void givenNull_thenReturnsNull() { + assertNull(KeyringUtils.formatKeyringUrl(null)); + } + } + + @Nested + class KeyRingUrl { + + @Test + void givenInvalidKeyringFormat_thenThrowsStoresNotInitializeException() { + assertThrows(StoresNotInitializeException.class, + () -> KeyringUtils.keyRingUrl("/some/file/path")); + } + + @Test + void givenInvalidFormat_thenExceptionContainsHelpfulMessage() { + StoresNotInitializeException ex = assertThrows(StoresNotInitializeException.class, + () -> KeyringUtils.keyRingUrl("notakeyring")); + assertThat(ex.getMessage(), containsString("Incorrect key ring format")); + assertThat(ex.getMessage(), containsString("safkeyring://userId/keyRing")); + } + + @Test + void givenNullInput_thenThrowsStoresNotInitializeException() { + assertThrows(StoresNotInitializeException.class, + () -> KeyringUtils.keyRingUrl(null)); + } + } + + @Nested + class ReadKeyStore { + + @Test + void givenValidPKCS12Stream_thenLoadsKeyStore() throws Exception { + // Use the test keystore from the project + String truststorePath = "../keystore/localhost/localhost.truststore.p12"; + java.io.File file = new java.io.File(truststorePath); + if (!file.exists()) { + // Skip if keystore not available in this environment + return; + } + + try (InputStream is = new java.io.FileInputStream(file)) { + KeyStore ks = KeyringUtils.readKeyStore(is, "password".toCharArray(), "PKCS12"); + assertNotNull(ks); + assertTrue(ks.size() > 0); + } + } + + @Test + void givenInvalidPassword_thenThrowsIOException() { + String truststorePath = "../keystore/localhost/localhost.truststore.p12"; + java.io.File file = new java.io.File(truststorePath); + if (!file.exists()) { + return; + } + + assertThrows(IOException.class, () -> { + try (InputStream is = new java.io.FileInputStream(file)) { + KeyringUtils.readKeyStore(is, "wrongpassword".toCharArray(), "PKCS12"); + } + }); + } + + @Test + void givenInvalidType_thenThrowsKeyStoreException() { + InputStream is = new ByteArrayInputStream(new byte[0]); + assertThrows(KeyStoreException.class, + () -> KeyringUtils.readKeyStore(is, "pass".toCharArray(), "INVALID_TYPE")); + } + } + + @Nested + class EnsureSafkeyringHandler { + + @Test + void givenEmptyProperty_thenSetsHandlerPackages() { + String original = System.getProperty("java.protocol.handler.pkgs", ""); + try { + System.setProperty("java.protocol.handler.pkgs", ""); + + KeyringUtils.ensureSafkeyringHandler(); + + String result = System.getProperty("java.protocol.handler.pkgs"); + assertThat(result, containsString("com.ibm.crypto.zsecurity.provider")); + assertThat(result, containsString("com.ibm.crypto.hdwrCCA.provider")); + } finally { + System.setProperty("java.protocol.handler.pkgs", original); + } + } + + @Test + void givenExistingPackages_thenAppendsWithPipeSeparator() { + String original = System.getProperty("java.protocol.handler.pkgs", ""); + try { + System.setProperty("java.protocol.handler.pkgs", "com.example.handler"); + + KeyringUtils.ensureSafkeyringHandler(); + + String result = System.getProperty("java.protocol.handler.pkgs"); + assertThat(result, startsWith("com.example.handler|")); + assertThat(result, containsString("com.ibm.crypto.zsecurity.provider")); + assertThat(result, containsString("com.ibm.crypto.hdwrCCA.provider")); + } finally { + System.setProperty("java.protocol.handler.pkgs", original); + } + } + + @Test + void givenAlreadyRegistered_thenDoesNotDuplicate() { + String original = System.getProperty("java.protocol.handler.pkgs", ""); + try { + System.setProperty("java.protocol.handler.pkgs", ""); + + KeyringUtils.ensureSafkeyringHandler(); + String firstRun = System.getProperty("java.protocol.handler.pkgs"); + + KeyringUtils.ensureSafkeyringHandler(); + String secondRun = System.getProperty("java.protocol.handler.pkgs"); + + assertEquals(firstRun, secondRun); + } finally { + System.setProperty("java.protocol.handler.pkgs", original); + } + } + } + + @Nested + class StoresNotInitializeExceptionTest { + + @Test + void givenMessage_thenExceptionContainsIt() { + StoresNotInitializeException ex = new StoresNotInitializeException("test error"); + assertEquals("test error", ex.getMessage()); + } + + @Test + void givenException_thenIsRuntimeException() { + StoresNotInitializeException ex = new StoresNotInitializeException("msg"); + assertThat(ex, instanceOf(RuntimeException.class)); + } + } +} diff --git a/settings.gradle b/settings.gradle index aeb34a82e7..290b442fd1 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 'certificate-common' include 'zosmf-jwt-check' include 'apiml-tomcat-common' include 'apiml-sample-extension' diff --git a/zosmf-jwt-check/build.gradle b/zosmf-jwt-check/build.gradle index 8a6d9a333d..f65a9adf8a 100644 --- a/zosmf-jwt-check/build.gradle +++ b/zosmf-jwt-check/build.gradle @@ -3,6 +3,7 @@ plugins { } dependencies { + implementation project(':certificate-common') implementation libs.picocli annotationProcessor libs.picocli.codegen 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 e4bbcf3963..c48aa1a3af 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 @@ -30,7 +30,7 @@ public class ZosmfJwtCheck { static final String VERIFY_DISABLED = "DISABLED"; public static int mainWithExitCode(String[] args) { - ensureSafkeyringHandler(); + org.zowe.apiml.common.KeyringUtils.ensureSafkeyringHandler(); try { ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); @@ -101,34 +101,6 @@ static void validateConfig(ZosmfJwtCheckConf conf) { } } - /** - * Registers IBM SAF keyring URL protocol handler packages via the - * {@code java.protocol.handler.pkgs} system property. - * - *

    On IBM Java 17/21 (z/OS), this property works in conjunction with - * {@code --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca} to enable - * the {@code safkeyring://} URL protocol. The {@code --add-modules} flag - * resolves the module (making classes accessible), while this property tells - * the {@link java.net.URL} class which packages to search for the handler.

    - */ - static void ensureSafkeyringHandler() { - String[] packagePrefixes = { - "com.ibm.crypto.zsecurity.provider", - "com.ibm.crypto.hdwrCCA.provider" - }; - String existing = System.getProperty("java.protocol.handler.pkgs", ""); - StringBuilder sb = new StringBuilder(existing); - for (String prefix : packagePrefixes) { - if (!existing.contains(prefix)) { - if (!sb.isEmpty()) { - sb.append('|'); - } - sb.append(prefix); - } - } - System.setProperty("java.protocol.handler.pkgs", sb.toString()); - } - public static void main(String[] args) { System.exit(mainWithExitCode(args)); } diff --git a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java index d393a5d395..00b8e88f2f 100644 --- a/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java +++ b/zosmf-jwt-check/src/main/java/org/zowe/apiml/ZosmfStores.java @@ -10,18 +10,18 @@ package org.zowe.apiml; +import org.zowe.apiml.common.KeyringUtils; +import org.zowe.apiml.common.StoresNotInitializeException; + 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; /** * Loads Java {@link java.security.KeyStore} instances from the filesystem @@ -30,8 +30,6 @@ @SuppressWarnings("squid:S106") public class ZosmfStores { - private static final Pattern KEYRING_PATTERN = Pattern.compile("^(safkeyring[^:]*):/{2,4}([^/]+)/([^/]+)$"); - private KeyStore keyStore; private KeyStore trustStore; private final ZosmfJwtCheckConfig conf; @@ -41,33 +39,6 @@ public ZosmfStores(ZosmfJwtCheckConfig 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); - if (matcher.matches()) { - return matcher.group(1) + "://" + matcher.group(2) + "/" + matcher.group(3); - } - return input; - } - void init() { try { initKeystore(); @@ -92,14 +63,14 @@ private void initTruststore() throws IOException, CertificateException, NoSuchAl } return; } - if (isKeyring(conf.getTrustStore())) { - URL url = keyRingUrl(conf.getTrustStore()); + if (KeyringUtils.isKeyring(conf.getTrustStore())) { + URL url = KeyringUtils.keyRingUrl(conf.getTrustStore()); try (InputStream trustStoreIStream = url.openStream()) { - this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + this.trustStore = KeyringUtils.readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); } } else { try (InputStream trustStoreIStream = new FileInputStream(conf.getTrustStore())) { - this.trustStore = readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); + this.trustStore = KeyringUtils.readKeyStore(trustStoreIStream, conf.getTrustStorePassword().toCharArray(), conf.getTrustStoreType()); } } } @@ -108,26 +79,20 @@ private void initKeystore() throws IOException, CertificateException, NoSuchAlgo 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()); + if (KeyringUtils.isKeyring(conf.getKeyStore())) { + try (InputStream keyringIStream = KeyringUtils.keyRingUrl(conf.getKeyStore()).openStream()) { + this.keyStore = KeyringUtils.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()); + this.keyStore = KeyringUtils.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; } @@ -139,20 +104,4 @@ public KeyStore getTrustStore() { public ZosmfJwtCheckConfig 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"); - } - String formatted = formatKeyringUrl(uri); - try { - return new URL(formatted); - } catch (MalformedURLException e) { - System.err.println("ERROR: Unknown protocol in '" + formatted + "': " + e.getMessage()); - System.err.println("Ensure the JVM is started with: --add-modules ibm.crypto.zsecurity,ibm.crypto.hdwrcca"); - System.err.println("And that ensureSafkeyringHandler() has been called to set java.protocol.handler.pkgs"); - throw e; - } - } } diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java new file mode 100644 index 0000000000..81a8ec2f09 --- /dev/null +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java @@ -0,0 +1,162 @@ +/* + * 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 com.sun.net.httpserver.HttpServer; +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.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class HttpClientWrapperTest { + + @Nested + class HttpMode { + + private HttpServer server; + private int port; + + @BeforeEach + void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress(0), 0); + port = server.getAddress().getPort(); + } + + @AfterEach + void stopServer() { + if (server != null) { + server.stop(0); + } + } + + @Test + void executesGetRequestAndReturnsResponse() throws IOException { + server.createContext("/test", exchange -> { + String body = "{\"status\":\"ok\"}"; + exchange.sendResponseHeaders(200, body.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body.getBytes()); + } + }); + server.start(); + + HttpClientWrapper client = new HttpClientWrapper(); + URL url = new URL("http://localhost:" + port + "/test"); + + HttpClientWrapper.Response response = client.executeCall(url, new HashMap<>()); + + assertEquals(200, response.getStatusCode()); + assertEquals("{\"status\":\"ok\"}", response.getBody()); + } + + @Test + void passesHeadersToServer() throws IOException { + final String[] receivedHeader = {null}; + server.createContext("/headers", exchange -> { + receivedHeader[0] = exchange.getRequestHeaders().getFirst("X-Custom-Header"); + exchange.sendResponseHeaders(204, -1); + }); + server.start(); + + HttpClientWrapper client = new HttpClientWrapper(); + URL url = new URL("http://localhost:" + port + "/headers"); + Map headers = new HashMap<>(); + headers.put("X-Custom-Header", "test-value"); + + client.executeCall(url, headers); + + assertEquals("test-value", receivedHeader[0]); + } + + @Test + void handles404Response() throws IOException { + server.createContext("/notfound", exchange -> { + String body = "Not Found"; + exchange.sendResponseHeaders(404, body.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body.getBytes()); + } + }); + server.start(); + + HttpClientWrapper client = new HttpClientWrapper(); + URL url = new URL("http://localhost:" + port + "/notfound"); + + HttpClientWrapper.Response response = client.executeCall(url, new HashMap<>()); + + assertEquals(404, response.getStatusCode()); + assertTrue(response.getBody().contains("Not Found")); + } + + @Test + void handles500Response() throws IOException { + server.createContext("/error", exchange -> { + String body = "Internal Server Error"; + exchange.sendResponseHeaders(500, body.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body.getBytes()); + } + }); + server.start(); + + HttpClientWrapper client = new HttpClientWrapper(); + URL url = new URL("http://localhost:" + port + "/error"); + + HttpClientWrapper.Response response = client.executeCall(url, new HashMap<>()); + + assertEquals(500, response.getStatusCode()); + } + + @Test + void handlesNullHeaders() throws IOException { + server.createContext("/nullheaders", exchange -> { + exchange.sendResponseHeaders(200, -1); + }); + server.start(); + + HttpClientWrapper client = new HttpClientWrapper(); + URL url = new URL("http://localhost:" + port + "/nullheaders"); + + HttpClientWrapper.Response response = client.executeCall(url, null); + + assertEquals(200, response.getStatusCode()); + } + } + + @Nested + class ResponseClass { + + @Test + void responseHoldsStatusCodeAndBody() { + HttpClientWrapper.Response response = new HttpClientWrapper.Response(201, "created"); + + assertEquals(201, response.getStatusCode()); + assertEquals("created", response.getBody()); + } + + @Test + void responseCanHaveNullBody() { + HttpClientWrapper.Response response = new HttpClientWrapper.Response(204, null); + + assertEquals(204, response.getStatusCode()); + assertNull(response.getBody()); + } + } +} diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java new file mode 100644 index 0000000000..fb96efe76f --- /dev/null +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java @@ -0,0 +1,112 @@ +/* + * 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.Nested; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SSLContextFactoryTest { + + private static final String TRUSTSTORE_PATH = "../keystore/localhost/localhost.truststore.p12"; + private static final String KEYSTORE_PATH = "../keystore/localhost/localhost.keystore.p12"; + private static final String PASSWORD = "password"; + + @Nested + class InitSSLContextWithRealStores { + + @Test + void createsNonNullSSLContext() throws Exception { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + SSLContextFactory factory = SSLContextFactory.initSSLContext(stores); + + assertNotNull(factory); + assertNotNull(factory.getSslContext()); + } + + @Test + void sslContextProtocolIsTLS() throws Exception { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + SSLContextFactory factory = SSLContextFactory.initSSLContext(stores); + + assertEquals("TLSv1.2", factory.getSslContext().getProtocol()); + } + + @Test + void worksWithKeystoreAndTruststore() throws Exception { + ZosmfJwtCheckConfig conf = createConf(KEYSTORE_PATH, PASSWORD, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + SSLContextFactory factory = SSLContextFactory.initSSLContext(stores); + + assertNotNull(factory.getSslContext()); + } + + @Test + void worksWithoutKeystore() throws Exception { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + SSLContextFactory factory = SSLContextFactory.initSSLContext(stores); + + SSLContext ctx = factory.getSslContext(); + assertNotNull(ctx.getSocketFactory()); + } + } + + @Nested + class InitTrustAllSSLContext { + + @Test + void createsNonNullSSLContext() throws Exception { + SSLContextFactory factory = SSLContextFactory.initTrustAllSSLContext(); + + assertNotNull(factory); + assertNotNull(factory.getSslContext()); + } + + @Test + void sslContextProtocolIsTLS() throws Exception { + SSLContextFactory factory = SSLContextFactory.initTrustAllSSLContext(); + + assertEquals("TLSv1.2", factory.getSslContext().getProtocol()); + } + + @Test + void socketFactoryIsAvailable() throws Exception { + SSLContextFactory factory = SSLContextFactory.initTrustAllSSLContext(); + + assertNotNull(factory.getSslContext().getSocketFactory()); + } + } + + private ZosmfJwtCheckConfig createConf(String keyStore, String keyStorePassword, + String trustStore, String trustStorePassword, + String storeType) { + ZosmfJwtCheckConfig conf = mock(ZosmfJwtCheckConfig.class); + when(conf.getKeyStore()).thenReturn(keyStore); + when(conf.getKeyStorePassword()).thenReturn(keyStorePassword); + when(conf.getKeyStoreType()).thenReturn(storeType); + when(conf.getTrustStore()).thenReturn(trustStore); + when(conf.getTrustStorePassword()).thenReturn(trustStorePassword); + when(conf.getTrustStoreType()).thenReturn(storeType); + return conf; + } +} diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfStoresTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfStoresTest.java new file mode 100644 index 0000000000..f97e36b48f --- /dev/null +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/ZosmfStoresTest.java @@ -0,0 +1,183 @@ +/* + * 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.Nested; +import org.junit.jupiter.api.Test; +import org.zowe.apiml.common.KeyringUtils; +import org.zowe.apiml.common.StoresNotInitializeException; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.security.KeyStore; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ZosmfStoresTest { + + private static final String TRUSTSTORE_PATH = "../keystore/localhost/localhost.truststore.p12"; + private static final String KEYSTORE_PATH = "../keystore/localhost/localhost.keystore.p12"; + private static final String PASSWORD = "password"; + + @Nested + class GivenValidFileBasedTruststore { + + @Test + void storesAreInitializedSuccessfully() { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + assertNotNull(stores.getTrustStore()); + } + + @Test + void truststoreContainsCertificates() throws Exception { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + assertTrue(stores.getTrustStore().size() > 0); + } + } + + @Nested + class GivenValidFileBasedKeystore { + + @Test + void keystoreAndTruststoreAreLoaded() { + ZosmfJwtCheckConfig conf = createConf(KEYSTORE_PATH, PASSWORD, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + assertNotNull(stores.getKeyStore()); + assertNotNull(stores.getTrustStore()); + } + + @Test + void confIsAccessible() { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + assertSame(conf, stores.getConf()); + } + } + + @Nested + class GivenNoTruststore { + + @Test + void emptyTruststoreIsCreated() { + ZosmfJwtCheckConfig conf = createConf(null, null, null, null, "PKCS12"); + ZosmfStores stores = new ZosmfStores(conf); + + assertNotNull(stores.getTrustStore()); + } + } + + @Nested + class GivenInvalidPaths { + + @Test + void nonExistentTruststoreThrowsException() { + ZosmfJwtCheckConfig conf = createConf(null, null, "/nonexistent/path.p12", PASSWORD, "PKCS12"); + + StoresNotInitializeException e = assertThrows(StoresNotInitializeException.class, + () -> new ZosmfStores(conf)); + assertNotNull(e.getMessage()); + } + + @Test + void wrongPasswordThrowsException() { + ZosmfJwtCheckConfig conf = createConf(null, null, TRUSTSTORE_PATH, "wrongpassword", "PKCS12"); + + assertThrows(StoresNotInitializeException.class, () -> new ZosmfStores(conf)); + } + + @Test + void nonExistentKeystoreThrowsException() { + ZosmfJwtCheckConfig conf = createConf("/nonexistent/key.p12", PASSWORD, TRUSTSTORE_PATH, PASSWORD, "PKCS12"); + + StoresNotInitializeException e = assertThrows(StoresNotInitializeException.class, + () -> new ZosmfStores(conf)); + assertTrue(e.getMessage().contains("Error while loading keystore file")); + } + } + + @Nested + class KeyringUtilsDelegation { + + @Test + void isKeyringReturnsTrueForValidUri() { + assertTrue(KeyringUtils.isKeyring("safkeyring://userId/keyRing")); + assertTrue(KeyringUtils.isKeyring("safkeyring:////userId/keyRing")); + } + + @Test + void isKeyringReturnsFalseForNonKeyring() { + assertFalse(KeyringUtils.isKeyring(null)); + assertFalse(KeyringUtils.isKeyring("/path/to/file.p12")); + assertFalse(KeyringUtils.isKeyring("https://server/resource")); + } + + @Test + void formatKeyringUrlNormalizesSlashes() { + assertEquals("safkeyring://userId/keyRing", + KeyringUtils.formatKeyringUrl("safkeyring:////userId/keyRing")); + } + + @Test + void formatKeyringUrlReturnsInputForNonKeyring() { + assertEquals("/some/path.p12", KeyringUtils.formatKeyringUrl("/some/path.p12")); + assertNull(KeyringUtils.formatKeyringUrl(null)); + } + + @Test + void keyRingUrlThrowsForInvalidFormat() { + assertThrows(StoresNotInitializeException.class, + () -> KeyringUtils.keyRingUrl("not://a/keyring/format/extra")); + } + + @Test + void keyRingUrlThrowsMalformedURLExceptionForValidButUnresolvable() { + // Valid keyring format but no protocol handler registered + assertThrows(MalformedURLException.class, + () -> KeyringUtils.keyRingUrl("safkeyring://userId/keyRing")); + } + + @Test + void readKeyStoreLoadsFromStream() throws Exception { + // Create an in-memory PKCS12 keystore + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, "test".toCharArray()); + + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + ks.store(baos, "test".toCharArray()); + + InputStream is = new ByteArrayInputStream(baos.toByteArray()); + KeyStore loaded = KeyringUtils.readKeyStore(is, "test".toCharArray(), "PKCS12"); + assertNotNull(loaded); + } + } + + private ZosmfJwtCheckConfig createConf(String keyStore, String keyStorePassword, + String trustStore, String trustStorePassword, + String storeType) { + ZosmfJwtCheckConfig conf = mock(ZosmfJwtCheckConfig.class); + when(conf.getKeyStore()).thenReturn(keyStore); + when(conf.getKeyStorePassword()).thenReturn(keyStorePassword); + when(conf.getKeyStoreType()).thenReturn(storeType); + when(conf.getTrustStore()).thenReturn(trustStore); + when(conf.getTrustStorePassword()).thenReturn(trustStorePassword); + when(conf.getTrustStoreType()).thenReturn(storeType); + return conf; + } +} From 6dd5a6ba338901138976fac90f46fcf63a949f27 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 3 Jun 2026 16:36:41 +0530 Subject: [PATCH 26/28] test update Signed-off-by: hrishikesh-nalawade --- .../java/org/zowe/apiml/common/KeyringUtilsTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java b/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java index 6fb3eff9ec..e988015886 100644 --- a/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java +++ b/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java @@ -129,18 +129,17 @@ void givenValidPKCS12Stream_thenLoadsKeyStore() throws Exception { } @Test - void givenInvalidPassword_thenThrowsIOException() { + void givenInvalidPassword_thenThrowsIOException() throws Exception { String truststorePath = "../keystore/localhost/localhost.truststore.p12"; java.io.File file = new java.io.File(truststorePath); if (!file.exists()) { return; } - assertThrows(IOException.class, () -> { - try (InputStream is = new java.io.FileInputStream(file)) { - KeyringUtils.readKeyStore(is, "wrongpassword".toCharArray(), "PKCS12"); - } - }); + try (InputStream is = new java.io.FileInputStream(file)) { + assertThrows(IOException.class, + () -> KeyringUtils.readKeyStore(is, "wrongpassword".toCharArray(), "PKCS12")); + } } @Test From 92bc33c9c2a3befef61cfc5beaac6147f21d3314 Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 3 Jun 2026 17:17:22 +0530 Subject: [PATCH 27/28] increasing test coverage Signed-off-by: hrishikesh-nalawade --- .../test/java/org/zowe/apiml/StoresTest.java | 121 ++++++++++++++++++ .../zowe/apiml/common/KeyringUtilsTest.java | 8 ++ gradle/coverage.gradle | 2 + .../org/zowe/apiml/HttpClientWrapperTest.java | 19 +++ .../zowe/apiml/JwkEndpointCheckerTest.java | 63 +++++++++ .../org/zowe/apiml/SSLContextFactoryTest.java | 44 +++++++ .../org/zowe/apiml/ZosmfJwtCheckTest.java | 69 ++++++++++ 7 files changed, 326 insertions(+) diff --git a/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java b/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java index da5abc3bea..dcc7f58ab5 100644 --- a/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java +++ b/certificate-analyser/src/test/java/org/zowe/apiml/StoresTest.java @@ -17,6 +17,9 @@ import picocli.CommandLine; import java.net.MalformedURLException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -134,4 +137,122 @@ void givenFormatKeyringUrl_whenNull_thenReturnsNull() { } } + @Nested + class GivenValidStores { + + private Stores createValidStores() { + String[] args = {"--keystore", "../keystore/localhost/localhost.keystore.p12", + "--truststore", "../keystore/localhost/localhost.truststore.p12", + "--keypasswd", "password", + "--keyalias", "localhost"}; + ApimlConf conf = new ApimlConf(); + new CommandLine(conf).parseArgs(args); + return new Stores(conf); + } + + @Test + void getListOfCertificatesReturnsNonEmptyMap() throws Exception { + Stores stores = createValidStores(); + Map certs = stores.getListOfCertificates(); + assertNotNull(certs); + assertFalse(certs.isEmpty()); + } + + @Test + void getListOfCertificatesReturnsCachedMap() throws Exception { + Stores stores = createValidStores(); + Map first = stores.getListOfCertificates(); + Map second = stores.getListOfCertificates(); + assertSame(first, second); + } + + @Test + void getX509CertificateReturnsValidCert() throws Exception { + Stores stores = createValidStores(); + X509Certificate cert = stores.getX509Certificate("localhost"); + assertNotNull(cert); + } + + @Test + void getServerCertificateChainWithNullAliasUsesFirstAlias() throws Exception { + Stores stores = createValidStores(); + Certificate[] chain = stores.getServerCertificateChain(null); + assertNotNull(chain); + assertTrue(chain.length > 0); + } + + @Test + void getServerCertificateChainWithExplicitAlias() throws Exception { + Stores stores = createValidStores(); + Certificate[] chain = stores.getServerCertificateChain("localhost"); + assertNotNull(chain); + assertTrue(chain.length > 0); + } + + @Test + void getKeyStoreReturnsNonNull() { + Stores stores = createValidStores(); + assertNotNull(stores.getKeyStore()); + } + + @Test + void getTrustStoreReturnsNonNull() { + Stores stores = createValidStores(); + assertNotNull(stores.getTrustStore()); + } + + @Test + void getConfReturnsNonNull() { + Stores stores = createValidStores(); + assertNotNull(stores.getConf()); + } + } + + @Nested + class GivenNullKeystore { + + @Test + void whenKeystoreIsNull_thenEmptyKeystoreCreated() { + String[] args = {"--truststore", "../keystore/localhost/localhost.truststore.p12", + "--keypasswd", "password"}; + ApimlConf conf = new ApimlConf(); + new CommandLine(conf).parseArgs(args); + Stores stores = new Stores(conf); + assertNotNull(stores.getTrustStore()); + } + } + + @Nested + class GivenNullTruststore { + + @Test + void whenTruststoreIsNull_thenEmptyTruststoreCreated() { + String[] args = {"--keystore", "../keystore/localhost/localhost.keystore.p12", + "--keypasswd", "password", + "--keyalias", "localhost"}; + ApimlConf conf = new ApimlConf(); + new CommandLine(conf).parseArgs(args); + Stores stores = new Stores(conf); + assertNotNull(stores.getTrustStore()); + } + } + + @Nested + class GivenMissingAlias { + + @Test + void getX509CertificateThrowsForNonexistentAlias() { + String[] args = {"--keystore", "../keystore/localhost/localhost.keystore.p12", + "--truststore", "../keystore/localhost/localhost.truststore.p12", + "--keypasswd", "password", + "--keyalias", "localhost"}; + ApimlConf conf = new ApimlConf(); + new CommandLine(conf).parseArgs(args); + Stores stores = new Stores(conf); + // getCertificateChain returns null for nonexistent alias, causing NPE + assertThrows(NullPointerException.class, + () -> stores.getX509Certificate("nonexistent-alias")); + } + } + } diff --git a/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java b/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java index e988015886..08d0abd772 100644 --- a/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java +++ b/certificate-common/src/test/java/org/zowe/apiml/common/KeyringUtilsTest.java @@ -106,6 +106,14 @@ void givenNullInput_thenThrowsStoresNotInitializeException() { assertThrows(StoresNotInitializeException.class, () -> KeyringUtils.keyRingUrl(null)); } + + @Test + void givenValidKeyringFormat_thenThrowsMalformedURLExceptionWithoutHandler() { + // Valid keyring format but no SAF protocol handler registered on this JVM + java.net.MalformedURLException ex = assertThrows(java.net.MalformedURLException.class, + () -> KeyringUtils.keyRingUrl("safkeyring://userId/keyRing")); + assertThat(ex.getMessage(), containsString("unknown protocol")); + } } @Nested diff --git a/gradle/coverage.gradle b/gradle/coverage.gradle index 01e3f2175a..746d7ad7e2 100644 --- a/gradle/coverage.gradle +++ b/gradle/coverage.gradle @@ -20,6 +20,8 @@ ext.javaProjectsWithUnitTests = [ 'apiml-security-common', 'zaas-client', 'certificate-analyser', + 'certificate-common', + 'zosmf-jwt-check', 'apiml-tomcat-common', 'apiml-extension-loader', 'zaas-service' diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java index 81a8ec2f09..056af0c3c9 100644 --- a/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/HttpClientWrapperTest.java @@ -138,6 +138,25 @@ void handlesNullHeaders() throws IOException { assertEquals(200, response.getStatusCode()); } + + @Test + void readBodyReturnsNullWhenStreamThrows() throws IOException { + server.createContext("/broken", exchange -> { + // Send headers claiming 100 bytes, then close immediately without body + exchange.sendResponseHeaders(200, 100); + exchange.getResponseBody().close(); + }); + server.start(); + + HttpClientWrapper client = new HttpClientWrapper(); + URL url = new URL("http://localhost:" + port + "/broken"); + + HttpClientWrapper.Response response = client.executeCall(url, null); + + assertEquals(200, response.getStatusCode()); + // readBody returns either truncated content or null if stream throws + // The body should be empty string or null depending on timing + } } @Nested 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 8e18228afb..0e2525d6a1 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 @@ -23,6 +23,7 @@ import java.io.PrintStream; import java.net.ConnectException; import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -142,5 +143,67 @@ void unexpectedExceptionIsHandled() throws IOException { assertFalse(checker.check()); assertTrue(errStream.toString().contains("verify hostname and port")); } + + @Test + void unknownHostExceptionReportsResolutionFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenThrow(new UnknownHostException("zosmf.example.com")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("could not be resolved")); + } + } + + @Nested + class VerboseMode { + + @Test + void verbosePrintsResponseBody() throws IOException { + when(mockConf.isVerbose()).thenReturn(true); + when(mockClient.executeCall(any(), anyMap())).thenReturn( + new HttpClientWrapper.Response(200, "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"abc123\"}]}")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertTrue(checker.check()); + assertTrue(outStream.toString().contains("Response body:")); + } + } + + @Nested + class ValidateJwkBody { + + @Test + void nullBodyIsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, null)); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("empty response body")); + } + + @Test + void emptyBodyIsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("empty response body")); + } + + @Test + void bodyWithoutNKeyIsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\"}]}")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("does not contain an RSA modulus")); + } + } + + @Nested + class UnexpectedResponseCodes { + + @Test + void unexpectedResponseCodeIsFailure() throws IOException { + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(301, "")); + JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); + assertFalse(checker.check()); + assertTrue(errStream.toString().contains("unexpected response code")); + } } } diff --git a/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java b/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java index fb96efe76f..9bfc2d42ef 100644 --- a/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java +++ b/zosmf-jwt-check/src/test/java/org/zowe/apiml/SSLContextFactoryTest.java @@ -10,10 +10,18 @@ package org.zowe.apiml; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.URL; +import java.security.KeyStore; +import java.util.Collections; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; @@ -95,6 +103,42 @@ void socketFactoryIsAvailable() throws Exception { assertNotNull(factory.getSslContext().getSocketFactory()); } + + @Test + void trustAllContextAcceptsSelfSignedCert() throws Exception { + // Start a local HTTPS server with a self-signed cert + KeyStore ks = KeyStore.getInstance("PKCS12"); + try (InputStream is = new java.io.FileInputStream("../keystore/localhost/localhost.keystore.p12")) { + ks.load(is, "password".toCharArray()); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "password".toCharArray()); + SSLContext serverCtx = SSLContext.getInstance("TLSv1.2"); + serverCtx.init(kmf.getKeyManagers(), null, null); + + HttpsServer server = HttpsServer.create(new InetSocketAddress(0), 0); + server.setHttpsConfigurator(new HttpsConfigurator(serverCtx)); + server.createContext("/", exchange -> { + byte[] resp = "OK".getBytes(); + exchange.sendResponseHeaders(200, resp.length); + exchange.getResponseBody().write(resp); + exchange.getResponseBody().close(); + }); + server.start(); + + try { + int port = server.getAddress().getPort(); + SSLContextFactory factory = SSLContextFactory.initTrustAllSSLContext(); + HttpClientWrapper client = new HttpClientWrapper(factory.getSslContext(), (hostname, session) -> true); + HttpClientWrapper.Response response = client.executeCall( + new URL("https://localhost:" + port + "/"), Collections.emptyMap()); + + assertEquals(200, response.getStatusCode()); + assertEquals("OK", response.getBody()); + } finally { + server.stop(0); + } + } } private ZosmfJwtCheckConfig createConf(String keyStore, String keyStorePassword, 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 9da86cbcea..ea5499e2b3 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 @@ -115,4 +115,73 @@ void httpDoesNotRequireTruststore() { assertFalse(errStream.toString().contains("--truststore-file is required")); } } + + @Nested + class HttpsModes { + + @Test + void httpsStrictWithValidTruststoreAttemptsConnection() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "https", + "--truststore-file", "../keystore/localhost/localhost.truststore.p12", + "--truststore-password", "password", + "--verify-certificates", "STRICT"}; + int exitCode = ZosmfJwtCheck.mainWithExitCode(args); + assertEquals(4, exitCode); + assertFalse(errStream.toString().contains("--truststore-file is required")); + } + + @Test + void httpsNonstrictWithValidTruststoreAttemptsConnection() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "https", + "--truststore-file", "../keystore/localhost/localhost.truststore.p12", + "--truststore-password", "password", + "--verify-certificates", "NONSTRICT"}; + int exitCode = ZosmfJwtCheck.mainWithExitCode(args); + assertEquals(4, exitCode); + assertTrue(outStream.toString().contains("Hostname verification is disabled")); + } + + @Test + void httpsStrictWithKeystoreAndTruststoreAttemptsConnection() { + String[] args = {"--zosmf-host", "localhost", "--zosmf-port", "19999", "--scheme", "https", + "--keystore-file", "../keystore/localhost/localhost.keystore.p12", + "--keystore-password", "password", + "--keystore-type", "PKCS12", + "--truststore-file", "../keystore/localhost/localhost.truststore.p12", + "--truststore-password", "password", + "--truststore-type", "PKCS12", + "--verify-certificates", "STRICT"}; + int exitCode = ZosmfJwtCheck.mainWithExitCode(args); + assertEquals(4, exitCode); + } + } + + @Nested + class ConfAccessors { + + @Test + void allConfGettersReturnExpectedValues() { + ZosmfJwtCheckConf conf = new ZosmfJwtCheckConf(); + picocli.CommandLine cmd = new picocli.CommandLine(conf); + cmd.parseArgs("--zosmf-host", "myhost", "--zosmf-port", "10443", + "--scheme", "https", "--keystore-file", "/ks.p12", + "--keystore-password", "kspass", "--keystore-type", "JKS", + "--truststore-file", "/ts.p12", "--truststore-password", "tspass", + "--truststore-type", "PKCS12", "--verify-certificates", "NONSTRICT", + "-v"); + + assertEquals("myhost", conf.getZosmfHost()); + assertEquals(10443, conf.getZosmfPort()); + assertEquals("https", conf.getScheme()); + assertEquals("/ks.p12", conf.getKeyStore()); + assertEquals("kspass", conf.getKeyStorePassword()); + assertEquals("JKS", conf.getKeyStoreType()); + assertEquals("/ts.p12", conf.getTrustStore()); + assertEquals("tspass", conf.getTrustStorePassword()); + assertEquals("PKCS12", conf.getTrustStoreType()); + assertEquals("NONSTRICT", conf.getVerifyCertificates()); + assertTrue(conf.isVerbose()); + assertFalse(conf.isHelpRequested()); + } + } } From bbfee62409c27fed0b8c8f9911aa093e9d8fcc7a Mon Sep 17 00:00:00 2001 From: hrishikesh-nalawade Date: Wed, 3 Jun 2026 17:52:53 +0530 Subject: [PATCH 28/28] Parameterized Test Signed-off-by: hrishikesh-nalawade --- .../zowe/apiml/JwkEndpointCheckerTest.java | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) 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 0e2525d6a1..93f39af3ef 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 @@ -170,28 +170,18 @@ void verbosePrintsResponseBody() throws IOException { @Nested class ValidateJwkBody { - @Test - void nullBodyIsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, null)); - JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); - assertFalse(checker.check()); - assertTrue(errStream.toString().contains("empty response body")); - } - - @Test - void emptyBodyIsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "")); - JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); - assertFalse(checker.check()); - assertTrue(errStream.toString().contains("empty response body")); - } - - @Test - void bodyWithoutNKeyIsFailure() throws IOException { - when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\"}]}")); + @ParameterizedTest + @CsvSource(value = { + "NULL_BODY | empty response body", + "EMPTY_BODY | empty response body", + "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\"}]} | does not contain an RSA modulus" + }, delimiter = '|') + void invalidBodiesAreFailures(String body, String expectedError) throws IOException { + String resolvedBody = "NULL_BODY".equals(body.trim()) ? null : "EMPTY_BODY".equals(body.trim()) ? "" : body.trim(); + when(mockClient.executeCall(any(), anyMap())).thenReturn(new HttpClientWrapper.Response(200, resolvedBody)); JwkEndpointChecker checker = new JwkEndpointChecker(mockClient, mockConf); assertFalse(checker.check()); - assertTrue(errStream.toString().contains("does not contain an RSA modulus")); + assertTrue(errStream.toString().contains(expectedError.trim())); } }