mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
Client cert lookup provider compliant to RFC 9440 (#36161)
* Client cert lookup provider compliant to RFC 9440 (#20761) Signed-off-by: Stephan Seifermann <seiferma@users.noreply.github.com> * Release notes Signed-off-by: Václav Muzikář <vmuzikar@redhat.com> --------- Signed-off-by: Stephan Seifermann <seiferma@users.noreply.github.com> Signed-off-by: Václav Muzikář <vmuzikar@redhat.com> Co-authored-by: Stephan Seifermann <seiferma@users.noreply.github.com> Co-authored-by: Václav Muzikář <vmuzikar@redhat.com>
This commit is contained in:
parent
efc75f09b0
commit
aefecade5c
@ -137,6 +137,12 @@ The `feature-<name>` 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
|
||||
|
||||
@ -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.
|
||||
|
||||
</@tmpl.guide>
|
||||
|
||||
=== 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.
|
||||
|
||||
@ -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 <a href="mailto:seiferma.dev+kc@gmail.com">Stephan Seifermann</a>
|
||||
* @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<X509Certificate> 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<String> 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<X509Certificate> getClientCertificateChainFromHeader(HttpRequest httpRequest) throws RfcViolationException, GeneralSecurityException {
|
||||
List<String> 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<String> 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<X509Certificate> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 <a href="mailto:seiferma.dev+kc@gmail.com">Stephan Seifermann</a>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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<String> 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<String> 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<MultivaluedMap<String, String>> configurer) {
|
||||
return createHttpRequest(configurer, true);
|
||||
}
|
||||
|
||||
private static HttpRequest createHttpRequest(Consumer<MultivaluedMap<String, String>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
:MIIBqDCCAU6gAwIBAgIBBzAKBggqhkjOPQQDAjA6MRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxGzAZBgNVBAMMEkxBIEludGVybWVkaWF0ZSBDQTAeFw0yMDAxMTQyMjU1MzNaFw0yMTAxMjMyMjU1MzNaMA0xCzAJBgNVBAMMAkJDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8YnXXfaUgmnMtOXU/IncWalRhebrXmckC8vdgJ1p5Be5F/3YC8OthxM4+k1M6aEAEFcGzkJiNy6J84y7uzo9M6NyMHAwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBRm3WjLa38lbEYCuiCPct0ZaSED2DAOBgNVHQ8BAf8EBAMCBsAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0RAQH/BBMwEYEPYmRjQGV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0gAMEUCIBHda/r1vaL6G3VliL4/Di6YK0Q6bMjeSkC3dFCOOB8TAiEAx/kHSB4urmiZ0NX5r5XarmPk0wmuydBVoU4hBVZ1yhk=:
|
||||
@ -0,0 +1 @@
|
||||
:MIIB5jCCAYugAwIBAgIBFjAKBggqhkjOPQQDAjBWMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTGV0J3MgQXV0aGVudGljYXRlMSowKAYDVQQDDCFMZXQncyBBdXRoZW50aWNhdGUgUm9vdCBBdXRob3JpdHkwHhcNMjAwMTE0MjEzMjMwWhcNMzAwMTExMjEzMjMwWjA6MRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxGzAZBgNVBAMMEkxBIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJf+aA54RC5pyLAR5yfXVYmNpgd+CGUTDp2KOGhc0gK91zxhHesEYkdXkpS2UN8Kati+yHtWCV3kkhCngGyv7RqjZjBkMB0GA1UdDgQWBBRm3WjLa38lbEYCuiCPct0ZaSED2DAfBgNVHSMEGDAWgBTEA2Q6eecKu9g9yb5glbkhhVINGDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNJADBGAiEA5pLvaFwRRkxomIAtDIwg9D7gC1xzxBl4r28EzmSO1pcCIQCJUShpSXO9HDIQMUgH69fNDEMHXD3RRX5gP7kuu2KGMg==:, :MIICBjCCAaygAwIBAgIJAKS0yiqKtlhoMAoGCCqGSM49BAMCMFYxCzAJBgNVBAYTAlVTMRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxKjAoBgNVBAMMIUxldCdzIEF1dGhlbnRpY2F0ZSBSb290IEF1dGhvcml0eTAeFw0yMDAxMTQyMTI1NDVaFw00MDAxMDkyMTI1NDVaMFYxCzAJBgNVBAYTAlVTMRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxKjAoBgNVBAMMIUxldCdzIEF1dGhlbnRpY2F0ZSBSb290IEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFoaHU+Z5bPKmGzlYXtCf+E6HYj62fORaHDOrt+yyh3H/rTcs7ynFfGn+gyFsrSP3Ez88rajv+U2NfD0o0uZ4PmjYzBhMB0GA1UdDgQWBBTEA2Q6eecKu9g9yb5glbkhhVINGDAfBgNVHSMEGDAWgBTEA2Q6eecKu9g9yb5glbkhhVINGDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNIADBFAiEAmAeg1ycKHriqHnaD4M/UDBpQRpkmdcRFYGMg1Qyrkx4CIB4ivz3wQcQkGhcsUZ1SOImd/lq1Q0FLf09rGfLQPWDc:
|
||||
@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user