package PGP::Certificate;

use strict;
use Carp;

use PGP::Constants qw(/^PKALG/);
use PGP::Certificate::Subkey;
use PGP::Certificate::Userid;
use PGP::Certificate::Signature;

BEGIN {
    no strict 'refs';

    foreach my $accessor (qw(created pkalg modlength fingerprint keyid)) {
	*{$accessor} = sub {
	    my ($this) = @_;
	    $this->{"packet"}->{$accessor};
	};
    }

    foreach my $accessor (qw(packet expires revocation direct_signatures
			     primary_userid secondary_userids subkeys)) {
	*{$accessor} = sub {
	    my ($this) = @_;
	    $this->{$accessor};
	};
    }
};

sub new {
    my ($classname, $packetsref) = @_;

    my $self = bless {}, $classname;

    my ($userid, $subkey, @subrevs);

    for my $packet (@$packetsref) {
	if ($packet->ctag == 6) {
	    $self->{"packet"} = $packet;
	    $self->{"expires"} = $packet->{"expires"};
	} elsif ($packet->ctag == 14) {
	    $subkey = new PGP::Certificate::Subkey($packet);
	    push(@{$self->{"subkeys"}}, $subkey);
	} elsif ($packet->ctag == 13) {
	    $userid = new PGP::Certificate::Userid($packet);
	    push(@{$self->{"secondary_userids"}}, $userid);
	} elsif ($packet->ctag == 2) {
	    my $sig = new PGP::Certificate::Signature($packet);
	    my $sigtype = $sig->sigtype;

	    if ($sigtype == 0x20) {
		# sig is computed over pubkey
		$self->set_revocation($sig);
	    } elsif ($sigtype == 0x1f) {
		$self->add_direct($sig);
	    } elsif (($sigtype >= 0x10) && ($sigtype <= 0x13)) {
		if (! $userid) {
		    push(@{$self->{"misordered_signatures"}}, $sig);
		} else {
		    $userid->add_signature($sig, $self->{"packet"}->keyid);
		}
	    } elsif ($sigtype == 0x28) {
		if (! $subkey) {
		    push(@{$self->{"misordered_signatures"}}, $sig);
		} else {
		    push(@subrevs, $sig);
		}
	    } elsif ($sigtype == 0x18) {
		if (! $subkey) {
		    push(@{$self->{"misordered_signatures"}}, $sig);
		} else {
		    $subkey->set_binding($sig);
		}
	    } elsif (! $userid && ! $subkey) {
		push(@{$self->{"misordered_signatures"}}, $sig);
	    }
	}
    }

    # now handle all the subkey revocations.  This kludge is necessary
    # because the old keyserver would treat the revocation as a
    # signature on the last userid, so I need to compute the
    # revocation hashes to see which revocations really go with which
    # subkeys.

    subrev: for my $subrev (@subrevs) {
	for $subkey (@{$self->subkeys}) {
	    my $check = $subrev->hashmsb16;
	    my $computed = substr($subrev->hash([$self, $subkey]),
				  0,2);
	    if ($check eq $computed) {
		$subkey->set_revocation($subrev);
		next subrev;
	    }
	}

	confess("No matching subkey for revocation");
    }

    # order the direct signatures

    if ($self->{"direct_signatures"}) {
	@{$self->{"direct_signatures"}} = 
	    sort { $b->created <=> $a->created }
	@{$self->{"direct_signatures"}};
    } else {
	$self->{"direct_signatures"} = [];
    }

    # order the userids
    # first, the userids whose self-sigs have the primary_userid
    #  flag set, in order of self-sig creation
    # next, the other userids which have self-sigs, in order of
    #  self-sig creation
    # finally, all other userids in cmp order

    @{$self->{"secondary_userids"}} = 
	sort {
	    (
	     (($b->self_signature?1:0) <=>
	      ($a->self_signature?1:0)) ||
	     (($b->self_signature?1:0) &&
	      ((($b->primary_userid?1:0) <=>
		($a->primary_userid?1:0)) ||
	       ($b->self_signature->created <=>
		$a->self_signature->created))) ||
	     ($a->name cmp
	      $b->name)
	     );
	} @{$self->{"secondary_userids"}};

    $self->{"primary_userid"} = shift(@{$self->{"secondary_userids"}});

    # order the subkeys

    if ($self->{"subkeys"}) {
	@{$self->{"subkeys"}} = 
	    sort { $b->created <=> $a->created } @{$self->{"subkeys"}};
    } else {
	$self->{"subkeys"} = [];
    }

    return($self);
}

sub _pkalg_sign {
    my ($alg) = @_;

    ($alg == PKALG_RSA ||
     $alg == PKALG_RSA_SIG ||
     $alg == PKALG_DSA);
}

sub _pkalg_encrypt {
    my ($alg) = @_;

    ($alg == PKALG_RSA ||
     $alg == PKALG_RSA_ENC ||
     $alg == PKALG_ELGAMAL);
}

sub set_revocation {
    my ($self, $sig) = @_;

    if (!$self->{"revocation"} ||
	$self->{"revocation"}->created < $sig->created) {
	$self->{"revocation"} = $sig;
    }
}

sub add_direct {
    my ($self, $sig) = @_;

    push(@{$self->{"direct_signatures"}}, $sig);

    my $spexpires = $sig->created + $sig->subpacket("key_expires");

    if ($spexpires &&
	(! $self->{"expires"} ||
	 $self->{"expires"} > $spexpires)) {
	$self->{"expires"} = $spexpires;
    }
}

# ================

sub usagestr {
    my ($self) = @_;

    my ($sign, $encrypt);

    # XXX The semantics here are the same as pgp5.  I'm not certain,
    # since the code is spaghetti, and I'm not excited by the
    # semantics anyway.

    $sign += _pkalg_sign($self->pkalg);
    $encrypt += _pkalg_encrypt($self->pkalg);

    if (!$self->expires ||
	$self->expires >= time()) {
	for my $sub (@{$self->subkeys}) {
	    $sign += _pkalg_sign($sub->pkalg);
	    $encrypt += _pkalg_encrypt($sub->pkalg);
	}
    }

    my $ret;

    if ($sign && $encrypt) {
	$ret = "Sign & Encrypt";
    } elsif ($sign) {
	$ret = "Sign only";
    } elsif ($encrypt) {
	$ret = "Encrypt only";
    } else {
	$ret = "";
    }

    return($ret);
}

sub userids {
    my ($self, $sig) = @_;

    [ $self->{"primary_userid"}, @{$self->{"secondary_userids"}} ];
}

sub _verify_sig {
    my ($sig, $dataref) = @_;

    if (! $sig) {
	return(1);
    }

    $sig->verify($dataref);
}

sub verify {
    my ($self) = @_;
    
    my $computed;

    # check if there were any misordered signatures

    my $mocount = ($self->{"misordered_signatures"}?
		   @{$self->{"misordered_signatures"}}:0);

    if ($mocount > 0) {
	warn("Certificate had $mocount misordered signatures\n");
    }

    # verify the revocation

    _verify_sig($self->revocation, [$self]) ||
	warn("revocation signature hash check failed\n");

    # verify the direct_signatures

    for my $dsig (@{$self->direct_signatures}) {
	_verify_sig($dsig, [$self]) ||
	    warn("direct signature hash check failed\n");
    }

    # verify the primary_userid (self_signature, other_signatures)
    
    _verify_sig($self->primary_userid->self_signature,
		[$self, $self->primary_userid]) ||
	warn("primary_userid self_signature hash check failed\n");

    for my $osig (@{$self->primary_userid->other_signatures}) {
	_verify_sig($osig, [$self, $self->primary_userid]) ||
	    warn("primary_userid other_signature (issuer = 0x".
		 $osig->issuertext.
		 ") hash check failed\n");
    }

    # verify the secondary_userids (self_signature, other_signatures)

    for my $suid (@{$self->secondary_userids}) {
	_verify_sig($suid->self_signature, [$self, $suid]) ||
	    warn("secondary_userid \"".
		 $suid->name.
		 "\" self_signature hash check failed\n");

	for my $osig (@{$suid->other_signatures}) {
	    _verify_sig($osig, [$self, $suid]) ||
		warn("secondary_userid \"".
		     $suid->name.
		     "\" other_signature (issuer = 0x".
		     $osig->issuertext.
		     ") hash check failed");
	}
    }

    # verify the subkeys (binding and revocation)

    for my $sk (@{$self->subkeys}) {
	_verify_sig($sk->binding, [$self, $sk]) ||
	    warn("subkey 0x".
		 (uc unpack("x4H8", $sk->keyid)).
		 " binding hash check failed");

	_verify_sig($sk->revocation, [$self, $sk]) ||
	    warn("subkey 0x".
		 (uc unpack("x4H8", $sk->keyid)).
		 " revocation hash check failed");
    }
}

1;
