Passivate imported keys if the associate certificate is expired

Closes #34973

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-11-20 14:55:04 +01:00 committed by Marek Posolda
parent 948760ae45
commit b0b247f1f1
16 changed files with 338 additions and 129 deletions

View File

@ -63,6 +63,8 @@ public interface CertificateUtilsProvider {
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber);
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate);
public List<String> getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException;
public List<String> getCRLDistributionPoints(X509Certificate cert) throws IOException;

View File

@ -21,6 +21,7 @@ import java.math.BigInteger;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.keycloak.common.crypto.CryptoIntegration;
@ -66,5 +67,9 @@ public class CertificateUtils {
public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber) {
return CryptoIntegration.getProvider().getCertificateUtils().generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber);
}
public static X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) {
return CryptoIntegration.getProvider().getCertificateUtils().generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, validityEndDate);
}
}

View File

@ -161,12 +161,16 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
}
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, 10);
return generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, calendar.getTime());
}
@Override
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) {
try {
X500Name subjectDN = new X500Name("CN=" + subject);
Date validityStartDate = new Date(System.currentTimeMillis() - 100000);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, 10);
Date validityEndDate = new Date(calendar.getTime().getTime());
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(caKeyPair.getPublic().getEncoded());
X509v1CertificateBuilder builder = new X509v1CertificateBuilder(subjectDN, serialNumber, validityStartDate,

View File

@ -166,6 +166,13 @@ public class ElytronCertificateUtilsProvider implements CertificateUtilsProvider
@Override
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject,
BigInteger serialNumber) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, 10);
return generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, calendar.getTime());
}
@Override
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) {
try {
X500Principal subjectdn = subjectToX500Principle(subject);
@ -173,9 +180,7 @@ public class ElytronCertificateUtilsProvider implements CertificateUtilsProvider
ZonedDateTime notBefore = ZonedDateTime.ofInstant(
(new Date(System.currentTimeMillis() - 100000)).toInstant(),
ZoneId.systemDefault());
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, 10);
Date validityEndDate = new Date(calendar.getTime().getTime());
ZonedDateTime notAfter = ZonedDateTime.ofInstant(validityEndDate.toInstant(),
ZoneId.systemDefault());

View File

@ -49,7 +49,6 @@ import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.common.crypto.CertificateUtilsProvider;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.JavaAlgorithm;
import java.io.ByteArrayInputStream;
@ -164,12 +163,16 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{
}
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, 10);
return generateV1SelfSignedCertificate(caKeyPair, subject, serialNumber, calendar.getTime());
}
@Override
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber, Date validityEndDate) {
try {
X500Name subjectDN = new X500Name("CN=" + subject);
Date validityStartDate = new Date(System.currentTimeMillis() - 100000);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, 10);
Date validityEndDate = new Date(calendar.getTime().getTime());
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(caKeyPair.getPublic().getEncoded());
X509v1CertificateBuilder builder = new X509v1CertificateBuilder(subjectDN, serialNumber, validityStartDate,

View File

@ -78,3 +78,9 @@ Previously, the three mappers (`Client Id`, `Client Host` and `Client IP Address
In this release, admin events might hold additional details about the context when the event is fired. When upgrading you should
expect the database schema being updated to add a new column `DETAILS_JSON` to the `ADMIN_EVENT_ENTITY` table.
= Imported key providers check and passivate keys with a expired cetificate
The key providers that allow to import externally generated keys (`rsa` and `java-keystore` factories) now check the validity of the associated certificate if present. Therefore a key with a certificate that is expired cannot be imported in {project_name} anymore. If the certificate expires at runtime, the key is converted into a passive key (enabled but not active). A passive key is not used for new tokens, but it is still valid for validating previous issued tokens.
The default `generated` key providers generate a certificate valid for 10 years (the types that have or can have an associated certificate). Because of the long validity and the recommendation to rotate keys frequently, the generated providers do not perform this check.

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,8 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -67,7 +68,7 @@ public abstract class AbstractImportedRsaKeyProviderFactory extends AbstractRsaK
}
if (model.contains(Attributes.CERTIFICATE_KEY)) {
Certificate certificate = null;
X509Certificate certificate = null;
try {
certificate = PemUtils.decodeCertificate(model.get(Attributes.CERTIFICATE_KEY));
} catch (Throwable t) {
@ -81,9 +82,15 @@ public abstract class AbstractImportedRsaKeyProviderFactory extends AbstractRsaK
if (!certificate.getPublicKey().equals(keyPair.getPublic())) {
throw new ComponentValidationException("Certificate does not match private key");
}
try {
certificate.checkValidity();
} catch (CertificateException e) {
throw new ComponentValidationException("Certificate is not valid", e);
}
} else {
try {
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName());
X509Certificate 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", t);

View File

@ -18,12 +18,15 @@
package org.keycloak.keys;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.*;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.models.RealmModel;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
@ -38,7 +41,7 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
private final ComponentModel model;
private final KeyWrapper key;
protected final KeyWrapper key;
private final String algorithm;
@ -57,7 +60,23 @@ public abstract class AbstractRsaKeyProvider implements KeyProvider {
}
}
protected abstract KeyWrapper loadKey(RealmModel realm, ComponentModel model);
public KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
String privateRsaKeyPem = model.getConfig().getFirst(Attributes.PRIVATE_KEY_KEY);
String certificatePem = model.getConfig().getFirst(Attributes.CERTIFICATE_KEY);
PrivateKey privateKey = PemUtils.decodePrivateKey(privateRsaKeyPem);
if (privateKey == null) {
throw new RuntimeException("Key not found on the server. Check key for " + ImportedRsaKeyProviderFactory.ID + " in realm " + realm.getName());
}
PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
KeyPair keyPair = new KeyPair(publicKey, privateKey);
X509Certificate certificate = PemUtils.decodeCertificate(certificatePem);
KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.name()).toUpperCase());
return createKeyWrapper(keyPair, certificate, keyUse);
}
@Override
public Stream<KeyWrapper> getKeysStream() {

View File

@ -43,7 +43,7 @@ public class GeneratedRsaKeyProviderFactory extends AbstractGeneratedRsaKeyProvi
// for backward compatibility : it allows "enc" key use for "rsa-generated" provider
model.put(Attributes.KEY_USE, KeyUse.SIG.name());
}
return new ImportedRsaKeyProvider(session.getContext().getRealm(), model);
return new AbstractRsaKeyProvider(session.getContext().getRealm(), model){};
}
@Override

View File

@ -17,17 +17,10 @@
package org.keycloak.keys;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.RealmModel;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -36,25 +29,8 @@ public class ImportedRsaKeyProvider extends AbstractRsaKeyProvider {
public ImportedRsaKeyProvider(RealmModel realm, ComponentModel model) {
super(realm, model);
// in imported key we check the notAfter of the certificate
KeyNoteUtils.attachKeyNotes(model, KeyWrapper.class.getName(), this.key);
}
@Override
public KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
String privateRsaKeyPem = model.getConfig().getFirst(Attributes.PRIVATE_KEY_KEY);
String certificatePem = model.getConfig().getFirst(Attributes.CERTIFICATE_KEY);
PrivateKey privateKey = PemUtils.decodePrivateKey(privateRsaKeyPem);
if (privateKey == null) {
throw new RuntimeException("Key not found on the server. Check key for " + ImportedRsaKeyProviderFactory.ID + " in realm " + realm.getName());
}
PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
KeyPair keyPair = new KeyPair(publicKey, privateKey);
X509Certificate certificate = PemUtils.decodeCertificate(certificatePem);
KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.name()).toUpperCase());
return createKeyWrapper(keyPair, certificate, keyUse);
}
}

View File

@ -42,23 +42,16 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.EdECPrivateKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.crypto.SecretKey;
@ -86,12 +79,12 @@ public class JavaKeystoreKeyProvider implements KeyProvider {
String defaultAlgorithmKey = KeyUse.ENC.name().equalsIgnoreCase(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());
} else {
key = loadKey(realm, model);
model.setNote(KeyWrapper.class.getName(), key);
KeyWrapper tmpKey = KeyNoteUtils.retrieveKeyFromNotes(model, KeyWrapper.class.getName());
if (tmpKey == null) {
tmpKey = loadKey(realm, model);
KeyNoteUtils.attachKeyNotes(model, KeyWrapper.class.getName(), tmpKey);
}
this.key = tmpKey;
}
protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
@ -221,15 +214,11 @@ public class JavaKeystoreKeyProvider implements KeyProvider {
}
private List<X509Certificate> loadCertificateChain(KeyStore.PrivateKeyEntry privateKeyEntry) throws GeneralSecurityException {
List<X509Certificate> chain = Optional.ofNullable(privateKeyEntry.getCertificateChain())
return Optional.ofNullable(privateKeyEntry.getCertificateChain())
.map(certificates -> Arrays.stream(certificates)
.map(X509Certificate.class::cast)
.collect(Collectors.toList()))
.orElseGet(Collections::emptyList);
validateCertificateChain(chain);
return chain;
}
private KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List<X509Certificate> certificateChain,
@ -275,35 +264,6 @@ public class JavaKeystoreKeyProvider implements KeyProvider {
return keyWrapper;
}
/**
* <p>Validates the giving certificate chain represented by {@code certificates}. If the list of certificates is empty
* or does not have at least 2 certificates (end-user certificate plus intermediary/root CAs) this method does nothing.
*
* <p>It should not be possible to import to keystores invalid chains though. So this is just an additional check
* that we can reuse later for other purposes when the cert chain is also provided manually, in PEM.
*
* @param certificates
*/
private void validateCertificateChain(List<X509Certificate> certificates) throws GeneralSecurityException {
if (certificates == null || certificates.isEmpty()) {
return;
}
Set<TrustAnchor> anchors = new HashSet<>();
// consider the last certificate in the chain as the most trusted cert
anchors.add(new TrustAnchor(certificates.get(certificates.size() - 1), null));
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
CertPath certPath = CertificateFactory.getInstance("X.509").generateCertPath(certificates);
CertPathValidator validator = CertPathValidator.getInstance(CertPathValidator.getDefaultType());
validator.validate(certPath, params);
}
@Override
public Stream<KeyWrapper> getKeysStream() {
return Stream.of(key);

View File

@ -24,14 +24,24 @@ import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.security.GeneralSecurityException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE;
@ -109,13 +119,43 @@ public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory {
.checkSingle(KEY_PASSWORD_PROPERTY, true);
try {
new JavaKeystoreKeyProvider(realm, model, session.vault()).loadKey(realm, model);
KeyWrapper key = new JavaKeystoreKeyProvider(realm, model, session.vault()).loadKey(realm, model);
validateCertificateChain(key.getCertificateChain());
} catch(GeneralSecurityException e) {
logger.error("Failed to load keys.", e);
throw new ComponentValidationException("Certificate error on server. " + e.getMessage(), e);
} catch (Throwable t) {
logger.error("Failed to load keys.", t);
throw new ComponentValidationException("Failed to load keys. " + t.getMessage(), t);
}
}
/**
* <p>Validates the certificate chain in the store entry if it exists.</p>
*
* @param certificates
* @throws GeneralSecurityException
*/
private static void validateCertificateChain(List<X509Certificate> certificates) throws GeneralSecurityException {
if (certificates == null || certificates.isEmpty()) {
return;
}
Set<TrustAnchor> anchors = new HashSet<>();
// consider the last certificate in the chain as the most trusted cert
anchors.add(new TrustAnchor(certificates.get(certificates.size() - 1), null));
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
CertPath certPath = CertificateFactory.getInstance("X.509").generateCertPath(certificates);
CertPathValidator validator = CertPathValidator.getInstance(CertPathValidator.getDefaultType());
validator.validate(certPath, params);
}
// merge the algorithms supported for RSA and EC keys and provide them as one configuration property
private static ProviderConfigProperty mergedAlgorithmProperties() {
List<String> algorithms = Stream.of(

View File

@ -0,0 +1,97 @@
/*
* Copyright 2024 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 java.security.cert.X509Certificate;
import java.util.Date;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyWrapper;
/**
*
* @author rmartinc
*/
public class KeyNoteUtils {
private static final Logger logger = Logger.getLogger(KeyNoteUtils.class);
private KeyNoteUtils() {
}
/**
* Creates two notes in the model to save the key in the cached model. The first
* note <em>name</em> is the key itself. The second note is the date the
* certificate expires <em>name.notAfter</em>, if there is a certificate
* defined in the key (second note can be missing).
*
* @param model The model component to attach the notes
* @param name The name of the note
* @param key The key to attach
*/
public static void attachKeyNotes(ComponentModel model, String name, KeyWrapper key) {
model.setNote(name, key);
Date notAfter = null;
if (key.getCertificateChain() != null && !key.getCertificateChain().isEmpty()) {
notAfter = key.getCertificateChain().stream().map(X509Certificate::getNotAfter).min(Date::compareTo).get();
}
if (key.getCertificate() != null) {
if (notAfter == null) {
notAfter = key.getCertificate().getNotAfter();
} else {
notAfter = notAfter.compareTo(key.getCertificate().getNotAfter()) < 0
? notAfter
: key.getCertificate().getNotAfter();
}
}
if (notAfter != null) {
model.setNote(name + ".notAfter", notAfter);
if (KeyStatus.ACTIVE.equals(key.getStatus())) {
checkNotAfter(model, key, notAfter);
}
}
}
/**
* Retrieves the key from the note in the model if available. The second key
* for expiration date is also checked to see if the certificate is expired.
* If expired the key is transformed into passive.
*
* @param model The model with the keys
* @param name The name of the key
* @return The attached key or null
*/
public static KeyWrapper retrieveKeyFromNotes(ComponentModel model, String name) {
KeyWrapper key = model.getNote(name);
if (key != null && KeyStatus.ACTIVE.equals(key.getStatus()) && model.hasNote(name + ".notAfter")) {
Date notAfter = model.getNote(name + ".notAfter");
checkNotAfter(model, key, notAfter);
}
return key;
}
private static void checkNotAfter(ComponentModel model, KeyWrapper key, Date notAfter) {
if (new Date(Time.currentTimeMillis()).compareTo(notAfter) > 0) {
logger.warnf("Certificate chain for kid '%s' (%s) is not valid anymore, disabling it (certificate expired on %s)",
key.getKid(), model.getName(), notAfter);
key.setStatus(KeyStatus.PASSIVE);
model.put(Attributes.ACTIVE_KEY, false);
}
}
}

View File

@ -25,6 +25,7 @@ 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.KeyStatus;
import org.keycloak.crypto.KeyUse;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.Attributes;
@ -40,11 +41,16 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.saml.AbstractSamlTest;
import jakarta.ws.rs.core.Response;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.cert.Certificate;
import java.util.List;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@ -169,7 +175,7 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.PRIORITY_KEY, "invalid");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "'Priority' should be a number");
assertError(response, "'Priority' should be a number");
}
@Test
@ -190,7 +196,7 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.ENABLED_KEY, "invalid");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "'Enabled' should be 'true' or 'false'");
assertError(response, "'Enabled' should be 'true' or 'false'");
}
@Test
@ -211,7 +217,7 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.ACTIVE_KEY, "invalid");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "'Active' should be 'true' or 'false'");
assertError(response, "'Active' should be 'true' or 'false'");
}
@Test
@ -230,15 +236,15 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
ComponentRepresentation rep = createRep("invalid", providerId);
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "'Private RSA Key' is required");
assertError(response, "'Private RSA Key' is required");
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, "nonsense");
response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Failed to decode private key");
assertError(response, "Failed to decode private key");
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, PemUtils.encodeKey(keyPair.getPublic()));
response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Failed to decode private key");
assertError(response, "Failed to decode private key");
}
@Test
@ -251,6 +257,54 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
invalidCertificate(ImportedRsaEncKeyProviderFactory.ID);
}
@Test
public void invalidExpiredCertificate() throws Exception {
ComponentRepresentation rep = createRep("invalid", ImportedRsaEncKeyProviderFactory.ID);
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, AbstractSamlTest.SAML_CLIENT_SALES_POST_SIG_EXPIRED_PRIVATE_KEY);
rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, AbstractSamlTest.SAML_CLIENT_SALES_POST_SIG_EXPIRED_CERTIFICATE);
Response response = adminClient.realm("test").components().add(rep);
assertError(response, "Certificate is not valid");
}
@Test
public void testExpiredCertificateInOneHour() {
long priority = System.currentTimeMillis();
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
keyPair, "test", new BigInteger("1"), Date.from(Instant.now().plus(1, ChronoUnit.HOURS)));
String certificatePem = PemUtils.encodeCertificate(certificate);
ComponentRepresentation rep = createRep("valid", ImportedRsaKeyProviderFactory.ID);
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));
String id;
try (Response response = adminClient.realm("test").components().add(rep)) {
id = ApiUtil.getCreatedId(response);
}
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst(Attributes.PRIVATE_KEY_KEY));
assertEquals(certificatePem, createdRep.getConfig().getFirst(Attributes.CERTIFICATE_KEY));
KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(certificatePem, key.getCertificate());
assertEquals(KeyUse.SIG, key.getUse());
assertEquals(KeyStatus.ACTIVE.name(), key.getStatus());
setTimeOffset(3610);
keys = adminClient.realm("test").keys().getKeyMetadata();
key = keys.getKeys().get(0);
assertEquals(KeyStatus.PASSIVE.name(), key.getStatus());
}
private void invalidCertificate(String providerId) throws Exception {
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
Certificate invalidCertificate = CertificateUtils.generateV1SelfSignedCertificate(KeyUtils.generateRsaKeyPair(2048), "test");
@ -260,15 +314,15 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, "nonsense");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Failed to decode certificate");
assertError(response, "Failed to decode certificate");
rep.getConfig().putSingle(Attributes.CERTIFICATE_KEY, PemUtils.encodeCertificate(invalidCertificate));
response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Certificate does not match private key");
assertError(response, "Certificate does not match private key");
}
protected void assertErrror(Response response, String error) {
protected void assertError(Response response, String error) {
if (!response.hasEntity()) {
fail("No error message set");
}
@ -287,6 +341,5 @@ public class ImportedRsaKeyProviderTest extends AbstractKeycloakTest {
rep.setConfig(new MultivaluedHashMap<>());
return rep;
}
}

View File

@ -25,10 +25,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.Attributes;
@ -46,10 +48,19 @@ import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.saml.AbstractSamlTest;
import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.KeystoreUtils;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.assertEquals;
@ -265,6 +276,42 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
assertError(response, "Invalid use enc for algorithm RS256.");
}
@Test
public void invalidKeystoreExpiredCertificate() throws Exception {
generateRSAExpiredCertificateStore(KeystoreUtils.getPreferredKeystoreType());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
Response response = adminClient.realm("test").components().add(rep);
assertError(response, "Certificate error on server.");
}
@Test
public void testExpiredCertificateInOneHour() throws Exception {
this.keyAlgorithm = Algorithm.RS256;
generateRSAExpiredInOneHourCertificateStore(KeystoreUtils.getPreferredKeystoreType());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
try (Response response = adminClient.realm("test").components().add(rep)) {
String id = ApiUtil.getCreatedId(response);
getCleanup().addComponentId(id);
}
KeysMetadataRepresentation keys = adminClient.realm("test").keys().getKeyMetadata();
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(AlgorithmType.RSA.name(), key.getType());
PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), KeyType.RSA);
PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), KeyType.RSA);
assertEquals(exp, got);
assertEquals(generatedKeystore.getCertificateInfo().getCertificate(), key.getCertificate());
assertEquals(KeyStatus.ACTIVE.name(), key.getStatus());
setTimeOffset(3610);
keys = adminClient.realm("test").keys().getKeyMetadata();
key = keys.getKeys().get(0);
assertEquals(KeyStatus.PASSIVE.name(), key.getStatus());
}
protected void assertError(Response response, String error) {
if (!response.hasEntity()) {
fail("No error message set");
@ -320,6 +367,19 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
}
}
private void generateRSAExpiredCertificateStore(KeystoreUtil.KeystoreFormat keystoreType) throws Exception {
PrivateKey privKey = PemUtils.decodePrivateKey(AbstractSamlTest.SAML_CLIENT_SALES_POST_SIG_EXPIRED_PRIVATE_KEY);
X509Certificate cert = PemUtils.decodeCertificate(AbstractSamlTest.SAML_CLIENT_SALES_POST_SIG_EXPIRED_CERTIFICATE);
this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password", privKey, cert);
}
private void generateRSAExpiredInOneHourCertificateStore(KeystoreUtil.KeystoreFormat keystoreType) throws Exception {
KeyPair keyPair = org.keycloak.common.util.KeyUtils.generateRsaKeyPair(2048);
Certificate cert = CertificateUtils.generateV1SelfSignedCertificate(
keyPair, "test", new BigInteger("1"), Date.from(Instant.now().plus(1, ChronoUnit.HOURS)));
this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "keyalias", "password", "password", keyPair.getPrivate(), cert);
}
private static boolean isFips() {
return AuthServerTestEnricher.AUTH_SERVER_FIPS_MODE != FipsMode.DISABLED;
}