From 55829b61f6a3fd1c984e5f95da470c94117ac15e Mon Sep 17 00:00:00 2001 From: jbuecher Date: Wed, 28 Jan 2026 17:23:24 +0100 Subject: [PATCH] build: regenerate CA certificates with default key usage if none is given --- .../DefaultCertificatesHandler.kt | 11 ++ .../DefaultCertificatesHandlerTest.java | 148 +++++++++++++++++- .../domain/CertificateGenerationParameters.kt | 2 +- .../requests/CertificateGenerateRequest.kt | 32 ++++ .../credhub/utils/TestConstants.kt | 26 +++ 5 files changed, 217 insertions(+), 2 deletions(-) diff --git a/backends/credhub/src/main/kotlin/org/cloudfoundry/credhub/certificates/DefaultCertificatesHandler.kt b/backends/credhub/src/main/kotlin/org/cloudfoundry/credhub/certificates/DefaultCertificatesHandler.kt index 2b3db04c0..ed4348409 100644 --- a/backends/credhub/src/main/kotlin/org/cloudfoundry/credhub/certificates/DefaultCertificatesHandler.kt +++ b/backends/credhub/src/main/kotlin/org/cloudfoundry/credhub/certificates/DefaultCertificatesHandler.kt @@ -18,6 +18,8 @@ import org.cloudfoundry.credhub.exceptions.PermissionException import org.cloudfoundry.credhub.generate.GenerationRequestGenerator import org.cloudfoundry.credhub.generate.UniversalCredentialGenerator import org.cloudfoundry.credhub.requests.CertificateGenerateRequest +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CRL_SIGN +import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_CERT_SIGN import org.cloudfoundry.credhub.requests.CertificateRegenerateRequest import org.cloudfoundry.credhub.requests.CreateVersionRequest import org.cloudfoundry.credhub.requests.UpdateTransitionalVersionRequest @@ -46,6 +48,7 @@ class DefaultCertificatesHandler( private val userContextHolder: UserContextHolder, @Value("\${security.authorization.acls.enabled}") private val enforcePermissions: Boolean, @Value("\${certificates.concatenate_cas:false}") var concatenateCas: Boolean, + @Value("\${certificates.enable_default_ca_key_usages:false}") var defaultCAKeyUsages: Boolean, ) : CertificatesHandler { override fun handleRegenerate( credentialUuid: String, @@ -71,6 +74,14 @@ class DefaultCertificatesHandler( if (request.duration != null) { (generateRequest as CertificateGenerateRequest).setDuration(request.duration!!) } + if (defaultCAKeyUsages && existingCredentialVersion.isCertificateAuthority) { + (generateRequest as CertificateGenerateRequest).setKeyUsage( + arrayOf( + KEY_CERT_SIGN, + CRL_SIGN, + ), + ) + } val credentialValue = credentialGenerator diff --git a/backends/credhub/src/test/java/org/cloudfoundry/credhub/handlers/DefaultCertificatesHandlerTest.java b/backends/credhub/src/test/java/org/cloudfoundry/credhub/handlers/DefaultCertificatesHandlerTest.java index 518fa2af3..fcb8d2341 100644 --- a/backends/credhub/src/test/java/org/cloudfoundry/credhub/handlers/DefaultCertificatesHandlerTest.java +++ b/backends/credhub/src/test/java/org/cloudfoundry/credhub/handlers/DefaultCertificatesHandlerTest.java @@ -8,6 +8,7 @@ import java.util.Objects; import java.util.UUID; +import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import org.cloudfoundry.credhub.ErrorMessages; import org.cloudfoundry.credhub.PermissionOperation; @@ -36,6 +37,7 @@ import org.cloudfoundry.credhub.services.DefaultCertificateService; import org.cloudfoundry.credhub.services.PermissionCheckingService; import org.cloudfoundry.credhub.utils.BouncyCastleFipsConfigurer; +import org.cloudfoundry.credhub.utils.CertificateReader; import org.cloudfoundry.credhub.utils.TestConstants; import org.cloudfoundry.credhub.views.CertificateCredentialView; import org.cloudfoundry.credhub.views.CertificateCredentialsView; @@ -52,7 +54,10 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.fail; +import static org.bouncycastle.asn1.x509.KeyUsage.cRLSign; +import static org.bouncycastle.asn1.x509.KeyUsage.keyCertSign; import static org.cloudfoundry.credhub.utils.TestConstants.TEST_CA; +import static org.cloudfoundry.credhub.utils.TestConstants.TEST_CA_WITH_DEFAULT_KEY_USAGE; import static org.cloudfoundry.credhub.utils.TestConstants.TEST_TRUSTED_CA; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; @@ -77,6 +82,7 @@ public class DefaultCertificatesHandlerTest { private DefaultCertificatesHandler subjectWithoutAcls; private DefaultCertificatesHandler subjectWithConcatenateCas; private DefaultCertificatesHandler subjectWithoutConcatenateCas; + private DefaultCertificatesHandler subjectWithDefaultCAKeyUsages; private UniversalCredentialGenerator universalCredentialGenerator; private GenerationRequestGenerator generationRequestGenerator; private DefaultCertificateService certificateService; @@ -110,6 +116,7 @@ public void beforeEach() { permissionCheckingService, userContextHolder, true, + false, false ); subjectWithoutAcls = new DefaultCertificatesHandler( @@ -120,6 +127,7 @@ public void beforeEach() { permissionCheckingService, userContextHolder, false, + false, false ); subjectWithConcatenateCas = new DefaultCertificatesHandler( @@ -130,7 +138,8 @@ public void beforeEach() { permissionCheckingService, userContextHolder, false, - true + true, + false ); subjectWithoutConcatenateCas = new DefaultCertificatesHandler( certificateService, @@ -140,8 +149,20 @@ public void beforeEach() { permissionCheckingService, userContextHolder, false, + false, false ); + subjectWithDefaultCAKeyUsages = new DefaultCertificatesHandler( + certificateService, + universalCredentialGenerator, + generationRequestGenerator, + new CEFAuditRecord(), + permissionCheckingService, + userContextHolder, + false, + false, + true + ); } @Test @@ -979,6 +1000,131 @@ public void handleRegenerate_whenDurationNotNull_passesValueToCertificateService assertEquals(4567, Objects.requireNonNull(regenerateRequest.getDuration()).intValue()); } + @Test + public void handleRegenerate_whenDefaultCAKeyUsageIsEnabled_andCertHasNoKeyUsages_setsDefaultKeyUsages() { + final CertificateCredentialVersion existingCert = mock(CertificateCredentialVersion.class); + final CertificateReader certReader = new CertificateReader(TEST_CA); // TEST_CA has no key usage extension + final CertificateGenerateRequest generateRequest = new CertificateGenerateRequest(); + + final CertificateGenerationParameters params = new CertificateGenerationParameters(certReader, null); + generateRequest.setCertificateGenerationParameters(params); + + final CertificateCredentialValue newValue = mock(CertificateCredentialValue.class); + final CertificateCredentialVersion newVersion = mock(CertificateCredentialVersion.class); + + when(existingCert.getName()).thenReturn("/test-ca"); + when(existingCert.getCertificate()).thenReturn(TEST_CA); + when(existingCert.isCertificateAuthority()).thenReturn(true); + when(existingCert.getParsedCertificate()).thenReturn(certReader); + + when(certificateService.findByCredentialUuid(UUID_STRING)).thenReturn(existingCert); + when(generationRequestGenerator.createGenerateRequest(existingCert)).thenReturn(generateRequest); + when(universalCredentialGenerator.generate(generateRequest)).thenReturn(newValue); + when(certificateService.save(eq(existingCert), any(), any())).thenReturn(newVersion); + + final CertificateRegenerateRequest regenerateRequest = new CertificateRegenerateRequest(true, false, null, null, null); + + subjectWithDefaultCAKeyUsages.handleRegenerate(UUID_STRING, regenerateRequest); + + final CertificateGenerationParameters updatedParams = (CertificateGenerationParameters) generateRequest.getGenerationParameters(); + assertThat(updatedParams.getKeyUsage(), IsEqual.equalTo( + new KeyUsage( + keyCertSign | cRLSign + ) + )); + } + + @Test + public void handleRegenerate_whenDefaultCAKeyUsageIsEnabled_andCertAlreadyHasKeyUsages_preservesExistingKeyUsages() { + final CertificateCredentialVersion existingCert = mock(CertificateCredentialVersion.class); + final CertificateReader certReader = new CertificateReader(TEST_CA_WITH_DEFAULT_KEY_USAGE); // Has key usages + final CertificateGenerateRequest generateRequest = new CertificateGenerateRequest(); + + final CertificateGenerationParameters params = new CertificateGenerationParameters(certReader, null); + final org.bouncycastle.asn1.x509.KeyUsage originalKeyUsage = params.getKeyUsage(); + generateRequest.setCertificateGenerationParameters(params); + + final CertificateCredentialValue newValue = mock(CertificateCredentialValue.class); + final CertificateCredentialVersion newVersion = mock(CertificateCredentialVersion.class); + + when(existingCert.getName()).thenReturn("/test-ca"); + when(existingCert.getCertificate()).thenReturn(TEST_CA_WITH_DEFAULT_KEY_USAGE); + when(existingCert.isCertificateAuthority()).thenReturn(true); + when(existingCert.getParsedCertificate()).thenReturn(certReader); + + when(certificateService.findByCredentialUuid(UUID_STRING)).thenReturn(existingCert); + when(generationRequestGenerator.createGenerateRequest(existingCert)).thenReturn(generateRequest); + when(universalCredentialGenerator.generate(generateRequest)).thenReturn(newValue); + when(certificateService.save(eq(existingCert), any(), any())).thenReturn(newVersion); + + final CertificateRegenerateRequest regenerateRequest = new CertificateRegenerateRequest(true, false, null, null, null); + + subjectWithDefaultCAKeyUsages.handleRegenerate(UUID_STRING, regenerateRequest); + + final CertificateGenerationParameters updatedParams = (CertificateGenerationParameters) generateRequest.getGenerationParameters(); + assertThat(updatedParams.getKeyUsage(), IsEqual.equalTo(originalKeyUsage)); + } + + @Test + public void handleRegenerate_whenDefaultCAKeyUsageIsDisabled_doesNotSetKeyUsages() { + final CertificateCredentialVersion existingCert = mock(CertificateCredentialVersion.class); + final CertificateReader certReader = new CertificateReader(TEST_CA); + final CertificateGenerateRequest generateRequest = new CertificateGenerateRequest(); + + final CertificateGenerationParameters params = new CertificateGenerationParameters(certReader, null); + generateRequest.setCertificateGenerationParameters(params); + + final CertificateCredentialValue newValue = mock(CertificateCredentialValue.class); + final CertificateCredentialVersion newVersion = mock(CertificateCredentialVersion.class); + + when(existingCert.getName()).thenReturn("/test-ca"); + when(existingCert.getCertificate()).thenReturn(TEST_CA); + when(existingCert.isCertificateAuthority()).thenReturn(true); + when(existingCert.getParsedCertificate()).thenReturn(certReader); + + when(certificateService.findByCredentialUuid(UUID_STRING)).thenReturn(existingCert); + when(generationRequestGenerator.createGenerateRequest(existingCert)).thenReturn(generateRequest); + when(universalCredentialGenerator.generate(generateRequest)).thenReturn(newValue); + when(certificateService.save(eq(existingCert), any(), any())).thenReturn(newVersion); + + final CertificateRegenerateRequest regenerateRequest = new CertificateRegenerateRequest(true, false, null, null, null); + + subjectWithoutAcls.handleRegenerate(UUID_STRING, regenerateRequest); + + final CertificateGenerationParameters updatedParams = (CertificateGenerationParameters) generateRequest.getGenerationParameters(); + assertNull(updatedParams.getKeyUsage()); + } + + @Test + public void handleRegenerate_whenDefaultCAKeyUsageIsEnabled_butNotCA_doesNotSetKeyUsages() { + final CertificateCredentialVersion existingCert = mock(CertificateCredentialVersion.class); + final CertificateReader certReader = new CertificateReader(TEST_CA); + final CertificateGenerateRequest generateRequest = new CertificateGenerateRequest(); + + final CertificateGenerationParameters params = new CertificateGenerationParameters(certReader, null); + generateRequest.setCertificateGenerationParameters(params); + + final CertificateCredentialValue newValue = mock(CertificateCredentialValue.class); + final CertificateCredentialVersion newVersion = mock(CertificateCredentialVersion.class); + + when(existingCert.getName()).thenReturn("/test-cert"); + when(existingCert.getCertificate()).thenReturn(TEST_CA); + when(existingCert.isCertificateAuthority()).thenReturn(false); // NOT a CA + when(existingCert.getParsedCertificate()).thenReturn(certReader); + + when(certificateService.findByCredentialUuid(UUID_STRING)).thenReturn(existingCert); + when(generationRequestGenerator.createGenerateRequest(existingCert)).thenReturn(generateRequest); + when(universalCredentialGenerator.generate(generateRequest)).thenReturn(newValue); + when(certificateService.save(eq(existingCert), any(), any())).thenReturn(newVersion); + + final CertificateRegenerateRequest regenerateRequest = new CertificateRegenerateRequest(true, false, null, null, null); + + subjectWithDefaultCAKeyUsages.handleRegenerate(UUID_STRING, regenerateRequest); + + final CertificateGenerationParameters updatedParams = (CertificateGenerationParameters) generateRequest.getGenerationParameters(); + assertNull(updatedParams.getKeyUsage()); + } + @Test public void handleDeleteVersionRequest_whenAclsEnabled_andHasUserPermission_deletesVersion() { UUID versionId = UUID.randomUUID(); 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..9969dc7ec 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 @@ -48,7 +48,7 @@ class CertificateGenerationParameters : GenerationParameters { val extendedKeyUsage: ExtendedKeyUsage? - val keyUsage: KeyUsage? + var keyUsage: KeyUsage? var allowTransitionalParentToSign: Boolean = false diff --git a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/requests/CertificateGenerateRequest.kt b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/requests/CertificateGenerateRequest.kt index ff5431478..dfebc12f9 100644 --- a/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/requests/CertificateGenerateRequest.kt +++ b/components/credentials/src/main/kotlin/org/cloudfoundry/credhub/requests/CertificateGenerateRequest.kt @@ -2,9 +2,19 @@ package org.cloudfoundry.credhub.requests import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty +import org.bouncycastle.asn1.x509.KeyUsage import org.cloudfoundry.credhub.ErrorMessages import org.cloudfoundry.credhub.domain.CertificateGenerationParameters 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 class CertificateGenerateRequest : BaseCredentialGenerateRequest() { @JsonProperty("parameters") @@ -60,4 +70,26 @@ class CertificateGenerateRequest : BaseCredentialGenerateRequest() { this.certificateGenerationParameters?.duration = duration this.certificateGenerationParameters?.validate() } + + fun setKeyUsage(keyUsage: Array) { + if (certificateGenerationParameters?.keyUsage == null) { + var bitmask = 0 + for (usage in keyUsage) { + 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) + } + } + certificateGenerationParameters?.keyUsage = KeyUsage(bitmask) + } + } } 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" } }