diff --git a/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateGenerateWitDefaultKeyUsagesTest.java b/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateGenerateWitDefaultKeyUsagesTest.java index 3cf7ea13c..3264955ae 100644 --- a/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateGenerateWitDefaultKeyUsagesTest.java +++ b/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateGenerateWitDefaultKeyUsagesTest.java @@ -36,7 +36,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.bouncycastle.asn1.x509.KeyUsage.cRLSign; +import static org.bouncycastle.asn1.x509.KeyUsage.digitalSignature; import static org.bouncycastle.asn1.x509.KeyUsage.keyCertSign; +import static org.bouncycastle.asn1.x509.KeyUsage.keyEncipherment; +import static org.cloudfoundry.credhub.helpers.RequestHelper.getCertificateId; import static org.cloudfoundry.credhub.utils.AuthConstants.ALL_PERMISSIONS_TOKEN; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; @@ -176,4 +179,124 @@ public void certificateGeneration_shouldGenerateCorrectCertificate() throws Exce assertThat(generatedKeyUsageCert, nullValue()); } + @Test + public void certificateRegeneration_shouldMaintainDefaultKeyUsages() throws Exception { + // Generate CA certificate with default key usages + final MockHttpServletRequestBuilder caPost = post("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"name\" : \"test-ca\",\n" + + " \"type\" : \"certificate\",\n" + + " \"parameters\" : {\n" + + " \"common_name\" : \"test-ca\",\n" + + " \"is_ca\" : true,\n" + + " \"duration\" : 365\n" + + " }\n" + + "}"); + + final String caResult = mockMvc.perform(caPost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String caCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate caCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(caCert.getBytes(UTF_8))); + + final byte[] caKeyUsageDer = caCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat(caKeyUsageDer, notNullValue()); + assertThat(Arrays.copyOfRange(caKeyUsageDer, 5, caKeyUsageDer.length), + equalTo(new KeyUsage(keyCertSign | cRLSign).getBytes())); + + final String certificateId = getCertificateId(mockMvc, "test-ca"); + + final MockHttpServletRequestBuilder regeneratePost = post("/api/v1/certificates/" + certificateId + "/regenerate") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"key_length\" : \"3072\"\n" + + "}"); + + final String regenerateResult = mockMvc.perform(regeneratePost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject regenerateResultJson = new JSONObject(regenerateResult); + final String regeneratedCaCert = regenerateResultJson.getJSONObject("value").getString("certificate"); + + // Verify regenerated CA still has the default key usages + final X509Certificate regeneratedCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(regeneratedCaCert.getBytes(UTF_8))); + final byte[] regeneratedCaKeyUsageDer = regeneratedCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat(regeneratedCaKeyUsageDer, notNullValue()); + assertThat(Arrays.copyOfRange(regeneratedCaKeyUsageDer, 5, regeneratedCaKeyUsageDer.length), + equalTo(new KeyUsage(keyCertSign | cRLSign).getBytes())); + } + + @Test + public void certificateRegeneration_shouldRegenerateCaWithExistingKeyUsages() throws Exception { + final MockHttpServletRequestBuilder caPost = post("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"name\" : \"test-ca-with-key-usage\",\n" + + " \"type\" : \"certificate\",\n" + + " \"parameters\" : {\n" + + " \"common_name\" : \"test-ca-with-key-usage\",\n" + + " \"is_ca\" : true,\n" + + " \"duration\" : 365,\n" + + " \"key_usage\" : [\"digital_signature\", \"key_encipherment\"]\n" + + " }\n" + + "}"); + + final String caResult = mockMvc.perform(caPost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String caCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate caCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(caCert.getBytes(UTF_8))); + + final byte[] caKeyUsageDer = caCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat(caKeyUsageDer, notNullValue()); + assertThat(Arrays.copyOfRange(caKeyUsageDer, 5, caKeyUsageDer.length), + equalTo(new KeyUsage(digitalSignature | keyEncipherment).getBytes())); + + final String certificateId = getCertificateId(mockMvc, "test-ca-with-key-usage"); + + final MockHttpServletRequestBuilder regeneratePost = post("/api/v1/certificates/" + certificateId + "/regenerate") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"key_length\" : \"3072\"\n" + + "}"); + + final String regenerateResult = mockMvc.perform(regeneratePost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject regenerateResultJson = new JSONObject(regenerateResult); + final String regeneratedCaCert = regenerateResultJson.getJSONObject("value").getString("certificate"); + + final X509Certificate regeneratedCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(regeneratedCaCert.getBytes(UTF_8))); + final byte[] regeneratedCaKeyUsageDer = regeneratedCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat(regeneratedCaKeyUsageDer, notNullValue()); + assertThat(Arrays.copyOfRange(regeneratedCaKeyUsageDer, 5, regeneratedCaKeyUsageDer.length), + equalTo(new KeyUsage(digitalSignature | keyEncipherment).getBytes())); + } } diff --git a/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateRegenerateWithDefaultKeyUsagesTest.java b/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateRegenerateWithDefaultKeyUsagesTest.java new file mode 100644 index 000000000..9ac02a570 --- /dev/null +++ b/backends/credhub/src/test/java/org/cloudfoundry/credhub/integration/CertificateRegenerateWithDefaultKeyUsagesTest.java @@ -0,0 +1,430 @@ +package org.cloudfoundry.credhub.integration; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.cloudfoundry.credhub.CredhubTestApp; +import org.cloudfoundry.credhub.utils.BouncyCastleFipsConfigurer; +import org.cloudfoundry.credhub.utils.DatabaseProfileResolver; +import org.cloudfoundry.credhub.utils.TestConstants; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.bouncycastle.asn1.x509.KeyUsage.cRLSign; +import static org.bouncycastle.asn1.x509.KeyUsage.keyCertSign; +import static org.cloudfoundry.credhub.helpers.RequestHelper.getCertificateId; +import static org.cloudfoundry.credhub.utils.AuthConstants.ALL_PERMISSIONS_TOKEN; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for verifying default CA key usage behavior during certificate generation + * and regeneration. + * + * This test class contains two nested test classes that verify behavior with the feature + * flag both DISABLED and ENABLED: + * + * 1. CertificateGenerateWithDefaultKeyUsagesDisabledTest: Tests behavior when certificates.enable_default_ca_key_usages=false + * - CAs should NOT have default key usages + * - Regeneration should preserve the absence of key usages + * + * 2. CertificateGenerateWithDefaultKeyUsagesEnabledTest: Tests behavior when certificates.enable_default_ca_key_usages=true + * - CAs should have default key usages (keyCertSign | cRLSign) + * - Regeneration should maintain default key usages + * - Explicit key usages should be preserved + * + * Together, these nested classes simulate the real-world deployment scenario: + * Phase 1 (Disabled): Old deployment - CAs created without key usages + * Phase 2 (Enabled): New deployment - CAs get/maintain key usages on generation/regeneration + * + */ +public final class CertificateRegenerateWithDefaultKeyUsagesTest { + + private CertificateRegenerateWithDefaultKeyUsagesTest() { + // Utility class - private constructor to prevent instantiation + } + + @BeforeClass + public static void beforeAll() { + BouncyCastleFipsConfigurer.configure(); + } + + @RunWith(SpringRunner.class) + @ActiveProfiles( + value = { + "unit-test", + "unit-test-permissions", + }, + resolver = DatabaseProfileResolver.class + ) + @SpringBootTest(classes = CredhubTestApp.class) + @TestPropertySource(properties = "certificates.enable_default_ca_key_usages=false") + @Transactional + public static class CertificateGenerateWithDefaultKeyUsagesDisabledTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + private MockMvc mockMvc; + + @Rule + public Timeout globalTimeout = Timeout.seconds(60); + + @Before + public void beforeEach() throws Exception { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void certificateRegeneration_shouldNotApplyDefaultKeyUsages() throws Exception { + final MockHttpServletRequestBuilder caPost = post("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"name\" : \"test-ca-no-key-usage\",\n" + + " \"type\" : \"certificate\",\n" + + " \"parameters\" : {\n" + + " \"common_name\" : \"test-ca-regenerate\",\n" + + " \"is_ca\" : true,\n" + + " \"duration\" : 365\n" + + " }\n" + + "}"); + + final String caResult = mockMvc.perform(caPost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String originalCaCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate originalCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(originalCaCert.getBytes(UTF_8))); + + final byte[] originalCaKeyUsageDer = originalCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Original CA should not have key usage extension when feature is disabled", + originalCaKeyUsageDer, nullValue()); + + final String certificateId = getCertificateId(mockMvc, "test-ca-no-key-usage"); + + final MockHttpServletRequestBuilder regeneratePost = post("/api/v1/certificates/" + certificateId + "/regenerate") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"key_length\": 3072\n" + + "}"); + + final String regenerateResult = mockMvc.perform(regeneratePost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject regenerateResultJson = new JSONObject(regenerateResult); + final String regeneratedCaCert = regenerateResultJson.getJSONObject("value").getString("certificate"); + + final X509Certificate regeneratedCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(regeneratedCaCert.getBytes(UTF_8))); + + final byte[] regeneratedCaKeyUsageDer = regeneratedCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Regenerated CA should still not have key usage extension when feature remains disabled", + regeneratedCaKeyUsageDer, nullValue()); + } + } + + @RunWith(SpringRunner.class) + @ActiveProfiles( + value = { + "unit-test", + "unit-test-permissions", + }, + resolver = DatabaseProfileResolver.class + ) + @SpringBootTest(classes = CredhubTestApp.class) + @TestPropertySource(properties = "certificates.enable_default_ca_key_usages=true") + @Transactional + public static class CertificateGenerateWithDefaultKeyUsagesEnabledTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + private MockMvc mockMvc; + + @Rule + public Timeout globalTimeout = Timeout.seconds(60); + + @Before + public void beforeEach() throws Exception { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .build(); + } + + @Test + public void certificateGeneration_shouldApplyDefaultKeyUsages() throws Exception { + final MockHttpServletRequestBuilder caPost = post("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"name\" : \"test-ca-with-default-key-usage\",\n" + + " \"type\" : \"certificate\",\n" + + " \"parameters\" : {\n" + + " \"common_name\" : \"test-ca-with-key-usage\",\n" + + " \"is_ca\" : true,\n" + + " \"duration\" : 365\n" + + " }\n" + + "}"); + + final String caResult = mockMvc.perform(caPost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String caCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate caCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(caCert.getBytes(UTF_8))); + + final byte[] caKeyUsageDer = caCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("CA should have key usage extension when feature is enabled", + caKeyUsageDer, notNullValue()); + assertThat("CA should have default CA key usages (keyCertSign | cRLSign)", + Arrays.copyOfRange(caKeyUsageDer, 5, caKeyUsageDer.length), + equalTo(new KeyUsage(keyCertSign | cRLSign).getBytes())); + } + + @Test + public void certificateRegeneration_shouldMaintainDefaultKeyUsages() throws Exception { + // Generate a CA with default key usages + final MockHttpServletRequestBuilder caPost = post("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"name\" : \"test-ca-regenerate-with-key-usage\",\n" + + " \"type\" : \"certificate\",\n" + + " \"parameters\" : {\n" + + " \"common_name\" : \"test-ca-regenerate-with-key-usage\",\n" + + " \"is_ca\" : true,\n" + + " \"duration\" : 365\n" + + " }\n" + + "}"); + + final String caResult = mockMvc.perform(caPost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String originalCaCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate originalCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(originalCaCert.getBytes(UTF_8))); + + final byte[] originalCaKeyUsageDer = originalCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Original CA should have key usage extension when feature is enabled", + originalCaKeyUsageDer, notNullValue()); + + // Regenerate the CA + final String certificateId = getCertificateId(mockMvc, "test-ca-regenerate-with-key-usage"); + + final MockHttpServletRequestBuilder regeneratePost = post("/api/v1/certificates/" + certificateId + "/regenerate") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"key_length\": 3072\n" + + "}"); + + final String regenerateResult = mockMvc.perform(regeneratePost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject regenerateResultJson = new JSONObject(regenerateResult); + final String regeneratedCaCert = regenerateResultJson.getJSONObject("value").getString("certificate"); + + final X509Certificate regeneratedCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(regeneratedCaCert.getBytes(UTF_8))); + + final byte[] regeneratedCaKeyUsageDer = regeneratedCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Regenerated CA should maintain key usage extension", + regeneratedCaKeyUsageDer, notNullValue()); + assertThat("Regenerated CA should maintain default CA key usages (keyCertSign | cRLSign)", + Arrays.copyOfRange(regeneratedCaKeyUsageDer, 5, regeneratedCaKeyUsageDer.length), + equalTo(new KeyUsage(keyCertSign | cRLSign).getBytes())); + } + + @Test + public void certificateRegeneration_shouldApplyDefaultKeyUsagesToCaCreatedWithoutThem() throws Exception { + + final String setJson = new ObjectMapper().writeValueAsString( + ImmutableMap.builder() + .put("certificate", TestConstants.TEST_CA) + .build()); + + final MockHttpServletRequestBuilder certificateSetRequest = put("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + //language=JSON + .content("{\n" + + " \"name\" : \"test-ca-without-key-usages\",\n" + + " \"type\" : \"certificate\",\n" + + " \"value\" : " + setJson + "}"); + + final String caResult = mockMvc.perform(certificateSetRequest) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String originalCaCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate originalCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(originalCaCert.getBytes(UTF_8))); + + final byte[] originalCaKeyUsageDer = originalCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Original CA (from TEST_CA constant) should not have key usage extension", + originalCaKeyUsageDer, nullValue()); + + final String certificateId = getCertificateId(mockMvc, "test-ca-without-key-usages"); + + final MockHttpServletRequestBuilder regeneratePost = post("/api/v1/certificates/" + certificateId + "/regenerate") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"key_length\": 3072\n" + + "}"); + + final String regenerateResult = mockMvc.perform(regeneratePost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject regenerateResultJson = new JSONObject(regenerateResult); + final String regeneratedCaCert = regenerateResultJson.getJSONObject("value").getString("certificate"); + + final X509Certificate regeneratedCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(regeneratedCaCert.getBytes(UTF_8))); + + final byte[] regeneratedCaKeyUsageDer = regeneratedCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Regenerated CA should now have key usage extension (feature applies default key usages)", + regeneratedCaKeyUsageDer, notNullValue()); + assertThat("Regenerated CA should have default CA key usages (keyCertSign | cRLSign)", + Arrays.copyOfRange(regeneratedCaKeyUsageDer, 5, regeneratedCaKeyUsageDer.length), + equalTo(new KeyUsage(keyCertSign | cRLSign).getBytes())); + } + + @Test + public void certificateRegeneration_withExplicitKeyUsages_shouldPreserveThem() throws Exception { + // Generate a CA with explicit non-default key usages + final MockHttpServletRequestBuilder caPost = post("/api/v1/data") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"name\" : \"test-ca-explicit-key-usage\",\n" + + " \"type\" : \"certificate\",\n" + + " \"parameters\" : {\n" + + " \"common_name\" : \"test-ca-explicit\",\n" + + " \"is_ca\" : true,\n" + + " \"duration\" : 365,\n" + + " \"key_usage\" : [\"digital_signature\", \"key_cert_sign\"]\n" + + " }\n" + + "}"); + + final String caResult = mockMvc.perform(caPost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject caResultJson = new JSONObject(caResult); + final String originalCaCert = caResultJson.getJSONObject("value").getString("certificate"); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + final X509Certificate originalCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(originalCaCert.getBytes(UTF_8))); + + final byte[] originalCaKeyUsageDer = originalCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Original CA should have explicit key usage extension", + originalCaKeyUsageDer, notNullValue()); + + final byte[] originalKeyUsageBytes = Arrays.copyOfRange(originalCaKeyUsageDer, 5, originalCaKeyUsageDer.length); + + // Regenerate the CA + final String certificateId = getCertificateId(mockMvc, "test-ca-explicit-key-usage"); + + final MockHttpServletRequestBuilder regeneratePost = post("/api/v1/certificates/" + certificateId + "/regenerate") + .header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN) + .accept(APPLICATION_JSON) + .contentType(APPLICATION_JSON) + .content("{\n" + + " \"key_length\": 3072\n" + + "}"); + + final String regenerateResult = mockMvc.perform(regeneratePost) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JSONObject regenerateResultJson = new JSONObject(regenerateResult); + final String regeneratedCaCert = regenerateResultJson.getJSONObject("value").getString("certificate"); + + final X509Certificate regeneratedCaCertPem = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(regeneratedCaCert.getBytes(UTF_8))); + + final byte[] regeneratedCaKeyUsageDer = regeneratedCaCertPem.getExtensionValue(Extension.keyUsage.getId()); + assertThat("Regenerated CA should preserve explicit key usage extension", + regeneratedCaKeyUsageDer, notNullValue()); + + final byte[] regeneratedKeyUsageBytes = Arrays.copyOfRange(regeneratedCaKeyUsageDer, 5, regeneratedCaKeyUsageDer.length); + assertThat("Regenerated CA should preserve the original explicit key usages", + regeneratedKeyUsageBytes, equalTo(originalKeyUsageBytes)); + } + } +} + diff --git a/components/credentials/src/main/java/org/cloudfoundry/credhub/service/regeneratables/CertificateCredentialRegeneratable.java b/components/credentials/src/main/java/org/cloudfoundry/credhub/service/regeneratables/CertificateCredentialRegeneratable.java index 76f1bba74..4662e3da8 100644 --- a/components/credentials/src/main/java/org/cloudfoundry/credhub/service/regeneratables/CertificateCredentialRegeneratable.java +++ b/components/credentials/src/main/java/org/cloudfoundry/credhub/service/regeneratables/CertificateCredentialRegeneratable.java @@ -1,5 +1,9 @@ package org.cloudfoundry.credhub.service.regeneratables; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import org.bouncycastle.asn1.x509.KeyUsage; import org.cloudfoundry.credhub.ErrorMessages; import org.cloudfoundry.credhub.domain.CertificateCredentialVersion; import org.cloudfoundry.credhub.domain.CertificateGenerationParameters; @@ -11,8 +15,17 @@ import static org.apache.commons.lang3.StringUtils.isEmpty; +@Component public class CertificateCredentialRegeneratable implements Regeneratable { + private final boolean defaultCAKeyUsages; + + public CertificateCredentialRegeneratable( + @Value("${certificates.enable_default_ca_key_usages:false}") boolean defaultCAKeyUsages + ) { + this.defaultCAKeyUsages = defaultCAKeyUsages; + } + @Override public BaseCredentialGenerateRequest createGenerateRequest(final CredentialVersion credentialVersion) { final CertificateCredentialVersion certificateCredential = (CertificateCredentialVersion) credentialVersion; @@ -20,13 +33,18 @@ public BaseCredentialGenerateRequest createGenerateRequest(final CredentialVersi if (isEmpty(certificateCredential.getCaName()) && !reader.isSelfSigned()) { throw new ParameterizedValidationException( - ErrorMessages.CANNOT_REGENERATE_NON_GENERATED_CERTIFICATE); + ErrorMessages.CANNOT_REGENERATE_NON_GENERATED_CERTIFICATE); } - final CertificateGenerationParameters certificateGenerationParameters = new CertificateGenerationParameters( - reader, - certificateCredential.getCaName() - ); + final CertificateGenerationParameters certificateGenerationParameters = + new CertificateGenerationParameters(reader, certificateCredential.getCaName()); + + if (defaultCAKeyUsages && certificateCredential.isCertificateAuthority()) { + if (reader.getKeyUsage() == null) { + certificateGenerationParameters.setKeyUsage( + new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); + } + } final CertificateGenerateRequest generateRequest = new CertificateGenerateRequest(); generateRequest.setName(certificateCredential.getName()); diff --git a/components/credentials/src/main/java/org/cloudfoundry/credhub/utils/CertificateReader.java b/components/credentials/src/main/java/org/cloudfoundry/credhub/utils/CertificateReader.java index b223debdc..5b9c295be 100644 --- a/components/credentials/src/main/java/org/cloudfoundry/credhub/utils/CertificateReader.java +++ b/components/credentials/src/main/java/org/cloudfoundry/credhub/utils/CertificateReader.java @@ -11,14 +11,20 @@ import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; @@ -26,6 +32,8 @@ import org.cloudfoundry.credhub.exceptions.MalformedCertificateException; import org.cloudfoundry.credhub.exceptions.MissingCertificateException; import org.cloudfoundry.credhub.exceptions.UnreadableCertificateException; +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters; +import org.jetbrains.annotations.Nullable; import static java.lang.Math.toIntExact; import static java.nio.charset.StandardCharsets.UTF_8; @@ -150,4 +158,104 @@ private X509Certificate parseStringIntoCertificate(final String pemString) throw .getInstance("X.509", BouncyCastleFipsProvider.PROVIDER_NAME) .generateCertificate(new ByteArrayInputStream(pemString.getBytes(UTF_8))); } + + public @Nullable String getCommonName() { + return getX500NameAttribute(BCStyle.CN); + } + + public @Nullable String getOrganization() { + return getX500NameAttribute(BCStyle.O); + } + + public @Nullable String getOrganizationUnit() { + return getX500NameAttribute(BCStyle.OU); + } + + public @Nullable String getLocality() { + return getX500NameAttribute(BCStyle.L); + } + + public @Nullable String getState() { + return getX500NameAttribute(BCStyle.ST); + } + + public @Nullable String getCountry() { + return getX500NameAttribute(BCStyle.C); + } + + public @Nullable String[] getExtendedKeyUsageStrings() { + final ExtendedKeyUsage extendedKeyUsage = getExtendedKeyUsage(); + if (extendedKeyUsage == null) { + return null; + } + + final KeyPurposeId[] keyPurposeIds = extendedKeyUsage.getUsages(); + final List extendedKeyUsageList = new ArrayList<>(); + + for (final KeyPurposeId keyPurposeId : keyPurposeIds) { + if (keyPurposeId.equals(KeyPurposeId.id_kp_serverAuth)) { + extendedKeyUsageList.add(CertificateGenerationRequestParameters.SERVER_AUTH); + } else if (keyPurposeId.equals(KeyPurposeId.id_kp_clientAuth)) { + extendedKeyUsageList.add(CertificateGenerationRequestParameters.CLIENT_AUTH); + } else if (keyPurposeId.equals(KeyPurposeId.id_kp_codeSigning)) { + extendedKeyUsageList.add(CertificateGenerationRequestParameters.CODE_SIGNING); + } else if (keyPurposeId.equals(KeyPurposeId.id_kp_emailProtection)) { + extendedKeyUsageList.add(CertificateGenerationRequestParameters.EMAIL_PROTECTION); + } else if (keyPurposeId.equals(KeyPurposeId.id_kp_timeStamping)) { + extendedKeyUsageList.add(CertificateGenerationRequestParameters.TIMESTAMPING); + } + } + + return extendedKeyUsageList.toArray(new String[0]); + } + + public @Nullable String[] getKeyUsageStrings() { + final KeyUsage keyUsage = getKeyUsage(); + if (keyUsage == null) { + return null; + } + + final List keyUsageList = new ArrayList<>(); + + if (keyUsage.hasUsages(KeyUsage.digitalSignature)) { + keyUsageList.add(CertificateGenerationRequestParameters.DIGITAL_SIGNATURE); + } + if (keyUsage.hasUsages(KeyUsage.nonRepudiation)) { + keyUsageList.add(CertificateGenerationRequestParameters.NON_REPUDIATION); + } + if (keyUsage.hasUsages(KeyUsage.keyEncipherment)) { + keyUsageList.add(CertificateGenerationRequestParameters.KEY_ENCIPHERMENT); + } + if (keyUsage.hasUsages(KeyUsage.dataEncipherment)) { + keyUsageList.add(CertificateGenerationRequestParameters.DATA_ENCIPHERMENT); + } + if (keyUsage.hasUsages(KeyUsage.keyAgreement)) { + keyUsageList.add(CertificateGenerationRequestParameters.KEY_AGREEMENT); + } + if (keyUsage.hasUsages(KeyUsage.keyCertSign)) { + keyUsageList.add(CertificateGenerationRequestParameters.KEY_CERT_SIGN); + } + if (keyUsage.hasUsages(KeyUsage.cRLSign)) { + keyUsageList.add(CertificateGenerationRequestParameters.CRL_SIGN); + } + if (keyUsage.hasUsages(KeyUsage.encipherOnly)) { + keyUsageList.add(CertificateGenerationRequestParameters.ENCIPHER_ONLY); + } + if (keyUsage.hasUsages(KeyUsage.decipherOnly)) { + keyUsageList.add(CertificateGenerationRequestParameters.DECIPHER_ONLY); + } + + return keyUsageList.toArray(new String[0]); + } + + private @Nullable String getX500NameAttribute(final org.bouncycastle.asn1.ASN1ObjectIdentifier attribute) { + final X500Name x500Name = new X500Name(certificate.getSubjectX500Principal().getName()); + final RDN[] rdns = x500Name.getRDNs(attribute); + + if (rdns.length == 0) { + return null; + } + + return rdns[0].getFirst().getValue().toString(); + } } diff --git a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/domain/CertificateGenerationParameters.kt b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/domain/CertificateGenerationParameters.kt index 45274920e..b7432fa7d 100644 --- a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/domain/CertificateGenerationParameters.kt +++ b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/domain/CertificateGenerationParameters.kt @@ -17,20 +17,12 @@ import org.cloudfoundry.credhub.exceptions.ParameterizedValidationException import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CLIENT_AUTH import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CODE_SIGNING -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CRL_SIGN -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DATA_ENCIPHERMENT -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DECIPHER_ONLY -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DIGITAL_SIGNATURE import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.EMAIL_PROTECTION -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.ENCIPHER_ONLY -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_AGREEMENT -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_CERT_SIGN -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_ENCIPHERMENT -import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.NON_REPUDIATION import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.SERVER_AUTH import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.TIMESTAMPING import org.cloudfoundry.credhub.requests.GenerationParameters import org.cloudfoundry.credhub.utils.CertificateReader +import org.cloudfoundry.credhub.utils.KeyUsageMapper import org.springframework.util.StringUtils import java.util.Objects import javax.security.auth.x500.X500Principal @@ -48,7 +40,7 @@ class CertificateGenerationParameters : GenerationParameters { val extendedKeyUsage: ExtendedKeyUsage? - val keyUsage: KeyUsage? + var keyUsage: KeyUsage? var allowTransitionalParentToSign: Boolean = false @@ -115,22 +107,7 @@ class CertificateGenerationParameters : GenerationParameters { if (keyUsageList.keyUsage == null) { return null } - var bitmask = 0 - for (keyUsage in keyUsageList.keyUsage!!) { - when (keyUsage) { - DIGITAL_SIGNATURE -> bitmask = bitmask or KeyUsage.digitalSignature - NON_REPUDIATION -> bitmask = bitmask or KeyUsage.nonRepudiation - KEY_ENCIPHERMENT -> bitmask = bitmask or KeyUsage.keyEncipherment - DATA_ENCIPHERMENT -> bitmask = bitmask or KeyUsage.dataEncipherment - KEY_AGREEMENT -> bitmask = bitmask or KeyUsage.keyAgreement - KEY_CERT_SIGN -> bitmask = bitmask or KeyUsage.keyCertSign - CRL_SIGN -> bitmask = bitmask or KeyUsage.cRLSign - ENCIPHER_ONLY -> bitmask = bitmask or KeyUsage.encipherOnly - DECIPHER_ONLY -> bitmask = bitmask or KeyUsage.decipherOnly - else -> throw ParameterizedValidationException(ErrorMessages.INVALID_KEY_USAGE, keyUsage) - } - } - return KeyUsage(bitmask) + return KeyUsageMapper.mapKeyUsage(keyUsageList.keyUsage!!) } private fun buildDn(params: CertificateGenerationRequestParameters): X500Principal { diff --git a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/generate/GenerationRequestGenerator.kt b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/generate/GenerationRequestGenerator.kt index 23eefbab9..33e675c26 100644 --- a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/generate/GenerationRequestGenerator.kt +++ b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/generate/GenerationRequestGenerator.kt @@ -12,11 +12,21 @@ import org.cloudfoundry.credhub.service.regeneratables.RsaCredentialRegeneratabl import org.cloudfoundry.credhub.service.regeneratables.SshCredentialRegeneratable import org.cloudfoundry.credhub.service.regeneratables.UserCredentialRegeneratable import org.springframework.stereotype.Component -import java.util.function.Supplier @Component -class GenerationRequestGenerator { - private val regeneratableTypeProducers: MutableMap> +class GenerationRequestGenerator( + private val certificateCredentialRegeneratable: CertificateCredentialRegeneratable, +) { + private val regeneratableTypeProducers: MutableMap Regeneratable> + + init { + regeneratableTypeProducers = HashMap() + regeneratableTypeProducers["password"] = { PasswordCredentialRegeneratable() } + regeneratableTypeProducers["user"] = { UserCredentialRegeneratable() } + regeneratableTypeProducers["ssh"] = { SshCredentialRegeneratable() } + regeneratableTypeProducers["rsa"] = { RsaCredentialRegeneratable() } + regeneratableTypeProducers["certificate"] = { certificateCredentialRegeneratable } // Use injected instance + } fun createGenerateRequest(credentialVersion: CredentialVersion?): BaseCredentialGenerateRequest { if (credentialVersion == null) { @@ -24,17 +34,8 @@ class GenerationRequestGenerator { } val regeneratable = regeneratableTypeProducers - .getOrDefault(credentialVersion.getCredentialType(), Supplier { NotRegeneratable() }) - .get() + .getOrDefault(credentialVersion.getCredentialType()) { NotRegeneratable() } + .invoke() return regeneratable.createGenerateRequest(credentialVersion) } - - init { - regeneratableTypeProducers = HashMap() - regeneratableTypeProducers["password"] = Supplier { PasswordCredentialRegeneratable() } - regeneratableTypeProducers["user"] = Supplier { UserCredentialRegeneratable() } - regeneratableTypeProducers["ssh"] = Supplier { SshCredentialRegeneratable() } - regeneratableTypeProducers["rsa"] = Supplier { RsaCredentialRegeneratable() } - regeneratableTypeProducers["certificate"] = Supplier { CertificateCredentialRegeneratable() } - } } diff --git a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/utils/KeyUsageMapper.kt b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/utils/KeyUsageMapper.kt new file mode 100644 index 000000000..5d02d6d9c --- /dev/null +++ b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/utils/KeyUsageMapper.kt @@ -0,0 +1,43 @@ +package org.cloudfoundry.credhub.utils + +import org.bouncycastle.asn1.x509.KeyUsage +import org.cloudfoundry.credhub.ErrorMessages +import org.cloudfoundry.credhub.exceptions.ParameterizedValidationException +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CRL_SIGN +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DATA_ENCIPHERMENT +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DECIPHER_ONLY +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DIGITAL_SIGNATURE +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.ENCIPHER_ONLY +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_AGREEMENT +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_CERT_SIGN +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_ENCIPHERMENT +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.NON_REPUDIATION + +object KeyUsageMapper { + /** + * Converts an array of key usage strings into a BouncyCastle KeyUsage object. + * + * @param keyUsages Array of key usage string values + * @return KeyUsage object with the appropriate bitmask + * @throws ParameterizedValidationException if an invalid key usage string is provided + */ + fun mapKeyUsage(keyUsages: Array): KeyUsage { + var bitmask = 0 + for (usage in keyUsages) { + bitmask = + when (usage) { + DIGITAL_SIGNATURE -> bitmask or KeyUsage.digitalSignature + NON_REPUDIATION -> bitmask or KeyUsage.nonRepudiation + KEY_ENCIPHERMENT -> bitmask or KeyUsage.keyEncipherment + DATA_ENCIPHERMENT -> bitmask or KeyUsage.dataEncipherment + KEY_AGREEMENT -> bitmask or KeyUsage.keyAgreement + KEY_CERT_SIGN -> bitmask or KeyUsage.keyCertSign + CRL_SIGN -> bitmask or KeyUsage.cRLSign + ENCIPHER_ONLY -> bitmask or KeyUsage.encipherOnly + DECIPHER_ONLY -> bitmask or KeyUsage.decipherOnly + else -> throw ParameterizedValidationException(ErrorMessages.INVALID_KEY_USAGE, usage) + } + } + return KeyUsage(bitmask) + } +} diff --git a/components/credentials/src/test/java/org/cloudfoundry/credhub/utils/CertificateReaderTest.java b/components/credentials/src/test/java/org/cloudfoundry/credhub/utils/CertificateReaderTest.java index 89aed18c9..9c03940ef 100644 --- a/components/credentials/src/test/java/org/cloudfoundry/credhub/utils/CertificateReaderTest.java +++ b/components/credentials/src/test/java/org/cloudfoundry/credhub/utils/CertificateReaderTest.java @@ -1,12 +1,27 @@ package org.cloudfoundry.credhub.utils; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.Security; +import java.util.Date; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.GeneralNamesBuilder; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.cloudfoundry.credhub.domain.CertificateGenerationParameters; import org.cloudfoundry.credhub.exceptions.MalformedCertificateException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -157,4 +172,130 @@ public void returnsParametersCorrectly() { equalTo(true)); assertThat(certificateReader.getSubjectName().toString(), equalTo(distinguishedName)); } + + @Test + public void regenerationConstructor_preservesDnsSans() throws Exception { + final GeneralNames expectedSans = new GeneralNames( + new GeneralName(GeneralName.dNSName, "SolarSystem")); + + final CertificateReader reader = new CertificateReader(BIG_TEST_CERT); + final CertificateGenerationParameters params = new CertificateGenerationParameters(reader, null); + + assertThat(params.getAlternativeNames(), equalTo(expectedSans)); + } + + @Test + public void regenerationConstructor_preservesIpAndDnsSans() throws Exception { + final String certPem = generateSelfSignedCert( + new GeneralName(GeneralName.iPAddress, "10.0.0.1"), + new GeneralName(GeneralName.dNSName, "example.com"), + new GeneralName(GeneralName.iPAddress, "192.168.1.100") + ); + + final GeneralNames expectedSans = new GeneralNamesBuilder() + .addName(new GeneralName(GeneralName.iPAddress, "10.0.0.1")) + .addName(new GeneralName(GeneralName.dNSName, "example.com")) + .addName(new GeneralName(GeneralName.iPAddress, "192.168.1.100")) + .build(); + + final CertificateReader reader = new CertificateReader(certPem); + final CertificateGenerationParameters params = new CertificateGenerationParameters(reader, null); + + assertThat(params.getAlternativeNames(), equalTo(expectedSans)); + } + + @Test + public void regenerationConstructor_preservesNullSans() { + final CertificateReader reader = new CertificateReader(SIMPLE_SELF_SIGNED_TEST_CERT); + final CertificateGenerationParameters params = new CertificateGenerationParameters(reader, null); + + assertThat(params.getAlternativeNames(), equalTo(null)); + } + + @Test + public void regenerationConstructor_preservesKeyUsage() throws Exception { + final KeyUsage expectedKeyUsage = new KeyUsage(KeyUsage.digitalSignature); + + final CertificateReader reader = new CertificateReader(BIG_TEST_CERT); + final CertificateGenerationParameters params = new CertificateGenerationParameters(reader, null); + + assertThat(params.getKeyUsage(), equalTo(expectedKeyUsage)); + } + + @Test + public void regenerationConstructor_preservesExtendedKeyUsage() { + final CertificateReader reader = new CertificateReader(BIG_TEST_CERT); + final CertificateGenerationParameters params = new CertificateGenerationParameters(reader, null); + + assertThat(asList(params.getExtendedKeyUsage().getUsages()), + containsInAnyOrder(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth)); + } + + @Test + public void regenerationConstructor_allowsKeyUsageOverride() throws Exception { + final String caCertPem = generateSelfSignedCaCert(); + final CertificateReader reader = new CertificateReader(caCertPem); + + assertThat("CA cert should have no key usage", reader.getKeyUsage(), equalTo(null)); + + final CertificateGenerationParameters params = new CertificateGenerationParameters(reader, null); + final KeyUsage defaultCaKeyUsage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + params.setKeyUsage(defaultCaKeyUsage); + + assertThat(params.getKeyUsage(), equalTo(defaultCaKeyUsage)); + } + + private static String generateSelfSignedCert(final GeneralName... sans) throws Exception { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( + "RSA", BouncyCastleFipsProvider.PROVIDER_NAME); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + final X500Name subject = new X500Name("CN=test"); + final Date notBefore = new Date(System.currentTimeMillis() - 86400000L); + final Date notAfter = new Date(System.currentTimeMillis() + 86400000L); + + final X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, BigInteger.ONE, notBefore, notAfter, subject, keyPair.getPublic()); + + final GeneralNamesBuilder sanBuilder = new GeneralNamesBuilder(); + for (final GeneralName san : sans) { + sanBuilder.addName(san); + } + certBuilder.addExtension(Extension.subjectAlternativeName, false, sanBuilder.build()); + + return signAndEncode(certBuilder, keyPair); + } + + private static String generateSelfSignedCaCert() throws Exception { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( + "RSA", BouncyCastleFipsProvider.PROVIDER_NAME); + keyPairGenerator.initialize(2048); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + final X500Name subject = new X500Name("CN=test-ca"); + final Date notBefore = new Date(System.currentTimeMillis() - 86400000L); + final Date notAfter = new Date(System.currentTimeMillis() + 86400000L); + + final X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, BigInteger.ONE, notBefore, notAfter, subject, keyPair.getPublic()); + certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + + return signAndEncode(certBuilder, keyPair); + } + + private static String signAndEncode(final X509v3CertificateBuilder certBuilder, final KeyPair keyPair) throws Exception { + final var cert = new JcaX509CertificateConverter() + .setProvider(BouncyCastleFipsProvider.PROVIDER_NAME) + .getCertificate(certBuilder.build( + new JcaContentSignerBuilder("SHA256withRSA") + .setProvider(BouncyCastleFipsProvider.PROVIDER_NAME) + .build(keyPair.getPrivate()))); + + final StringWriter stringWriter = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(cert); + } + return stringWriter.toString(); + } } diff --git a/components/credentials/src/test/kotlin/org/cloudfoundry/credhub/utils/KeyUsageMapperTest.kt b/components/credentials/src/test/kotlin/org/cloudfoundry/credhub/utils/KeyUsageMapperTest.kt new file mode 100644 index 000000000..870a730a8 --- /dev/null +++ b/components/credentials/src/test/kotlin/org/cloudfoundry/credhub/utils/KeyUsageMapperTest.kt @@ -0,0 +1,119 @@ +package org.cloudfoundry.credhub.utils + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.bouncycastle.asn1.x509.KeyUsage +import org.cloudfoundry.credhub.ErrorMessages +import org.cloudfoundry.credhub.exceptions.ParameterizedValidationException +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CRL_SIGN +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DATA_ENCIPHERMENT +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DECIPHER_ONLY +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.DIGITAL_SIGNATURE +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.ENCIPHER_ONLY +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_AGREEMENT +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_CERT_SIGN +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_ENCIPHERMENT +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.NON_REPUDIATION +import org.junit.jupiter.api.Test + +class KeyUsageMapperTest { + @Test + fun `mapKeyUsage maps digital_signature correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(DIGITAL_SIGNATURE)) + assertThat(keyUsage.hasUsages(KeyUsage.digitalSignature)).isTrue() + } + + @Test + fun `mapKeyUsage maps non_repudiation correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(NON_REPUDIATION)) + assertThat(keyUsage.hasUsages(KeyUsage.nonRepudiation)).isTrue() + } + + @Test + fun `mapKeyUsage maps key_encipherment correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(KEY_ENCIPHERMENT)) + assertThat(keyUsage.hasUsages(KeyUsage.keyEncipherment)).isTrue() + } + + @Test + fun `mapKeyUsage maps data_encipherment correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(DATA_ENCIPHERMENT)) + assertThat(keyUsage.hasUsages(KeyUsage.dataEncipherment)).isTrue() + } + + @Test + fun `mapKeyUsage maps key_agreement correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(KEY_AGREEMENT)) + assertThat(keyUsage.hasUsages(KeyUsage.keyAgreement)).isTrue() + } + + @Test + fun `mapKeyUsage maps key_cert_sign correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(KEY_CERT_SIGN)) + assertThat(keyUsage.hasUsages(KeyUsage.keyCertSign)).isTrue() + } + + @Test + fun `mapKeyUsage maps crl_sign correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(CRL_SIGN)) + assertThat(keyUsage.hasUsages(KeyUsage.cRLSign)).isTrue() + } + + @Test + fun `mapKeyUsage maps encipher_only correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(ENCIPHER_ONLY)) + assertThat(keyUsage.hasUsages(KeyUsage.encipherOnly)).isTrue() + } + + @Test + fun `mapKeyUsage maps decipher_only correctly`() { + val keyUsage = KeyUsageMapper.mapKeyUsage(arrayOf(DECIPHER_ONLY)) + assertThat(keyUsage.hasUsages(KeyUsage.decipherOnly)).isTrue() + } + + @Test + fun `mapKeyUsage maps multiple key usages correctly`() { + val keyUsage = + KeyUsageMapper.mapKeyUsage( + arrayOf(DIGITAL_SIGNATURE, KEY_ENCIPHERMENT, DATA_ENCIPHERMENT), + ) + assertThat(keyUsage.hasUsages(KeyUsage.digitalSignature)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.keyEncipherment)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.dataEncipherment)).isTrue() + } + + @Test + fun `mapKeyUsage throws exception for invalid key usage`() { + assertThatThrownBy { + KeyUsageMapper.mapKeyUsage(arrayOf("invalid_usage")) + }.isInstanceOf(ParameterizedValidationException::class.java) + .hasFieldOrPropertyWithValue("message", ErrorMessages.INVALID_KEY_USAGE) + } + + @Test + fun `mapKeyUsage handles all key usages at once`() { + val keyUsage = + KeyUsageMapper.mapKeyUsage( + arrayOf( + DIGITAL_SIGNATURE, + NON_REPUDIATION, + KEY_ENCIPHERMENT, + DATA_ENCIPHERMENT, + KEY_AGREEMENT, + KEY_CERT_SIGN, + CRL_SIGN, + ENCIPHER_ONLY, + DECIPHER_ONLY, + ), + ) + assertThat(keyUsage.hasUsages(KeyUsage.digitalSignature)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.nonRepudiation)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.keyEncipherment)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.dataEncipherment)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.keyAgreement)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.keyCertSign)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.cRLSign)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.encipherOnly)).isTrue() + assertThat(keyUsage.hasUsages(KeyUsage.decipherOnly)).isTrue() + } +} diff --git a/components/test-support/src/test/kotlin/org/cloudfoundry/credhub/utils/TestConstants.kt b/components/test-support/src/test/kotlin/org/cloudfoundry/credhub/utils/TestConstants.kt index e863a89f8..947fe06dd 100644 --- a/components/test-support/src/test/kotlin/org/cloudfoundry/credhub/utils/TestConstants.kt +++ b/components/test-support/src/test/kotlin/org/cloudfoundry/credhub/utils/TestConstants.kt @@ -628,5 +628,31 @@ class TestConstants { "6xCXz32y9vQHG76WYKBjGatP5OygNqk8v/8KFBO/fZszgFmrbGi5sUl2XrW0sQtp\n" + "dJYEOgm6e8EO0Ve1uD/dFHfxcQIjt0uTzGjMJdYBm9EHl+bJz5JdTBp6aapaSQ==\n" + "-----END RSA PRIVATE KEY-----\n" + + const val TEST_CA_WITH_DEFAULT_KEY_USAGE: String = + "-----BEGIN CERTIFICATE-----\n" + + "MIIEGzCCAoOgAwIBAgIUTCn06/xEbAHM4pMdObKPFmVXtLQwDQYJKoZIhvcNAQEL\n" + + "BQAwFTETMBEGA1UEAxMKTXkgUm9vdCBDQTAeFw0yNjAxMjgxNDUzMzBaFw0zNjAx\n" + + "MjYxNDUzMzBaMBUxEzARBgNVBAMTCk15IFJvb3QgQ0EwggGiMA0GCSqGSIb3DQEB\n" + + "AQUAA4IBjwAwggGKAoIBgQCKlonuBSgy/5CL3lkcwrBYtQDgugX2RhzsvNPzzqMX\n" + + "rGXd3jcbz66Uy1aRdM1Xiw7mu9q1dkKGBBlvmYrFm3mQjTkbCXHa+rFbTyuMhAiy\n" + + "0qoEiJlTdVsBwO7Z4jLloODuZ1hHOQI+rL1gWeLB3V2/BKtobgg1ouvZtyBqHoL2\n" + + "wGWeHJCUonFIgpOrg/j+XR+z0GGRGkd7ovKeSF/kYdg/SQaGtmg4pWkhKsGSFJaz\n" + + "EXP1FX4+rlkNcGdxARNhsHewgwvflw9F2NTQ6cM7pn1fDEB6XFqu+gaT7iM+x2cg\n" + + "AY7WoQPjA8yKn56gvk+CRM/HNv2PYDYN996+V3T+gSKL3dsiCY98TSWZH03TTlco\n" + + "1M+iUKDc8mH3ifH52EENjMtJOKrhtweAg78t8+fLrxu+/A6hnwCEJmXAwD2bamg/\n" + + "1BRTiV01xF3WUVkbNRxKrcUnbWIHLFTwEDFsZ0dItXBHaGOUukWsbJEQNmhpIpZk\n" + + "KP2kAM/l2CbsM6US8qGFtgMCAwEAAaNjMGEwHQYDVR0OBBYEFPy0WtILdHVwvER1\n" + + "A/kFUWOQuPdFMA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT8tFrSC3R1cLxE\n" + + "dQP5BVFjkLj3RTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBgQAS\n" + + "hpAhPkadAdJELOYDogl7fbKU8LAO9UTMTKBK0QgPVXJLgSswWbebfgZofYHHy7wa\n" + + "P1r9LEoqPcGbXlfnUpGY6MwW450cmXv934xqSr9lNPM7zF57CUNrnnZw2INTS8NQ\n" + + "3xhSSt+DagQn9nuuhX16hGCO/OC/Bf4Don7akamAvRGoVVP5ppSYkyukTnpnypoQ\n" + + "KpA1MA4bQX7jmIQAXzD9BoVEahOZ2fXZqwAxBtbxlJ4kuWkOCqLyo3hJrPfJFVl/\n" + + "UFjnKJ429WqRd9RToU5rFaLhiwjvXJpUrirEBGyTn6UKfDRftdmIa1Jd25BhH0PZ\n" + + "N+AwZy6zgbPkm2E8s6fFvyXAKJm/XKZh2ABVguSoxhnkexYLGcJ7dsfNXZC84DdM\n" + + "so34gYtRgwrtV6GUpcDaSr4pwPzkjLjIrOzTEAX7UjJ92/Yrqf3hTs/Taso7BBvk\n" + + "mBQdyi36ka3lGnn0mfhxssU7UJe/PWJHeCHl1RNK8Cn6mi0Nu2MWsBovxMwJCaQ=\n" + + "-----END CERTIFICATE-----\n" } }