diff --git a/jsign-core/src/test/java/net/jsign/jca/AzureKeyVaultSigningServiceTest.java b/jsign-core/src/test/java/net/jsign/jca/AzureKeyVaultSigningServiceTest.java new file mode 100644 index 00000000..227295ef --- /dev/null +++ b/jsign-core/src/test/java/net/jsign/jca/AzureKeyVaultSigningServiceTest.java @@ -0,0 +1,248 @@ +/** + * Copyright 2023 Emmanuel Bourg + * + * 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 net.jsign.jca; + +import java.io.FileInputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStoreException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import net.jsign.DigestAlgorithm; + +import static net.jadler.Jadler.*; +import static org.junit.Assert.*; + +public class AzureKeyVaultSigningServiceTest { + + @Before + public void setUp() { + initJadler().withDefaultResponseStatus(404); + } + + @After + public void tearDown() { + closeJadler(); + } + + @Test + public void testGetAliases() throws Exception { + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificates.json")); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + List aliases = service.aliases(); + + assertEquals("aliases", Arrays.asList("test1", "test2", "test3"), aliases); + } + + @Test + public void testGetAliasesError() { + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + try { + service.aliases(); + fail("Exception not thrown"); + } catch (KeyStoreException e) { + assertEquals("message", "Unable to retrieve Azure Key Vault certificate aliases", e.getMessage()); + } + } + + @Test + public void testGetCertificateChain() throws Exception { + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates/test1") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificate.json")); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + Certificate[] chain = service.getCertificateChain("test1"); + assertNotNull("chain", chain); + assertEquals("number of certificates", 1, chain.length); + assertEquals("subject name", "CN=Jsign Test Certificate", ((X509Certificate) chain[0]).getSubjectDN().getName()); + + // check if the certificate is cached + Certificate[] chain2 = service.getCertificateChain("test1"); + assertEquals("certificate", chain[0], chain2[0]); + } + + @Test + public void testGetCertificateChainError() throws Exception { + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + try { + service.getCertificateChain("test1"); + fail("Exception not thrown"); + } catch (KeyStoreException e) { + assertEquals("message", "Unable to retrieve Azure Key Vault certificate 'test1'", e.getMessage()); + } + } + + @Test + public void testGetPrivateKey() throws Exception { + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates/test1") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificate.json")); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + SigningServicePrivateKey privateKey = service.getPrivateKey("test1", null); + assertNotNull("privateKey", privateKey); + assertEquals("algorithm", "https://jsigntestkeyvault.vault.azure.net/keys/test1/38ca3e3560b94086ac604c5dd21aa055", privateKey.getId()); + assertEquals("algorithm", "RSA", privateKey.getAlgorithm()); + } + + @Test + public void testGetPrivateKeyError() { + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + try { + service.getPrivateKey("test1", null); + fail("Exception not thrown"); + } catch (UnrecoverableKeyException e) { + assertEquals("message", "Unable to fetch Azure Key Vault private key for the certificate 'test1'", e.getMessage()); + } + } + + @Test + public void testSign() throws Exception { + byte[] data = "0123456789ABCDEF0123456789ABCDEF".getBytes(); + byte[] digest = DigestAlgorithm.SHA256.getMessageDigest().digest(data); + + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates/test1") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificate.json")); + + onRequest() + .havingMethodEqualTo("POST") + .havingPathEqualTo("/keys/test1/38ca3e3560b94086ac604c5dd21aa055/sign") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .havingBodyEqualTo("{\"alg\":\"RS256\",\"value\":\"" + Base64.getEncoder().encodeToString(digest) + "\"}") + .respond() + .withStatus(200) + .withBody("{\"kid\":\"https://jsigntestkeyvault.vault.azure.net/keys/test1/38ca3e3560b94086ac604c5dd21aa055\",\"value\":\"" + Base64.getEncoder().encodeToString(new byte[32]) + "\"}"); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + SigningServicePrivateKey privateKey = service.getPrivateKey("test1", null); + String keyId = privateKey.getId().replace("https://jsigntestkeyvault.vault.azure.net", "http://localhost:" + port()); + privateKey = new SigningServicePrivateKey(keyId, privateKey.getAlgorithm()); + + byte[] signature = service.sign(privateKey, "SHA256withRSA", data); + assertNotNull("signature", signature); + assertArrayEquals("signature", new byte[32], signature); + } + + @Test + public void testSignWithRSNULL() throws Exception { + byte[] data = "0123456789ABCDEF0123456789ABCDEF".getBytes(); + + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates/test1") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificate.json")); + + onRequest() + .havingMethodEqualTo("POST") + .havingPathEqualTo("/keys/test1/38ca3e3560b94086ac604c5dd21aa055/sign") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .havingBodyEqualTo("{\"alg\":\"RSNULL\",\"value\":\"MCEwCQYFKw4DAhoFAAQUTYV9JAiwDD3RfwxP/PFbl/EEmGc=\"}") + .respond() + .withStatus(200) + .withBody("{\"kid\":\"https://jsigntestkeyvault.vault.azure.net/keys/test1/38ca3e3560b94086ac604c5dd21aa055\",\"value\":\"" + Base64.getEncoder().encodeToString(new byte[32]) + "\"}"); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + SigningServicePrivateKey privateKey = service.getPrivateKey("test1", null); + String keyId = privateKey.getId().replace("https://jsigntestkeyvault.vault.azure.net", "http://localhost:" + port()); + privateKey = new SigningServicePrivateKey(keyId, privateKey.getAlgorithm()); + + byte[] signature = service.sign(privateKey, "SHA1withRSA", data); + assertNotNull("signature", signature); + assertArrayEquals("signature", new byte[32], signature); + } + + @Test + public void testSignWithUnsupportedAlgorithm() throws Exception { + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates/test1") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificate.json")); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + SigningServicePrivateKey privateKey = service.getPrivateKey("test1", null); + + try { + service.sign(privateKey, "MD5withRSA", new byte[0]); + fail("Exception not thrown"); + } catch (InvalidAlgorithmParameterException e) { + assertEquals("message", "Unsupported signing algorithm: MD5withRSA", e.getMessage()); + } + } + + @Test(expected = GeneralSecurityException.class) + public void testSignError() throws Exception { + onRequest() + .havingMethodEqualTo("GET") + .havingPathEqualTo("/certificates/test1") + .havingQueryStringEqualTo("api-version=7.2") + .havingHeaderEqualTo("Authorization", "Bearer token") + .respond() + .withStatus(200) + .withBody(new FileInputStream("target/test-classes/services/azure-certificate.json")); + + SigningService service = new AzureKeyVaultSigningService("http://localhost:" + port(), "token"); + SigningServicePrivateKey privateKey = service.getPrivateKey("test1", null); + String keyId = privateKey.getId().replace("https://jsigntestkeyvault.vault.azure.net", "http://localhost:" + port()); + privateKey = new SigningServicePrivateKey(keyId, privateKey.getAlgorithm()); + + service.sign(privateKey, "SHA256withRSA", new byte[0]); + } +} diff --git a/jsign-core/src/test/resources/services/azure-certificate.json b/jsign-core/src/test/resources/services/azure-certificate.json new file mode 100644 index 00000000..268d7f10 --- /dev/null +++ b/jsign-core/src/test/resources/services/azure-certificate.json @@ -0,0 +1,67 @@ +{ + "id": "https://jsigntestkeyvault.vault.azure.net/certificates/test1/38ca3e3560b94086ac604c5dd21aa055", + "kid": "https://jsigntestkeyvault.vault.azure.net/keys/test1/38ca3e3560b94086ac604c5dd21aa055", + "sid": "https://jsigntestkeyvault.vault.azure.net/secrets/test1/38ca3e3560b94086ac604c5dd21aa055", + "x5t": "v0sWC5lKV5G4k4UdqPWBo1PhBqs", + "cer": "MIIDSDCCAjCgAwIBAgIQI+1K7AuPQaOHuA2AcfOx1zANBgkqhkiG9w0BAQsFADAhMR8wHQYDVQQDExZKc2lnbiBUZXN0IENlcnRpZmljYXRlMB4XDTIxMDYxMjA4MjcwOVoXDTIyMDYxMjA4MzcwOVowITEfMB0GA1UEAxMWSnNpZ24gVGVzdCBDZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK+hb3oSz3Dmpsua/hdchodcxPVoC0Edc3yyYeXPyU15eZJggxmKFSbtuhhuMSCnyy/U5IdkwGL1Itb9Z0JkNbCuz0Qoj2US3lO1zG6BWLYATQzLI0P8gcC77MRFTolkCs8Db4zF/fm7887XjlnBIDKxxSaSFxXKMCRnnYkS71IxrmhcEI8UO9jaP6c5aux61nPEqn/eO64WEYp5FkjHrsmKz9T98MijLorMSCKnClniGBpJDOMy2koLNuWjYhjU5dwBcP0EaydZWm8ithVxQNgxFQTeaNf4q4kwZggULSlaIst9zLlXz1DDQSPrTJNIHs3TataFehpVpaezb+b4bPECAwEAAaN8MHowDgYDVR0PAQH/BAQDAgWgMAkGA1UdEwQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB8GA1UdIwQYMBaAFPdrIYZz5JtpoVDDU9rGMSNHwjkEMB0GA1UdDgQWBBT3ayGGc+SbaaFQw1PaxjEjR8I5BDANBgkqhkiG9w0BAQsFAAOCAQEAHDS/9U5nYwnDDEZ5V0wSNnGSfO5MuOINeD/5qiQx7v4K+6mvx+tX9cKRaNsf7zHv6lEcuuwZgGS61dyl3RItKURrGQxpAk07oEEuNgj+8EcShfwZ7Flufk1DGaawYklBCY7JNvUctUqOGeZ56Vy94r42rleh2w9FPWcoA7ZypnYq1Z7aM6AWQwQ7AJuPxrp49ATBQFUTG17QrVh8EX8b3gG6D/IV3WhrFr9BX3DOcACdgKT82oLyTfFQOZRxM4hZzhLlBQWu5oBe1ZcGGhhz3GwIFSScuxxCJkA9FiiU31COaW/4Zmj86JM/sHzO9ntn9UKqpbB7bI964v8EKPo/Pg==", + "attributes": { + "enabled": true, + "nbf": 1623486429, + "exp": 1655023029, + "created": 1623487029, + "updated": 1623487029, + "recoveryLevel": "Recoverable+Purgeable", + "recoverableDays": 90 + }, + "policy": { + "id": "https://jsigntestkeyvault.vault.azure.net/certificates/test1/policy", + "key_props": { + "exportable": false, + "kty": "RSA", + "key_size": 2048, + "reuse_key": false + }, + "secret_props": { + "contentType": "application/x-pkcs12" + }, + "x509_props": { + "subject": "CN=Jsign Test Certificate", + "sans": { + "dns_names": [] + }, + "ekus": [ + "1.3.6.1.5.5.7.3.1", + "1.3.6.1.5.5.7.3.2" + ], + "key_usage": [ + "digitalSignature", + "keyEncipherment" + ], + "validity_months": 12, + "basic_constraints": { + "ca": false + } + }, + "lifetime_actions": [ + { + "trigger": { + "lifetime_percentage": 80 + }, + "action": { + "action_type": "AutoRenew" + } + } + ], + "issuer": { + "name": "Self" + }, + "attributes": { + "enabled": true, + "created": 1623486458, + "updated": 1623486458 + } + }, + "pending": { + "id": "https://jsigntestkeyvault.vault.azure.net/certificates/test1/pending" + } +} diff --git a/jsign-core/src/test/resources/services/azure-certificates.json b/jsign-core/src/test/resources/services/azure-certificates.json new file mode 100644 index 00000000..823a3e34 --- /dev/null +++ b/jsign-core/src/test/resources/services/azure-certificates.json @@ -0,0 +1,41 @@ +{ + "value": [ + { + "id": "https://jsigntestkeyvault.vault.azure.net/certificates/test1", + "x5t": "v0sWC5lKV5G4k4UdqPWBo1PhBqs", + "attributes": { + "enabled": true, + "nbf": 1623486429, + "exp": 1655023029, + "created": 1623487029, + "updated": 1623487029 + }, + "subject": "" + }, + { + "id": "https://jsigntestkeyvault.vault.azure.net/certificates/test2", + "x5t": "u9_e8Jr50AmAGj-4TBPcxLxeFfU", + "attributes": { + "enabled": true, + "nbf": 1623486175, + "exp": 1655022775, + "created": 1623486775, + "updated": 1623486775 + }, + "subject": "" + }, + { + "id": "https://jsigntestkeyvault.vault.azure.net/certificates/test3", + "x5t": "KDt72FfYbTV8Wi4ZPmGtUx5rrHg", + "attributes": { + "enabled": true, + "nbf": 1623486380, + "exp": 1655022980, + "created": 1623486981, + "updated": 1623486981 + }, + "subject": "" + } + ], + "nextLink": null +}