Skip to content

Commit

Permalink
Load client keys using SubjectPublicKeyInfo and upload jwks type into…
Browse files Browse the repository at this point in the history
… the jwks attributes for OIDC ones

Closes keycloak#33820

Signed-off-by: rmartinc <[email protected]>
  • Loading branch information
rmartinc authored and pedroigor committed Oct 22, 2024
1 parent 5d73a96 commit 98e635f
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 47 deletions.
8 changes: 6 additions & 2 deletions core/src/test/java/org/keycloak/util/PemUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ public void testEncodeAndDecodeGeneratedObjects() {
public void testDecodeObjectsInPEMFormat() {
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 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 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==";

Expand All @@ -84,6 +87,7 @@ public void testDecodeObjectsInPEMFormat() {

testPrivateKeyEncodeDecode(privateKey1);
testPublicKeyEncodeDecode(publicKey1);
testPublicKeyEncodeDecode(publicKeyEC);
testPrivateKeyEncodeDecode(PemUtils.removeBeginEnd(privateKey2).replace("\n", ""));
testCertificateEncodeDecode(cert1);
testCertificateEncodeDecode(cert2);
Expand Down Expand Up @@ -125,7 +129,7 @@ private void testPrivateKeyEncodeDecode(String origPrivateKeyPem) {
private void testPublicKeyEncodeDecode(String origPublicKeyPem) {
PublicKey decodedPublicKey = PemUtils.decodePublicKey(origPublicKeyPem);
String encodedPublicKey = PemUtils.encodeKey(decodedPublicKey);
assertEquals(origPublicKeyPem, encodedPublicKey);
assertEquals(PemUtils.removeBeginEnd(origPublicKeyPem), encodedPublicKey);
}

private void testCertificateEncodeDecode(String origCertPem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@

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.keycloak.common.util.DerUtils;
import org.keycloak.common.util.PemException;
import org.keycloak.common.crypto.PemUtilsProvider;

import java.io.StringWriter;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
* Encodes Key or Certificates to PEM format string
Expand Down Expand Up @@ -59,6 +62,22 @@ protected String encode(Object obj) {
}
}

@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
public PrivateKey decodePrivateKey(String pem) {
if (pem == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,21 @@
package org.keycloak.crypto.fips;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.common.util.DerUtils;
import org.keycloak.common.util.PemException;
import org.keycloak.common.crypto.PemUtilsProvider;
import org.keycloak.common.util.PemUtils;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.PublicKey;

/**
* Encodes Key or Certificates to PEM format string
Expand Down Expand Up @@ -69,6 +68,22 @@ protected String encode(Object obj) {
}
}

@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
public PrivateKey decodePrivateKey(String pem) {
if (pem == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,18 @@
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
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.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.KeyStoreConfig;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
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.Consumes;
Expand All @@ -60,10 +56,9 @@
import jakarta.ws.rs.core.Response;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Set;
Expand Down Expand Up @@ -140,7 +135,6 @@ public CertificateRepresentation generate() {
/**
* Upload certificate and eventually private key
*
* @param input
* @return
* @throws IOException
*/
Expand All @@ -154,9 +148,7 @@ public CertificateRepresentation uploadJks() throws IOException {
auth.clients().requireConfigure(client);

try {
CertificateRepresentation info = getCertFromRequest();
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);

CertificateRepresentation info = updateCertFromRequest();
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info;
} catch (IllegalStateException ise) {
Expand All @@ -167,7 +159,6 @@ public CertificateRepresentation uploadJks() throws IOException {
/**
* Upload only certificate, not private key
*
* @param input
* @return information extracted from uploaded certificate - not necessarily the new state of certificate on the server
* @throws IOException
*/
Expand All @@ -181,18 +172,15 @@ public CertificateRepresentation uploadJksCertificate() throws IOException {
auth.clients().requireConfigure(client);

try {
CertificateRepresentation info = getCertFromRequest();
info.setPrivateKey(null);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);

CertificateRepresentation info = updateCertFromRequest();
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
return info;
} catch (IllegalStateException ise) {
throw new ErrorResponseException("certificate-not-found", "Certificate or key with given alias not found in the keystore", Response.Status.BAD_REQUEST);
}
}

private CertificateRepresentation getCertFromRequest() throws IOException {
private CertificateRepresentation updateCertFromRequest() throws IOException {
auth.clients().requireManage(client);
CertificateRepresentation info = new CertificateRepresentation();
MultivaluedMap<String, FormPartValue> uploadForm = session.getContext().getHttpRequest().getMultiPartFormParameters();
Expand All @@ -203,39 +191,35 @@ private CertificateRepresentation getCertFromRequest() throws IOException {
String keystoreFormat = keystoreFormatPart.asString();
FormPartValue inputParts = uploadForm.getFirst("file");
if (keystoreFormat.equals(CERTIFICATE_PEM)) {
String pem = StreamUtil.readString(inputParts.asInputStream());

String pem = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8);
pem = PemUtils.removeBeginEnd(pem);

// Validate format
KeycloakModelUtils.getCertificate(pem);

info.setCertificate(pem);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
return info;
} else if (keystoreFormat.equals(PUBLIC_KEY_PEM)) {
String pem = StreamUtil.readString(inputParts.asInputStream());
String pem = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8);

// Validate format
KeycloakModelUtils.getPublicKey(pem);

info.setPublicKey(pem);
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
return info;
} else if (keystoreFormat.equals(JSON_WEB_KEY_SET)) {
InputStream stream = inputParts.asInputStream();
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");
String jwks = StreamUtil.readString(inputParts.asInputStream(), StandardCharsets.UTF_8);

info = CertificateInfoHelper.jwksStringToSigCertificateRepresentation(jwks);
// jwks is only valid for OIDC clients
if (OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol())) {
CertificateInfoHelper.updateClientModelJwksString(client, attributePrefix, jwks);
} else {
PublicKey publicKey = JWKParser.create(publicKeyJwk).toPublicKey();
String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey);
info.setPublicKey(publicKeyPem);
info.setKid(publicKeyJwk.getKeyId());
return info;
CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
}
return info;
}


String keyAlias = uploadForm.getFirst("keyAlias").asString();
FormPartValue keyPasswordPart = uploadForm.getFirst("keyPassword");
char[] keyPassword = keyPasswordPart != null ? keyPasswordPart.asString().toCharArray() : null;
Expand Down Expand Up @@ -267,6 +251,7 @@ private CertificateRepresentation getCertFromRequest() throws IOException {
info.setCertificate(certPem);
}

CertificateInfoHelper.updateClientModelCertificateInfo(client, info, attributePrefix);
return info;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@
package org.keycloak.services.util;

import org.keycloak.models.ClientModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;

import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
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:[email protected]">Marek Posolda</a>
Expand All @@ -48,6 +54,11 @@ public static CertificateRepresentation getCertificateFromClient(ClientModel cli
String publicKeyAttribute = attributePrefix + "." + PUBLIC_KEY;
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();
rep.setCertificate(client.getAttribute(certificateAttribute));
rep.setPublicKey(client.getAttribute(publicKeyAttribute));
Expand All @@ -57,13 +68,30 @@ public static CertificateRepresentation getCertificateFromClient(ClientModel cli
return rep;
}

public static CertificateRepresentation jwksStringToSigCertificateRepresentation(String jwks) {
if (jwks == null) {
throw new IllegalStateException("The jwks is null!");
}

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;
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) {
if (rep.getPublicKey() == null && rep.getCertificate() == null) {
throw new IllegalStateException("Both certificate and publicKey are null!");
}
Expand All @@ -72,10 +100,42 @@ public static void updateClientModelCertificateInfo(ClientModel client, Certific
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, publicKeyAttribute, rep.getPublicKey());
setOrRemoveAttr(client, certificateAttribute, rep.getCertificate());
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) {
Expand Down
Loading

0 comments on commit 98e635f

Please sign in to comment.