1#!/usr/local/bin/perl
2
3# Copyright Rainer Wichmann (2004)
4#
5# License Information:
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19#
20
21use warnings;
22use strict;
23use Getopt::Long;
24use File::Basename;
25use File::Copy;
26use File::stat;
27use File::Temp qw/ tempfile tempdir unlink0 /;
28use IO::Handle;
29use Fcntl qw(:DEFAULT :flock);
30use Tie::File;
31
32# Do I/O to the data file in binary mode (so it
33# wouldn't complain about invalid UTF-8 characters).
34use bytes;
35
36File::Temp->safe_level( File::Temp::HIGH );
37
38my %opts = ();
39my $action;
40my $file1;
41my $file2;
42my $passphrase;
43my $secretkey;
44my $return_from_sign = 0;
45my $no_print_examine = 0;
46my $no_remove_lock   = 0;
47my $base = basename($0);
48
49my $cfgfile  = "@myconffile@";
50my $datafile = "@mydatafile@";
51my $daemon   = "@sbindir@/@install_name@";
52my $signify  = "@mysignify@";
53
54my $SIGDIR   = "$ENV{'HOME'}/.signify";
55my $KEYID    = "@install_name@";
56
57$cfgfile  =~ s/^REQ_FROM_SERVER//;
58$datafile =~ s/^REQ_FROM_SERVER//;
59
60$signify = "signify-openbsd" if ($signify eq "");
61
62sub usage() {
63    print "Usage:\n";
64    print "  $base { -m F | --create-cfgfile }    [options] [in.cfgfile]\n";
65    print "    Sign the configuration file. If in.cfgfile is given, sign it\n";
66    print "    and install it as configuration file.\n\n";
67
68    print "  $base { -m f | --print-cfgfile }     [options] \n";
69    print "    Print the configuration file to stdout. Signatures are removed.\n\n";
70
71    print "  $base { -m D | --create-datafile }   [options] [in.datafile]\n";
72    print "    Sign the database file. If in.datafile is given, sign it\n";
73    print "    and install it as database file.\n\n";
74
75    print "  $base { -m d | --print-datafile }    [options] \n";
76    print "    Print the database file to stdout. Signatures are removed. Use\n";
77    print "    option --list to list files in database rather than printing the raw file.\n\n";
78
79    print "  $base { -m R | --remove-signature }  [options] file1 [file2 ...]\n";
80    print "    Remove cleartext signature from input file(s). The file\n";
81    print "    is replaced by the non-signed file.\n\n";
82
83    print "  $base { -m E | --sign }              [options] file1 [file2 ...]\n";
84    print "    Sign file(s) with a cleartext signature. The file\n";
85    print "    is replaced by the signed file.\n\n";
86
87    print "  $base { -m e | --examine }           [options] file1 [file2 ...]\n";
88    print "    Report signature status of file(s).\n\n";
89
90    print "  $base { -m G | --generate-keys }     [options] \n";
91    print "    Generate a signify keypair to use for signing.\n\n";
92
93    print "Options:\n";
94    print "  -c cfgfile    --cfgfile cfgfile\n";
95    print "    Select an alternate configuration file.\n\n";
96
97    print "  -d datafile   --datafile datafile\n";
98    print "    Select an alternate database file.\n\n";
99
100    print "  -p passphrase --passphrase passphrase\n";
101    print "    Set the passphrase for signify. By default, signify will ask.\n\n";
102
103    print "  -s signify_dir --signify-dir signify_dir\n";
104    print "    Select an alternate directory to locate the secret keyring.\n";
105    print "    Will use '$ENV{'HOME'}/.signify/' by default.\n\n";
106
107    print "  -k keyid      --keyid keyid\n";
108    print "   Select the keyid to use for signing.\n\n";
109
110    print "  -l            --list\n";
111    print "    List the files in database rather than printing the raw file.\n\n";
112
113    print "  -v            --verbose\n";
114    print "    Verbose output.\n\n";
115    return;
116}
117
118sub check_signify_uid () {
119    if (0 != $>) {
120	print "--------------------------------------------------\n";
121	print "\n";
122	print " You are not root. Please remember that samhain/yule\n";
123	print " will use the public key of root to verify a signature.\n";
124	print "\n";
125	print "--------------------------------------------------\n";
126    } else {
127	if (!("@yulectl_prg@" =~ //)) {
128	    print "--------------------------------------------------\n";
129	    print "\n";
130	    print " Please remember that yule will drop root after startup. Signature\n";
131	    print " verification on SIGHUP will fail if you do not import the public key\n";
132	    print " into the ~/.signify/ directory of the non-root yule user.\n";
133	    print "\n";
134	    print "--------------------------------------------------\n";
135	}
136    }
137}
138
139sub check_signify_sign () {
140    if ( defined($secretkey)) {
141        if ( (!-d "$secretkey")){
142            print "--------------------------------------------------\n";
143            print "\n";
144            print " Secret key $secretkey not found!\n";
145            print "\n";
146            print " Please check the path/name of the alternate secret key.\n";
147            print "\n";
148            print "--------------------------------------------------\n";
149            print "\n";
150            exit;
151        }
152    } else {
153        if ( (!-d "$SIGDIR") || (!-e "${SIGDIR}/${KEYID}.sec")) {
154	    print "--------------------------------------------------\n";
155	    print "\n";
156	    if (!-d "$SIGDIR") {
157	        print " Directory $SIGDIR not found!\n";
158	    } else {
159	        print " Secret key ${SIGDIR}/${KEYID}.sec not found!\n";
160	    }
161	    print "\n";
162	    print " This indicates that you have never created a \n";
163	    print " public/private keypair, and thus cannot sign.\n";
164	    print " \n";
165	    print " Please use $0 --generate-keys or\n";
166	    print " $signify -G -s ${SIGDIR}/${KEYID}.sec -p ${SIGDIR}/${KEYID}.pub\n";
167	    print " to generate a public/private keypair first.\n";
168	    print "\n";
169	    print "--------------------------------------------------\n";
170	    print "\n";
171	    exit;
172        }
173    }
174}
175
176sub check_signify_verify () {
177    if ( (!-d "${SIGDIR}") || (!-e "${SIGDIR}/${KEYID}.pub")) {
178	print "--------------------------------------------------\n";
179	print "\n";
180	if (!-d "$SIGDIR") {
181	    print " Directory $SIGDIR not found!\n";
182	} else {
183	    print " Public key ${SIGDIR}/${KEYID}.pub not found!\n";
184	}
185	print "\n";
186	print " This indicates that you have no public key\n";
187	print " to verify signatures.\n";
188	print " \n";
189	print " Please copy the public key ${KEYID}.pub of\n";
190	print " the user who is signing the configuration/database files\n";
191	print " into the directory $SIGDIR.\n";
192	print "\n";
193	print "--------------------------------------------------\n";
194	print "\n";
195	exit;
196    }
197}
198
199
200sub generate () {
201    my $command = "$signify -G -s ${SIGDIR}/${KEYID}.sec -p ${SIGDIR}/${KEYID}.pub";
202    if (!-d "${SIGDIR}") {
203	unless(mkdir "$SIGDIR", 0750) {
204	    die "Creating directory $SIGDIR failed: $?";
205	}
206    }
207    check_signify_uid();
208    system ($command) == 0
209	or die "system $command failed: $?";
210    exit;
211}
212
213sub examine () {
214    my $iscfg = 0;
215    my $have_fp  = 0;
216    my $have_sig = 0;
217    my $message = '';
218    my $retval  = 9;
219    my $fh;
220    my $filename;
221
222    if (!($file1 =~ /^\-$/)) {
223	die ("Cannot open $file1 for read: $!") unless ((-e $file1) && (-r _));
224    }
225    open FIN,  "<$file1" or die "Cannot open $file1 for read: $!";
226
227    my $dir = tempdir( CLEANUP => 1 );
228    $filename = $dir . "/exa_jhfdbilw." . $$;
229    open $fh, ">$filename" or die "Cannot open $filename";
230    autoflush $fh 1;
231
232    while (<FIN>) {
233	print $fh $_;
234	if ($_ =~ /^\s*\[Misc\]/) {
235	    $iscfg = 1;
236	}
237    }
238    if ($iscfg == 1) {
239	$message .=  "File $file1 is a configuration file\n\n";
240    } else {
241	$message .=  "File $file1 is a database file\n\n";
242    }
243
244
245    my $command = "$signify -Vem /dev/null -p ${SIGDIR}/${KEYID}.pub ";
246    $command .= "-x $filename ";
247    if (defined($opts{'v'})) {
248	$command .= "2>&1";
249    } else {
250	$command .= "2>/dev/null";
251    }
252
253    print STDOUT "Using: $command\n\n" if (defined($opts{'v'}));
254    open  SIGIN, "$command |" or die "Cannot fork: $!";
255
256    while (<SIGIN>) {
257	chomp ($_);
258	if ($_ =~ /^Signature Verified$/) {
259	    $message .= "GOOD signature with key: ${SIGDIR}/${KEYID}.pub\n";
260	    $have_sig = 1;
261	    $retval   = 0;
262	}
263	print STDOUT $_ if (defined($opts{'v'}));
264    }
265    close (SIGIN);
266    print STDOUT "\n" if (defined($opts{'v'}));
267    if ($have_sig == 0) {
268	$message .=  "NO valid signature found\n";
269    }
270    close (FIN);
271    if ($no_print_examine == 0) {
272	print STDOUT $message;
273    }
274    unlink0( $fh, $filename ) or die "Cannot unlink $filename safely";
275    return $retval;
276}
277
278sub wstrip ($) {
279    $_ = shift;
280    $_ =~ s/\s+//g;
281    return $_;
282}
283
284sub remove () {
285    my $bodystart = 1;
286    my $sigstart  = 0;
287    my $sigend    = 0;
288    my $filename  = "";
289    my $fh;
290    my $stats;
291
292    open FH, "<$file1" or die "Cannot open file $file1 for read: $!";
293    if (!($file1 =~ /^\-$/)) {
294	flock(FH, LOCK_EX) unless ($no_remove_lock == 1);
295	my $dir = tempdir( CLEANUP => 1 ) or die "Tempdir failed";
296	$filename = $dir . "/rem_iqegBCQb." . $$;
297	open $fh, ">$filename" or die "Cannot open $filename";
298	$stats = stat($file1);
299    } else {
300	open $fh, ">$file1" or die "Cannot open file $file1 for write: $!";
301    }
302    autoflush $fh 1;
303    while (<FH>) {
304	if ($_ =~ /^untrusted comment: /) {
305	    $sigstart = 1;
306	    $bodystart = 0;
307	    next;
308	} elsif (($sigstart == 1) && (wstrip($_) =~ m{^(?: [A-Za-z0-9+/]{4} )*(?:[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$}xm )) {
309	    $sigstart = 0;
310	    $bodystart = 1;
311	    next;
312	} elsif (($sigstart == 1) && ($bodystart == 0)) {
313	    # comment NOT followed by signature
314	    $sigstart = 0;
315	    next;
316	}
317
318	if ($bodystart == 1) {
319	    print $fh $_;
320	}
321    }
322    if (!($file1 =~ /^\-$/)) {
323	copy("$filename", "$file1")
324	    or die "Copy $filename to $file1 failed: $!";
325	chmod $stats->mode, $file1;
326	chown $stats->uid, $stats->gid, $file1;
327	flock(FH, LOCK_UN) unless ($no_remove_lock == 1);
328	close FH;
329    }
330    unlink0( $fh, $filename ) or die "Cannot unlink $filename safely";
331    return;
332}
333
334sub print_cfgfile () {
335    my $bodystart = 0;
336    my $sigstart  = 0;
337
338    if (!defined($file2)) {
339	$file2 = '-';
340    }
341
342    open FH, "<$file1" or die "Cannot open file $file1 for read: $!";
343    open FO, ">$file2" or die "Cannot open file $file2 for write: $!";
344    while (<FH>) {
345	if ($_ =~ /^untrusted comment: /) {
346	    $sigstart = 1;
347	    $bodystart = 0;
348	    next;
349	} elsif (($sigstart == 1) && (wstrip($_) =~ m{^(?: [A-Za-z0-9+/]{4} )*(?:[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$}xm )) {
350	    $sigstart = 0;
351	    $bodystart = 1;
352	    next;
353	} elsif (($sigstart == 1) && ($bodystart == 0)) {
354	    # comment NOT followed by signature
355	    $sigstart = 0;
356	    next;
357	}
358	if ($bodystart == 1) {
359	    print FO $_;
360	}
361    }
362    exit;
363}
364
365sub print_datafile () {
366    die ("Cannot find program $daemon")
367	unless (-e $daemon);
368    if (defined($opts{'v'})) {
369	open FH, "$daemon --full-detail -d $datafile |"
370	    or die "Cannot open datafile $datafile for read: $!";
371    } else {
372	open FH, "$daemon -d $datafile |"
373	    or die "Cannot open datafile $datafile for read: $!";
374    }
375    while (<FH>) {
376	print $_;
377    }
378    exit;
379}
380
381sub sign_file () {
382
383    my $fileout = '';
384    my $bodystart = 1;
385    my $sigstart  = 0;
386    my $sigend    = 0;
387    my $stats;
388    my $fh1;
389    my $filename1;
390    my $flag1     = 0;
391
392    check_signify_uid();
393
394    if (!defined($file2)) {
395	$file2 = $file1;
396    }
397
398    if ($file1 =~ /^\-$/) {
399	my $dir = tempdir( CLEANUP => 1 ) or die "Tempdir failed";
400	$filename1 = $dir . "/sig_vs8827sd." . $$;
401	open $fh1, ">$filename1" or die "Cannot open $filename1";
402	$flag1 = 1;
403	# my ($fh1, $filename1) = tempfile(UNLINK => 1);
404
405	while (<STDIN>) {
406	    if ($_ =~ /^untrusted comment: /) {
407		$sigstart = 1;
408		$bodystart = 0;
409		next;
410	    } elsif (($sigstart == 1) && (wstrip($_) =~ m{^(?: [A-Za-z0-9+/]{4} )*(?:[A-Za-z0-9+/]{2} [AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?$}xm )) {
411		$sigstart = 0;
412		$bodystart = 1;
413		next;
414	    } elsif (($sigstart == 1) && ($bodystart == 0)) {
415		#comment NOT followed by signature
416		$sigstart = 0;
417		next;
418	    }
419
420	    if ($bodystart == 1) {
421		print $fh1 $_;
422	    }
423	}
424	$file1 = $filename1;
425	$fileout = '-';
426    } else {
427	open (LOCKFILE, "<$file1") or die "Cannot open $file1: $!";
428	flock(LOCKFILE, LOCK_EX);
429	$no_print_examine = 1;
430	$no_remove_lock   = 1;
431	if (examine() < 2) {
432	    remove();
433	}
434	$fileout = $file1 . ".sig";
435	$stats   = stat($file1)
436	    or die "No file $file1: $!";
437    }
438
439    my $command = "$signify -Se ";
440    $command .= "-s ${SIGDIR}/${KEYID}.sec ";
441    $command .= "-x ${fileout} ";
442    $command .= "-m $file1";
443
444    if (defined($passphrase)) {
445	local $SIG{PIPE} = 'IGNORE';
446	open (FH, "|$command")  or die "can't fork: $!";
447	print FH "$passphrase"  or die "can't write: $!";
448	close FH                or die "can't close: status=$?";
449    } else {
450	system("$command") == 0
451	    or die "system $command failed: $?";
452    }
453
454    if (!($fileout =~ /^\-$/)) {
455	my $st_old = stat($file1)
456	    or die "No file $file1: $!";
457	my $st_new = stat($fileout)
458	    or die "No file $fileout: $!";
459	die ("Signed file is smaller than unsigned file")
460	    unless ($st_new->size > $st_old->size);
461	move("$fileout", "$file2")
462	    or die "Move $fileout to $file2 failed: $!";
463	chmod $stats->mode, $file2;
464	chown $stats->uid, $stats->gid, $file2;
465	flock(LOCKFILE, LOCK_UN);
466    }
467
468    if ($flag1 == 1) {
469	unlink0( $fh1, $filename1 ) or die "Cannot unlink $filename1 safely";
470    }
471    if ($return_from_sign == 1) {
472	return;
473    }
474    exit;
475}
476
477Getopt::Long::Configure ("posix_default");
478Getopt::Long::Configure ("bundling");
479# Getopt::Long::Configure ("debug");
480
481GetOptions (\%opts, 'm=s', 'h|help', 'v|verbose', 'l|list',
482	    'c|cfgfile=s',
483	    'd|datafile=s',
484	    'p|passphrase=s',
485	    's|secretkey=s',
486            'k|keyid=s',
487	    'create-cfgfile',  # -m F
488	    'print-cfgfile',   # -m f
489	    'create-datafile', # -m D
490	    'print-datafile',  # -m d
491	    'remove-signature',# -m R
492	    'sign',            # -m E
493	    'examine',         # -m e
494	    'generate-keys');  # -m G
495
496if (defined ($opts{'h'})) {
497    usage();
498    exit;
499}
500
501if (defined($opts{'k'})) {
502    $KEYID = $opts{'k'};
503}
504if (defined($opts{'c'})) {
505    $cfgfile = $opts{'c'};
506}
507if (defined($opts{'d'})) {
508    $datafile = $opts{'d'};
509}
510if (defined($opts{'p'})) {
511    $passphrase = $opts{'p'};
512}
513if (defined($opts{'s'})) {
514    $SIGDIR = $opts{'s'};
515}
516
517if (defined ($opts{'m'}) && ($opts{'m'} =~ /[FfDdREeG]{1}/) ) {
518    $action = $opts{'m'};
519}
520elsif (defined ($opts{'create-cfgfile'})) {
521    $action = 'F';
522}
523elsif (defined ($opts{'print-cfgfile'})) {
524    $action = 'f';
525}
526elsif (defined ($opts{'create-datafile'})) {
527    $action = 'D';
528}
529elsif (defined ($opts{'print-datafile'})) {
530    $action = 'd';
531}
532elsif (defined ($opts{'remove-signature'})) {
533    $action = 'R';
534}
535elsif (defined ($opts{'sign'})) {
536    $action = 'E';
537}
538elsif (defined ($opts{'examine'})) {
539    $action = 'e';
540}
541elsif (defined ($opts{'generate-keys'})) {
542    $action = 'G';
543}
544else {
545    usage();
546    die ("No valid action specified !");
547}
548
549if (defined($ARGV[0])) {
550    $file1 = $ARGV[0];
551}
552if (defined($ARGV[1])) {
553    $file2 = $ARGV[1];
554}
555
556
557if (($action =~ /[REe]{1}/) && !defined($file1)) {
558    usage();
559    die("Option -m $action requires a filename (or '-' for stdio)\n");
560}
561
562if ($action =~ /^F$/) {
563    if (!defined($file1)) {
564	$file1 = $cfgfile;
565    }
566    $file2 = $cfgfile;
567    sign_file ();
568}
569
570if ($action =~ /^D$/) {
571    if (!defined($file1)) {
572	$file1 = $datafile;
573    }
574    $file2 = $datafile;
575    sign_file ();
576}
577
578if ($action =~ /^R$/) {
579    # $file1 defined
580    my $i = 0;
581    while (defined($ARGV[$i])) {
582	$file1 = $ARGV[$i];
583	remove ();
584	++$i;
585    }
586}
587
588if ($action =~ /^E$/) {
589    # $file1 defined
590    # default: $file2 = $file1
591    check_signify_sign();
592    my $i = 0;
593    while (defined($ARGV[$i])) {
594	$file1 = $ARGV[$i];
595	$file2 = $file1;
596	$return_from_sign = 1;
597	sign_file ();
598	++$i;
599    }
600}
601
602if ($action =~ /^e$/) {
603    # $file1 defined
604    # default: $file2 = stdout
605    check_signify_verify();
606    my $i = 0;
607    my $ret = 0;
608    while (defined($ARGV[$i])) {
609	print "\n";
610	$file1 = $ARGV[$i];
611	$ret += examine ();
612	++$i;
613	print "\n--------------------------------\n" if (defined($ARGV[$i]));
614    }
615    exit($ret);
616}
617
618if ($action =~ /^f$/) {
619    $file1 = $cfgfile;
620    $file2 = "-";
621    print_cfgfile ();
622}
623
624if ($action =~ /^d$/) {
625    # $file1 irrelevant
626    if (defined($opts{'l'})) {
627	print_datafile ();
628    } else {
629	$file1 = $datafile;
630	$file2 = "-";
631	print_cfgfile ();
632    }
633}
634
635
636
637