Skip to content

Commit 6c24f60

Browse files
authored
Allow creating non-CA (leaf) certificates (#6657)
Motivation: Previously, the certificate generation logic for both self-signed and signed certificates was hardcoded to set the `isCA` (is Certificate Authority) basic constraint to `true`. This meant that every generated certificate was effectively a CA certificate. Using a CA certificate where a leaf certificate is expected can lead to TLS validation failures in clients or systems with strict Public Key Infrastructure (PKI) policies. Modifications: - Added a new boolean parameter, `isCA`, to the certificate generation methods. Result: - You can now generate both CA and non-CA (leaf) certificates.
1 parent d8c902d commit 6c24f60

6 files changed

Lines changed: 141 additions & 23 deletions

File tree

core/src/main/java/com/linecorp/armeria/internal/common/util/CertificateParams.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,17 @@ final class CertificateParams {
4141
private final Date notAfter;
4242
private final String algorithm;
4343
private final ImmutableList<String> subjectAlternativeNames;
44+
private final boolean isCA;
4445

4546
@Nullable
4647
private final X509Certificate issuerCert;
4748
@Nullable
4849
private final PrivateKey issuerPrivateKey;
4950
private final X500Name issuerName;
5051

52+
// For self-signed certificate
5153
CertificateParams(String fqdn, Random random, int bits, Date notBefore, Date notAfter,
52-
String algorithm, Iterable<String> subjectAlternativeNames) {
54+
String algorithm, Iterable<String> subjectAlternativeNames, boolean isCA) {
5355
this.fqdn = requireNonNull(fqdn, "fqdn");
5456
this.random = requireNonNull(random, "random");
5557
this.bits = bits;
@@ -58,21 +60,17 @@ final class CertificateParams {
5860
this.algorithm = requireNonNull(algorithm, "algorithm");
5961
this.subjectAlternativeNames =
6062
ImmutableList.copyOf(requireNonNull(subjectAlternativeNames, "subjectAlternativeNames"));
63+
this.isCA = isCA;
6164
issuerCert = null;
6265
issuerPrivateKey = null;
6366

6467
ownerName = new X500Name("CN=" + fqdn);
6568
issuerName = ownerName;
6669
}
6770

71+
// For signed certificate
6872
CertificateParams(String fqdn, Random random, int bits, Date notBefore, Date notAfter,
69-
SignedCertificate issuer)
70-
throws CertificateException {
71-
this(fqdn, random, bits, notBefore, notAfter, issuer, ImmutableList.of());
72-
}
73-
74-
CertificateParams(String fqdn, Random random, int bits, Date notBefore, Date notAfter,
75-
SignedCertificate issuer, Iterable<String> subjectAlternativeNames)
73+
SignedCertificate issuer, Iterable<String> subjectAlternativeNames, boolean isCA)
7674
throws CertificateException {
7775
this.fqdn = requireNonNull(fqdn, "fqdn");
7876
ownerName = new X500Name("CN=" + fqdn);
@@ -83,6 +81,7 @@ final class CertificateParams {
8381
this.notAfter = requireNonNull(notAfter, "notAfter");
8482
this.subjectAlternativeNames =
8583
ImmutableList.copyOf(requireNonNull(subjectAlternativeNames, "subjectAlternativeNames"));
84+
this.isCA = isCA;
8685
requireNonNull(issuer, "issuer");
8786
algorithm = issuer.key().getAlgorithm();
8887
issuerCert = issuer.cert();
@@ -133,4 +132,8 @@ X500Name ownerName() {
133132
ImmutableList<String> subjectAlternativeNames() {
134133
return subjectAlternativeNames;
135134
}
135+
136+
boolean isCA() {
137+
return isCA;
138+
}
136139
}

core/src/main/java/com/linecorp/armeria/internal/common/util/SelfSignedCertificate.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,25 @@ public SelfSignedCertificate(String fqdn, Random random, int bits, Date notBefor
217217
public SelfSignedCertificate(String fqdn, Random random, int bits, Date notBefore, Date notAfter,
218218
String algorithm, Iterable<String> subjectAlternativeNames)
219219
throws CertificateException {
220+
this(fqdn, random, bits, notBefore, notAfter, algorithm, subjectAlternativeNames, true);
221+
}
222+
223+
/**
224+
* Creates a new instance.
225+
*
226+
* @param fqdn a fully qualified domain name
227+
* @param random the {@link Random} to use
228+
* @param bits the number of bits of the generated private key
229+
* @param notBefore Certificate is not valid before this time
230+
* @param notAfter Certificate is not valid after this time
231+
* @param algorithm Key pair algorithm
232+
* @param subjectAlternativeNames additional Subject Alternative Names
233+
* @param isCA whether the certificate should be a CA certificate
234+
*/
235+
public SelfSignedCertificate(String fqdn, Random random, int bits, Date notBefore, Date notAfter,
236+
String algorithm, Iterable<String> subjectAlternativeNames, boolean isCA)
237+
throws CertificateException {
220238
super(new CertificateParams(fqdn, random, bits, notBefore, notAfter, algorithm,
221-
subjectAlternativeNames));
239+
subjectAlternativeNames, isCA));
222240
}
223241
}

core/src/main/java/com/linecorp/armeria/internal/common/util/SignedCertificate.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
import org.slf4j.Logger;
7373
import org.slf4j.LoggerFactory;
7474

75+
import com.google.common.collect.ImmutableList;
76+
7577
import com.linecorp.armeria.common.util.Exceptions;
7678

7779
import io.netty.buffer.ByteBuf;
@@ -136,7 +138,7 @@ public class SignedCertificate {
136138
* <p> Algorithm: RSA </p>
137139
*/
138140
public SignedCertificate(SignedCertificate parent) throws CertificateException {
139-
this("localhost", parent);
141+
this("localhost", parent, true);
140142
}
141143

142144
/**
@@ -146,9 +148,7 @@ public SignedCertificate(SignedCertificate parent) throws CertificateException {
146148
* @param fqdn a fully qualified domain name
147149
*/
148150
public SignedCertificate(String fqdn, SignedCertificate parent) throws CertificateException {
149-
this(new CertificateParams(requireNonNull(fqdn, "fqdn"), ThreadLocalRandom.current(),
150-
DEFAULT_KEY_LENGTH_BITS, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER,
151-
requireNonNull(parent, "parent")));
151+
this(fqdn, parent, true);
152152
}
153153

154154
/**
@@ -161,10 +161,38 @@ public SignedCertificate(String fqdn, SignedCertificate parent) throws Certifica
161161
*/
162162
public SignedCertificate(String fqdn, SignedCertificate parent,
163163
Iterable<String> subjectAlternativeNames) throws CertificateException {
164+
this(fqdn, parent, subjectAlternativeNames, true);
165+
}
166+
167+
/**
168+
* Creates a new instance.
169+
* <p> Algorithm: RSA </p>
170+
*
171+
* @param fqdn a fully qualified domain name
172+
* @param parent the parent certificate to sign with
173+
* @param isCA whether the certificate should be a CA certificate
174+
*/
175+
public SignedCertificate(String fqdn, SignedCertificate parent, boolean isCA)
176+
throws CertificateException {
177+
this(fqdn, parent, ImmutableList.of(), isCA);
178+
}
179+
180+
/**
181+
* Creates a new instance.
182+
* <p> Algorithm: RSA </p>
183+
*
184+
* @param fqdn a fully qualified domain name
185+
* @param parent the parent certificate to sign with
186+
* @param subjectAlternativeNames additional Subject Alternative Names
187+
* @param isCA whether the certificate should be a CA certificate
188+
*/
189+
public SignedCertificate(String fqdn, SignedCertificate parent,
190+
Iterable<String> subjectAlternativeNames, boolean isCA)
191+
throws CertificateException {
164192
this(new CertificateParams(requireNonNull(fqdn, "fqdn"), ThreadLocalRandom.current(),
165193
DEFAULT_KEY_LENGTH_BITS, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER,
166194
requireNonNull(parent, "parent"),
167-
requireNonNull(subjectAlternativeNames, "subjectAlternativeNames")));
195+
requireNonNull(subjectAlternativeNames, "subjectAlternativeNames"), isCA));
168196
}
169197

170198
/**
@@ -298,8 +326,7 @@ private static String[] generate(KeyPair keypair, CertificateParams params) thro
298326
builder.addExtension(subjectAlternativeName,
299327
false,
300328
new GeneralNames(names.toArray(new GeneralName[0])));
301-
// certs can sign other certs
302-
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
329+
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(params.isCA()));
303330
if (params.issuerCert() != null) {
304331
final JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
305332
builder.addExtension(Extension.authorityKeyIdentifier, false,

junit5/src/main/java/com/linecorp/armeria/testing/junit5/server/SelfSignedCertificateExtension.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,27 @@ public SelfSignedCertificateExtension(String fqdn, SecureRandom random, int bits
114114
public SelfSignedCertificateExtension(String fqdn, SecureRandom random, int bits,
115115
TemporalAccessor notBefore, TemporalAccessor notAfter,
116116
Iterable<String> subjectAlternativeNames) {
117+
this(fqdn, random, bits, notBefore, notAfter, subjectAlternativeNames, true);
118+
}
119+
120+
/**
121+
* Creates a new instance.
122+
*
123+
* @param fqdn a fully qualified domain name
124+
* @param random the {@link SecureRandom} to use
125+
* @param bits the number of bits of the generated private key
126+
* @param notBefore {@link Certificate} is not valid before this time
127+
* @param notAfter {@link Certificate} is not valid after this time
128+
* @param subjectAlternativeNames additional Subject Alternative Names
129+
* @param isCA whether the certificate should be a CA certificate
130+
*/
131+
public SelfSignedCertificateExtension(String fqdn, SecureRandom random, int bits,
132+
TemporalAccessor notBefore, TemporalAccessor notAfter,
133+
Iterable<String> subjectAlternativeNames, boolean isCA) {
117134
super(() -> new SelfSignedCertificate(fqdn, random, bits,
118135
toDate(requireNonNull(notBefore, "notBefore")),
119136
toDate(requireNonNull(notAfter, "notAfter")), "RSA",
120-
subjectAlternativeNames));
137+
subjectAlternativeNames, isCA));
121138
}
122139

123140
@SuppressWarnings("UseOfObsoleteDateTimeApi")

junit5/src/main/java/com/linecorp/armeria/testing/junit5/server/SignedCertificateExtension.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.junit.jupiter.api.extension.Extension;
2929
import org.junit.jupiter.api.extension.ExtensionContext;
3030

31+
import com.google.common.collect.ImmutableList;
32+
3133
import com.linecorp.armeria.common.TlsKeyPair;
3234
import com.linecorp.armeria.common.annotation.Nullable;
3335
import com.linecorp.armeria.common.annotation.UnstableApi;
@@ -49,9 +51,8 @@ public class SignedCertificateExtension extends AbstractAllOrEachExtension {
4951
/**
5052
* Creates a new instance.
5153
*/
52-
@SuppressWarnings("CopyConstructorMissesField")
5354
public SignedCertificateExtension(SignedCertificateExtension parent) {
54-
this(() -> new SignedCertificate(requireNonNull(parent, "parent").signedCertificate()));
55+
this(parent, true);
5556
}
5657

5758
/**
@@ -60,7 +61,7 @@ public SignedCertificateExtension(SignedCertificateExtension parent) {
6061
* @param fqdn a fully qualified domain name
6162
*/
6263
public SignedCertificateExtension(String fqdn, SignedCertificateExtension parent) {
63-
this(() -> new SignedCertificate(fqdn, requireNonNull(parent, "parent").signedCertificate()));
64+
this(fqdn, parent, ImmutableList.of());
6465
}
6566

6667
/**
@@ -72,8 +73,42 @@ public SignedCertificateExtension(String fqdn, SignedCertificateExtension parent
7273
*/
7374
public SignedCertificateExtension(String fqdn, SignedCertificateExtension parent,
7475
Iterable<String> subjectAlternativeNames) {
76+
this(fqdn, parent, subjectAlternativeNames, true);
77+
}
78+
79+
/**
80+
* Creates a new instance.
81+
*
82+
* @param parent the parent extension to sign with
83+
* @param isCA whether the certificate should be a CA certificate
84+
*/
85+
public SignedCertificateExtension(SignedCertificateExtension parent, boolean isCA) {
86+
this("localhost", parent, isCA);
87+
}
88+
89+
/**
90+
* Creates a new instance.
91+
*
92+
* @param fqdn a fully qualified domain name
93+
* @param parent the parent extension to sign with
94+
* @param isCA whether the certificate should be a CA certificate
95+
*/
96+
public SignedCertificateExtension(String fqdn, SignedCertificateExtension parent, boolean isCA) {
97+
this(fqdn, parent, ImmutableList.of(), isCA);
98+
}
99+
100+
/**
101+
* Creates a new instance.
102+
*
103+
* @param fqdn a fully qualified domain name
104+
* @param parent the parent extension to sign with
105+
* @param subjectAlternativeNames additional Subject Alternative Names
106+
* @param isCA whether the certificate should be a CA certificate
107+
*/
108+
public SignedCertificateExtension(String fqdn, SignedCertificateExtension parent,
109+
Iterable<String> subjectAlternativeNames, boolean isCA) {
75110
this(() -> new SignedCertificate(fqdn, requireNonNull(parent, "parent").signedCertificate(),
76-
subjectAlternativeNames));
111+
subjectAlternativeNames, isCA));
77112
}
78113

79114
SignedCertificateExtension(ThrowingSupplier<SignedCertificate> certificateFactory) {

junit5/src/test/java/com/linecorp/armeria/testing/junit5/server/SignedCertificateExtensionTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ class SignedCertificateExtensionTest {
4444
"*.example.com",
4545
"192.168.1.1",
4646
"spiffe://example.com/service"
47-
)
48-
);
47+
),
48+
false);
49+
50+
@RegisterExtension
51+
@Order(3)
52+
static final SignedCertificateExtension intermediateCa = new SignedCertificateExtension(
53+
"intermediate.example.com", parent, true);
4954

5055
@Test
5156
void signedCertificateWithParentAndSans() throws Exception {
@@ -75,6 +80,19 @@ void signedCertificateWithParentAndSans() throws Exception {
7580
assertThat(dnsNames).contains("service.example.com", "api.example.com", "*.example.com");
7681
assertThat(ipAddresses).contains("192.168.1.1");
7782
assertThat(uris).contains("spiffe://example.com/service");
83+
84+
// Leaf cert must not be a CA
85+
assertThat(cert.getBasicConstraints()).isEqualTo(-1);
86+
// Parent (self-signed root) must be a CA
87+
assertThat(parent.certificate().getBasicConstraints()).isGreaterThanOrEqualTo(0);
88+
}
89+
90+
@Test
91+
void intermediateCaIsSignedByParentAndIsCA() throws Exception {
92+
final X509Certificate cert = intermediateCa.certificate();
93+
cert.verify(parent.certificate().getPublicKey());
94+
// isCA=true → getBasicConstraints() >= 0
95+
assertThat(cert.getBasicConstraints()).isGreaterThanOrEqualTo(0);
7896
}
7997
}
8098

0 commit comments

Comments
 (0)