Skip to content

Commit

Permalink
Set serverAuth extended key usage for generated certificates and CSRs. (
Browse files Browse the repository at this point in the history
#86311)

This commit extends `CertGenUtils` to allow configuring
`ExtendedKeyUsage` extension for generated certificates
and adds `id_kp_serverAuth` (1.3.6.1.5.5.7.3.1) key usage
to places where we know that the certificate is meant only
to be used for server authentication.

Closes #81067
  • Loading branch information
slobodanadamovic committed May 6, 2022
1 parent e3778f7 commit 1bc90ea
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/changelog/86311.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 86311
summary: Set `serverAuth` extended key usage for generated certificates and CSRs
area: TLS
type: enhancement
issues:
- 81067
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

import org.apache.commons.io.FileUtils;
import org.apache.lucene.util.SetOnce;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.cli.ExitCodes;
Expand Down Expand Up @@ -459,7 +461,8 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
httpCaKey,
false,
HTTP_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM
SIGNATURE_ALGORITHM,
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
);

// the HTTP CA PEM file is provided "just in case". The node doesn't use it, but clients (configured manually, outside of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
Expand Down Expand Up @@ -166,6 +167,20 @@ public static X509Certificate generateSignedCertificate(
boolean isCa,
int days,
String signatureAlgorithm
) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, isCa, days, signatureAlgorithm, Set.of());
}

public static X509Certificate generateSignedCertificate(
X500Principal principal,
GeneralNames subjectAltNames,
KeyPair keyPair,
X509Certificate caCert,
PrivateKey caPrivKey,
boolean isCa,
int days,
String signatureAlgorithm,
Set<ExtendedKeyUsage> extendedKeyUsages
) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
Objects.requireNonNull(keyPair, "Key-Pair must not be null");
final ZonedDateTime notBefore = ZonedDateTime.now(ZoneOffset.UTC);
Expand All @@ -182,7 +197,8 @@ public static X509Certificate generateSignedCertificate(
isCa,
notBefore,
notAfter,
signatureAlgorithm
signatureAlgorithm,
extendedKeyUsages
);
}

Expand All @@ -196,6 +212,32 @@ public static X509Certificate generateSignedCertificate(
ZonedDateTime notBefore,
ZonedDateTime notAfter,
String signatureAlgorithm
) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
return generateSignedCertificate(
principal,
subjectAltNames,
keyPair,
caCert,
caPrivKey,
isCa,
notBefore,
notAfter,
signatureAlgorithm,
Set.of()
);
}

public static X509Certificate generateSignedCertificate(
X500Principal principal,
GeneralNames subjectAltNames,
KeyPair keyPair,
X509Certificate caCert,
PrivateKey caPrivKey,
boolean isCa,
ZonedDateTime notBefore,
ZonedDateTime notAfter,
String signatureAlgorithm,
Set<ExtendedKeyUsage> extendedKeyUsages
) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
final BigInteger serial = CertGenUtils.getSerial();
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
Expand Down Expand Up @@ -230,6 +272,12 @@ public static X509Certificate generateSignedCertificate(
}
builder.addExtension(Extension.basicConstraints, isCa, new BasicConstraints(isCa));

if (extendedKeyUsages != null) {
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
builder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
}
}

PrivateKey signingKey = caPrivKey != null ? caPrivKey : keyPair.getPrivate();
ContentSigner signer = new JcaContentSignerBuilder(
(Strings.isNullOrEmpty(signatureAlgorithm)) ? getDefaultSignatureAlgorithm(signingKey) : signatureAlgorithm
Expand Down Expand Up @@ -270,13 +318,41 @@ private static String getDefaultSignatureAlgorithm(PrivateKey key) {
*/
static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) throws IOException,
OperatorCreationException {
return generateCSR(keyPair, principal, sanList, Set.of());
}

/**
* Generates a certificate signing request
*
* @param keyPair the key pair that will be associated by the certificate generated from the certificate signing request
* @param principal the principal of the certificate; commonly referred to as the distinguished name (DN)
* @param sanList the subject alternative names that should be added to the certificate as an X509v3 extension. May be
* {@code null}
* @param extendedKeyUsages the extended key usages that should be added to the certificate as an X509v3 extension. May be empty.
* @return a certificate signing request
*/
static PKCS10CertificationRequest generateCSR(
KeyPair keyPair,
X500Principal principal,
GeneralNames sanList,
Set<ExtendedKeyUsage> extendedKeyUsages
) throws IOException, OperatorCreationException {
Objects.requireNonNull(keyPair, "Key-Pair must not be null");
Objects.requireNonNull(keyPair.getPublic(), "Public-Key must not be null");
Objects.requireNonNull(principal, "Principal must not be null");
Objects.requireNonNull(extendedKeyUsages, "extendedKeyUsages must not be null");
JcaPKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder(principal, keyPair.getPublic());

ExtensionsGenerator extGen = new ExtensionsGenerator();
if (sanList != null) {
ExtensionsGenerator extGen = new ExtensionsGenerator();
extGen.addExtension(Extension.subjectAlternativeName, false, sanList);
}

for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
extGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
}

if (extGen.isEmpty() == false) {
builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extGen.generate());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import joptsimple.OptionSet;

import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
Expand Down Expand Up @@ -69,6 +71,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -332,7 +335,12 @@ private void writeCertificateAndKeyDetails(
// (i.e. show them the certutil cert command that they would need).
if (ca == null) {
// No local CA, generate a CSR instead
final PKCS10CertificationRequest csr = CertGenUtils.generateCSR(keyPair, cert.subject, sanList);
final PKCS10CertificationRequest csr = CertGenUtils.generateCSR(
keyPair,
cert.subject,
sanList,
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
);
final String csrFile = "http-" + cert.name + ".csr";
final String keyFile = "http-" + cert.name + ".key";
final String certName = "http-" + cert.name + ".crt";
Expand Down Expand Up @@ -363,7 +371,8 @@ private void writeCertificateAndKeyDetails(
false,
notBefore,
notAfter,
null
null,
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
);

final String p12Name = "http.p12";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.apache.commons.io.FileUtils;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.common.network.NetworkService;
import org.elasticsearch.common.settings.KeyStoreWrapper;
Expand Down Expand Up @@ -162,6 +163,7 @@ public void testGeneratedHTTPCertificateSANs() throws Exception {
assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false));
verifyExtendedKeyUsage(httpCertificate);
} finally {
deleteDirectory(tempDir);
}
Expand All @@ -183,6 +185,7 @@ public void testGeneratedHTTPCertificateSANs() throws Exception {
assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false));
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(true));
verifyExtendedKeyUsage(httpCertificate);
} finally {
deleteDirectory(tempDir);
}
Expand All @@ -208,6 +211,7 @@ public void testGeneratedHTTPCertificateSANs() throws Exception {
assertThat(checkGeneralNameSan(httpCertificate, "balkan.beast", GeneralName.dNSName), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false));
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false));
verifyExtendedKeyUsage(httpCertificate);
} finally {
deleteDirectory(tempDir);
}
Expand Down Expand Up @@ -259,6 +263,13 @@ private boolean checkGeneralNameSan(X509Certificate certificate, String generalN
return false;
}

private void verifyExtendedKeyUsage(X509Certificate httpCertificate) throws Exception {
List<String> extendedKeyUsage = httpCertificate.getExtendedKeyUsage();
assertEquals("Only one extended key usage expected for HTTP certificate.", 1, extendedKeyUsage.size());
String expectedServerAuthUsage = KeyPurposeId.id_kp_serverAuth.toASN1Primitive().toString();
assertEquals("Expected serverAuth extended key usage.", expectedServerAuthUsage, extendedKeyUsage.get(0));
}

private X509Certificate runAutoConfigAndReturnHTTPCertificate(Path configDir, Settings settings) throws Exception {
final Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", configDir).put(settings).build());
// runs the command to auto-generate the config files and the keystore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

package org.elasticsearch.xpack.security.cli;

import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.core.SuppressForbidden;
Expand All @@ -25,6 +27,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
Expand Down Expand Up @@ -156,7 +159,8 @@ public void testIssuerCertSubjectDN() throws Exception {
true,
notBefore,
notAfter,
null
null,
Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage))
);

final X509Certificate[] certChain = new X509Certificate[] { endEntityCert, subCaCert, rootCaCert };
Expand All @@ -166,6 +170,9 @@ public void testIssuerCertSubjectDN() throws Exception {
assertThat(subCaCert.getIssuerX500Principal(), equalTo(rootCaCert.getSubjectX500Principal()));
assertThat(rootCaCert.getIssuerX500Principal(), equalTo(rootCaCert.getSubjectX500Principal()));

// verify custom extended key usage
assertThat(endEntityCert.getExtendedKeyUsage(), equalTo(List.of(KeyPurposeId.anyExtendedKeyUsage.toASN1Primitive().toString())));

// verify cert chaining based on PKIX rules (ex: SubjectDNs/IssuerDNs, SKIs/AKIs, BC, KU, EKU, etc)
final KeyStore trustStore = KeyStore.getInstance("PKCS12", "SunJSSE"); // EX: SunJSSE, BC, BC-FIPS
trustStore.load(null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.pkcs.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.bouncycastle.util.io.pem.PemObject;
Expand Down Expand Up @@ -88,6 +90,7 @@
import static org.elasticsearch.test.FileMatchers.isRegularFile;
import static org.elasticsearch.test.FileMatchers.pathExists;
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.guessFileType;
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
Expand Down Expand Up @@ -687,9 +690,11 @@ private void verifyCertificationRequest(
assertThat(extensionAttributes[0].getAttributeValues(), arrayWithSize(1));
assertThat(extensionAttributes[0].getAttributeValues()[0], instanceOf(DLSequence.class));

// We register 1 extension - the subject alternative names
// We register 1 extension with the subject alternative names and extended key usage
final Extensions extensions = Extensions.getInstance(extensionAttributes[0].getAttributeValues()[0]);
assertThat(extensions, notNullValue());
assertThat(extensions.getExtensionOIDs(), arrayWithSize(2));

final GeneralNames names = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName);
assertThat(names.getNames(), arrayWithSize(hostNames.size() + ipAddresses.size()));
for (GeneralName name : names.getNames()) {
Expand All @@ -702,6 +707,9 @@ private void verifyCertificationRequest(
assertThat(ip, in(ipAddresses));
}
}

ExtendedKeyUsage extendedKeyUsage = ExtendedKeyUsage.fromExtensions(extensions);
assertThat(extendedKeyUsage.getUsages(), arrayContainingInAnyOrder(KeyPurposeId.id_kp_serverAuth));
}

private void verifyCertificate(
Expand Down

0 comments on commit 1bc90ea

Please sign in to comment.