diff --git a/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java new file mode 100644 index 0000000000..90574e0d38 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import org.keycloak.common.util.CertificateUtils; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.PemUtils; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.crypto.KeyUse; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ConfigurationValidationHelper; +import org.keycloak.provider.ProviderConfigurationBuilder; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; + +/** + * @author Stian Thorgersen + * @author Filipe Bojikian Rissi + */ +public abstract class AbstractImportedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactory { + + public final static ProviderConfigurationBuilder rsaKeyConfigurationBuilder() { + return ProviderConfigurationBuilder.create() + .property(Attributes.PRIORITY_PROPERTY) + .property(Attributes.ENABLED_PROPERTY) + .property(Attributes.ACTIVE_PROPERTY) + .property(Attributes.PRIVATE_KEY_PROPERTY) + .property(Attributes.CERTIFICATE_PROPERTY); + } + + @Override + public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { + ConfigurationValidationHelper.check(model) + .checkLong(Attributes.PRIORITY_PROPERTY, false) + .checkBoolean(Attributes.ENABLED_PROPERTY, false) + .checkBoolean(Attributes.ACTIVE_PROPERTY, false) + .checkSingle(Attributes.PRIVATE_KEY_PROPERTY, true) + .checkSingle(Attributes.CERTIFICATE_PROPERTY, false); + + KeyPair keyPair; + try { + PrivateKey privateKey = PemUtils.decodePrivateKey(model.get(Attributes.PRIVATE_KEY_KEY)); + PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); + keyPair = new KeyPair(publicKey, privateKey); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to decode private key", t); + } + + if (model.contains(Attributes.CERTIFICATE_KEY)) { + Certificate certificate = null; + try { + certificate = PemUtils.decodeCertificate(model.get(Attributes.CERTIFICATE_KEY)); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to decode certificate", t); + } + + if (certificate == null) { + throw new ComponentValidationException("Failed to decode certificate"); + } + + if (!certificate.getPublicKey().equals(keyPair.getPublic())) { + throw new ComponentValidationException("Certificate does not match private key"); + } + } else { + try { + Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName()); + model.put(Attributes.CERTIFICATE_KEY, PemUtils.encodeCertificate(certificate)); + } catch (Throwable t) { + throw new ComponentValidationException("Failed to generate self-signed certificate"); + } + } + } + + abstract protected boolean isValidKeyUse(KeyUse keyUse); + + abstract protected boolean isSupportedRsaAlgorithm(String algorithm); + +} diff --git a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java index 47e36ba047..0ca522d257 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java @@ -20,6 +20,7 @@ package org.keycloak.keys; import org.keycloak.common.util.KeyUtils; import org.keycloak.component.ComponentModel; import org.keycloak.crypto.*; +import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.models.RealmModel; import java.security.KeyPair; @@ -44,7 +45,9 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider { public AbstractRsaKeyProvider(RealmModel realm, ComponentModel model) { this.model = model; this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true)); - this.algorithm = model.get(Attributes.ALGORITHM_KEY, Algorithm.RS256); + + String defaultAlgorithmKey = KeyUse.ENC.name().equals(model.get(Attributes.KEY_USE)) ? JWEConstants.RSA_OAEP : Algorithm.RS256; + this.algorithm = model.get(Attributes.ALGORITHM_KEY, defaultAlgorithmKey); if (model.hasNote(KeyWrapper.class.getName())) { key = model.getNote(KeyWrapper.class.getName()); diff --git a/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java new file mode 100644 index 0000000000..7433286f99 --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/ImportedRsaEncKeyProviderFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.keys; + +import org.keycloak.component.ComponentModel; +import org.keycloak.crypto.KeyUse; +import org.keycloak.jose.jwe.JWEConstants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.List; + +/** + * @author Filipe Bojikian Rissi + */ +public class ImportedRsaEncKeyProviderFactory extends AbstractImportedRsaKeyProviderFactory { + + public static final String ID = "rsa-enc"; + + private static final String HELP_TEXT = "RSA for key encryption provider that can optionally generated a self-signed certificate"; + + private static final List CONFIG_PROPERTIES = AbstractImportedRsaKeyProviderFactory.rsaKeyConfigurationBuilder() + .property(Attributes.RS_ENC_ALGORITHM_PROPERTY) + .build(); + + @Override + public KeyProvider create(KeycloakSession session, ComponentModel model) { + model.put(Attributes.KEY_USE, KeyUse.ENC.name()); + return new ImportedRsaKeyProvider(session.getContext().getRealm(), model); + } + + @Override + public String getHelpText() { + return HELP_TEXT; + } + + @Override + public List getConfigProperties() { + return CONFIG_PROPERTIES; + } + + @Override + protected boolean isValidKeyUse(KeyUse keyUse) { + return keyUse.equals(KeyUse.ENC); + } + + @Override + protected boolean isSupportedRsaAlgorithm(String algorithm) { + return algorithm.equals(JWEConstants.RSA1_5) + || algorithm.equals(JWEConstants.RSA_OAEP) + || algorithm.equals(JWEConstants.RSA_OAEP_256); + } + + @Override + public String getId() { + return ID; + } + +} diff --git a/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProviderFactory.java index 57a0978bb5..281d023b05 100644 --- a/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProviderFactory.java @@ -17,84 +17,36 @@ package org.keycloak.keys; -import org.keycloak.common.util.CertificateUtils; -import org.keycloak.common.util.KeyUtils; -import org.keycloak.common.util.PemUtils; import org.keycloak.component.ComponentModel; -import org.keycloak.component.ComponentValidationException; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.provider.ConfigurationValidationHelper; import org.keycloak.provider.ProviderConfigProperty; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.Certificate; import java.util.List; /** * @author Stian Thorgersen */ -public class ImportedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactory { +public class ImportedRsaKeyProviderFactory extends AbstractImportedRsaKeyProviderFactory { public static final String ID = "rsa"; - private static final String HELP_TEXT = "RSA key provider that can optionally generated a self-signed certificate"; + private static final String HELP_TEXT = "RSA signature key provider that can optionally generated a self-signed certificate"; - private static final List CONFIG_PROPERTIES = AbstractRsaKeyProviderFactory.configurationBuilder() - .property(Attributes.PRIVATE_KEY_PROPERTY) - .property(Attributes.CERTIFICATE_PROPERTY) - .property(Attributes.KEY_USE_PROPERTY) + private static final List CONFIG_PROPERTIES = AbstractImportedRsaKeyProviderFactory.rsaKeyConfigurationBuilder() + .property(Attributes.RS_ALGORITHM_PROPERTY) .build(); @Override public KeyProvider create(KeycloakSession session, ComponentModel model) { + if (model.getConfig().get(Attributes.KEY_USE) == null) { + // for backward compatibility : it allows "enc" key use for "rsa" provider + model.put(Attributes.KEY_USE, KeyUse.SIG.name()); + } return new ImportedRsaKeyProvider(session.getContext().getRealm(), model); } - @Override - public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { - super.validateConfiguration(session, realm, model); - - ConfigurationValidationHelper.check(model) - .checkSingle(Attributes.PRIVATE_KEY_PROPERTY, true) - .checkSingle(Attributes.CERTIFICATE_PROPERTY, false); - - KeyPair keyPair; - try { - PrivateKey privateKey = PemUtils.decodePrivateKey(model.get(Attributes.PRIVATE_KEY_KEY)); - PublicKey publicKey = KeyUtils.extractPublicKey(privateKey); - keyPair = new KeyPair(publicKey, privateKey); - } catch (Throwable t) { - throw new ComponentValidationException("Failed to decode private key", t); - } - - if (model.contains(Attributes.CERTIFICATE_KEY)) { - Certificate certificate = null; - try { - certificate = PemUtils.decodeCertificate(model.get(Attributes.CERTIFICATE_KEY)); - } catch (Throwable t) { - throw new ComponentValidationException("Failed to decode certificate", t); - } - - if (certificate == null) { - throw new ComponentValidationException("Failed to decode certificate"); - } - - if (!certificate.getPublicKey().equals(keyPair.getPublic())) { - throw new ComponentValidationException("Certificate does not match private key"); - } - } else { - try { - Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName()); - model.put(Attributes.CERTIFICATE_KEY, PemUtils.encodeCertificate(certificate)); - } catch (Throwable t) { - throw new ComponentValidationException("Failed to generate self-signed certificate"); - } - } - } - @Override public String getHelpText() { return HELP_TEXT; @@ -105,6 +57,21 @@ public class ImportedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactory return CONFIG_PROPERTIES; } + @Override + protected boolean isValidKeyUse(KeyUse keyUse) { + return keyUse.equals(KeyUse.SIG); + } + + @Override + protected boolean isSupportedRsaAlgorithm(String algorithm) { + return algorithm.equals(Algorithm.RS256) + || algorithm.equals(Algorithm.PS256) + || algorithm.equals(Algorithm.RS384) + || algorithm.equals(Algorithm.PS384) + || algorithm.equals(Algorithm.RS512) + || algorithm.equals(Algorithm.PS512); + } + @Override public String getId() { return ID; diff --git a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory index 4a587c481e..02b8fb901a 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.keys.KeyProviderFactory @@ -21,4 +21,5 @@ org.keycloak.keys.GeneratedRsaKeyProviderFactory org.keycloak.keys.JavaKeystoreKeyProviderFactory org.keycloak.keys.ImportedRsaKeyProviderFactory org.keycloak.keys.GeneratedEcdsaKeyProviderFactory -org.keycloak.keys.GeneratedRsaEncKeyProviderFactory \ No newline at end of file +org.keycloak.keys.GeneratedRsaEncKeyProviderFactory +org.keycloak.keys.ImportedRsaEncKeyProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java index 47e850e928..d2ac940538 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/ImportedRsaKeyProviderTest.java @@ -25,10 +25,12 @@ import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyUse; +import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.jose.jws.AlgorithmType; import org.keycloak.keys.Attributes; +import org.keycloak.keys.ImportedRsaEncKeyProviderFactory; import org.keycloak.keys.ImportedRsaKeyProviderFactory; -import org.keycloak.keys.KeyMetadata; import org.keycloak.keys.KeyProvider; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; @@ -69,13 +71,22 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { } @Test - public void privateKeyOnly() throws Exception { + public void privateKeyOnlyForSig() throws Exception { + privateKeyOnly(ImportedRsaKeyProviderFactory.ID, KeyUse.SIG, Algorithm.RS256); + } + + @Test + public void privateKeyOnlyForEnc() throws Exception { + privateKeyOnly(ImportedRsaEncKeyProviderFactory.ID, KeyUse.ENC, JWEConstants.RSA_OAEP); + } + + private void privateKeyOnly(String providerId, KeyUse keyUse, String algorithm) throws Exception { long priority = System.currentTimeMillis(); KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); String kid = KeyUtils.createKeyId(keyPair.getPublic()); - ComponentRepresentation rep = createRep("valid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("valid", providerId); rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate())); rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority)); @@ -91,7 +102,7 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata(); - assertEquals(kid, keys.getActive().get(Algorithm.RS256)); + assertEquals(kid, keys.getActive().get(algorithm)); KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0); @@ -101,17 +112,27 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { assertEquals(kid, key.getKid()); assertEquals(PemUtils.encodeKey(keyPair.getPublic()), keys.getKeys().get(0).getPublicKey()); assertEquals(keyPair.getPublic(), PemUtils.decodeCertificate(key.getCertificate()).getPublicKey()); + assertEquals(keyUse, keys.getKeys().get(0).getUse()); } @Test - public void keyAndCertificate() throws Exception { + public void keyAndCertificateForSig() throws Exception { + keyAndCertificate(ImportedRsaKeyProviderFactory.ID, KeyUse.SIG); + } + + @Test + public void keyAndCertificateForEnc() throws Exception { + keyAndCertificate(ImportedRsaEncKeyProviderFactory.ID, KeyUse.ENC); + } + + private void keyAndCertificate(String providerId, KeyUse keyUse) throws Exception { long priority = System.currentTimeMillis(); KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, "test"); String certificatePem = PemUtils.encodeCertificate(certificate); - ComponentRepresentation rep = createRep("valid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("valid", providerId); rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate())); rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, certificatePem); rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority)); @@ -128,13 +149,23 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0); assertEquals(certificatePem, key.getCertificate()); + assertEquals(keyUse, keys.getKeys().get(0).getUse()); } @Test - public void invalidPriority() throws Exception { + public void invalidPriorityForSig() throws Exception { + invalidPriority(ImportedRsaKeyProviderFactory.ID); + } + + @Test + public void invalidPriorityForEnc() throws Exception { + invalidPriority(ImportedRsaEncKeyProviderFactory.ID); + } + + private void invalidPriority(String providerId) throws Exception { KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); - ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("invalid", providerId); rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate())); rep.getConfig().putSingle(Attributes.PRIORITY_KEY, "invalid"); @@ -143,10 +174,19 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { } @Test - public void invalidEnabled() throws Exception { + public void invalidEnabledForSig() throws Exception { + invalidEnabled(ImportedRsaKeyProviderFactory.ID); + } + + @Test + public void invalidEnabledForEnc() throws Exception { + invalidEnabled(ImportedRsaEncKeyProviderFactory.ID); + } + + private void invalidEnabled(String providerId) throws Exception { KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); - ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("invalid", providerId); rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate())); rep.getConfig().putSingle(Attributes.ENABLED_KEY, "invalid"); @@ -155,10 +195,19 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { } @Test - public void invalidActive() throws Exception { + public void invalidActiveForSig() throws Exception { + invalidActive(ImportedRsaKeyProviderFactory.ID); + } + + @Test + public void invalidActiveForEnc() throws Exception { + invalidActive(ImportedRsaEncKeyProviderFactory.ID); + } + + private void invalidActive(String providerId) throws Exception { KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); - ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("invalid", providerId); rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate())); rep.getConfig().putSingle(Attributes.ACTIVE_KEY, "invalid"); @@ -167,10 +216,19 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { } @Test - public void invalidPrivateKey() throws Exception { + public void invalidPrivateKeyForSig() throws Exception { + invalidPrivateKey(ImportedRsaKeyProviderFactory.ID); + } + + @Test + public void invalidPrivateKeyForEnc() throws Exception { + invalidPrivateKey(ImportedRsaEncKeyProviderFactory.ID); + } + + private void invalidPrivateKey(String providerId) throws Exception { KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); - ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("invalid", providerId); Response response = adminClient.realm("test").components().add(rep); assertErrror(response, "'Private RSA Key' is required"); @@ -185,11 +243,20 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest { } @Test - public void invalidCertificate() throws Exception { + public void invalidCertificateForSig() throws Exception { + invalidCertificate(ImportedRsaKeyProviderFactory.ID); + } + + @Test + public void invalidCertificateForEnc() throws Exception { + invalidCertificate(ImportedRsaEncKeyProviderFactory.ID); + } + + private void invalidCertificate(String providerId) throws Exception { KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048); Certificate invalidCertificate = CertificateUtils.generateV1SelfSignedCertificate(KeyUtils.generateRsaKeyPair(2048), "test"); - ComponentRepresentation rep = createRep("invalid", ImportedRsaKeyProviderFactory.ID); + ComponentRepresentation rep = createRep("invalid", providerId); rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPrivate())); rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, "nonsense");