#!/usr/pkg/bin/perl

# small script to show in an intuitive way who signed which of your user ids
#
# Copyright (c) 2004 Uli Martens <uli@youam.net>
# Copyright (c) 2005 Peter Palfrader <peter@palfrader.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=pod

=head1 NAME

gpglist -- show who signed which of your UIDs

=head1 SYNOPSIS

=over

=item B<gpglist> [B<--signer=>I<keyid>] [B<--show-revoked>] I<keyid>

=back

=head1 DESCRIPTION

B<gpglist> takes a keyid and creates a listing showing who signed I<keyid>'s user IDs.

	$ gpglist 6D8ABE71
	+-----  1 Christoph Berg <cb@df7cb.de>
	|  +--  2 Christoph Berg <cb@cs.uni-sb.de>
	1  2  
	x     7929AB90F7AC3AF0 Martin Helas <mhelas@helas.net>
	x  x  29BE5D2268FD549F Martin Michlmayr <tbm@cyrius.com>
	   x  7DDB2B8DB4B462C5 Martin Wanke <mawan@mawan.de>

By default only non-revoked identities are listed, but it can be
overridden it with B<--show-revoked>.

One or more B<--signer> option can be used to limit signers to the
matching keys.  See the GnuPG manual for the different ways to specify a
key or user ID.

The path to the gpg binary can be specified with the I<GNUPGBIN>
environment variable (default: C<gpg>).

=head1 AUTHORS

=over

=item Uli Martens <uli@youam.net>

=item Peter Palfrader <peter@palfrader.org>

=back

=head1 SEE ALSO

gpgsigs(1), gpg(1), caff(1).

=cut

use strict;
use warnings;
use English '-no_match_vars';
use Getopt::Long;

my $now = time;
my $show_revoked;
my @signers;

sub usage() {
	die "Usage: $PROGRAM_NAME [--signer=<keyid>] [--show-revoked] <keyid>\n";
}

sub gpg(@) {
	my @cmd = ($ENV{GNUPGBIN} // 'gpg', qw/--no-auto-check-trustdb --fixed-list-mode --with-colons/, @_);
	my $pid = open my $fh, '-|', @cmd or die "gpg failed: $!";
	return ($fh, $pid);
}

GetOptions("show-revoked" => \$show_revoked, 'signer=s@' => \@signers) or usage();
usage() unless @ARGV and $#ARGV == 0;
my $key = shift @ARGV;

my @signing_keys;
if (@signers) {
    foreach (@signers) {
        if (/^(?:0x)?(\p{AHex}{16})$/) {
            push @signing_keys, uc $1;
        } elsif (/^(?:\p{AHex}{40}|(?:\p{AHex}{4} ){5}(?: \p{AHex}{4}){5})$/) {
            tr/ //d;
            push @signing_keys, uc (substr y/ //dr, -16);
        } else {
            my ($fh, $pid) = gpg(qw/--list-keys --/, $_);
            while (<$fh>) {
                push @signing_keys, $1 if /^pub:(?:[^:]*:){3}([0-9A-F]{16}):/;
            }
            waitpid $pid, 0;
            die "gpg exited with value ".($? >> 8)."\n" if $? > 0;
            close $fh;
        }
    }
}

my ($SIGS) = gpg(qw/--list-options show-sig-subpackets --list-sigs/, $key);

my ($uid, $id) = ('', '');
my (%uids, @uids);
my %sigs;
my %revs;
my %ids;
my $longkey;
while (<$SIGS>) {
	if (/^uid:(?:[^:]*:){6}([0-9A-F]{40}):[^:]*:([^:]+)/) {
		$uid = $1; # use the hash to have proper distinction between UATs
		push @uids, $uid; # preserve the order
		$uids{$uid} = $2;
	}
	elsif (/^sig:(?:[^:]*:){3}([0-9A-F]{16}):(\d+):(\d*):(?:[^:]*:){2}([^:]+):(1[0-3fF])[lx](?::.*)?$/) {
		$id = $1;
		next if $3 ne '' and $3 < $now; # expired
		next if lc $5 eq '1f'; # direct-key signature
		$ids{$id} = $4;
		# keep only the most recent sig (a more recent sig might appear anywhere in the list)
		$sigs{$id}->{$uid} = $2 unless defined $sigs{$id}->{$uid} and
									  ($sigs{$id}->{$uid} < 0 or # non revocable sig
									  $sigs{$id}->{$uid} > $2);
	}
	elsif (/^spk:7:1:1:%00$/) { # non-revocable signature
		$sigs{$id}->{$uid} = -1;
	}
	elsif (/^rev:(?:[^:]*:){3}([0-9A-F]{16}):(\d+):(?:[^:]*:){4}30x(?:,\p{AHex}{2})?(?::.*)?$/) {
		$revs{$1}->{$uid} = $2;
	}
	elsif (/^uat:(?:[^:]*:){6}([0-9A-F]{40}):/) {
		$uid = $1;
		push @uids, $uid; # preserve the order
		$uids{$uid} = "Photo ID"; # XXX [jpeg image of size ...]
	}
	elsif (/^pub:(?:[^:]*:){3}([0-9A-F]{16}):/) {
		$longkey = $1;
	}
	elsif (/^sub:/) {
		last;
	}
	elsif (!/^(?:fpr|tru|rvk|spk):/) {
		print STDERR "hi, i'm a bug. please report me to my owner\n";
		die "input: $_, key: $key";
	}
}
close $SIGS;

sub is_revoked($;$) {
	my $uid = shift;
	my $keyid = shift // $longkey;
	defined $revs{$keyid}->{$uid} and defined $sigs{$keyid}->{$uid} and
	$sigs{$keyid}->{$uid} > 0 and # < 0 means non-revocable
	$revs{$keyid}->{$uid} > $sigs{$keyid}->{$uid}
		? 1 : 0;
}
@uids = grep { !is_revoked($_) } @uids unless $show_revoked;

for ( my $a=0; $a <= $#uids; $a++ ) {
	printf "|  " x $a
	     . "+--"
	     . "---" x ($#uids-$a)
		 . (is_revoked($uids[$a]) ? 'R' : ' ')
	     . "%2i $uids{$uids[$a]}\n", $a+1;
}

for ( my $a=0; $a <= $#uids; $a++ ) {
	printf "%-2i ", $a+1;
}
print "\n";

for my $id (sort {$ids{$a} cmp $ids{$b}} keys %ids) {
	next unless !@signers or grep {$id eq $_} @signing_keys;
	foreach my $uid (@uids) {
		my $x = is_revoked($uid,$id) ? 'R' :
				defined $sigs{$id}->{$uid} ? 'x' :
				' ';
		print $x.'  ';
	}
	print "$id $ids{$id}\n";
}
