From ebc184bf94393b06a5efc3c00d3408686069e5d5 Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Wed, 11 May 2016 12:05:42 +0200 Subject: [PATCH] KEYCLOAK-2863 ClientAttributeCertificateResource --- .../ClientAttributeCertificateResource.java | 11 +- .../admin/client/CredentialsTest.java | 133 ++++++++++++++++++ 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java index 9f78aa8313..2675e19cfb 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientAttributeCertificateResource.java @@ -22,11 +22,8 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; import org.jboss.resteasy.annotations.cache.NoCache; -import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.idm.CertificateRepresentation; @@ -61,26 +58,26 @@ public interface ClientAttributeCertificateResource { /** * Upload certificate and eventually private key * - * @param input + * @param output * @return */ @POST @Path("upload") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) - public CertificateRepresentation uploadJks(MultipartFormDataOutput input); + public CertificateRepresentation uploadJks(MultipartFormDataOutput output); /** * Upload only certificate, not private key * - * @param input + * @param output * @return */ @POST @Path("upload-certificate") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) - public CertificateRepresentation uploadJksCertificate(MultipartFormDataOutput input); + public CertificateRepresentation uploadJksCertificate(MultipartFormDataOutput output); /** * Get a keystore file for the client, containing private key and public certificate diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java index 2b79e50af9..5db15e01ec 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java @@ -17,18 +17,34 @@ package org.keycloak.testsuite.admin.client; +import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ClientAttributeCertificateResource; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; +import javax.ws.rs.core.MediaType; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.Key; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * @@ -71,4 +87,121 @@ public class CredentialsTest extends AbstractClientTest { assertEquals(cert.getCertificate(), certFromGet.getCertificate()); assertEquals(cert.getPrivateKey(), certFromGet.getPrivateKey()); } + + @Test + public void testUploadKeyAndCertificate() throws Exception { + String privateKey = "MIIEowIBAAKCAQEAhSOOC/5Xez3o75lr3TTYun+2u0a4cF5p5Uv10UowrM7Yw+p1GYcHg+o2UN13bxHB/lefqJZ0WnJQo6cj/JcMuF1y4WlHSww0r8L0u36FKk8Uu7MOqC0+AOi2UzGIchYM5nuD3+A9g1ds2+O/ydKLKqiC6gJCKJp9b3Rs8eyJUt0/tkhTAJx+LWpCbsWHFEnU2Jbl29SS4KedYR/RdH5bNzl4L0SAHS1osWI+xIQiVYybnGVqFjJeQ9006pmOJGetNablji6TxlywP8ps9N//u3txBeKlVqzCCN1iLWQrb/NHA6GDVDBYVf+qa91358vFXRHpWpEOGftB6nZzHAzEuwIDAQABAoIBAHb5IsJM8lfLJxCVBPKTeuiNn/kSZVbkx7SDgJMZvQ1vefz40tOQ+oJDFW6FuWijcbubCa1ZZXg9lxnnDh11zYQi3bnYnkDOE3bMvG2fzdfU+y4QABUA+NtPGT6WkNuCIN0Fmv7AH7fys/B7QLNVVc807me2xPALvfOPEpvNR5mnjquCTOfDzbh5U6hGFcuLnZdQbCK2hG5R8DXE2pLvoa0i1cMMgVaWQ5mVSg0N3G0Q5ZF8YJEasAeJUCGlPFgJ4ySfGsKSUcMODQzHmqvLzArJmFgW6Uah0CgqedBTujmzJ6FwfbzGR0wpk08cf5BPzs9Flwka10ITA4h4QzlBnuECgYEA8TFZWq42biHaZmo0NVVEoltIDl1ci5m2xr7yU6TMfrsGKFFiszCPWuKcK5J8Svm0P9H9vlVpCHZ+JVfEGnve1/wVB/6E0lY6cz4uJTV4t4F1QJN5j9nyRrS0i9zDEIRgO4mvD9Zlm/OvHEdTmtVg97cbS4nWvRAPdB2DaZ0w0V8CgYEAjVAAb5Q6Jqb5XT5ZM1Cc6S3PzBAA7GGc3Rqyugxts5WEReRXdNITocj/71c0VZ+qC9+EvV8im/7QPl5NbRiI2p3oPqqV5Brk/MVfDLhu/mkawW0mlPtuBkZIRE0/eXTGN9Dq6yvxo9d6kwka7RW1CBZxi1/M78hKGCHXM7umviUCgYEA4cLvgJHRIQVPCM4gUEugEtieedOp7IHVM/NHoEOBpp4pBVQortGlXcz/oUlcTlGtBo/ok2AfEGzZZtrgFGoeDM1IYlM6wCc2TujFCM8kT6A9wFRKVPwMa2J6HPBnJe7CpPgbhReJxJA0OKQK/cL9IOGkCvDar914mZeGijU4nMECgYAqZL7Muo47fEpBE+xUvbFlLu4xDPgJ8jrKBjFqKUJb5tYY1aj7De7/0Toexm2X5l9wUm0TFtBeNjKpE0dtHDgqRccfzbNMDFl4D4o1WbtKraNuNd2mQku+rCUQAJCzUjoJEq73QGasvX8zTz75s1JtC7ailmn34YGA/d3+0iPy1QKBgHXneWpJVcQ9Lk34DnSLZLK+W1sTK8xLTJSyy3U0F84r+ir8bvsP9EQpZI0Nx3DqvF4/ZHmK2cfSxGSKm4VhZfG0LYCqtSmaHErZJaLJA8xJELkkEKj/ZUqkZ+4zhY7RMwyZtmXcxvaR/pzRZZwbTQ4ueZKKUIsK2AaHTsSCGDMq"; + String certificate = "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="; + String certificate2 = "MIICnTCCAYUCBgFPPQDGxTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE4NTAwNVoXDTI1MDgxNzE4NTE0NVowEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMw3PaBffWxgS2PYSDDBp6As+cNvv9kt2C4f/RDAGmvSIHPFev9kuQiKs3Oaws3ZsV4JG3qHEuYgnh9W4vfe3DwNwtD1bjL5FYBhPBFTw0lAQECYxaBHnkjHwUKp957FqdSPPICm3LjmTcEdlH+9dpp9xHCMbbiNiWDzWI1xSxC8Fs2d0hwz1sd+Q4QeTBPIBWcPM+ICZtNG5MN+ORfayu4X+Me5d0tXG2fQO//rAevk1i5IFjKZuOjTwyKB5SJIY4b8QTeg0g/50IU7Ht00Pxw6CK02dHS+FvXHasZlD3ckomqCDjStTBWdhJo5dST0CbOqalkkpLlCCbGA1yEQRsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUIMeJ+EAo8eNpCG/nXImacjrKakbFnZYBGD/gqeTGaZynkX+jgBSructTHR83zSH+yELEhsAy+3BfK4EEihp+PEcRnK2fASVkHste8AQ7rlzC+HGGirlwrVhWCdizNUCGK80DE537IZ7nmZw6LFG9P5/Q2MvCsOCYjRUvMkukq6TdXBXR9tETwZ+0gpSfsOxjj0ZF7ftTRUSzx4rFfcbM9fRNdVizdOuKGc8HJPA5lLOxV6CyaYIvi3y5RlQI1OHeS34lE4w9CNPRFa/vdxXvN7ClyzA0HMFNWxBN7pC/Ht/FbhSvaAagJBHg+vCrcY5C26Oli7lAglf/zZrwUPs0w=="; + + ClientAttributeCertificateResource certRsc = accountClient.getCertficateResource("jwt.credential"); + + // Upload privateKey and certificate as JKS store + MultipartFormDataOutput keyCertForm = new MultipartFormDataOutput(); + keyCertForm.addFormData("keystoreFormat", "JKS", MediaType.TEXT_PLAIN_TYPE); + keyCertForm.addFormData("keyAlias", "clientkey", MediaType.TEXT_PLAIN_TYPE); + keyCertForm.addFormData("keyPassword", "keypass", MediaType.TEXT_PLAIN_TYPE); + keyCertForm.addFormData("storePassword", "storepass", MediaType.TEXT_PLAIN_TYPE); + + URL idpMeta = getClass().getClassLoader().getResource("client-auth-test/keystore-client1.jks"); + byte [] content = Files.readAllBytes(Paths.get(idpMeta.toURI())); + keyCertForm.addFormData("file", content, MediaType.APPLICATION_OCTET_STREAM_TYPE); + CertificateRepresentation cert = certRsc.uploadJks(keyCertForm); + + // Returned cert is not the new state but rather what was extracted from inputs + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", certificate, cert.getCertificate()); + assertEquals("privateKey properly extracted", privateKey, cert.getPrivateKey()); + + // Get the certificate - to make sure cert was properly updated + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", certificate, cert.getCertificate()); + assertEquals("privateKey properly set", privateKey, cert.getPrivateKey()); + + // Upload a different certificate via /upload-certificate, privateKey should be nullified + MultipartFormDataOutput form = new MultipartFormDataOutput(); + form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); + form.addFormData("file", certificate2.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); + cert = certRsc.uploadJksCertificate(form); + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", certificate2, cert.getCertificate()); + assertNull("privateKey not included", cert.getPrivateKey()); + + // Get the certificate - to make sure cert was properly updated, and privateKey is null + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", certificate2, cert.getCertificate()); + // TODO: KEYCLOAK-2981 + //assertNull("privateKey nullified", cert.getPrivateKey()); + + // Re-upload the private key + certRsc.uploadJks(keyCertForm); + + // Upload certificate as PEM via /upload - nullifies the private key + form = new MultipartFormDataOutput(); + form.addFormData("keystoreFormat", "Certificate PEM", MediaType.TEXT_PLAIN_TYPE); + form.addFormData("file", certificate2.getBytes(Charset.forName("ASCII")), MediaType.APPLICATION_OCTET_STREAM_TYPE); + cert = certRsc.uploadJks(form); + assertNotNull("cert not null", cert); + assertEquals("cert properly extracted", certificate2, cert.getCertificate()); + assertNull("privateKey not included", cert.getPrivateKey()); + + // Get the certificate again - to make sure cert is set, and privateKey is null + cert = certRsc.getKeyInfo(); + assertEquals("cert properly set", certificate2, cert.getCertificate()); + assertNull("privateKey nullified", cert.getPrivateKey()); + } + + @Test + public void testDownloadKeystore() throws Exception { + ClientAttributeCertificateResource certRsc = accountClient.getCertficateResource("jwt.credential"); + + // generate a key pair first + CertificateRepresentation certrep = certRsc.generate(); + + // download the key and certificate + KeyStoreConfig config = new KeyStoreConfig(); + config.setFormat("JKS"); + config.setKeyAlias("alias"); + config.setKeyPassword("keyPass"); + config.setStorePassword("storePass"); + byte[] result = certRsc.getKeystore(config); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(new ByteArrayInputStream(result), "storePass".toCharArray()); + Key key = keyStore.getKey("alias", "keyPass".toCharArray()); + Certificate cert = keyStore.getCertificate("alias"); + + assertTrue("Certificat is X509", cert instanceof X509Certificate); + String keyPem = KeycloakModelUtils.getPemFromKey(key); + String certPem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) cert); + + assertEquals("key match", certrep.getPrivateKey(), keyPem); + assertEquals("cert match", certrep.getCertificate(), certPem); + } + + @Test + public void testGenerateAndDownloadKeystore() throws Exception { + ClientAttributeCertificateResource certRsc = accountClient.getCertficateResource("jwt.credential"); + + // generate a key pair first + CertificateRepresentation firstcert = certRsc.generate(); + + KeyStoreConfig config = new KeyStoreConfig(); + config.setFormat("JKS"); + config.setKeyAlias("alias"); + config.setKeyPassword("keyPass"); + config.setStorePassword("storePass"); + byte[] result = certRsc.generateAndGetKeystore(config); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(new ByteArrayInputStream(result), "storePass".toCharArray()); + Key key = keyStore.getKey("alias", "keyPass".toCharArray()); + Certificate cert = keyStore.getCertificate("alias"); + + assertTrue("Certificat is X509", cert instanceof X509Certificate); + String keyPem = KeycloakModelUtils.getPemFromKey(key); + String certPem = KeycloakModelUtils.getPemFromCertificate((X509Certificate) cert); + + assertNotEquals("new key generated", firstcert.getPrivateKey(), keyPem); + assertNotEquals("new cert generated", firstcert.getCertificate(), certPem); + } }