Commit 99c1507e authored by Leigh B Stoller's avatar Leigh B Stoller

Reorganize the protogeni installation code.

* Split all of the certificate stuff out of initsite into initcerts so
  that it can be run independently, and when updating the IP/domain of
  a site.

* Redo initsite in terms of libinstall. Fully automated now, no user
  intervention needed.

* Regarding above statement, the new site no longer has to email the
  new CA certificate to us; a new web page is exported from the
  clearing house website that allows a new CA to be "provisionally"
  accepted; the new CA will be allowed to register their new protogeni
  certificates, but otherwise will have no access to anything else
  until someone at the ClearingHouse moves them from the unapproved to
  the approved column. 

* New script called "cacontrol" that should be used from now on to
  manage the CA certificates. Also called from the web interface to
  provisionally install a new CA certificate into an "unapproved"
  bundle that is not distributed to other protogeni sites. Otherwise,
  cacontrol should be used as follows:

	boss$ perl cacontrol -h
	Usage: cacontrol [-a] [-n] [-d] <certfile>
	       cacontrol [-n] [-d] -c <commonname>
	       cacontrol [-n] [-d] -r <commonname>
	Options
	  -n     - Impotent mode; do not do anything for real
	  -d     - Turn on debugging.
	  -a     - Add certificate to approved list instead.
	  -c     - Move certificate (commonname) to approved list.
	  -r     - Remove certificate with given commonname.

  In the first form, add a new CA certificate to the unapproved list
  (this is the entrypoint used by the web page mentioned above). If
  you add the -a option, it goes right into the approved bundle
  (approved means it goes into the xmlsec directory and is exported to
  other sites).

  The second form is used to move a CA from the unapproved column to
  the approved colum.

  The third form is used to delete a CA certificate.

  NO MORE HAND EDITING OF THE FILES!
parent 82e1d812
#
#
#
use strict;
use libinstall;
my $CACONTROL = "$TBROOT/sbin/protogeni/cacontrol";
sub InstallUpdate($$)
{
my ($version, $phase) = @_;
#
# Need to run this after the protogeni SQL update and install
#
if ($phase eq "post") {
Phase "cacontrol", "Initializing protogeni ca table", sub {
ExecQuietFatal("$CACONTROL -i");
};
}
return 0;
}
1;
# Local Variables:
# mode:perl
# End:
#
# GENIPUBLIC-COPYRIGHT
# Copyright (c) 2008-2011 University of Utah and the Flux Group.
# Copyright (c) 2008-2012 University of Utah and the Flux Group.
# All rights reserved.
#
......@@ -14,13 +14,13 @@ include $(OBJDIR)/Makeconf
SBIN_STUFF = cleanupslice
PSBIN_STUFF = register_resources expire_daemon gencrl postcrl \
initsite addauthority getcacerts \
addauthority getcacerts \
gencrlbundle shutdownslice remauthority listusage \
update reregister cleanupticket listhistory \
register_sliver sa_daemon genadmincredential \
getchcredential genallow_extcred advt-merge.py \
reservevlans delgeniuser delegatecredential \
updatecert fixcerts
updatecert fixcerts initcerts cacontrol webcacontrol
ifeq ($(ISCLEARINGHOUSE),1)
PSBIN_STUFF += ch_daemon gencabundle
......@@ -35,20 +35,28 @@ SETUID_LIBX_SCRIPTS =
# Force dependencies on the scripts so that they will be rerun through
# configure if the .in file is changed.
#
all: $(SBIN_STUFF) $(PSBIN_STUFF) resolve resolvenode resolve-ch
all: $(SBIN_STUFF) $(PSBIN_STUFF) initsite resolve resolvenode resolve-ch
include $(TESTBED_SRCDIR)/GNUmakerules
install: $(addprefix $(INSTALL_SBINDIR)/, $(SBIN_STUFF)) \
$(addprefix $(INSTALL_SBINDIR)/protogeni/, $(PSBIN_STUFF))
$(addprefix $(INSTALL_SBINDIR)/protogeni/, $(PSBIN_STUFF)) \
$(INSTALL_LIBEXECDIR)/webcacontrol
control-install:
clean:
rm -f *.o core *.pl *.pm *.py $(SBIN_STUFF) $(PSBIN_STUFF)
$(INSTALL_SBINDIR)/protogeni/gencabundle: gencabundle
echo "Installing (setuid) $<"
-mkdir -p $(INSTALL_SBINDIR)
$(SUDO) $(INSTALL) -o root -m 4755 $< $@
$(INSTALL_SBINDIR)/protogeni/%: %
@echo "Installing $<"
-mkdir -p $(INSTALL_SBINDIR)/protogeni
$(INSTALL) $< $@
#!/usr/bin/perl -w
#
# GENIPUBLIC-COPYRIGHT
# Copyright (c) 2008-2012 University of Utah and the Flux Group.
# All rights reserved.
#
use strict;
use English;
use Data::Dumper;
use Getopt::Std;
use POSIX;
#
# Add a root CA.
#
sub usage()
{
print STDERR "Usage: cacontrol [-a] [-n] [-d] <certfile>\n";
print STDERR " cacontrol [-n] [-d] -c <commonname>\n";
print STDERR " cacontrol [-n] [-d] -r <commonname>\n";
print STDERR "Options\n";
print STDERR " -n - Impotent mode; do not do anything for real\n";
print STDERR " -d - Turn on debugging.\n";
print STDERR " -a - Add certificate to approved list instead.\n";
print STDERR " -c - Move certificate (commonname) to approved list.\n";
print STDERR " -r - Remove certificate with given commonname.\n";
exit(1);
}
my $optlist = "dnawcri";
my $fromweb = 0;
my $approve = 0;
my $remove = 0;
my $impotent = 0;
my $debug = 0;
my $commonname;
my $certfile;
my $certificate;
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $PGENISUPPORT = @PROTOGENI_SUPPORT@;
my $ISCLRHOUSE = @PROTOGENI_ISCLEARINGHOUSE@;
my $MAINBUNDLE = "$TB/etc/genica.bundle";
my $TEMPBUNDLE = "$TB/etc/unapproved.bundle";
my $CERTDIR = "$TB/etc/genicacerts";
my $GENCABUNDLE = "$TB/sbin/protogeni/gencabundle";
# un-taint path
$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin:/usr/site/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
# Protos
sub fatal($);
sub ReadBundle($);
sub Regenerate($$);
sub AddCertificate($$);
sub Initialize();
# Flag to regen the unapproved bundle
my $regen = 0;
# Flag to restart apache.
my $restart = 0;
#
# Turn off line buffering on output
#
$| = 1;
# We always use the CH db.
use vars qw($GENI_DBNAME);
$GENI_DBNAME = "geni-ch";
# Now we can load the libraries after setting the proper DB.
use lib '@prefix@/lib';
use libaudit;
require GeniDB;
import GeniDB;
require GeniCertificate;
require GeniAuthority;
#
# Check args.
#
my %options = ();
if (! getopts($optlist, \%options)) {
usage();
}
if (defined($options{"i"})) {
exit(Initialize());
}
if (defined($options{"a"})) {
$approve = 1;
}
if (defined($options{"n"})) {
$impotent = 1;
}
if (defined($options{"d"})) {
$debug = 1;
}
if (defined($options{"w"})) {
$fromweb = 1;
# Force from web.
$approve = 0;
}
usage()
if (@ARGV != 1);
if (defined($options{"c"})) {
$commonname = $ARGV[0];
$approve = 1;
}
elsif (defined($options{"r"})) {
$commonname = $ARGV[0];
$remove = 1;
}
else {
$certfile = $ARGV[0];
}
exit(0)
if (!$PGENISUPPORT);
if ($UID && !($fromweb || $impotent)) {
fatal("Must be root to run this script");
}
# Record output in case of error.
LogStart(0, undef, LIBAUDIT_LOGTBOPS())
if ($fromweb);
# Read the current unapproved list.
my %unapproved_certs = ReadBundle($TEMPBUNDLE);
#
# Grab all of the approved certs from the directory.
#
opendir(DIR, $CERTDIR) ||
fatal("cannot opendir $CERTDIR: $!");
my @pems = grep { /\.pem$/ && -f "$CERTDIR/$_" } readdir(DIR);
closedir(DIR);
my %approved_certs = ();
foreach my $pem (@pems) {
my $pemcert = GeniCertificate->LoadFromFile("$CERTDIR/$pem");
if (!defined($pemcert)) {
fatal("Could not get certificate from $CERTDIR/$pem");
}
print "Approved: " . $pemcert->DN() . "\n"
if ($debug > 1);
$approved_certs{$pemcert->DN()} = $pemcert;
}
#
# Deal with removing first. Find the matching common name in either
# of the lists.
#
if ($remove) {
#
# Search the unapproved bundle for one matching commonname
#
foreach my $cert (values(%unapproved_certs)) {
if ($cert->CommonName() eq $commonname) {
delete($unapproved_certs{$cert->DN()});
print "Deleting from unapproved list: " . $cert->DN() . "\n";
Regenerate(1, 1);
exit(0);
}
}
#
# Search the approved certificates for one matching commonname.
# Once we find it, delete from the DB. This is how the CH knows
# that a CA should be be trusted to do more then just register
# their certificates.
#
foreach my $cert (values(%approved_certs)) {
if ($cert->CommonName() eq $commonname) {
delete($approved_certs{$cert->DN()});
#
# Need to delete the pem file.
#
print "Deleting from approved: " . $cert->certfile() . "\n";
if (!$impotent) {
my $certfile = $cert->certfile();
my $hash = $cert->SubjectHash();
DBQueryWarn("delete from geni_cas where hash='$hash'")
or fatal("Could not delete $commonname from DB");
system("/bin/mv -f ${certfile} ${certfile}.del") == 0
or fatal("Could not remove ${certfile}: $!");
}
Regenerate(0, 1);
}
}
fatal("No matching certificate in either list");
}
#
# Adding by certfile so load it.
#
if (defined($certfile)) {
$certificate = GeniCertificate->LoadFromFile($certfile);
if (!defined($certificate)) {
fatal("Could not get certificate from $certfile");
}
print STDERR $certificate->asText()
if ($fromweb);
}
else {
#
# If given a commonname, need to find it in one of the lists
#
foreach my $cert (values(%unapproved_certs)) {
print "Unapproved: " . $cert->DN() . "\n"
if ($debug > 1);
if ($cert->CommonName() eq $commonname) {
$certificate = $cert;
last;
}
}
# Not found yet, check approved certs.
if (!defined($certificate)) {
foreach my $cert (values(%approved_certs)) {
print "Approved: " . $cert->DN() . "\n"
if ($debug > 1);
if ($cert->CommonName() eq $commonname) {
$certificate = $cert;
last;
}
}
}
fatal("Could not find matching commonname in the bundles.")
if (!defined($certificate));
}
#
# If given a certfile, we are adding a new one and so there should not
# be already be one in the unapproved or the approved list.
#
if ($certfile) {
#
# Already in the approved list? Do nothing.
#
if (exists($approved_certs{$certificate->DN()})) {
my $existing = $approved_certs{$certificate->DN()};
# Identical.
if ($existing->cert() eq $certificate->cert()) {
print("Certificate already in the approved bundle\n");
exit(0);
}
# Same CN, but different certificate. Need admin intervention.
fatal("Certificate with the same DN is in the approved bundle");
}
#
# Now check the unapproved bundle.
#
if (exists($unapproved_certs{$certificate->DN()})) {
my $existing = $unapproved_certs{$certificate->DN()};
# Identical.
if ($existing->cert() eq $certificate->cert()) {
print("Certificate already in the unapproved bundle\n");
exit(0);
}
# Same CN, but different certificate. Need admin intervention.
fatal("Certificate with the same DN is in the unapproved bundle");
}
#
# Okay, add to either unapproved or approved list and regen.
#
exit(AddCertificate($certificate, $approve));
}
# Only way to get here is if $approve is true
fatal("Out of whack")
if (!$approve);
#
# Last case; moving a certificate from the unapproved column to the
# approved column. Sanity check to make sure no duplicates in the
# approved list.
#
if (exists($approved_certs{$certificate->DN()})) {
my $existing = $approved_certs{$certificate->DN()};
# Identical.
if ($existing->cert() eq $certificate->cert()) {
print("Certificate already in the approved bundle\n");
exit(0);
}
# Same CN, but different certificate. Need admin intervention.
fatal("Certificate with the same DN is in the approved bundle");
}
# And better exist in unapproved of course.
fatal("Not an unapproved certificate!")
if (!exists($unapproved_certs{$certificate->DN()}));
exit(AddCertificate($certificate, 1));
#
# Approve a certificate. Delete from unapproved too.
#
sub AddCertificate($$)
{
my ($certificate, $approve) = @_;
if ($approve) {
#
# Drop the certificate into a new file in the genicerts directory.
# Being in this directory "approves" CA cert.
#
my $hash = $certificate->SubjectHash();
my $pemfile = "$CERTDIR/${hash}.pem";
if (-e $pemfile) {
fatal("$pemfile already exists!");
}
print "Creating $pemfile for " . $certificate->DN() . "\n";
if (!$impotent) {
open(PEM, "> $pemfile") or
fatal("cannot open $pemfile for writing: $!");
print PEM $certificate->asText();
close(PEM);
chmod(0644, $pemfile)
or fatal("Could not chmod $pemfile: $!");
}
$restart = 1;
#
# Delete from unapproved and regen the file.
#
if (exists($unapproved_certs{$certificate->DN()})) {
delete($unapproved_certs{$certificate->DN()});
print "Deleting from unapproved list: " . $certificate->DN() . "\n";
$regen = 1;
}
#
# Update DB, which tells the CH the certificate is approved and is
# allowed to do more then just register.
#
if (!$impotent) {
my $safe_cert = DBQuoteSpecial($certificate->cert());
my $safe_dn = $certificate->DN();
$safe_dn =~ s/^subject=\s*//i;
$safe_dn = DBQuoteSpecial($safe_dn);
DBQueryWarn("replace into geni_cas set ".
" hash='$hash', created=now(), ".
" cert=$safe_cert, DN=$safe_dn")
or fatal("Could not update DB");
}
}
else {
#
# Tack onto the unapproved list and create the file again.
#
print "Adding to unapproved bundle: " . $certificate->DN() . "\n";
$unapproved_certs{$certificate->DN()} = $certificate;
$regen = 1;
}
Regenerate($regen, $restart);
return 0;
}
#
# Regen the unapproved bundle and/or the CA bundle.
#
sub Regenerate($$)
{
my ($unapproved, $approved) = @_;
#
# Regenerate the unapproved list if it changed.
#
if ($unapproved) {
open(BUN, "> ${TEMPBUNDLE}.new") or
fatal("cannot opendir ${TEMPBUNDLE}.new for writing: $!");
foreach my $cert (values(%unapproved_certs)) {
print BUN $cert->asText();
}
close(BUN);
if (!$impotent) {
if (-e $TEMPBUNDLE) {
system("/bin/cp -f $TEMPBUNDLE $TEMPBUNDLE.$$") == 0
or fatal("Could not backup $TEMPBUNDLE: $!");
}
system("/bin/mv -f ${TEMPBUNDLE}.new $TEMPBUNDLE") == 0
or fatal("Could not mv ${TEMPBUNDLE}.new into place: $!");
if (-e "${TEMPBUNDLE}.$$") {
system("/bin/mv -f ${TEMPBUNDLE}.$$ ${TEMPBUNDLE}.old") == 0
or fatal("Could not save old $TEMPBUNDLE.$$: $!");
}
}
}
#
# And generate a new bundle for apache. This restarts apache if needed.
# The problem is that we are here from a web request, so we need
# to detach and let the caller get back an okay exit status.
#
if (!$impotent && ($unapproved || $approved)) {
if ($fromweb) {
# Send any output to this point.
LogEnd();
# Yack. apache does not close fds before the exec, so parent
# is not actually detached.
no warnings;
for (my $i = 3; $i < 128; $i++) {
POSIX::close($i);
}
# Now audit again, but in daemon mode.
if (AuditStart(1, undef, LIBAUDIT_LOGTBOPS())) {
# Child starts.
sleep(1);
# Parent returns to web interface.
exit(0);
}
# Make sure the parent gets a chance to return before we
# kill apache. Not good for the client.
sleep(2);
}
# Child runs this.
system("$GENCABUNDLE") == 0
or fatal("$GENCABUNDLE failed");
}
return 0;
}
#
# Read bundle and return list.
#
sub ReadBundle($)
{
my ($filename) = @_;
my @certs = ();
my $certstr;
if (! open(BUNDLE, $filename)) {
return ();
}
while (<BUNDLE>) {
if ($_ =~ /^-----BEGIN CERT/) {
$certstr = "";
next;
}
if ($_ =~ /^-----END CERT/) {
push(@certs, $certstr);
$certstr = undef;
next;
}
if (defined($certstr)) {
$certstr .= $_;
next;
}
}
# If the file is properly terminated, there should be no certificate in
# progress. Hopefully the file is not trashed at a boundry.
fatal("Trashed bundle file")
if ($certstr);
close(BUNDLE);
my %certs = ();
foreach my $cert (@certs) {
my $certificate = GeniCertificate->LoadFromString($cert);
if (!defined($certificate)) {
print STDERR $cert;
fatal("Could not load certificate");
}
my $DN = $certificate->DN();
$certs{$DN} = $certificate;
}
return %certs;
}
#
# Initialize the table. Only on a clearinghouse.
#
sub Initialize()
{
exit(0)
if (! ($PGENISUPPORT && $ISCLRHOUSE));
#
# Grab all of the approved certs from the directory.
#
opendir(DIR, $CERTDIR) ||
fatal("cannot opendir $CERTDIR: $!");
my @pems = grep { /\.pem$/ && -f "$CERTDIR/$_" } readdir(DIR);
closedir(DIR);
foreach my $pem (@pems) {
my $pemcert = GeniCertificate->LoadFromFile("$CERTDIR/$pem");
if (!defined($pemcert)) {
fatal("Could not get certificate from $CERTDIR/$pem");
}
my $hash = $pemcert->SubjectHash();
my $safe_cert = DBQuoteSpecial($pemcert->cert());
my $safe_dn = $pemcert->DN();
$safe_dn =~ s/^subject=\s*//i;
$safe_dn = DBQuoteSpecial($safe_dn);
DBQueryWarn("replace into geni_cas set ".
" hash='$hash', created=now(), ".
" cert=$safe_cert, DN=$safe_dn")
or fatal("Could not update DB");
}
return 0;
}
sub fatal($)
{
my ($msg) = @_;
die("*** $0:\n".
" $msg\n");
}
#!/usr/bin/perl -w
#
# GENIPUBLIC-COPYRIGHT
# Copyright (c) 2011 University of Utah and the Flux Group.
# Copyright (c) 2011-2012 University of Utah and the Flux Group.
# All rights reserved.
#
use strict;
......@@ -27,6 +27,7 @@ my $TBOPS = "@TBOPSEMAIL@";
my $TBLOGS = "@TBLOGSEMAIL@";
my $PGENIDOMAIN = "@PROTOGENI_DOMAIN@";
my $PGENISUPPORT = @PROTOGENI_SUPPORT@;
my $ISCLRHOUSE = @PROTOGENI_ISCLEARINGHOUSE@;
my $WWWBUNDLE = "$TB/www/genica.bundle";
my $BUNDLE = "$TB/etc/genica.bundle";
my $APACHE_START = "@APACHE_START_COMMAND@";
......@@ -43,8 +44,16 @@ sub fatal($);
#
$| = 1;
if ($UID != 0) {
fatal("Must be root to run this script\n");
if (!$ISCLRHOUSE) {
fatal("This script can only be run on a Clearing House!");
}
# We do not want to run this script unless its the real version.
if ($EUID != 0) {
fatal("Must be root! Maybe its a development version?");
}
# XXX Hacky!
if ($TB ne "/usr/testbed") {
fatal("Wrong version. Maybe its a development version?");
}