Search code examples
javacmdopensslinputstreamprocessbuilder

Sign .csr file with "openssl ca" command using Java ProcessBuilder (through either cmd.exe or openssl.exe)


I'm trying to automatize the process of manual generation of .p12 signed certificate for web authorization.

KeyPair (.p12) and CertReq (.csr) files are generated using JDK's keytool program. Now I need to sign .p12 with an intermediate certificate using OpenSSL, but unlike executing "keytool" commands in ProcessBuilder or in Runtime.getRuntime().exec(...), openssl struggles working through Java's Process object. I don't know what's wrong with it.

The command I need to be executed:

openssl ca -config ./CA_config.cnf -extensions my_client_cert -infiles ./CA_certreqs/MY_CLIENT_AAAAAA.csr

There are 3 moments when it waits for user input and outputs text in-between them:

  1. enter CA_certificate.crt password (the .crt file CA_config.cnf is pointing at);
  2. sign the certificate - [y/n];
  3. commit the results - [y/n].

The code snapshot is provided below. Most vars are replaced with hardcode for easier reading.

private static void signCertReqWithOpenSSL2() throws IOException {
    String command = "openssl ca -config ./CA_config.cnf -extensions my_client_cert -infiles ./CA_certreqs/MY_CLIENT_AAAAAA.csr"
    String[] commandSeparated = command.split(" ");
    
    //init cmd process
    ProcessBuilder pb = new ProcessBuilder("cmd.exe");
    pb.redirectErrorStream(true);
    pb.directory(new File("../dir1/dir2/").getAbsoluteFile());
    pb.command(commandSeparated);
    Process process = pb.start();
    
    try (InputStream in = process.getInputStream());
         OutputStream out = process.getOutputStream()) {
        System.out.println("--- begin---");
        
        readAllConsoleOutputFromBuffer(in, 80); //93 bytes actually
        
        //enter CA_certificate.crt password
        enterUserInputToOutputStream(out, caPassword);
        readAllConsoleOutputFromBuffer(in, 10); //350
        
        //sign the certificate
        enterUserInputToOutputStream(out, "y");
        readAllConsoleOutputFromBuffer(in, 10); //56
        
        //commit the certification
        enterUserInputToOutputStream(out, "y");
        readAllConsoleOutputFromBuffer(process.getInputStream(), 10); //4815

        System.out.println("--- end ---");
    }
    process.destroy();
}

private static void enterUserInputToOutputStream(OutputStream out, String input) throws IOException {
    out.write(String.format("%s%n", input).getBytes());
    out.flush();
}

//if the stream has enough text to be printed (indicating that it's probably ready for user input), print it
private static void readAllConsoleOutputFromBuffer(InputStream in, int minTextSizeInBytes) throws IOException {
//loop is made just to make it scanning the stream during some time. I know there're better ways
    for (int i = 0; i < 100000; i++) {
        if (in.available() > minTextSizeInBytes) {
            String line;
            BufferedReader buff = new BufferedReader(new InputStreamReader(in));
            while ((line = buff.readLine()) != null) {
                System.out.println(line);
            }
            break;
        }
    }
}

Problem: I cannot make it reach the end, so it generates a new .pem file and/or outputs "BEGIN CERTIFICATE" text in the console for my further processing.

It doesn't reach even the first input spot where I need to enter CA_certificate.crt password. At best, I catch the first output line "Using configuration from ./CA_config.cnf".

I'm sure everything is set up fine.

  • openssl directory is present in %PATH%;
  • all files and folders exist and OpenSSL finds them (if I make a mistake in CA_config.cnf or remove any file needed for execution, I catch the error in the console output that something is not found).

What I've tried:

  • ignoring the console outputs (interaction with InputStream);
  • various ways of waiting for a while, so openssl would be ready to consume inputs from me (Thread.sleep, other Thread checking conditions or sleeping, for loop to make some time pass, etc.);
  • using openssl.exe as the executable instead of cmd.exe - I rewrote the paths in the command and CA_config.cnf and got the same result as with cmd.exe and its relative paths.
  • messing with strings and encodings in case it somehow stucks at the line terminator after the first output line being read, even though I doubt it's the root cause.

Any help or ideas how to make work fine other than deligating the command to a .bat file? Maybe I don't interact with the Process object's Input and Output streams in the right way.

Any help is appreciated!

OS: Windows 10 x64


Solution

  • It doesn't reach even the first input spot where I need to enter CA_certificate.crt password.

    You use BufferedReader.readLine which returns when, and only when, it reads a line terminator aka end-of-line/EOL -- usually LF on Unix, CRLF on Windows, or CR on classic Mac, but it accepts any of the three on any type of system -- or the input stream hits end-of-file/EOF, and since the connection from a child process' stdout or stderr (or both when you combine them) is a pipe, EOF occurs only when the child process closes its end of the pipe or exits. In general a prompt is neither terminated by LF/CRLF/CR nor followed immediately by closing or exiting, hence your code won't read it. But for this prompt see more below.

    various ways of waiting for a while, so openssl would be ready to consume inputs from me

    This is unnecessary and useless; the connection to a child process' stdin is also a pipe and pipes are buffered, so you can write at least a few kilobytes even if the child isn't ready to read yet.

    Your actual problem is that openssl passphrase prompts do not use stderr (or stdout) and stdin, but instead directly use the console, which ProcessBuilder cannot capture or handle. Instead of trying to answer this prompt, add to your command two additional arguments -passin pass:<value>.

    After doing that you can answer y+EOL to (with or without previously reading) the 'sign' and 'commit' prompts, but more easily you can add another argument -batch which skips these prompts entirely.

    Aside: since you're using keytool already, have you considered using keytool -gencert to generate the (child) cert from CSR instead of futzing with openssl? It doesn't maintain a 'database' for you like openssl ca does, but if you actually want that you can do it yourself in Java, and probably better.