diff --git a/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java b/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java index 5000d64f61a..6a15c9b98fe 100755 --- a/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java @@ -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 getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException; public List getCRLDistributionPoints(X509Certificate cert) throws IOException; diff --git a/common/src/main/java/org/keycloak/common/util/CertificateUtils.java b/common/src/main/java/org/keycloak/common/util/CertificateUtils.java index 1a4f3585e18..30964729ea8 100755 --- a/common/src/main/java/org/keycloak/common/util/CertificateUtils.java +++ b/common/src/main/java/org/keycloak/common/util/CertificateUtils.java @@ -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); + } } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java index cddba3f9503..98ca18c9015 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java @@ -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, diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java index cfabff65c6c..273e0b205b0 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronCertificateUtilsProvider.java @@ -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()); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java index 03ed5b81100..5a1bd610bea 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java @@ -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, diff --git a/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc index d5ab41a474d..87f359dacd7 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc @@ -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. \ No newline at end of file diff --git a/js/apps/account-ui/test/realms/verifiable-credentials-realm.json b/js/apps/account-ui/test/realms/verifiable-credentials-realm.json index 4bbc072483e..ae6f2b03e1e 100644 --- a/js/apps/account-ui/test/realms/verifiable-credentials-realm.json +++ b/js/apps/account-ui/test/realms/verifiable-credentials-realm.json @@ -206,34 +206,6 @@ ] } } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "a4589e8f-7f82-4345-b2ea-ccc9d4366600", - "name": "test-key", - "providerId": "rsa", - "subComponents": {}, - "config": { - "privateKey": [ - "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA1M1vl2mW0ewMctKEoCYG6+SgV9TqN+4oIt2ZLkQb1O+yWGTW\nuu8h2U7yZ+Dc1JfSPUd45eU1p9j3rYu5Bs2Labc6zZUYyBMjZXopv/AqIOhvuTRR\ng7v4yRkC6QACniLndPCkanlp/8dL98Gmm8x+oOjYf1UFbGxjGqqVxfNVZmGi9NLE\n6AM0e4wmBVknwWTcC3TTHDxgAxHHa0GhL1y7OYsmw9Kz1riUWlr0Az3lBclOFACb\nOp/cGnyHnotErw1xKVQtGOv4GIsYQZr4jIeQkoFcqbAQVOk30NjTRNgVra2JzEpM\nvhbm4l+WHK2OfsPfBx6OKTOmet6zJnnNC608jQIDAQABAoIBAA+gyro28fGWwU9J\ncJ4GTOnUD4aDx3O2FNKCrbY9IEIiHFnrhe2SlEzORHUmiXE/eRww/Ir9q1QZVg8z\nvLHoNH3eC/5/HaPL7ASO1TQYYi+qglH6qqXfDyVNpe4QpyCP9amb5qc/JW64ZzbZ\nzO+SNBaDIysuxkgxKZISxw8TkMkE7jvh6RafPkIBldIhGunWQ33on08UJaTAeCz2\nw+3g7Ei8Ejg6sMZ+HGTJ9omA7xVOT+baLm9zwwQh1RcYV9MRywljAnJRs9iDwRzn\n2uWOpqFbmdTRDRuZmocP2ks9Wty5Ub3iyu7n1M9y5W8fxdmuZ4mXSYoG6jm/tJzo\nd8yRHYECgYEA/KP8MFyhWsH9F/eMDdq6oMG/Gk5VQcp6gadoHy2g710h37stTPRN\nnyitAo6enWoKNFH2jZBvGxrUY4n7+Zib51zhUf6L6YvP77yYn4txIwZYITzdr4X7\nivL4IH4qmoM5T2DRMD/bSN2c87v5IKpnuySL81C+irr1ZFSqDe5anKECgYEA16HW\nebVv87icGHDlkON21VCeTpnpofntjKSvEYfHRsyzldgkaurGSjIjojtdj0t2fHfx\nMt0VBHLENop1E0Up/jhroYjlBXFcIPwaBwSECoIzzoZApL/kJciijek3V6GWne0R\nIskVLTBxvVf4gz2bovRY4zgsi3cLs719KL8hDG0CgYEAsJohC910nXDFbx+IM5cW\nppFI+SaQynCzujY/vquyuCAuMasyO3z7VaqlZgg0MG2TvIcfBk5UnGng1cP687sO\nIGj4yMxbGWK2dCsttTlQWN9yc6mMfcn20GaPtIb9WQ0p3qcbE9NPglwH/wkDWSZF\nZLhjbC6hQ3D1YLEePqbDiIECgYBgwl5bfu8djll9HivlOCy6y9I9sxMDfAL8eWmV\nlDf3rSNoufSdhXw1DwquYbU598LTV38EM/CabmVdlAO1AfQ1/1tMwQED0DpnErkb\nLQuTK5nTsqqPQww9aCqJQ31x9TCA7UAjO9gkzvg63p7FRX/xP3QjgbF7Y4/8t6rR\n/fH2gQKBgAvBF7+OQWNWC4VPYGX6GMveqvN7/87qwGMltB5OQZWiODEuKYPLKmll\nsin1Xlek/Fe4zRSe+mWTs9pnrZ7VWIUUlBbW9V/tgRKcW7c6S4r69Gih5gToFE6Q\nEH4j0DoVcaH3nWOxwQqKrQL0tk2faSS0Y0O6wGDQiEuDvk4AGmGp\n-----END RSA PRIVATE KEY-----\n\n" - ], - "certificate": [ - "-----BEGIN CERTIFICATE-----\nMIIE/jCCA+agAwIBAgISA6pEpAokqYJAJqQPgnCnsHAsMA0GCSqGSIb3DQEBCwUA\nMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD\nEwJSMzAeFw0yMzEyMjQwNTQxMzFaFw0yNDAzMjMwNTQxMzBaMCMxITAfBgNVBAMT\nGGNvbnN1bWVyLmRvbWUuZml3YXJlLmRldjCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBANTNb5dpltHsDHLShKAmBuvkoFfU6jfuKCLdmS5EG9Tvslhk1rrv\nIdlO8mfg3NSX0j1HeOXlNafY962LuQbNi2m3Os2VGMgTI2V6Kb/wKiDob7k0UYO7\n+MkZAukAAp4i53TwpGp5af/HS/fBppvMfqDo2H9VBWxsYxqqlcXzVWZhovTSxOgD\nNHuMJgVZJ8Fk3At00xw8YAMRx2tBoS9cuzmLJsPSs9a4lFpa9AM95QXJThQAmzqf\n3Bp8h56LRK8NcSlULRjr+BiLGEGa+IyHkJKBXKmwEFTpN9DY00TYFa2ticxKTL4W\n5uJflhytjn7D3wcejikzpnresyZ5zQutPI0CAwEAAaOCAhswggIXMA4GA1UdDwEB\n/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/\nBAIwADAdBgNVHQ4EFgQUxOKFQ5oAbX4U5ixy4ofvajMkTdAwHwYDVR0jBBgwFoAU\nFC6zF7dYVsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzAB\nhhVodHRwOi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5p\nLmxlbmNyLm9yZy8wIwYDVR0RBBwwGoIYY29uc3VtZXIuZG9tZS5maXdhcmUuZGV2\nMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBBQYKKwYBBAHWeQIEAgSB9gSB8wDxAHcA\nSLDja9qmRzQP5WoC+p0w6xxSActW3SyB2bu/qznYhHMAAAGMmo6vgQAABAMASDBG\nAiEAzJ0YhzMGyKKrkD66BAJQkWOqQS4E32X9jYvVL/XjqR4CIQCtrHjnCE7LdIBh\nESY873ctjvd3izH/F+OeoLocfP/p8wB2AHb/iD8KtvuVUcJhzPWHujS0pM27Kdxo\nQgqf5mdMWjp0AAABjJqOsBQAAAQDAEcwRQIhALZ8CQK+/Rj8p0krq96y68KED2qN\n9VWEA/diHmc3BSPkAiBhmZRBIDYZ3+BwiYQXLmWB34Uc8RCvsEHBHLVsLWJtizAN\nBgkqhkiG9w0BAQsFAAOCAQEAn/2qNjtU0v1fQbTnFgrOzvCDnruhWSgqC7t9/vAv\n+mK5t/KEIwMfDAXiaNXofn8me5nXXsfGSxhqNfXpBBfzGA6MEM3Rfqd+D2ie5+oW\ntNY+5Tdoi/jdaww07ZiiFsFPPfPgHZ6LbU/jDP4J0VwwYt30+FWMkKecsXKOCt+V\nUB3tgo0PY3DQOsbmSt9rFAIv8LHa8mQ/ikF+sk07BP+CfAjPz/4Rg8AR8A9mtqKM\nlD3AvvMLxVga/KgEEaB4vGrcK1liBFZF6/RRwExeXn0nErcHYGiqeiyYD8sI07QC\nRv2bv0LYkmEKqB/RxgTWMkltTQUFjUD55Dy1/4UTXtM9yg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw\nWhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\nRW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP\nR5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx\nsxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm\nNHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg\nZ3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG\n/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC\nAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA\nFHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw\nAoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw\nOi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB\ngt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W\nPTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl\nikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz\nCkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm\nlJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4\navAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2\nyJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O\nyK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids\nhCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+\nHlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv\nMldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX\nnLRbwHOoq7hHwg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC\nov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL\nwYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D\nLtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK\n4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5\nbHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y\nsR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ\nXmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4\nFQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc\nSLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql\nPRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND\nTwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw\nSwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1\nc3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx\n+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB\nATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu\nb3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E\nU1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu\nMA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC\n5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW\n9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG\nWCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O\nhe8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC\nDfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5\n-----END CERTIFICATE-----\n\n" - ], - "active": [ - "true" - ], - "priority": [ - "0" - ], - "enabled": [ - "true" - ], - "algorithm": [ - "RS256" - ] - } - } ] } - } \ No newline at end of file + } diff --git a/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java index bbab1aa1be3..64e87ba798b 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/AbstractImportedRsaKeyProviderFactory.java @@ -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 Stian Thorgersen @@ -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); diff --git a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java index 2ec55fc2721..5c3fecf0a08 100644 --- a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProvider.java @@ -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 getKeysStream() { diff --git a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java index 7d2f911be55..65466ce0b5d 100644 --- a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java @@ -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 diff --git a/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java b/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java index e0516d543c1..ca16c80cba1 100644 --- a/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/ImportedRsaKeyProvider.java @@ -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 Stian Thorgersen @@ -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); - } - } diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java index ae687c34f00..0abccb6c2f8 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java @@ -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 loadCertificateChain(KeyStore.PrivateKeyEntry privateKeyEntry) throws GeneralSecurityException { - List 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 certificateChain, @@ -275,35 +264,6 @@ public class JavaKeystoreKeyProvider implements KeyProvider { return keyWrapper; } - /** - *

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. - * - *

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 certificates) throws GeneralSecurityException { - if (certificates == null || certificates.isEmpty()) { - return; - } - - Set 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 getKeysStream() { return Stream.of(key); diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java index 1accaf57ab3..71e67bbe3ff 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java @@ -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); } } + /** + *

Validates the certificate chain in the store entry if it exists.

+ * + * @param certificates + * @throws GeneralSecurityException + */ + private static void validateCertificateChain(List certificates) throws GeneralSecurityException { + if (certificates == null || certificates.isEmpty()) { + return; + } + + Set 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 algorithms = Stream.of( diff --git a/services/src/main/java/org/keycloak/keys/KeyNoteUtils.java b/services/src/main/java/org/keycloak/keys/KeyNoteUtils.java new file mode 100644 index 00000000000..76de745639a --- /dev/null +++ b/services/src/main/java/org/keycloak/keys/KeyNoteUtils.java @@ -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 name is the key itself. The second note is the date the + * certificate expires name.notAfter, 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); + } + } +} 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 c9ee1fd4bbc..d51814dddde 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,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; } - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java index c662ebf08e3..a74b0708544 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java @@ -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; }