Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions docs/changelog/126376.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 126376
summary: Set `keyUsage` for generated HTTP certificates and self-signed CA
area: TLS
type: bug
issues:
- 117769
5 changes: 4 additions & 1 deletion docs/reference/elasticsearch/command-line-tools/certutil.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>]
Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
import static org.elasticsearch.discovery.SettingsBasedSeedHostsProvider.DISCOVERY_SEED_HOSTS_SETTING;
import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
import static org.elasticsearch.xpack.core.security.CommandLineHttpClient.createURL;
import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage;
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CA_KEY_USAGE;
import static org.elasticsearch.xpack.security.cli.HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE;

/**
* Configures a new cluster node, by appending to the elasticsearch.yml, so that it forms a single node cluster with
Expand Down Expand Up @@ -411,7 +414,9 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
null,
true,
TRANSPORT_CA_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM
SIGNATURE_ALGORITHM,
null,
Set.of()
);
// transport key/certificate
final KeyPair transportKeyPair = CertGenUtils.generateKeyPair(TRANSPORT_KEY_SIZE);
Expand All @@ -424,7 +429,9 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
transportCaKey,
false,
TRANSPORT_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM
SIGNATURE_ALGORITHM,
null,
Set.of()
);

final KeyPair httpCaKeyPair = CertGenUtils.generateKeyPair(HTTP_CA_KEY_SIZE);
Expand All @@ -438,7 +445,9 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
null,
true,
HTTP_CA_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM
SIGNATURE_ALGORITHM,
buildKeyUsage(DEFAULT_CA_KEY_USAGE),
Set.of()
);
} catch (Throwable t) {
try {
Expand All @@ -464,6 +473,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce
false,
HTTP_CERTIFICATE_DAYS,
SIGNATURE_ALGORITHM,
buildKeyUsage(DEFAULT_CERT_KEY_USAGE),
Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth))
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
Expand Down Expand Up @@ -53,10 +54,14 @@
import java.sql.Date;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;

import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
Expand All @@ -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());
}

/**
Expand All @@ -107,7 +131,7 @@ public static X509Certificate generateSignedCertificate(
PrivateKey caPrivKey,
int days
) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, null);
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, null, null, Set.of());
}

/**
Expand All @@ -123,54 +147,14 @@ public static X509Certificate generateSignedCertificate(
* certificate
* @param caPrivKey the CA private key. If {@code null}, this results in a self signed
* certificate
* @param days no of days certificate will be valid from now
* @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
* empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
* @return a signed {@link X509Certificate}
*/
public static X509Certificate generateSignedCertificate(
X500Principal principal,
GeneralNames subjectAltNames,
KeyPair keyPair,
X509Certificate caCert,
PrivateKey caPrivKey,
int days,
String signatureAlgorithm
) throws OperatorCreationException, CertificateException, CertIOException, NoSuchAlgorithmException {
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, false, days, signatureAlgorithm);
}

/**
* Generates a signed certificate
*
* @param principal the principal of the certificate; commonly referred to as the
* distinguished name (DN)
* @param subjectAltNames the subject alternative names that should be added to the
* certificate as an X509v3 extension. May be {@code null}
* @param keyPair the key pair that will be associated with the certificate
* @param caCert the CA certificate. If {@code null}, this results in a self signed
* certificate
* @param caPrivKey the CA private key. If {@code null}, this results in a self signed
* certificate
* @param isCa whether or not the generated certificate is a CA
* @param days no of days certificate will be valid from now
* @param signatureAlgorithm algorithm used for signing certificate. If {@code null} or
* empty, then use default algorithm {@link CertGenUtils#getDefaultSignatureAlgorithm(PrivateKey)}
* @param keyUsage the key usage that should be added to the certificate as a X509v3 extension (can be {@code null})
* @param extendedKeyUsages the extended key usages that should be added to the certificate as a X509v3 extension (can be empty)
* @return a signed {@link X509Certificate}
*/
public static X509Certificate generateSignedCertificate(
X500Principal principal,
GeneralNames subjectAltNames,
KeyPair keyPair,
X509Certificate caCert,
PrivateKey caPrivKey,
boolean isCa,
int days,
String signatureAlgorithm
) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException {
return generateSignedCertificate(principal, subjectAltNames, keyPair, caCert, caPrivKey, isCa, days, signatureAlgorithm, Set.of());
}

public static X509Certificate generateSignedCertificate(
X500Principal principal,
GeneralNames subjectAltNames,
Expand All @@ -180,6 +164,7 @@ public static X509Certificate generateSignedCertificate(
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");
Expand All @@ -198,6 +183,7 @@ public static X509Certificate generateSignedCertificate(
notBefore,
notAfter,
signatureAlgorithm,
keyUsage,
extendedKeyUsages
);
}
Expand All @@ -223,6 +209,7 @@ public static X509Certificate generateSignedCertificate(
notBefore,
notAfter,
signatureAlgorithm,
null,
Set.of()
);
}
Expand All @@ -237,6 +224,7 @@ public static X509Certificate generateSignedCertificate(
ZonedDateTime notBefore,
ZonedDateTime notAfter,
String signatureAlgorithm,
KeyUsage keyUsage,
Set<ExtendedKeyUsage> extendedKeyUsages
) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException {
final BigInteger serial = CertGenUtils.getSerial();
Expand Down Expand Up @@ -272,6 +260,11 @@ public static X509Certificate generateSignedCertificate(
}
builder.addExtension(Extension.basicConstraints, isCa, new BasicConstraints(isCa));

if (keyUsage != null) {
// as per RFC 5280 (section 4.2.1.3), if the key usage is present, then it SHOULD be marked as critical.
final boolean isCritical = true;
builder.addExtension(Extension.keyUsage, isCritical, keyUsage);
}
if (extendedKeyUsages != null) {
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
builder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
Expand Down Expand Up @@ -318,7 +311,7 @@ private static String getDefaultSignatureAlgorithm(PrivateKey key) {
*/
static PKCS10CertificationRequest generateCSR(KeyPair keyPair, X500Principal principal, GeneralNames sanList) throws IOException,
OperatorCreationException {
return generateCSR(keyPair, principal, sanList, Set.of());
return generateCSR(keyPair, principal, sanList, null, Set.of());
}

/**
Expand All @@ -335,6 +328,7 @@ static PKCS10CertificationRequest generateCSR(
KeyPair keyPair,
X500Principal principal,
GeneralNames sanList,
KeyUsage keyUsage,
Set<ExtendedKeyUsage> extendedKeyUsages
) throws IOException, OperatorCreationException {
Objects.requireNonNull(keyPair, "Key-Pair must not be null");
Expand All @@ -347,7 +341,9 @@ static PKCS10CertificationRequest generateCSR(
if (sanList != null) {
extGen.addExtension(Extension.subjectAlternativeName, false, sanList);
}

if (keyUsage != null) {
extGen.addExtension(Extension.keyUsage, true, keyUsage);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's debatable if CSR should include keyUsage. I went with adding it to the request and letting the issuer decide if it should be respected or not.

}
for (ExtendedKeyUsage extendedKeyUsage : extendedKeyUsages) {
extGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage);
}
Expand Down Expand Up @@ -430,4 +426,31 @@ public static GeneralName createCommonName(String cn) {
public static String buildDnFromDomain(String domain) {
return "DC=" + domain.replace(".", ",DC=");
}

public static KeyUsage buildKeyUsage(Collection<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
Loading
Loading