Bläddra i källkod

Set `keyUsage` for generated HTTP certificates and self-signed CA (#126376)

The `elasticsearch-certutil http` command, and security auto-configuration, 
generate the HTTP certificate and CA without setting the `keyUsage` extension.

This PR fixes this by setting (by default):
- `keyCertSign` and `cRLSign` for self-signed CAs 
- `digitalSignature` and `keyEncipherment` for HTTP certificates and CSRs

These defaults can be overridden when running `elasticsearch-certutil http` 
command. The user will be prompted to change them as they wish.

For `elasticsearch-certutil ca`, the default value can be overridden by passing 
the `--keysage` option, e.g.
```
elasticsearch-certutil ca --keyusage "digitalSignature,keyCertSign,cRLSign" -pem    
```

Fixes #117769
Slobodan Adamović 6 månader sedan
förälder
incheckning
284121ad9f

+ 6 - 0
docs/changelog/126376.yaml

@@ -0,0 +1,6 @@
+pr: 126376
+summary: Set `keyUsage` for generated HTTP certificates and self-signed CA
+area: TLS
+type: bug
+issues:
+ - 117769

+ 4 - 1
docs/reference/elasticsearch/command-line-tools/certutil.md

@@ -13,7 +13,7 @@ The `elasticsearch-certutil` command simplifies the creation of certificates for
 ```shell
 bin/elasticsearch-certutil
 (
-(ca [--ca-dn <name>] [--days <n>] [--pem])
+(ca [--ca-dn <name>] [--keyusage <key_usages>] [--days <n>] [--pem])
 
 | (cert ([--ca <file_path>] | [--ca-cert <file_path> --ca-key <file_path>])
 [--ca-dn <name>] [--ca-pass <password>] [--days <n>]
@@ -105,6 +105,9 @@ The `http` mode guides you through the process of generating certificates for us
 `--ca-pass <password>`
 :   Specifies the password for an existing CA private key or the generated CA private key. This parameter is only applicable to the `cert` parameter
 
+`--keyusage <key_usages>`
+:   Specifies a comma-separated list of key usage restrictions (as per RFC 5280) that are used for the generated CA certificate. The default value is `keyCertSign,cRLSign`. This parameter may only be used with the `ca` parameter.
+
 `--days <n>`
 :   Specifies an integer value that represents the number of days the generated certificates are valid. The default value is `1095`. This parameter cannot be used with the `csr` or `http` parameters.
 

+ 13 - 3
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java

@@ -102,6 +102,9 @@ import static org.elasticsearch.common.ssl.PemUtils.parsePKCS8PemString;
 import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
 import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
 import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL;
+import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
+import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
+import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE;
 
 /**
  * Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
@@ -411,7 +414,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
                     null,
                     true,
                     TRANSPORT_CA_CERTIFICATE_DAYS,
-                    SIGNATURE_ALGORITHM
+                    SIGNATURE_ALGORITHM,
+                    null,
+                    Set.of()
                 );
                 // transport key/certificate
                 final KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
@@ -424,7 +429,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
                     transportCaKey,
                     false,
                     TRANSPORT_CERTIFICATE_DAYS,
-                    SIGNATURE_ALGORITHM
+                    SIGNATURE_ALGORITHM,
+                    null,
+                    Set.of()
                 );
 
                 final KeyPair httpCaKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
@@ -438,7 +445,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
                     null,
                     true,
                     HTTP_CA_CERTIFICATE_DAYS,
-                    SIGNATURE_ALGORITHM
+                    SIGNATURE_ALGORITHM,
+                    buildKeyUsage(DEFAULT_CA_KEY_USAGE),
+                    Set.of()
                 );
             } catch (Throwable t) {
                 try {
@@ -464,6 +473,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
                 false,
                 HTTP_CERTIFICATE_DAYS,
                 SIGNATURE_ALGORITHM,
+                buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
                 Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
             );
 

+ 70 - 47
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java

@@ -20,6 +20,7 @@ import org.bouncycastle.asn1.x509.Extension;
 import org.bouncycastle.asn1.x509.ExtensionsGenerator;
 import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyUsage;
 import org.bouncycastle.asn1.x509.Time;
 import org.bouncycastle.cert.CertIOException;
 import org.bouncycastle.cert.X509CertificateHolder;
@@ -53,10 +54,14 @@ import java.security.cert.X509Certificate;
 import java.sql.Date;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.TreeMap;
 
 import javax.net.ssl.X509ExtendedKeyManager;
 import javax.net.ssl.X509ExtendedTrustManager;
@@ -73,14 +78,33 @@ public class CertGenUtils {
     private static final int SERIAL_BIT_LENGTH = 20 * 8;
     private static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider();
 
+    /**
+     * The mapping of key usage names to their corresponding integer values as defined in {@code KeyUsage} class.
+     */
+    public static final Map<String, Integer> KEY_USAGE_MAPPINGS = Collections.unmodifiableMap(
+        new TreeMap<>(
+            Map.ofEntries(
+                Map.entry("digitalSignature", KeyUsage.digitalSignature),
+                Map.entry("nonRepudiation", KeyUsage.nonRepudiation),
+                Map.entry("keyEncipherment", KeyUsage.keyEncipherment),
+                Map.entry("dataEncipherment", KeyUsage.dataEncipherment),
+                Map.entry("keyAgreement", KeyUsage.keyAgreement),
+                Map.entry("keyCertSign", KeyUsage.keyCertSign),
+                Map.entry("cRLSign", KeyUsage.cRLSign),
+                Map.entry("encipherOnly", KeyUsage.encipherOnly),
+                Map.entry("decipherOnly", KeyUsage.decipherOnly)
+            )
+        )
+    );
+
     private CertGenUtils() {}
 
     /**
      * Generates a CA certificate
      */
-    public static X509Certificate generateCACertificate(X500Principal x500Principal, KeyPair keyPair, int days)
+    public static X509Certificate generateCACertificate(X500Principal x500Principal, KeyPair keyPair, int days, KeyUsage keyUsage)
         throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
-        return generateSignedCertificate(x500Principal, null, keyPair, null, null, true, days, null);
+        return generateSignedCertificate(x500Principal, null, keyPair, null, null, true, days, null, keyUsage, Set.of());
     }
 
     /**
@@ -107,7 +131,7 @@ public class CertGenUtils {
         PrivateKey caPrivKey,
         int days
     ) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
-        return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, null);
+        return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, null, null, Set.of());
     }
 
     /**
@@ -123,54 +147,14 @@ public class CertGenUtils {
      *                           certificate
      * @param caPrivKey          the CA private key. If {@code null}, this results in a self signed
      *                           certificate
-     * @param days               no of days certificate will be valid from now
-     * @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
-     *                           empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
-     * @return a signed {@link X509Certificate}
-     */
-    public static X509Certificate generateSignedCertificate(
-        X500Principal principal,
-        GeneralNames subjectAltNames,
-        KeyPair keyPair,
-        X509Certificate caCert,
-        PrivateKey caPrivKey,
-        int days,
-        String signatureAlgorithm
-    ) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
-        return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, signatureAlgorithm);
-    }
-
-    /**
-     * Generates a signed certificate
-     *
-     * @param principal          the principal of the certificate; commonly referred to as the
-     *                           distinguished name (DN)
-     * @param subjectAltNames    the subject alternative names that should be added to the
-     *                           certificate as an X509v3 extension. May be {@code null}
-     * @param keyPair            the key pair that will be associated with the certificate
-     * @param caCert             the CA certificate. If {@code null}, this results in a self signed
-     *                           certificate
-     * @param caPrivKey          the CA private key. If {@code null}, this results in a self signed
-     *                           certificate
      * @param isCa               whether or not the generated certificate is a CA
      * @param days               no of days certificate will be valid from now
      * @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
      *                           empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
+     * @param keyUsage          the key usage that should be added to the certificate as a X509v3 extension (can be {@code null})
+     * @param extendedKeyUsages the extended key usages that should be added to the certificate as a X509v3 extension (can be empty)
      * @return a signed {@link X509Certificate}
      */
-    public static X509Certificate generateSignedCertificate(
-        X500Principal principal,
-        GeneralNames subjectAltNames,
-        KeyPair keyPair,
-        X509Certificate caCert,
-        PrivateKey caPrivKey,
-        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,
@@ -180,6 +164,7 @@ public class CertGenUtils {
         boolean isCa,
         int days,
         String signatureAlgorithm,
+        KeyUsage keyUsage,
         Set<ExtendedKeyUsage> extendedKeyUsages
     ) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
         Objects.requireNonNull(keyPair, "Key-Pair must not be null");
@@ -198,6 +183,7 @@ public class CertGenUtils {
             notBefore,
             notAfter,
             signatureAlgorithm,
+            keyUsage,
             extendedKeyUsages
         );
     }
@@ -223,6 +209,7 @@ public class CertGenUtils {
             notBefore,
             notAfter,
             signatureAlgorithm,
+            null,
             Set.of()
         );
     }
@@ -237,6 +224,7 @@ public class CertGenUtils {
         ZonedDateTime notBefore,
         ZonedDateTime notAfter,
         String signatureAlgorithm,
+        KeyUsage keyUsage,
         Set<ExtendedKeyUsage> extendedKeyUsages
     ) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
         final BigInteger serial = CertGenUtils.getSerial();
@@ -272,6 +260,11 @@ public class CertGenUtils {
         }
         builder.addExtension(Extension.basicConstraints, isCa, new BasicConstraints(isCa));
 
+        if (keyUsage != null) {
+            // as per RFC 5280 (section 4.2.1.3), if the key usage is present, then it SHOULD be marked as critical.
+            final boolean isCritical = true;
+            builder.addExtension(Extension.keyUsage, isCritical, keyUsage);
+        }
         if (extendedKeyUsages != null) {
             for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
                 builder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
@@ -318,7 +311,7 @@ public class CertGenUtils {
      */
     static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) throws IOException,
         OperatorCreationException {
-        return generateCSR(keyPair, principal, sanList, Set.of());
+        return generateCSR(keyPair, principal, sanList, null, Set.of());
     }
 
     /**
@@ -335,6 +328,7 @@ public class CertGenUtils {
         KeyPair keyPair,
         X500Principal principal,
         GeneralNames sanList,
+        KeyUsage keyUsage,
         Set<ExtendedKeyUsage> extendedKeyUsages
     ) throws IOException, OperatorCreationException {
         Objects.requireNonNull(keyPair, "Key-Pair must not be null");
@@ -347,7 +341,9 @@ public class CertGenUtils {
         if (sanList != null) {
             extGen.addExtension(Extension.subjectAlternativeName, false, sanList);
         }
-
+        if (keyUsage != null) {
+            extGen.addExtension(Extension.keyUsage, true, keyUsage);
+        }
         for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
             extGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
         }
@@ -430,4 +426,31 @@ public class CertGenUtils {
     public static String buildDnFromDomain(String domain) {
         return "DC=" + domain.replace(".", ",DC=");
     }
+
+    public static KeyUsage buildKeyUsage(Collection<String> keyUsages) {
+        if (keyUsages == null || keyUsages.isEmpty()) {
+            return null;
+        }
+
+        int usageBits = 0;
+        for (String keyUsageName : keyUsages) {
+            Integer keyUsageValue = findKeyUsageByName(keyUsageName);
+            if (keyUsageValue == null) {
+                throw new IllegalArgumentException("Unknown keyUsage: " + keyUsageName);
+            }
+            usageBits |= keyUsageValue;
+        }
+        return new KeyUsage(usageBits);
+    }
+
+    public static boolean isValidKeyUsage(String keyUsage) {
+        return findKeyUsageByName(keyUsage) != null;
+    }
+
+    private static Integer findKeyUsageByName(String keyUsageName) {
+        if (keyUsageName == null) {
+            return null;
+        }
+        return KEY_USAGE_MAPPINGS.get(keyUsageName.trim());
+    }
 }

+ 1 - 1
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java

@@ -403,7 +403,7 @@ class CertificateGenerateTool extends EnvironmentAwareCommand {
         // generate the CA keys and cert
         X500Principal x500Principal = new X500Principal(dn);
         KeyPair keyPair = CertGenUtils.generateKeyPair(keysize);
-        Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, days);
+        Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, days, null);
         final char[] password;
         if (prompt) {
             password = terminal.readSecret("Enter password for CA private key: ");

+ 34 - 4
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java

@@ -15,6 +15,7 @@ import joptsimple.OptionSpecBuilder;
 import org.bouncycastle.asn1.DERIA5String;
 import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyUsage;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.openssl.PEMEncryptor;
 import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
@@ -110,6 +111,7 @@ class CertificateTool extends MultiCommand {
         "[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1," + MAX_FILENAME_LENGTH + "}"
     );
     private static final int DEFAULT_KEY_SIZE = 2048;
+    static final List<String> DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign");
 
     // Older versions of OpenSSL had a max internal password length.
     // We issue warnings when writing files with passwords that would not be usable in those versions of OpenSSL.
@@ -202,6 +204,7 @@ class CertificateTool extends MultiCommand {
         final OptionSpec<String> outputPathSpec;
         final OptionSpec<String> outputPasswordSpec;
         final OptionSpec<Integer> keysizeSpec;
+        OptionSpec<String> caKeyUsageSpec;
 
         OptionSpec<Void> pemFormatSpec;
         OptionSpec<Integer> daysSpec;
@@ -274,6 +277,16 @@ class CertificateTool extends MultiCommand {
             inputFileSpec = parser.accepts("in", "file containing details of the instances in yaml format").withRequiredArg();
         }
 
+        final void acceptCertificateAuthorityKeyUsage() {
+            caKeyUsageSpec = parser.accepts(
+                "keyusage",
+                "comma separated key usages to use for the generated CA. "
+                    + "defaults to '"
+                    + Strings.collectionToCommaDelimitedString(DEFAULT_CA_KEY_USAGE)
+                    + "'"
+            ).withRequiredArg();
+        }
+
         // For testing
         OptionParser getParser() {
             return parser;
@@ -309,6 +322,23 @@ class CertificateTool extends MultiCommand {
             }
         }
 
+        final List<String> getCaKeyUsage(OptionSet options) {
+            if (options.has(caKeyUsageSpec)) {
+                final Function<String, Stream<? extends String>> splitByComma = v -> Stream.of(Strings.splitStringByCommaToArray(v));
+                final List<String> caKeyUsage = caKeyUsageSpec.values(options)
+                    .stream()
+                    .flatMap(splitByComma)
+                    .filter(v -> false == Strings.isNullOrEmpty(v))
+                    .toList();
+                if (caKeyUsage.isEmpty()) {
+                    return DEFAULT_CA_KEY_USAGE;
+                }
+                return caKeyUsage;
+            } else {
+                return DEFAULT_CA_KEY_USAGE;
+            }
+        }
+
         final int getDays(OptionSet options) {
             if (options.has(daysSpec)) {
                 return daysSpec.value(options);
@@ -396,7 +426,8 @@ class CertificateTool extends MultiCommand {
             }
             X500Principal x500Principal = new X500Principal(dn);
             KeyPair keyPair = CertGenUtils.generateKeyPair(getKeySize(options));
-            X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options));
+            final KeyUsage caKeyUsage = CertGenUtils.buildKeyUsage(getCaKeyUsage(options));
+            X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options), caKeyUsage);
 
             if (options.hasArgument(caPasswordSpec)) {
                 char[] password = getChars(caPasswordSpec.value(options));
@@ -933,9 +964,7 @@ class CertificateTool extends MultiCommand {
                     keyPair,
                     null,
                     null,
-                    false,
-                    days,
-                    null
+                    days
                 );
             }
             return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate());
@@ -949,6 +978,7 @@ class CertificateTool extends MultiCommand {
             super("generate a new local certificate authority");
             acceptCertificateGenerationOptions();
             acceptsCertificateAuthorityName();
+            acceptCertificateAuthorityKeyUsage();
             super.caPasswordSpec = super.outputPasswordSpec;
         }
 

+ 97 - 5
x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java

@@ -68,6 +68,8 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -80,7 +82,9 @@ import java.util.zip.ZipOutputStream;
 
 import javax.security.auth.x500.X500Principal;
 
+import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
 import static org.elasticsearch.xpack.security.cli.CertGenUtils.generateSignedCertificate;
+import static org.elasticsearch.xpack.security.cli.CertGenUtils.isValidKeyUsage;
 
 /**
  * This command is the "elasticsearch-certutil http" command. It provides a guided process for creating
@@ -95,7 +99,8 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
     static final X500Principal DEFAULT_CA_NAME = new X500Principal("CN=Elasticsearch HTTP CA");
     static final int DEFAULT_CA_KEY_SIZE = DEFAULT_CERT_KEY_SIZE;
     static final Period DEFAULT_CA_VALIDITY = DEFAULT_CERT_VALIDITY;
-
+    static final List<String> DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign");
+    static final List<String> DEFAULT_CERT_KEY_USAGE = List.of("digitalSignature", "keyEncipherment");
     private static final String ES_README_CSR = "es-readme-csr.txt";
     private static final String ES_YML_CSR = "es-sample-csr.yml";
     private static final String ES_README_P12 = "es-readme-p12.txt";
@@ -133,14 +138,24 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
         final List<String> dnsNames;
         final List<String> ipNames;
         final int keySize;
+        final List<String> keyUsage;
         final Period validity;
 
-        private CertOptions(String name, X500Principal subject, List<String> dnsNames, List<String> ipNames, int keySize, Period validity) {
+        private CertOptions(
+            String name,
+            X500Principal subject,
+            List<String> dnsNames,
+            List<String> ipNames,
+            int keySize,
+            List<String> keyUsage,
+            Period validity
+        ) {
             this.name = name;
             this.subject = subject;
             this.dnsNames = dnsNames;
             this.ipNames = ipNames;
             this.keySize = keySize;
+            this.keyUsage = keyUsage;
             this.validity = validity;
         }
     }
@@ -194,6 +209,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
             terminal.println(Terminal.Verbosity.VERBOSE, "\tDNS Names: " + Strings.collectionToCommaDelimitedString(cert.dnsNames));
             terminal.println(Terminal.Verbosity.VERBOSE, "\tIP Names: " + Strings.collectionToCommaDelimitedString(cert.ipNames));
             terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Size: " + cert.keySize);
+            terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Usage: " + Strings.collectionToCommaDelimitedString(cert.keyUsage));
             terminal.println(Terminal.Verbosity.VERBOSE, "\tValidity: " + toString(cert.validity));
             certificates.add(cert);
 
@@ -339,6 +355,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
                     keyPair,
                     cert.subject,
                     sanList,
+                    buildKeyUsage(cert.keyUsage),
                     Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
                 );
                 final String csrFile = "http-" + cert.name + ".csr";
@@ -372,6 +389,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
                     notBefore,
                     notAfter,
                     null,
+                    buildKeyUsage(cert.keyUsage),
                     Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
                 );
 
@@ -692,10 +710,12 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
         }
         X500Principal dn = buildDistinguishedName(certName);
         int keySize = DEFAULT_CERT_KEY_SIZE;
+        List<String> keyUsage = DEFAULT_CERT_KEY_USAGE;
         while (true) {
             terminal.println(Terminal.Verbosity.SILENT, "Key Name: " + certName);
             terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
             terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
+            terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
             terminal.println(Terminal.Verbosity.SILENT, "");
             if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
                 break;
@@ -736,9 +756,22 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
 
             keySize = readKeySize(terminal, keySize);
             terminal.println("");
+
+            printHeader("What key usage should your certificate have?", terminal);
+            terminal.println("The key usage extension defines the purpose of the key contained in the certificate.");
+            terminal.println("The usage restriction might be employed when a key, that could be used for more than ");
+            terminal.println("one operation, is to be restricted.");
+            terminal.println("You may enter the key usage as a comma-delimited list of following values: ");
+            for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) {
+                terminal.println(" - " + keyUsageName);
+            }
+            terminal.println("");
+
+            keyUsage = readKeyUsage(terminal, keyUsage);
+            terminal.println("");
         }
 
-        return new CertOptions(certName, dn, dnsNames, ipNames, keySize, validity);
+        return new CertOptions(certName, dn, dnsNames, ipNames, keySize, keyUsage, validity);
     }
 
     private static String validateHostname(String name) {
@@ -859,10 +892,12 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
         X500Principal dn = DEFAULT_CA_NAME;
         Period validity = DEFAULT_CA_VALIDITY;
         int keySize = DEFAULT_CA_KEY_SIZE;
+        List<String> keyUsage = DEFAULT_CA_KEY_USAGE;
         while (true) {
             terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
             terminal.println(Terminal.Verbosity.SILENT, "Validity: " + toString(validity));
             terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
+            terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
             terminal.println(Terminal.Verbosity.SILENT, "");
             if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
                 break;
@@ -904,13 +939,38 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
 
             keySize = readKeySize(terminal, keySize);
             terminal.println("");
+
+            printHeader("What key usage should your CA have?", terminal);
+            terminal.println("The key usage extension defines the purpose of the key contained in the certificate.");
+            terminal.println("The usage restriction might be employed when a key, that could be used for more than ");
+            terminal.println("one operation, is to be restricted.");
+            terminal.println("You may enter the key usage as a comma-delimited list of following values: ");
+            for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) {
+                terminal.println(" - " + keyUsageName);
+            }
+            terminal.println("");
+
+            keyUsage = readKeyUsage(terminal, keyUsage);
+            terminal.println("");
         }
 
         try {
             final KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
             final ZonedDateTime notBefore = ZonedDateTime.now(ZoneOffset.UTC);
             final ZonedDateTime notAfter = notBefore.plus(validity);
-            X509Certificate caCert = generateSignedCertificate(dn, null, keyPair, null, null, true, notBefore, notAfter, null);
+            X509Certificate caCert = generateSignedCertificate(
+                dn,
+                null,
+                keyPair,
+                null,
+                null,
+                true,
+                notBefore,
+                notAfter,
+                null,
+                buildKeyUsage(keyUsage),
+                Set.of()
+            );
 
             printHeader("CA password", terminal);
             terminal.println("We recommend that you protect your CA private key with a strong password.");
@@ -979,6 +1039,31 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
         });
     }
 
+    private static List<String> readKeyUsage(Terminal terminal, List<String> defaultKeyUsage) {
+        return tryReadInput(terminal, "Key Usage", defaultKeyUsage, input -> {
+            final String[] keyUsages = input.split(",");
+            final List<String> resolvedKeyUsages = new ArrayList<>(keyUsages.length);
+            for (String keyUsage : keyUsages) {
+                keyUsage = keyUsage.trim();
+                if (keyUsage.isEmpty()) {
+                    terminal.println("Key usage cannot be blank or empty");
+                    return null;
+                }
+                if (isValidKeyUsage(keyUsage) == false) {
+                    terminal.println("Invalid key usage: " + keyUsage);
+                    terminal.println("The key usage should be one of the following values: ");
+                    for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) {
+                        terminal.println(" - " + keyUsageName);
+                    }
+                    terminal.println("");
+                    return null;
+                }
+                resolvedKeyUsages.add(keyUsage);
+            }
+            return Collections.unmodifiableList(resolvedKeyUsages);
+        });
+    }
+
     private static char[] readPassword(Terminal terminal, String prompt, boolean confirm) {
         while (true) {
             final char[] password = terminal.readSecret(prompt + " [<ENTER> for none]");
@@ -1080,7 +1165,14 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
     }
 
     private static <T> T tryReadInput(Terminal terminal, String prompt, T defaultValue, Function<String, T> parser) {
-        final String defaultStr = defaultValue instanceof Period ? toString((Period) defaultValue) : String.valueOf(defaultValue);
+        final String defaultStr;
+        if (defaultValue instanceof Period) {
+            defaultStr = toString((Period) defaultValue);
+        } else if (defaultValue instanceof Collection<?> collection) {
+            defaultStr = Strings.collectionToCommaDelimitedString(collection);
+        } else {
+            defaultStr = String.valueOf(defaultValue);
+        }
         while (true) {
             final String input = terminal.readText(prompt + " [" + defaultStr + "] ");
             if (Strings.isEmpty(input)) {

+ 7 - 5
x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java

@@ -37,6 +37,7 @@ import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.AUTO_CONFIG
 import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.AUTO_CONFIG_TRANSPORT_ALT_DN;
 import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.anyRemoteHostNodeAddress;
 import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.removePreviousAutoconfiguration;
+import static org.elasticsearch.xpack.security.cli.CertGenUtilsTests.assertExpectedKeyUsage;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 
@@ -149,7 +150,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
         }
     }
 
-    public void testGeneratedHTTPCertificateSANs() throws Exception {
+    public void testGeneratedHTTPCertificateSANsAndKeyUsage() throws Exception {
         // test no publish settings
         Path tempDir = createTempDir();
         try {
@@ -180,7 +181,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
             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);
+            verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
         } finally {
             deleteDirectory(tempDir);
         }
@@ -202,7 +203,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
             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);
+            verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
         } finally {
             deleteDirectory(tempDir);
         }
@@ -228,7 +229,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
             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);
+            verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
         } finally {
             deleteDirectory(tempDir);
         }
@@ -288,11 +289,12 @@ public class AutoConfigureNodeTests extends ESTestCase {
         return false;
     }
 
-    private void verifyExtendedKeyUsage(X509Certificate httpCertificate) throws Exception {
+    private void verifyKeyUsageAndExtendedKeyUsage(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));
+        assertExpectedKeyUsage(httpCertificate, HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE);
     }
 
     private X509Certificate runAutoConfigAndReturnHTTPCertificate(Path configDir, Settings settings) throws Exception {

+ 143 - 2
x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java

@@ -7,10 +7,13 @@
 
 package org.elasticsearch.xpack.security.cli;
 
+import com.unboundid.util.ssl.cert.KeyUsageExtension;
+
 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.asn1.x509.KeyUsage;
 import org.elasticsearch.common.network.InetAddresses;
 import org.elasticsearch.common.network.NetworkAddress;
 import org.elasticsearch.core.SuppressForbidden;
@@ -27,22 +30,57 @@ import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.security.auth.x500.X500Principal;
 
+import static org.elasticsearch.xpack.security.cli.CertGenUtils.KEY_USAGE_MAPPINGS;
+import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
+import static org.elasticsearch.xpack.security.cli.CertGenUtils.isValidKeyUsage;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
 
 /**
  * Unit tests for cert utils
  */
 public class CertGenUtilsTests extends ESTestCase {
 
+    /**
+     * The mapping of key usage names to their corresponding bit index as defined in {@code KeyUsage} class:
+     *
+     * <ul>
+     * <li>digitalSignature        (0)</li>
+     * <li>nonRepudiation          (1)</li>
+     * <li>keyEncipherment         (2)</li>
+     * <li>dataEncipherment        (3)</li>
+     * <li>keyAgreement            (4)</li>
+     * <li>keyCertSign             (5)</li>
+     * <li>cRLSign                 (6)</li>
+     * <li>encipherOnly            (7)</li>
+     * <li>decipherOnly            (8)</li>
+     * </ul>
+     */
+    private static final Map<String, Integer> KEY_USAGE_BITS = Map.ofEntries(
+        Map.entry("digitalSignature", 0),
+        Map.entry("nonRepudiation", 1),
+        Map.entry("keyEncipherment", 2),
+        Map.entry("dataEncipherment", 3),
+        Map.entry("keyAgreement", 4),
+        Map.entry("keyCertSign", 5),
+        Map.entry("cRLSign", 6),
+        Map.entry("encipherOnly", 7),
+        Map.entry("decipherOnly", 8)
+    );
+
     @BeforeClass
     public static void muteInFips() {
         assumeFalse("Can't run in a FIPS JVM", inFipsJvm());
@@ -103,6 +141,7 @@ public class CertGenUtilsTests extends ESTestCase {
         // root CA
         final X500Principal rootCaPrincipal = new X500Principal("DC=example.com");
         final KeyPair rootCaKeyPair = CertGenUtils.generateKeyPair(2048);
+        final List<String> rootCaKeyUsages = List.of("keyCertSign", "cRLSign");
         final X509Certificate rootCaCert = CertGenUtils.generateSignedCertificate(
             rootCaPrincipal,
             null,
@@ -112,12 +151,15 @@ public class CertGenUtilsTests extends ESTestCase {
             true,
             notBefore,
             notAfter,
-            null
+            null,
+            buildKeyUsage(rootCaKeyUsages),
+            Set.of()
         );
 
         // sub CA
         final X500Principal subCaPrincipal = new X500Principal("DC=Sub CA,DC=example.com");
         final KeyPair subCaKeyPair = CertGenUtils.generateKeyPair(2048);
+        final List<String> subCaKeyUsage = List.of("digitalSignature", "keyCertSign", "cRLSign");
         final X509Certificate subCaCert = CertGenUtils.generateSignedCertificate(
             subCaPrincipal,
             null,
@@ -127,12 +169,15 @@ public class CertGenUtilsTests extends ESTestCase {
             true,
             notBefore,
             notAfter,
-            null
+            null,
+            buildKeyUsage(subCaKeyUsage),
+            Set.of()
         );
 
         // end entity
         final X500Principal endEntityPrincipal = new X500Principal("CN=TLS Client\\+Server,DC=Sub CA,DC=example.com");
         final KeyPair endEntityKeyPair = CertGenUtils.generateKeyPair(2048);
+        final List<String> endEntityKeyUsage = randomBoolean() ? null : List.of("digitalSignature", "keyEncipherment");
         final X509Certificate endEntityCert = CertGenUtils.generateSignedCertificate(
             endEntityPrincipal,
             null,
@@ -143,6 +188,7 @@ public class CertGenUtilsTests extends ESTestCase {
             notBefore,
             notAfter,
             null,
+            buildKeyUsage(endEntityKeyUsage),
             Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage))
         );
 
@@ -162,6 +208,101 @@ public class CertGenUtilsTests extends ESTestCase {
         trustStore.setCertificateEntry("trustAnchor", rootCaCert); // anchor: any part of the chain, or issuer of last entry in chain
 
         validateEndEntityTlsChain(trustStore, certChain, true, true);
+
+        // verify custom key usages
+        assertExpectedKeyUsage(rootCaCert, rootCaKeyUsages);
+        assertExpectedKeyUsage(subCaCert, subCaKeyUsage);
+        // when key usage is not specified, the key usage bits should be null
+        if (endEntityKeyUsage == null) {
+            assertThat(endEntityCert.getKeyUsage(), is(nullValue()));
+            assertThat(endEntityCert.getCriticalExtensionOIDs().contains(KeyUsageExtension.KEY_USAGE_OID.toString()), is(false));
+        } else {
+            assertExpectedKeyUsage(endEntityCert, endEntityKeyUsage);
+        }
+
+    }
+
+    public void testBuildKeyUsage() {
+        // sanity check that lookup maps are containing the same keyUsage entries
+        assertThat(KEY_USAGE_BITS.keySet(), containsInAnyOrder(KEY_USAGE_MAPPINGS.keySet().toArray()));
+
+        // passing null or empty list of keyUsage names should return null
+        assertThat(buildKeyUsage(null), is(nullValue()));
+        assertThat(buildKeyUsage(List.of()), is(nullValue()));
+
+        // invalid names should throw IAE
+        var e = expectThrows(IllegalArgumentException.class, () -> buildKeyUsage(List.of(randomAlphanumericOfLength(5))));
+        assertThat(e.getMessage(), containsString("Unknown keyUsage"));
+
+        {
+            final List<String> keyUsages = randomNonEmptySubsetOf(KEY_USAGE_MAPPINGS.keySet());
+            final KeyUsage keyUsage = buildKeyUsage(keyUsages);
+            for (String usageName : keyUsages) {
+                final Integer usage = KEY_USAGE_MAPPINGS.get(usageName);
+                assertThat(" mapping for keyUsage [" + usageName + "] is missing", usage, is(notNullValue()));
+                assertThat("expected keyUsage [" + usageName + "] to be set in [" + keyUsage + "]", keyUsage.hasUsages(usage), is(true));
+            }
+
+            final Set<String> keyUsagesNotSet = KEY_USAGE_MAPPINGS.keySet()
+                .stream()
+                .filter(u -> keyUsages.contains(u) == false)
+                .collect(Collectors.toSet());
+
+            for (String usageName : keyUsagesNotSet) {
+                final Integer usage = KEY_USAGE_MAPPINGS.get(usageName);
+                assertThat(" mapping for keyUsage [" + usageName + "] is missing", usage, is(notNullValue()));
+                assertThat(
+                    "expected keyUsage [" + usageName + "] not to be set in [" + keyUsage + "]",
+                    keyUsage.hasUsages(usage),
+                    is(false)
+                );
+            }
+
+        }
+
+        {
+            // test that duplicates and whitespaces are ignored
+            KeyUsage keyUsage = buildKeyUsage(
+                List.of("digitalSignature   ", "    nonRepudiation", "\tkeyEncipherment", "keyEncipherment\n")
+            );
+            assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("digitalSignature")), is(true));
+            assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("nonRepudiation")), is(true));
+            assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("digitalSignature")), is(true));
+            assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("keyEncipherment")), is(true));
+        }
+    }
+
+    public void testIsValidKeyUsage() {
+        assertThat(isValidKeyUsage(randomFrom(KEY_USAGE_MAPPINGS.keySet())), is(true));
+        assertThat(isValidKeyUsage(randomAlphanumericOfLength(5)), is(false));
+
+        // keyUsage names are case-sensitive
+        assertThat(isValidKeyUsage("DigitalSignature"), is(false));
+
+        // white-spaces are ignored
+        assertThat(isValidKeyUsage("keyAgreement "), is(true));
+        assertThat(isValidKeyUsage("keyCertSign\n"), is(true));
+        assertThat(isValidKeyUsage("\tcRLSign  "), is(true));
+    }
+
+    public static void assertExpectedKeyUsage(X509Certificate certificate, List<String> expectedKeyUsage) {
+        final boolean[] keyUsage = certificate.getKeyUsage();
+        assertThat("Expected " + KEY_USAGE_BITS.size() + " bits for key usage", keyUsage.length, equalTo(KEY_USAGE_BITS.size()));
+        final Set<Integer> expectedBitsToBeSet = expectedKeyUsage.stream().map(KEY_USAGE_BITS::get).collect(Collectors.toSet());
+
+        for (int i = 0; i < keyUsage.length; i++) {
+            if (expectedBitsToBeSet.contains(i)) {
+                assertThat("keyUsage bit [" + i + "] expected to be set: " + expectedKeyUsage, keyUsage[i], equalTo(true));
+            } else {
+                assertThat("keyUsage bit [" + i + "] not expected to be set: " + expectedKeyUsage, keyUsage[i], equalTo(false));
+            }
+        }
+        // key usage must be marked as critical
+        assertThat(
+            "keyUsage extension should be marked as critical",
+            certificate.getCriticalExtensionOIDs().contains(KeyUsageExtension.KEY_USAGE_OID.toString()),
+            is(true)
+        );
     }
 
     /**

+ 1 - 1
x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java

@@ -274,7 +274,7 @@ public class CertificateGenerateToolTests extends ESTestCase {
         final int keysize = randomFrom(1024, 2048);
         final int days = randomIntBetween(1, 1024);
         KeyPair keyPair = CertGenUtils.generateKeyPair(keysize);
-        X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
+        X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days, null);
 
         final boolean generatedCa = randomBoolean();
         final char[] keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD.toCharArray() : null;

+ 11 - 2
x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java

@@ -415,7 +415,13 @@ public class CertificateToolTests extends ESTestCase {
         int days = randomIntBetween(1, 1024);
 
         KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
-        X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
+        List<String> caKeyUsage = randomBoolean() ? null : CertificateTool.DEFAULT_CA_KEY_USAGE;
+        X509Certificate caCert = CertGenUtils.generateCACertificate(
+            new X500Principal("CN=test ca"),
+            keyPair,
+            days,
+            CertGenUtils.buildKeyUsage(caKeyUsage)
+        );
 
         final boolean selfSigned = randomBoolean();
         final String keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD : null;
@@ -1191,6 +1197,7 @@ public class CertificateToolTests extends ESTestCase {
         final int caKeySize = randomIntBetween(4, 8) * 512;
         final int days = randomIntBetween(7, 1500);
         final String caPassword = randomFrom("", randomAlphaOfLengthBetween(4, 80));
+        final String caKeyUsage = randomFrom("", Strings.collectionToCommaDelimitedString(CertificateTool.DEFAULT_CA_KEY_USAGE));
 
         final CertificateAuthorityCommand caCommand = new PathAwareCertificateAuthorityCommand(caFile);
         String[] args = {
@@ -1203,7 +1210,9 @@ public class CertificateToolTests extends ESTestCase {
             "-keysize",
             String.valueOf(caKeySize),
             "-days",
-            String.valueOf(days) };
+            String.valueOf(days),
+            "-keyusage",
+            caKeyUsage };
         if (pem) {
             args = ArrayUtils.append(args, "--pem");
         }

+ 17 - 1
x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java

@@ -23,6 +23,7 @@ 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.asn1.x509.KeyUsage;
 import org.bouncycastle.pkcs.PKCS10CertificationRequest;
 import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
 import org.bouncycastle.util.io.pem.PemObject;
@@ -30,6 +31,7 @@ import org.bouncycastle.util.io.pem.PemReader;
 import org.elasticsearch.cli.MockTerminal;
 import org.elasticsearch.cli.ProcessInfo;
 import org.elasticsearch.common.CheckedBiFunction;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.network.NetworkAddress;
 import org.elasticsearch.common.ssl.PemUtils;
 import org.elasticsearch.core.CheckedFunction;
@@ -89,10 +91,12 @@ import javax.security.auth.x500.X500Principal;
 import static org.elasticsearch.test.FileMatchers.isDirectory;
 import static org.elasticsearch.test.FileMatchers.isRegularFile;
 import static org.elasticsearch.test.FileMatchers.pathExists;
+import static org.elasticsearch.xpack.security.cli.CertGenUtilsTests.assertExpectedKeyUsage;
 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.equalTo;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.in;
 import static org.hamcrest.Matchers.instanceOf;
@@ -369,21 +373,25 @@ public class HttpCertificateCommandTests extends ESTestCase {
         final String caDN;
         final int caYears;
         final int caKeySize;
+        final List<String> caKeyUsage;
         // randomise whether to change CA defaults.
         if (randomBoolean()) {
             terminal.addTextInput("y"); // Change defaults
             caDN = "CN=" + randomAlphaOfLengthBetween(3, 8);
             caYears = randomIntBetween(1, 3);
             caKeySize = randomFrom(2048, 3072, 4096);
+            caKeyUsage = randomSubsetOf(CertGenUtils.KEY_USAGE_MAPPINGS.keySet());
             terminal.addTextInput(caDN);
             terminal.addTextInput(caYears + "y");
             terminal.addTextInput(Integer.toString(caKeySize));
+            terminal.addTextInput(Strings.collectionToCommaDelimitedString(caKeyUsage));
             terminal.addTextInput("n"); // Don't change values
         } else {
             terminal.addTextInput(randomBoolean() ? "n" : ""); // Don't change defaults
             caDN = HttpCertificateCommand.DEFAULT_CA_NAME.toString();
             caYears = HttpCertificateCommand.DEFAULT_CA_VALIDITY.getYears();
             caKeySize = HttpCertificateCommand.DEFAULT_CA_KEY_SIZE;
+            caKeyUsage = HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
         }
 
         final String caPassword = randomPassword(randomBoolean());
@@ -463,6 +471,7 @@ public class HttpCertificateCommandTests extends ESTestCase {
         verifyCertificate(caCertKey.v1(), caDN.replaceFirst("CN=", ""), caYears, List.of(), List.of());
         assertThat(getRSAKeySize(caCertKey.v1().getPublicKey()), is(caKeySize));
         assertThat(getRSAKeySize(caCertKey.v2()), is(caKeySize));
+        assertExpectedKeyUsage(caCertKey.v1(), caKeyUsage);
 
         assertThat(zipRoot.resolve("elasticsearch"), isDirectory());
 
@@ -486,6 +495,7 @@ public class HttpCertificateCommandTests extends ESTestCase {
             verifyChain(certAndKey.v1(), caCertKey.v1());
             assertThat(getRSAKeySize(certAndKey.v1().getPublicKey()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
             assertThat(getRSAKeySize(certAndKey.v2()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
+            assertExpectedKeyUsage(certAndKey.v1(), HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE);
 
             // Verify the README
             assertThat(readme, containsString(p12Path.getFileName().toString()));
@@ -692,7 +702,10 @@ public class HttpCertificateCommandTests extends ESTestCase {
         // 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));
+        assertThat(
+            extensions.getExtensionOIDs(),
+            arrayContainingInAnyOrder(Extension.subjectAlternativeName, Extension.keyUsage, Extension.extendedKeyUsage)
+        );
 
         final GeneralNames names = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName);
         assertThat(names.getNames(), arrayWithSize(hostNames.size() + ipAddresses.size()));
@@ -709,6 +722,9 @@ public class HttpCertificateCommandTests extends ESTestCase {
 
         ExtendedKeyUsage extendedKeyUsage = ExtendedKeyUsage.fromExtensions(extensions);
         assertThat(extendedKeyUsage.getUsages(), arrayContainingInAnyOrder(KeyPurposeId.id_kp_serverAuth));
+
+        KeyUsage keyUsage = KeyUsage.fromExtensions(extensions);
+        assertThat(keyUsage, is(equalTo(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment))));
     }
 
     private void verifyCertificate(