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

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
This commit is contained in:
Slobodan Adamović 2025-04-08 10:55:37 +02:00 committed by GitHub
parent 67d688eadf
commit 112859b85d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 404 additions and 72 deletions

View File

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

View File

@ -13,7 +13,7 @@ The `elasticsearch-certutil` command simplifies the creation of certificates for
```shell ```shell
bin/elasticsearch-certutil 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>]) | (cert ([--ca <file_path>] | [--ca-cert <file_path> --ca-key <file_path>])
[--ca-dn <name>] [--ca-pass <password>] [--days <n>] [--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>` `--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 : 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>` `--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. : 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.

View File

@ -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.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
import static org.elasticsearch.node.Node.NODE_NAME_SETTING; import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL; 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 * 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, null,
true, true,
TRANSPORT_CA_CERTIFICATE_DAYS, TRANSPORT_CA_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM SIGNATURE_ALGORITHM,
null,
Set.of()
); );
// transport key/certificate // transport key/certificate
final KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE); final KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
@ -424,7 +429,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
transportCaKey, transportCaKey,
false, false,
TRANSPORT_CERTIFICATE_DAYS, TRANSPORT_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM SIGNATURE_ALGORITHM,
null,
Set.of()
); );
final KeyPair httpCaKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE); final KeyPair httpCaKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
@ -438,7 +445,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
null, null,
true, true,
HTTP_CA_CERTIFICATE_DAYS, HTTP_CA_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM SIGNATURE_ALGORITHM,
buildKeyUsage(DEFAULT_CA_KEY_USAGE),
Set.of()
); );
} catch (Throwable t) { } catch (Throwable t) {
try { try {
@ -464,6 +473,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
false, false,
HTTP_CERTIFICATE_DAYS, HTTP_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM, SIGNATURE_ALGORITHM,
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
); );

View File

@ -20,6 +20,7 @@ import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator; import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.Time; import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.cert.CertIOException; import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509CertificateHolder;
@ -53,10 +54,14 @@ import java.security.cert.X509Certificate;
import java.sql.Date; import java.sql.Date;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509ExtendedTrustManager;
@ -73,14 +78,33 @@ public class CertGenUtils {
private static final int SERIAL_BIT_LENGTH = 20 * 8; private static final int SERIAL_BIT_LENGTH = 20 * 8;
private static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider(); 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() {} private CertGenUtils() {}
/** /**
* Generates a CA certificate * 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 { 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, PrivateKey caPrivKey,
int days int days
) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException { ) 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 * certificate
* @param caPrivKey the CA private key. If {@code null}, this results in a self signed * @param caPrivKey the CA private key. If {@code null}, this results in a self signed
* certificate * 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 isCa whether or not the generated certificate is a CA
* @param days no of days certificate will be valid from now * @param days no of days certificate will be valid from now
* @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or * @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
* empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)} * 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} * @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( public static X509Certificate generateSignedCertificate(
X500Principal principal, X500Principal principal,
GeneralNames subjectAltNames, GeneralNames subjectAltNames,
@ -180,6 +164,7 @@ public class CertGenUtils {
boolean isCa, boolean isCa,
int days, int days,
String signatureAlgorithm, String signatureAlgorithm,
KeyUsage keyUsage,
Set<ExtendedKeyUsage> extendedKeyUsages Set<ExtendedKeyUsage> extendedKeyUsages
) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException { ) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
Objects.requireNonNull(keyPair, "Key-Pair must not be null"); Objects.requireNonNull(keyPair, "Key-Pair must not be null");
@ -198,6 +183,7 @@ public class CertGenUtils {
notBefore, notBefore,
notAfter, notAfter,
signatureAlgorithm, signatureAlgorithm,
keyUsage,
extendedKeyUsages extendedKeyUsages
); );
} }
@ -223,6 +209,7 @@ public class CertGenUtils {
notBefore, notBefore,
notAfter, notAfter,
signatureAlgorithm, signatureAlgorithm,
null,
Set.of() Set.of()
); );
} }
@ -237,6 +224,7 @@ public class CertGenUtils {
ZonedDateTime notBefore, ZonedDateTime notBefore,
ZonedDateTime notAfter, ZonedDateTime notAfter,
String signatureAlgorithm, String signatureAlgorithm,
KeyUsage keyUsage,
Set<ExtendedKeyUsage> extendedKeyUsages Set<ExtendedKeyUsage> extendedKeyUsages
) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException { ) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
final BigInteger serial = CertGenUtils.getSerial(); final BigInteger serial = CertGenUtils.getSerial();
@ -272,6 +260,11 @@ public class CertGenUtils {
} }
builder.addExtension(Extension.basicConstraints, isCa, new BasicConstraints(isCa)); 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) { if (extendedKeyUsages != null) {
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) { for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
builder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); builder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
@ -318,7 +311,7 @@ public class CertGenUtils {
*/ */
static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) throws IOException, static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) throws IOException,
OperatorCreationException { 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, KeyPair keyPair,
X500Principal principal, X500Principal principal,
GeneralNames sanList, GeneralNames sanList,
KeyUsage keyUsage,
Set<ExtendedKeyUsage> extendedKeyUsages Set<ExtendedKeyUsage> extendedKeyUsages
) throws IOException, OperatorCreationException { ) throws IOException, OperatorCreationException {
Objects.requireNonNull(keyPair, "Key-Pair must not be null"); Objects.requireNonNull(keyPair, "Key-Pair must not be null");
@ -347,7 +341,9 @@ public class CertGenUtils {
if (sanList != null) { if (sanList != null) {
extGen.addExtension(Extension.subjectAlternativeName, false, sanList); extGen.addExtension(Extension.subjectAlternativeName, false, sanList);
} }
if (keyUsage != null) {
extGen.addExtension(Extension.keyUsage, true, keyUsage);
}
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) { for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
extGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); extGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
} }
@ -430,4 +426,31 @@ public class CertGenUtils {
public static String buildDnFromDomain(String domain) { public static String buildDnFromDomain(String domain) {
return "DC=" + domain.replace(".", ",DC="); 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());
}
} }

View File

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

View File

@ -15,6 +15,7 @@ import joptsimple.OptionSpecBuilder;
import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMEncryptor; import org.bouncycastle.openssl.PEMEncryptor;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
@ -110,6 +111,7 @@ class CertificateTool extends MultiCommand {
"[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1," + MAX_FILENAME_LENGTH + "}" "[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1," + MAX_FILENAME_LENGTH + "}"
); );
private static final int DEFAULT_KEY_SIZE = 2048; 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. // 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. // 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> outputPathSpec;
final OptionSpec<String> outputPasswordSpec; final OptionSpec<String> outputPasswordSpec;
final OptionSpec<Integer> keysizeSpec; final OptionSpec<Integer> keysizeSpec;
OptionSpec<String> caKeyUsageSpec;
OptionSpec<Void> pemFormatSpec; OptionSpec<Void> pemFormatSpec;
OptionSpec<Integer> daysSpec; 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(); 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 // For testing
OptionParser getParser() { OptionParser getParser() {
return parser; 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) { final int getDays(OptionSet options) {
if (options.has(daysSpec)) { if (options.has(daysSpec)) {
return daysSpec.value(options); return daysSpec.value(options);
@ -396,7 +426,8 @@ class CertificateTool extends MultiCommand {
} }
X500Principal x500Principal = new X500Principal(dn); X500Principal x500Principal = new X500Principal(dn);
KeyPair keyPair = CertGenUtils.generateKeyPair(getKeySize(options)); 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)) { if (options.hasArgument(caPasswordSpec)) {
char[] password = getChars(caPasswordSpec.value(options)); char[] password = getChars(caPasswordSpec.value(options));
@ -933,9 +964,7 @@ class CertificateTool extends MultiCommand {
keyPair, keyPair,
null, null,
null, null,
false, days
days,
null
); );
} }
return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate()); return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate());
@ -949,6 +978,7 @@ class CertificateTool extends MultiCommand {
super("generate a new local certificate authority"); super("generate a new local certificate authority");
acceptCertificateGenerationOptions(); acceptCertificateGenerationOptions();
acceptsCertificateAuthorityName(); acceptsCertificateAuthorityName();
acceptCertificateAuthorityKeyUsage();
super.caPasswordSpec = super.outputPasswordSpec; super.caPasswordSpec = super.outputPasswordSpec;
} }

View File

@ -68,6 +68,8 @@ import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -80,7 +82,9 @@ import java.util.zip.ZipOutputStream;
import javax.security.auth.x500.X500Principal; 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.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 * 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 X500Principal DEFAULT_CA_NAME = new X500Principal("CN=Elasticsearch HTTP CA");
static final int DEFAULT_CA_KEY_SIZE = DEFAULT_CERT_KEY_SIZE; static final int DEFAULT_CA_KEY_SIZE = DEFAULT_CERT_KEY_SIZE;
static final Period DEFAULT_CA_VALIDITY = DEFAULT_CERT_VALIDITY; 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_README_CSR = "es-readme-csr.txt";
private static final String ES_YML_CSR = "es-sample-csr.yml"; private static final String ES_YML_CSR = "es-sample-csr.yml";
private static final String ES_README_P12 = "es-readme-p12.txt"; 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> dnsNames;
final List<String> ipNames; final List<String> ipNames;
final int keySize; final int keySize;
final List<String> keyUsage;
final Period validity; 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.name = name;
this.subject = subject; this.subject = subject;
this.dnsNames = dnsNames; this.dnsNames = dnsNames;
this.ipNames = ipNames; this.ipNames = ipNames;
this.keySize = keySize; this.keySize = keySize;
this.keyUsage = keyUsage;
this.validity = validity; 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, "\tDNS Names: " + Strings.collectionToCommaDelimitedString(cert.dnsNames));
terminal.println(Terminal.Verbosity.VERBOSE, "\tIP Names: " + Strings.collectionToCommaDelimitedString(cert.ipNames)); 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 Size: " + cert.keySize);
terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Usage: " + Strings.collectionToCommaDelimitedString(cert.keyUsage));
terminal.println(Terminal.Verbosity.VERBOSE, "\tValidity: " + toString(cert.validity)); terminal.println(Terminal.Verbosity.VERBOSE, "\tValidity: " + toString(cert.validity));
certificates.add(cert); certificates.add(cert);
@ -339,6 +355,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
keyPair, keyPair,
cert.subject, cert.subject,
sanList, sanList,
buildKeyUsage(cert.keyUsage),
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
); );
final String csrFile = "http-" + cert.name + ".csr"; final String csrFile = "http-" + cert.name + ".csr";
@ -372,6 +389,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
notBefore, notBefore,
notAfter, notAfter,
null, null,
buildKeyUsage(cert.keyUsage),
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
); );
@ -692,10 +710,12 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
} }
X500Principal dn = buildDistinguishedName(certName); X500Principal dn = buildDistinguishedName(certName);
int keySize = DEFAULT_CERT_KEY_SIZE; int keySize = DEFAULT_CERT_KEY_SIZE;
List<String> keyUsage = DEFAULT_CERT_KEY_USAGE;
while (true) { while (true) {
terminal.println(Terminal.Verbosity.SILENT, "Key Name: " + certName); terminal.println(Terminal.Verbosity.SILENT, "Key Name: " + certName);
terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn); terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize); terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
terminal.println(Terminal.Verbosity.SILENT, ""); terminal.println(Terminal.Verbosity.SILENT, "");
if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) { if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
break; break;
@ -736,9 +756,22 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
keySize = readKeySize(terminal, keySize); keySize = readKeySize(terminal, keySize);
terminal.println(""); 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) { private static String validateHostname(String name) {
@ -859,10 +892,12 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
X500Principal dn = DEFAULT_CA_NAME; X500Principal dn = DEFAULT_CA_NAME;
Period validity = DEFAULT_CA_VALIDITY; Period validity = DEFAULT_CA_VALIDITY;
int keySize = DEFAULT_CA_KEY_SIZE; int keySize = DEFAULT_CA_KEY_SIZE;
List<String> keyUsage = DEFAULT_CA_KEY_USAGE;
while (true) { while (true) {
terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn); terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
terminal.println(Terminal.Verbosity.SILENT, "Validity: " + toString(validity)); terminal.println(Terminal.Verbosity.SILENT, "Validity: " + toString(validity));
terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize); terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
terminal.println(Terminal.Verbosity.SILENT, ""); terminal.println(Terminal.Verbosity.SILENT, "");
if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) { if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
break; break;
@ -904,13 +939,38 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
keySize = readKeySize(terminal, keySize); keySize = readKeySize(terminal, keySize);
terminal.println(""); 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 { try {
final KeyPair keyPair = CertGenUtils.generateKeyPair(keySize); final KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
final ZonedDateTime notBefore = ZonedDateTime.now(ZoneOffset.UTC); final ZonedDateTime notBefore = ZonedDateTime.now(ZoneOffset.UTC);
final ZonedDateTime notAfter = notBefore.plus(validity); 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); printHeader("CA password", terminal);
terminal.println("We recommend that you protect your CA private key with a strong password."); 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) { private static char[] readPassword(Terminal terminal, String prompt, boolean confirm) {
while (true) { while (true) {
final char[] password = terminal.readSecret(prompt + " [<ENTER> for none]"); 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) { 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) { while (true) {
final String input = terminal.readText(prompt + " [" + defaultStr + "] "); final String input = terminal.readText(prompt + " [" + defaultStr + "] ");
if (Strings.isEmpty(input)) { if (Strings.isEmpty(input)) {

View File

@ -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.AUTO_CONFIG_TRANSPORT_ALT_DN;
import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.anyRemoteHostNodeAddress; 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.AutoConfigureNode.removePreviousAutoconfiguration;
import static org.elasticsearch.xpack.security.cli.CertGenUtilsTests.assertExpectedKeyUsage;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; 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 // test no publish settings
Path tempDir = createTempDir(); Path tempDir = createTempDir();
try { try {
@ -180,7 +181,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true)); assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(true)); assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false)); assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false));
verifyExtendedKeyUsage(httpCertificate); verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
} finally { } finally {
deleteDirectory(tempDir); deleteDirectory(tempDir);
} }
@ -202,7 +203,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true)); assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false)); assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false));
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(true)); assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(true));
verifyExtendedKeyUsage(httpCertificate); verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
} finally { } finally {
deleteDirectory(tempDir); deleteDirectory(tempDir);
} }
@ -228,7 +229,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
assertThat(checkGeneralNameSan(httpCertificate, "balkan.beast", GeneralName.dNSName), is(true)); assertThat(checkGeneralNameSan(httpCertificate, "balkan.beast", GeneralName.dNSName), is(true));
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false)); assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false));
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false)); assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false));
verifyExtendedKeyUsage(httpCertificate); verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
} finally { } finally {
deleteDirectory(tempDir); deleteDirectory(tempDir);
} }
@ -288,11 +289,12 @@ public class AutoConfigureNodeTests extends ESTestCase {
return false; return false;
} }
private void verifyExtendedKeyUsage(X509Certificate httpCertificate) throws Exception { private void verifyKeyUsageAndExtendedKeyUsage(X509Certificate httpCertificate) throws Exception {
List<String> extendedKeyUsage = httpCertificate.getExtendedKeyUsage(); List<String> extendedKeyUsage = httpCertificate.getExtendedKeyUsage();
assertEquals("Only one extended key usage expected for HTTP certificate.", 1, extendedKeyUsage.size()); assertEquals("Only one extended key usage expected for HTTP certificate.", 1, extendedKeyUsage.size());
String expectedServerAuthUsage = KeyPurposeId.id_kp_serverAuth.toASN1Primitive().toString(); String expectedServerAuthUsage = KeyPurposeId.id_kp_serverAuth.toASN1Primitive().toString();
assertEquals("Expected serverAuth extended key usage.", expectedServerAuthUsage, extendedKeyUsage.get(0)); 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 { private X509Certificate runAutoConfigAndReturnHTTPCertificate(Path configDir, Settings settings) throws Exception {

View File

@ -7,10 +7,13 @@
package org.elasticsearch.xpack.security.cli; package org.elasticsearch.xpack.security.cli;
import com.unboundid.util.ssl.cert.KeyUsageExtension;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.network.InetAddresses;
import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.network.NetworkAddress;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
@ -27,22 +30,57 @@ import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager; import javax.net.ssl.X509ExtendedTrustManager;
import javax.security.auth.x500.X500Principal; 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.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
/** /**
* Unit tests for cert utils * Unit tests for cert utils
*/ */
public class CertGenUtilsTests extends ESTestCase { 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 @BeforeClass
public static void muteInFips() { public static void muteInFips() {
assumeFalse("Can't run in a FIPS JVM", inFipsJvm()); assumeFalse("Can't run in a FIPS JVM", inFipsJvm());
@ -103,6 +141,7 @@ public class CertGenUtilsTests extends ESTestCase {
// root CA // root CA
final X500Principal rootCaPrincipal = new X500Principal("DC=example.com"); final X500Principal rootCaPrincipal = new X500Principal("DC=example.com");
final KeyPair rootCaKeyPair = CertGenUtils.generateKeyPair(2048); final KeyPair rootCaKeyPair = CertGenUtils.generateKeyPair(2048);
final List<String> rootCaKeyUsages = List.of("keyCertSign", "cRLSign");
final X509Certificate rootCaCert = CertGenUtils.generateSignedCertificate( final X509Certificate rootCaCert = CertGenUtils.generateSignedCertificate(
rootCaPrincipal, rootCaPrincipal,
null, null,
@ -112,12 +151,15 @@ public class CertGenUtilsTests extends ESTestCase {
true, true,
notBefore, notBefore,
notAfter, notAfter,
null null,
buildKeyUsage(rootCaKeyUsages),
Set.of()
); );
// sub CA // sub CA
final X500Principal subCaPrincipal = new X500Principal("DC=Sub CA,DC=example.com"); final X500Principal subCaPrincipal = new X500Principal("DC=Sub CA,DC=example.com");
final KeyPair subCaKeyPair = CertGenUtils.generateKeyPair(2048); final KeyPair subCaKeyPair = CertGenUtils.generateKeyPair(2048);
final List<String> subCaKeyUsage = List.of("digitalSignature", "keyCertSign", "cRLSign");
final X509Certificate subCaCert = CertGenUtils.generateSignedCertificate( final X509Certificate subCaCert = CertGenUtils.generateSignedCertificate(
subCaPrincipal, subCaPrincipal,
null, null,
@ -127,12 +169,15 @@ public class CertGenUtilsTests extends ESTestCase {
true, true,
notBefore, notBefore,
notAfter, notAfter,
null null,
buildKeyUsage(subCaKeyUsage),
Set.of()
); );
// end entity // end entity
final X500Principal endEntityPrincipal = new X500Principal("CN=TLS Client\\+Server,DC=Sub CA,DC=example.com"); final X500Principal endEntityPrincipal = new X500Principal("CN=TLS Client\\+Server,DC=Sub CA,DC=example.com");
final KeyPair endEntityKeyPair = CertGenUtils.generateKeyPair(2048); final KeyPair endEntityKeyPair = CertGenUtils.generateKeyPair(2048);
final List<String> endEntityKeyUsage = randomBoolean() ? null : List.of("digitalSignature", "keyEncipherment");
final X509Certificate endEntityCert = CertGenUtils.generateSignedCertificate( final X509Certificate endEntityCert = CertGenUtils.generateSignedCertificate(
endEntityPrincipal, endEntityPrincipal,
null, null,
@ -143,6 +188,7 @@ public class CertGenUtilsTests extends ESTestCase {
notBefore, notBefore,
notAfter, notAfter,
null, null,
buildKeyUsage(endEntityKeyUsage),
Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)) 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 trustStore.setCertificateEntry("trustAnchor", rootCaCert); // anchor: any part of the chain, or issuer of last entry in chain
validateEndEntityTlsChain(trustStore, certChain, true, true); 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)
);
} }
/** /**

View File

@ -274,7 +274,7 @@ public class CertificateGenerateToolTests extends ESTestCase {
final int keysize = randomFrom(1024, 2048); final int keysize = randomFrom(1024, 2048);
final int days = randomIntBetween(1, 1024); final int days = randomIntBetween(1, 1024);
KeyPair keyPair = CertGenUtils.generateKeyPair(keysize); 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 boolean generatedCa = randomBoolean();
final char[] keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD.toCharArray() : null; final char[] keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD.toCharArray() : null;

View File

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

View File

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