From 0f6dffde04bca7811e10cd1f3328c4bd90bb82c1 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Sun, 6 Apr 2025 22:31:01 +0200 Subject: [PATCH 01/19] Set `keyUsage` for generated HTTP certificates and self-signed CA --- .../xpack/security/cli/AutoConfigureNode.java | 14 +++- .../xpack/security/cli/CertGenUtils.java | 65 ++++++------------- .../xpack/security/cli/CertificateTool.java | 4 +- .../security/cli/HttpCertificateCommand.java | 3 + .../security/cli/AutoConfigureNodeTests.java | 23 +++++-- .../xpack/security/cli/CertGenUtilsTests.java | 1 + .../cli/HttpCertificateCommandTests.java | 10 ++- 7 files changed, 63 insertions(+), 57 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java index dbe0e0b0e9577..302367634c7d0 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java @@ -15,6 +15,7 @@ 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; @@ -411,7 +412,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); @@ -424,7 +427,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); @@ -438,7 +443,9 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce null, true, HTTP_CA_CERTIFICATE_DAYS, - SIGNATURE_ALGORITHM + SIGNATURE_ALGORITHM, + new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign), + Set.of() ); } catch (Throwable t) { try { @@ -464,6 +471,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce false, HTTP_CERTIFICATE_DAYS, SIGNATURE_ALGORITHM, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index c3f4d8a57b560..90f32df66563c 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -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; @@ -80,7 +81,7 @@ private CertGenUtils() {} */ public static X509Certificate generateCACertificate(X500Principal x500Principal, KeyPair keyPair, int days) 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, null, Set.of()); } /** @@ -107,7 +108,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()); } /** @@ -123,54 +124,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, @@ -180,6 +141,7 @@ public static X509Certificate generateSignedCertificate( boolean isCa, int days, String signatureAlgorithm, + KeyUsage keyUsage, Set extendedKeyUsages ) throws NoSuchAlgorithmException, CertificateException, CertIOException, OperatorCreationException { Objects.requireNonNull(keyPair, "Key-Pair must not be null"); @@ -198,6 +160,7 @@ public static X509Certificate generateSignedCertificate( notBefore, notAfter, signatureAlgorithm, + keyUsage, extendedKeyUsages ); } @@ -223,6 +186,7 @@ public static X509Certificate generateSignedCertificate( notBefore, notAfter, signatureAlgorithm, + null, Set.of() ); } @@ -237,6 +201,7 @@ public static X509Certificate generateSignedCertificate( ZonedDateTime notBefore, ZonedDateTime notAfter, String signatureAlgorithm, + KeyUsage keyUsage, Set extendedKeyUsages ) throws NoSuchAlgorithmException, CertIOException, OperatorCreationException, CertificateException { final BigInteger serial = CertGenUtils.getSerial(); @@ -272,6 +237,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); @@ -318,7 +288,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()); } /** @@ -335,6 +305,7 @@ static PKCS10CertificationRequest generateCSR( KeyPair keyPair, X500Principal principal, GeneralNames sanList, + KeyUsage keyUsage, Set extendedKeyUsages ) throws IOException, OperatorCreationException { Objects.requireNonNull(keyPair, "Key-Pair must not be null"); @@ -347,7 +318,9 @@ static PKCS10CertificationRequest generateCSR( 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); } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index a9c0653716851..e31684a6d75a8 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -933,9 +933,7 @@ private static CertificateAndKey generateCertificateAndKey( keyPair, null, null, - false, - days, - null + days ); } return new CertificateAndKey((X509Certificate) certificate, keyPair.getPrivate()); diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index 0e96911405b30..290acbd96643a 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -14,6 +14,7 @@ import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.cert.CertIOException; import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; @@ -339,6 +340,7 @@ private void writeCertificateAndKeyDetails( keyPair, cert.subject, sanList, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); final String csrFile = "http-" + cert.name + ".csr"; @@ -372,6 +374,7 @@ private void writeCertificateAndKeyDetails( notBefore, notAfter, null, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java index a03d9a7822e88..c01b35a468839 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java @@ -9,6 +9,8 @@ import joptsimple.OptionParser; +import com.unboundid.util.ssl.cert.KeyUsageExtension; + import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.elasticsearch.cli.MockTerminal; @@ -37,6 +39,7 @@ 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.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -180,7 +183,7 @@ public void testGeneratedHTTPCertificateSANs() throws Exception { 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 +205,7 @@ public void testGeneratedHTTPCertificateSANs() throws Exception { 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 +231,7 @@ public void testGeneratedHTTPCertificateSANs() throws Exception { 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 +291,23 @@ private boolean checkSubjectAndIssuerDN(X509Certificate certificate, String subj return false; } - private void verifyExtendedKeyUsage(X509Certificate httpCertificate) throws Exception { + private void verifyKeyUsageAndExtendedKeyUsage(X509Certificate httpCertificate) throws Exception { List 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)); + final boolean[] keyUsage = httpCertificate.getKeyUsage(); + assertThat("Expected 9 bits for key usage.", keyUsage.length, equalTo(9)); + for (int i = 0; i < keyUsage.length; i++) { + if (i == 0 /* digitalSignature */ || i == 2 /* keyEncipherment */) { + assertThat("keyUsage bit [" + i + "] expected to be set", keyUsage[i], equalTo(true)); + } else { + assertThat("keyUsage bit [" + i + "] not expected to be set", keyUsage[i], equalTo(false)); + } + } + // key usage must be marked as critical + assertThat(httpCertificate.getCriticalExtensionOIDs(), contains(KeyUsageExtension.KEY_USAGE_OID.toString())); + } private X509Certificate runAutoConfigAndReturnHTTPCertificate(Path configDir, Settings settings) throws Exception { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index 5c1f5a97d4335..64a7fbd7b4418 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java @@ -143,6 +143,7 @@ public void testIssuerCertSubjectDN() throws Exception { notBefore, notAfter, null, + null, Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)) ); diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java index 1033d4e51ebba..cafb0e6b51066 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java @@ -23,6 +23,7 @@ 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; @@ -93,6 +94,7 @@ 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; @@ -692,7 +694,10 @@ private void verifyCertificationRequest( // 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 +714,9 @@ private void verifyCertificationRequest( 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( From 2ed99915a2597c789413970f3ef5289b193dc25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slobodan=20Adamovi=C4=87?= Date: Sun, 6 Apr 2025 22:49:31 +0200 Subject: [PATCH 02/19] Update docs/changelog/126376.yaml --- docs/changelog/126376.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changelog/126376.yaml diff --git a/docs/changelog/126376.yaml b/docs/changelog/126376.yaml new file mode 100644 index 0000000000000..5ac6bc747f160 --- /dev/null +++ b/docs/changelog/126376.yaml @@ -0,0 +1,6 @@ +pr: 126376 +summary: Set `keyUsage` for generated HTTP certificates and self-signed CA +area: TLS +type: bug +issues: + - 117769 From 2a77f86a33cd3853366311ce433727e405583160 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 12:00:29 +0200 Subject: [PATCH 03/19] make default HTTP `keyUsage` overridable --- .../xpack/security/cli/AutoConfigureNode.java | 8 +- .../xpack/security/cli/CertGenUtils.java | 82 ++++++++++++++++ .../security/cli/HttpCertificateCommand.java | 98 +++++++++++++++++-- .../security/cli/AutoConfigureNodeTests.java | 19 +--- .../xpack/security/cli/CertGenUtilsTests.java | 63 ++++++++++-- .../cli/HttpCertificateCommandTests.java | 8 ++ 6 files changed, 243 insertions(+), 35 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java index 302367634c7d0..689134a5eba17 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/AutoConfigureNode.java @@ -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; @@ -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 @@ -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) { @@ -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)) ); diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 90f32df66563c..0c2e46f39b68f 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -54,8 +54,10 @@ import java.sql.Date; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.Collection; import java.util.HashSet; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -74,6 +76,66 @@ 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 KEY_USAGE_MAPPINGS = 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: + * + *
    + *
  • digitalSignature (0)
  • + *
  • nonRepudiation (1)
  • + *
  • keyEncipherment (2)
  • + *
  • dataEncipherment (3)
  • + *
  • keyAgreement (4)
  • + *
  • keyCertSign (5)
  • + *
  • cRLSign (6)
  • + *
  • encipherOnly (7)
  • + *
  • decipherOnly (8)
  • + *
+ */ + public static final Map 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() {} /** @@ -403,4 +465,24 @@ public static GeneralName createCommonName(String cn) { public static String buildDnFromDomain(String domain) { return "DC=" + domain.replace(".", ",DC="); } + + public static KeyUsage buildKeyUsage(Collection keyUsages) { + if (keyUsages == null || keyUsages.isEmpty()) { + return null; + } + + int usageBits = 0; + for (String keyUsage : keyUsages) { + Integer keyUsageValue = KEY_USAGE_MAPPINGS.get(keyUsage); + if (keyUsageValue == null) { + throw new IllegalArgumentException("Unknown keyUsage: " + keyUsage); + } + usageBits |= keyUsageValue; + } + return new KeyUsage(usageBits); + } + + public static boolean isValidKeyUsage(String keyUsage) { + return KEY_USAGE_MAPPINGS.containsKey(keyUsage); + } } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index 290acbd96643a..d3624673b8b90 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -14,7 +14,6 @@ import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.cert.CertIOException; import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; @@ -69,6 +68,7 @@ import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; @@ -81,6 +81,7 @@ import javax.security.auth.x500.X500Principal; +import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage; import static org.elasticsearch.xpack.security.cli.CertGenUtils.generateSignedCertificate; /** @@ -96,7 +97,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 DEFAULT_CA_KEY_USAGE = List.of("keyCertSign", "cRLSign"); + static final List 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"; @@ -134,14 +136,24 @@ private class CertOptions { final List dnsNames; final List ipNames; final int keySize; + final List keyUsage; final Period validity; - private CertOptions(String name, X500Principal subject, List dnsNames, List ipNames, int keySize, Period validity) { + private CertOptions( + String name, + X500Principal subject, + List dnsNames, + List ipNames, + int keySize, + List keyUsage, + Period validity + ) { this.name = name; this.subject = subject; this.dnsNames = dnsNames; this.ipNames = ipNames; this.keySize = keySize; + this.keyUsage = keyUsage; this.validity = validity; } } @@ -195,6 +207,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce 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); @@ -340,7 +353,7 @@ private void writeCertificateAndKeyDetails( keyPair, cert.subject, sanList, - new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment), + buildKeyUsage(DEFAULT_CERT_KEY_USAGE), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); final String csrFile = "http-" + cert.name + ".csr"; @@ -374,7 +387,7 @@ private void writeCertificateAndKeyDetails( notBefore, notAfter, null, - new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment), + buildKeyUsage(DEFAULT_CERT_KEY_USAGE), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); @@ -695,10 +708,12 @@ private CertOptions getCertificateConfiguration( } X500Principal dn = buildDistinguishedName(certName); int keySize = DEFAULT_CERT_KEY_SIZE; + List 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; @@ -739,9 +754,21 @@ private CertOptions getCertificateConfiguration( 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 one operation, is to be restricted." + ); + terminal.println("You may enter the key usage as a comma-delimited list of following values: "); + terminal.println(" - " + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted()); + 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) { @@ -862,10 +889,12 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) { X500Principal dn = DEFAULT_CA_NAME; Period validity = DEFAULT_CA_VALIDITY; int keySize = DEFAULT_CA_KEY_SIZE; + List 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; @@ -907,13 +936,37 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) { 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 one operation, is to be restricted." + ); + terminal.println("You may enter the key usage as a comma-delimited list of following values: "); + terminal.println(" - " + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted()); + 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."); @@ -982,6 +1035,28 @@ private static Integer readKeySize(Terminal terminal, int keySize) { }); } + private static List readKeyUsage(Terminal terminal, List defaultKeyUsage) { + return tryReadInput(terminal, "Key Usage", defaultKeyUsage, input -> { + final String[] keyUsages = input.split(","); + final List resolvedKeyUsages = new ArrayList<>(keyUsages.length); + for (String keyUsage : keyUsages) { + if (keyUsage.isEmpty()) { + terminal.println("Key usage cannot be empty"); + return null; + } + if (CertGenUtils.isValidKeyUsage(keyUsage) == false) { + terminal.println("Invalid key usage: " + keyUsage); + terminal.println( + "The key usage should be one of [" + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted() + "] values" + ); + return null; + } + resolvedKeyUsages.add(keyUsage); + } + return resolvedKeyUsages; + }); + } + private static char[] readPassword(Terminal terminal, String prompt, boolean confirm) { while (true) { final char[] password = terminal.readSecret(prompt + " [ for none]"); @@ -1083,7 +1158,14 @@ private static boolean askExistingCertificateAuthority(Terminal terminal) { } private static T tryReadInput(Terminal terminal, String prompt, T defaultValue, Function 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)) { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java index c01b35a468839..8330fb5d575ac 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/AutoConfigureNodeTests.java @@ -9,8 +9,6 @@ import joptsimple.OptionParser; -import com.unboundid.util.ssl.cert.KeyUsageExtension; - import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.elasticsearch.cli.MockTerminal; @@ -39,7 +37,7 @@ 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.hamcrest.Matchers.contains; +import static org.elasticsearch.xpack.security.cli.CertGenUtilsTests.assertExpectedKeyUsage; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -152,7 +150,7 @@ public void testSubjectAndIssuerForGeneratedCertificates() throws Exception { } } - public void testGeneratedHTTPCertificateSANs() throws Exception { + public void testGeneratedHTTPCertificateSANsAndKeyUsage() throws Exception { // test no publish settings Path tempDir = createTempDir(); try { @@ -296,18 +294,7 @@ private void verifyKeyUsageAndExtendedKeyUsage(X509Certificate httpCertificate) 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)); - final boolean[] keyUsage = httpCertificate.getKeyUsage(); - assertThat("Expected 9 bits for key usage.", keyUsage.length, equalTo(9)); - for (int i = 0; i < keyUsage.length; i++) { - if (i == 0 /* digitalSignature */ || i == 2 /* keyEncipherment */) { - assertThat("keyUsage bit [" + i + "] expected to be set", keyUsage[i], equalTo(true)); - } else { - assertThat("keyUsage bit [" + i + "] not expected to be set", keyUsage[i], equalTo(false)); - } - } - // key usage must be marked as critical - assertThat(httpCertificate.getCriticalExtensionOIDs(), contains(KeyUsageExtension.KEY_USAGE_OID.toString())); - + assertExpectedKeyUsage(httpCertificate, HttpCertificateCommand.DEFAULT_CERT_KEY_USAGE); } private X509Certificate runAutoConfigAndReturnHTTPCertificate(Path configDir, Settings settings) throws Exception { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index 64a7fbd7b4418..dc4ea1af98d09 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java @@ -7,6 +7,8 @@ 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; @@ -17,6 +19,11 @@ import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.security.auth.x500.X500Principal; + import java.math.BigInteger; import java.net.InetAddress; import java.security.KeyPair; @@ -28,15 +35,14 @@ import java.util.Collections; import java.util.List; 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_BITS; +import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage; 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 @@ -103,6 +109,7 @@ public void testIssuerCertSubjectDN() throws Exception { // root CA final X500Principal rootCaPrincipal = new X500Principal("DC=example.com"); final KeyPair rootCaKeyPair = CertGenUtils.generateKeyPair(2048); + final List rootCaKeyUsages = List.of("keyCertSign", "cRLSign"); final X509Certificate rootCaCert = CertGenUtils.generateSignedCertificate( rootCaPrincipal, null, @@ -112,12 +119,15 @@ public void testIssuerCertSubjectDN() throws Exception { 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 subCaKeyUsage = List.of("digitalSignature", "keyCertSign", "cRLSign"); final X509Certificate subCaCert = CertGenUtils.generateSignedCertificate( subCaPrincipal, null, @@ -127,12 +137,15 @@ public void testIssuerCertSubjectDN() throws Exception { 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 endEntityKeyUsage = randomBoolean() ? null : List.of("digitalSignature", "keyEncipherment"); final X509Certificate endEntityCert = CertGenUtils.generateSignedCertificate( endEntityPrincipal, null, @@ -143,7 +156,7 @@ public void testIssuerCertSubjectDN() throws Exception { notBefore, notAfter, null, - null, + buildKeyUsage(endEntityKeyUsage), Set.of(new ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)) ); @@ -163,6 +176,40 @@ public void testIssuerCertSubjectDN() throws Exception { 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 static void assertExpectedKeyUsage(X509Certificate certificate, List 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 expectedBitsToBeSet = expectedKeyUsage.stream() + .map(CertGenUtils.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) + ); } /** diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java index cafb0e6b51066..792796fea8c35 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommandTests.java @@ -31,6 +31,7 @@ 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; @@ -90,6 +91,7 @@ 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; @@ -371,21 +373,25 @@ public void testGenerateMultipleCertificateWithNewCA() throws Exception { final String caDN; final int caYears; final int caKeySize; + final List 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()); @@ -465,6 +471,7 @@ public void testGenerateMultipleCertificateWithNewCA() throws Exception { 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()); @@ -488,6 +495,7 @@ public void testGenerateMultipleCertificateWithNewCA() throws Exception { 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())); From 8c7fa75787c152526064ff26b9c22da163f78cec Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 7 Apr 2025 10:08:20 +0000 Subject: [PATCH 04/19] [CI] Auto commit changes from spotless --- .../xpack/security/cli/CertGenUtilsTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index dc4ea1af98d09..b853a9299cfde 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java @@ -19,11 +19,6 @@ import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.security.auth.x500.X500Principal; - import java.math.BigInteger; import java.net.InetAddress; import java.security.KeyPair; @@ -37,6 +32,11 @@ 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_BITS; import static org.elasticsearch.xpack.security.cli.CertGenUtils.buildKeyUsage; import static org.hamcrest.Matchers.equalTo; From 09f698e9bf4867bb280ab98627ff7e6feb0f098c Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 12:22:35 +0200 Subject: [PATCH 05/19] respect custom keyUsage --- .../xpack/security/cli/HttpCertificateCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index d3624673b8b90..2f7a72de2a8a7 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -353,7 +353,7 @@ private void writeCertificateAndKeyDetails( keyPair, cert.subject, sanList, - buildKeyUsage(DEFAULT_CERT_KEY_USAGE), + buildKeyUsage(cert.keyUsage), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); final String csrFile = "http-" + cert.name + ".csr"; @@ -387,7 +387,7 @@ private void writeCertificateAndKeyDetails( notBefore, notAfter, null, - buildKeyUsage(DEFAULT_CERT_KEY_USAGE), + buildKeyUsage(cert.keyUsage), Set.of(new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)) ); From c0f66f3600e308119ca7bb2ef61dcea5e482cf22 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 12:55:10 +0200 Subject: [PATCH 06/19] fix terminal messages and ignore leading and trailing whitespaces --- .../xpack/security/cli/CertGenUtils.java | 44 +++++++++++-------- .../security/cli/HttpCertificateCommand.java | 22 ++++++---- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 0c2e46f39b68f..95fc9e8f599b3 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -55,11 +55,13 @@ 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; @@ -79,25 +81,29 @@ public class CertGenUtils { /** * The mapping of key usage names to their corresponding integer values as defined in {@code KeyUsage} class. */ - public static final Map KEY_USAGE_MAPPINGS = 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 + public static final Map 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 + ) + ) ); /** diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index 2f7a72de2a8a7..b2507de384fff 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -69,6 +69,7 @@ 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; @@ -757,11 +758,12 @@ private CertOptions getCertificateConfiguration( 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 one operation, is to be restricted." - ); + 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: "); - terminal.println(" - " + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted()); + for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) { + terminal.println(" - " + keyUsageName); + } terminal.println(""); keyUsage = readKeyUsage(terminal, keyUsage); @@ -939,11 +941,12 @@ private CertificateTool.CAInfo createNewCA(Terminal terminal) { 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 one operation, is to be restricted." - ); + 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: "); - terminal.println(" - " + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted()); + for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) { + terminal.println(" - " + keyUsageName); + } terminal.println(""); keyUsage = readKeyUsage(terminal, keyUsage); @@ -1040,6 +1043,7 @@ private static List readKeyUsage(Terminal terminal, List default final String[] keyUsages = input.split(","); final List resolvedKeyUsages = new ArrayList<>(keyUsages.length); for (String keyUsage : keyUsages) { + keyUsage = keyUsage.trim(); if (keyUsage.isEmpty()) { terminal.println("Key usage cannot be empty"); return null; @@ -1053,7 +1057,7 @@ private static List readKeyUsage(Terminal terminal, List default } resolvedKeyUsages.add(keyUsage); } - return resolvedKeyUsages; + return Collections.unmodifiableList(resolvedKeyUsages); }); } From 814f5db072033246072fdabe965a0b7ff3cf0551 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 13:04:40 +0200 Subject: [PATCH 07/19] nit: error message --- .../org/elasticsearch/xpack/security/cli/CertGenUtils.java | 2 +- .../xpack/security/cli/HttpCertificateCommand.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 95fc9e8f599b3..2e955b2abcc62 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -479,7 +479,7 @@ public static KeyUsage buildKeyUsage(Collection keyUsages) { int usageBits = 0; for (String keyUsage : keyUsages) { - Integer keyUsageValue = KEY_USAGE_MAPPINGS.get(keyUsage); + Integer keyUsageValue = KEY_USAGE_MAPPINGS.get(keyUsage.trim()); if (keyUsageValue == null) { throw new IllegalArgumentException("Unknown keyUsage: " + keyUsage); } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index b2507de384fff..6bc9c1a6a014d 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -1050,9 +1050,10 @@ private static List readKeyUsage(Terminal terminal, List default } if (CertGenUtils.isValidKeyUsage(keyUsage) == false) { terminal.println("Invalid key usage: " + keyUsage); - terminal.println( - "The key usage should be one of [" + CertGenUtils.KEY_USAGE_MAPPINGS.keySet().stream().sorted() + "] values" - ); + terminal.println("The key usage should be one of the following values: "); + for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) { + terminal.println(" - " + keyUsageName); + } return null; } resolvedKeyUsages.add(keyUsage); From 411f5cf4f7a14f620b442f70adf5ffd18073e25f Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 13:10:12 +0200 Subject: [PATCH 08/19] add extra line after error message --- .../elasticsearch/xpack/security/cli/HttpCertificateCommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index 6bc9c1a6a014d..905db00f5e695 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -1054,6 +1054,7 @@ private static List readKeyUsage(Terminal terminal, List default for (String keyUsageName : CertGenUtils.KEY_USAGE_MAPPINGS.keySet()) { terminal.println(" - " + keyUsageName); } + terminal.println(""); return null; } resolvedKeyUsages.add(keyUsage); From e1ef610199a0f4a30e21c97a85642d789e4e931e Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 14:02:56 +0200 Subject: [PATCH 09/19] more test coverage --- .../xpack/security/cli/CertGenUtils.java | 15 ++-- .../security/cli/HttpCertificateCommand.java | 3 +- .../xpack/security/cli/CertGenUtilsTests.java | 68 +++++++++++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 2e955b2abcc62..510411744fd9d 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -478,10 +478,10 @@ public static KeyUsage buildKeyUsage(Collection keyUsages) { } int usageBits = 0; - for (String keyUsage : keyUsages) { - Integer keyUsageValue = KEY_USAGE_MAPPINGS.get(keyUsage.trim()); + for (String keyUsageName : keyUsages) { + Integer keyUsageValue = findKeyUsageByName(keyUsageName); if (keyUsageValue == null) { - throw new IllegalArgumentException("Unknown keyUsage: " + keyUsage); + throw new IllegalArgumentException("Unknown keyUsage: " + keyUsageName); } usageBits |= keyUsageValue; } @@ -489,6 +489,13 @@ public static KeyUsage buildKeyUsage(Collection keyUsages) { } public static boolean isValidKeyUsage(String keyUsage) { - return KEY_USAGE_MAPPINGS.containsKey(keyUsage); + return findKeyUsageByName(keyUsage) != null; + } + + private static Integer findKeyUsageByName(String keyUsageName) { + if (keyUsageName == null) { + return null; + } + return KEY_USAGE_MAPPINGS.get(keyUsageName.trim()); } } diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index 905db00f5e695..2a502b5c93d94 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -84,6 +84,7 @@ 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 @@ -1048,7 +1049,7 @@ private static List readKeyUsage(Terminal terminal, List default terminal.println("Key usage cannot be empty"); return null; } - if (CertGenUtils.isValidKeyUsage(keyUsage) == false) { + 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()) { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index b853a9299cfde..60f56a6791af2 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java @@ -13,6 +13,7 @@ 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; @@ -38,7 +39,11 @@ import javax.security.auth.x500.X500Principal; import static org.elasticsearch.xpack.security.cli.CertGenUtils.KEY_USAGE_BITS; +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; @@ -190,6 +195,69 @@ public void testIssuerCertSubjectDN() throws Exception { } + 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 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 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 expectedKeyUsage) { final boolean[] keyUsage = certificate.getKeyUsage(); assertThat("Expected " + KEY_USAGE_BITS.size() + " bits for key usage", keyUsage.length, equalTo(KEY_USAGE_BITS.size())); From d8c8d30256f4caa198b33d77d90be5a2bc7c198c Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 14:08:01 +0200 Subject: [PATCH 10/19] nit: error message --- .../xpack/security/cli/HttpCertificateCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java index 2a502b5c93d94..23a7bb96be2af 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/HttpCertificateCommand.java @@ -1046,7 +1046,7 @@ private static List readKeyUsage(Terminal terminal, List default for (String keyUsage : keyUsages) { keyUsage = keyUsage.trim(); if (keyUsage.isEmpty()) { - terminal.println("Key usage cannot be empty"); + terminal.println("Key usage cannot be blank or empty"); return null; } if (isValidKeyUsage(keyUsage) == false) { From b05a3677800d7bf5ba2a7d1a06980520450d20bf Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 18:26:58 +0200 Subject: [PATCH 11/19] add `ca-keyusage` option --- .../xpack/security/cli/CertGenUtils.java | 4 +-- .../security/cli/CertificateGenerateTool.java | 2 +- .../xpack/security/cli/CertificateTool.java | 30 ++++++++++++++++++- .../cli/CertificateGenerateToolTests.java | 2 +- .../security/cli/CertificateToolTests.java | 8 ++++- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 510411744fd9d..a338d3076026d 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -147,9 +147,9 @@ 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()); } /** diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java index a6716f0360a1b..a342e3cca3e94 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateGenerateTool.java @@ -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: "); diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index e31684a6d75a8..b0f286aafae02 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -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; @@ -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 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 @@ abstract static class CertificateCommand extends EnvironmentAwareCommand { final OptionSpec outputPathSpec; final OptionSpec outputPasswordSpec; final OptionSpec keysizeSpec; + OptionSpec caKeyUsageSpec; OptionSpec pemFormatSpec; OptionSpec daysSpec; @@ -247,6 +250,7 @@ final void acceptsCertificateAuthority() { .withOptionalArg(); acceptsCertificateAuthorityName(); + acceptCertificateAuthorityKeyUsage(); } void acceptsCertificateAuthorityName() { @@ -274,6 +278,20 @@ 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 " + 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; @@ -396,7 +414,16 @@ 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 Function> splitByComma = v -> Arrays.stream(Strings.splitStringByCommaToArray(v)); + final KeyUsage caKeyUsage; + if (options.hasArgument(caKeyUsageSpec)) { + List keyUsageNames = caKeyUsageSpec.values(options).stream().flatMap(splitByComma).toList(); + caKeyUsage = CertGenUtils.buildKeyUsage(keyUsageNames); + } else { + caKeyUsage = CertGenUtils.buildKeyUsage(DEFAULT_CA_KEY_USAGE); + } + + X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options), caKeyUsage); if (options.hasArgument(caPasswordSpec)) { char[] password = getChars(caPasswordSpec.value(options)); @@ -947,6 +974,7 @@ static class CertificateAuthorityCommand extends CertificateCommand { super("generate a new local certificate authority"); acceptCertificateGenerationOptions(); acceptsCertificateAuthorityName(); + acceptCertificateAuthorityKeyUsage(); super.caPasswordSpec = super.outputPasswordSpec; } diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java index 1faabcfd46fdb..69ba80c729254 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateGenerateToolTests.java @@ -274,7 +274,7 @@ public void testGeneratingSignedCertificates() throws Exception { 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; diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java index 1a11234c98e6e..f52f72038eef8 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java @@ -415,7 +415,13 @@ public void testGeneratingSignedPemCertificates() throws Exception { int days = randomIntBetween(1, 1024); KeyPair keyPair = CertGenUtils.generateKeyPair(keySize); - X509Certificate caCert = CertGenUtils.generateCACertificate(new X500Principal("CN=test ca"), keyPair, days); + List 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; From adcf14c56503e2ad075b956e63e4b58469323e8a Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 21:59:59 +0200 Subject: [PATCH 12/19] cleanup, test and update docs --- .../command-line-tools/certutil.md | 7 ++++-- .../xpack/security/cli/CertificateTool.java | 22 +++++++++++-------- .../security/cli/CertificateToolTests.java | 9 +++++++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/reference/elasticsearch/command-line-tools/certutil.md b/docs/reference/elasticsearch/command-line-tools/certutil.md index e01540f26dc17..5db4547771bff 100644 --- a/docs/reference/elasticsearch/command-line-tools/certutil.md +++ b/docs/reference/elasticsearch/command-line-tools/certutil.md @@ -13,10 +13,10 @@ The `elasticsearch-certutil` command simplifies the creation of certificates for ```shell bin/elasticsearch-certutil ( -(ca [--ca-dn ] [--days ] [--pem]) +(ca [--ca-dn ] [--ca-keyusage ] [--days ] [--pem]) | (cert ([--ca ] | [--ca-cert --ca-key ]) -[--ca-dn ] [--ca-pass ] [--days ] +[--ca-dn ] [--ca-keyusage ] [--ca-pass ] [--days ] [--dns ] [--in ] [--ip ] [--multiple] [--name ] [--pem] [--self-signed]) @@ -99,6 +99,9 @@ The `http` mode guides you through the process of generating certificates for us `--ca-dn ` : 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 ` +: 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 ` : 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. diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index b0f286aafae02..0a4ad2547fe3c 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -327,6 +327,18 @@ final int getKeySize(OptionSet options) { } } + final List 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); @@ -414,15 +426,7 @@ CAInfo generateCA(Terminal terminal, OptionSet options) throws Exception { } X500Principal x500Principal = new X500Principal(dn); KeyPair keyPair = CertGenUtils.generateKeyPair(getKeySize(options)); - final Function> splitByComma = v -> Arrays.stream(Strings.splitStringByCommaToArray(v)); - final KeyUsage caKeyUsage; - if (options.hasArgument(caKeyUsageSpec)) { - List keyUsageNames = caKeyUsageSpec.values(options).stream().flatMap(splitByComma).toList(); - caKeyUsage = CertGenUtils.buildKeyUsage(keyUsageNames); - } else { - caKeyUsage = CertGenUtils.buildKeyUsage(DEFAULT_CA_KEY_USAGE); - } - + final KeyUsage caKeyUsage = CertGenUtils.buildKeyUsage(getCaKeyUsage(options)); X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options), caKeyUsage); if (options.hasArgument(caPasswordSpec)) { diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java index f52f72038eef8..97734e6a463cb 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java @@ -1197,6 +1197,11 @@ private String generateCA(Path caFile, MockTerminal terminal, Environment env, b 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), + Strings.collectionToCommaDelimitedString(randomNonEmptySubsetOf(CertGenUtils.KEY_USAGE_MAPPINGS.keySet())) + ); final CertificateAuthorityCommand caCommand = new PathAwareCertificateAuthorityCommand(caFile); String[] args = { @@ -1209,7 +1214,9 @@ private String generateCA(Path caFile, MockTerminal terminal, Environment env, b "-keysize", String.valueOf(caKeySize), "-days", - String.valueOf(days) }; + String.valueOf(days), + "--ca-keyusage", + caKeyUsage }; if (pem) { args = ArrayUtils.append(args, "--pem"); } From 5f160cff1b2e7e6dd2af6c1567207cd9349a57d6 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 22:03:40 +0200 Subject: [PATCH 13/19] update ca-keyusage description --- .../elasticsearch/xpack/security/cli/CertificateTool.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index 0a4ad2547fe3c..7c9be03140615 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -281,7 +281,10 @@ final void acceptInputFile() { final void acceptCertificateAuthorityKeyUsage() { OptionSpecBuilder builder = parser.accepts( "ca-keyusage", - "comma separated key usages to use for the generated CA. defaults to " + DEFAULT_CA_KEY_USAGE + "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); From 992b8eea302ab097532a086b787ed985d2e6fc91 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 7 Apr 2025 22:38:07 +0200 Subject: [PATCH 14/19] remove randomization to avoid raising ValidatorException: `does not have keyCertSign bit set in KeyUsage extension` --- .../xpack/security/cli/CertificateToolTests.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java index 97734e6a463cb..1881f277b0589 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java @@ -1197,11 +1197,7 @@ private String generateCA(Path caFile, MockTerminal terminal, Environment env, b 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), - Strings.collectionToCommaDelimitedString(randomNonEmptySubsetOf(CertGenUtils.KEY_USAGE_MAPPINGS.keySet())) - ); + final String caKeyUsage = randomFrom("", Strings.collectionToCommaDelimitedString(CertificateTool.DEFAULT_CA_KEY_USAGE)); final CertificateAuthorityCommand caCommand = new PathAwareCertificateAuthorityCommand(caFile); String[] args = { From 41b982f707a0cff716fef060add147bb1b207b31 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 8 Apr 2025 07:37:13 +0200 Subject: [PATCH 15/19] move bit map to test class --- .../xpack/security/cli/CertGenUtils.java | 36 ------------------- .../xpack/security/cli/CertGenUtilsTests.java | 33 ++++++++++++++--- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index a338d3076026d..838f9123a62c8 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -106,42 +106,6 @@ public class CertGenUtils { ) ); - /** - * The mapping of key usage names to their corresponding bit index as defined in {@code KeyUsage} class: - * - *
    - *
  • digitalSignature (0)
  • - *
  • nonRepudiation (1)
  • - *
  • keyEncipherment (2)
  • - *
  • dataEncipherment (3)
  • - *
  • keyAgreement (4)
  • - *
  • keyCertSign (5)
  • - *
  • cRLSign (6)
  • - *
  • encipherOnly (7)
  • - *
  • decipherOnly (8)
  • - *
- */ - public static final Map 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() {} /** diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index 60f56a6791af2..85df07be87faa 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java @@ -30,6 +30,7 @@ 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; @@ -38,7 +39,6 @@ import javax.net.ssl.X509ExtendedTrustManager; import javax.security.auth.x500.X500Principal; -import static org.elasticsearch.xpack.security.cli.CertGenUtils.KEY_USAGE_BITS; 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; @@ -54,6 +54,33 @@ */ public class CertGenUtilsTests extends ESTestCase { + /** + * The mapping of key usage names to their corresponding bit index as defined in {@code KeyUsage} class: + * + *
    + *
  • digitalSignature (0)
  • + *
  • nonRepudiation (1)
  • + *
  • keyEncipherment (2)
  • + *
  • dataEncipherment (3)
  • + *
  • keyAgreement (4)
  • + *
  • keyCertSign (5)
  • + *
  • cRLSign (6)
  • + *
  • encipherOnly (7)
  • + *
  • decipherOnly (8)
  • + *
+ */ + public static final Map 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()); @@ -261,9 +288,7 @@ public void testIsValidKeyUsage() { public static void assertExpectedKeyUsage(X509Certificate certificate, List 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 expectedBitsToBeSet = expectedKeyUsage.stream() - .map(CertGenUtils.KEY_USAGE_BITS::get) - .collect(Collectors.toSet()); + final Set expectedBitsToBeSet = expectedKeyUsage.stream().map(KEY_USAGE_BITS::get).collect(Collectors.toSet()); for (int i = 0; i < keyUsage.length; i++) { if (expectedBitsToBeSet.contains(i)) { From fa68b1a7a5e04cfc59e9352450216955bcd1ef5b Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 8 Apr 2025 07:49:57 +0200 Subject: [PATCH 16/19] map of entires --- .../xpack/security/cli/CertGenUtils.java | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java index 838f9123a62c8..b82e2e1d77faf 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertGenUtils.java @@ -83,25 +83,16 @@ public class CertGenUtils { */ public static final Map 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 + 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) ) ) ); From 5fdda888ae611cfd57364106e3599c655a4916dd Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 8 Apr 2025 08:05:14 +0200 Subject: [PATCH 17/19] rename `ca-keyusage` to `keyusage` and make it applicable only to `ca` --- .../elasticsearch/command-line-tools/certutil.md | 10 +++++----- .../xpack/security/cli/CertificateTool.java | 14 +++++++++----- .../xpack/security/cli/CertificateToolTests.java | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/reference/elasticsearch/command-line-tools/certutil.md b/docs/reference/elasticsearch/command-line-tools/certutil.md index 5db4547771bff..db5d8fb73d3cc 100644 --- a/docs/reference/elasticsearch/command-line-tools/certutil.md +++ b/docs/reference/elasticsearch/command-line-tools/certutil.md @@ -13,10 +13,10 @@ The `elasticsearch-certutil` command simplifies the creation of certificates for ```shell bin/elasticsearch-certutil ( -(ca [--ca-dn ] [--ca-keyusage ] [--days ] [--pem]) +(ca [--ca-dn ] [--keyusage ] [--days ] [--pem]) | (cert ([--ca ] | [--ca-cert --ca-key ]) -[--ca-dn ] [--ca-keyusage ] [--ca-pass ] [--days ] +[--ca-dn ] [--ca-pass ] [--days ] [--dns ] [--in ] [--ip ] [--multiple] [--name ] [--pem] [--self-signed]) @@ -99,15 +99,15 @@ The `http` mode guides you through the process of generating certificates for us `--ca-dn ` : 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 ` -: 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 ` : 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. `--ca-pass ` : 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 ` +: 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 ` : 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. diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index 7c9be03140615..b5e9ecfeaee68 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -250,7 +250,6 @@ final void acceptsCertificateAuthority() { .withOptionalArg(); acceptsCertificateAuthorityName(); - acceptCertificateAuthorityKeyUsage(); } void acceptsCertificateAuthorityName() { @@ -280,7 +279,7 @@ final void acceptInputFile() { final void acceptCertificateAuthorityKeyUsage() { OptionSpecBuilder builder = parser.accepts( - "ca-keyusage", + "keyusage", "comma separated key usages to use for the generated CA. " + "defaults to '" + Strings.collectionToCommaDelimitedString(DEFAULT_CA_KEY_USAGE) @@ -332,11 +331,16 @@ final int getKeySize(OptionSet options) { final List getCaKeyUsage(OptionSet options) { if (options.has(caKeyUsageSpec)) { - String rawCaKeyUsage = caKeyUsageSpec.value(options); - if (Strings.isNullOrEmpty(rawCaKeyUsage)) { + final Function> splitByComma = v -> Stream.of(Strings.splitStringByCommaToArray(v)); + final List caKeyUsage = caKeyUsageSpec.values(options) + .stream() + .flatMap(splitByComma) + .filter(v -> false == Strings.isNullOrEmpty(v)) + .toList(); + if (caKeyUsage.isEmpty()) { return DEFAULT_CA_KEY_USAGE; } - return List.of(Strings.splitStringByCommaToArray(rawCaKeyUsage)); + return caKeyUsage; } else { return DEFAULT_CA_KEY_USAGE; } diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java index 1881f277b0589..32b2aabb29611 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java @@ -1211,7 +1211,7 @@ private String generateCA(Path caFile, MockTerminal terminal, Environment env, b String.valueOf(caKeySize), "-days", String.valueOf(days), - "--ca-keyusage", + "-keyusage", caKeyUsage }; if (pem) { args = ArrayUtils.append(args, "--pem"); From e03f0962c669c4db46d8adbad30d85fdb68358c9 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 8 Apr 2025 08:13:03 +0200 Subject: [PATCH 18/19] remove obsolete dependencies to unapplicable ca options --- .../xpack/security/cli/CertificateTool.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java index b5e9ecfeaee68..b64e21786279b 100644 --- a/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java +++ b/x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java @@ -278,20 +278,13 @@ final void acceptInputFile() { } final void acceptCertificateAuthorityKeyUsage() { - OptionSpecBuilder builder = parser.accepts( + caKeyUsageSpec = parser.accepts( "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(); + ).withRequiredArg(); } // For testing From 06c04c06f44bc32b43090ce2a9e8cf0b795cb65e Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Tue, 8 Apr 2025 08:16:50 +0200 Subject: [PATCH 19/19] make it private --- .../org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java index 85df07be87faa..ff264f8ef166b 100644 --- a/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java +++ b/x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertGenUtilsTests.java @@ -69,7 +69,7 @@ public class CertGenUtilsTests extends ESTestCase { *
  • decipherOnly (8)
  • * */ - public static final Map KEY_USAGE_BITS = Map.ofEntries( + private static final Map KEY_USAGE_BITS = Map.ofEntries( Map.entry("digitalSignature", 0), Map.entry("nonRepudiation", 1), Map.entry("keyEncipherment", 2),