in x-pack/plugin/security/cli/src/main/java/org/elasticsearch/xpack/security/cli/CertificateTool.java [188:647]
is used as the directory name (within the zip) and the prefix for the key and
certificate files. The filename is required if you are prompted and the name
is not displayed in the prompt.
* IP addresses and DNS names are optional. Multiple values can be specified as a
comma separated string. If no IP addresses or DNS names are provided, you may
disable hostname verification in your SSL configuration.""".indent(4);
static final String CA_EXPLANATION = """
* All certificates generated by this tool will be signed by a certificate authority (CA)
unless the --self-signed command line option is specified.
The tool can automatically generate a new CA for you, or you can provide your own with
the --ca or --ca-cert command line options.""".indent(4);
abstract static class CertificateCommand extends EnvironmentAwareCommand {
// Common option for multiple commands.
// Not every command uses every option, but where they are common we want to keep them consistent
final OptionSpec<String> outputPathSpec;
final OptionSpec<String> outputPasswordSpec;
final OptionSpec<Integer> keysizeSpec;
OptionSpec<String> caKeyUsageSpec;
OptionSpec<Void> pemFormatSpec;
OptionSpec<Integer> daysSpec;
OptionSpec<String> caPkcs12PathSpec;
OptionSpec<String> caCertPathSpec;
OptionSpec<String> caKeyPathSpec;
OptionSpec<String> caPasswordSpec;
OptionSpec<String> caDnSpec;
OptionSpec<Void> multipleNodesSpec;
OptionSpec<String> nameSpec;
OptionSpec<String> dnsNamesSpec;
OptionSpec<String> ipAddressesSpec;
OptionSpec<String> inputFileSpec;
CertificateCommand(String description) {
super(description);
outputPathSpec = parser.accepts("out", "path to the output file that should be produced").withRequiredArg();
outputPasswordSpec = parser.accepts("pass", "password for generated private keys").withOptionalArg();
keysizeSpec = parser.accepts("keysize", "size in bits of RSA keys").withRequiredArg().ofType(Integer.class);
}
final void acceptCertificateGenerationOptions() {
pemFormatSpec = parser.accepts("pem", "output certificates and keys in PEM format instead of PKCS#12");
daysSpec = parser.accepts("days", "number of days that the generated certificates are valid")
.withRequiredArg()
.ofType(Integer.class);
}
final void acceptsCertificateAuthority() {
caPkcs12PathSpec = parser.accepts("ca", "path to an existing ca key pair (in PKCS#12 format)").withRequiredArg();
caCertPathSpec = parser.accepts("ca-cert", "path to an existing ca certificate")
.availableUnless(caPkcs12PathSpec)
.withRequiredArg();
caKeyPathSpec = parser.accepts("ca-key", "path to an existing ca private key")
.availableIf(caCertPathSpec)
.requiredIf(caCertPathSpec)
.withRequiredArg();
caPasswordSpec = parser.accepts("ca-pass", "password for an existing ca private key or the generated ca private key")
.withOptionalArg();
acceptsCertificateAuthorityName();
}
void acceptsCertificateAuthorityName() {
OptionSpecBuilder builder = parser.accepts(
"ca-dn",
"distinguished name to use for the generated ca. defaults to " + AUTO_GEN_CA_DN
);
if (caPkcs12PathSpec != null) {
builder = builder.availableUnless(caPkcs12PathSpec);
}
if (caCertPathSpec != null) {
builder = builder.availableUnless(caCertPathSpec);
}
caDnSpec = builder.withRequiredArg();
}
final void acceptInstanceDetails() {
multipleNodesSpec = parser.accepts("multiple", "generate files for multiple instances");
nameSpec = parser.accepts("name", "name of the generated certificate").availableUnless(multipleNodesSpec).withRequiredArg();
dnsNamesSpec = parser.accepts("dns", "comma separated DNS names").availableUnless(multipleNodesSpec).withRequiredArg();
ipAddressesSpec = parser.accepts("ip", "comma separated IP addresses").availableUnless(multipleNodesSpec).withRequiredArg();
}
final void acceptInputFile() {
inputFileSpec = parser.accepts("in", "file containing details of the instances in yaml format").withRequiredArg();
}
final void acceptCertificateAuthorityKeyUsage() {
caKeyUsageSpec = parser.accepts(
"keyusage",
"comma separated key usages to use for the generated CA. "
+ "defaults to '"
+ Strings.collectionToCommaDelimitedString(DEFAULT_CA_KEY_USAGE)
+ "'"
).withRequiredArg();
}
// For testing
OptionParser getParser() {
return parser;
}
/**
* Checks for output file in the user specified options or prompts the user for the output file.
* The resulting path is stored in the {@code config} parameter.
*/
Path resolveOutputPath(Terminal terminal, OptionSet options, String defaultFilename) throws IOException {
return resolveOutputPath(terminal, outputPathSpec.value(options), defaultFilename);
}
static Path resolveOutputPath(Terminal terminal, String userOption, String defaultFilename) {
Path file;
if (userOption != null) {
file = CertificateTool.resolvePath(userOption);
} else {
file = CertificateTool.resolvePath(defaultFilename);
String input = terminal.readText("Please enter the desired output file [" + file + "]: ");
if (input.isEmpty() == false) {
file = CertificateTool.resolvePath(input);
}
}
return file.toAbsolutePath();
}
final int getKeySize(OptionSet options) {
if (options.has(keysizeSpec)) {
return keysizeSpec.value(options);
} else {
return DEFAULT_KEY_SIZE;
}
}
final List<String> getCaKeyUsage(OptionSet options) {
if (options.has(caKeyUsageSpec)) {
final Function<String, Stream<? extends String>> splitByComma = v -> Stream.of(Strings.splitStringByCommaToArray(v));
final List<String> caKeyUsage = caKeyUsageSpec.values(options)
.stream()
.flatMap(splitByComma)
.filter(v -> false == Strings.isNullOrEmpty(v))
.toList();
if (caKeyUsage.isEmpty()) {
return DEFAULT_CA_KEY_USAGE;
}
return caKeyUsage;
} else {
return DEFAULT_CA_KEY_USAGE;
}
}
final int getDays(OptionSet options) {
if (options.has(daysSpec)) {
return daysSpec.value(options);
} else {
return DEFAULT_DAYS;
}
}
boolean usePemFormat(OptionSet options) {
return options.has(pemFormatSpec);
}
boolean useOutputPassword(OptionSet options) {
return options.has(outputPasswordSpec);
}
char[] getOutputPassword(OptionSet options) {
return getChars(outputPasswordSpec.value(options));
}
protected Path resolvePath(OptionSet options, OptionSpec<String> spec) {
final String value = spec.value(options);
if (Strings.isNullOrEmpty(value)) {
return null;
}
return CertificateTool.resolvePath(value);
}
/**
* Returns the CA certificate and private key that will be used to sign certificates. These may be specified by the user or
* automatically generated
*
* @return CA cert and private key
*/
CAInfo getCAInfo(Terminal terminal, OptionSet options, Environment env) throws Exception {
if (options.has(caPkcs12PathSpec)) {
return loadPkcs12CA(terminal, options, env);
} else if (options.has(caCertPathSpec)) {
return loadPemCA(terminal, options, env);
} else {
terminal.println("Note: Generating certificates without providing a CA certificate is deprecated.");
terminal.println(" A CA certificate will become mandatory in the next major release.");
terminal.println("");
return generateCA(terminal, options);
}
}
private CAInfo loadPkcs12CA(Terminal terminal, OptionSet options, Environment env) throws Exception {
Path path = resolvePath(options, caPkcs12PathSpec);
char[] passwordOption = getChars(caPasswordSpec.value(options));
Map<Certificate, Key> keys = withPassword(
"CA (" + path + ")",
passwordOption,
terminal,
false,
password -> CertParsingUtils.readPkcs12KeyPairs(path, password, a -> password)
);
if (keys.size() != 1) {
throw new IllegalArgumentException(
"expected a single key in file [" + path.toAbsolutePath() + "] but found [" + keys.size() + "]"
);
}
final Map.Entry<Certificate, Key> pair = keys.entrySet().iterator().next();
return new CAInfo((X509Certificate) pair.getKey(), (PrivateKey) pair.getValue());
}
private CAInfo loadPemCA(Terminal terminal, OptionSet options, Environment env) throws Exception {
if (options.hasArgument(caKeyPathSpec) == false) {
throw new UserException(ExitCodes.USAGE, "Option " + caCertPathSpec + " also requires " + caKeyPathSpec);
}
Path cert = resolvePath(options, caCertPathSpec);
Path key = resolvePath(options, caKeyPathSpec);
String password = caPasswordSpec.value(options);
X509Certificate caCert = CertParsingUtils.readX509Certificate(cert);
PrivateKey privateKey = readPrivateKey(key, getChars(password), terminal);
return new CAInfo(caCert, privateKey);
}
CAInfo generateCA(Terminal terminal, OptionSet options) throws Exception {
String dn = caDnSpec.value(options);
if (Strings.isNullOrEmpty(dn)) {
dn = AUTO_GEN_CA_DN;
}
X500Principal x500Principal = new X500Principal(dn);
KeyPair keyPair = CertGenUtils.generateKeyPair(getKeySize(options));
final KeyUsage caKeyUsage = CertGenUtils.buildKeyUsage(getCaKeyUsage(options));
X509Certificate caCert = CertGenUtils.generateCACertificate(x500Principal, keyPair, getDays(options), caKeyUsage);
if (options.hasArgument(caPasswordSpec)) {
char[] password = getChars(caPasswordSpec.value(options));
checkAndConfirmPasswordLengthForOpenSSLCompatibility(password, terminal, false);
return new CAInfo(caCert, keyPair.getPrivate(), true, password);
}
if (options.has(caPasswordSpec)) {
return withPassword("CA Private key", null, terminal, true, p -> new CAInfo(caCert, keyPair.getPrivate(), true, p.clone()));
}
return new CAInfo(caCert, keyPair.getPrivate(), true, null);
}
/**
* This method handles the collection of information about each instance that is necessary to generate a certificate. The user may
* be prompted or the information can be gathered from a file
*
* @return a {@link Collection} of {@link CertificateInformation} that represents each instance
*/
Collection<CertificateInformation> getCertificateInformationList(Terminal terminal, OptionSet options) throws Exception {
final Path input = resolvePath(options, inputFileSpec);
if (input != null) {
return parseAndValidateFile(terminal, input.toAbsolutePath());
}
if (options.has(multipleNodesSpec)) {
return readMultipleCertificateInformation(terminal);
} else {
final Function<String, Stream<? extends String>> splitByComma = v -> Arrays.stream(Strings.splitStringByCommaToArray(v));
final List<String> dns = dnsNamesSpec.values(options).stream().flatMap(splitByComma).collect(Collectors.toList());
final List<String> ip = ipAddressesSpec.values(options).stream().flatMap(splitByComma).collect(Collectors.toList());
final List<String> cn = null;
final String name = getCertificateName(options);
final String fileName;
if (Name.isValidFilename(name)) {
fileName = name;
} else {
fileName = requestFileName(terminal, name);
}
CertificateInformation information = new CertificateInformation(name, fileName, ip, dns, cn);
List<String> validationErrors = information.validate();
if (validationErrors.isEmpty()) {
return Collections.singleton(information);
} else {
validationErrors.forEach(terminal::errorPrintln);
return Collections.emptyList();
}
}
}
protected String getCertificateName(OptionSet options) {
return options.has(nameSpec) ? nameSpec.value(options) : DEFAULT_CERT_NAME;
}
static Collection<CertificateInformation> readMultipleCertificateInformation(Terminal terminal) {
Map<String, CertificateInformation> map = new HashMap<>();
boolean done = false;
while (done == false) {
String name = terminal.readText("Enter instance name: ");
if (name.isEmpty() == false) {
String filename = requestFileName(terminal, name);
String ipAddresses = terminal.readText("Enter IP Addresses for instance (comma-separated if more than one) []: ");
String dnsNames = terminal.readText("Enter DNS names for instance (comma-separated if more than one) []: ");
List<String> ipList = Arrays.asList(Strings.splitStringByCommaToArray(ipAddresses));
List<String> dnsList = Arrays.asList(Strings.splitStringByCommaToArray(dnsNames));
List<String> commonNames = null;
CertificateInformation information = new CertificateInformation(name, filename, ipList, dnsList, commonNames);
List<String> validationErrors = information.validate();
if (validationErrors.isEmpty()) {
if (map.containsKey(name)) {
terminal.println("Overwriting previously defined instance information [" + name + "]");
}
map.put(name, information);
} else {
for (String validationError : validationErrors) {
terminal.println(validationError);
}
terminal.println("Skipping entry as invalid values were found");
}
} else {
terminal.println("A name must be provided");
}
String exit = terminal.readText(
"Would you like to specify another instance? Press 'y' to continue entering instance " + "information: "
);
if ("y".equals(exit) == false) {
done = true;
}
}
return map.values();
}
private static String requestFileName(Terminal terminal, String certName) {
final boolean isNameValidFilename = Name.isValidFilename(certName);
while (true) {
String filename = terminal.readText(
"Enter name for directories and files of " + certName + (isNameValidFilename ? " [" + certName + "]" : "") + ": "
);
if (filename.isEmpty() && isNameValidFilename) {
return certName;
}
if (Name.isValidFilename(filename)) {
return filename;
} else {
terminal.errorPrintln(Terminal.Verbosity.SILENT, "'" + filename + "' is not a valid filename");
continue;
}
}
}
/**
* This method handles writing out the certificate authority in PEM format to a zip file.
*
* @param outputStream the output stream to write to
* @param pemWriter the writer for PEM objects
* @param info the certificate authority information
* @param includeKey if true, write the CA key in PEM format
*/
static void writeCAInfo(ZipOutputStream outputStream, JcaPEMWriter pemWriter, CAInfo info, boolean includeKey) throws Exception {
final String caDirName = createCaDirectory(outputStream);
outputStream.putNextEntry(new ZipEntry(caDirName + "ca.crt"));
pemWriter.writeObject(info.certAndKey.cert);
pemWriter.flush();
outputStream.closeEntry();
if (includeKey) {
outputStream.putNextEntry(new ZipEntry(caDirName + "ca.key"));
if (info.password != null && info.password.length > 0) {
try {
PEMEncryptor encryptor = getEncrypter(info.password);
pemWriter.writeObject(info.certAndKey.key, encryptor);
} finally {
// we can safely nuke the password chars now
Arrays.fill(info.password, (char) 0);
}
} else {
pemWriter.writeObject(info.certAndKey.key);
}
pemWriter.flush();
outputStream.closeEntry();
}
}
private static String createCaDirectory(ZipOutputStream outputStream) throws IOException {
final String caDirName = "ca/";
ZipEntry zipEntry = new ZipEntry(caDirName);
assert zipEntry.isDirectory();
outputStream.putNextEntry(zipEntry);
return caDirName;
}
static void writePkcs12(
String fileName,
OutputStream output,
String alias,
CertificateAndKey pair,
X509Certificate caCert,
char[] password,
Terminal terminal
) throws Exception {
final KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
pkcs12.load(null);
withPassword(fileName, password, terminal, true, p12Password -> {
if (isAscii(p12Password)) {
pkcs12.setKeyEntry(alias, pair.key, p12Password, new Certificate[] { pair.cert });
if (caCert != null) {
pkcs12.setCertificateEntry("ca", caCert);
}
pkcs12.store(output, p12Password);
return null;
} else {
throw new UserException(ExitCodes.CONFIG, "PKCS#12 passwords must be plain ASCII");
}
});
}
/**
* Verify that the provided certificate is validly signed by the provided CA
*/
static void verifyIssuer(Certificate certificate, CAInfo caInfo, Terminal terminal) throws UserException {
try {
certificate.verify(caInfo.certAndKey.cert.getPublicKey());
} catch (GeneralSecurityException e) {
terminal.errorPrintln("");
terminal.errorPrintln("* ERROR *");
terminal.errorPrintln("Verification of generated certificate failed.");
terminal.errorPrintln("This usually occurs if the provided CA certificate does not match with the CA key.");
terminal.errorPrintln("Cause: " + e);
for (var c = e.getCause(); c != null; c = c.getCause()) {
terminal.errorPrintln(" - " + c);
}
throw new UserException(ExitCodes.CONFIG, "Certificate verification failed");
}
}
protected void writePemPrivateKey(
Terminal terminal,
OptionSet options,
ZipOutputStream outputStream,
JcaPEMWriter pemWriter,
String keyFileName,
PrivateKey privateKey
) throws IOException {
final boolean usePassword = useOutputPassword(options);
final char[] outputPassword = getOutputPassword(options);
outputStream.putNextEntry(new ZipEntry(keyFileName));
if (usePassword) {
withPassword(keyFileName, outputPassword, terminal, true, password -> {
pemWriter.writeObject(privateKey, getEncrypter(password));
return null;
});
} else {
pemWriter.writeObject(privateKey);
}
pemWriter.flush();
outputStream.closeEntry();
}
}