8000 Initial draft to check CA subject DN in X.509 client authenticator by rmartinc · Pull Request #48550 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 59 additions & 13 deletions common/src/main/java/org/keycloak/common/util/KeystoreUtil.java
10000
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,36 @@
*/
public class KeystoreUtil {

public enum KeystoreFormat {
public interface StoreFormat {
List<String> getFileExtensions();
String getPrimaryExtension();
};

public enum TruststoreFormat implements StoreFormat {
JKS("jks", "truststore"),
PKCS12("p12", "pfx", "pkcs12"),
PEM("pem", "ca", "crt"),
BCFKS("bcfks");

// Typical file extension for this keystore format
private final List<String> fileExtensions;

TruststoreFormat(String... extensions) {
this.fileExtensions = Arrays.asList(extensions);
}

@Override
public List<String> getFileExtensions() {
return fileExtensions;
}

@Override
public String getPrimaryExtension() {
return fileExtensions.get(0);
}
}

public enum KeystoreFormat implements StoreFormat {
JKS("jks"),
PKCS12("p12", "pfx", "pkcs12"),
BCFKS("bcfks");
Expand All @@ -48,10 +77,12 @@ public enum KeystoreFormat {
this.fileExtensions = Arrays.asList(extensions);
}

@Override
public List<String> getFileExtensions() {
return fileExtensions;
}

@Override
public String getPrimaryExtension() {
return fileExtensions.get(0);
}
Expand Down Expand Up @@ -104,39 +135,54 @@ public static KeyPair loadKeyPairFromKeystore(String keystoreFile, String storeP
}
}

public static Optional<KeystoreFormat> getKeystoreFormat(String path) {
private static <T extends StoreFormat> Optional<T> getStoreFormat(String path, T[] values) {
int lastDotIndex = path.lastIndexOf('.');
if (lastDotIndex > -1) {
String ext = path.substring(lastDotIndex + 1).toLowerCase();
return Arrays.stream(KeystoreUtil.KeystoreFormat.values())
return Arrays.stream(values)
.filter(ksFormat -> ksFormat.getFileExtensions().contains(ext))
.findFirst();
}
return Optional.empty();
}

public static Optional<TruststoreFormat> getTruststoreFormat(String path) {
return getStoreFormat(path, TruststoreFormat.values());
}

/**
* Try to return supported keystore type
*
* @param preferredType The preferred format - usually the one from the configuration. When present, it should be preferred over anything else
* @param path Path of the file. We can try to detect keystore type from that (EG. my-keystore.pkcs12 will return "pkcs12") in case that preferredType is not defined
* @param defaultType Default format as last fallback when none of the above can be used. Should be non-null
* @return format as specified above
*/
public static String getKeystoreType(String preferredType, String path, String defaultType) {
public static Optional<KeystoreFormat> getKeystoreFormat(String path) {
return getStoreFormat(path, KeystoreFormat.values());
}

private static <T extends StoreFormat> String getStoreType(String preferredType, String path, String defaultType, T[] values) {
// Configured type has precedence
if (preferredType != null) {
return preferredType;
}

// Fallback to path
Optional<KeystoreFormat> format = getKeystoreFormat(path);
Optional<StoreFormat> format = getStoreFormat(path, values);
if (format.isPresent()) {
return format.get().toString();
}

// Fallback to default
return defaultType;
}

public static String getTruststoreType(String preferredType, String path, String defaultType) {
return getStoreType(preferredType, path, defaultType, TruststoreFormat.values());
}

/**
* Try to return supported keystore type
*
* @param preferredType The preferred format - usually the one from the configuration. When present, it should be preferred over anything else
* @param path Path of the file. We can try to detect keystore type from that (EG. my-keystore.pkcs12 will return "pkcs12") in case that preferredType is not defined
* @param defaultType Default format as last fallback when none of the above can be used. Should be non-null
* @return format as specified above
*/
public static String getKeystoreType(String preferredType, String path, String defaultType) {
return getStoreType(preferredType, path, defaultType, KeystoreFormat.values());
}
}
Binary file modified docs/documentation/server_admin/images/x509-client-auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ One of several purposes for this executor is to realize the security requirement
* Enforce SAML Redirect binding cannot be used or SAML requests and assertions are signed
* Enforce scopes granted in link:{securing_apps_token_exchange_link}#_standard-token-exchange[Standard token exchange] or in JWT Authorization Grant are restricted to the ones present in the initial `subject_token` or `assertion` JWT. This executor only allows downscoping of the presented assertion. An error is returned if any extra scope, not originally granted to the JWT, is requested.
* Enforce claims for assertion grants (`subject_token` in Token Exchange and `assertion` in JWT Authorization Grant). The executor enforces the presence and specific values of a claim in a JWT. It uses a Java regex so it is quite versatile.
* Add default **Certificate Authority subject DN** for the client registration service when TLS client authentication (`tls_client_auth`) is configured (see *Using the client registration service* in the link:{securing_apps_link}[securing apps] section for more information). The `tls-client-auth-ca-subject-dn` executor can be used to provide the new compulsory attribute in client registration interface. It can also enforce the configured CA name to reject unwanted values.

Another available executor is the `auth-flow-enforce`, which can be used to enforce an authentication flow during an authentication request. For instance, it can be used to select a flow based on certain conditions, such as a specific scope or an ACR value. For more details, see the <<_client-policy-auth-flow, related documentation>>.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ Like in the *Signed JWT* authenticator you can configure the *Signature algorith
.X509 certificate
image:images/x509-client-auth.png[]

The validator also checks the Subject DN field of the certificate with a configured regexp validation expression. For some
use cases, it is sufficient to accept all certificates. In that case, you can use `(.*?)(?:$)` expression.
The validator also checks the Subject DN of the client certificate and the root Certificate Authority (CA) Subject DN (trust anchor). This way, {project_name} ensures the certificate presented by the client is the expected one and it was issued by the correct CA. As the trust-store or mTLS store is shared among all the realms, this extra security check avoids interferences between CAs.

The certificate Subject DN can use a regexp validation expression instead of the exact DN. If you want to accept all certificates for a specific CA Subject DN, the expression `(.*?)(?:$)` can be used. Nevertheless this configuration is deprecated and will be removed in a future version.

Two ways exist for {project_name} to obtain the Client ID from the request:

Expand Down
20 changes: 20 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_7_0.adoc 10BC8
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ When searching for users using the Admin REST API, the user profile metadata is
This change was made to improve performance of user search operations, as fetching user profile metadata for each user can be resource-intensive.
The user profile metadata can still be retrieved when fetching a specific user by their identifier.

=== X509 client authentication requires CA Subject DN

Since this release, the X509 client authentication, that allows a client authenticate presenting a certificate in the mutual TLS (mTLS) handshake, requires to configure a new option called **Certificate Authority subject DN** to identify the trusted anchor CA for the client certificate. As the {project_name}'s trust-store (or mTLS store) is shared for all the realms, the new option allows to identify the correct CA of the certificate and avoids interferences with other unrelated CAs.

The new option is compulsory in the administration console when configuring X509 credentials for the client. For the moment, previous configurations without the CA subject DN still work to maintain backwards compatibility. In the next major version, the validation will be present at the server side, making any update, creation or import invalid if the new configuration option is not present. Please, if you use the X509 authenticator for clients ensure you add this new attribute to avoid issues in the future.

Besides, the option **Allow regex pattern comparison** is deprecated. This option allows to configure a regexp instead of the exact DN of the client certificate. It is now deprecated and it will be removed in a future version. The certificate for the client should be unambiguously identified.

For the link:https://www.keycloak.org/securing-apps/client-registration[client registration service], there is a new link:{adminguide_link}#_client_policies[client policy executor] named `tls-client-auth-ca-subject-dn` which can be used to assign a default CA subject DN. The executor can also enforce the configured CA DN to reject unwanted CAs.

In general, all this new configuration requires {project_name} to always revalidate the trusted anchors for the client certificate. This feature can be problematic in certain configurations (for example if a proxy is configured as a TLS termination and the certificates are passed to {project_name} using headers). The trust-store or mTLS store in {project_name} should always be configured correctly to validate the client certificate chain. Please see link:https://www.keycloak.org/server/reverseproxy[Using a reverse proxy] and link:https://www.keycloak.org/server/mutual-tls[Configuring trusted certificates for mTLS], for more information if you are using a TLS termination proxy and X509 client authentication.

// ------------------------ Notable changes ------------------------ //
== Notable changes

Expand Down Expand Up @@ -147,6 +159,14 @@ This parameter is sent by clients during token refresh and is used when the `${a
It was primarily used by legacy adapters that have been removed.
This improves security by ensuring that only trusted, pre-registered hosts can be used in backchannel logout URLs.

== Deprecated features

The following sections provide details on deprecated features.

=== Option Allow regex pattern comparison is deprecated for the X509 client authenticator

The option **Allow regex pattern comparison** is deprecated for the X509 client authenticator. This option allows to configure a Java regular expression instead of the exact DN for the client certificate subject DN. As previously commented in the breaking changes section, this option is now deprecated and it will be removed in a future version. The certificate for the client should be unambiguously identified because of security reasons.

// ------------------------ Removed features ------------------------ //
== Removed features

Expand Down
10BC8
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,7 @@ flowType=Flow type
syncLDAPGroupsSuccessful=Data successfully synced {{result}}
policyEnforcementModes.PERMISSIVE=Permissive
subject=Subject DN
casubject=Certificate Authority subject DN
use=Use
defaultAdminInitiated=Default Admin-Initiated Action Lifespan
chooseAMapperType=Choose a mapper type
Expand Down Expand Up @@ -2139,7 +2140,7 @@ clientProfileName=Client profile name
effectiveProtocolMappers=Effective protocol mappers
userVerify.preferred=Preferred
syncModes.legacy=Legacy
allowRegexComparisonHelp=If OFF, then the Subject DN from given client certificate must exactly match the given DN from the 'Subject DN' property as described in the RFC8705 specification. The Subject DN can be in the RFC4514 or RFC1779 format. If ON, then the Subject DN from given client certificate should match regex specified by 'Subject DN' property.
allowRegexComparisonHelp=If OFF, then the Subject DN from given client certificate must exactly match the given DN from the 'Subject DN' property as described in the RFC8705 specification. The Subject DN can be in the RFC4514 or RFC1779 format. If ON, then the Subject DN from given client certificate should match regex specified by 'Subject DN' property. This option is deprectated, in the future only exact DN comparison will be allowed.
eventTypes.UPDATE_TOTP_ERROR.description=Update totp error
titleEvents=Events
signServiceProviderMetadata=Sign service provider metadata
Expand Down Expand Up @@ -2864,8 +2865,8 @@ inputTypeSize=Input size
createAttributeSubTitle=Create a new attribute
eventTypes.CODE_TO_TOKEN_ERROR.name=Code to token error
emptyAuthorizationInstructions=If you want to create authorization scopes, please click the button below to create the authorization scope
subjectHelp=A regular expression for validating Subject DN in the Client Certificate. Use "(.*?)(?\:$)" to match all kind of expressions.
updatePolicySuccess=Successfully updated the policy
subjectHelp=Subject DN in the Client Certificate issued for the client. When using a regular expression, you can use "(.*?)(?\:$)" to match all kind of expressions.
casubjectHelp=Subject DN of the root Certificate Authority (CA) that issued the certificate for the client (trust anchor). The CA Subject DN can be in the RFC4514 or RFC1779 format.
eventTypes.CUSTOM_REQUIRED_ACTION.name=Custom required action
updateExecutorError=Executor not updated: {{error}}
clientIdHelpHelp=Client ID of client to which LDAP role mappings will be mapped. Applicable only if 'Use Realm Roles Mapping' is false.
Expand Down
26 changes: 24 additions & 2 deletions js/apps/admin-ui/src/clients/credentials/X509.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { useTranslation } from "react-i18next";
import { TextControl } from "@keycloak/keycloak-ui-shared";
import { useFormContext } from "react-hook-form";
import { HelpItem, TextControl } from "@keycloak/keycloak-ui-shared";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";

export const X509 = () => {
const { t } = useTranslation();
const { watch } = useFormContext<FormFields>();
const regexEnabled = watch(
convertAttributeNameToForm<FormFields>(
"attributes.x509.allow.regex.pattern.comparison",
),
"false",
);
return (
<>
<DefaultSwitchControl
name={convertAttributeNameToForm<FormFields>(
"attributes.x509.allow.regex.pattern.comparison",
)}
label={t("allowRegexComparison")}
labelIcon={t("allowRegexComparisonHelp")}
labelIcon={
<HelpItem
helpText={t("allowRegexComparisonHelp")}
fieldLabelId="allowRegexComparison"
isRecommendation={regexEnabled === "true"}
/>
}
stringify
/>
<TextControl
Expand All @@ -24,6 +38,14 @@ export const X509 = () => {
required: t("required"),
}}
/>
<TextControl
name={convertAttributeNameToForm("attributes.x509.casubjectdn")}
label={t("casubject")}
labelIcon={t("casubjectHelp")}
rules={{
required: t("required"),
}}
/>
</>
);
};
6 changes: 4 additions & 2 deletions js/libs/ui-shared/src/controls/FormLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FormGroup, FormGroupProps } from "@patternfly/react-core";
import { PropsWithChildren, ReactNode } from "react";
import { PropsWithChildren, ReactNode, isValidElement } from "react";
import { FieldError, FieldValues, Merge } from "react-hook-form";
import { FormErrorText } from "./FormErrorText";
import { HelpItem } from "./HelpItem";
Expand Down Expand Up @@ -28,7 +28,9 @@ export const FormLabel = ({
label={label || name}
fieldId={id || name}
labelIcon={
labelIcon ? (
isValidElement(labelIcon) ? (
labelIcon
) : labelIcon ? (
<HelpItem helpText={labelIcon} fieldLabelId={id || name} />
) : undefined
}
Expand Down
3 changes: 2 additions & 1 deletion js/libs/ui-shared/src/controls/SwitchControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useFormContext,
} from "react-hook-form";
import { SwitchProps, Switch } from "@patternfly/react-core";
import { ReactNode } from "react";
import { FormLabel } from "./FormLabel";
import { debeerify } from "../user-profile/utils";

Expand All @@ -17,7 +18,7 @@ export type SwitchControlProps<
UseControllerProps<any, P> & {
name: string;
label?: string;
labelIcon?: string;
labelIcon?: string | ReactNode;
labelOn: string;
labelOff: string;
stringify?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,14 @@ public interface TruststoreProvider extends Provider {
* @return intermediate certificates from the configured truststore as a map where the key is the X500Principal of the corresponding X509Certificate
*/
Map<X500Principal, List<X509Certificate>> getIntermediateCertificates();

/**
* Returns the truststore used for mTLS
* @return The mTLS truststore
*/
KeyStore getHttpsTruststore();

Map<X500Principal, List<X509Certificate>> getHttpsRootCertificates();

Map<X500Principal, List<X509Certificate>> getHttpsIntermediateCertificates();
}
Loading
Loading
0