Key generation for client authentication is always RSA 2048 with a 10-year validity, regardless of the selected algorithm

Closes #38620

Signed-off-by: Douglas Palmer <dpalmer@redhat.com>
This commit is contained in:
Douglas Palmer 2025-06-11 11:39:40 -07:00 committed by Marek Posolda
parent 30979dc873
commit 1183157d86
9 changed files with 70 additions and 4 deletions

View File

@ -29,6 +29,8 @@ public class KeyStoreConfig {
protected String keyAlias; protected String keyAlias;
protected String realmAlias; protected String realmAlias;
protected String format; protected String format;
protected Integer keySize;
protected Integer validity;
public Boolean isRealmCertificate() { public Boolean isRealmCertificate() {
return realmCertificate; return realmCertificate;
@ -77,4 +79,20 @@ public class KeyStoreConfig {
public void setFormat(String format) { public void setFormat(String format) {
this.format = format; this.format = format;
} }
public Integer getKeySize() {
return keySize;
}
public void setKeySize(Integer keySize) {
this.keySize = keySize;
}
public Integer getValidity() {
return validity;
}
public void setValidity(Integer validity) {
this.validity = validity;
}
} }

View File

@ -17,6 +17,7 @@
package org.keycloak.admin.client.resource; package org.keycloak.admin.client.resource;
import jakarta.ws.rs.QueryParam;
import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.CertificateRepresentation;

View File

@ -480,7 +480,7 @@ userInfoResponseEncryptionKeyManagementAlgorithmHelp=JWA Algorithm used for key
authnContextDeclRefsHelp=Ordered list of requested AuthnContext DeclRefs. authnContextDeclRefsHelp=Ordered list of requested AuthnContext DeclRefs.
inherent=Inherited inherent=Inherited
tableTitle=Attributes groups tableTitle=Attributes groups
generateNewKeys=Generate new keys generateNewKeys=Generate RSA keys
updateClientPolicySuccess=Client policy updated updateClientPolicySuccess=Client policy updated
unlock=Unlock unlock=Unlock
validateRealm=You must enter a realm validateRealm=You must enter a realm
@ -826,6 +826,8 @@ removeImported=Remove imported
endpoints=Endpoints endpoints=Endpoints
roleSaveError=Could not save role\: {{error}} roleSaveError=Could not save role\: {{error}}
keySize=Key size keySize=Key size
validity=Certificate expiration
validityHelp=Number of years the generated certificate is valid for.
membershipUserLdapAttributeHelp=Used only if the Membership Attribute Type is UID. It is the name of the LDAP attribute on the user, which is used for membership mappings. It is typically 'uid'. For example, if the value of 'Membership User LDAP Attribute' is 'uid' and the LDAP group has 'memberUid\: john', it is expected that particular LDAP user will have the attribute 'uid\: john'. membershipUserLdapAttributeHelp=Used only if the Membership Attribute Type is UID. It is the name of the LDAP attribute on the user, which is used for membership mappings. It is typically 'uid'. For example, if the value of 'Membership User LDAP Attribute' is 'uid' and the LDAP group has 'memberUid\: john', it is expected that particular LDAP user will have the attribute 'uid\: john'.
samlCapabilityConfig=SAML capabilities samlCapabilityConfig=SAML capabilities
accessTokenSignatureAlgorithmHelp=JWA algorithm used for signing access tokens. accessTokenSignatureAlgorithmHelp=JWA algorithm used for signing access tokens.

View File

@ -1,6 +1,7 @@
import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig"; import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig";
import { import {
HelpItem, HelpItem,
NumberControl,
SelectControl, SelectControl,
FileUploadControl, FileUploadControl,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
@ -59,6 +60,7 @@ export const KeyForm = ({
...(cryptoInfo?.supportedKeystoreTypes ?? []), ...(cryptoInfo?.supportedKeystoreTypes ?? []),
...(hasPem ? [CERT_PEM] : []), ...(hasPem ? [CERT_PEM] : []),
]; ];
const keySizes = ["4096", "3072", "2048"];
return ( return (
<Form className="pf-v5-u-pt-lg"> <Form className="pf-v5-u-pt-lg">
@ -91,6 +93,25 @@ export const KeyForm = ({
{format !== CERT_PEM && ( {format !== CERT_PEM && (
<StoreSettings hidePassword={useFile} isSaml={isSaml} /> <StoreSettings hidePassword={useFile} isSaml={isSaml} />
)} )}
<SelectControl
name="keySize"
label={t("keySize")}
labelIcon={t("keySizeHelp")}
controller={{
defaultValue: keySizes[0],
}}
menuAppendTo="parent"
options={keySizes}
/>
<NumberControl
name="validity"
label={t("validity")}
labelIcon={t("validityHelp")}
controller={{
defaultValue: 3,
rules: { required: t("required"), min: 1, max: 10 },
}}
/>
</Form> </Form>
); );
}; };

View File

@ -8,4 +8,6 @@ export default interface KeyStoreConfig {
keyAlias?: string; keyAlias?: string;
realmAlias?: string; realmAlias?: string;
format?: string; format?: string;
keySize?: number;
validity?: number;
} }

View File

@ -65,6 +65,7 @@ import jakarta.transaction.InvalidTransactionException;
import jakarta.transaction.SystemException; import jakarta.transaction.SystemException;
import jakarta.transaction.Transaction; import jakarta.transaction.Transaction;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.Key; import java.security.Key;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PrivateKey; import java.security.PrivateKey;
@ -72,6 +73,7 @@ import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Calendar;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -218,8 +220,14 @@ public final class KeycloakModelUtils {
} }
public static CertificateRepresentation generateKeyPairCertificate(String subject) { public static CertificateRepresentation generateKeyPairCertificate(String subject) {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); Calendar calendar = Calendar.getInstance();
X509Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject); calendar.add(Calendar.YEAR, 3);
return generateKeyPairCertificate(subject, 4096, calendar);
}
public static CertificateRepresentation generateKeyPairCertificate(String subject, int keysize, Calendar endDate) {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(keysize);
X509Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject, BigInteger.valueOf(System.currentTimeMillis()), endDate.getTime());
String privateKeyPem = PemUtils.encodeKey(keyPair.getPrivate()); String privateKeyPem = PemUtils.encodeKey(keyPair.getPrivate());
String certPem = PemUtils.encodeCertificate(certificate); String certPem = PemUtils.encodeCertificate(certificate);

View File

@ -18,6 +18,7 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import jakarta.ws.rs.QueryParam;
import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
@ -64,6 +65,7 @@ import java.security.PrivateKey;
import java.security.UnrecoverableKeyException; import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -340,7 +342,15 @@ public class ClientAttributeCertificateResource {
throw new ErrorResponseException("password-missing", "Need to specify a store password for jks generation and download", Response.Status.BAD_REQUEST); throw new ErrorResponseException("password-missing", "Need to specify a store password for jks generation and download", Response.Status.BAD_REQUEST);
} }
CertificateRepresentation info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId()); CertificateRepresentation info;
if (config.getKeySize() <= 0 || config.getValidity() <= 0) {
info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId());
}
else {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, config.getValidity());
info = KeycloakModelUtils.generateKeyPairCertificate(client.getClientId(), config.getKeySize(), calendar);
}
byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate()); byte[] rtn = getKeystore(config, info.getPrivateKey(), info.getCertificate());
info.setPrivateKey(null); info.setPrivateKey(null);

View File

@ -240,6 +240,8 @@ public class CredentialsTest extends AbstractClientTest {
config.setKeyAlias("alias"); config.setKeyAlias("alias");
config.setKeyPassword("keyPass"); config.setKeyPassword("keyPass");
config.setStorePassword("storePass"); config.setStorePassword("storePass");
config.setKeySize(4096);
config.setValidity(3);
byte[] result = certRsc.generateAndGetKeystore(config); byte[] result = certRsc.generateAndGetKeystore(config);
KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(preferredKeystoreType); KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(preferredKeystoreType);
keyStore.load(new ByteArrayInputStream(result), "storePass".toCharArray()); keyStore.load(new ByteArrayInputStream(result), "storePass".toCharArray());

View File

@ -389,6 +389,8 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe
keyStoreConfig.setKeyPassword(keyPassword); keyStoreConfig.setKeyPassword(keyPassword);
keyStoreConfig.setStorePassword(storePassword); keyStoreConfig.setStorePassword(storePassword);
keyStoreConfig.setKeyAlias(keyAlias); keyStoreConfig.setKeyAlias(keyAlias);
keyStoreConfig.setKeySize(4096);
keyStoreConfig.setValidity(3);
client = getClient(testRealm.getRealm(), client.getId()).toRepresentation(); client = getClient(testRealm.getRealm(), client.getId()).toRepresentation();
final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR); final String certOld = client.getAttributes().get(JWTClientAuthenticator.CERTIFICATE_ATTR);