diff --git a/docs/changelog/126376.yaml b/docs/changelog/126376.yaml new file mode 100644 index 0000000000000..5ac6bc747f160 --- /dev/null +++ b/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 diff --git a/docs/reference/commands/certutil.asciidoc b/docs/reference/commands/certutil.asciidoc index 6720aef470049..9f7588b61f1c6 100644 --- a/docs/reference/commands/certutil.asciidoc +++ b/docs/reference/commands/certutil.asciidoc @@ -11,7 +11,7 @@ use with Transport Layer Security (TLS) in the {stack}. -------------------------------------------------- bin/elasticsearch-certutil ( -(ca [--ca-dn ] [--days ] [--pem]) +(ca [--ca-dn ] [--keyusage ] [--days ] [--pem]) | (cert ([--ca ] | [--ca-cert --ca-key ]) [--ca-dn ] [--ca-pass ] [--days ] @@ -158,6 +158,10 @@ parameter is only applicable to the `cert` parameter. `--ca-pass `:: 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 `:: 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 `:: 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. diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java index 3994fb50c7fc6..42dbf317b1ee8 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java @@ -102,6 +102,9 @@ 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 void execute(Terminal terminal, OptionSet options, Environment env, Proce 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 void execute(Terminal terminal, OptionSet options, Environment env, Proce 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 void execute(Terminal terminal, OptionSet options, Environment env, Proce 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 void execute(Terminal terminal, OptionSet options, Environment env, Proce false, HTTP_CERTIFICATE_DAYS, SIGNATURE_ALGORITHM, + buildKeyUsage(DEFAULT_CERT_KEY_USAGE), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index c3f4d8a57b560..b82e2e1d77faf 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -20,6 +20,7 @@ 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.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 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 static X509Certificate generateSignedCertificate( 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 static X509Certificate generateSignedCertificate( * 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 static X509Certificate generateSignedCertificate( boolean isCa, int days, String signatureAlgorithm, + KeyUsage keyUsage, Set extendedKeyUsages ) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException { Objects.requireNonNull(keyPair, "Key-Pair must not be null"); @@ -198,6 +183,7 @@ public static X509Certificate generateSignedCertificate( notBefore, notAfter, signatureAlgorithm, + keyUsage, extendedKeyUsages ); } @@ -223,6 +209,7 @@ public static X509Certificate generateSignedCertificate( notBefore, notAfter, signatureAlgorithm, + null, Set.of() ); } @@ -237,6 +224,7 @@ public static X509Certificate generateSignedCertificate( ZonedDateTime notBefore, ZonedDateTime notAfter, String signatureAlgorithm, + KeyUsage keyUsage, Set extendedKeyUsages ) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException { final BigInteger serial = CertGenUtils.getSerial(); @@ -272,6 +260,11 @@ public static X509Certificate generateSignedCertificate( } 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 @@ 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()); + return generateCSR(keyPair, principal, sanList, null, Set.of()); } /** @@ -335,6 +328,7 @@ static PKCS10CertificationRequest generateCSR( KeyPair keyPair, X500Principal principal, GeneralNames sanList, + KeyUsage keyUsage, Set extendedKeyUsages ) throws IOException, OperatorCreationException { Objects.requireNonNull(keyPair, "Key-Pair must not be null"); @@ -347,7 +341,9 @@ static PKCS10CertificationRequest generateCSR( 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 static GeneralName createCommonName(String cn) { public static String buildDnFromDomain(String domain) { return "DC=" + domain.replace(".", ",DC="); } + + public static KeyUsage buildKeyUsage(Collection 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()); + } } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java index a6716f0360a1b..a342e3cca3e94 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java @@ -403,7 +403,7 @@ static CAInfo getCAInfo( // 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: "); diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index a9c0653716851..b64e21786279b 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -15,6 +15,7 @@ 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 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 @@ abstract static class CertificateCommand extends EnvironmentAwareCommand { final OptionSpec outputPathSpec; final OptionSpec outputPasswordSpec; final OptionSpec keysizeSpec; + OptionSpec caKeyUsageSpec; OptionSpec pemFormatSpec; OptionSpec daysSpec; @@ -274,6 +277,16 @@ final void acceptInputFile() { 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 @@ final int getKeySize(OptionSet options) { } } + final List getCaKeyUsage(OptionSet options) { + if (options.has(caKeyUsageSpec)) { + final Function> splitByComma = v -> Stream.of(Strings.splitStringByCommaToArray(v)); + final List 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 @@ CAInfo generateCA(Terminal terminal, OptionSet options) throws Exception { } 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 @@ private static CertificateAndKey generateCertificateAndKey( keyPair, null, null, - false, - days, - null + days ); } return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate()); @@ -949,6 +978,7 @@ static class CertificateAuthorityCommand extends CertificateCommand { super("generate a new local certificate authority"); acceptCertificateGenerationOptions(); acceptsCertificateAuthorityName(); + acceptCertificateAuthorityKeyUsage(); super.caPasswordSpec = super.outputPasswordSpec; } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index b67bb9898991f..12e98e69d7300 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -68,6 +68,8 @@ 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 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 DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign"); + static final List 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 @@ private class CertOptions { final List dnsNames; final List ipNames; final int keySize; + final List keyUsage; final Period validity; - private CertOptions(String name, X500Principal subject, List dnsNames, List ipNames, int keySize, Period validity) { + private CertOptions( + String name, + X500Principal subject, + List dnsNames, + List ipNames, + int keySize, + List 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 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce 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 @@ private void writeCertificateAndKeyDetails( 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 @@ private void writeCertificateAndKeyDetails( notBefore, notAfter, null, + buildKeyUsage(cert.keyUsage), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); @@ -692,10 +710,12 @@ private CertOptions getCertificateConfiguration( } X500Principal dn = buildDistinguishedName(certName); int keySize = DEFAULT_CERT_KEY_SIZE; + List 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 @@ private CertOptions getCertificateConfiguration( 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 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) { X500Principal dn = DEFAULT_CA_NAME; Period validity = DEFAULT_CA_VALIDITY; int keySize = DEFAULT_CA_KEY_SIZE; + List 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 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) { 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 @@ private static Integer readKeySize(Terminal terminal, int keySize) { }); } + private static List readKeyUsage(Terminal terminal, List defaultKeyUsage) { + return tryReadInput(terminal, "Key Usage", defaultKeyUsage, input -> { + final String[] keyUsages = input.split(","); + final List 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 + " [ for none]"); @@ -1080,7 +1165,14 @@ private static boolean askExistingCertificateAuthority(Terminal terminal) { } private static T tryReadInput(Terminal terminal, String prompt, T defaultValue, Function 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)) { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java index 129d85d0818b2..66567877a7812 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java +++ b/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_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 void testSubjectAndIssuerForGeneratedCertificates() throws Exception { } } - public void testGeneratedHTTPCertificateSANs() throws Exception { + public void testGeneratedHTTPCertificateSANsAndKeyUsage() throws Exception { // test no publish settings Path tempDir = createTempDir(); try { @@ -180,7 +181,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); + verifyKeyUsageAndExtendedKeyUsage(httpCertificate); } finally { deleteDirectory(tempDir); } @@ -202,7 +203,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); + verifyKeyUsageAndExtendedKeyUsage(httpCertificate); } finally { deleteDirectory(tempDir); } @@ -228,7 +229,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); + verifyKeyUsageAndExtendedKeyUsage(httpCertificate); } finally { deleteDirectory(tempDir); } @@ -288,11 +289,12 @@ private boolean checkSubjectAndIssuerDN(X509Certificate certificate, String subj return false; } - private void verifyExtendedKeyUsage(X509Certificate httpCertificate) throws Exception { + private void verifyKeyUsageAndExtendedKeyUsage(X509Certificate httpCertificate) throws Exception { List 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 { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index 5c1f5a97d4335..ae905d179688b 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/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.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: + * + *
    + *
  • digitalSignature (0)
  • + *
  • nonRepudiation (1)
  • + *
  • keyEncipherment (2)
  • + *
  • dataEncipherment (3)
  • + *
  • keyAgreement (4)
  • + *
  • keyCertSign (5)
  • + *
  • cRLSign (6)
  • + *
  • encipherOnly (7)
  • + *
  • decipherOnly (8)
  • + *
+ */ + private static final Map 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 void testIssuerCertSubjectDN() throws Exception { // root CA final X500Principal rootCaPrincipal = new X500Principal("DC=example.com"); final KeyPair rootCaKeyPair = CertGenUtils.generateKeyPair(2048); + final List rootCaKeyUsages = List.of("keyCertSign", "cRLSign"); final X509Certificate rootCaCert = CertGenUtils.generateSignedCertificate( rootCaPrincipal, null, @@ -112,12 +151,15 @@ public void testIssuerCertSubjectDN() throws Exception { 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 subCaKeyUsage = List.of("digitalSignature", "keyCertSign", "cRLSign"); final X509Certificate subCaCert = CertGenUtils.generateSignedCertificate( subCaPrincipal, null, @@ -127,12 +169,15 @@ public void testIssuerCertSubjectDN() throws Exception { 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 endEntityKeyUsage = randomBoolean() ? null : List.of("digitalSignature", "keyEncipherment"); final X509Certificate endEntityCert = CertGenUtils.generateSignedCertificate( endEntityPrincipal, null, @@ -143,6 +188,7 @@ public void testIssuerCertSubjectDN() throws Exception { notBefore, notAfter, null, + buildKeyUsage(endEntityKeyUsage), Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)) ); @@ -162,6 +208,101 @@ public void testIssuerCertSubjectDN() throws Exception { 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(randomAlphaOfLengthBetween(3, 5)))); + assertThat(e.getMessage(), containsString("Unknown keyUsage")); + + { + final List 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 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(randomAlphaOfLengthBetween(3, 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 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 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) + ); } /** diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java index 1faabcfd46fdb..69ba80c729254 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java @@ -274,7 +274,7 @@ public void testGeneratingSignedCertificates() throws Exception { 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; diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java index 1a11234c98e6e..32b2aabb29611 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java @@ -415,7 +415,13 @@ public void testGeneratingSignedPemCertificates() throws Exception { int days = randomIntBetween(1, 1024); KeyPair keyPair = CertGenUtils.generateKeyPair(keySize); - X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days); + List 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 @@ private String generateCA(Path caFile, MockTerminal terminal, Environment env, b 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 @@ private String generateCA(Path caFile, MockTerminal terminal, Environment env, b "-keysize", String.valueOf(caKeySize), "-days", - String.valueOf(days) }; + String.valueOf(days), + "-keyusage", + caKeyUsage }; if (pem) { args = ArrayUtils.append(args, "--pem"); } diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java index 1033d4e51ebba..57ad76af3317a 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java @@ -23,6 +23,7 @@ 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.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 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 void testGenerateMultipleCertificateWithNewCA() throws Exception { final String caDN; final int caYears; final int caKeySize; + final List 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 = randomNonEmptySubsetOf(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 void testGenerateMultipleCertificateWithNewCA() throws Exception { 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 void testGenerateMultipleCertificateWithNewCA() throws Exception { 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 @@ private void verifyCertificationRequest( // 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 @@ private void verifyCertificationRequest( 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(