Skip to content

Commit

Permalink
feat: Forward client certificate from central gateway to domain gatew…
Browse files Browse the repository at this point in the history
…ay in request header (#3046)

* Add new filter for re-sending auth source.

Signed-off-by: Petr Weinfurt <[email protected]>

* add license text

Signed-off-by: Petr Weinfurt <[email protected]>

* Read certificate from request header

Signed-off-by: at670475 <[email protected]>

* add integration test

Signed-off-by: at670475 <[email protected]>

* add header to test

Signed-off-by: at670475 <[email protected]>

* add unit test

Signed-off-by: at670475 <[email protected]>

* add error to the log

Signed-off-by: at670475 <[email protected]>

* optmize code and check for certificate in the attibute

Signed-off-by: at670475 <[email protected]>

* Add certificate signature to the additional header

Signed-off-by: Petr Weinfurt <[email protected]>

* Add public rest endpoint to provide JWK set.

Signed-off-by: Petr Weinfurt <[email protected]>

* WellKnownRestController and tests

Signed-off-by: Petr Weinfurt <[email protected]>

* add licenses

Signed-off-by: Petr Weinfurt <[email protected]>

* Add simple integration test

Signed-off-by: Petr Weinfurt <[email protected]>

* Add integration tests for well-known endpoint.

Signed-off-by: Petr Weinfurt <[email protected]>

* Add some javadoc.

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix integration test

Signed-off-by: Petr Weinfurt <[email protected]>

* validate cert

Signed-off-by: at670475 <[email protected]>

* Simplify retrieving the public key.

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix for integration tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Cloud gateway provides certificate chain on public endpoint (instead of public key). Certificate is sent in Client-Cert header.

Signed-off-by: Petr Weinfurt <[email protected]>

* Cloud gateway implementation fixes

Signed-off-by: Petr Weinfurt <[email protected]>

* Categorize certs filter WIP

Signed-off-by: Petr Weinfurt <[email protected]>

* Merge with master branch

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix casting exception

Signed-off-by: Petr Weinfurt <[email protected]>

* Cleanup. Add log messages.

Signed-off-by: Petr Weinfurt <[email protected]>

* Fixes

Signed-off-by: Petr Weinfurt <[email protected]>

* Add tests for CategorizeCertsFilter

Signed-off-by: Petr Weinfurt <[email protected]>

* Add CertificateValidatorTest

Signed-off-by: Petr Weinfurt <[email protected]>

* Add caching trusted certificates

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix message types

Signed-off-by: Petr Weinfurt <[email protected]>

* Add javadoc

Signed-off-by: Petr Weinfurt <[email protected]>

* remove unnecessary dependency

Signed-off-by: Petr Weinfurt <[email protected]>

* Prepare SSL Auth for integration test.

Signed-off-by: Petr Weinfurt <[email protected]>

* Remove obsolete test

Signed-off-by: Petr Weinfurt <[email protected]>

* Add new runtime variables to start.sh files.

Signed-off-by: Petr Weinfurt <[email protected]>

* Add new options to manifest.yaml files

Signed-off-by: Petr Weinfurt <[email protected]>

* fix integration test

Signed-off-by: Petr Weinfurt <[email protected]>

* Update IT

Signed-off-by: Petr Weinfurt <[email protected]>

* Update keystore for IT

Signed-off-by: Petr Weinfurt <[email protected]>

* Update error messages

Signed-off-by: Petr Weinfurt <[email protected]>

* Update common name in the certificate

Signed-off-by: Petr Weinfurt <[email protected]>

* Update common name in the certificate

Signed-off-by: Petr Weinfurt <[email protected]>

* Update common name in the certificate

Signed-off-by: Petr Weinfurt <[email protected]>

* Update common name in the certificate

Signed-off-by: Petr Weinfurt <[email protected]>

* Add tests for CategorizeCertsFilter

Signed-off-by: Petr Weinfurt <[email protected]>

* Add CA certificate to keystore

Signed-off-by: Petr Weinfurt <[email protected]>

* Add unit tests

Signed-off-by: Petr Weinfurt <[email protected]>

* disable forwarded cert in gateway on localhost

Signed-off-by: Petr Weinfurt <[email protected]>

* Resolve code review

Signed-off-by: Petr Weinfurt <[email protected]>

* Increase coverage

Signed-off-by: Petr Weinfurt <[email protected]>

* Externalize the update public keys function to the CategorizeCertsFilter. Update tests.

Signed-off-by: Petr Weinfurt <[email protected]>

* Increase coverage.

Signed-off-by: Petr Weinfurt <[email protected]>

---------

Signed-off-by: Petr Weinfurt <[email protected]>
Signed-off-by: at670475 <[email protected]>
Co-authored-by: Andrea Tabone <[email protected]>
Co-authored-by: achmelo <[email protected]>
  • Loading branch information
3 people authored Aug 31, 2023
1 parent 3c1bb91 commit eda4750
Show file tree
Hide file tree
Showing 37 changed files with 1,986 additions and 69 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ jobs:
image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }}
gateway-service:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true
APIML_SECURITY_X509_CERTIFICATESURL: https://cloud-gateway-service:10023/gateway/certificates
gateway-service-2:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
Expand All @@ -150,11 +153,12 @@ jobs:
image: ghcr.io/balhar-jakub/cloud-gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_GATEWAY_TIMEOUT: 2
APIML_SERVICE_FORWARDCLIENTCERTENABLED: true
discoverable-client:
image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }}

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}

Expand Down Expand Up @@ -198,7 +202,7 @@ jobs:
mock-services:
image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.zowe.apiml.security.common.filter.CategorizeCertsFilter;
import org.zowe.apiml.security.common.login.LoginFilter;
import org.zowe.apiml.security.common.login.ShouldBeAlreadyAuthenticatedFilter;
import org.zowe.apiml.security.common.verify.CertificateValidator;

import java.util.Collections;
import java.util.Set;
Expand All @@ -69,6 +70,7 @@ public class SecurityConfiguration {
private final HandlerInitializer handlerInitializer;
private final GatewayLoginProvider gatewayLoginProvider;
private final GatewayTokenProvider gatewayTokenProvider;
private final CertificateValidator certificateValidator;
@Qualifier("publicKeyCertificatesBase64")
private final Set<String> publicKeyCertificatesBase64;
@Value("${server.attls.enabled:false}")
Expand Down Expand Up @@ -124,9 +126,9 @@ private UserDetailsService x509UserDetailsService() {
}

private CategorizeCertsFilter reversedCategorizeCertFilter() {
CategorizeCertsFilter out = new CategorizeCertsFilter(publicKeyCertificatesBase64);
CategorizeCertsFilter out = new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator);
out.setCertificateForClientAuth(crt -> out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt)));
out.setNotCertificateForClientAuth(crt -> !out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt)));
out.setApimlCertificate(crt -> !out.getPublicKeyCertificatesBase64().contains(out.base64EncodePublicKey(crt)));
return out;
}
}
Expand Down
1 change: 1 addition & 0 deletions apiml-security-common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
implementation libs.apache.commons.lang3
implementation libs.bcprov;
implementation libs.bcpkix;
implementation libs.http.client;

implementation libs.spring.aop
implementation libs.spring.beans
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,28 @@

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

import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.verify.CertificateValidator;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Base64;
import java.util.Set;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand All @@ -38,28 +45,91 @@ public class CategorizeCertsFilter extends OncePerRequestFilter {
private static final String ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE = "client.auth.X509Certificate";
private static final String ATTRNAME_JAVAX_SERVLET_REQUEST_X509_CERTIFICATE = "javax.servlet.request.X509Certificate";
private static final String LOG_FORMAT_FILTERING_CERTIFICATES = "Filtering certificates: {} -> {}";
private static final String CLIENT_CERT_HEADER = "Client-Cert";

@InjectApimlLogger
private final ApimlLogger apimlLog = ApimlLogger.empty();

@Getter
private final Set<String> publicKeyCertificatesBase64;

public Set<String> getPublicKeyCertificatesBase64() {
return publicKeyCertificatesBase64;
}
private final CertificateValidator certificateValidator;

/**
* 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.
* If authentication via certificate in header is enabled, get client certificate from the Client-Cert header
* only if the request certificates are all trusted and validated by {@link CertificateValidator} service.
* The client certificate is then stored in a separate custom request attribute. The APIML certificates stays
* in the default attribute.
*
* @param request Request to filter certificates
*/
private void categorizeCerts(ServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(ATTRNAME_JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
if (certs != null) {
if (certificateValidator.isForwardingEnabled() && certificateValidator.isTrusted(certs)) {
certificateValidator.updateAPIMLPublicKeyCertificates(certs);
Optional<Certificate> clientCert = getClientCertFromHeader((HttpServletRequest) request);
if (clientCert.isPresent()) {
// 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);
certs = Arrays.copyOf(certs, certs.length + 1);
certs[certs.length - 1] = (X509Certificate) clientCert.get();
}
}

request.setAttribute(ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE, selectCerts(certs, certificateForClientAuth));
request.setAttribute(ATTRNAME_JAVAX_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, notCertificateForClientAuth));
request.setAttribute(ATTRNAME_JAVAX_SERVLET_REQUEST_X509_CERTIFICATE, selectCerts(certs, apimlCertificate));
log.debug(LOG_FORMAT_FILTERING_CERTIFICATES, ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE, request.getAttribute(ATTRNAME_CLIENT_AUTH_X509_CERTIFICATE));
}
}

private Optional<Certificate> getClientCertFromHeader(HttpServletRequest request) {
String certFromHeader = request.getHeader(CLIENT_CERT_HEADER);

if (StringUtils.isNotEmpty(certFromHeader)) {
try {
Certificate certificate = CertificateFactory
.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(certFromHeader)));
return Optional.of(certificate);
} catch (Exception e) {
apimlLog.log("org.zowe.apiml.security.common.filter.errorParsingCertificate", e.getMessage(), certFromHeader);
}
}
return Optional.empty();
}

/**
* Wraps the http servlet request into wrapper which makes sure that the Client-Cert header
* will not be accessible anymore from the request
*
* @param originalRequest incoming original http request object
* @return wrapped http request object with overridden functions
*/
private HttpServletRequest mutate(HttpServletRequest originalRequest) {
return new HttpServletRequestWrapper(originalRequest) {

@Override
public String getHeader(String name) {
if (CLIENT_CERT_HEADER.equalsIgnoreCase(name)) {
return null;
}
return super.getHeader(name);
}

@Override
public Enumeration<String> getHeaders(String name) {
if (CLIENT_CERT_HEADER.equalsIgnoreCase(name)) {
return Collections.enumeration(new LinkedList<>());
}
return super.getHeaders(name);
}
};
}

/**
* This filter removes all certificates in attribute "javax.servlet.request.X509Certificate" which has no relations
* with private certificate of apiml and then call original implementation (without "foreign" certificates)
Expand All @@ -71,7 +141,7 @@ private void categorizeCerts(ServletRequest request) {
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
categorizeCerts(request);
filterChain.doFilter(request, response);
filterChain.doFilter(mutate(request), response);
}

private X509Certificate[] selectCerts(X509Certificate[] certs, Predicate<X509Certificate> test) {
Expand All @@ -88,12 +158,12 @@ public void setCertificateForClientAuth(Predicate<X509Certificate> certificateFo
this.certificateForClientAuth = certificateForClientAuth;
}

public void setNotCertificateForClientAuth(Predicate<X509Certificate> notCertificateForClientAuth) {
this.notCertificateForClientAuth = notCertificateForClientAuth;
public void setApimlCertificate(Predicate<X509Certificate> apimlCertificate) {
this.apimlCertificate = apimlCertificate;
}

Predicate<X509Certificate> certificateForClientAuth = crt -> !getPublicKeyCertificatesBase64().contains(base64EncodePublicKey(crt));
Predicate<X509Certificate> notCertificateForClientAuth = crt -> getPublicKeyCertificatesBase64().contains(base64EncodePublicKey(crt));
Predicate<X509Certificate> apimlCertificate = crt -> getPublicKeyCertificatesBase64().contains(base64EncodePublicKey(crt));


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

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;

import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;
import java.util.Set;

/**
* Service to verify if given certificate chain can be trusted.
*/
@Service
@Slf4j
public class CertificateValidator {

final TrustedCertificatesProvider trustedCertificatesProvider;

@InjectApimlLogger
private final ApimlLogger apimlLog = ApimlLogger.empty();

@Getter
@Value("${apiml.security.x509.acceptForwardedCert:false}")
private boolean forwardingEnabled;

@Value("${apiml.security.x509.certificatesUrl:}")
private String proxyCertificatesEndpoint;
private final Set<String> publicKeyCertificatesBase64;


@Autowired
public CertificateValidator(TrustedCertificatesProvider trustedCertificatesProvider,
@Qualifier("publicKeyCertificatesBase64") Set<String> publicKeyCertificatesBase64) {
this.trustedCertificatesProvider = trustedCertificatesProvider;
this.publicKeyCertificatesBase64 = publicKeyCertificatesBase64;
}

/**
* Compare given certificates with a list of trusted certs.
*
* @param certs Certificates to compare with known trusted ones
* @return true if all given certificates are known false otherwise
*/
public boolean isTrusted(X509Certificate[] certs) {
List<Certificate> trustedCerts = trustedCertificatesProvider.getTrustedCerts(proxyCertificatesEndpoint);
for (X509Certificate cert : certs) {
if (!trustedCerts.contains(cert)) {
apimlLog.log("org.zowe.apiml.security.common.verify.untrustedCert");
log.debug("Untrusted certificate is {}", cert);
return false;
}
}
log.debug("All certificates are trusted.");
return true;
}

/**
* Updates the list of public keys from certificates that belong to APIML
*
* @param certs List of certificates coming from the central Gateway
*/
public void updateAPIMLPublicKeyCertificates(X509Certificate[] certs) {
for (X509Certificate cert : certs) {
String publicKey = Base64.getEncoder().encodeToString(cert.getPublicKey().getEncoded());
publicKeyCertificatesBase64.add(publicKey);
}
}
}
Loading

0 comments on commit eda4750

Please sign in to comment.