Load client keys using SubjectPublicKeyInfo and upload jwks type into the jwks attributes for OIDC ones

Closes #33820

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-10-13 12:23:57 +02:00 committed by Marek Posolda
parent 01026fab79
commit 6d52520730
6 changed files with 138 additions and 49 deletions

View file

@ -61,7 +61,10 @@ public abstract class PemUtilsTest {
public void testDecodeObjectsInPEMFormat() { public void testDecodeObjectsInPEMFormat() {
String privateKey1 = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y="; String privateKey1 = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
String publicKey1 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; String publicKey1 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
String publicKeyEC = "-----BEGIN PUBLIC KEY-----\n"
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElyCs9XI47lFR5l4WafsZZrAiUmEr\n"
+ "+kYeStgx3tyPntt3YNfs6kAVNozI4aJqdqDjITJWatHm6boJ0BRLPNphRA==\n"
+ "-----END PUBLIC KEY-----";
String cert1 = "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="; String cert1 = "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ==";
String cert2 = "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w=="; String cert2 = "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w==";
@ -84,6 +87,7 @@ public abstract class PemUtilsTest {
testPrivateKeyEncodeDecode(privateKey1); testPrivateKeyEncodeDecode(privateKey1);
testPublicKeyEncodeDecode(publicKey1); testPublicKeyEncodeDecode(publicKey1);
testPublicKeyEncodeDecode(publicKeyEC);
testPrivateKeyEncodeDecode(PemUtils.removeBeginEnd(privateKey2).replace("\n", "")); testPrivateKeyEncodeDecode(PemUtils.removeBeginEnd(privateKey2).replace("\n", ""));
testCertificateEncodeDecode(cert1); testCertificateEncodeDecode(cert1);
testCertificateEncodeDecode(cert2); testCertificateEncodeDecode(cert2);
@ -125,7 +129,7 @@ public abstract class PemUtilsTest {
private void testPublicKeyEncodeDecode(String origPublicKeyPem) { private void testPublicKeyEncodeDecode(String origPublicKeyPem) {
PublicKey decodedPublicKey = PemUtils.decodePublicKey(origPublicKeyPem); PublicKey decodedPublicKey = PemUtils.decodePublicKey(origPublicKeyPem);
String encodedPublicKey = PemUtils.encodeKey(decodedPublicKey); String encodedPublicKey = PemUtils.encodeKey(decodedPublicKey);
assertEquals(origPublicKeyPem, encodedPublicKey); assertEquals(PemUtils.removeBeginEnd(origPublicKeyPem), encodedPublicKey);
} }
private void testCertificateEncodeDecode(String origCertPem) { private void testCertificateEncodeDecode(String origCertPem) {

View file

@ -17,6 +17,8 @@
package org.keycloak.crypto.def; package org.keycloak.crypto.def;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.keycloak.common.util.DerUtils; import org.keycloak.common.util.DerUtils;
import org.keycloak.common.util.PemException; import org.keycloak.common.util.PemException;
@ -24,6 +26,7 @@ import org.keycloak.common.crypto.PemUtilsProvider;
import java.io.StringWriter; import java.io.StringWriter;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey;
/** /**
* Encodes Key or Certificates to PEM format string * Encodes Key or Certificates to PEM format string
@ -59,6 +62,22 @@ public class BCPemUtilsProvider extends PemUtilsProvider {
} }
} }
@Override
public PublicKey decodePublicKey(String pem) {
try {
// try to decode using SubjectPublicKeyInfo which allows to know the key type
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemToDer(pem));
if (publicKeyInfo != null && publicKeyInfo.getAlgorithm() != null) {
return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo);
}
} catch (Exception e) {
// error reading PEM object just go to previous RSA forced key
}
// assume RSA if it cannot be decoded from BC knowing the key
return decodePublicKey(pem, "RSA");
}
@Override @Override
public PrivateKey decodePrivateKey(String pem) { public PrivateKey decodePrivateKey(String pem) {
if (pem == null) { if (pem == null) {

View file

@ -18,12 +18,12 @@
package org.keycloak.crypto.fips; package org.keycloak.crypto.fips;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.keycloak.common.util.BouncyIntegration; import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.common.util.DerUtils;
import org.keycloak.common.util.PemException; import org.keycloak.common.util.PemException;
import org.keycloak.common.crypto.PemUtilsProvider; import org.keycloak.common.crypto.PemUtilsProvider;
import org.keycloak.common.util.PemUtils; import org.keycloak.common.util.PemUtils;
@ -31,9 +31,8 @@ import org.keycloak.common.util.PemUtils;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import java.security.KeyFactory;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.PublicKey;
/** /**
* Encodes Key or Certificates to PEM format string * Encodes Key or Certificates to PEM format string
@ -69,6 +68,22 @@ public class BCFIPSPemUtilsProvider extends PemUtilsProvider {
} }
} }
@Override
public PublicKey decodePublicKey(String pem) {
try {
// try to decode using SubjectPublicKeyInfo which allows to know the key type
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemToDer(pem));
if (publicKeyInfo != null && publicKeyInfo.getAlgorithm() != null) {
return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo);
}
} catch (Exception e) {
// error reading PEM object just go to previous RSA forced key
}
// assume RSA if it cannot be decoded from BC knowing the key
return decodePublicKey(pem, "RSA");
}
@Override @Override
public PrivateKey decodePrivateKey(String pem) { public PrivateKey decodePrivateKey(String pem) {
if (pem == null) { if (pem == null) {

View file

@ -32,22 +32,18 @@ import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
import org.keycloak.http.FormPartValue; import org.keycloak.http.FormPartValue;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager; import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.util.JWKSUtils;
import org.keycloak.util.JsonSerialization;
import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
@ -60,10 +56,9 @@ import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.nio.charset.StandardCharsets;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Set; import java.util.Set;
@ -140,7 +135,6 @@ public class ClientAttributeCertificateResource {
/** /**
* Upload certificate and eventually private key * Upload certificate and eventually private key
* *
* @param input
* @return * @return
* @throws IOException * @throws IOException
*/ */
@ -154,9 +148,7 @@ public class ClientAttributeCertificateResource {
auth.clients().requireConfigure(client); auth.clients().requireConfigure(client);
try { try {
CertificateRepresentation info = getCertFromRequest(); CertificateRepresentation info = updateCertFromRequest();
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info; return info;
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
@ -167,7 +159,6 @@ public class ClientAttributeCertificateResource {
/** /**
* Upload only certificate, not private key * Upload only certificate, not private key
* *
* @param input
* @return information extracted from uploaded certificate - not necessarily the new state of certificate on the server * @return information extracted from uploaded certificate - not necessarily the new state of certificate on the server
* @throws IOException * @throws IOException
*/ */
@ -181,10 +172,7 @@ public class ClientAttributeCertificateResource {
auth.clients().requireConfigure(client); auth.clients().requireConfigure(client);
try { try {
CertificateRepresentation info = getCertFromRequest(); CertificateRepresentation info = updateCertFromRequest();
info.setPrivateKey(null);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success(); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info; return info;
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
@ -192,7 +180,7 @@ public class ClientAttributeCertificateResource {
} }
} }
private CertificateRepresentation getCertFromRequest() throws IOException { private CertificateRepresentation updateCertFromRequest() throws IOException {
auth.clients().requireManage(client); auth.clients().requireManage(client);
CertificateRepresentation info = new CertificateRepresentation(); CertificateRepresentation info = new CertificateRepresentation();
MultivaluedMap<String, FormPartValue> uploadForm = session.getContext().getHttpRequest().getMultiPartFormParameters(); MultivaluedMap<String, FormPartValue> uploadForm = session.getContext().getHttpRequest().getMultiPartFormParameters();
@ -203,38 +191,34 @@ public class ClientAttributeCertificateResource {
String keystoreFormat = keystoreFormatPart.asString(); String keystoreFormat = keystoreFormatPart.asString();
FormPartValue inputParts = uploadForm.getFirst("file"); FormPartValue inputParts = uploadForm.getFirst("file");
if (keystoreFormat.equals(CERTIFICATE_PEM)) { if (keystoreFormat.equals(CERTIFICATE_PEM)) {
String pem = StreamUtil.readString(inputParts.asInputStream()); String pem = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8);
pem = PemUtils.removeBeginEnd(pem); pem = PemUtils.removeBeginEnd(pem);
// Validate format // Validate format
KeycloakModelUtils.getCertificate(pem); KeycloakModelUtils.getCertificate(pem);
info.setCertificate(pem); info.setCertificate(pem);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
return info; return info;
} else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) { } else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) {
String pem = StreamUtil.readString(inputParts.asInputStream()); String pem = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8);
// Validate format // Validate format
KeycloakModelUtils.getPublicKey(pem); KeycloakModelUtils.getPublicKey(pem);
info.setPublicKey(pem); info.setPublicKey(pem);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
return info; return info;
} else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) { } else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
InputStream stream = inputParts.asInputStream(); String jwks = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8);
JSONWebKeySet keySet = JsonSerialization.readValue(stream, JSONWebKeySet.class);
JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
if (publicKeyJwk == null) {
throw new IllegalStateException("Certificate not found for use sig");
} else {
PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey();
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
info.setPublicKey(publicKeyPem);
info.setKid(publicKeyJwk.getKeyId());
return info;
}
}
info = CertificateInfoHelper.jwksStringToSigCertificateRepresentation(jwks);
// jwks is only valid for OIDC clients
if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) {
CertificateInfoHelper.updateClientModelJwksString(client, attributePrefix, jwks);
} else {
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
}
return info;
}
String keyAlias = uploadForm.getFirst("keyAlias").asString(); String keyAlias = uploadForm.getFirst("keyAlias").asString();
FormPartValue keyPasswordPart = uploadForm.getFirst("keyPassword"); FormPartValue keyPasswordPart = uploadForm.getFirst("keyPassword");
@ -267,6 +251,7 @@ public class ClientAttributeCertificateResource {
info.setCertificate(certPem); info.setCertificate(certPem);
} }
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
return info; return info;
} }

View file

@ -18,14 +18,20 @@
package org.keycloak.services.util; package org.keycloak.services.util;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import java.io.IOException;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.HashMap; import java.util.HashMap;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.util.JWKSUtils;
import org.keycloak.util.JsonSerialization;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -48,6 +54,11 @@ public class CertificateInfoHelper {
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY; String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID; String kidAttribute = attributePrefix + "." + KID;
if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())
&& Boolean.parseBoolean(client.getAttribute(OIDCConfigAttributes.USE_JWKS_STRING))) {
return jwksStringToSigCertificateRepresentation(client.getAttribute(OIDCConfigAttributes.JWKS_STRING));
}
CertificateRepresentation rep = new CertificateRepresentation(); CertificateRepresentation rep = new CertificateRepresentation();
rep.setCertificate(client.getAttribute(certificateAttribute)); rep.setCertificate(client.getAttribute(certificateAttribute));
rep.setPublicKey(client.getAttribute(publicKeyAttribute)); rep.setPublicKey(client.getAttribute(publicKeyAttribute));
@ -57,13 +68,30 @@ public class CertificateInfoHelper {
return rep; return rep;
} }
public static CertificateRepresentation jwksStringToSigCertificateRepresentation(String jwks) {
if (jwks == null) {
throw new IllegalStateException("The jwks is null!");
}
try {
JSONWebKeySet keySet = JsonSerialization.readValue(jwks, JSONWebKeySet.class);
JWK publicKeyJwk = JWKSUtils.getKeyForUse(keySet, JWK.Use.SIG);
if (publicKeyJwk == null) {
throw new IllegalStateException("Certificate not found for use sig");
}
PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey();
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
CertificateRepresentation info = new CertificateRepresentation();
info.setPublicKey(publicKeyPem);
info.setKid(publicKeyJwk.getKeyId());
return info;
} catch (IOException e) {
throw new IllegalStateException("Invalid jwks representation!", e);
}
}
public static void updateClientModelCertificateInfo(ClientModel client, CertificateRepresentation rep, String attributePrefix) { public static void updateClientModelCertificateInfo(ClientModel client, CertificateRepresentation rep, String attributePrefix) {
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID;
if (rep.getPublicKey() == null && rep.getCertificate() == null) { if (rep.getPublicKey() == null && rep.getCertificate() == null) {
throw new IllegalStateException("Both certificate and publicKey are null!"); throw new IllegalStateException("Both certificate and publicKey are null!");
} }
@ -72,10 +100,42 @@ public class CertificateInfoHelper {
throw new IllegalStateException("Both certificate and publicKey are not null!"); throw new IllegalStateException("Both certificate and publicKey are not null!");
} }
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID;
setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey()); setOrRemoveAttr(client, privateKeyAttribute, rep.getPrivateKey());
setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey()); setOrRemoveAttr(client, publicKeyAttribute, rep.getPublicKey());
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate()); setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
setOrRemoveAttr(client, kidAttribute, rep.getKid()); setOrRemoveAttr(client, kidAttribute, rep.getKid());
if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) {
setOrRemoveAttr(client, OIDCConfigAttributes.USE_JWKS_STRING, null);
setOrRemoveAttr(client, OIDCConfigAttributes.JWKS_STRING, null);
}
}
public static void updateClientModelJwksString(ClientModel client, String attributePrefix, String jwks) {
if (jwks == null) {
throw new IllegalStateException("jwks string is null!");
}
if (!OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) {
throw new IllegalStateException("jwks can only be set for OIDC clients!");
}
String privateKeyAttribute = attributePrefix + "." + PRIVATE_KEY;
String certificateAttribute = attributePrefix + "." + X509CERTIFICATE;
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
String kidAttribute = attributePrefix + "." + KID;
setOrRemoveAttr(client, privateKeyAttribute, null);
setOrRemoveAttr(client, publicKeyAttribute, null);
setOrRemoveAttr(client, certificateAttribute, null);
setOrRemoveAttr(client, kidAttribute, null);
setOrRemoveAttr(client, OIDCConfigAttributes.USE_JWKS_STRING, Boolean.TRUE.toString());
setOrRemoveAttr(client, OIDCConfigAttributes.JWKS_STRING, jwks);
} }
private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) { private static void setOrRemoveAttr(ClientModel client, String attrName, String attrValue) {

View file

@ -99,6 +99,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -475,9 +476,14 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe
final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY); final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY);
assertEquals("Certificates don't match", pem, publicKeyNew); assertEquals("Certificates don't match", pem, publicKeyNew);
} else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) { } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.JSON_WEB_KEY_SET)) {
final String publicKeyNew = client.getAttributes().get(JWTClientAuthenticator.ATTR_PREFIX + "." + CertificateInfoHelper.PUBLIC_KEY); Assert.assertEquals("true", client.getAttributes().get(OIDCConfigAttributes.USE_JWKS_STRING));
String jwks = new String(Files.readAllBytes(keystoreFile.toPath()));
Assert.assertEquals(jwks, client.getAttributes().get(OIDCConfigAttributes.JWKS_STRING));
CertificateRepresentation info = getClient(testRealm.getRealm(), client.getId())
.getCertficateResource(JWTClientAuthenticator.ATTR_PREFIX).getKeyInfo();
Assert.assertNotNull(info.getPublicKey());
// Just assert it's valid public key // Just assert it's valid public key
PublicKey pk = KeycloakModelUtils.getPublicKey(publicKeyNew); PublicKey pk = KeycloakModelUtils.getPublicKey(info.getPublicKey());
Assert.assertNotNull(pk); Assert.assertNotNull(pk);
} else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) { } else if (keystoreFormat.equals(org.keycloak.services.resources.admin.ClientAttributeCertificateResource.CERTIFICATE_PEM)) {
String pem = new String(Files.readAllBytes(keystoreFile.toPath())); String pem = new String(Files.readAllBytes(keystoreFile.toPath()));