Mailing List Archive

Request for comments on GnuPG usage
I'm designing a variety of systems that use GnuPG-signed control messages.
These systems will mostly replace SSH-based interactive connections with
SMTP-based non-interactive ones. The goal is to provide secure message
transport between machines when those machines do not necessarily have
reliable full-time internet connectivity with each other.

I have attached a prototype message authenticator at the bottom of
this message. The authenticator reads a file on stdin which contains
an OpenPGP message, authenticates it as described below, then invokes
another process and passes the plaintext (or dearmored text) of the
message through a pipe.

Perl junkies can skip to the end and read the code directly.

My comments are below; please correct any assertion you believe to be
wrong. There's also some notes about workarounds for various GnuPG
behavior.

Here is what my prototype tries to do:

0. Assume that the prototype authenticator has a secret key in
"shell-authorization-ring" which it can use to decrypt messages,
and that it has access to a public key ring which contains at
least the set of public keys that will be accepted as authorized
messages. The latter can be replaced by a public key server.
.gnupg/options is set up for whatever keyservers, RSA extensions,
and other glue may be required.

1. Interpret message on stdin using GnuPG. Temporary files are
used to work around GnuPG pipe handling--I find that when I
use gpg with stdin and stdout as pipes, the data is always
corrupted. Three temporary files are created: the input,
the plaintext output of GnuPG (either decrypted or merely
dearmored), and the status-fd output.

A non-standard secret keyring is used because the secret key
must have no passphrase for this to work, and GnuPG will
spew annoying "Secret key <id> is not protected" warnings
_every_ time it is invoked if there are such keys in the
private keyring. There seems to be no way to turn this off,
at least not in 1.0.1.

2. Examine the --status-fd output to find the fingerprint
of the public key used to generate a valid signature, and the
timestamp of that signature.

3. Check the fingerprint of the public key used in the signature
against a table of fingerprints of authorized signing
keys ($HOME/.mail-shell-authorized in the prototype).
Authorization fails if the fingerprint of the key signing
the message is not listed here.

4. Check the signature timestamp to see if this is a replay of
a previous message ($HOME/.mail-shell-seq in the prototype).
Authorization fails if this is the case.

5. If all has gone well this far, accept the output of gpg as
the authorized message, and act on it (feed it to a shell
in the prototype).

Here are some limitations or risks of the prototype that I know about:

1. I really feel uncomfortable about relying on GnuPG to know
what to do when it is fed arbitrary input without a command
such as "--decrypt" or "--verify". However, I've generated
a number of test messages including secret and public keys,
and gpg doesn't do anything I wouldn't want it to do on any
test case I've tried.

Is 'gpg' without any command intended to always do the most
harmless thing appropriate? Did I miss something?

2. --status-fd is cool. ;-)

I don't use the TRUST_* results from GnuPG. Whoever set
up the message authenticator must appropriately verify the
fingerprint of every public key that will be used to sign
authorized messages.

The "--always-trust" suppresses GnuPG's public key validity
checking, which otherwise causes failures in many cases
(GnuPG tries to prompt for a "really use this key", even in
the presence of --batch and --yes, which causes a failure
when there is no user to prompt).

3. It would be nice if we had exactly one authorized key
fingerprint, and any public key signed by that one true
authorized key (modulo revocations) was also authorized.
I'm not sure how to do this, but it would be cool.
I think I start with

gpg --with-colons --with-fingerprint --fast-list-mode \
--list-sigs 0xKEY-ID-HERE

and do a tree search until I hit a key that I like, or
if I trust GnuPG's trustdb, just simply:

gpg --with-colons --with-fingerprint --list-sigs 0xKEY-ID-HERE

and read the first two lines; the trust value is the second
field of the first line. I assume that GnuPG prevents
user ID's with newlines in them, otherwise key fingerprints
could be spoofed.

OTOH, I like to KISS, and just use my own key-to-authority
mapping mechanism. The key can be used as a user ID in a
system with multiple access modes, which makes validating
the key less useful since the external program that uses the
authenticator must know what privileges are associated with
all authorized signing key fingerprints anyway.

This prototype has a list of fingerprints that it will
accept, and simply rejects any GnuPG status-fd output unless
it contains a VALIDSIG line and one of the desired
fingerprints. It's simple. It's predictable. It's easier
to explain than the trustdb model.

4. The prototype just checks that the timestamp is greater than
any previously received timestamp, which works well in
the case of only one authorized key holder. This can be
trivially extended to store separate last received timestamps
from each authorized signature key.

This does have a significant drawback in that if messages
arrive out of order, the earlier messages will be discarded.
It also has a less significant drawback in that only one
message can be authenticated per second. My hardware can't
generate more than one signed message per second per signing
key, so I don't consider this to be a problem.

Maintaining a database of "SIG_ID"'s could solve this, but
requires storing every SIG_ID ever received for the entire
lifetime of the signing key.

SIG_ID combined with timestamp could be used to put an upper
bound on the storage space used for SIG_ID's. Perhaps the
reply test should be "SIG_ID not yet received and signature
timestamp within last N days" with a policy of expiring
SIG_ID's in the receiver database after 2N days.

5. Hopefully nothing weird happens with clearsigned text
messages, such as a change in line ending format.

I'm assuming that all of the following vulnerabilities are non-problems:

- denial of service: it takes several seconds to process each
incoming message on the P133-class systems I'll be using, so
it would be easy to feed megabytes of junk data in 200-byte
messages to the system and kill it.

- attacks outside of the authentication system: Tempest
monitoring, cracking root on the same machine, stealing the
hardware, storage of decrypted messages, etc.

- evil authorized message signers: of course, if there are
vulnerabilities in the receiver of authenticated messages,
(e.g. the message receiver is the stdin of an unrestricted
Bourne shell with read/write access to the .gnupg directory
;-), these vulnerabilities could be exploited by authorized
message signers. My concern ends when I am assured that
those vulnerabilities cannot be exploited by _un_authorized
message signers.

- there is no management facility: it would be nice, but
not essential, if it could handle importing keys and revocation
certificates, especially if combined with the "all authorized
keys are signed by the one true key" authentication mechanism;
however, for small-scale operations, it's easier to maintain
a list of fingerprints outside of GnuPG.

GnuPG prevents forgery, modification, and (if encryption is used)
disclosure of messages. The signature database in the authenticator
prevents attacks based on replaying previous valid messages.
The fingerprint database in the authenticator provides trust and
authorization data.

That's all the attacks I can think of.

Am I missing anything?

Prototype perl script follows:

#!/usr/bin/perl -w
use strict;

print STDERR '$Id: mail-shell,v 1.1 2000/02/21 14:42:20 cvs Exp $', "\n";

# Set up work area

my $tmp = "/tmp/ms-$$";
my $pid = $$;
mkdir($tmp, 0700) or die "mkdir: $tmp: $!";

# Destroy work area when we're done with it.

my $badness = 1;

END {
$pid == $$ && system('rm', '-rf', $tmp);

# Perl has strange ideas about what the exit status of the
# process should be if there are END blocks.
# This makes perl think the code is buggy, which is good enough
# for propagating a non-zero exit status to the caller,
# but generates an annoying warning message.

exit($badness);
};

# Capture incoming message data.

system("cat > $tmp/in") and die "cat: exit status $?";

# Analyze data with gpg.

my $rv = system("gpg \\
--batch \\
--yes \\
--secret-keyring shell-authorization-ring \\
--status-fd 3 \\
--output $tmp/out \\
$tmp/in 3> $tmp/status");

# If gpg doesn't like it, then we don't either.

if ($rv) {
die "Message decryption/verify failed.";
exit(1);
}

# OK, so GPG has analyzed it. Now we look at the results of that analysis.

open(STATUS, "<$tmp/status") or die "No status information from gpg: $!";

my ($signature, $sig_date, $sig_time);

while (<STATUS>) {
($signature, $sig_date, $sig_time) = ($1, $2, $3) if m/^\[GNUPG:\] VALIDSIG ([A-Fa-f\d]+) (\d\d\d\d-\d\d-\d\d) (\d+)\r?\n$/os;
}

close(STATUS) or die "Error reading status information from ggp: $!";

die "No valid signature detected in gpg status information" unless defined($sig_time);

print STDERR "Found signature data: $signature, $sig_date, $sig_time\n";

# OK, so GPG has verified a signature. Now we check to see if we
# trust the owner of the signature's key.

open(AUTHORIZED_KEYS, "<$ENV{HOME}/.mail-shell-authorized") or die "No authorized key fingerprints list: $!";
my $authorized = 0;
while (<AUTHORIZED_KEYS>) {
next if /^\s*#/os;
s/\s+//gos;
s/[^A-Fa-f0-9]+/ /gos;
s/.* //gos;
next unless length($_) >= 32;
$authorized = 1 if $_ eq $signature;
}
close(AUTHORIZED_KEYS) or die "Error reading authorized keys file: $!";

die "Signature valid, but not authorized" unless $authorized;

# At this point we are sure that the message is signed, and we like the
# person whose key it was signed with.

# Trivial protection against repeated message attacks.
# Yes, this does mean you can send at most one message per second,
# and they must appear in order. Darn.

open(SEQUENCE, "<$ENV{HOME}/.mail-shell-seq")
or die "You must set the message sequence number, e.g. by 'echo 1 > \$HOME/.mail-shell-seq'";

my $sequence = <SEQUENCE>;
$sequence += 0;

die "Zero or negative sequence number is probably an error and therefore not allowed."
unless $sequence > 0;

close(SEQUENCE);

die "Message sequence number repeated (got $sig_time, last message was $sequence)\n"
if $sig_time <= $sequence;

open(SEQUENCE, ">$ENV{HOME}/.mail-shell-seq.tmp.$$") or die "open new sequence number: $!";
print SEQUENCE $sig_time or die "write new sequence number: $!";
close(SEQUENCE) or die "close new sequence number: $!";
rename("$ENV{HOME}/.mail-shell-seq.tmp.$$", "$ENV{HOME}/.mail-shell-seq") or die "rename: new to old sequence number: $!";

# OK, now we are sure that this is an original message, not a repeat.

system('sh', '-x', "$tmp/out") and die "Command returned exit status $?";
$badness = 0;
exit(0);


--
OpenPGP email preferred at <zblaxell@feedme.hungrycats.org>.
OpenPGP key available on www.keyserver.net and other fine keyservers.
OpenPGP fingerprint: 2B32 546D 21A5 0DB2 20C8 AF10 1D4A 610E 6972 2DEE