#!/usr/local/bin/perl # Copyright Rainer Wichmann (2004) # # License Information: # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # use warnings; use strict; use Getopt::Long; use File::Basename; use File::Copy; use File::stat; use File::Temp qw/ tempfile tempdir unlink0 /; use IO::Handle; use Fcntl qw(:DEFAULT :flock); use Tie::File; # Do I/O to the data file in binary mode (so it # wouldn't complain about invalid UTF-8 characters). use bytes; File::Temp->safe_level( File::Temp::HIGH ); my %opts = (); my $action; my $file1; my $file2; my $passphrase; my $secretkey; my $return_from_sign = 0; my $no_print_examine = 0; my $no_remove_lock = 0; my $base = basename($0); my $cfgfile = "@myconffile@"; my $datafile = "@mydatafile@"; my $daemon = "@sbindir@/@install_name@"; my $signify = "@mysignify@"; my $SIGDIR = "$ENV{'HOME'}/.signify"; my $KEYID = "@install_name@"; $cfgfile =~ s/^REQ_FROM_SERVER//; $datafile =~ s/^REQ_FROM_SERVER//; $signify = "signify-openbsd" if ($signify eq ""); sub usage() { print "Usage:\n"; print " $base { -m F | --create-cfgfile } [options] [in.cfgfile]\n"; print " Sign the configuration file. If in.cfgfile is given, sign it\n"; print " and install it as configuration file.\n\n"; print " $base { -m f | --print-cfgfile } [options] \n"; print " Print the configuration file to stdout. Signatures are removed.\n\n"; print " $base { -m D | --create-datafile } [options] [in.datafile]\n"; print " Sign the database file. If in.datafile is given, sign it\n"; print " and install it as database file.\n\n"; print " $base { -m d | --print-datafile } [options] \n"; print " Print the database file to stdout. Signatures are removed. Use\n"; print " option --list to list files in database rather than printing the raw file.\n\n"; print " $base { -m R | --remove-signature } [options] file1 [file2 ...]\n"; print " Remove cleartext signature from input file(s). The file\n"; print " is replaced by the non-signed file.\n\n"; print " $base { -m E | --sign } [options] file1 [file2 ...]\n"; print " Sign file(s) with a cleartext signature. The file\n"; print " is replaced by the signed file.\n\n"; print " $base { -m e | --examine } [options] file1 [file2 ...]\n"; print " Report signature status of file(s).\n\n"; print " $base { -m G | --generate-keys } [options] \n"; print " Generate a signify keypair to use for signing.\n\n"; print "Options:\n"; print " -c cfgfile --cfgfile cfgfile\n"; print " Select an alternate configuration file.\n\n"; print " -d datafile --datafile datafile\n"; print " Select an alternate database file.\n\n"; print " -p passphrase --passphrase passphrase\n"; print " Set the passphrase for signify. By default, signify will ask.\n\n"; print " -s signify_dir --signify-dir signify_dir\n"; print " Select an alternate directory to locate the secret keyring.\n"; print " Will use '$ENV{'HOME'}/.signify/' by default.\n\n"; print " -k keyid --keyid keyid\n"; print " Select the keyid to use for signing.\n\n"; print " -l --list\n"; print " List the files in database rather than printing the raw file.\n\n"; print " -v --verbose\n"; print " Verbose output.\n\n"; return; } sub check_signify_uid () { if (0 != $>) { print "--------------------------------------------------\n"; print "\n"; print " You are not root. Please remember that samhain/yule\n"; print " will use the public key of root to verify a signature.\n"; print "\n"; print "--------------------------------------------------\n"; } else { if (!("@yulectl_prg@" =~ //)) { print "--------------------------------------------------\n"; print "\n"; print " Please remember that yule will drop root after startup. Signature\n"; print " verification on SIGHUP will fail if you do not import the public key\n"; print " into the ~/.signify/ directory of the non-root yule user.\n"; print "\n"; print "--------------------------------------------------\n"; } } } sub check_signify_sign () { if ( defined($secretkey)) { if ( (!-d "$secretkey")){ print "--------------------------------------------------\n"; print "\n"; print " Secret key $secretkey not found!\n"; print "\n"; print " Please check the path/name of the alternate secret key.\n"; print "\n"; print "--------------------------------------------------\n"; print "\n"; exit; } } else { if ( (!-d "$SIGDIR") || (!-e "${SIGDIR}/${KEYID}.sec")) { print "--------------------------------------------------\n"; print "\n"; if (!-d "$SIGDIR") { print " Directory $SIGDIR not found!\n"; } else { print " Secret key ${SIGDIR}/${KEYID}.sec not found!\n"; } print "\n"; print " This indicates that you have never created a \n"; print " public/private keypair, and thus cannot sign.\n"; print " \n"; print " Please use $0 --generate-keys or\n"; print " $signify -G -s ${SIGDIR}/${KEYID}.sec -p ${SIGDIR}/${KEYID}.pub\n"; print " to generate a public/private keypair first.\n"; print "\n"; print "--------------------------------------------------\n"; print "\n"; exit; } } } sub check_signify_verify () { if ( (!-d "${SIGDIR}") || (!-e "${SIGDIR}/${KEYID}.pub")) { print "--------------------------------------------------\n"; print "\n"; if (!-d "$SIGDIR") { print " Directory $SIGDIR not found!\n"; } else { print " Public key ${SIGDIR}/${KEYID}.pub not found!\n"; } print "\n"; print " This indicates that you have no public key\n"; print " to verify signatures.\n"; print " \n"; print " Please copy the public key ${KEYID}.pub of\n"; print " the user who is signing the configuration/database files\n"; print " into the directory $SIGDIR.\n"; print "\n"; print "--------------------------------------------------\n"; print "\n"; exit; } } sub generate () { my $command = "$signify -G -s ${SIGDIR}/${KEYID}.sec -p ${SIGDIR}/${KEYID}.pub"; if (!-d "${SIGDIR}") { unless(mkdir "$SIGDIR", 0750) { die "Creating directory $SIGDIR failed: $?"; } } check_signify_uid(); system ($command) == 0 or die "system $command failed: $?"; exit; } sub examine () { my $iscfg = 0; my $have_fp = 0; my $have_sig = 0; my $message = ''; my $retval = 9; my $fh; my $filename; if (!($file1 =~ /^\-$/)) { die ("Cannot open $file1 for read: $!") unless ((-e $file1) && (-r _)); } open FIN, "<$file1" or die "Cannot open $file1 for read: $!"; my $dir = tempdir( CLEANUP => 1 ); $filename = $dir . "/exa_jhfdbilw." . $$; open $fh, ">$filename" or die "Cannot open $filename"; autoflush $fh 1; while () { print $fh $_; if ($_ =~ /^\s*\[Misc\]/) { $iscfg = 1; } } if ($iscfg == 1) { $message .= "File $file1 is a configuration file\n\n"; } else { $message .= "File $file1 is a database file\n\n"; } my $command = "$signify -Vem /dev/null -p ${SIGDIR}/${KEYID}.pub "; $command .= "-x $filename "; if (defined($opts{'v'})) { $command .= "2>&1"; } else { $command .= "2>/dev/null"; } print STDOUT "Using: $command\n\n" if (defined($opts{'v'})); open SIGIN, "$command |" or die "Cannot fork: $!"; while () { chomp ($_); if ($_ =~ /^Signature Verified$/) { $message .= "GOOD signature with key: ${SIGDIR}/${KEYID}.pub\n"; $have_sig = 1; $retval = 0; } print STDOUT $_ if (defined($opts{'v'})); } close (SIGIN); print STDOUT "\n" if (defined($opts{'v'})); if ($have_sig == 0) { $message .= "NO valid signature found\n"; } close (FIN); if ($no_print_examine == 0) { print STDOUT $message; } unlink0( $fh, $filename ) or die "Cannot unlink $filename safely"; return $retval; } sub wstrip ($) { $_ = shift; $_ =~ s/\s+//g; return $_; } sub remove () { my $bodystart = 1; my $sigstart = 0; my $sigend = 0; my $filename = ""; my $fh; my $stats; open FH, "<$file1" or die "Cannot open file $file1 for read: $!"; if (!($file1 =~ /^\-$/)) { flock(FH, LOCK_EX) unless ($no_remove_lock == 1); my $dir = tempdir( CLEANUP => 1 ) or die "Tempdir failed"; $filename = $dir . "/rem_iqegBCQb." . $$; open $fh, ">$filename" or die "Cannot open $filename"; $stats = stat($file1); } else { open $fh, ">$file1" or die "Cannot open file $file1 for write: $!"; } autoflush $fh 1; while () { if ($_ =~ /^untrusted comment: /) { $sigstart = 1; $bodystart = 0; next; } elsif (($sigstart == 1) && (wstrip($_) =~ m{^(?: [A-Za-z0-9+/]{4} )*(?:[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$}xm )) { $sigstart = 0; $bodystart = 1; next; } elsif (($sigstart == 1) && ($bodystart == 0)) { # comment NOT followed by signature $sigstart = 0; next; } if ($bodystart == 1) { print $fh $_; } } if (!($file1 =~ /^\-$/)) { copy("$filename", "$file1") or die "Copy $filename to $file1 failed: $!"; chmod $stats->mode, $file1; chown $stats->uid, $stats->gid, $file1; flock(FH, LOCK_UN) unless ($no_remove_lock == 1); close FH; } unlink0( $fh, $filename ) or die "Cannot unlink $filename safely"; return; } sub print_cfgfile () { my $bodystart = 0; my $sigstart = 0; if (!defined($file2)) { $file2 = '-'; } open FH, "<$file1" or die "Cannot open file $file1 for read: $!"; open FO, ">$file2" or die "Cannot open file $file2 for write: $!"; while () { if ($_ =~ /^untrusted comment: /) { $sigstart = 1; $bodystart = 0; next; } elsif (($sigstart == 1) && (wstrip($_) =~ m{^(?: [A-Za-z0-9+/]{4} )*(?:[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$}xm )) { $sigstart = 0; $bodystart = 1; next; } elsif (($sigstart == 1) && ($bodystart == 0)) { # comment NOT followed by signature $sigstart = 0; next; } if ($bodystart == 1) { print FO $_; } } exit; } sub print_datafile () { die ("Cannot find program $daemon") unless (-e $daemon); if (defined($opts{'v'})) { open FH, "$daemon --full-detail -d $datafile |" or die "Cannot open datafile $datafile for read: $!"; } else { open FH, "$daemon -d $datafile |" or die "Cannot open datafile $datafile for read: $!"; } while () { print $_; } exit; } sub sign_file () { my $fileout = ''; my $bodystart = 1; my $sigstart = 0; my $sigend = 0; my $stats; my $fh1; my $filename1; my $flag1 = 0; check_signify_uid(); if (!defined($file2)) { $file2 = $file1; } if ($file1 =~ /^\-$/) { my $dir = tempdir( CLEANUP => 1 ) or die "Tempdir failed"; $filename1 = $dir . "/sig_vs8827sd." . $$; open $fh1, ">$filename1" or die "Cannot open $filename1"; $flag1 = 1; # my ($fh1, $filename1) = tempfile(UNLINK => 1); while () { if ($_ =~ /^untrusted comment: /) { $sigstart = 1; $bodystart = 0; next; } elsif (($sigstart == 1) && (wstrip($_) =~ m{^(?: [A-Za-z0-9+/]{4} )*(?:[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$}xm )) { $sigstart = 0; $bodystart = 1; next; } elsif (($sigstart == 1) && ($bodystart == 0)) { #comment NOT followed by signature $sigstart = 0; next; } if ($bodystart == 1) { print $fh1 $_; } } $file1 = $filename1; $fileout = '-'; } else { open (LOCKFILE, "<$file1") or die "Cannot open $file1: $!"; flock(LOCKFILE, LOCK_EX); $no_print_examine = 1; $no_remove_lock = 1; if (examine() < 2) { remove(); } $fileout = $file1 . ".sig"; $stats = stat($file1) or die "No file $file1: $!"; } my $command = "$signify -Se "; $command .= "-s ${SIGDIR}/${KEYID}.sec "; $command .= "-x ${fileout} "; $command .= "-m $file1"; if (defined($passphrase)) { local $SIG{PIPE} = 'IGNORE'; open (FH, "|$command") or die "can't fork: $!"; print FH "$passphrase" or die "can't write: $!"; close FH or die "can't close: status=$?"; } else { system("$command") == 0 or die "system $command failed: $?"; } if (!($fileout =~ /^\-$/)) { my $st_old = stat($file1) or die "No file $file1: $!"; my $st_new = stat($fileout) or die "No file $fileout: $!"; die ("Signed file is smaller than unsigned file") unless ($st_new->size > $st_old->size); move("$fileout", "$file2") or die "Move $fileout to $file2 failed: $!"; chmod $stats->mode, $file2; chown $stats->uid, $stats->gid, $file2; flock(LOCKFILE, LOCK_UN); } if ($flag1 == 1) { unlink0( $fh1, $filename1 ) or die "Cannot unlink $filename1 safely"; } if ($return_from_sign == 1) { return; } exit; } Getopt::Long::Configure ("posix_default"); Getopt::Long::Configure ("bundling"); # Getopt::Long::Configure ("debug"); GetOptions (\%opts, 'm=s', 'h|help', 'v|verbose', 'l|list', 'c|cfgfile=s', 'd|datafile=s', 'p|passphrase=s', 's|secretkey=s', 'k|keyid=s', 'create-cfgfile', # -m F 'print-cfgfile', # -m f 'create-datafile', # -m D 'print-datafile', # -m d 'remove-signature',# -m R 'sign', # -m E 'examine', # -m e 'generate-keys'); # -m G if (defined ($opts{'h'})) { usage(); exit; } if (defined($opts{'k'})) { $KEYID = $opts{'k'}; } if (defined($opts{'c'})) { $cfgfile = $opts{'c'}; } if (defined($opts{'d'})) { $datafile = $opts{'d'}; } if (defined($opts{'p'})) { $passphrase = $opts{'p'}; } if (defined($opts{'s'})) { $SIGDIR = $opts{'s'}; } if (defined ($opts{'m'}) && ($opts{'m'} =~ /[FfDdREeG]{1}/) ) { $action = $opts{'m'}; } elsif (defined ($opts{'create-cfgfile'})) { $action = 'F'; } elsif (defined ($opts{'print-cfgfile'})) { $action = 'f'; } elsif (defined ($opts{'create-datafile'})) { $action = 'D'; } elsif (defined ($opts{'print-datafile'})) { $action = 'd'; } elsif (defined ($opts{'remove-signature'})) { $action = 'R'; } elsif (defined ($opts{'sign'})) { $action = 'E'; } elsif (defined ($opts{'examine'})) { $action = 'e'; } elsif (defined ($opts{'generate-keys'})) { $action = 'G'; } else { usage(); die ("No valid action specified !"); } if (defined($ARGV[0])) { $file1 = $ARGV[0]; } if (defined($ARGV[1])) { $file2 = $ARGV[1]; } if (($action =~ /[REe]{1}/) && !defined($file1)) { usage(); die("Option -m $action requires a filename (or '-' for stdio)\n"); } if ($action =~ /^F$/) { if (!defined($file1)) { $file1 = $cfgfile; } $file2 = $cfgfile; sign_file (); } if ($action =~ /^D$/) { if (!defined($file1)) { $file1 = $datafile; } $file2 = $datafile; sign_file (); } if ($action =~ /^R$/) { # $file1 defined my $i = 0; while (defined($ARGV[$i])) { $file1 = $ARGV[$i]; remove (); ++$i; } } if ($action =~ /^E$/) { # $file1 defined # default: $file2 = $file1 check_signify_sign(); my $i = 0; while (defined($ARGV[$i])) { $file1 = $ARGV[$i]; $file2 = $file1; $return_from_sign = 1; sign_file (); ++$i; } } if ($action =~ /^e$/) { # $file1 defined # default: $file2 = stdout check_signify_verify(); my $i = 0; my $ret = 0; while (defined($ARGV[$i])) { print "\n"; $file1 = $ARGV[$i]; $ret += examine (); ++$i; print "\n--------------------------------\n" if (defined($ARGV[$i])); } exit($ret); } if ($action =~ /^f$/) { $file1 = $cfgfile; $file2 = "-"; print_cfgfile (); } if ($action =~ /^d$/) { # $file1 irrelevant if (defined($opts{'l'})) { print_datafile (); } else { $file1 = $datafile; $file2 = "-"; print_cfgfile (); } }