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:
parent
67d688eadf
commit
112859b85d
|
@ -0,0 +1,6 @@
|
|||
pr: 126376
|
||||
summary: Set `keyUsage` for generated HTTP certificates and self-signed CA
|
||||
area: TLS
|
||||
type: bug
|
||||
issues:
|
||||
- 117769
|
|
@ -13,7 +13,7 @@ The `elasticsearch-certutil` command simplifies the creation of certificates for
|
|||
```shell
|
||||
bin/elasticsearch-certutil
|
||||
(
|
||||
(ca [--ca-dn <name>] [--days <n>] [--pem])
|
||||
(ca [--ca-dn <name>] [--keyusage <key_usages>] [--days <n>] [--pem])
|
||||
|
||||
| (cert ([--ca <file_path>] | [--ca-cert <file_path> --ca-key <file_path>])
|
||||
[--ca-dn <name>] [--ca-pass <password>] [--days <n>]
|
||||
|
@ -105,6 +105,9 @@ The `http` mode guides you through the process of generating certificates for us
|
|||
`--ca-pass <password>`
|
||||
: Specifies the password for an existing CA private key or the generated CA private key. This parameter is only applicable to the `cert` parameter
|
||||
|
||||
`--keyusage <key_usages>`
|
||||
: Specifies a comma-separated list of key usage restrictions (as per RFC 5280) that are used for the generated CA certificate. The default value is `keyCertSign,cRLSign`. This parameter may only be used with the `ca` parameter.
|
||||
|
||||
`--days <n>`
|
||||
: Specifies an integer value that represents the number of days the generated certificates are valid. The default value is `1095`. This parameter cannot be used with the `csr` or `http` parameters.
|
||||
|
||||
|
|
|
@ -102,6 +102,9 @@ import static org.elasticsearch.common.ssl.PemUtils.parsePKCS8PemString;
|
|||
import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
|
||||
import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
|
||||
import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
|
||||
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
|
||||
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE;
|
||||
|
||||
/**
|
||||
* Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
|
||||
|
@ -411,7 +414,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
|
|||
null,
|
||||
true,
|
||||
TRANSPORT_CA_CERTIFICATE_DAYS,
|
||||
SIGNATURE_ALGORITHM
|
||||
SIGNATURE_ALGORITHM,
|
||||
null,
|
||||
Set.of()
|
||||
);
|
||||
// transport key/certificate
|
||||
final KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
|
||||
|
@ -424,7 +429,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
|
|||
transportCaKey,
|
||||
false,
|
||||
TRANSPORT_CERTIFICATE_DAYS,
|
||||
SIGNATURE_ALGORITHM
|
||||
SIGNATURE_ALGORITHM,
|
||||
null,
|
||||
Set.of()
|
||||
);
|
||||
|
||||
final KeyPair httpCaKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
|
||||
|
@ -438,7 +445,9 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
|
|||
null,
|
||||
true,
|
||||
HTTP_CA_CERTIFICATE_DAYS,
|
||||
SIGNATURE_ALGORITHM
|
||||
SIGNATURE_ALGORITHM,
|
||||
buildKeyUsage(DEFAULT_CA_KEY_USAGE),
|
||||
Set.of()
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
try {
|
||||
|
@ -464,6 +473,7 @@ public class AutoConfigureNode extends EnvironmentAwareCommand {
|
|||
false,
|
||||
HTTP_CERTIFICATE_DAYS,
|
||||
SIGNATURE_ALGORITHM,
|
||||
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
|
||||
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
|
||||
);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.bouncycastle.asn1.x509.Extension;
|
|||
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.asn1.x509.Time;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
|
@ -53,10 +54,14 @@ import java.security.cert.X509Certificate;
|
|||
import java.sql.Date;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
|
@ -73,14 +78,33 @@ public class CertGenUtils {
|
|||
private static final int SERIAL_BIT_LENGTH = 20 * 8;
|
||||
private static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider();
|
||||
|
||||
/**
|
||||
* The mapping of key usage names to their corresponding integer values as defined in {@code KeyUsage} class.
|
||||
*/
|
||||
public static final Map<String, Integer> KEY_USAGE_MAPPINGS = Collections.unmodifiableMap(
|
||||
new TreeMap<>(
|
||||
Map.ofEntries(
|
||||
Map.entry("digitalSignature", KeyUsage.digitalSignature),
|
||||
Map.entry("nonRepudiation", KeyUsage.nonRepudiation),
|
||||
Map.entry("keyEncipherment", KeyUsage.keyEncipherment),
|
||||
Map.entry("dataEncipherment", KeyUsage.dataEncipherment),
|
||||
Map.entry("keyAgreement", KeyUsage.keyAgreement),
|
||||
Map.entry("keyCertSign", KeyUsage.keyCertSign),
|
||||
Map.entry("cRLSign", KeyUsage.cRLSign),
|
||||
Map.entry("encipherOnly", KeyUsage.encipherOnly),
|
||||
Map.entry("decipherOnly", KeyUsage.decipherOnly)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
private CertGenUtils() {}
|
||||
|
||||
/**
|
||||
* Generates a CA certificate
|
||||
*/
|
||||
public static X509Certificate generateCACertificate(X500Principal x500Principal, KeyPair keyPair, int days)
|
||||
public static X509Certificate generateCACertificate(X500Principal x500Principal, KeyPair keyPair, int days, KeyUsage keyUsage)
|
||||
throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
|
||||
return generateSignedCertificate(x500Principal, null, keyPair, null, null, true, days, null);
|
||||
return generateSignedCertificate(x500Principal, null, keyPair, null, null, true, days, null, keyUsage, Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,7 +131,7 @@ public class CertGenUtils {
|
|||
PrivateKey caPrivKey,
|
||||
int days
|
||||
) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
|
||||
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, null);
|
||||
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, null, null, Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -123,54 +147,14 @@ public class CertGenUtils {
|
|||
* certificate
|
||||
* @param caPrivKey the CA private key. If {@code null}, this results in a self signed
|
||||
* certificate
|
||||
* @param days no of days certificate will be valid from now
|
||||
* @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
|
||||
* empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
|
||||
* @return a signed {@link X509Certificate}
|
||||
*/
|
||||
public static X509Certificate generateSignedCertificate(
|
||||
X500Principal principal,
|
||||
GeneralNames subjectAltNames,
|
||||
KeyPair keyPair,
|
||||
X509Certificate caCert,
|
||||
PrivateKey caPrivKey,
|
||||
int days,
|
||||
String signatureAlgorithm
|
||||
) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
|
||||
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, signatureAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signed certificate
|
||||
*
|
||||
* @param principal the principal of the certificate; commonly referred to as the
|
||||
* distinguished name (DN)
|
||||
* @param subjectAltNames the subject alternative names that should be added to the
|
||||
* certificate as an X509v3 extension. May be {@code null}
|
||||
* @param keyPair the key pair that will be associated with the certificate
|
||||
* @param caCert the CA certificate. If {@code null}, this results in a self signed
|
||||
* certificate
|
||||
* @param caPrivKey the CA private key. If {@code null}, this results in a self signed
|
||||
* certificate
|
||||
* @param isCa whether or not the generated certificate is a CA
|
||||
* @param days no of days certificate will be valid from now
|
||||
* @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
|
||||
* empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
|
||||
* @param keyUsage the key usage that should be added to the certificate as a X509v3 extension (can be {@code null})
|
||||
* @param extendedKeyUsages the extended key usages that should be added to the certificate as a X509v3 extension (can be empty)
|
||||
* @return a signed {@link X509Certificate}
|
||||
*/
|
||||
public static X509Certificate generateSignedCertificate(
|
||||
X500Principal principal,
|
||||
GeneralNames subjectAltNames,
|
||||
KeyPair keyPair,
|
||||
X509Certificate caCert,
|
||||
PrivateKey caPrivKey,
|
||||
boolean isCa,
|
||||
int days,
|
||||
String signatureAlgorithm
|
||||
) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
|
||||
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, isCa, days, signatureAlgorithm, Set.of());
|
||||
}
|
||||
|
||||
public static X509Certificate generateSignedCertificate(
|
||||
X500Principal principal,
|
||||
GeneralNames subjectAltNames,
|
||||
|
@ -180,6 +164,7 @@ public class CertGenUtils {
|
|||
boolean isCa,
|
||||
int days,
|
||||
String signatureAlgorithm,
|
||||
KeyUsage keyUsage,
|
||||
Set<ExtendedKeyUsage> extendedKeyUsages
|
||||
) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
|
||||
Objects.requireNonNull(keyPair, "Key-Pair must not be null");
|
||||
|
@ -198,6 +183,7 @@ public class CertGenUtils {
|
|||
notBefore,
|
||||
notAfter,
|
||||
signatureAlgorithm,
|
||||
keyUsage,
|
||||
extendedKeyUsages
|
||||
);
|
||||
}
|
||||
|
@ -223,6 +209,7 @@ public class CertGenUtils {
|
|||
notBefore,
|
||||
notAfter,
|
||||
signatureAlgorithm,
|
||||
null,
|
||||
Set.of()
|
||||
);
|
||||
}
|
||||
|
@ -237,6 +224,7 @@ public class CertGenUtils {
|
|||
ZonedDateTime notBefore,
|
||||
ZonedDateTime notAfter,
|
||||
String signatureAlgorithm,
|
||||
KeyUsage keyUsage,
|
||||
Set<ExtendedKeyUsage> extendedKeyUsages
|
||||
) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
|
||||
final BigInteger serial = CertGenUtils.getSerial();
|
||||
|
@ -272,6 +260,11 @@ public class CertGenUtils {
|
|||
}
|
||||
builder.addExtension(Extension.basicConstraints, isCa, new BasicConstraints(isCa));
|
||||
|
||||
if (keyUsage != null) {
|
||||
// as per RFC 5280 (section 4.2.1.3), if the key usage is present, then it SHOULD be marked as critical.
|
||||
final boolean isCritical = true;
|
||||
builder.addExtension(Extension.keyUsage, isCritical, keyUsage);
|
||||
}
|
||||
if (extendedKeyUsages != null) {
|
||||
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
|
||||
builder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
|
||||
|
@ -318,7 +311,7 @@ public class CertGenUtils {
|
|||
*/
|
||||
static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) throws IOException,
|
||||
OperatorCreationException {
|
||||
return generateCSR(keyPair, principal, sanList, Set.of());
|
||||
return generateCSR(keyPair, principal, sanList, null, Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -335,6 +328,7 @@ public class CertGenUtils {
|
|||
KeyPair keyPair,
|
||||
X500Principal principal,
|
||||
GeneralNames sanList,
|
||||
KeyUsage keyUsage,
|
||||
Set<ExtendedKeyUsage> extendedKeyUsages
|
||||
) throws IOException, OperatorCreationException {
|
||||
Objects.requireNonNull(keyPair, "Key-Pair must not be null");
|
||||
|
@ -347,7 +341,9 @@ public class CertGenUtils {
|
|||
if (sanList != null) {
|
||||
extGen.addExtension(Extension.subjectAlternativeName, false, sanList);
|
||||
}
|
||||
|
||||
if (keyUsage != null) {
|
||||
extGen.addExtension(Extension.keyUsage, true, keyUsage);
|
||||
}
|
||||
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
|
||||
extGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
|
||||
}
|
||||
|
@ -430,4 +426,31 @@ public class CertGenUtils {
|
|||
public static String buildDnFromDomain(String domain) {
|
||||
return "DC=" + domain.replace(".", ",DC=");
|
||||
}
|
||||
|
||||
public static KeyUsage buildKeyUsage(Collection<String> keyUsages) {
|
||||
if (keyUsages == null || keyUsages.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int usageBits = 0;
|
||||
for (String keyUsageName : keyUsages) {
|
||||
Integer keyUsageValue = findKeyUsageByName(keyUsageName);
|
||||
if (keyUsageValue == null) {
|
||||
throw new IllegalArgumentException("Unknown keyUsage: " + keyUsageName);
|
||||
}
|
||||
usageBits |= keyUsageValue;
|
||||
}
|
||||
return new KeyUsage(usageBits);
|
||||
}
|
||||
|
||||
public static boolean isValidKeyUsage(String keyUsage) {
|
||||
return findKeyUsageByName(keyUsage) != null;
|
||||
}
|
||||
|
||||
private static Integer findKeyUsageByName(String keyUsageName) {
|
||||
if (keyUsageName == null) {
|
||||
return null;
|
||||
}
|
||||
return KEY_USAGE_MAPPINGS.get(keyUsageName.trim());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -403,7 +403,7 @@ class CertificateGenerateTool extends EnvironmentAwareCommand {
|
|||
// generate the CA keys and cert
|
||||
X500Principal x500Principal = new X500Principal(dn);
|
||||
KeyPair keyPair = CertGenUtils.generateKeyPair(keysize);
|
||||
Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, days);
|
||||
Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, days, null);
|
||||
final char[] password;
|
||||
if (prompt) {
|
||||
password = terminal.readSecret("Enter password for CA private key: ");
|
||||
|
|
|
@ -15,6 +15,7 @@ import joptsimple.OptionSpecBuilder;
|
|||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMEncryptor;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
|
@ -110,6 +111,7 @@ class CertificateTool extends MultiCommand {
|
|||
"[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1," + MAX_FILENAME_LENGTH + "}"
|
||||
);
|
||||
private static final int DEFAULT_KEY_SIZE = 2048;
|
||||
static final List<String> DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign");
|
||||
|
||||
// Older versions of OpenSSL had a max internal password length.
|
||||
// We issue warnings when writing files with passwords that would not be usable in those versions of OpenSSL.
|
||||
|
@ -202,6 +204,7 @@ class CertificateTool extends MultiCommand {
|
|||
final OptionSpec<String> outputPathSpec;
|
||||
final OptionSpec<String> outputPasswordSpec;
|
||||
final OptionSpec<Integer> keysizeSpec;
|
||||
OptionSpec<String> caKeyUsageSpec;
|
||||
|
||||
OptionSpec<Void> pemFormatSpec;
|
||||
OptionSpec<Integer> daysSpec;
|
||||
|
@ -274,6 +277,16 @@ class CertificateTool extends MultiCommand {
|
|||
inputFileSpec = parser.accepts("in", "file containing details of the instances in yaml format").withRequiredArg();
|
||||
}
|
||||
|
||||
final void acceptCertificateAuthorityKeyUsage() {
|
||||
caKeyUsageSpec = parser.accepts(
|
||||
"keyusage",
|
||||
"comma separated key usages to use for the generated CA. "
|
||||
+ "defaults to '"
|
||||
+ Strings.collectionToCommaDelimitedString(DEFAULT_CA_KEY_USAGE)
|
||||
+ "'"
|
||||
).withRequiredArg();
|
||||
}
|
||||
|
||||
// For testing
|
||||
OptionParser getParser() {
|
||||
return parser;
|
||||
|
@ -309,6 +322,23 @@ class CertificateTool extends MultiCommand {
|
|||
}
|
||||
}
|
||||
|
||||
final List<String> getCaKeyUsage(OptionSet options) {
|
||||
if (options.has(caKeyUsageSpec)) {
|
||||
final Function<String, Stream<? extends String>> splitByComma = v -> Stream.of(Strings.splitStringByCommaToArray(v));
|
||||
final List<String> caKeyUsage = caKeyUsageSpec.values(options)
|
||||
.stream()
|
||||
.flatMap(splitByComma)
|
||||
.filter(v -> false == Strings.isNullOrEmpty(v))
|
||||
.toList();
|
||||
if (caKeyUsage.isEmpty()) {
|
||||
return DEFAULT_CA_KEY_USAGE;
|
||||
}
|
||||
return caKeyUsage;
|
||||
} else {
|
||||
return DEFAULT_CA_KEY_USAGE;
|
||||
}
|
||||
}
|
||||
|
||||
final int getDays(OptionSet options) {
|
||||
if (options.has(daysSpec)) {
|
||||
return daysSpec.value(options);
|
||||
|
@ -396,7 +426,8 @@ class CertificateTool extends MultiCommand {
|
|||
}
|
||||
X500Principal x500Principal = new X500Principal(dn);
|
||||
KeyPair keyPair = CertGenUtils.generateKeyPair(getKeySize(options));
|
||||
X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options));
|
||||
final KeyUsage caKeyUsage = CertGenUtils.buildKeyUsage(getCaKeyUsage(options));
|
||||
X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options), caKeyUsage);
|
||||
|
||||
if (options.hasArgument(caPasswordSpec)) {
|
||||
char[] password = getChars(caPasswordSpec.value(options));
|
||||
|
@ -933,9 +964,7 @@ class CertificateTool extends MultiCommand {
|
|||
keyPair,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
days,
|
||||
null
|
||||
days
|
||||
);
|
||||
}
|
||||
return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate());
|
||||
|
@ -949,6 +978,7 @@ class CertificateTool extends MultiCommand {
|
|||
super("generate a new local certificate authority");
|
||||
acceptCertificateGenerationOptions();
|
||||
acceptsCertificateAuthorityName();
|
||||
acceptCertificateAuthorityKeyUsage();
|
||||
super.caPasswordSpec = super.outputPasswordSpec;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,8 @@ import java.time.format.DateTimeFormatter;
|
|||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
@ -80,7 +82,9 @@ import java.util.zip.ZipOutputStream;
|
|||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.generateSignedCertificate;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.isValidKeyUsage;
|
||||
|
||||
/**
|
||||
* This command is the "elasticsearch-certutil http" command. It provides a guided process for creating
|
||||
|
@ -95,7 +99,8 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
static final X500Principal DEFAULT_CA_NAME = new X500Principal("CN=Elasticsearch HTTP CA");
|
||||
static final int DEFAULT_CA_KEY_SIZE = DEFAULT_CERT_KEY_SIZE;
|
||||
static final Period DEFAULT_CA_VALIDITY = DEFAULT_CERT_VALIDITY;
|
||||
|
||||
static final List<String> DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign");
|
||||
static final List<String> DEFAULT_CERT_KEY_USAGE = List.of("digitalSignature", "keyEncipherment");
|
||||
private static final String ES_README_CSR = "es-readme-csr.txt";
|
||||
private static final String ES_YML_CSR = "es-sample-csr.yml";
|
||||
private static final String ES_README_P12 = "es-readme-p12.txt";
|
||||
|
@ -133,14 +138,24 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
final List<String> dnsNames;
|
||||
final List<String> ipNames;
|
||||
final int keySize;
|
||||
final List<String> keyUsage;
|
||||
final Period validity;
|
||||
|
||||
private CertOptions(String name, X500Principal subject, List<String> dnsNames, List<String> ipNames, int keySize, Period validity) {
|
||||
private CertOptions(
|
||||
String name,
|
||||
X500Principal subject,
|
||||
List<String> dnsNames,
|
||||
List<String> ipNames,
|
||||
int keySize,
|
||||
List<String> keyUsage,
|
||||
Period validity
|
||||
) {
|
||||
this.name = name;
|
||||
this.subject = subject;
|
||||
this.dnsNames = dnsNames;
|
||||
this.ipNames = ipNames;
|
||||
this.keySize = keySize;
|
||||
this.keyUsage = keyUsage;
|
||||
this.validity = validity;
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +209,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
terminal.println(Terminal.Verbosity.VERBOSE, "\tDNS Names: " + Strings.collectionToCommaDelimitedString(cert.dnsNames));
|
||||
terminal.println(Terminal.Verbosity.VERBOSE, "\tIP Names: " + Strings.collectionToCommaDelimitedString(cert.ipNames));
|
||||
terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Size: " + cert.keySize);
|
||||
terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Usage: " + Strings.collectionToCommaDelimitedString(cert.keyUsage));
|
||||
terminal.println(Terminal.Verbosity.VERBOSE, "\tValidity: " + toString(cert.validity));
|
||||
certificates.add(cert);
|
||||
|
||||
|
@ -339,6 +355,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
keyPair,
|
||||
cert.subject,
|
||||
sanList,
|
||||
buildKeyUsage(cert.keyUsage),
|
||||
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
|
||||
);
|
||||
final String csrFile = "http-" + cert.name + ".csr";
|
||||
|
@ -372,6 +389,7 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
notBefore,
|
||||
notAfter,
|
||||
null,
|
||||
buildKeyUsage(cert.keyUsage),
|
||||
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
|
||||
);
|
||||
|
||||
|
@ -692,10 +710,12 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
}
|
||||
X500Principal dn = buildDistinguishedName(certName);
|
||||
int keySize = DEFAULT_CERT_KEY_SIZE;
|
||||
List<String> keyUsage = DEFAULT_CERT_KEY_USAGE;
|
||||
while (true) {
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Key Name: " + certName);
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
|
||||
terminal.println(Terminal.Verbosity.SILENT, "");
|
||||
if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
|
||||
break;
|
||||
|
@ -736,9 +756,22 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
|
||||
keySize = readKeySize(terminal, keySize);
|
||||
terminal.println("");
|
||||
|
||||
printHeader("What key usage should your certificate have?", terminal);
|
||||
terminal.println("The key usage extension defines the purpose of the key contained in the certificate.");
|
||||
terminal.println("The usage restriction might be employed when a key, that could be used for more than ");
|
||||
terminal.println("one operation, is to be restricted.");
|
||||
terminal.println("You may enter the key usage as a comma-delimited list of following values: ");
|
||||
for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) {
|
||||
terminal.println(" - " + keyUsageName);
|
||||
}
|
||||
terminal.println("");
|
||||
|
||||
keyUsage = readKeyUsage(terminal, keyUsage);
|
||||
terminal.println("");
|
||||
}
|
||||
|
||||
return new CertOptions(certName, dn, dnsNames, ipNames, keySize, validity);
|
||||
return new CertOptions(certName, dn, dnsNames, ipNames, keySize, keyUsage, validity);
|
||||
}
|
||||
|
||||
private static String validateHostname(String name) {
|
||||
|
@ -859,10 +892,12 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
X500Principal dn = DEFAULT_CA_NAME;
|
||||
Period validity = DEFAULT_CA_VALIDITY;
|
||||
int keySize = DEFAULT_CA_KEY_SIZE;
|
||||
List<String> keyUsage = DEFAULT_CA_KEY_USAGE;
|
||||
while (true) {
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Validity: " + toString(validity));
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
|
||||
terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
|
||||
terminal.println(Terminal.Verbosity.SILENT, "");
|
||||
if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
|
||||
break;
|
||||
|
@ -904,13 +939,38 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
|
||||
keySize = readKeySize(terminal, keySize);
|
||||
terminal.println("");
|
||||
|
||||
printHeader("What key usage should your CA have?", terminal);
|
||||
terminal.println("The key usage extension defines the purpose of the key contained in the certificate.");
|
||||
terminal.println("The usage restriction might be employed when a key, that could be used for more than ");
|
||||
terminal.println("one operation, is to be restricted.");
|
||||
terminal.println("You may enter the key usage as a comma-delimited list of following values: ");
|
||||
for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) {
|
||||
terminal.println(" - " + keyUsageName);
|
||||
}
|
||||
terminal.println("");
|
||||
|
||||
keyUsage = readKeyUsage(terminal, keyUsage);
|
||||
terminal.println("");
|
||||
}
|
||||
|
||||
try {
|
||||
final KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
|
||||
final ZonedDateTime notBefore = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final ZonedDateTime notAfter = notBefore.plus(validity);
|
||||
X509Certificate caCert = generateSignedCertificate(dn, null, keyPair, null, null, true, notBefore, notAfter, null);
|
||||
X509Certificate caCert = generateSignedCertificate(
|
||||
dn,
|
||||
null,
|
||||
keyPair,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
notBefore,
|
||||
notAfter,
|
||||
null,
|
||||
buildKeyUsage(keyUsage),
|
||||
Set.of()
|
||||
);
|
||||
|
||||
printHeader("CA password", terminal);
|
||||
terminal.println("We recommend that you protect your CA private key with a strong password.");
|
||||
|
@ -979,6 +1039,31 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
});
|
||||
}
|
||||
|
||||
private static List<String> readKeyUsage(Terminal terminal, List<String> defaultKeyUsage) {
|
||||
return tryReadInput(terminal, "Key Usage", defaultKeyUsage, input -> {
|
||||
final String[] keyUsages = input.split(",");
|
||||
final List<String> resolvedKeyUsages = new ArrayList<>(keyUsages.length);
|
||||
for (String keyUsage : keyUsages) {
|
||||
keyUsage = keyUsage.trim();
|
||||
if (keyUsage.isEmpty()) {
|
||||
terminal.println("Key usage cannot be blank or empty");
|
||||
return null;
|
||||
}
|
||||
if (isValidKeyUsage(keyUsage) == false) {
|
||||
terminal.println("Invalid key usage: " + keyUsage);
|
||||
terminal.println("The key usage should be one of the following values: ");
|
||||
for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) {
|
||||
terminal.println(" - " + keyUsageName);
|
||||
}
|
||||
terminal.println("");
|
||||
return null;
|
||||
}
|
||||
resolvedKeyUsages.add(keyUsage);
|
||||
}
|
||||
return Collections.unmodifiableList(resolvedKeyUsages);
|
||||
});
|
||||
}
|
||||
|
||||
private static char[] readPassword(Terminal terminal, String prompt, boolean confirm) {
|
||||
while (true) {
|
||||
final char[] password = terminal.readSecret(prompt + " [<ENTER> for none]");
|
||||
|
@ -1080,7 +1165,14 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
|
|||
}
|
||||
|
||||
private static <T> T tryReadInput(Terminal terminal, String prompt, T defaultValue, Function<String, T> parser) {
|
||||
final String defaultStr = defaultValue instanceof Period ? toString((Period) defaultValue) : String.valueOf(defaultValue);
|
||||
final String defaultStr;
|
||||
if (defaultValue instanceof Period) {
|
||||
defaultStr = toString((Period) defaultValue);
|
||||
} else if (defaultValue instanceof Collection<?> collection) {
|
||||
defaultStr = Strings.collectionToCommaDelimitedString(collection);
|
||||
} else {
|
||||
defaultStr = String.valueOf(defaultValue);
|
||||
}
|
||||
while (true) {
|
||||
final String input = terminal.readText(prompt + " [" + defaultStr + "] ");
|
||||
if (Strings.isEmpty(input)) {
|
||||
|
|
|
@ -37,6 +37,7 @@ import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.AUTO_CONFIG
|
|||
import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.AUTO_CONFIG_TRANSPORT_ALT_DN;
|
||||
import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.anyRemoteHostNodeAddress;
|
||||
import static org.elasticsearch.xpack.security.cli.AutoConfigureNode.removePreviousAutoconfiguration;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtilsTests.assertExpectedKeyUsage;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
|
@ -149,7 +150,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
public void testGeneratedHTTPCertificateSANs() throws Exception {
|
||||
public void testGeneratedHTTPCertificateSANsAndKeyUsage() throws Exception {
|
||||
// test no publish settings
|
||||
Path tempDir = createTempDir();
|
||||
try {
|
||||
|
@ -180,7 +181,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
|
|||
assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true));
|
||||
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(true));
|
||||
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false));
|
||||
verifyExtendedKeyUsage(httpCertificate);
|
||||
verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
|
@ -202,7 +203,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
|
|||
assertThat(checkGeneralNameSan(httpCertificate, "localhost", GeneralName.dNSName), is(true));
|
||||
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false));
|
||||
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(true));
|
||||
verifyExtendedKeyUsage(httpCertificate);
|
||||
verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
|
@ -228,7 +229,7 @@ public class AutoConfigureNodeTests extends ESTestCase {
|
|||
assertThat(checkGeneralNameSan(httpCertificate, "balkan.beast", GeneralName.dNSName), is(true));
|
||||
assertThat(checkGeneralNameSan(httpCertificate, "172.168.1.100", GeneralName.iPAddress), is(false));
|
||||
assertThat(checkGeneralNameSan(httpCertificate, "10.10.10.100", GeneralName.iPAddress), is(false));
|
||||
verifyExtendedKeyUsage(httpCertificate);
|
||||
verifyKeyUsageAndExtendedKeyUsage(httpCertificate);
|
||||
} finally {
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
|
@ -288,11 +289,12 @@ public class AutoConfigureNodeTests extends ESTestCase {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void verifyExtendedKeyUsage(X509Certificate httpCertificate) throws Exception {
|
||||
private void verifyKeyUsageAndExtendedKeyUsage(X509Certificate httpCertificate) throws Exception {
|
||||
List<String> extendedKeyUsage = httpCertificate.getExtendedKeyUsage();
|
||||
assertEquals("Only one extended key usage expected for HTTP certificate.", 1, extendedKeyUsage.size());
|
||||
String expectedServerAuthUsage = KeyPurposeId.id_kp_serverAuth.toASN1Primitive().toString();
|
||||
assertEquals("Expected serverAuth extended key usage.", expectedServerAuthUsage, extendedKeyUsage.get(0));
|
||||
assertExpectedKeyUsage(httpCertificate, HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE);
|
||||
}
|
||||
|
||||
private X509Certificate runAutoConfigAndReturnHTTPCertificate(Path configDir, Settings settings) throws Exception {
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
|
||||
package org.elasticsearch.xpack.security.cli;
|
||||
|
||||
import com.unboundid.util.ssl.cert.KeyUsageExtension;
|
||||
|
||||
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.elasticsearch.common.network.InetAddresses;
|
||||
import org.elasticsearch.common.network.NetworkAddress;
|
||||
import org.elasticsearch.core.SuppressForbidden;
|
||||
|
@ -27,22 +30,57 @@ import java.time.ZonedDateTime;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.KEY_USAGE_MAPPINGS;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtils.isValidKeyUsage;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
/**
|
||||
* Unit tests for cert utils
|
||||
*/
|
||||
public class CertGenUtilsTests extends ESTestCase {
|
||||
|
||||
/**
|
||||
* The mapping of key usage names to their corresponding bit index as defined in {@code KeyUsage} class:
|
||||
*
|
||||
* <ul>
|
||||
* <li>digitalSignature (0)</li>
|
||||
* <li>nonRepudiation (1)</li>
|
||||
* <li>keyEncipherment (2)</li>
|
||||
* <li>dataEncipherment (3)</li>
|
||||
* <li>keyAgreement (4)</li>
|
||||
* <li>keyCertSign (5)</li>
|
||||
* <li>cRLSign (6)</li>
|
||||
* <li>encipherOnly (7)</li>
|
||||
* <li>decipherOnly (8)</li>
|
||||
* </ul>
|
||||
*/
|
||||
private static final Map<String, Integer> KEY_USAGE_BITS = Map.ofEntries(
|
||||
Map.entry("digitalSignature", 0),
|
||||
Map.entry("nonRepudiation", 1),
|
||||
Map.entry("keyEncipherment", 2),
|
||||
Map.entry("dataEncipherment", 3),
|
||||
Map.entry("keyAgreement", 4),
|
||||
Map.entry("keyCertSign", 5),
|
||||
Map.entry("cRLSign", 6),
|
||||
Map.entry("encipherOnly", 7),
|
||||
Map.entry("decipherOnly", 8)
|
||||
);
|
||||
|
||||
@BeforeClass
|
||||
public static void muteInFips() {
|
||||
assumeFalse("Can't run in a FIPS JVM", inFipsJvm());
|
||||
|
@ -103,6 +141,7 @@ public class CertGenUtilsTests extends ESTestCase {
|
|||
// root CA
|
||||
final X500Principal rootCaPrincipal = new X500Principal("DC=example.com");
|
||||
final KeyPair rootCaKeyPair = CertGenUtils.generateKeyPair(2048);
|
||||
final List<String> rootCaKeyUsages = List.of("keyCertSign", "cRLSign");
|
||||
final X509Certificate rootCaCert = CertGenUtils.generateSignedCertificate(
|
||||
rootCaPrincipal,
|
||||
null,
|
||||
|
@ -112,12 +151,15 @@ public class CertGenUtilsTests extends ESTestCase {
|
|||
true,
|
||||
notBefore,
|
||||
notAfter,
|
||||
null
|
||||
null,
|
||||
buildKeyUsage(rootCaKeyUsages),
|
||||
Set.of()
|
||||
);
|
||||
|
||||
// sub CA
|
||||
final X500Principal subCaPrincipal = new X500Principal("DC=Sub CA,DC=example.com");
|
||||
final KeyPair subCaKeyPair = CertGenUtils.generateKeyPair(2048);
|
||||
final List<String> subCaKeyUsage = List.of("digitalSignature", "keyCertSign", "cRLSign");
|
||||
final X509Certificate subCaCert = CertGenUtils.generateSignedCertificate(
|
||||
subCaPrincipal,
|
||||
null,
|
||||
|
@ -127,12 +169,15 @@ public class CertGenUtilsTests extends ESTestCase {
|
|||
true,
|
||||
notBefore,
|
||||
notAfter,
|
||||
null
|
||||
null,
|
||||
buildKeyUsage(subCaKeyUsage),
|
||||
Set.of()
|
||||
);
|
||||
|
||||
// end entity
|
||||
final X500Principal endEntityPrincipal = new X500Principal("CN=TLS Client\\+Server,DC=Sub CA,DC=example.com");
|
||||
final KeyPair endEntityKeyPair = CertGenUtils.generateKeyPair(2048);
|
||||
final List<String> endEntityKeyUsage = randomBoolean() ? null : List.of("digitalSignature", "keyEncipherment");
|
||||
final X509Certificate endEntityCert = CertGenUtils.generateSignedCertificate(
|
||||
endEntityPrincipal,
|
||||
null,
|
||||
|
@ -143,6 +188,7 @@ public class CertGenUtilsTests extends ESTestCase {
|
|||
notBefore,
|
||||
notAfter,
|
||||
null,
|
||||
buildKeyUsage(endEntityKeyUsage),
|
||||
Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage))
|
||||
);
|
||||
|
||||
|
@ -162,6 +208,101 @@ public class CertGenUtilsTests extends ESTestCase {
|
|||
trustStore.setCertificateEntry("trustAnchor", rootCaCert); // anchor: any part of the chain, or issuer of last entry in chain
|
||||
|
||||
validateEndEntityTlsChain(trustStore, certChain, true, true);
|
||||
|
||||
// verify custom key usages
|
||||
assertExpectedKeyUsage(rootCaCert, rootCaKeyUsages);
|
||||
assertExpectedKeyUsage(subCaCert, subCaKeyUsage);
|
||||
// when key usage is not specified, the key usage bits should be null
|
||||
if (endEntityKeyUsage == null) {
|
||||
assertThat(endEntityCert.getKeyUsage(), is(nullValue()));
|
||||
assertThat(endEntityCert.getCriticalExtensionOIDs().contains(KeyUsageExtension.KEY_USAGE_OID.toString()), is(false));
|
||||
} else {
|
||||
assertExpectedKeyUsage(endEntityCert, endEntityKeyUsage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void testBuildKeyUsage() {
|
||||
// sanity check that lookup maps are containing the same keyUsage entries
|
||||
assertThat(KEY_USAGE_BITS.keySet(), containsInAnyOrder(KEY_USAGE_MAPPINGS.keySet().toArray()));
|
||||
|
||||
// passing null or empty list of keyUsage names should return null
|
||||
assertThat(buildKeyUsage(null), is(nullValue()));
|
||||
assertThat(buildKeyUsage(List.of()), is(nullValue()));
|
||||
|
||||
// invalid names should throw IAE
|
||||
var e = expectThrows(IllegalArgumentException.class, () -> buildKeyUsage(List.of(randomAlphanumericOfLength(5))));
|
||||
assertThat(e.getMessage(), containsString("Unknown keyUsage"));
|
||||
|
||||
{
|
||||
final List<String> keyUsages = randomNonEmptySubsetOf(KEY_USAGE_MAPPINGS.keySet());
|
||||
final KeyUsage keyUsage = buildKeyUsage(keyUsages);
|
||||
for (String usageName : keyUsages) {
|
||||
final Integer usage = KEY_USAGE_MAPPINGS.get(usageName);
|
||||
assertThat(" mapping for keyUsage [" + usageName + "] is missing", usage, is(notNullValue()));
|
||||
assertThat("expected keyUsage [" + usageName + "] to be set in [" + keyUsage + "]", keyUsage.hasUsages(usage), is(true));
|
||||
}
|
||||
|
||||
final Set<String> keyUsagesNotSet = KEY_USAGE_MAPPINGS.keySet()
|
||||
.stream()
|
||||
.filter(u -> keyUsages.contains(u) == false)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (String usageName : keyUsagesNotSet) {
|
||||
final Integer usage = KEY_USAGE_MAPPINGS.get(usageName);
|
||||
assertThat(" mapping for keyUsage [" + usageName + "] is missing", usage, is(notNullValue()));
|
||||
assertThat(
|
||||
"expected keyUsage [" + usageName + "] not to be set in [" + keyUsage + "]",
|
||||
keyUsage.hasUsages(usage),
|
||||
is(false)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
// test that duplicates and whitespaces are ignored
|
||||
KeyUsage keyUsage = buildKeyUsage(
|
||||
List.of("digitalSignature ", " nonRepudiation", "\tkeyEncipherment", "keyEncipherment\n")
|
||||
);
|
||||
assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("digitalSignature")), is(true));
|
||||
assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("nonRepudiation")), is(true));
|
||||
assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("digitalSignature")), is(true));
|
||||
assertThat(keyUsage.hasUsages(KEY_USAGE_MAPPINGS.get("keyEncipherment")), is(true));
|
||||
}
|
||||
}
|
||||
|
||||
public void testIsValidKeyUsage() {
|
||||
assertThat(isValidKeyUsage(randomFrom(KEY_USAGE_MAPPINGS.keySet())), is(true));
|
||||
assertThat(isValidKeyUsage(randomAlphanumericOfLength(5)), is(false));
|
||||
|
||||
// keyUsage names are case-sensitive
|
||||
assertThat(isValidKeyUsage("DigitalSignature"), is(false));
|
||||
|
||||
// white-spaces are ignored
|
||||
assertThat(isValidKeyUsage("keyAgreement "), is(true));
|
||||
assertThat(isValidKeyUsage("keyCertSign\n"), is(true));
|
||||
assertThat(isValidKeyUsage("\tcRLSign "), is(true));
|
||||
}
|
||||
|
||||
public static void assertExpectedKeyUsage(X509Certificate certificate, List<String> expectedKeyUsage) {
|
||||
final boolean[] keyUsage = certificate.getKeyUsage();
|
||||
assertThat("Expected " + KEY_USAGE_BITS.size() + " bits for key usage", keyUsage.length, equalTo(KEY_USAGE_BITS.size()));
|
||||
final Set<Integer> expectedBitsToBeSet = expectedKeyUsage.stream().map(KEY_USAGE_BITS::get).collect(Collectors.toSet());
|
||||
|
||||
for (int i = 0; i < keyUsage.length; i++) {
|
||||
if (expectedBitsToBeSet.contains(i)) {
|
||||
assertThat("keyUsage bit [" + i + "] expected to be set: " + expectedKeyUsage, keyUsage[i], equalTo(true));
|
||||
} else {
|
||||
assertThat("keyUsage bit [" + i + "] not expected to be set: " + expectedKeyUsage, keyUsage[i], equalTo(false));
|
||||
}
|
||||
}
|
||||
// key usage must be marked as critical
|
||||
assertThat(
|
||||
"keyUsage extension should be marked as critical",
|
||||
certificate.getCriticalExtensionOIDs().contains(KeyUsageExtension.KEY_USAGE_OID.toString()),
|
||||
is(true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -274,7 +274,7 @@ public class CertificateGenerateToolTests extends ESTestCase {
|
|||
final int keysize = randomFrom(1024, 2048);
|
||||
final int days = randomIntBetween(1, 1024);
|
||||
KeyPair keyPair = CertGenUtils.generateKeyPair(keysize);
|
||||
X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
|
||||
X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days, null);
|
||||
|
||||
final boolean generatedCa = randomBoolean();
|
||||
final char[] keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD.toCharArray() : null;
|
||||
|
|
|
@ -415,7 +415,13 @@ public class CertificateToolTests extends ESTestCase {
|
|||
int days = randomIntBetween(1, 1024);
|
||||
|
||||
KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
|
||||
X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days);
|
||||
List<String> caKeyUsage = randomBoolean() ? null : CertificateTool.DEFAULT_CA_KEY_USAGE;
|
||||
X509Certificate caCert = CertGenUtils.generateCACertificate(
|
||||
new X500Principal("CN=test ca"),
|
||||
keyPair,
|
||||
days,
|
||||
CertGenUtils.buildKeyUsage(caKeyUsage)
|
||||
);
|
||||
|
||||
final boolean selfSigned = randomBoolean();
|
||||
final String keyPassword = randomBoolean() ? SecuritySettingsSourceField.TEST_PASSWORD : null;
|
||||
|
@ -1191,6 +1197,7 @@ public class CertificateToolTests extends ESTestCase {
|
|||
final int caKeySize = randomIntBetween(4, 8) * 512;
|
||||
final int days = randomIntBetween(7, 1500);
|
||||
final String caPassword = randomFrom("", randomAlphaOfLengthBetween(4, 80));
|
||||
final String caKeyUsage = randomFrom("", Strings.collectionToCommaDelimitedString(CertificateTool.DEFAULT_CA_KEY_USAGE));
|
||||
|
||||
final CertificateAuthorityCommand caCommand = new PathAwareCertificateAuthorityCommand(caFile);
|
||||
String[] args = {
|
||||
|
@ -1203,7 +1210,9 @@ public class CertificateToolTests extends ESTestCase {
|
|||
"-keysize",
|
||||
String.valueOf(caKeySize),
|
||||
"-days",
|
||||
String.valueOf(days) };
|
||||
String.valueOf(days),
|
||||
"-keyusage",
|
||||
caKeyUsage };
|
||||
if (pem) {
|
||||
args = ArrayUtils.append(args, "--pem");
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.bouncycastle.asn1.x509.Extensions;
|
|||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
|
@ -30,6 +31,7 @@ import org.bouncycastle.util.io.pem.PemReader;
|
|||
import org.elasticsearch.cli.MockTerminal;
|
||||
import org.elasticsearch.cli.ProcessInfo;
|
||||
import org.elasticsearch.common.CheckedBiFunction;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.network.NetworkAddress;
|
||||
import org.elasticsearch.common.ssl.PemUtils;
|
||||
import org.elasticsearch.core.CheckedFunction;
|
||||
|
@ -89,10 +91,12 @@ import javax.security.auth.x500.X500Principal;
|
|||
import static org.elasticsearch.test.FileMatchers.isDirectory;
|
||||
import static org.elasticsearch.test.FileMatchers.isRegularFile;
|
||||
import static org.elasticsearch.test.FileMatchers.pathExists;
|
||||
import static org.elasticsearch.xpack.security.cli.CertGenUtilsTests.assertExpectedKeyUsage;
|
||||
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.guessFileType;
|
||||
import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
|
||||
import static org.hamcrest.Matchers.arrayWithSize;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.in;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
@ -369,21 +373,25 @@ public class HttpCertificateCommandTests extends ESTestCase {
|
|||
final String caDN;
|
||||
final int caYears;
|
||||
final int caKeySize;
|
||||
final List<String> caKeyUsage;
|
||||
// randomise whether to change CA defaults.
|
||||
if (randomBoolean()) {
|
||||
terminal.addTextInput("y"); // Change defaults
|
||||
caDN = "CN=" + randomAlphaOfLengthBetween(3, 8);
|
||||
caYears = randomIntBetween(1, 3);
|
||||
caKeySize = randomFrom(2048, 3072, 4096);
|
||||
caKeyUsage = randomSubsetOf(CertGenUtils.KEY_USAGE_MAPPINGS.keySet());
|
||||
terminal.addTextInput(caDN);
|
||||
terminal.addTextInput(caYears + "y");
|
||||
terminal.addTextInput(Integer.toString(caKeySize));
|
||||
terminal.addTextInput(Strings.collectionToCommaDelimitedString(caKeyUsage));
|
||||
terminal.addTextInput("n"); // Don't change values
|
||||
} else {
|
||||
terminal.addTextInput(randomBoolean() ? "n" : ""); // Don't change defaults
|
||||
caDN = HttpCertificateCommand.DEFAULT_CA_NAME.toString();
|
||||
caYears = HttpCertificateCommand.DEFAULT_CA_VALIDITY.getYears();
|
||||
caKeySize = HttpCertificateCommand.DEFAULT_CA_KEY_SIZE;
|
||||
caKeyUsage = HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
|
||||
}
|
||||
|
||||
final String caPassword = randomPassword(randomBoolean());
|
||||
|
@ -463,6 +471,7 @@ public class HttpCertificateCommandTests extends ESTestCase {
|
|||
verifyCertificate(caCertKey.v1(), caDN.replaceFirst("CN=", ""), caYears, List.of(), List.of());
|
||||
assertThat(getRSAKeySize(caCertKey.v1().getPublicKey()), is(caKeySize));
|
||||
assertThat(getRSAKeySize(caCertKey.v2()), is(caKeySize));
|
||||
assertExpectedKeyUsage(caCertKey.v1(), caKeyUsage);
|
||||
|
||||
assertThat(zipRoot.resolve("elasticsearch"), isDirectory());
|
||||
|
||||
|
@ -486,6 +495,7 @@ public class HttpCertificateCommandTests extends ESTestCase {
|
|||
verifyChain(certAndKey.v1(), caCertKey.v1());
|
||||
assertThat(getRSAKeySize(certAndKey.v1().getPublicKey()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
|
||||
assertThat(getRSAKeySize(certAndKey.v2()), is(HttpCertificateCommand.DEFAULT_CERT_KEY_SIZE));
|
||||
assertExpectedKeyUsage(certAndKey.v1(), HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE);
|
||||
|
||||
// Verify the README
|
||||
assertThat(readme, containsString(p12Path.getFileName().toString()));
|
||||
|
@ -692,7 +702,10 @@ public class HttpCertificateCommandTests extends ESTestCase {
|
|||
// We register 1 extension with the subject alternative names and extended key usage
|
||||
final Extensions extensions = Extensions.getInstance(extensionAttributes[0].getAttributeValues()[0]);
|
||||
assertThat(extensions, notNullValue());
|
||||
assertThat(extensions.getExtensionOIDs(), arrayWithSize(2));
|
||||
assertThat(
|
||||
extensions.getExtensionOIDs(),
|
||||
arrayContainingInAnyOrder(Extension.subjectAlternativeName, Extension.keyUsage, Extension.extendedKeyUsage)
|
||||
);
|
||||
|
||||
final GeneralNames names = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName);
|
||||
assertThat(names.getNames(), arrayWithSize(hostNames.size() + ipAddresses.size()));
|
||||
|
@ -709,6 +722,9 @@ public class HttpCertificateCommandTests extends ESTestCase {
|
|||
|
||||
ExtendedKeyUsage extendedKeyUsage = ExtendedKeyUsage.fromExtensions(extensions);
|
||||
assertThat(extendedKeyUsage.getUsages(), arrayContainingInAnyOrder(KeyPurposeId.id_kp_serverAuth));
|
||||
|
||||
KeyUsage keyUsage = KeyUsage.fromExtensions(extensions);
|
||||
assertThat(keyUsage, is(equalTo(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment))));
|
||||
}
|
||||
|
||||
private void verifyCertificate(
|
||||
|
|
Loading…
Reference in New Issue