Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0f6dffd
Set `keyUsage` for generated HTTP certificates and self-signed CA
slobodanadamovic Apr 6, 2025
2ed9991
Update docs/changelog/126376.yaml
slobodanadamovic Apr 6, 2025
11e96ee
Merge branch 'main' into sa-include-key-usage-in-http-certs
slobodanadamovic Apr 6, 2025
2a77f86
make default HTTP `keyUsage` overridable
slobodanadamovic Apr 7, 2025
8c7fa75
[CI] Auto commit changes from spotless
Apr 7, 2025
09f698e
respect custom keyUsage
slobodanadamovic Apr 7, 2025
c0f66f3
fix terminal messages and ignore leading and trailing whitespaces
slobodanadamovic Apr 7, 2025
814f5db
nit: error message
slobodanadamovic Apr 7, 2025
411f5cf
add extra line after error message
slobodanadamovic Apr 7, 2025
e1ef610
more test coverage
slobodanadamovic Apr 7, 2025
d8c8d30
nit: error message
slobodanadamovic Apr 7, 2025
b05a367
add `ca-keyusage` option
slobodanadamovic Apr 7, 2025
adcf14c
cleanup, test and update docs
slobodanadamovic Apr 7, 2025
5f160cf
update ca-keyusage description
slobodanadamovic Apr 7, 2025
6d59a54
Merge branch 'main' into sa-include-key-usage-in-http-certs
slobodanadamovic Apr 7, 2025
992b8ee
remove randomization to avoid raising ValidatorException:
slobodanadamovic Apr 7, 2025
10d682d
Merge branch 'sa-include-key-usage-in-http-certs' of github.com:slobo…
slobodanadamovic Apr 7, 2025
41b982f
move bit map to test class
slobodanadamovic Apr 8, 2025
fa68b1a
map of entires
slobodanadamovic Apr 8, 2025
5fdda88
rename `ca-keyusage` to `keyusage` and make it applicable only to `ca`
slobodanadamovic Apr 8, 2025
e03f096
remove obsolete dependencies to unapplicable ca options
slobodanadamovic Apr 8, 2025
06c04c0
make it private
slobodanadamovic Apr 8, 2025
f238cd0
Merge branch 'main' into sa-include-key-usage-in-http-certs
slobodanadamovic Apr 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/reference/elasticsearch/command-line-tools/certutil.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ 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>] [--ca-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>]
[--ca-dn <name>] [--ca-keyusage <key_usages>] [--ca-pass <password>] [--days <n>]
[--dns <domain_name>] [--in <input_file>] [--ip <ip_addresses>]
[--multiple] [--name <file_name>] [--pem] [--self-signed])

Expand Down Expand Up @@ -99,6 +99,9 @@ The `http` mode guides you through the process of generating certificates for us
`--ca-dn <name>`
: Defines the *Distinguished Name* (DN) that is used for the generated CA certificate. The default value is `CN=Elastic Certificate Tool Autogenerated CA`. This parameter cannot be used with the `csr` or `http` parameters.

`--ca-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 cannot be used with the `csr` or `http` parameters.

`--ca-key <file_path>`
: Specifies the path to an existing CA private key (in PEM format). You must also specify the `--ca-cert` parameter. The `--ca-key` parameter is only applicable to the `cert` parameter.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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.openssl.jcajce.JcaPEMWriter;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.cli.ExitCodes;
Expand Down Expand Up @@ -103,6 +102,9 @@
import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL;
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE;

/**
* Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
Expand Down Expand Up @@ -444,7 +446,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
true,
HTTP_CA_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM,
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign),
buildKeyUsage(DEFAULT_CA_KEY_USAGE),
Set.of()
);
} catch (Throwable t) {
Expand All @@ -471,7 +473,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
false,
HTTP_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment),
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@
import java.sql.Date;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;

import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
Expand All @@ -74,14 +78,78 @@ 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.of(
"digitalSignature",
KeyUsage.digitalSignature,
"nonRepudiation",
KeyUsage.nonRepudiation,
"keyEncipherment",
KeyUsage.keyEncipherment,
"dataEncipherment",
KeyUsage.dataEncipherment,
"keyAgreement",
KeyUsage.keyAgreement,
"keyCertSign",
KeyUsage.keyCertSign,
"cRLSign",
KeyUsage.cRLSign,
"encipherOnly",
KeyUsage.encipherOnly,
"decipherOnly",
KeyUsage.decipherOnly
)
)
);

/**
* 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>
*/
public static final Map<String, Integer> KEY_USAGE_BITS = Map.of(
"digitalSignature",
0,
"nonRepudiation",
1,
"keyEncipherment",
2,
"dataEncipherment",
3,
"keyAgreement",
4,
"keyCertSign",
5,
"cRLSign",
6,
"encipherOnly",
7,
"decipherOnly",
8
);

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, null, Set.of());
return generateSignedCertificate(x500Principal, null, keyPair, null, null, true, days, null, keyUsage, Set.of());
}

/**
Expand Down Expand Up @@ -403,4 +471,31 @@ public static GeneralName createCommonName(String cn) {
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ static CAInfo getCAInfo(
// generate the CA keys and cert
X500Principal x500Principal = new X500Principal(dn);
KeyPair keyPair = CertGenUtils.generateKeyPair(keysize);
Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, days);
Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, days, null);
final char[] password;
if (prompt) {
password = terminal.readSecret("Enter password for CA private key: ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMEncryptor;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -202,6 +204,7 @@ abstract static class CertificateCommand extends EnvironmentAwareCommand {
final OptionSpec<String> outputPathSpec;
final OptionSpec<String> outputPasswordSpec;
final OptionSpec<Integer> keysizeSpec;
OptionSpec<String> caKeyUsageSpec;

OptionSpec<Void> pemFormatSpec;
OptionSpec<Integer> daysSpec;
Expand Down Expand Up @@ -247,6 +250,7 @@ final void acceptsCertificateAuthority() {
.withOptionalArg();

acceptsCertificateAuthorityName();
acceptCertificateAuthorityKeyUsage();
}

void acceptsCertificateAuthorityName() {
Expand Down Expand Up @@ -274,6 +278,23 @@ final void acceptInputFile() {
inputFileSpec = parser.accepts("in", "file containing details of the instances in yaml format").withRequiredArg();
}

final void acceptCertificateAuthorityKeyUsage() {
OptionSpecBuilder builder = parser.accepts(
"ca-keyusage",
"comma separated key usages to use for the generated CA. "
+ "defaults to '"
+ Strings.collectionToCommaDelimitedString(DEFAULT_CA_KEY_USAGE)
+ "'"
);
if (caPkcs12PathSpec != null) {
builder = builder.availableUnless(caPkcs12PathSpec);
}
if (caCertPathSpec != null) {
builder = builder.availableUnless(caCertPathSpec);
}
caKeyUsageSpec = builder.withRequiredArg();
}

// For testing
OptionParser getParser() {
return parser;
Expand Down Expand Up @@ -309,6 +330,18 @@ final int getKeySize(OptionSet options) {
}
}

final List<String> getCaKeyUsage(OptionSet options) {
if (options.has(caKeyUsageSpec)) {
String rawCaKeyUsage = caKeyUsageSpec.value(options);
if (Strings.isNullOrEmpty(rawCaKeyUsage)) {
return DEFAULT_CA_KEY_USAGE;
}
return List.of(Strings.splitStringByCommaToArray(rawCaKeyUsage));
} else {
return DEFAULT_CA_KEY_USAGE;
}
}

final int getDays(OptionSet options) {
if (options.has(daysSpec)) {
return daysSpec.value(options);
Expand Down Expand Up @@ -396,7 +429,8 @@ CAInfo generateCA(Terminal terminal, OptionSet options) throws Exception {
}
X500Principal x500Principal = new X500Principal(dn);
KeyPair keyPair = CertGenUtils.generateKeyPair(getKeySize(options));
X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options));
final KeyUsage caKeyUsage = CertGenUtils.buildKeyUsage(getCaKeyUsage(options));
X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options), caKeyUsage);

if (options.hasArgument(caPasswordSpec)) {
char[] password = getChars(caPasswordSpec.value(options));
Expand Down Expand Up @@ -947,6 +981,7 @@ static class CertificateAuthorityCommand extends CertificateCommand {
super("generate a new local certificate authority");
acceptCertificateGenerationOptions();
acceptsCertificateAuthorityName();
acceptCertificateAuthorityKeyUsage();
super.caPasswordSpec = super.outputPasswordSpec;
}

Expand Down
Loading
Loading