#! /usr/local/bin/perl
use LWP::UserAgent;
use HTTP::Request;
use Getopt::Long;

#
# parse options and configuration

$tempfile="/tmp/getcert.$$.temp";
$openssl="openssl"; # just use default path
$polname="ca-signing-policy.conf";
$opt_c="certs.cf";
@optdef=qw( v V d|certdir=s reloadcerts=s@ c|cf|config=s now h|help );
$0 =~ s/.*\///;
$|=1;
$Getopt::Long::ignorecase=0;
&GetOptions(@optdef);
$opt_V and $opt_v++;
$opt_h and &help($0);
$opt_d or $opt_d=".";
-f $opt_c or die "Cannot find configuration $opt_c\n";
$config=`cat $opt_c`;
$rc=eval $config;
( "$@" eq "" ) or die "Invalid configuration file $opt_c: $@";

# check target directory
-d "$opt_d" or die "$0: $opt_d does not exist\n";
 
# implement the random wait
$opt_now or do {
  $sleeptime=60*rand(10);
  sleep($sleeptime);
};

# overview of procedure: 
# - put in arbitrary wait upto 10 minutes
# - test validity on a per-ca basis and add the status field
#   0) if this ca is in reloadcerts list, load CA cert and store
#      in this case be verybose
#   1) is current cert equal to repository? If not, warn and
#      ignore a CRL from that repository
#   2) reload the CRL from the repository if possible
#      if it is valid acc. to CA cert, replace current CRL
#   3) check expiration of CRL. Warn and remove from list if expired
# - then, loop over all ca's that have no "dis" set and are not
#   invalidated by previous loop. Write out the ca-signing-policy
#

$casp="# ca-signing-policy.conf\n#\n";
$casp.="# generated by get_certificates on ".`date`."#\n#\n";

foreach $caname ( keys %calist ) {
  %ca=%{$calist{$caname}};

  $ca{crl} and $ca{loadcrl}=1;

  $opt_v and print "\nProcessing CA $caname\n";
  $opt_V and print "  CERT URL $ca{cert}\n";
  $opt_V and print "  CRL  URL $ca{crl}\n";
  $opt_V and do { foreach $dom ( @{$ca{auth}} ) { print "  CA  AUTH $dom\n" } };
  $opt_V and print "  CA STATU $ca{dis}\n";
  $opt_V and print "\n";

  isin_str($caname,@opt_reloadcerts) and do {
    if ( $ca{cert} ) {
      $cacert=&readurl($ca{cert},$ca{cert_filter});
      $cacert and &writeca($caname,"cert",$cacert);
    } else { warn "    CA $caname cannot be reloaded (no URL)\n"; }
  };

  $ca{cert} and do { # check for cert validity
    $cert=&readpem($caname,"cert");
    if ( $cert ) {
      $currfp=(&certinfo($cert))[2];
      $webdata=&readurl($ca{cert});
      $webfp=(&certinfo($webdata))[2];
      $opt_V and print "  CURR FP $currfp\n  WEB  FP $webfp\n";
      $webfp eq $currfp or do {
        warn "  FPCHECK $caname: inconsistent fingerprint\n".
             "    URL  $ca{cert}\n    CURRFP $currfp\n    WEB FP $webfp\n";
        $ca{loadcrl}=0;
      };
    } else {
      warn "  FPCHECK $caname: cert not found\n";
      $ca{dis}="CA cert not there";
    }
  };

  grep { 
    /Certificate has expired/ and  do {
      $ca{dis}="Certificate has expired";
      $ca{loadcrl}=0;
      warn "  CERT EXPIRED $caname\n"; 
    };
  } `$openssl verify $opt_d/cacert-$caname.pem 2>&1 `;


  $ca{loadcrl} and do {
    $opt_V and print "  CRL LOAD=$ca{loadcrl} FROM $ca{crl}\n";
    $cacrl=&readurl($ca{crl},$ca{crl_filter});
    if ( $cacrl ) {
      ($nextupd=&crlvalid($caname,$cacrl)) and &writeca($caname,"crl",$cacrl);
      $opt_v and print "  RELOAD CRL $ca{crl}\n  NEXT UPD $nextupd\n";
    } else {
      warn "  FAILED $ca{crl}\n";
    }
  };


  if ( ! $ca{dis} ) {
    ($myhash,$mysub,$myfp,$myiss,$myser)=&certinfo(&readpem($caname,"cert"));
    $opt_V and print "  CA HASH $myhash\n";
    $opt_V and print "  CA SUBJ $mysub\n";
    $opt_V and print "  CA MD5F $myfp\n";
    $opt_V and print "  CA ISSU $myiss\n";
    $opt_V and print "  CA SERL $myser\n";

    $opt_v and print "  ADDED to ca-signing-policy\n";

    # also write out per-ca signing policy for GT2
    if ( open PERCASP,">$opt_d/$myhash.signing_policy" ) {
        print PERCASP "# EACL for $myhash (symname: $caname)\n";
	print PERCASP " access_id_CA   X509    '$mysub'\n";
	print PERCASP " pos_rights     globus  CA:sign\n";
	print PERCASP " cond_subjects  globus  '\"$mysub\" ";
	foreach ( @{$ca{auth}} ) { print PERCASP "\"$_\" "; }
	print PERCASP "'\n\n";
	close PERCASP;
    } else {
        warn "$0: writing $opt_d/$myhash.signing_policy:\n  $!\n";
    }
    # end of hack

    $casp.="# EACL for $myhash (symname: $caname)\n";
    $casp.=" access_id_CA   X509    '$mysub'\n";
    $casp.=" pos_rights     globus  CA:sign\n";
    $casp.=" cond_subjects  globus  '\"$mysub\" ";
    foreach ( @{$ca{auth}} ) { $casp.="\"$_\" "; }
    $casp.="'\n\n";

    &hashlink($caname,$myhash);
  } else {
    $opt_v and print "  DISTRST $ca{dis}\n";
  }
}

if ( open CASP,">$opt_d/ca-signing-policy.conf" ) {
  print CASP $casp;
  close CASP;
} else {
  warn "$0: writing $opt_d/ca-signing-policy.conf:\n  $!\n";
}

exit(0);















sub readurl {
  my ($url,$filter) = @_;

  my $ua = LWP::UserAgent->new;
  my $req = HTTP::Request->new('GET', $url);
  my $resp = $ua->request($req);
  
  unless ($resp->is_success()) {
    $!="readurl($url): $!";
    return undef;
  }
  $data=$resp->content;
  $filter and do {
    open DER,">$tempfile" or die "$0::readurl($url,$filter): cannot open temp\n  $!\n";
    print DER $resp->content;
    close DER;
    $filter=~s/^\|//;
    open OPENSSL,"$filter -in $tempfile |" or die "$0::readurl($url,$filter): cannot open $filter pipe:\n  $!\n";
    $data="";
    while (<OPENSSL>) {
	  $data.=$_;
    }
    close OPENSSL;
  };

  return $data;
}

sub readpem {
  my ($caname,$type) = @_;
  my ($fname)="$opt_d/ca$type-$caname.pem";
  my ($txt);
  
  ( $type ne "cert" ) and ( $type ne "crl" ) and 
    die "$0::writeca: invalid argument $type\n";

  open PEM,$fname or do {
    warn "$0::readpem($fname): opening error:\n  $!\n";
    return undef
  };
  while (<PEM>) { $txt.=$_; };
  close PEM;
  return $txt;
}


sub writeca {
  my ($caname,$type,$data) = @_;
  my ($fname)="$opt_d/ca$type-$caname.pem";

  ( $type ne "cert" ) and ( $type ne "crl" ) and 
    die "$0::writeca: invalid argument $type\n";
  $data or do {
    warn "$0::writeca($caname): $type contains no data\n";
    return undef;
  };
  &isa_pem($type,$data) or do {
    warn "$0::writeca($caname): data not of type $type\n";
    $opt_V and print "-- DATA:\n$data\n-- END OF DATA\n";
    return undef;
  };


  -f "$fname" and do {
    unlink "$fname.old";
    rename "$fname","$fname.old" or do {
      warn "$0::writeca($caname,$type): cannot make backup in $opt_d:\n  $!\n";
      return undef;
    };
  };
      
  open PEMDATA,">$fname" or do {
    warn "$0::cawrite($caname): writing $fname:\n  $!\n";
    return undef;
  };
  print PEMDATA $data;
  close PEMDATA;

  $opt_V and print "  Written $type for $caname\n";
  return 1;
}

sub isin_str {
  my ($needle,@haystack) = @_;

  foreach $hay ( @haystack ) {
    $needle =~ /$hay/ and return 1;
  }
  return 0;
}

sub isa_pem {
  my ($type,$data) = @_;
  @lines=split(/\n/,$data);
  foreach $l ( @lines ) {
    $type eq "cert" and $l =~ /-----BEGIN CERTIFICATE-----/ and return 1;
    $type eq "crl" and $l =~ /-----BEGIN X509 CRL-----/ and return 1;
  }
  return undef;
}

sub certinfo {
  my ($cert) = @_;
  my ($key,$val);
  my ($myhash,$myfp,$mysubj,$myiss,$myser);

  open MKINFO,
  "|$openssl x509 -noout -hash -fingerprint -subject -issuer -serial >$tempfile"
	or do {
    warn "$0:certinfo(): cannot open $openssl or $tempfile:\n  $!\n";
    return undef;
  };
  print MKINFO $cert;
  close MKINFO;

  open MKINFO,"$tempfile" or do {
    warn "$0:certinfo(): cannot open $tempfile:\n  $!\n";
    return undef;
  };
  while (<MKINFO>) {
    chomp($_);
    $key="";
    ($key,$val)=/^([^=]*)=\s*(.*)$/;
    $opt_V and print "  CERT INFO read $_\n";
    length==8 and $myhash=$_;
    ( $key eq "MD5 Fingerprint" ) and $myfp=$val;
    ( $key eq "subject" ) and $mysubj=$val;
    ( $key eq "issuer" ) and $myiss=$val;
    ( $key eq "serial" ) and $myser=$val;
  }
  close MKINFO;

  return ($myhash,$mysubj,$myfp,$myiss,$myser);
}

sub crlvalid {
  my ($caname,$crldata) = @_;
  my ($cafname)="$opt_d/cacert-$caname.pem";
  my ($ok,$nextupd);

  open OPENSSL,"|$openssl crl -noout -CAfile $cafname -nextupdate > $tempfile 2>&1"
    or do {
    warn "$0::crlvalid($caname,...): cannot run $openssl:\n  $!\n";
    return 0;
  };

  print OPENSSL $crldata;
  close OPENSSL;

  open OPENSSL,"$tempfile" or do {
    warn "$0::crlvalid($caname,...): cannot open output tempfile:\n  $!\n";
    return 0;
  };

  $ok=0;
  while (<OPENSSL>) {
    /verify OK/ and $ok=1;
    /nextUpdate/ and ($key,$nextupd)=split(/=/,$_,2);
  } 
  close OPENSSL;
  chomp($nextupd);
  $ok and return $nextupd;

  return undef;
}

sub hashlink {
  my ($caname,$cahash)=@_;

  ( -f "$opt_d/$cahash.0" and ! -s "$opt_d/$cahash.0" ) and do {
    warn "  HASH ref to CERT $caname is not a symlink\n"; return undef; 
  };
  ( -f "$opt_d/$cahash.r0" and ! -s "$opt_d/$cahash.r0" ) and do {
    warn "  HASH ref to CRL $caname is not a symlink\n"; return undef; 
  };

  unlink "$opt_d/$cahash.0","$opt_d/$cahash.r0";
  -f "$opt_d/cacert-$caname.pem" and do {
    symlink "cacert-$caname.pem","$opt_d/$cahash.0" or 
    warn "$0::hashlink:symlink(cacert-$caname.pem,$opt_d/$cahash.0 failed:\n  $!\n";
  };
  -f "$opt_d/cacrl-$caname.pem" and do {
    symlink "cacrl-$caname.pem","$opt_d/$cahash.r0" or 
    warn "$0::hashlink:symlink(cacrl-$caname.pem,$opt_d/$cahash.0 failed:\n  $!\n";
  };

  return 1;
}

sub help {
  my ($name) = @_;

  print <<EOD;

Usage: $name [-v] [-V] [-h] [--now] [-c config] [-d certdir]
    [--reloadcerts caname_regex]

Update or initialize an OpenSSL CA certificates repository by 
retrieving CRLs and CA certs accoring to the specified config file.
Will write a ca-signing-policy.conf file conforming to Globus 1.1.3
GSI standards. Integrity of CA certs and CRLs retrieved will be verified
and warning issues on expired certs and CRLs.

  -v -V          be verbose or verybose
  -h             gave this help
  --now          perform the requested action now (will 
                 wait upto 10min by default)
  -c <config>    use <config> as a list of CAs to process. See certs.cf(5)
                 for details
  -d <certdir>   repository of CA certificates and location 
                 of ca-signing-policy.conf
  --reloadcerts <regex>
                 list of ca symbolic names of which the CA certs is to
                 be (re)loaded from the web repository

EOD

$opt_v and print <<EOD;

certs.cf(5) format description
------------------------------
For every CA, a symbolic abbrev should be defined, and in the CWD
there should be a self-signed CERT named "cacert-${caname}.pem"
if the CRL URL is correct, it will be retreived automatically,
otherwise, put the file by hand in this directory as "cacrl-${caname}.pem".
the hash is compuled automatically and symlinks to cert and crl made
accordingly. The "hash.0" and "hash.r0" MUST be symlinks.

The "auth" entry in the hash is compulsory: it should be a list
of DN prefixes signed by that CA, inclusing any wildcards.

The "cert" entry is NOT used: it would be catastrophic to retreive
a CA cert insecurely and at random. Use of the "cert" field is
to be forbidden for automatic updates!

if the "dis" entry is defined, the CA will not be added to the
ca-signing-policy.conf file. The textual value of the "dis" entry
will be echoed to the screen.

The list of CA's, in perl syntax as a hash of hashes:

%calist = (
        "cern" => {
          cert  => 'http://globus.home.cern.ch/globus/ca/c35c1972.0',
          crl   => 'http://globus.home.cern.ch/globus/ca/cern.crl.pem',
          auth  => ["/C=ch/O=CERN/*","/C=CH/O=CERN/*",
                        "/O=Grid/O=CERN/*","/O=CERN/O=Grid/"]
          },
        "nikhef-ms" => {
          cert  => 'http://certificate.nikhef.nl/medium/cacert.pem',
          crl   => 'http://certificate.nikhef.nl/medium/cacrl.pem',
          auth  => ["/O=dutchgrid/O=users/*","/O=dutchgrid/O=hosts/*"]
          },
        "nikhef-test-low" => {
          cert  => 'http://certificate.nikhef.nl/test-low/cacert.pem',
          crl   => 'http://certificate.nikhef.nl/test-low/cacrl.pem',
          auth  => ["/C=nl/O=nikhef/*","/C=nl/O=UvA/OU=wins/*",
                        "/C=nl/O=amolf/*","/O=dutchgrid/*"]
          },
        "infn-2" => {
          crl   => 'http://security.fi.infn.it/CA/crl.crl',
          crl_filter    => "$openssl crl -inform der -text",
          auth  => ["/C=IT/O=INFN/*"]
        },
        "globus" => {
          dis   => "distrusted due to local NIKHEF policy",
          auth  => ["/C=us/O=Globus/*","/C=US/O=Globus/*","/O=Grid/O=Globus/*"]
        }
);

EOD

  exit(0);
}

