Use an expect script to help users generate GPG keys.
There are occasions when you can take advantage of Unix's flexibility to control some other tool or system that is less flexible. I've used Unix scripts to update databases on user-unfriendly mainframe systems when the alternative was an expensive mainframe-programming service contract. You can use the same approach in reverse to let the user interact with a tool, but with a constrained set of choices.
The Expect scripting language is ideal for creating such interactive scripts. It is available from NetBSD pkgsrc as pkgsrc/lang/tcl-expect or pkgsrc/lang/tk-expect, as well as from the FreeBSD ports and OpenBSD packages collections. We'll use the command-line version for this example, but keep in mind that expect-tk allows you to provide a GUI frontend to a command-line process if you're willing to write a more complex script.
In this case, we'll script the generation of a GPG key. Install GPG from either pkgsrc/security/gnupg or the appropriate port or package.
7.8.1 The Key Generation Process
During the process of generating a GPG key, the program asks the user several questions. We may wish to impose constraints so that a set of users ends up with keys with similar parameters. We could train the users, but that would not guarantee correct results. Scripting the generation makes the process easier and eliminates errors.
First, let's look at a typical key generation session:
% gpg --gen-key gpg (GnuPG) 1.2.4; Copyright (C) 2003 Free Software Foundation, Inc. This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the file COPYING for details. Please select what kind of key you want: (1) DSA and ElGamal (default) (2) DSA (sign only) (4) RSA (sign only) Your selection? 4 What keysize do you want? (1024) 2048 Requested keysize is 2048 bits Please specify how long the key should be valid. 0 = key does not expire <n> = key expires in n days <n>w = key expires in n weeks <n>m = key expires in n months <n>y = key expires in n years Key is valid for? (0) 0 Key does not expire at all Is this correct (y/n)? y You need a User-ID to identify your key; the software constructs the user id from Real Name, Comment and Email Address in this form: "Heinrich Heine (Der Dichter) <email@example.com>" Real name:
Let's pause there to consider the elements we can constrain.
You probably want to specify the cryptographic algorithm and key length for all users consistently, based on your security and interoperability requirements. I'll choose RSA signing and encryption keys, but GPG doesn't provide a menu option for that. I'll have to create the signing key first and then add the encryption subkey.
7.8.2 A Simple Script
Here's an expect script that would duplicate the session shown so far:
#!/usr/pkg/bin/expect -f set timeout -1 spawn gpg --gen-key match_max 100000 expect "(4) RSA (sign only)" expect "Your selection? " send "4\r" expect "What keysize do you want? (1024) " send "2048\r" expect "Key is valid for? (0) " send -- "0\r" expect "Key does not expire at all" expect "Is this correct (y/n)? " send -- "y\r" expect "Real name: "
The script begins by setting timeout to infinite, or -1, so expect will wait forever to match the provided input. Then we spawn the process that we're going to control, gpg --gen-key. match_max sets some buffer size constraints in bytes, and the given value is far more than we will need.
After the initial settings, the script simply consists of strings that we expect from the program and strings that we send in reply. This means that the script will answer all of the questions GPG asks until Real name: , without waiting for the user's input.
Note that in several places we expect things besides the prompt. For example, before responding to the Your selection? prompt, we verify that the version of GPG we have executed still has the same meaning for the fourth option, by expecting that the text of that menu choice is still RSA (sign only). If this were a real, production-ready script, we should print a warning message and terminate the script if the value does not match our expectations, and perhaps include a check of the GPG version number. In this simple example, the script will hang, and you must break out of it with Ctrl-c.
7.8.3 Adding User Interaction
There are several ways of handling the fields we do want the user to provide. For the greatest degree of control over the user experience, we could use individual expect commands, but here we will take a simpler approach. Here's some more of the script:
interact "\r" return send "\r" expect "Email address: " interact "\r" return send "\r" expect "Comment: " interact "\r" return send "\r" expect "Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? " interact "\r" return send "\r" expect "Enter passphrase: " interact "\r" return send "\r" expect "Repeat passphrase: " interact "\r" return send "\r"
The interact command allows the user to interact directly with the spawned program. We place a constraint that the user's interaction ends as soon as the user presses the Enter key, which sends the carriage return character, \r. At that point, the interact command returns and the script resumes. Note that we have to send the \r from the script; expect intercepted the carriage return and GPG did not see it.
7.8.4 Handling Incorrect Input
Again, a correct script would have a more complex flow of execution and allow for cases where the spawned program rejects the user's input with an error message. For example, the Real Name field must be more than five characters long. If a user types less than five characters, GPG will prompt him to retype his username. However, the expect script just shown will not accept the new user input, because it is now waiting for the Email address: prompt.
Alternatively, we could replace these three lines:
interact "\r" return send "\r" expect "Email address: "
interact -o "Email address: " return send_user "Email address: "
Instead of stopping interaction when the user presses return, we stop interaction when the program outputs the Email address: prompt. That's the difference between interact and interact -o; the former stops interaction based on input from the user, and the latter on output from the program. This time, we don't need to send the carriage return, because the user's keypress is passed through to GPG. However, we do need to echo the prompt, because expect has consumed it. This method lets GPG handle the error conditions for us:
Real name: abc Name must be at least 5 characters long Real name: abcde Email address:
7.8.5 Hacking the Hack
After GPG receives the information it needs to generate the key, it might not be able to find enough high-quality random data from the system. The script ought to handle that by spawning a process to generate more system activity, such as performing a lot of disk activity by running a find across the entire disk.
After generating the signing key, the script could spawn a new instance of GPG with the --edit-key option, to generate the desired RSA encryption key.
Although the final script may end up executing three processes, the whole process is seamless to the user. You can hide even more of the guts by using expect's log_user setting to hide the output of the programs at points where the user does not need to see them.
You can use a script like this in conjunction with any Unix command-line program. By combining expect with telnet or ssh, you can control non-Unix systems, thereby leveraging the flexibility of Unix into a non-Unix domain. This even works with programs for which you do not have source code, such as control utilities for commercial databases or application software.
In the case of GPG, we do have source code, so we could modify the program, but writing an expect script is easier. A carefully designed expect script may not require changes when a new version of GPG is released. Source code changes to GPG would require integration with any new version of GPG.
7.8.6 See Also