diff --git a/docs/documentation/release_notes/topics/26_5_0.adoc b/docs/documentation/release_notes/topics/26_5_0.adoc index 91a1ed4ffeb..ab9183e53a1 100644 --- a/docs/documentation/release_notes/topics/26_5_0.adoc +++ b/docs/documentation/release_notes/topics/26_5_0.adoc @@ -137,6 +137,12 @@ The `feature-` option takes precedence over both `features` and `features- For more details, see the https://www.keycloak.org/server/features[Enabling and disabling features] guide. +== Client certificate lookup compliant with RFC 9440 + +You can now use a new client certificate lookup provider that is compliant with https://datatracker.ietf.org/doc/html/rfc9440[RFC 9440]. +This enables native support e.g. for Caddy and other reverse proxies that follow the RFC. +For details, navigate to link:{server_guide_base_link}/reverseproxy#_enabling_client_certificate_lookup[Enabling Client Certificate Lookup] section of the documentation. + = Observability == Export traces with custom request headers diff --git a/docs/guides/server/reverseproxy.adoc b/docs/guides/server/reverseproxy.adoc index 41296af1649..2847c3bc779 100644 --- a/docs/guides/server/reverseproxy.adoc +++ b/docs/guides/server/reverseproxy.adoc @@ -198,6 +198,9 @@ The server supports some of the most commons TLS termination proxies such as: |nginx |=== +Besides the providers for particular proxies from the table above, there is also an implementation for proxies that are compliant to link:https://datatracker.ietf.org/doc/rfc9440/[RFC 9440]. +Please refer to the provider by the name `rfc9440`. + To configure how client certificates are retrieved from the requests you need to: .Enable the corresponding proxy provider @@ -213,24 +216,35 @@ The available options for configuring a provider are: [%autowidth] |=== -|Option|Description +|Option|Description|Supporting Providers |ssl-client-cert | The name of the header holding the client certificate +| all |ssl-cert-chain-prefix | The prefix of the headers holding additional certificates in the chain and used to retrieve individual certificates accordingly to the length of the chain. For instance, a value `CERT_CHAIN` will tell the server to load additional certificates from headers `CERT_CHAIN_0` to `CERT_CHAIN_9` if `certificate-chain-length` is set to `10`. +| all but `rfc9440` + +|ssl-cert-chain +| The name of the header holding additional certificates in the chain. This is not a prefix but the full name of the header + because RFC 9440 mandates that the chain certificates are contained in one header. +| `rfc9440` |certificate-chain-length | The maximum length of the certificate chain. +| all |trust-proxy-verification | Enable trusting NGINX proxy certificate verification, instead of forwarding the certificate to {project_name} and verifying it in {project_name}. +| `nginx` |cert-is-url-encoded | Whether the forwarded certificate is url-encoded or not. In NGINX, this corresponds to the `$ssl_client_cert` and `$ssl_client_escaped_cert` variables. This can also be used for the Traefik PassTlsClientCert middleware, as it sends the client certficate unencoded. +| `nginx` + |=== === Configuring the NGINX provider @@ -241,3 +255,26 @@ If you are using this provider, see <@links.server id="keycloak-truststore"/> fo to configure a {project_name} Truststore. + +=== Configuring the rfc9440 provider + +If you stick to the header names mentioned in RFC 9440, you do not need to configure any additional options after selecting the `rfc9440` provider. +The default values of the options are as follows: + +[%autowidth] +|=== +|Option|Default + +|ssl-client-cert +| Client-Cert + +|ssl-cert-chain +| Client-Cert-Chain + +|certificate-chain-length +| 1 + +|=== + +If your certificate chain is longer than the given default, you must define the option with an appropriate number. +Otherwise, the provider will discard the request. diff --git a/services/src/main/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookup.java new file mode 100644 index 00000000000..172e1ac8ffe --- /dev/null +++ b/services/src/main/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookup.java @@ -0,0 +1,191 @@ +package org.keycloak.services.x509; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.keycloak.common.util.Base64; +import org.keycloak.common.util.DerUtils; +import org.keycloak.http.HttpRequest; + +import org.jboss.logging.Logger; + +/** + * The provider allows to extract a client certificate forwarded + * to the keycloak middleware configured behind a reverse proxy that is + * compliant with RFC 9440. + * + * @author Stephan Seifermann + * @version $Revision: 1 $ + * @since 12/30/2024 + */ +public class Rfc9440ClientCertificateLookup implements X509ClientCertificateLookup { + + public static class RfcViolationException extends Exception { + public RfcViolationException(String rfc, String section, String details, Throwable cause) { + super("Violation of RFC " + rfc + " (see section " + section + "): " + details, cause); + } + } + + public static class Rfc9440ViolationException extends RfcViolationException { + public Rfc9440ViolationException(String section, String details) { + this(section, details, null); + } + public Rfc9440ViolationException(String section, String details, Throwable cause) { + super("9440", section, details, cause); + } + } + + public static class Rfc8941ViolationException extends RfcViolationException { + public Rfc8941ViolationException(String section, String details) { + this(section, details, null); + } + public Rfc8941ViolationException(String section, String details, Throwable cause) { + super("8941", section, details, cause); + } + } + + private static final Logger log = Logger.getLogger(Rfc9440ClientCertificateLookup.class); + protected final String sslClientCertHttpHeader; + protected final String sslCertChainHttpHeader; + protected final int certificateChainLength; + + public Rfc9440ClientCertificateLookup(String sslClientCertHttpHeader, + String sslCertChainHttpHeader, + int certificateChainLength) { + this.sslClientCertHttpHeader = Optional.ofNullable(sslClientCertHttpHeader) + .filter(s -> !s.isBlank()) + .orElseThrow(() -> new IllegalArgumentException("sslClientCertHttpHeader")); + + this.sslCertChainHttpHeader = Optional.ofNullable(sslCertChainHttpHeader) + .filter(s -> !s.isBlank()) + .orElseThrow(() -> new IllegalArgumentException("sslCertChainHttpHeader")); + + this.certificateChainLength = certificateChainLength; + } + + @Override + public X509Certificate[] getCertificateChain(HttpRequest httpRequest) throws GeneralSecurityException { + if (!httpRequest.isProxyTrusted()) { + log.warnf("HTTP header \"%s\" is not trusted", sslClientCertHttpHeader); + return null; + } + try { + List chain = new ArrayList<>(); + X509Certificate clientCertificate = getClientCertificateFromHeader(httpRequest); + if (clientCertificate != null) { + chain.add(clientCertificate); + chain.addAll(getClientCertificateChainFromHeader(httpRequest)); + } + return chain.toArray(new X509Certificate[0]); + } catch (RfcViolationException e) { + throw new GeneralSecurityException(e); + } + } + + @Override + public void close() { + // intentionally left blank + } + + /** + * Extract the client certificate from the {@link #sslClientCertHttpHeader} header. + * + * @param httpRequest The request containing the headers. + * @return The extracted certificate or null if no certificate was presented. + * @throws RfcViolationException thrown if the header is missing or its value do not comply with the relevant RFCs. + */ + protected X509Certificate getClientCertificateFromHeader(HttpRequest httpRequest) throws RfcViolationException { + List headerValues = httpRequest.getHttpHeaders().getRequestHeader(sslClientCertHttpHeader); + if (headerValues.isEmpty()) { + return null; + } + if (headerValues.size() > 1) { + throw new Rfc9440ViolationException("2.2", "client cert header must occur at most once"); + } + + return parseCertificateFromHttpByteSequence(headerValues.get(0)); + } + + /** + * Extract the certificate chain from the {@link #sslCertChainHttpHeader} header. + * + * @param httpRequest The request containing the headers. + * @return A list of extracted certificates in the order of occurrence in the header. + * @throws RfcViolationException thrown if the header values do not comply with the relevant RFCs. + * @throws GeneralSecurityException thrown if the length of the chain is bigger than the configured maximum length (see {@link #certificateChainLength}). + */ + protected List getClientCertificateChainFromHeader(HttpRequest httpRequest) throws RfcViolationException, GeneralSecurityException { + List chainHeaderValues = httpRequest.getHttpHeaders().getRequestHeader(sslCertChainHttpHeader); + if (chainHeaderValues == null || chainHeaderValues.isEmpty()) { + // header is optional as of sec. 2.3 of RFC 9440 + return Collections.emptyList(); + } + + // header may be split according to sec. 3.1 of RFC 8941 + List encodedCerts = new ArrayList<>(); + for (String chainHeaderValue : chainHeaderValues) { + // lists may contain multiple entries separated by comma followed by optional whitespace according to sec. 3.1 of RFC 8941 + String[] listEntries = chainHeaderValue.split(",\\s*"); + encodedCerts.addAll(Arrays.asList(listEntries)); + } + + // the chain might be bigger than the configured limit + if (encodedCerts.size() > certificateChainLength) { + throw new GeneralSecurityException( + "The amount of certificates in the chain header " + encodedCerts.size() + + " is bigger than the configured limit of " + certificateChainLength + "." + ); + } + + // list entries are byte sequences encoded according to sec. 2.1 of RFC 9440 + List parsedCertificates = new ArrayList<>(); + for (String encodedCert : encodedCerts) { + parsedCertificates.add(parseCertificateFromHttpByteSequence(encodedCert)); + } + return parsedCertificates; + } + + /** + * Parses a X509 certificate from a byte sequence encoded according to sec. 2.1 of RFC 9440. + * + * @param byteSequence the byte sequence of a certificate encoded according to sec. 2.1 of RFC 9440 + * @return the extracted X509 certificate + * @throws RfcViolationException thrown if input does not conform to RFC + */ + protected static X509Certificate parseCertificateFromHttpByteSequence(String byteSequence) throws RfcViolationException { + if (byteSequence.length() < 2 || !byteSequence.startsWith(":") || !byteSequence.endsWith(":")) { + throw new Rfc8941ViolationException("3.3.5", "value is not encoded as byte sequence"); + } + String base64EncodedByteSequence = byteSequence.substring(1, byteSequence.length() - 1); + + byte[] certificateBytes; + try { + certificateBytes = Base64.decode(base64EncodedByteSequence); + } catch (IOException e) { + throw new Rfc9440ViolationException("2.1", "value does not contain base64 encoded content", e); + } + + X509Certificate certificate; + try (InputStream is = new ByteArrayInputStream(certificateBytes)) { + certificate = DerUtils.decodeCertificate(is); + } catch (Exception e) { + throw new Rfc9440ViolationException("2.1", "value does not contain DER encoded certificate", e); + } + + if (certificate == null) { + throw new Rfc9440ViolationException("2.1", "value does not contain DER encoded certificate"); + } + + log.debugf("Parsed certificate : Subject DN=[%s] SerialNumber=[%s]", certificate.getSubjectX500Principal(), certificate.getSerialNumber()); + return certificate; + } + +} diff --git a/services/src/main/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookupFactory.java new file mode 100644 index 00000000000..c1684d81925 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookupFactory.java @@ -0,0 +1,63 @@ +package org.keycloak.services.x509; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import org.jboss.logging.Logger; + +/** + * The factory and the corresponding providers extract a client certificate + * from a reverse proxy that is compliant with RFC 9440. + * + * @author Stephan Seifermann + * @version $Revision: 1 $ + * @since 12/30/2024 + */ +public class Rfc9440ClientCertificateLookupFactory implements X509ClientCertificateLookupFactory { + + private final static Logger logger = Logger.getLogger(Rfc9440ClientCertificateLookupFactory.class); + private final static String PROVIDER = "rfc9440"; + + protected final static String HTTP_HEADER_CLIENT_CERT = "sslClientCert"; + protected final static String HTTP_HEADER_CLIENT_CERT_DEFAULT = "Client-Cert"; + protected final static String HTTP_HEADER_CERT_CHAIN = "sslCertChain"; + protected final static String HTTP_HEADER_CERT_CHAIN_DEFAULT = "Client-Cert-Chain"; + protected final static String HTTP_HEADER_CERT_CHAIN_LENGTH = "certificateChainLength"; + protected final static int HTTP_HEADER_CERT_CHAIN_LENGTH_DEFAULT = 1; + + protected String sslClientCertHttpHeader; + protected String sslChainHttpHeader; + protected int certificateChainLength; + + @Override + public void init(Config.Scope config) { + certificateChainLength = config.getInt(HTTP_HEADER_CERT_CHAIN_LENGTH, HTTP_HEADER_CERT_CHAIN_LENGTH_DEFAULT); + sslClientCertHttpHeader = config.get(HTTP_HEADER_CLIENT_CERT, HTTP_HEADER_CLIENT_CERT_DEFAULT); + sslChainHttpHeader = config.get(HTTP_HEADER_CERT_CHAIN, HTTP_HEADER_CERT_CHAIN_DEFAULT); + + logger.tracev("{0}: ''{1}''", HTTP_HEADER_CLIENT_CERT, sslClientCertHttpHeader); + logger.tracev("{0}: ''{1}''", HTTP_HEADER_CERT_CHAIN, sslChainHttpHeader); + logger.tracev("{0}: ''{1}''", HTTP_HEADER_CERT_CHAIN_LENGTH, certificateChainLength); + } + + @Override + public X509ClientCertificateLookup create(KeycloakSession session) { + return new Rfc9440ClientCertificateLookup(sslClientCertHttpHeader, sslChainHttpHeader, certificateChainLength); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // intentionally left blank + } + + @Override + public void close() { + // intentionally left blank + } + + @Override + public String getId() { + return PROVIDER; + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory index 5940bcfca3f..480c918960c 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory @@ -20,3 +20,4 @@ org.keycloak.services.x509.DefaultClientCertificateLookupFactory org.keycloak.services.x509.HaProxySslClientCertificateLookupFactory org.keycloak.services.x509.ApacheProxySslClientCertificateLookupFactory org.keycloak.services.x509.NginxProxySslClientCertificateLookupFactory +org.keycloak.services.x509.Rfc9440ClientCertificateLookupFactory diff --git a/services/src/test/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookupTest.java b/services/src/test/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookupTest.java new file mode 100644 index 00000000000..71268a27764 --- /dev/null +++ b/services/src/test/java/org/keycloak/services/x509/Rfc9440ClientCertificateLookupTest.java @@ -0,0 +1,256 @@ +package org.keycloak.services.x509; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.function.Consumer; + +import jakarta.ws.rs.core.MultivaluedMap; + +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.http.HttpRequest; +import org.keycloak.services.resteasy.HttpRequestImpl; + +import com.google.common.base.Splitter; +import org.apache.commons.io.IOUtils; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize; +import static org.hamcrest.collection.IsArrayWithSize.emptyArray; +import static org.junit.Assert.assertThrows; + +public class Rfc9440ClientCertificateLookupTest { + + private static final String TEST_CLIENT_CERT_FILE = "/org/keycloak/test/services/x509/header_value_rfc_9440_client_cert"; + private static final String TEST_CLIENT_CHAIN_FILE = "/org/keycloak/test/services/x509/header_value_rfc_9440_client_chain"; + private static final String CLIENT_CERT_HEADER = "SSL_CLIENT_CERT "; + private static final String CLIENT_CHAIN_HEADER = "CERT_CHAIN "; + + private static String clientCertHeaderValue; + private static String clientChainHeaderValue; + + private static class UntrustedHttpRequestImpl extends HttpRequestImpl { + public UntrustedHttpRequestImpl(MockHttpRequest delegate) { + super(delegate); + } + + @Override + public boolean isProxyTrusted() { + return false; + } + } + + @BeforeClass + public static void init() throws IOException { + CryptoIntegration.init(Rfc9440ClientCertificateLookupTest.class.getClassLoader()); + + URL certHeaderValueResource = Rfc9440ClientCertificateLookupTest.class.getResource(TEST_CLIENT_CERT_FILE); + assert certHeaderValueResource != null; + clientCertHeaderValue = IOUtils.toString(certHeaderValueResource, StandardCharsets.UTF_8); + + URL chainHeaderValueResource = Rfc9440ClientCertificateLookupTest.class.getResource(TEST_CLIENT_CHAIN_FILE); + assert chainHeaderValueResource != null; + clientChainHeaderValue = IOUtils.toString(chainHeaderValueResource, StandardCharsets.UTF_8); + } + + @Test + public void testRequestFromUntrustedProxyIsDiscarded() throws GeneralSecurityException { + Rfc9440ClientCertificateLookup subject = createSubject(1); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + }, false); + + X509Certificate[] actualChain = subject.getCertificateChain(httpRequest); + + assertThat(actualChain, is(nullValue())); + } + + @Test + public void testClientCertOnly() throws GeneralSecurityException { + Rfc9440ClientCertificateLookup subject = createSubject(1); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + }); + + X509Certificate[] actualChain = subject.getCertificateChain(httpRequest); + + assertThat(actualChain, is(not(nullValue()))); + assertThat(actualChain, is(arrayWithSize(1))); + } + + @Test + public void testClientCertAndChain() throws GeneralSecurityException { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + headers.add(CLIENT_CHAIN_HEADER, clientChainHeaderValue); + }); + + X509Certificate[] actualChain = subject.getCertificateChain(httpRequest); + + assertThat(actualChain, is(not(nullValue()))); + assertThat(actualChain, is(arrayWithSize(3))); + } + + @Test + public void testClientCertAndChainInMultipleIndividualFields() throws GeneralSecurityException { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + List chainCerts = Splitter.on(", ").splitToList(clientChainHeaderValue); + chainCerts.forEach(cert -> headers.add(CLIENT_CHAIN_HEADER, cert)); + }); + + X509Certificate[] actualChain = subject.getCertificateChain(httpRequest); + + assertThat(actualChain, is(not(nullValue()))); + assertThat(actualChain, is(arrayWithSize(3))); + } + + @Test + public void testClientCertAndChainInMultipleMixedFields() throws GeneralSecurityException { + Rfc9440ClientCertificateLookup subject = createSubject(3); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + List chainCerts = Splitter.on(", ").splitToList(clientChainHeaderValue); + headers.add(CLIENT_CHAIN_HEADER, chainCerts.get(0)); + headers.add(CLIENT_CHAIN_HEADER, chainCerts.get(0) + "," + chainCerts.get(1)); + }); + + X509Certificate[] actualChain = subject.getCertificateChain(httpRequest); + + assertThat(actualChain, is(not(nullValue()))); + assertThat(actualChain, is(arrayWithSize(4))); + } + + @Test + public void testEmptyChainOnMissingClientCert() throws GeneralSecurityException { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> {}); + + X509Certificate[] actualChain = subject.getCertificateChain(httpRequest); + + assertThat(actualChain, is(not(nullValue()))); + assertThat(actualChain, is(emptyArray())); + } + + @Test + public void testErrorOnEmptyClientCertByteArray() { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, "::"); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + @Test + public void testErrorOnIncorrectlyStartedClientCert() { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue.substring(1)); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + @Test + public void testErrorOnIncorrectlyTerminatedClientCert() { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue.substring(0, clientCertHeaderValue.length() - 1)); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + @Test + public void testErrorOnInvalidClientCertBase64Payload() { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, ":Zm9_:"); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + @Test + public void testErrorOnInvalidClientCertDerPayload() { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, ":Zm9v:"); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + @Test + public void testErrorOnMultipleClientCerts() { + Rfc9440ClientCertificateLookup subject = createSubject(2); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + @Test + public void testErrorOnTooLongChain() { + Rfc9440ClientCertificateLookup subject = createSubject(1); + HttpRequest httpRequest = createHttpRequest(headers -> { + headers.add(CLIENT_CERT_HEADER, clientCertHeaderValue); + headers.add(CLIENT_CHAIN_HEADER, clientChainHeaderValue); + }); + + assertThrows(GeneralSecurityException.class, () -> { + subject.getCertificateChain(httpRequest); + }); + } + + private static Rfc9440ClientCertificateLookup createSubject(int certificateChainLength) { + return new Rfc9440ClientCertificateLookup(CLIENT_CERT_HEADER, CLIENT_CHAIN_HEADER, certificateChainLength); + } + + private static HttpRequest createHttpRequest(Consumer> configurer) { + return createHttpRequest(configurer, true); + } + + private static HttpRequest createHttpRequest(Consumer> configurer, boolean fromTrustedProxy) { + MockHttpRequest requestMock; + try { + requestMock = MockHttpRequest.get("foo"); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + configurer.accept(requestMock.getMutableHeaders()); + if (fromTrustedProxy) { + return new HttpRequestImpl(requestMock); + } else { + return new UntrustedHttpRequestImpl(requestMock); + } + } + +} diff --git a/services/src/test/resources/org/keycloak/test/services/x509/header_value_rfc_9440_client_cert b/services/src/test/resources/org/keycloak/test/services/x509/header_value_rfc_9440_client_cert new file mode 100644 index 00000000000..5d6851e344b --- /dev/null +++ b/services/src/test/resources/org/keycloak/test/services/x509/header_value_rfc_9440_client_cert @@ -0,0 +1 @@ +:MIIBqDCCAU6gAwIBAgIBBzAKBggqhkjOPQQDAjA6MRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxGzAZBgNVBAMMEkxBIEludGVybWVkaWF0ZSBDQTAeFw0yMDAxMTQyMjU1MzNaFw0yMTAxMjMyMjU1MzNaMA0xCzAJBgNVBAMMAkJDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8YnXXfaUgmnMtOXU/IncWalRhebrXmckC8vdgJ1p5Be5F/3YC8OthxM4+k1M6aEAEFcGzkJiNy6J84y7uzo9M6NyMHAwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBRm3WjLa38lbEYCuiCPct0ZaSED2DAOBgNVHQ8BAf8EBAMCBsAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0RAQH/BBMwEYEPYmRjQGV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0gAMEUCIBHda/r1vaL6G3VliL4/Di6YK0Q6bMjeSkC3dFCOOB8TAiEAx/kHSB4urmiZ0NX5r5XarmPk0wmuydBVoU4hBVZ1yhk=: \ No newline at end of file diff --git a/services/src/test/resources/org/keycloak/test/services/x509/header_value_rfc_9440_client_chain b/services/src/test/resources/org/keycloak/test/services/x509/header_value_rfc_9440_client_chain new file mode 100644 index 00000000000..3e5c1df9f5b --- /dev/null +++ b/services/src/test/resources/org/keycloak/test/services/x509/header_value_rfc_9440_client_chain @@ -0,0 +1 @@ +:MIIB5jCCAYugAwIBAgIBFjAKBggqhkjOPQQDAjBWMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTGV0J3MgQXV0aGVudGljYXRlMSowKAYDVQQDDCFMZXQncyBBdXRoZW50aWNhdGUgUm9vdCBBdXRob3JpdHkwHhcNMjAwMTE0MjEzMjMwWhcNMzAwMTExMjEzMjMwWjA6MRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxGzAZBgNVBAMMEkxBIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJf+aA54RC5pyLAR5yfXVYmNpgd+CGUTDp2KOGhc0gK91zxhHesEYkdXkpS2UN8Kati+yHtWCV3kkhCngGyv7RqjZjBkMB0GA1UdDgQWBBRm3WjLa38lbEYCuiCPct0ZaSED2DAfBgNVHSMEGDAWgBTEA2Q6eecKu9g9yb5glbkhhVINGDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNJADBGAiEA5pLvaFwRRkxomIAtDIwg9D7gC1xzxBl4r28EzmSO1pcCIQCJUShpSXO9HDIQMUgH69fNDEMHXD3RRX5gP7kuu2KGMg==:, :MIICBjCCAaygAwIBAgIJAKS0yiqKtlhoMAoGCCqGSM49BAMCMFYxCzAJBgNVBAYTAlVTMRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxKjAoBgNVBAMMIUxldCdzIEF1dGhlbnRpY2F0ZSBSb290IEF1dGhvcml0eTAeFw0yMDAxMTQyMTI1NDVaFw00MDAxMDkyMTI1NDVaMFYxCzAJBgNVBAYTAlVTMRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxKjAoBgNVBAMMIUxldCdzIEF1dGhlbnRpY2F0ZSBSb290IEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFoaHU+Z5bPKmGzlYXtCf+E6HYj62fORaHDOrt+yyh3H/rTcs7ynFfGn+gyFsrSP3Ez88rajv+U2NfD0o0uZ4PmjYzBhMB0GA1UdDgQWBBTEA2Q6eecKu9g9yb5glbkhhVINGDAfBgNVHSMEGDAWgBTEA2Q6eecKu9g9yb5glbkhhVINGDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNIADBFAiEAmAeg1ycKHriqHnaD4M/UDBpQRpkmdcRFYGMg1Qyrkx4CIB4ivz3wQcQkGhcsUZ1SOImd/lq1Q0FLf09rGfLQPWDc: \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 0a07beb652b..01dfd0d48b6 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -250,6 +250,11 @@ "sslClientCert": "x-ssl-client-cert", "sslCertChainPrefix": "x-ssl-client-cert-chain", "certificateChainLength": 1 + }, + "rfc9440": { + "sslClientCert": "x-ssl-client-cert", + "sslCertChain": "x-ssl-client-cert-chain", + "certificateChainLength": 1 } }, diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index 86ebd65ac55..970ece0383b 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -176,6 +176,11 @@ "sslClientCert": "x-ssl-client-cert", "sslCertChainPrefix": "x-ssl-client-cert-chain", "certificateChainLength": 1 + }, + "rfc9440": { + "sslClientCert": "x-ssl-client-cert", + "sslCertChain": "x-ssl-client-cert-chain", + "certificateChainLength": 1 } },