#!/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 $secretkeyring; 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 $gpg = "@mygpg@"; my $TARGETKEYID = "@mykeyid@"; my $KEYTAG = "@mykeytag@"; $cfgfile =~ s/^REQ_FROM_SERVER//; $datafile =~ s/^REQ_FROM_SERVER//; $gpg = "gpg" if ($gpg eq ""); sub check_gpg_agent() { my $gpgconf = "$ENV{'HOME'}/.gnupg/gpg.conf"; if (!-f "$gpgconf") { $gpgconf = "$ENV{'HOME'}/.gnupg/options"; } if (-f $gpgconf) { my @array = (); tie @array, 'Tie::File', $gpgconf or die "Cannot tie ${gpgconf}: $!"; my @grep = grep(/^\s*use-agent/, @array); # print "matches = $#grep\n"; if ($#grep >= 0) { if (exists $ENV{'GPG_AGENT_INFO'}) { my $socke = $ENV{'GPG_AGENT_INFO'}; $socke =~ s/:.*//; # print "socke = $socke\n"; if (! -S $socke) { print "--------------------------------------------------\n"; print "\n"; print " GPG is set to use gpg-agent, but GPG agent is"; print " not running, though GPG_AGENT_INFO is defined.\n\n"; print " Please restart gpg-agent, or remove the use-agent\n"; print " option from ${gpgconf} and unset GPG_AGENT_INFO\n\n"; print "--------------------------------------------------\n"; print "\n"; exit 1; } } else { print "--------------------------------------------------\n"; print "\n"; print " GPG is set to use gpg-agent, but "; print " GPG_AGENT_INFO is not defined.\n\n"; print " Please start gpg-agent, or remove the use-agent\n"; print " option from ${gpgconf}\n\n"; print "--------------------------------------------------\n"; print "\n"; exit 1; } } untie @array; } } 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 PGP 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 gpg. By default, gpg will ask.\n\n"; print " -s gnupg_homedir --secretkeyring gnupg_homedir\n"; print " Select an alternate gpg homedirectory to locate the secret keyring.\n"; print " Will use '$ENV{'HOME'}/.gnupg/' 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_gpg_uid () { if (0 != $>) { print "--------------------------------------------------\n"; print "\n"; print " You are not root. Please remember that samhain/yule\n"; print " will use the public keyring 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 keyring of the non-root yule user.\n"; print "\n"; print "--------------------------------------------------\n"; } } } sub check_gpg_sign () { if ( defined($secretkeyring)) { if ( (!-d "$secretkeyring")){ print "--------------------------------------------------\n"; print "\n"; print " Secret keyring $secretkeyring not found!\n"; print "\n"; print " Please check the path/name of the alternate secret keyring.\n"; print "\n"; print "--------------------------------------------------\n"; print "\n"; exit; } } else { if ( (!-d "$ENV{'HOME'}/.gnupg") || (!-e "$ENV{'HOME'}/.gnupg/secring.gpg")) { print "--------------------------------------------------\n"; print "\n"; if (!-d "$ENV{'HOME'}/.gnupg") { print " Directory \$HOME/.gnupg not found!\n"; } else { print " Secret keyring \$HOME/.gnupg/secring.gpg 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 gpg --gen-key\n"; print " to generate a public/private keypair first.\n"; print "\n"; print "--------------------------------------------------\n"; print "\n"; exit; } } } sub check_gpg_verify () { if ( (!-d "$ENV{'HOME'}/.gnupg") || (!-e "$ENV{'HOME'}/.gnupg/pubring.gpg")) { print "--------------------------------------------------\n"; print "\n"; if (!-d "$ENV{'HOME'}/.gnupg") { print " Directory \$HOME/.gnupg not found!\n"; } else { print " Public keyring \$HOME/.gnupg/pubring.gpg not found!\n"; } print "\n"; print " This indicates that you have never used gpg before \n"; print " and/or have no public keys to verify signatures.\n"; print " \n"; print " Please use 'gpg --export key_id' to export the public\n"; print " signing key of the user who is signing the\n"; print " configuration/database files.\n\n"; print " Then you can use 'gpg --import keyfile' to import the key\n"; print " into this user's public keyring.\n"; print "\n"; print "--------------------------------------------------\n"; print "\n"; exit; } } sub generate () { my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg --gen-key"; check_gpg_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 = "$gpg --homedir $ENV{'HOME'}/.gnupg --status-fd 1 "; $command .= "--verbose " if (defined($opts{'v'})); $command .= "--verify $filename "; if (defined($opts{'v'})) { $command .= "2>&1"; } else { $command .= "2>/dev/null"; } print STDOUT "Using: $command\n\n" if (defined($opts{'v'})); open GPGIN, "$command |" or die "Cannot fork: $!"; while () { if ($_ =~ /^\[GNUPG:\] GOODSIG ([0-9A-F]+) (.*)$/) { $message .= "GOOD signature with key: $1\n"; $message .= "Key owner: $2\n"; $have_sig = 1; $retval = 0; } if ($_ =~ /^\[GNUPG:\] VALIDSIG ([0-9A-F]+) ([0-9\-]+)\s/) { $message .= "Key fingerprint: $1\n"; $message .= "Signature generated on: $2\n\n"; $have_fp = 1; $message .= "This file is signed with a valid signature.\n" if ($have_sig == 1); $have_sig = 1; $have_fp = 1; } if ($_ =~ /^\[GNUPG:\] NODATA 1/) { $message .= "NO signature found.\n\n"; $message .= "This file is not signed !!!\n"; $have_sig = 1; $have_fp = 1; $retval = 2; } if ($_ =~ /^\[GNUPG:\] BADSIG ([0-9A-F]+) (.*)$/) { $message .= "BAD signature with key: $1\n"; $message .= "Key owner: $2\n\n"; $message .= "This file is signed with an invalid signature !!!\n"; $have_sig = 1; $have_fp = 1; $retval = 1; } if ($_ =~ /^\[GNUPG:\] NO_PUBKEY ([0-9A-F]+)/) { $message .= "NOT CHECKED signature with key: $1\n\n"; $message .= "The signature of this file cannot be checked: no public key available !!!\n"; $have_sig = 1; $have_fp = 1; $retval = 1; } print STDOUT $_ if (defined($opts{'v'})); } close (GPGIN); print STDOUT "\n" if (defined($opts{'v'})); if ($have_sig == 0) { $message .= "NO valid signature found\n"; } elsif ($have_fp == 0) { $message .= "NO fingerprint found\n"; } close (FIN); if ($no_print_examine == 0) { print STDOUT $message; } unlink0( $fh, $filename ) or die "Cannot unlink $filename safely"; return $retval; } 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); # ($fh, $filename) = tempfile(UNLINK => 1); } else { open $fh, ">$file1" or die "Cannot open file $file1 for write: $!"; } autoflush $fh 1; while () { if ($_ =~ /^-----BEGIN PGP SIGNED MESSAGE-----/) { $sigstart = 1; $bodystart = 0; next; } elsif (($sigstart == 1) && ($_ =~ /^\s+$/)) { $sigstart = 0; $bodystart = 1; next; } elsif ($_ =~ /^-----BEGIN PGP SIGNATURE-----/) { $bodystart = 0; $sigend = 1; next; } elsif (($sigend == 1) && ($_ =~ /^-----END PGP SIGNATURE-----/)) { $sigend = 0; $bodystart = 1; 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 ($_ =~ /^-----BEGIN PGP SIGNED MESSAGE-----/) { $sigstart = 1; next; } elsif (($sigstart == 1) && ($_ =~ /^\s+$/)) { $sigstart = 0; $bodystart = 1; next; } elsif ($_ =~ /^-----BEGIN PGP SIGNATURE-----/) { $bodystart = 0; exit; } 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_gpg_uid(); check_gpg_agent(); 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 ($_ =~ /^-----BEGIN PGP SIGNED MESSAGE-----/) { $sigstart = 1; $bodystart = 0; next; } elsif (($sigstart == 1) && ($_ =~ /^\s+$/)) { $sigstart = 0; $bodystart = 1; next; } elsif ($_ =~ /^-----BEGIN PGP SIGNATURE-----/) { $bodystart = 0; $sigend = 1; next; } elsif (($sigend == 1) && ($_ =~ /^-----END PGP SIGNATURE-----/)) { $sigend = 0; $bodystart = 1; next; } if ($bodystart == 1) { print $fh1 $_; } # # 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 . ".asc"; $stats = stat($file1) or die "No file $file1: $!"; } if (defined($passphrase)) { local $SIG{PIPE} = 'IGNORE'; my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg --passphrase-fd 0 -a ${KEYTAG} ${TARGETKEYID} --clearsign -o $fileout --not-dash-escaped "; $command .= "--secret-keyring $secretkeyring " if (defined($opts{'s'})); $command .= "$file1"; 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 { my $command = "$gpg --homedir $ENV{'HOME'}/.gnupg -a ${KEYTAG} ${TARGETKEYID} --clearsign -o $fileout --not-dash-escaped "; $command .= "--secret-keyring $secretkeyring " if (defined($opts{'s'})); $command .= "$file1"; 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|secretkeyring=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'})) { $TARGETKEYID = $opts{'k'}; $KEYTAG = "--default-key"; } 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'})) { $secretkeyring = $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_gpg_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_gpg_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 (); } }