mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Passivate imported keys if the associate certificate is expired
Closes #34973 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
948760ae45
commit
b0b247f1f1
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
97
services/src/main/java/org/keycloak/keys/KeyNoteUtils.java
Normal file
97
services/src/main/java/org/keycloak/keys/KeyNoteUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user