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:
Stephan Seifermann 2025-12-19 12:38:54 +01:00 committed by GitHub
parent efc75f09b0
commit aefecade5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 567 additions and 1 deletions

View File

@ -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

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -0,0 +1 @@
:MIIBqDCCAU6gAwIBAgIBBzAKBggqhkjOPQQDAjA6MRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxGzAZBgNVBAMMEkxBIEludGVybWVkaWF0ZSBDQTAeFw0yMDAxMTQyMjU1MzNaFw0yMTAxMjMyMjU1MzNaMA0xCzAJBgNVBAMMAkJDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8YnXXfaUgmnMtOXU/IncWalRhebrXmckC8vdgJ1p5Be5F/3YC8OthxM4+k1M6aEAEFcGzkJiNy6J84y7uzo9M6NyMHAwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBRm3WjLa38lbEYCuiCPct0ZaSED2DAOBgNVHQ8BAf8EBAMCBsAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0RAQH/BBMwEYEPYmRjQGV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0gAMEUCIBHda/r1vaL6G3VliL4/Di6YK0Q6bMjeSkC3dFCOOB8TAiEAx/kHSB4urmiZ0NX5r5XarmPk0wmuydBVoU4hBVZ1yhk=:

View File

@ -0,0 +1 @@
:MIIB5jCCAYugAwIBAgIBFjAKBggqhkjOPQQDAjBWMQswCQYDVQQGEwJVUzEbMBkGA1UECgwSTGV0J3MgQXV0aGVudGljYXRlMSowKAYDVQQDDCFMZXQncyBBdXRoZW50aWNhdGUgUm9vdCBBdXRob3JpdHkwHhcNMjAwMTE0MjEzMjMwWhcNMzAwMTExMjEzMjMwWjA6MRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxGzAZBgNVBAMMEkxBIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJf+aA54RC5pyLAR5yfXVYmNpgd+CGUTDp2KOGhc0gK91zxhHesEYkdXkpS2UN8Kati+yHtWCV3kkhCngGyv7RqjZjBkMB0GA1UdDgQWBBRm3WjLa38lbEYCuiCPct0ZaSED2DAfBgNVHSMEGDAWgBTEA2Q6eecKu9g9yb5glbkhhVINGDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNJADBGAiEA5pLvaFwRRkxomIAtDIwg9D7gC1xzxBl4r28EzmSO1pcCIQCJUShpSXO9HDIQMUgH69fNDEMHXD3RRX5gP7kuu2KGMg==:, :MIICBjCCAaygAwIBAgIJAKS0yiqKtlhoMAoGCCqGSM49BAMCMFYxCzAJBgNVBAYTAlVTMRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxKjAoBgNVBAMMIUxldCdzIEF1dGhlbnRpY2F0ZSBSb290IEF1dGhvcml0eTAeFw0yMDAxMTQyMTI1NDVaFw00MDAxMDkyMTI1NDVaMFYxCzAJBgNVBAYTAlVTMRswGQYDVQQKDBJMZXQncyBBdXRoZW50aWNhdGUxKjAoBgNVBAMMIUxldCdzIEF1dGhlbnRpY2F0ZSBSb290IEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFoaHU+Z5bPKmGzlYXtCf+E6HYj62fORaHDOrt+yyh3H/rTcs7ynFfGn+gyFsrSP3Ez88rajv+U2NfD0o0uZ4PmjYzBhMB0GA1UdDgQWBBTEA2Q6eecKu9g9yb5glbkhhVINGDAfBgNVHSMEGDAWgBTEA2Q6eecKu9g9yb5glbkhhVINGDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNIADBFAiEAmAeg1ycKHriqHnaD4M/UDBpQRpkmdcRFYGMg1Qyrkx4CIB4ivz3wQcQkGhcsUZ1SOImd/lq1Q0FLf09rGfLQPWDc:

View File

@ -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
}
},

View File

@ -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
}
},