Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import java.util.*;
import java.util.function.Predicate;

import static org.zowe.apiml.util.ServletRequestUtils.isClientCertificateIgnored;

/**
* This filter processes certificates on request. It decides, which certificates are considered for client authentication
*/
Expand Down Expand Up @@ -66,21 +68,30 @@ public class CategorizeCertsFilter extends OncePerRequestFilter {
* @param request Request to filter certificates
*/
private void categorizeCerts(ServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE);
var httpServletRequest = (HttpServletRequest) request;
var certs = (X509Certificate[]) httpServletRequest.getAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE);
if (certs != null && certs.length > 0) {
Optional<Certificate> clientCert = getClientCertFromHeader((HttpServletRequest) request);
if (certificateValidator.isForwardingEnabled() && certificateValidator.hasGatewayChain(certs) && clientCert.isPresent()) {
certificateValidator.updateAPIMLPublicKeyCertificates(certs);
// add the client certificate to the certs array
String subjectDN = ((X509Certificate) clientCert.get()).getSubjectX500Principal().getName();
log.debug("Found client certificate in header, adding it to the request. Subject DN: {}", subjectDN);
request.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(new X509Certificate[]{(X509Certificate) clientCert.get()}, certificateForClientAuth));
} else {
request.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth));
request.setAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate));
Optional<Certificate> clientCert = getClientCertFromHeader(httpServletRequest);
if (certificateValidator.isForwardingEnabled() && certificateValidator.hasGatewayChain(certs)) {
if (clientCert.isPresent()) {
certificateValidator.updateAPIMLPublicKeyCertificates(certs);
// add the client certificate to the certs array
String subjectDN = ((X509Certificate) clientCert.get()).getSubjectX500Principal().getName();
log.debug("Found client certificate in header, adding it to the request. Subject DN: {}", subjectDN);
httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(new X509Certificate[]{(X509Certificate) clientCert.get()}, certificateForClientAuth));
return;
} else if (isClientCertificateIgnored(httpServletRequest)) {
log.debug("Client certificate is ignored.");
httpServletRequest.removeAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE);
httpServletRequest.removeAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE);
return;
}
}

log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, request.getAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE));
httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth));
httpServletRequest.setAttribute(ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate));

log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, httpServletRequest.getAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,66 @@ void thenClientCertHeaderIgnored() throws ServletException, IOException {
}
}
}

@Nested
class GivenNoCertificateHeader {

@BeforeEach
void setUp() {
certificateValidator = mock(CertificateValidator.class);
when(certificateValidator.isForwardingEnabled()).thenReturn(true);
when(certificateValidator.hasGatewayChain(any())).thenReturn(true);

filter = new CategorizeCertsFilter(new HashSet<>(), certificateValidator);
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
chain = new MockFilterChain();
}

@Test
void whenClientCertHeaderEmpty_thenAttributesRemovedAndReturn() throws ServletException, IOException {
X509Certificate[] certs = new X509Certificate[]{
X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1"))
};
request.setAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, certs);

request.addHeader(CategorizeCertsFilter.CLIENT_CERT_HEADER, "");

filter.doFilter(request, response, chain);

assertNull(request.getAttribute(CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE),
"Client auth cert attribute should be removed");
assertNull(request.getAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE),
"Jakarta servlet cert attribute should be removed");

assertNotNull(chain.getRequest());
}

@Test
void whenClientCertHeaderNotDefined_thenReturnFalse() throws ServletException, IOException {

filter = new CategorizeCertsFilter(new HashSet<>(), certificateValidator);

X509Certificate[] certs = new X509Certificate[]{
X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1"))
};
request.setAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE, certs);
request.setAttribute(CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, certs);

request.addHeader(CategorizeCertsFilter.CLIENT_CERT_HEADER, "");

filter.doFilter(request, response, chain);

assertNull(
request.getAttribute(CategorizeCertsFilter.ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE),
"Expected client.auth.X509Certificate attribute to be removed"
);
assertNull(
request.getAttribute(CategorizeCertsFilter.ATTR_NAME_JAKARTA_SERVLET_REQUEST_X509_CERTIFICATE),
"Expected jakarta.servlet.request.X509Certificate attribute to be removed"
);

assertNotNull(chain.getRequest(), "Filter chain should continue normally");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter;
import org.zowe.apiml.util.ServletRequestUtils;
import org.zowe.commons.attls.InboundAttls;

import java.io.ByteArrayInputStream;
Expand All @@ -34,17 +35,24 @@
@Slf4j
public class AttlsFilter extends OncePerRequestFilter {

private static final String CLIENT_CERT_HEADER = "Client-Cert";

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
try {
byte[] certificate = InboundAttls.getCertificate();
if (certificate != null && certificate.length > 0) {
log.debug("Certificate length: {}", certificate.length);
populateRequestWithCertificate(request, certificate);
if (ServletRequestUtils.isClientCertificateIgnored(request)) {
log.debug("Client certificate is ignored.");
} else {
log.debug("Updating request with client certificate from the AT-TLS context.");
try {
byte[] certificate = InboundAttls.getCertificate();
if (certificate != null && certificate.length > 0) {
log.debug("Certificate length: {}", certificate.length);
populateRequestWithCertificate(request, certificate);
}
} catch (Exception e) {
logger.error("Not possible to get certificate from AT-TLS context", e);
AttlsErrorHandler.handleError(response, "Exception reading certificate");
}
} catch (Exception e) {
logger.error("Not possible to get certificate from AT-TLS context", e);
AttlsErrorHandler.handleError(response, "Exception reading certificate");
}
filterChain.doFilter(request, response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ ServerHttpRequest updateCertificate(ServerHttpRequest request, HttpServletReques
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HttpHandler httpHandler) {
return (HttpHandler) (request, response) -> {
log.debug("Initialize reactive AT-TLS handler");
try {
var attlsContext = InboundAttls.get();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.SocketChannel;

/**
* Customizes Tomcat connectors to enable AT-TLS support.
* <p>
* This component replaces the default Tomcat socket handler with a custom
* {@link ApimlAttlsHandler} that initializes and disposes AT-TLS contexts for
* each incoming connection. It allows the API ML to operate in AT-TLS mode on z/OS.
* </p>
*
* <p>
* Activated when <code>server.attlsServer.enabled=true</code> is set.
* </p>
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "server.attlsServer.enabled", havingValue = "true")
Expand Down Expand Up @@ -61,6 +73,10 @@ public void customize(Connector connector) {
}
}

/**
* Custom Tomcat socket handler that wraps request processing with AT-TLS context
* initialization and cleanup.
*/
public static class ApimlAttlsHandler<S> implements AbstractEndpoint.Handler<S> {

@Delegate(excludes = Overridden.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
* This bean is related only to z/OS.
*/
@Slf4j
@Configuration
@Configuration(proxyBeanMethods = false)
public class TomcatAcceptFixConfig {

@Value("${server.tomcat.retryRebindTimeoutSecs:10}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Customizer for Tomcat SSL configuration that fixes SAF keyring URLs.
* <p>
* If a keyStore or trustStore uses a SAF keyring (e.g. {@code safkeyring:///USER/KEYRING}),
* this class reformats the URL and sets default passwords if none are provided.
* </p>
*/
@Slf4j
@Component
public class TomcatKeyringFix implements WebServerFactoryCustomizer<AbstractConfigurableWebServerFactory> {
Expand Down Expand Up @@ -51,6 +58,17 @@ boolean isKeyring(String input) {
return matcher.matches();
}

/**
* Normalizes the given keyring URL into a Tomcat-compatible format.
* <p>
* For example, converts:
* <pre>
* safkeyring:///USER/KEYRING → safkeyring://USER/KEYRING
* </pre>
*
* @param keyringUrl the original keyring URL
* @return the normalized URL or {@code null} if the input was {@code null}
*/
static String formatKeyringUrl(String keyringUrl) {
if (keyringUrl == null) return null;
Matcher matcher = KEYRING_PATTERN.matcher(keyringUrl);
Expand All @@ -60,6 +78,16 @@ static String formatKeyringUrl(String keyringUrl) {
return keyringUrl;
}

/**
* Customizes the Tomcat {@link Ssl} configuration before the web server starts.
* <p>
* If keyring-based stores are detected, this method updates their configuration
* (URL format, alias, and password) to ensure that Tomcat can correctly access
* the keyring at runtime.
* </p>
*
* @param factory the Tomcat web server factory being customized
*/
@Override
public void customize(AbstractConfigurableWebServerFactory factory) {
Ssl ssl = factory.getSsl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.tomcat.util.codec.binary.Base64;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.zowe.commons.attls.ContextIsNotInitializedException;
import org.zowe.commons.attls.InboundAttls;

import java.io.IOException;
import java.security.cert.CertificateException;
import java.util.Base64;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.*;

class AttlsFilterTest {

@Test
void providedCertificateInCorrectFormat_thenPopulateRequest() throws CertificateException, ContextIsNotInitializedException {
void providedCertificateInCorrectFormat_thenPopulateRequest() throws CertificateException {
AttlsFilter attlsFilter = new AttlsFilter();
String certificate = """
MIID8TCCAtmgAwIBAgIUVyBCWfHF/ZwZKVsBEpTNIBj9mQcwDQYJKoZIhvcNAQEL
Expand Down Expand Up @@ -58,7 +59,8 @@ void providedCertificateInCorrectFormat_thenPopulateRequest() throws Certificate
""".stripIndent();

HttpServletRequest request = new MockHttpServletRequest();
attlsFilter.populateRequestWithCertificate(request, Base64.decodeBase64(certificate));
byte[] decoded = Base64.getMimeDecoder().decode(certificate);
attlsFilter.populateRequestWithCertificate(request, decoded);
assertNotNull(request.getAttribute("jakarta.servlet.request.X509Certificate"));
}

Expand All @@ -71,4 +73,38 @@ void whenExceptionOccurs_thenCreateCorrectResponse() throws ServletException, IO
assertEquals(500, response.getStatus());
}

@Test
void whenClientCertIsEmpty_thenIgnoreCertAndContinueWithChain() throws ServletException, IOException {
AttlsFilter filter = new AttlsFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Client-Cert", "");

FilterChain chain = mock(FilterChain.class);
HttpServletResponse response = new MockHttpServletResponse();
filter.doFilterInternal(request, response, chain);
assertEquals(200, response.getStatus());
}

@Test
void whenValidCertificateFromInboundAttls_thenPopulateRequestAndContinueChain() throws ServletException, IOException, CertificateException {
AttlsFilter filter = spy(new AttlsFilter());
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);

byte[] dummyCert = "dummyCertificate".getBytes();

try (MockedStatic<InboundAttls> mockedInboundAttls = mockStatic(InboundAttls.class)) {
mockedInboundAttls.when(InboundAttls::getCertificate).thenReturn(dummyCert);

doNothing().when(filter).populateRequestWithCertificate(request, dummyCert);

filter.doFilterInternal(request, response, chain);

mockedInboundAttls.verify(InboundAttls::getCertificate);
verify(filter, times(1)).populateRequestWithCertificate(request, dummyCert);
verify(chain, times(1)).doFilter(request, response);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.util;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

/**
* Utility class providing helper methods for working with {@link jakarta.servlet.http.HttpServletRequest}.
*/
@Slf4j
public class ServletRequestUtils {

private static final String CLIENT_CERT_HEADER = "Client-Cert";

/**
* Determines whether the client certificate should be ignored based on the Client-Cert HTTP header.
* @param request the HTTP request to inspect
* @return true if the client certificate should be ignored, false otherwise.
*/
public static boolean isClientCertificateIgnored(HttpServletRequest request) {
var forwardedClientCertificate = request.getHeader(CLIENT_CERT_HEADER);
if (forwardedClientCertificate == null) {
// no header means the certificate shouldn't be removed
log.debug("Request header Client-Cert was not defined.");
return false;
}
// empty header means to ignore the certificate from the request
return StringUtils.isBlank(forwardedClientCertificate);
}
}
Loading
Loading