Skip to content

Commit 2a77f86

Browse files
make default HTTP keyUsage overridable
1 parent 11e96ee commit 2a77f86

File tree

6 files changed

+243
-35
lines changed

6 files changed

+243
-35
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.bouncycastle.asn1.x509.GeneralName;
1616
import org.bouncycastle.asn1.x509.GeneralNames;
1717
import org.bouncycastle.asn1.x509.KeyPurposeId;
18-
import org.bouncycastle.asn1.x509.KeyUsage;
1918
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
2019
import org.elasticsearch.ExceptionsHelper;
2120
import org.elasticsearch.cli.ExitCodes;
@@ -103,6 +102,9 @@
103102
import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
104103
import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
105104
import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL;
105+
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
106+
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
107+
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE;
106108

107109
/**
108110
* Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
@@ -444,7 +446,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
444446
true,
445447
HTTP_CA_CERTIFICATE_DAYS,
446448
SIGNATURE_ALGORITHM,
447-
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign),
449+
buildKeyUsage(DEFAULT_CA_KEY_USAGE),
448450
Set.of()
449451
);
450452
} catch (Throwable t) {
@@ -471,7 +473,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
471473
false,
472474
HTTP_CERTIFICATE_DAYS,
473475
SIGNATURE_ALGORITHM,
474-
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment),
476+
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
475477
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
476478
);
477479

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@
5454
import java.sql.Date;
5555
import java.time.ZoneOffset;
5656
import java.time.ZonedDateTime;
57+
import java.util.Collection;
5758
import java.util.HashSet;
5859
import java.util.Locale;
60+
import java.util.Map;
5961
import java.util.Objects;
6062
import java.util.Set;
6163

@@ -74,6 +76,66 @@ public class CertGenUtils {
7476
private static final int SERIAL_BIT_LENGTH = 20 * 8;
7577
private static final BouncyCastleProvider BC_PROV = new BouncyCastleProvider();
7678

79+
/**
80+
* The mapping of key usage names to their corresponding integer values as defined in {@code KeyUsage} class.
81+
*/
82+
public static final Map<String, Integer> KEY_USAGE_MAPPINGS = Map.of(
83+
"digitalSignature",
84+
KeyUsage.digitalSignature,
85+
"nonRepudiation",
86+
KeyUsage.nonRepudiation,
87+
"keyEncipherment",
88+
KeyUsage.keyEncipherment,
89+
"dataEncipherment",
90+
KeyUsage.dataEncipherment,
91+
"keyAgreement",
92+
KeyUsage.keyAgreement,
93+
"keyCertSign",
94+
KeyUsage.keyCertSign,
95+
"cRLSign",
96+
KeyUsage.cRLSign,
97+
"encipherOnly",
98+
KeyUsage.encipherOnly,
99+
"decipherOnly",
100+
KeyUsage.decipherOnly
101+
);
102+
103+
/**
104+
* The mapping of key usage names to their corresponding bit index as defined in {@code KeyUsage} class:
105+
*
106+
* <ul>
107+
* <li>digitalSignature (0)</li>
108+
* <li>nonRepudiation (1)</li>
109+
* <li>keyEncipherment (2)</li>
110+
* <li>dataEncipherment (3)</li>
111+
* <li>keyAgreement (4)</li>
112+
* <li>keyCertSign (5)</li>
113+
* <li>cRLSign (6)</li>
114+
* <li>encipherOnly (7)</li>
115+
* <li>decipherOnly (8)</li>
116+
* </ul>
117+
*/
118+
public static final Map<String, Integer> KEY_USAGE_BITS = Map.of(
119+
"digitalSignature",
120+
0,
121+
"nonRepudiation",
122+
1,
123+
"keyEncipherment",
124+
2,
125+
"dataEncipherment",
126+
3,
127+
"keyAgreement",
128+
4,
129+
"keyCertSign",
130+
5,
131+
"cRLSign",
132+
6,
133+
"encipherOnly",
134+
7,
135+
"decipherOnly",
136+
8
137+
);
138+
77139
private CertGenUtils() {}
78140

79141
/**
@@ -403,4 +465,24 @@ public static GeneralName createCommonName(String cn) {
403465
public static String buildDnFromDomain(String domain) {
404466
return "DC=" + domain.replace(".", ",DC=");
405467
}
468+
469+
public static KeyUsage buildKeyUsage(Collection<String> keyUsages) {
470+
if (keyUsages == null || keyUsages.isEmpty()) {
471+
return null;
472+
}
473+
474+
int usageBits = 0;
475+
for (String keyUsage : keyUsages) {
476+
Integer keyUsageValue = KEY_USAGE_MAPPINGS.get(keyUsage);
477+
if (keyUsageValue == null) {
478+
throw new IllegalArgumentException("Unknown keyUsage: " + keyUsage);
479+
}
480+
usageBits |= keyUsageValue;
481+
}
482+
return new KeyUsage(usageBits);
483+
}
484+
485+
public static boolean isValidKeyUsage(String keyUsage) {
486+
return KEY_USAGE_MAPPINGS.containsKey(keyUsage);
487+
}
406488
}

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

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
1515
import org.bouncycastle.asn1.x509.GeneralNames;
1616
import org.bouncycastle.asn1.x509.KeyPurposeId;
17-
import org.bouncycastle.asn1.x509.KeyUsage;
1817
import org.bouncycastle.cert.CertIOException;
1918
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
2019
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
@@ -69,6 +68,7 @@
6968
import java.time.format.DateTimeParseException;
7069
import java.util.ArrayList;
7170
import java.util.Arrays;
71+
import java.util.Collection;
7272
import java.util.List;
7373
import java.util.Locale;
7474
import java.util.Map;
@@ -81,6 +81,7 @@
8181

8282
import javax.security.auth.x500.X500Principal;
8383

84+
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
8485
import static org.elasticsearch.xpack.security.cli.CertGenUtils.generateSignedCertificate;
8586

8687
/**
@@ -96,7 +97,8 @@ class HttpCertificateCommand extends EnvironmentAwareCommand {
9697
static final X500Principal DEFAULT_CA_NAME = new X500Principal("CN=Elasticsearch HTTP CA");
9798
static final int DEFAULT_CA_KEY_SIZE = DEFAULT_CERT_KEY_SIZE;
9899
static final Period DEFAULT_CA_VALIDITY = DEFAULT_CERT_VALIDITY;
99-
100+
static final List<String> DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign");
101+
static final List<String> DEFAULT_CERT_KEY_USAGE = List.of("digitalSignature", "keyEncipherment");
100102
private static final String ES_README_CSR = "es-readme-csr.txt";
101103
private static final String ES_YML_CSR = "es-sample-csr.yml";
102104
private static final String ES_README_P12 = "es-readme-p12.txt";
@@ -134,14 +136,24 @@ private class CertOptions {
134136
final List<String> dnsNames;
135137
final List<String> ipNames;
136138
final int keySize;
139+
final List<String> keyUsage;
137140
final Period validity;
138141

139-
private CertOptions(String name, X500Principal subject, List<String> dnsNames, List<String> ipNames, int keySize, Period validity) {
142+
private CertOptions(
143+
String name,
144+
X500Principal subject,
145+
List<String> dnsNames,
146+
List<String> ipNames,
147+
int keySize,
148+
List<String> keyUsage,
149+
Period validity
150+
) {
140151
this.name = name;
141152
this.subject = subject;
142153
this.dnsNames = dnsNames;
143154
this.ipNames = ipNames;
144155
this.keySize = keySize;
156+
this.keyUsage = keyUsage;
145157
this.validity = validity;
146158
}
147159
}
@@ -195,6 +207,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
195207
terminal.println(Terminal.Verbosity.VERBOSE, "\tDNS Names: " + Strings.collectionToCommaDelimitedString(cert.dnsNames));
196208
terminal.println(Terminal.Verbosity.VERBOSE, "\tIP Names: " + Strings.collectionToCommaDelimitedString(cert.ipNames));
197209
terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Size: " + cert.keySize);
210+
terminal.println(Terminal.Verbosity.VERBOSE, "\tKey Usage: " + Strings.collectionToCommaDelimitedString(cert.keyUsage));
198211
terminal.println(Terminal.Verbosity.VERBOSE, "\tValidity: " + toString(cert.validity));
199212
certificates.add(cert);
200213

@@ -340,7 +353,7 @@ private void writeCertificateAndKeyDetails(
340353
keyPair,
341354
cert.subject,
342355
sanList,
343-
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment),
356+
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
344357
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
345358
);
346359
final String csrFile = "http-" + cert.name + ".csr";
@@ -374,7 +387,7 @@ private void writeCertificateAndKeyDetails(
374387
notBefore,
375388
notAfter,
376389
null,
377-
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment),
390+
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
378391
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
379392
);
380393

@@ -695,10 +708,12 @@ private CertOptions getCertificateConfiguration(
695708
}
696709
X500Principal dn = buildDistinguishedName(certName);
697710
int keySize = DEFAULT_CERT_KEY_SIZE;
711+
List<String> keyUsage = DEFAULT_CERT_KEY_USAGE;
698712
while (true) {
699713
terminal.println(Terminal.Verbosity.SILENT, "Key Name: " + certName);
700714
terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
701715
terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
716+
terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
702717
terminal.println(Terminal.Verbosity.SILENT, "");
703718
if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
704719
break;
@@ -739,9 +754,21 @@ private CertOptions getCertificateConfiguration(
739754

740755
keySize = readKeySize(terminal, keySize);
741756
terminal.println("");
757+
758+
printHeader("What key usage should your certificate have?", terminal);
759+
terminal.println("The key usage extension defines the purpose of the key contained in the certificate.");
760+
terminal.println(
761+
"The usage restriction might be employed when a key, that could be used for more than one operation, is to be restricted."
762+
);
763+
terminal.println("You may enter the key usage as a comma-delimited list of following values: ");
764+
terminal.println(" - " + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted());
765+
terminal.println("");
766+
767+
keyUsage = readKeyUsage(terminal, keyUsage);
768+
terminal.println("");
742769
}
743770

744-
return new CertOptions(certName, dn, dnsNames, ipNames, keySize, validity);
771+
return new CertOptions(certName, dn, dnsNames, ipNames, keySize, keyUsage, validity);
745772
}
746773

747774
private static String validateHostname(String name) {
@@ -862,10 +889,12 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) {
862889
X500Principal dn = DEFAULT_CA_NAME;
863890
Period validity = DEFAULT_CA_VALIDITY;
864891
int keySize = DEFAULT_CA_KEY_SIZE;
892+
List<String> keyUsage = DEFAULT_CA_KEY_USAGE;
865893
while (true) {
866894
terminal.println(Terminal.Verbosity.SILENT, "Subject DN: " + dn);
867895
terminal.println(Terminal.Verbosity.SILENT, "Validity: " + toString(validity));
868896
terminal.println(Terminal.Verbosity.SILENT, "Key Size: " + keySize);
897+
terminal.println(Terminal.Verbosity.SILENT, "Key Usage: " + Strings.collectionToCommaDelimitedString(keyUsage));
869898
terminal.println(Terminal.Verbosity.SILENT, "");
870899
if (terminal.promptYesNo("Do you wish to change any of these options?", false) == false) {
871900
break;
@@ -907,13 +936,37 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) {
907936

908937
keySize = readKeySize(terminal, keySize);
909938
terminal.println("");
939+
940+
printHeader("What key usage should your CA have?", terminal);
941+
terminal.println("The key usage extension defines the purpose of the key contained in the certificate.");
942+
terminal.println(
943+
"The usage restriction might be employed when a key, that could be used for more than one operation, is to be restricted."
944+
);
945+
terminal.println("You may enter the key usage as a comma-delimited list of following values: ");
946+
terminal.println(" - " + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted());
947+
terminal.println("");
948+
949+
keyUsage = readKeyUsage(terminal, keyUsage);
950+
terminal.println("");
910951
}
911952

912953
try {
913954
final KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
914955
final ZonedDateTime notBefore = ZonedDateTime.now(ZoneOffset.UTC);
915956
final ZonedDateTime notAfter = notBefore.plus(validity);
916-
X509Certificate caCert = generateSignedCertificate(dn, null, keyPair, null, null, true, notBefore, notAfter, null);
957+
X509Certificate caCert = generateSignedCertificate(
958+
dn,
959+
null,
960+
keyPair,
961+
null,
962+
null,
963+
true,
964+
notBefore,
965+
notAfter,
966+
null,
967+
buildKeyUsage(keyUsage),
968+
Set.of()
969+
);
917970

918971
printHeader("CA password", terminal);
919972
terminal.println("We recommend that you protect your CA private key with a strong password.");
@@ -982,6 +1035,28 @@ private static Integer readKeySize(Terminal terminal, int keySize) {
9821035
});
9831036
}
9841037

1038+
private static List<String> readKeyUsage(Terminal terminal, List<String> defaultKeyUsage) {
1039+
return tryReadInput(terminal, "Key Usage", defaultKeyUsage, input -> {
1040+
final String[] keyUsages = input.split(",");
1041+
final List<String> resolvedKeyUsages = new ArrayList<>(keyUsages.length);
1042+
for (String keyUsage : keyUsages) {
1043+
if (keyUsage.isEmpty()) {
1044+
terminal.println("Key usage cannot be empty");
1045+
return null;
1046+
}
1047+
if (CertGenUtils.isValidKeyUsage(keyUsage) == false) {
1048+
terminal.println("Invalid key usage: " + keyUsage);
1049+
terminal.println(
1050+
"The key usage should be one of [" + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted() + "] values"
1051+
);
1052+
return null;
1053+
}
1054+
resolvedKeyUsages.add(keyUsage);
1055+
}
1056+
return resolvedKeyUsages;
1057+
});
1058+
}
1059+
9851060
private static char[] readPassword(Terminal terminal, String prompt, boolean confirm) {
9861061
while (true) {
9871062
final char[] password = terminal.readSecret(prompt + " [<ENTER> for none]");
@@ -1083,7 +1158,14 @@ private static boolean askExistingCertificateAuthority(Terminal terminal) {
10831158
}
10841159

10851160
private static <T> T tryReadInput(Terminal terminal, String prompt, T defaultValue, Function<String, T> parser) {
1086-
final String defaultStr = defaultValue instanceof Period ? toString((Period) defaultValue) : String.valueOf(defaultValue);
1161+
final String defaultStr;
1162+
if (defaultValue instanceof Period) {
1163+
defaultStr = toString((Period) defaultValue);
1164+
} else if (defaultValue instanceof Collection<?> collection) {
1165+
defaultStr = Strings.collectionToCommaDelimitedString(collection);
1166+
} else {
1167+
defaultStr = String.valueOf(defaultValue);
1168+
}
10871169
while (true) {
10881170
final String input = terminal.readText(prompt + " [" + defaultStr + "] ");
10891171
if (Strings.isEmpty(input)) {

0 commit comments

Comments
 (0)