#!/usr/bin/env perl
# $Id: signed.pl,v 1.4 2012/10/19 19:58:21 ksb Exp $
# $Source: /usr/msrc/usr/local/libexec/jacket/RCS/signed.pl,v $
#
# You want to let a Developer run a script that does specail things, but
# they can also write on the script (via some move to production process).
# So you want to assure that Ops has reviewed changes to  the script before
# it is allowed to run in production.  This is helmet for you.  We run
# cksum over the proposed target porgram.  When the sum doesn't match the
# one sent in the environment we fail the escalation.

use strict;
use Getopt::Std;
# Auto-flush standard out, we need the child to see it ASAP
select STDERR; $| = 1;
select STDOUT; $| = 1;

my($progname, $usage, %opts);
($progname = $0) =~ s,.*/,,;

# Untaint our environment
$ENV{'PATH'} = "/bin:/usr/bin:/usr/local/bin:/usr/local/sbin";
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

# Document the parameters you expect in the rule-provided environment
# $SIGNED_FILTER_cksum=sums	use and consumed or not
# $SIGNED_CMD_size=output	use anc consume
# $SIGNED_REVEAL=prefix		standard reveal logic
# $SIGNED_WARN=sorry		common error rejection override
my($Reveal) = 'SIGNED_REVEAL';
my($Filter) = 'SIGNED_FILTER_';
my($Cmd) = 'SIGNED_CMD_';
my($Warn) = 'SIGNED_WARN';
my($Warning) = 'Sorry';


$usage = "$progname: usage -P pid [helmet-opts] -- mnemonic program euid:egid cred_type:cred";
getopts("VhHP:u:g:f:j:m:R:C:", \%opts);

if ($opts{'V'}) {
	print "$progname: ", '$Id: signed.pl,v 1.4 2012/10/19 19:58:21 ksb Exp $', "\n";
	exit 0;
}

if ($opts{'h'}) {
        print   "$usage\n",
                "$progname: usage -h\n",
                "$progname: usage -V\n",
		"P pid       process that we jacket\n",
                "h           print only this help message\n",
                "V           show only version info\n",
                "helmet-opts arguments to any op helmet program\n",
		"mnemonic    the requested op mnemonic\n",
		"program     client application\n",
		"euid:egid   target login and group uids\n",
		"cred_type   the credential type that granted access (groups, users, or netgroups)\n",
		"cred        the matching group, login, or netgroup\n";
	exit 0;
}
if ($opts{'H'}) {
	print "$progname: check program against a known signature\n",
		"SIGNED_FILTER_cmd=output compare output signature to cmd <file\n",
		"SIGNED_CMD_cmd=output\t compare output signature to cmd file\n",
		"$Reveal\t\t standard reveal logic\n",
		"$Warn\t\t replace the sorry message\n";
	exit 0;
}

# Untaint our params, common code to most jackets. --ksb
shift @ARGV if (scalar(@ARGV) && $ARGV[0] =~ m/^--$/o);
if (scalar(@ARGV) != 4) {
	print STDERR "$progname: exactly 4 positional parameters required\n";
	print "64\n" if $opts{'P'};
	exit 64;
}
if ($ARGV[0] !~ m|^([-/\@\w.]+)$|o) {
	print STDERR "$progname: mnemonic is zero width, or spelled badly\n";
	print "64\n" if $opts{'P'};
	exit 64;
}
my($MNEMONIC) = $1;
if ($ARGV[1] !~ m|^([-/\@\w.]+)$|o) {
	print STDERR "$progname: program specification looks bogus\n";
	print "64\n" if $opts{'P'};
	exit 64;
}
my($PROGRAM) = $1;
if ($ARGV[2] !~ m/^(\d+):(\d+)$/o) {
	print STDERR "$progname: euid:egid format error (wants digits:digits)\n";
	print "65\n" if $opts{'P'};
	exit 65;
}
my($EUID, $EGID) = ($1, $2);
if ($ARGV[3] !~ m/^([^:]*):([^:]*)$/o) {
	print STDERR "$progname: cred_type:cred $ARGV[3] missing colon\n";
	print "76\n" if $opts{'P'};
	exit 76;
}

# Modify the escalated process with "~prefix", "-ENV", "$ENV=value", or
# "$ENV=value" here, remove the reveal code if you must.
if (exists $ENV{$Reveal} and $ENV{$Reveal} =~ m/(.*)/o) {
	print "-$Reveal\n~$1\n";
	delete $ENV{$Reveal};
}
if (exists $ENV{$Warn} and $ENV{$Warn} =~ m/(.*)/o) {
	$Warning = $1;
	delete $ENV{$Warn};
}

my($checks) = 0;
if ('echo' eq $PROGRAM) {
	# echo is not real, but is acutally the op binary so we trust it
	$checks = 'the echo';
	# remove the (unused) configuration environment
	foreach (grep(m/^$Filter/o || m/^$Cmd/o, keys(%ENV))) {
		print "-$_\n";
	}
	goto ok;
}
if (exists $opts{'R'} and $opts{'R'} =~ m!^(/.+)$!o) {
	my($prefix) = $1;
	if ($PROGRAM =~ m!^/!o or $prefix =~ m!/$!o) {
		$PROGRAM = "$prefix$PROGRAM";
	} else {
		$PROGRAM = "$prefix/$PROGRAM";
	}
}
my(@stProg) = stat $PROGRAM;
if (0 == scalar(@stProg)) {
	print STDERR "$Warning\n";
	print "69\n" if $opts{'P'};
	exit 69;
}

# tainted fixes from perlsec
my($mypath) = "/bin:/usr/bin:/usr/local/bin:/usr/openwin/bin:/usr/X11/bin:/usr/X11R6/bin:/usr/local/sbin";
my($herpath) = $ENV{'PATH'} =~ m/^(.*)/o;
if (defined($herpath)) {
	if ($herpath =~ m/[']/o) {
		$herpath =~ s/[']/'\\''/g;
	}
	$herpath = "export PATH='$herpath';";
} else {
	$herpath = '';
}
my($env, $value, $fh, $line);
my(@todo) = grep(m/^$Filter/o || m/^$Cmd/o, keys(%ENV));
while ($env = shift(@todo)) {
	next unless ($ENV{$env} =~ m/^(.*)$/so);
	++$checks;
	$value = $1;
	delete $ENV{$env};
	print "-$env\n";

	if ($env =~ s/^$Filter//o) {
		$env .= " <$PROGRAM";
	} elsif ($env =~ s/^$Cmd//o) {
		$env .= " $PROGRAM";
	}
	if (! open($fh, '-|', $herpath.$env)) {
		print STDERR "$Warning\n";
		print "66\n" if $opts{'P'};
		exit 66;
	}
	while ($line = <$fh>) {
		my($l);
		chomp $line;
		$value =~ s/^\s*//;
		$line =~ s/^\s*//;
		$line =~ s/\s\s*/ /g;
		$l = length($line);
		last if ($line ne substr($value, 0, $l));
		$value = substr($value, $l);
	}
	if ($value !~ m/^\s*$/o) {
		print STDERR "$Warning\n";
		print "77\n" if $opts{'P'};
		exit 77;
	}
	close($fh);
	if (0 != $?) {
		print STDERR "$Warning\n";
		print "69\n" if $opts{'P'};
		exit($? < 256 ? $? : ($? >> 8));
	}
}

ok:
print "# $progname passed $checks signature checks\n";
my($kid, $status);
$status = 0;
if ($opts{'P'}) {
	open STDOUT, ">/dev/null";
	$kid = waitpid $opts{'P'}, 0;
	$status = $? < 256 ? $? : ($? >> 8);
}

# Exit with (a function of) her exit code
exit $status;
