Skip to content

Commit 07296d5

Browse files
authored
Respect --pass option in certutil csr mode (#109834)
elasticsearch-certutil csr generates a private key and a certificate signing request (CSR) file. It has always accepted the "--pass" command line option, but ignore it and always generated an unencrypted private key. This commit fixes the utility so the --pass option is respected and the private key is encrypted. Backport of: #106105
1 parent de87788 commit 07296d5

File tree

3 files changed

+185
-38
lines changed

3 files changed

+185
-38
lines changed

docs/changelog/106105.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 106105
2+
summary: Respect --pass option in certutil csr mode
3+
area: TLS
4+
type: bug
5+
issues: []

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

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,29 @@ static void writePkcs12(
589589
}
590590
});
591591
}
592+
593+
protected void writePemPrivateKey(
594+
Terminal terminal,
595+
OptionSet options,
596+
ZipOutputStream outputStream,
597+
JcaPEMWriter pemWriter,
598+
String keyFileName,
599+
PrivateKey privateKey
600+
) throws IOException {
601+
final boolean usePassword = useOutputPassword(options);
602+
final char[] outputPassword = getOutputPassword(options);
603+
outputStream.putNextEntry(new ZipEntry(keyFileName));
604+
if (usePassword) {
605+
withPassword(keyFileName, outputPassword, terminal, true, password -> {
606+
pemWriter.writeObject(privateKey, getEncrypter(password));
607+
return null;
608+
});
609+
} else {
610+
pemWriter.writeObject(privateKey);
611+
}
612+
pemWriter.flush();
613+
outputStream.closeEntry();
614+
}
592615
}
593616

594617
static class SigningRequestCommand extends CertificateCommand {
@@ -620,9 +643,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
620643
terminal.println("");
621644

622645
final Path output = resolveOutputPath(terminal, options, DEFAULT_CSR_ZIP);
623-
final int keySize = getKeySize(options);
624-
Collection<CertificateInformation> certificateInformations = getCertificateInformationList(terminal, options);
625-
generateAndWriteCsrs(output, keySize, certificateInformations);
646+
generateAndWriteCsrs(terminal, options, output);
626647

627648
terminal.println("");
628649
terminal.println("Certificate signing requests have been written to " + output);
@@ -638,12 +659,25 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th
638659
terminal.println("follow the SSL configuration instructions in the product guide.");
639660
}
640661

662+
// For testing
663+
void generateAndWriteCsrs(Terminal terminal, OptionSet options, Path output) throws Exception {
664+
final int keySize = getKeySize(options);
665+
Collection<CertificateInformation> certificateInformations = getCertificateInformationList(terminal, options);
666+
generateAndWriteCsrs(terminal, options, output, keySize, certificateInformations);
667+
}
668+
641669
/**
642670
* Generates certificate signing requests and writes them out to the specified file in zip format
643671
*
644672
* @param certInfo the details to use in the certificate signing requests
645673
*/
646-
void generateAndWriteCsrs(Path output, int keySize, Collection<CertificateInformation> certInfo) throws Exception {
674+
void generateAndWriteCsrs(
675+
Terminal terminal,
676+
OptionSet options,
677+
Path output,
678+
int keySize,
679+
Collection<CertificateInformation> certInfo
680+
) throws Exception {
647681
fullyWriteZipFile(output, (outputStream, pemWriter) -> {
648682
for (CertificateInformation certificateInformation : certInfo) {
649683
KeyPair keyPair = CertGenUtils.generateKeyPair(keySize);
@@ -666,10 +700,14 @@ void generateAndWriteCsrs(Path output, int keySize, Collection<CertificateInform
666700
outputStream.closeEntry();
667701

668702
// write private key
669-
outputStream.putNextEntry(new ZipEntry(dirName + certificateInformation.name.filename + ".key"));
670-
pemWriter.writeObject(keyPair.getPrivate());
671-
pemWriter.flush();
672-
outputStream.closeEntry();
703+
super.writePemPrivateKey(
704+
terminal,
705+
options,
706+
outputStream,
707+
pemWriter,
708+
dirName + certificateInformation.name.filename + ".key",
709+
keyPair.getPrivate()
710+
);
673711
}
674712
});
675713
}
@@ -805,10 +843,8 @@ void generateAndWriteSignedCertificates(
805843

806844
final int keySize = getKeySize(options);
807845
final int days = getDays(options);
808-
final char[] outputPassword = super.getOutputPassword(options);
809846
if (writeZipFile) {
810847
final boolean usePem = usePemFormat(options);
811-
final boolean usePassword = super.useOutputPassword(options);
812848
fullyWriteZipFile(output, (outputStream, pemWriter) -> {
813849
// write out the CA info first if it was generated
814850
if (caInfo != null && caInfo.generated) {
@@ -838,20 +874,10 @@ void generateAndWriteSignedCertificates(
838874
outputStream.closeEntry();
839875

840876
// write private key
841-
final String keyFileName = entryBase + ".key";
842-
outputStream.putNextEntry(new ZipEntry(keyFileName));
843-
if (usePassword) {
844-
withPassword(keyFileName, outputPassword, terminal, true, password -> {
845-
pemWriter.writeObject(pair.key, getEncrypter(password));
846-
return null;
847-
});
848-
} else {
849-
pemWriter.writeObject(pair.key);
850-
}
851-
pemWriter.flush();
852-
outputStream.closeEntry();
877+
writePemPrivateKey(terminal, options, outputStream, pemWriter, entryBase + ".key", pair.key);
853878
} else {
854879
final String fileName = entryBase + ".p12";
880+
final char[] outputPassword = super.getOutputPassword(options);
855881
outputStream.putNextEntry(new ZipEntry(fileName));
856882
writePkcs12(
857883
fileName,
@@ -868,6 +894,7 @@ void generateAndWriteSignedCertificates(
868894
});
869895
} else {
870896
assert certs.size() == 1;
897+
final char[] outputPassword = super.getOutputPassword(options);
871898
CertificateInformation certificateInformation = certs.iterator().next();
872899
CertificateAndKey pair = generateCertificateAndKey(certificateInformation, caInfo, keySize, days);
873900
fullyWriteFile(

x-pack/plugin/security/cli/src/test/java/org/elasticsearch/xpack/security/cli/CertificateToolTests.java

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import org.bouncycastle.cert.X509CertificateHolder;
2727
import org.bouncycastle.openssl.PEMDecryptorProvider;
2828
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
29+
import org.bouncycastle.openssl.PEMKeyPair;
2930
import org.bouncycastle.openssl.PEMParser;
31+
import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider;
3032
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
3133
import org.elasticsearch.cli.MockTerminal;
3234
import org.elasticsearch.cli.Terminal;
@@ -59,6 +61,7 @@
5961
import java.io.Reader;
6062
import java.net.InetAddress;
6163
import java.net.URI;
64+
import java.net.URISyntaxException;
6265
import java.nio.charset.StandardCharsets;
6366
import java.nio.file.FileSystem;
6467
import java.nio.file.FileSystems;
@@ -74,6 +77,7 @@
7477
import java.security.cert.X509Certificate;
7578
import java.security.interfaces.RSAKey;
7679
import java.time.temporal.ChronoUnit;
80+
import java.util.ArrayList;
7781
import java.util.Arrays;
7882
import java.util.Collection;
7983
import java.util.Collections;
@@ -85,6 +89,7 @@
8589
import java.util.concurrent.atomic.AtomicBoolean;
8690
import java.util.function.Function;
8791
import java.util.stream.Collectors;
92+
import java.util.stream.Stream;
8893

8994
import javax.net.ssl.KeyManagerFactory;
9095
import javax.net.ssl.TrustManagerFactory;
@@ -257,17 +262,35 @@ public void testParsingFileWithInvalidDetails() throws Exception {
257262
assertThat(terminal.getErrorOutput(), containsString("could not be converted to a valid DN"));
258263
}
259264

260-
public void testGeneratingCsr() throws Exception {
265+
public void testGeneratingCsrFromInstancesFile() throws Exception {
261266
Path tempDir = initTempDir();
262267
Path outputFile = tempDir.resolve("out.zip");
268+
MockTerminal terminal = new MockTerminal();
269+
final List<String> args = new ArrayList<>();
270+
263271
Path instanceFile = writeInstancesTo(tempDir.resolve("instances.yml"));
264272
Collection<CertificateInformation> certInfos = CertificateTool.parseFile(instanceFile);
265273
assertEquals(4, certInfos.size());
266274

267275
assertFalse(Files.exists(outputFile));
268276
int keySize = randomFrom(1024, 2048);
269277

270-
new CertificateTool.SigningRequestCommand().generateAndWriteCsrs(outputFile, keySize, certInfos);
278+
final boolean encrypt = randomBoolean();
279+
final String password = encrypt ? randomAlphaOfLengthBetween(8, 12) : null;
280+
if (encrypt) {
281+
args.add("--pass");
282+
if (randomBoolean()) {
283+
args.add(password);
284+
} else {
285+
for (Object ignore : certInfos) {
286+
terminal.addSecretInput(password);
287+
}
288+
}
289+
}
290+
291+
final CertificateTool.SigningRequestCommand command = new CertificateTool.SigningRequestCommand();
292+
final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
293+
command.generateAndWriteCsrs(terminal, options, outputFile, keySize, certInfos);
271294
assertTrue(Files.exists(outputFile));
272295

273296
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
@@ -284,7 +307,6 @@ public void testGeneratingCsr() throws Exception {
284307
assertTrue(Files.exists(zipRoot.resolve(filename)));
285308
final Path csr = zipRoot.resolve(filename + "/" + filename + ".csr");
286309
assertTrue(Files.exists(csr));
287-
assertTrue(Files.exists(zipRoot.resolve(filename + "/" + filename + ".key")));
288310
PKCS10CertificationRequest request = readCertificateRequest(csr);
289311
assertEquals(certInfo.name.x500Principal.getName(), request.getSubject().toString());
290312
Attribute[] extensionsReq = request.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
@@ -296,7 +318,82 @@ public void testGeneratingCsr() throws Exception {
296318
} else {
297319
assertEquals(0, extensionsReq.length);
298320
}
321+
322+
final Path keyPath = zipRoot.resolve(filename + "/" + filename + ".key");
323+
assertTrue(Files.exists(keyPath));
324+
PEMKeyPair key = readPrivateKey(keyPath, password);
325+
assertNotNull(key);
326+
}
327+
}
328+
329+
public void testGeneratingCsrFromCommandLineParameters() throws Exception {
330+
Path tempDir = initTempDir();
331+
Path outputFile = tempDir.resolve("out.zip");
332+
MockTerminal terminal = new MockTerminal();
333+
final List<String> args = new ArrayList<>();
334+
335+
final int keySize = randomFrom(1024, 2048);
336+
args.add("--keysize");
337+
args.add(String.valueOf(keySize));
338+
339+
final String name = randomAlphaOfLengthBetween(4, 16);
340+
args.add("--name");
341+
args.add(name);
342+
343+
final List<String> dns = randomList(0, 4, () -> randomAlphaOfLengthBetween(4, 8) + "." + randomAlphaOfLengthBetween(2, 5));
344+
dns.stream().map(s -> "--dns=" + s).forEach(args::add);
345+
final List<String> ip = randomList(
346+
0,
347+
2,
348+
() -> Stream.generate(() -> randomIntBetween(10, 250)).limit(4).map(String::valueOf).collect(Collectors.joining("."))
349+
);
350+
ip.stream().map(s -> "--ip=" + s).forEach(args::add);
351+
352+
final boolean encrypt = randomBoolean();
353+
final String password = encrypt ? randomAlphaOfLengthBetween(8, 12) : null;
354+
if (encrypt) {
355+
args.add("--pass");
356+
if (randomBoolean()) {
357+
args.add(password);
358+
} else {
359+
terminal.addSecretInput(password);
360+
}
361+
}
362+
363+
final CertificateTool.SigningRequestCommand command = new CertificateTool.SigningRequestCommand();
364+
final OptionSet options = command.getParser().parse(Strings.toStringArray(args));
365+
command.generateAndWriteCsrs(terminal, options, outputFile);
366+
assertTrue(Files.exists(outputFile));
367+
368+
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(outputFile);
369+
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_READ));
370+
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_WRITE));
371+
assertEquals(perms.toString(), 2, perms.size());
372+
373+
final Path zipRoot = getRootPathOfZip(outputFile);
374+
375+
assertFalse(Files.exists(zipRoot.resolve("ca")));
376+
assertTrue(Files.exists(zipRoot.resolve(name)));
377+
final Path csr = zipRoot.resolve(name + "/" + name + ".csr");
378+
assertTrue(Files.exists(csr));
379+
380+
PKCS10CertificationRequest request = readCertificateRequest(csr);
381+
assertEquals("CN=" + name, request.getSubject().toString());
382+
383+
Attribute[] extensionsReq = request.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
384+
if (dns.size() > 0 || ip.size() > 0) {
385+
assertEquals(1, extensionsReq.length);
386+
Extensions extensions = Extensions.getInstance(extensionsReq[0].getAttributeValues()[0]);
387+
GeneralNames subjAltNames = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName);
388+
assertSubjAltNames(subjAltNames, ip, dns);
389+
} else {
390+
assertEquals(0, extensionsReq.length);
299391
}
392+
393+
final Path keyPath = zipRoot.resolve(name + "/" + name + ".key");
394+
assertTrue(Files.exists(keyPath));
395+
PEMKeyPair key = readPrivateKey(keyPath, password);
396+
assertNotNull(key);
300397
}
301398

302399
public void testGeneratingSignedPemCertificates() throws Exception {
@@ -968,19 +1065,6 @@ private int getDurationInDays(X509Certificate cert) {
9681065
return (int) ChronoUnit.DAYS.between(cert.getNotBefore().toInstant(), cert.getNotAfter().toInstant());
9691066
}
9701067

971-
private void assertSubjAltNames(Certificate certificate, String ip, String dns) throws Exception {
972-
final X509CertificateHolder holder = new X509CertificateHolder(certificate.getEncoded());
973-
final GeneralNames names = GeneralNames.fromExtensions(holder.getExtensions(), Extension.subjectAlternativeName);
974-
final CertificateInformation certInfo = new CertificateInformation(
975-
"n",
976-
"n",
977-
Collections.singletonList(ip),
978-
Collections.singletonList(dns),
979-
Collections.emptyList()
980-
);
981-
assertSubjAltNames(names, certInfo);
982-
}
983-
9841068
/**
9851069
* Checks whether there are keys in {@code keyStore} that are trusted by {@code trustStore}.
9861070
*/
@@ -1014,13 +1098,39 @@ private PKCS10CertificationRequest readCertificateRequest(Path path) throws Exce
10141098
}
10151099
}
10161100

1101+
private PEMKeyPair readPrivateKey(Path path, String password) throws Exception {
1102+
try (Reader reader = Files.newBufferedReader(path); PEMParser pemParser = new PEMParser(reader)) {
1103+
Object object = pemParser.readObject();
1104+
if (password == null) {
1105+
assertThat(object, instanceOf(PEMKeyPair.class));
1106+
return (PEMKeyPair) object;
1107+
} else {
1108+
assertThat(object, instanceOf(PEMEncryptedKeyPair.class));
1109+
final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) object;
1110+
assertThat(encryptedKeyPair.getDekAlgName(), is("AES-128-CBC"));
1111+
return encryptedKeyPair.decryptKeyPair(new BcPEMDecryptorProvider(password.toCharArray()));
1112+
}
1113+
}
1114+
}
1115+
10171116
private X509Certificate readX509Certificate(InputStream input) throws Exception {
10181117
List<Certificate> list = CertParsingUtils.readCertificates(input);
10191118
assertEquals(1, list.size());
10201119
assertThat(list.get(0), instanceOf(X509Certificate.class));
10211120
return (X509Certificate) list.get(0);
10221121
}
10231122

1123+
private void assertSubjAltNames(Certificate certificate, String ip, String dns) throws Exception {
1124+
final X509CertificateHolder holder = new X509CertificateHolder(certificate.getEncoded());
1125+
final GeneralNames names = GeneralNames.fromExtensions(holder.getExtensions(), Extension.subjectAlternativeName);
1126+
assertSubjAltNames(names, Collections.singletonList(ip), Collections.singletonList(dns));
1127+
}
1128+
1129+
private void assertSubjAltNames(GeneralNames generalNames, List<String> ip, List<String> dns) throws Exception {
1130+
final CertificateInformation certInfo = new CertificateInformation("n", "n", ip, dns, Collections.emptyList());
1131+
assertSubjAltNames(generalNames, certInfo);
1132+
}
1133+
10241134
private void assertSubjAltNames(GeneralNames subjAltNames, CertificateInformation certInfo) throws Exception {
10251135
final int expectedCount = certInfo.ipAddresses.size() + certInfo.dnsNames.size() + certInfo.commonNames.size();
10261136
assertEquals(expectedCount, subjAltNames.getNames().length);
@@ -1102,6 +1212,11 @@ private static Path resolvePath(String path) {
11021212
return PathUtils.get(path).toAbsolutePath();
11031213
}
11041214

1215+
private static Path getRootPathOfZip(Path pemZip) throws IOException, URISyntaxException {
1216+
FileSystem zipFS = FileSystems.newFileSystem(new URI("jar:" + pemZip.toUri()), Collections.emptyMap());
1217+
return zipFS.getPath("/");
1218+
}
1219+
11051220
private String generateCA(Path caFile, MockTerminal terminal, Environment env) throws Exception {
11061221
final int caKeySize = randomIntBetween(4, 8) * 512;
11071222
final int days = randomIntBetween(7, 1500);

0 commit comments

Comments
 (0)