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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.springframework.web.filter.OncePerRequestFilter;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.security.common.util.CertificateLoggingUtils;
import org.zowe.apiml.security.common.verify.CertificateValidator;

import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -57,6 +58,23 @@ public class CategorizeCertsFilter extends OncePerRequestFilter {

private final CertificateValidator certificateValidator;

/**
* Logs information about certificates that were ignored during authentication.
* Delegates to {@link CertificateLoggingUtils} for the actual logging implementation.
*
* @param originalCerts The original array of certificates before filtering
* @param filteredCerts The array of certificates after filtering for authentication
*/
private void logIgnoredCertificates(X509Certificate[] originalCerts, X509Certificate[] filteredCerts) {
CertificateLoggingUtils.logIgnoredCertificates(
originalCerts,
filteredCerts,
publicKeyCertificatesBase64,
log,
CategorizeCertsFilter::base64EncodePublicKey
);
}

/**
* Get certificates from request (if exists), separate them (to use only APIML certificate to request sign and
* other for authentication) and store again into request.
Expand All @@ -78,7 +96,12 @@ private void categorizeCerts(ServletRequest request) {
// 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));

X509Certificate[] headerCerts = new X509Certificate[]{(X509Certificate) clientCert.get()};
X509Certificate[] clientAuthCerts = selectCerts(headerCerts, certificateForClientAuth);
logIgnoredCertificates(headerCerts, clientAuthCerts);

httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, clientAuthCerts);
return;
} else if (isClientCertificateIgnored(httpServletRequest)) {
log.debug("Client certificate is ignored.");
Expand All @@ -88,7 +111,10 @@ private void categorizeCerts(ServletRequest request) {
}
}

httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth));
X509Certificate[] clientAuthCerts = selectCerts(certs, certificateForClientAuth);
logIgnoredCertificates(certs, clientAuthCerts);

httpServletRequest.setAttribute(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, clientAuthCerts);
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
@@ -0,0 +1,98 @@
/*
* 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.security.common.util;

import lombok.experimental.UtilityClass;
import org.slf4j.Logger;

import java.security.cert.X509Certificate;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* Utility class for logging certificate-related operations, particularly for logging
* certificates that were ignored/filtered during client authentication.
*/
@UtilityClass
public class CertificateLoggingUtils {

/**
* Logs information about certificates that were ignored during authentication.
* Compares the original set of certificates with the filtered set to identify ignored certificates.
* Uses Base64-encoded public keys for reliable comparison instead of X509Certificate object equality.
*
* @param originalCerts The original array of certificates before filtering
* @param filteredCerts The array of certificates after filtering for authentication
* @param publicKeyCertificatesBase64 Set of Base64-encoded public keys of known APIML certificates
* @param logger The logger to use for output
* @param base64Encoder Function to encode certificate public key to Base64
*/
public static void logIgnoredCertificates(

Check failure on line 39 in apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/CertificateLoggingUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZsJzXb0TbLXjo7bOY52&open=AZsJzXb0TbLXjo7bOY52&pullRequest=4415
X509Certificate[] originalCerts,
X509Certificate[] filteredCerts,
Set<String> publicKeyCertificatesBase64,
Logger logger,
Function<X509Certificate, String> base64Encoder
) {
if (originalCerts == null || originalCerts.length == 0) return;

Set<String> originalKeys = Arrays.stream(originalCerts)
.map(base64Encoder)
.collect(Collectors.toSet());

Set<String> filteredKeys = filteredCerts != null
? Arrays.stream(filteredCerts)
.map(base64Encoder)
.collect(Collectors.toSet())
: new HashSet<>();

Set<String> ignoredKeys = new HashSet<>(originalKeys);
ignoredKeys.removeAll(filteredKeys);

if (!ignoredKeys.isEmpty()) {
List<X509Certificate> ignoredCerts = Arrays.stream(originalCerts)
.filter(cert -> ignoredKeys.contains(base64Encoder.apply(cert)))
.collect(Collectors.toList());

Check warning on line 64 in apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/CertificateLoggingUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZsJzXb0TbLXjo7bOY53&open=AZsJzXb0TbLXjo7bOY53&pullRequest=4415

logger.debug("Certificates ignored/not used for authentication: {}",
ignoredCerts.stream()
.map(cert -> {
String subjectDN = cert.getSubjectX500Principal() != null
? cert.getSubjectX500Principal().getName()
: "Unknown";

Check failure on line 71 in apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/CertificateLoggingUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Unknown" 3 times.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZsJzXb0TbLXjo7bOY51&open=AZsJzXb0TbLXjo7bOY51&pullRequest=4415
String issuerDN = cert.getIssuerX500Principal() != null
? cert.getIssuerX500Principal().getName()
: "Unknown";
String publicKeyBase64 = base64Encoder.apply(cert);
return String.format("[Subject: %s, Issuer: %s, Public Key (first 20 chars): %s...]",
subjectDN, issuerDN, publicKeyBase64.substring(0, Math.min(20, publicKeyBase64.length())));
})
.collect(Collectors.joining(", ")));

Check warning on line 79 in apiml-security-common/src/main/java/org/zowe/apiml/security/common/util/CertificateLoggingUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Invoke method(s) only conditionally.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZsJzXb0TbLXjo7bOY50&open=AZsJzXb0TbLXjo7bOY50&pullRequest=4415

ignoredCerts.forEach(cert -> {
String publicKeyBase64 = base64Encoder.apply(cert);
boolean isApimlCert = publicKeyCertificatesBase64.contains(publicKeyBase64);
String subjectDN = cert.getSubjectX500Principal() != null
? cert.getSubjectX500Principal().getName()
: "Unknown";
if (isApimlCert) {
logger.debug("Certificate with subject '{}' was ignored because it is an APIML Gateway certificate (not used for client authentication)",
subjectDN);
} else {
logger.debug("Certificate with subject '{}' was ignored for unknown reason (not in APIML cert set, but filtered by predicate)",
subjectDN);
}
});
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@

package org.zowe.apiml.security.common.filter;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
Expand All @@ -32,6 +38,8 @@
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
Expand Down Expand Up @@ -72,6 +80,9 @@

private CertificateValidator certificateValidator;

private Logger logger;
private ListAppender<ILoggingEvent> logAppender;

@BeforeAll
public static void init() throws CertificateException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Expand All @@ -87,6 +98,19 @@
certificateValidator = mock(CertificateValidator.class);
when(certificateValidator.isForwardingEnabled()).thenReturn(false);
when(certificateValidator.hasGatewayChain(any())).thenReturn(false);

logger = (Logger) LoggerFactory.getLogger(CategorizeCertsFilter.class);
logAppender = new ListAppender<>();
logAppender.start();
logger.addAppender(logAppender);
logger.setLevel(Level.DEBUG);
}

@AfterEach
public void tearDown() {

Check warning on line 110 in apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZsJzXeWTbLXjo7bOY54&open=AZsJzXeWTbLXjo7bOY54&pullRequest=4415
if (logger != null && logAppender != null) {
logger.detachAppender(logAppender);
}
}

@Nested
Expand Down Expand Up @@ -548,4 +572,82 @@
assertNotNull(chain.getRequest(), "Filter chain should continue normally");
}
}

@Nested
class LogIgnoredCertificatesTests {

@BeforeEach
void setUp() {
var serverCertChain = new HashSet<>(Arrays.asList(
X509Utils.correctBase64("apimlCert1"),
X509Utils.correctBase64("apimlCertCA")
));
filter = new CategorizeCertsFilter(serverCertChain, certificateValidator);
}

@Test
void whenApimlCertIsIgnored_thenLogsCorrectly() throws ServletException, IOException {
X509Certificate[] certs = new X509Certificate[]{
X509Utils.getCertificate(X509Utils.correctBase64("apimlCert1")),
X509Utils.getCertificate(X509Utils.correctBase64("apimlCertCA"))
};
request.setAttribute("jakarta.servlet.request.X509Certificate", certs);

filter.doFilter(request, response, chain);
List<ILoggingEvent> logsList = logAppender.list;

List<ILoggingEvent> ignoredCertLogs = logsList.stream()
.filter(event -> event.getMessage().contains("ignored"))
.collect(Collectors.toList());

Check warning on line 601 in apiml-security-common/src/test/java/org/zowe/apiml/security/common/filter/CategorizeCertsFilterTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZsJzXeWTbLXjo7bOY55&open=AZsJzXeWTbLXjo7bOY55&pullRequest=4415

assertFalse(ignoredCertLogs.isEmpty(), "Should have logged information about ignored certificates");

boolean hasSummaryLog = logsList.stream()
.anyMatch(event -> event.getMessage().contains("Certificates ignored/not used for authentication"));
assertTrue(hasSummaryLog, "Should have summary log about ignored certificates");

boolean hasDetailedLog = logsList.stream()
.anyMatch(event -> event.getFormattedMessage().contains("is an APIML Gateway certificate"));
assertTrue(hasDetailedLog, "Should explain that certificate is an APIML Gateway certificate");
}

@Test
void whenOnlyClientCert_thenNoIgnoredLogs() throws ServletException, IOException {
filter = new CategorizeCertsFilter(new HashSet<>(), certificateValidator);

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

filter.doFilter(request, response, chain);

List<ILoggingEvent> logsList = logAppender.list;

boolean hasIgnoredLog = logsList.stream()
.anyMatch(event -> event.getMessage().contains("Certificates ignored/not used for authentication"));
assertFalse(hasIgnoredLog, "Should NOT log ignored certificates when only client cert is present");
}

@Test
void whenMixedCertChain_thenLogsOnlyIgnoredOnes() throws ServletException, IOException {
X509Certificate[] certs = new X509Certificate[]{
X509Utils.getCertificate(X509Utils.correctBase64("foreignCert1")),
X509Utils.getCertificate(X509Utils.correctBase64("apimlCert1"))
};
request.setAttribute("jakarta.servlet.request.X509Certificate", certs);

filter.doFilter(request, response, chain);

List<ILoggingEvent> logsList = logAppender.list;

boolean hasIgnoredLog = logsList.stream()
.anyMatch(event -> event.getMessage().contains("Certificates ignored/not used for authentication"));
assertTrue(hasIgnoredLog, "Should log ignored certificates in mixed chain");

boolean mentionsApimlCert = logsList.stream()
.anyMatch(event -> event.getFormattedMessage().contains("is an APIML Gateway certificate"));
assertTrue(mentionsApimlCert, "Should mention ignored APIML certificate");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.springframework.web.server.WebFilterChain;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.security.common.util.CertificateLoggingUtils;
import org.zowe.apiml.security.common.verify.CertificateValidator;
import reactor.core.publisher.Mono;

Expand Down Expand Up @@ -97,6 +98,9 @@ private ServerWebExchange categorizeCerts(ServerWebExchange exchange) {
new X509Certificate[]{clientCertFromHeader.get()},
certificateForClientAuth
);

logIgnoredCertificates(new X509Certificate[]{clientCertFromHeader.get()}, clientAuthCerts);

exchange.getAttributes().put(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, clientAuthCerts);
log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, Arrays.toString(clientAuthCerts));

Expand All @@ -108,6 +112,9 @@ private ServerWebExchange categorizeCerts(ServerWebExchange exchange) {

} else {
X509Certificate[] clientAuthCerts = selectCerts(certsFromTls, certificateForClientAuth);

logIgnoredCertificates(certsFromTls, clientAuthCerts);

exchange.getAttributes().put(ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, clientAuthCerts);
log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTR_NAME_CLIENT_AUTH_X509_CERTIFICATE, Arrays.toString(clientAuthCerts));

Expand All @@ -127,6 +134,23 @@ private ServerWebExchange categorizeCerts(ServerWebExchange exchange) {
return exchange.mutate().request(requestBuilder.build()).build();
}

/**
* Logs information about certificates that were ignored during authentication.
* Delegates to {@link CertificateLoggingUtils} for the actual logging implementation.
*
* @param originalCerts The original array of certificates before filtering
* @param filteredCerts The array of certificates after filtering for authentication
*/
private void logIgnoredCertificates(X509Certificate[] originalCerts, X509Certificate[] filteredCerts) {
CertificateLoggingUtils.logIgnoredCertificates(
originalCerts,
filteredCerts,
publicKeyCertificatesBase64,
log,
CategorizeCertsWebFilter::base64EncodePublicKey
);
}

/**
* Extracts and decodes an X.509 certificate from the CLIENT_CERT_HEADER.
*
Expand Down
Loading
Loading