Skip to main content
  1. posts/

Testing the compatibility of gpg and bouncy-gpg

·958 words·5 mins
Coding Bouncy-Gpg Java Testing
Table of Contents
neuhalje/bouncy-gpg

Make using Bouncy Castle with OpenPGP fun again!

Java
202
60
Driving an external program from java and parsing its output is seldom needed. When it is needed, in often means: no stable API (changes in command output), configuration files, sporadic errors, and orchestrating parallel execution. To test the compatibility with gpg I needed to drive the GnuPG executable.

How to be sure that you implement it correctly?
#

In order to implement key generation for bouncy-gpg I had to make sure that everything I did was compatible with gnupg.

This meant for example that I needed to automatically run the following tests:

  • Generate keys in bouncy-gpg and import them in gpg
  • Encrypt messages in bouncy-gpg and decrypt them in gpg
  • Encrypt messages in gpg and decrypt them in bouncy-gpg

All with different variants of keyrings, e.g.

  • Only a master key with no subkeys
  • Different subkeys
  • RSA and ECC keys

One of my tests looks like this:

  1. generate a keyring for Juliet Capulet with BouncyGPG,
  2. copy the public key to GPG,
  3. encrypt a message in GPG,
  4. and finally decrypt the message in BouncyGPG

The next test works the other way around, the private key is exported to gpg and bouncy-gpg encrypts to gpg:

  1. generate a keyring for Juliet with BouncyGPG.
  2. copy the private key to GPG,
  3. encrypt a message in BouncyGPG,
  4. and finally decrypt the message in gpg

Generating keys in gpg is not automated because nearly all unit tests use keys generated in gpg.

These tests mean a lot of different gpg calls, and even more parsing of gpg output. Implementing this should make it easy to run commands, and to add new commands.

Example implementations
#

In Java, we can use Runtime.getRuntime().exec() or ProcessBuilder to run external shell commands. The interesting part is that the call results need to be interpreted. This can get ugly pretty quick. To make the whole thing a bit more interesting we need to setup the gpg home directory and configuration, and tear it down afterwards. Oh, and gpg can be named gpg or gpg2.

Entangling test code and the driver for gpg was not an option, so a command based architecture was chosen.

For example, here is how to import a secret key into gpg:

        final byte[] encoded = secretKeyRings.getEncoded();
        assertEquals(0, gpg
                .runCommand(
                        Commands.importKey(encoded, passphrase)
                ).exitCode());

Not very spectacular? Here is the whole flow to encrypt plaintext in gpg and to read out the ciphertext:

    private byte[] encryptMessageInGPG(final GPGExec gpg,
                                       final String plaintext,
                                       final String recipient) throws IOException, InterruptedException {

        final EncryptCommandResult encryptCommandResult = gpg
                .runCommand(Commands.encrypt(plaintext.getBytes(), recipient));
        Assert.assertEquals(0, encryptCommandResult.exitCode());

        return encryptCommandResult.getCiphertext();
    }

The whole test suite can be found on GitHub.

Shelling out gpg
#

gpg is orchestrated via a command pattern architecture. The core functionality to shell out gpg is located in GPGExec:

 // IOSniffer wraps stdin, stdout and stderr and "tees" everything into logfiles
  private IOSniffer gpg(Command<?> cmd) throws IOException, InterruptedException {

    List<String> command = new ArrayList<>();
    command.add(gpgExecutable());
    command.add("--homedir");
    command.add(homeDir.toAbsolutePath().toString());

    command.addAll(cmd.getArgs());

    ProcessBuilder pb =
        new ProcessBuilder(command);
    Map<String, String> env = pb.environment();
    env.put("GNUPGHOME", homeDir.toAbsolutePath().toString());

    pb
        .redirectErrorStream(false)
        .directory(homeDir.toFile());

    Process p = pb.start();

    IOSniffer sniffer = IOSniffer.wrapIO(p);
    cmd.io(sniffer.getOutputStream(), sniffer.getInputStream(), sniffer.getErrorStream());
    sniffer.getOutputStream().flush();
    sniffer.getOutputStream().close();
    p.waitFor(15, TimeUnit.SECONDS);

    if (p.isAlive()) {
      // hmm
      LOGGER.warn("Forcibly destroy process " + String.join(" ", command));
      p.destroyForcibly();
    }

    return sniffer;
  }

As an example for commands, first the VersionCommand. It runs gpg --version and parses the gpg version:

// the boring parts are deleted
public class VersionCommand implements Command {
  // gpg (GnuPG/MacGPG2) 2.2.10
  private Pattern VERSION_STRING = Pattern
      .compile("^gpg.* (?<major>[\\d]+).(?<minor>[\\d]+).(?<revision>[\\d]+)$");

  @Override
  public List<String> getArgs() {
    return asList("--version");
  }

  @Override
  public VersionCommandResult parse(InputStream stdout, int exitCode) {
    try (
        Scanner sc = new Scanner(stdout)
    ) {
      while (sc.hasNext()) {

        final String line = sc.nextLine();
        final Matcher matcher = VERSION_STRING.matcher(line);
        if (matcher.matches()) {
          final int major = Integer.parseInt(matcher.group("major"));
          final int minor = Integer.parseInt(matcher.group("minor"));
          final int revision = Integer.parseInt(matcher.group("revision"));

          return new VersionCommandResult(exitCode, line, major, minor, revision);
        }
      }
    }
    return VersionCommandResult.UNKNOWN;
  }

  public final static class VersionCommandResult implements Result<VersionCommand> {
    public final static VersionCommandResult UNKNOWN = new VersionCommandResult(-1, "", 0, 0, 0);

    private final int exitCode;
    private final String versionString;
    private final int major;
    private final int minor;
    private final int revision;

    private VersionCommandResult(final int exitCode, final String versionString, final int major,
        final int minor,
        final int revision) {
    // [...]
    }
    // [...]
    public boolean isAtLeast(int major) {
      return this.major >= major;
    }

    public boolean isAtLeast(int major, int minor) {
      return (this.major > major) || (this.major == major && this.minor >= minor);
    }

    @Override
    public int exitCode() {
      return exitCode;
    }
    // [...]
  }
}

The command to import keys is not that different:

public class ImportCommand implements Command {
  private final byte[] keyData;
  private final String passphrase;
  // [...]
  @Override
  public List<String> getArgs() {

    final List<String> args = new ArrayList<>(asList("--import",
        "--batch"));
    if (passphrase != null) {
      args.add("--passphrase");
      args.add(passphrase);
    }
    return args;
  }

  public void io(OutputStream outputStream, InputStream inputStream, InputStream errorStream)
      throws IOException {
    outputStream.write(keyData);
    outputStream.close();
  }

  @Override
  public ImportCommandResult parse(InputStream stdout, int exitCode) {
    //  nothing to do
    String output;
    try {
      final byte[] bytes = Streams.readAll(stdout);
      output = new String(bytes);
    } catch (IOException e) {
      output = e.getMessage();
    }
    return new ImportCommandResult(exitCode, output);
  }

  public final static class ImportCommandResult implements Result<ImportCommand> {
    // [...]

    @Override
    public int exitCode() {
      return exitCode;
    }
    // [...]
  }
}

Was it worth it?
#

Shelling out executables from Java feels plain wrong. But in this case it was necessary and greatly improved my confidence that gpg and bouncy-gpg are compatible. I found out that generated ECC keys are not compatible with gpg, and found out that RSA works very good, no matter where the keys are generated.

Considering that these are “just” integration tests, a lot of work went into writing the driver for gpg. But it was worth it.