Skip to content

Commit 9b47c4c

Browse files
jbuecherpeterhaochen47
authored andcommitted
build: default key usage for CA certificates
1 parent c53a539 commit 9b47c4c

File tree

9 files changed

+320
-4
lines changed

9 files changed

+320
-4
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ ENV SERVER_CA_PRIVATE_KEY_PATH="/etc/server_certs/server_ca_private.pem"
4242
ENV UAA_CA_PATH="/etc/trusted_cas/dev_uaa.pem"
4343
ENV UAA_URL="https://35.196.32.64:8443"
4444
ENV SUBJECT_ALTERNATIVE_NAMES="DNS:localhost, IP:127.0.0.1"
45+
ENV ENABLE_DEFAULT_CA_KEY_USAGES=false
4546

4647
CMD /app/setup_trust_store.sh && /app/start_server.sh

applications/credhub-api/src/main/resources/application-dev.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ spring:
5656

5757
certificates:
5858
concatenate_cas: true
59+
enable_default_ca_key_usages: false
5960

6061
backend:
6162
socket_file: "/tmp/socket/test.sock"

backends/credhub/src/main/kotlin/org/cloudfoundry/credhub/credentials/DefaultCredentialsHandler.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import org.cloudfoundry.credhub.generate.UniversalCredentialGenerator
1818
import org.cloudfoundry.credhub.requests.BaseCredentialGenerateRequest
1919
import org.cloudfoundry.credhub.requests.BaseCredentialSetRequest
2020
import org.cloudfoundry.credhub.requests.CertificateGenerateRequest
21+
import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.CRL_SIGN
22+
import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.Companion.KEY_CERT_SIGN
2123
import org.cloudfoundry.credhub.requests.CertificateSetRequest
2224
import org.cloudfoundry.credhub.services.CertificateAuthorityService
2325
import org.cloudfoundry.credhub.services.CredentialService
@@ -42,6 +44,7 @@ class DefaultCredentialsHandler(
4244
private val credentialGenerator: UniversalCredentialGenerator,
4345
@Value("\${security.authorization.acls.enabled}") private val enforcePermissions: Boolean,
4446
@Value("\${certificates.concatenate_cas:false}") var concatenateCas: Boolean,
47+
@Value("\${certificates.enable_default_ca_key_usages:false}") var defaultCAKeyUsages: Boolean,
4548
) : CredentialsHandler {
4649
override fun findStartingWithPath(
4750
path: String,
@@ -64,11 +67,17 @@ class DefaultCredentialsHandler(
6467
if (generateRequest.type == "certificate") {
6568
val req = generateRequest as CertificateGenerateRequest
6669
val caName = req.generationRequestParameters?.caName
70+
val isCa = req.generationRequestParameters?.isCa ?: false
71+
val noKeyUsages = req.generationRequestParameters?.keyUsage?.isEmpty() ?: true
72+
if (isCa && noKeyUsages && defaultCAKeyUsages) {
73+
req.generationRequestParameters?.keyUsage = arrayOf(KEY_CERT_SIGN, CRL_SIGN)
74+
}
6775
if (caName == null) {
68-
if (req.generationRequestParameters?.isCa!!) {
76+
if (isCa) {
6977
generateRequest.generationRequestParameters?.caName = req.name
7078
generateRequest.generationRequestParameters?.isSelfSigned = true
71-
val certificateGenerationParameters = CertificateGenerationParameters(generateRequest.generationRequestParameters!!)
79+
val certificateGenerationParameters =
80+
CertificateGenerationParameters(generateRequest.generationRequestParameters!!)
7281
generateRequest.setCertificateGenerationParameters(certificateGenerationParameters)
7382
}
7483
} else {

backends/credhub/src/test/java/org/cloudfoundry/credhub/handlers/DefaultCredentialsHandlerTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import static java.util.Collections.EMPTY_SET;
5757
import static java.util.Collections.emptyList;
5858
import static org.assertj.core.api.Assertions .fail;
59+
import static org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.CRL_SIGN;
60+
import static org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.KEY_CERT_SIGN;
5961
import static org.hamcrest.MatcherAssert.assertThat;
6062
import static org.hamcrest.Matchers.hasSize;
6163
import static org.hamcrest.Matchers.samePropertyValuesAs;
@@ -81,6 +83,7 @@ public class DefaultCredentialsHandlerTest {
8183
private DefaultCredentialsHandler subjectWithAcls;
8284
private DefaultCredentialsHandler subjectWithoutAcls;
8385
private DefaultCredentialsHandler subjectWithAclsAndConcatenate;
86+
private DefaultCredentialsHandler subjectWithDefaultCAKeyUsages;
8487
private DefaultCredentialService credentialService;
8588
private CEFAuditRecord auditRecord;
8689
private PermissionCheckingService permissionCheckingService;
@@ -114,6 +117,7 @@ public void beforeEach() {
114117
certificateAuthorityService,
115118
universalCredentialGenerator,
116119
true,
120+
false,
117121
false);
118122

119123
subjectWithoutAcls = new DefaultCredentialsHandler(
@@ -124,6 +128,7 @@ public void beforeEach() {
124128
certificateAuthorityService,
125129
universalCredentialGenerator,
126130
false,
131+
false,
127132
false);
128133

129134
subjectWithAclsAndConcatenate = new DefaultCredentialsHandler(
@@ -134,6 +139,18 @@ public void beforeEach() {
134139
certificateAuthorityService,
135140
universalCredentialGenerator,
136141
true,
142+
true,
143+
false);
144+
145+
subjectWithDefaultCAKeyUsages = new DefaultCredentialsHandler(
146+
credentialService,
147+
auditRecord,
148+
permissionCheckingService,
149+
userContextHolder,
150+
certificateAuthorityService,
151+
universalCredentialGenerator,
152+
true,
153+
false,
137154
true);
138155

139156
generationParameters = new StringGenerationParameters();
@@ -1020,4 +1037,49 @@ public void findContainingName_withAclsDisabled_returnsUnfilteredCredentials() {
10201037
verify(permissionCheckingService, times(0)).findAllPathsByActor(any());
10211038
}
10221039

1040+
@Test
1041+
public void generateCredential_whenCertificateWithIsCaAndNoKeyUsagesAndDefaultCAKeyUsagesEnabled_setsDefaultKeyUsages() {
1042+
CertificateGenerationRequestParameters requestParameters = new CertificateGenerationRequestParameters();
1043+
requestParameters.setCa(true);
1044+
requestParameters.setKeyUsage(null);
1045+
1046+
CertificateGenerateRequest generateRequest = new CertificateGenerateRequest();
1047+
generateRequest.setRequestGenerationParameters(requestParameters);
1048+
generateRequest.setName(CREDENTIAL_NAME);
1049+
generateRequest.setType(CredentialType.CERTIFICATE.toString());
1050+
1051+
when(permissionCheckingService.hasPermission(USER, CREDENTIAL_NAME, PermissionOperation.WRITE))
1052+
.thenReturn(true);
1053+
when(credentialService.findActiveByName(CREDENTIAL_NAME))
1054+
.thenReturn(emptyList());
1055+
1056+
final CertificateCredentialValue generatedValue = new CertificateCredentialValue(
1057+
null,
1058+
TestConstants.TEST_CA,
1059+
TestConstants.TEST_PRIVATE_KEY,
1060+
null,
1061+
true,
1062+
false,
1063+
false,
1064+
false
1065+
);
1066+
final CertificateCredentialVersion credentialVersion = new CertificateCredentialVersion(CREDENTIAL_NAME);
1067+
credentialVersion.setCa(generatedValue.getCa());
1068+
credentialVersion.setEncryptor(encryptor);
1069+
credentialVersion.setCertificate(generatedValue.getCertificate());
1070+
credentialVersion.setPrivateKey(generatedValue.getPrivateKey());
1071+
credentialVersion.setUuid(UUID.randomUUID());
1072+
credentialVersion.getCredential().setUuid(UUID.randomUUID());
1073+
credentialVersion.setVersionCreatedAt(VERSION1_CREATED_AT);
1074+
1075+
when(universalCredentialGenerator.generate(any())).thenReturn(generatedValue);
1076+
when(credentialService.save(any(), any(), any())).thenReturn(credentialVersion);
1077+
1078+
subjectWithDefaultCAKeyUsages.generateCredential(generateRequest);
1079+
1080+
ArgumentCaptor<CertificateGenerateRequest> requestCaptor = ArgumentCaptor.forClass(CertificateGenerateRequest.class);
1081+
verify(universalCredentialGenerator).generate(requestCaptor.capture());
1082+
CertificateGenerationRequestParameters capturedParams = requestCaptor.getValue().getGenerationRequestParameters();
1083+
assertThat(capturedParams.getKeyUsage(), equalTo(new String[]{KEY_CERT_SIGN, CRL_SIGN}));
1084+
}
10231085
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package org.cloudfoundry.credhub.integration;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.security.cert.CertificateFactory;
5+
import java.security.cert.X509Certificate;
6+
import java.time.temporal.ChronoUnit;
7+
import java.util.Arrays;
8+
import java.util.Calendar;
9+
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.test.context.ActiveProfiles;
13+
import org.springframework.test.context.TestPropertySource;
14+
import org.springframework.test.context.junit4.SpringRunner;
15+
import org.springframework.test.web.servlet.MockMvc;
16+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
17+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
18+
import org.springframework.transaction.annotation.Transactional;
19+
import org.springframework.web.context.WebApplicationContext;
20+
21+
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
22+
import org.bouncycastle.asn1.x509.Extension;
23+
import org.bouncycastle.asn1.x509.KeyUsage;
24+
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
25+
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
26+
import org.cloudfoundry.credhub.CredhubTestApp;
27+
import org.cloudfoundry.credhub.utils.BouncyCastleFipsConfigurer;
28+
import org.cloudfoundry.credhub.utils.DatabaseProfileResolver;
29+
import org.json.JSONObject;
30+
import org.junit.Before;
31+
import org.junit.BeforeClass;
32+
import org.junit.Rule;
33+
import org.junit.Test;
34+
import org.junit.rules.Timeout;
35+
import org.junit.runner.RunWith;
36+
37+
import static java.nio.charset.StandardCharsets.UTF_8;
38+
import static org.bouncycastle.asn1.x509.KeyUsage.cRLSign;
39+
import static org.bouncycastle.asn1.x509.KeyUsage.keyCertSign;
40+
import static org.cloudfoundry.credhub.utils.AuthConstants.ALL_PERMISSIONS_TOKEN;
41+
import static org.hamcrest.CoreMatchers.notNullValue;
42+
import static org.hamcrest.CoreMatchers.nullValue;
43+
import static org.hamcrest.MatcherAssert.assertThat;
44+
import static org.hamcrest.core.IsEqual.equalTo;
45+
import static org.springframework.http.MediaType.APPLICATION_JSON;
46+
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
47+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
48+
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
49+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
50+
51+
@RunWith(SpringRunner.class)
52+
@ActiveProfiles(
53+
value = {
54+
"unit-test",
55+
"unit-test-permissions",
56+
},
57+
resolver = DatabaseProfileResolver.class
58+
)
59+
@SpringBootTest(classes = CredhubTestApp.class)
60+
@TestPropertySource(properties = "certificates.enable_default_ca_key_usages=true")
61+
@Transactional
62+
public class CertificateGenerateWitDefaultKeyUsagesTest {
63+
64+
@Autowired
65+
private WebApplicationContext webApplicationContext;
66+
private MockMvc mockMvc;
67+
68+
@Rule
69+
public Timeout globalTimeout = Timeout.seconds(60);
70+
71+
@BeforeClass
72+
public static void beforeAll() {
73+
BouncyCastleFipsConfigurer.configure();
74+
}
75+
76+
@Before
77+
public void beforeEach() throws Exception {
78+
mockMvc = MockMvcBuilders
79+
.webAppContextSetup(webApplicationContext)
80+
.apply(springSecurity())
81+
.build();
82+
}
83+
84+
@Test
85+
public void certificateGeneration_shouldGenerateCorrectCertificate() throws Exception {
86+
final MockHttpServletRequestBuilder caPost = post("/api/v1/data")
87+
.header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN)
88+
.accept(APPLICATION_JSON)
89+
.contentType(APPLICATION_JSON)
90+
//language=JSON
91+
.content("{\n"
92+
+ " \"name\" : \"picard\",\n"
93+
+ " \"type\" : \"certificate\",\n"
94+
+ " \"parameters\" : {\n"
95+
+ " \"common_name\" : \"federation\",\n"
96+
+ " \"is_ca\" : true,\n"
97+
+ " \"duration\" : 1 \n"
98+
+ " }\n"
99+
+ "}");
100+
101+
final String caResult = mockMvc.perform(caPost)
102+
.andDo(print())
103+
.andExpect(status().isOk())
104+
.andReturn().getResponse().getContentAsString();
105+
106+
JSONObject result = new JSONObject(caResult);
107+
final String picardCert = result.getJSONObject("value").getString("certificate");
108+
final String picardCA = result.getJSONObject("value").getString("ca");
109+
assertThat(picardCert, equalTo(picardCA));
110+
111+
final String expiryDate = result.getString("expiry_date");
112+
final String truncatedExpiryDate = expiryDate.substring(0, expiryDate.indexOf('T'));
113+
114+
final Calendar calendar = Calendar.getInstance();
115+
calendar.add(Calendar.DATE, 1);
116+
final String expectedTime = calendar.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toString();
117+
final String truncatedExpected = expectedTime.substring(0, expectedTime.indexOf('T'));
118+
119+
120+
assertThat(truncatedExpiryDate, equalTo(truncatedExpected));
121+
assertThat(result.getBoolean("certificate_authority"), equalTo(true));
122+
assertThat(result.getBoolean("self_signed"), equalTo(true));
123+
assertThat(result.getBoolean("generated"), equalTo(true));
124+
assertThat(result.getBoolean("duration_overridden"), equalTo(false));
125+
assertThat(result.getInt("duration_used"), equalTo(1));
126+
127+
assertThat(picardCert, notNullValue());
128+
129+
final MockHttpServletRequestBuilder certPost = post("/api/v1/data")
130+
.header("Authorization", "Bearer " + ALL_PERMISSIONS_TOKEN)
131+
.accept(APPLICATION_JSON)
132+
.contentType(APPLICATION_JSON)
133+
//language=JSON
134+
.content("{\n"
135+
+ " \"name\" : \"riker\",\n"
136+
+ " \"type\" : \"certificate\",\n"
137+
+ " \"parameters\" : {\n"
138+
+ " \"common_name\" : \"federation\",\n"
139+
+ " \"ca\" : \"picard\"\n"
140+
+ " }\n"
141+
+ "}");
142+
143+
final String certResult = mockMvc.perform(certPost)
144+
.andDo(print())
145+
.andExpect(status().isOk())
146+
.andReturn().getResponse().getContentAsString();
147+
148+
final String certCa = (new JSONObject(certResult)).getJSONObject("value").getString("ca");
149+
final String cert = (new JSONObject(certResult)).getJSONObject("value").getString("certificate");
150+
151+
assertThat(certCa, equalTo(picardCert));
152+
153+
154+
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
155+
final X509Certificate caPem = (X509Certificate) certificateFactory
156+
.generateCertificate(new ByteArrayInputStream(picardCert.getBytes(UTF_8)));
157+
158+
final X509Certificate certPem = (X509Certificate) certificateFactory
159+
.generateCertificate(new ByteArrayInputStream(cert.getBytes(UTF_8)));
160+
161+
final byte[] subjectKeyIdDer = caPem.getExtensionValue(Extension.subjectKeyIdentifier.getId());
162+
final SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(JcaX509ExtensionUtils.parseExtensionValue(subjectKeyIdDer));
163+
final byte[] subjectKeyId = subjectKeyIdentifier.getKeyIdentifier();
164+
165+
final byte[] authorityKeyIdDer = certPem.getExtensionValue(Extension.authorityKeyIdentifier.getId());
166+
final AuthorityKeyIdentifier authorityKeyIdentifier = AuthorityKeyIdentifier.getInstance(JcaX509ExtensionUtils.parseExtensionValue(authorityKeyIdDer));
167+
final byte[] authKeyId = authorityKeyIdentifier.getKeyIdentifier();
168+
169+
assertThat(subjectKeyId, equalTo(authKeyId));
170+
171+
final byte[] generatedKeyUsageCA = caPem.getExtensionValue(Extension.keyUsage.getId());
172+
assertThat(generatedKeyUsageCA, notNullValue());
173+
assertThat(Arrays.copyOfRange(generatedKeyUsageCA, 5, generatedKeyUsageCA.length), equalTo(new KeyUsage(keyCertSign | cRLSign).getBytes()));
174+
175+
final byte[] generatedKeyUsageCert = certPem.getExtensionValue(Extension.keyUsage.getId());
176+
assertThat(generatedKeyUsageCert, nullValue());
177+
}
178+
179+
}

components/credentials/src/main/kotlin/org/cloudfoundry/credhub/domain/CertificateGenerationParameters.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,11 @@ import org.cloudfoundry.credhub.requests.CertificateGenerationRequestParameters.
3232
import org.cloudfoundry.credhub.requests.GenerationParameters
3333
import org.cloudfoundry.credhub.utils.CertificateReader
3434
import org.springframework.util.StringUtils
35-
import java.util.Arrays
3635
import java.util.Objects
3736
import javax.security.auth.x500.X500Principal
3837

3938
class CertificateGenerationParameters : GenerationParameters {
40-
val validKeyLengths = Arrays.asList(2048, 3072, 4096)
39+
val validKeyLengths = listOf(2048, 3072, 4096)
4140
var keyLength: Int
4241
var duration: Int
4342
val isSelfSigned: Boolean

0 commit comments

Comments
 (0)