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:
parent
01026fab79
commit
6d52520730
6 changed files with 138 additions and 49 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
info = CertificateInfoHelper.jwksStringToSigCertificateRepresentation(jwks);
|
||||||
if (publicKeyJwk == null) {
|
// jwks is only valid for OIDC clients
|
||||||
throw new IllegalStateException("Certificate not found for use sig");
|
if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) {
|
||||||
|
CertificateInfoHelper.updateClientModelJwksString(client, attributePrefix, jwks);
|
||||||
} else {
|
} else {
|
||||||
PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey();
|
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
|
||||||
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
|
}
|
||||||
info.setPublicKey(publicKeyPem);
|
|
||||||
info.setKid(publicKeyJwk.getKeyId());
|
|
||||||
return info;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
Loading…
Reference in a new issue