Home | History | Annotate | Download | only in codesign
      1 #!/usr/perl5/bin/perl
      2 #
      3 # CDDL HEADER START
      4 #
      5 # The contents of this file are subject to the terms of the
      6 # Common Development and Distribution License (the "License").
      7 # You may not use this file except in compliance with the License.
      8 #
      9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
     10 # or http://www.opensolaris.org/os/licensing.
     11 # See the License for the specific language governing permissions
     12 # and limitations under the License.
     13 #
     14 # When distributing Covered Code, include this CDDL HEADER in each
     15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     16 # If applicable, add the following below this CDDL HEADER, with the
     17 # fields enclosed by brackets "[]" replaced with your own identifying
     18 # information: Portions Copyright [yyyy] [name of copyright owner]
     19 #
     20 # CDDL HEADER END
     21 #
     22 #
     23 # ident	"%Z%%M%	%I%	%E% SMI"
     24 #
     25 # Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
     26 # Use is subject to license terms.
     27 #
     28 
     29 # Server program for code signing server
     30 #
     31 # This program implements an ssh-based service to add digital
     32 # signatures to files. The sshd_config file on the server
     33 # contains an entry like the following to invoke this program:
     34 #
     35 #	Subsystem codesign /opt/signing/bin/server
     36 #
     37 # The client program sends a ZIP archive of the file to be
     38 # signed along with the name of a signing credential stored
     39 # on the server. Each credential is a directory containing
     40 # a public-key certificate, private key, and a script to
     41 # perform the appropriate signing operation.
     42 #
     43 # This program unpacks the input ZIP archive, invokes the
     44 # signing script for the specified credential, and sends
     45 # back an output ZIP archive, which typically contains the
     46 # (modified) input file but may also contain additional
     47 # files created by the signing script.
     48 
     49 use strict;
     50 use File::Temp 'tempdir';
     51 use File::Path;
     52 
     53 my $Base = "/opt/signing";
     54 my $Tmpdir = tempdir(CLEANUP => 1);	# Temporary directory
     55 my $Session = $$;
     56 
     57 #
     58 # Main program
     59 #
     60 
     61 # Set up
     62 open(AUDIT, ">>$Base/audit/log");
     63 $| = 1;	# Flush output on every write
     64 
     65 # Record user and client system
     66 my $user = `/usr/ucb/whoami`;
     67 chomp($user);
     68 my ($client) = split(/\s/, $ENV{SSH_CLIENT});
     69 audit("START User=$user Client=$client");
     70 
     71 # Process signing requests
     72 while (<STDIN>) {
     73 	if (/^SIGN (\d+) (\S+) (\S+)/) {
     74 		sign($1, $2, $3);
     75 	} else {
     76 		abnormal("WARNING Unknown command");
     77 	}
     78 }
     79 exit(0);
     80 
     81 #
     82 # get_credential(name)
     83 #
     84 # Verify that the user is allowed to use the named credential and
     85 # return the path to the credential directory. If the user is not
     86 # authorized to use the credential, return undef.
     87 #
     88 sub get_credential {
     89 	my $name = shift;
     90 	my $dir;
     91 
     92 	$dir = "$Base/cred/$2";
     93 	if (!open(F, "<$dir/private")) {
     94 		abnormal("WARNING Credential $name not available");
     95 		$dir = undef;
     96 	}
     97 	close(F);
     98 	return $dir;
     99 }
    100 
    101 #
    102 # sign(size, cred, path)
    103 #
    104 # Sign an individual file.
    105 #
    106 sub sign {
    107 	my ($size, $cred, $path) = @_;
    108 	my ($cred_dir, $msg);
    109 
    110 	# Read input file
    111 	recvfile("$Tmpdir/in.zip", $size) || return;
    112 
    113 	# Check path for use of .. or absolute pathname
    114 	my @comp = split(m:/:, $path);
    115 	foreach my $elem (@comp) {
    116 		if ($elem eq "" || $elem eq "..") {
    117 			abnormal("WARNING Invalid path $path");
    118 			return;
    119 		}
    120 	}
    121 
    122 	# Get credential directory
    123 	$cred_dir = get_credential($cred) || return;
    124 
    125 	# Create work area
    126 	rmtree("$Tmpdir/reloc");
    127 	mkdir("$Tmpdir/reloc");
    128 	chdir("$Tmpdir/reloc");
    129 
    130 	# Read and unpack input ZIP archive
    131 	system("/usr/bin/unzip -qo ../in.zip $path");
    132 
    133 	# Sign input file using credential-specific script
    134 	$msg = `cd $cred_dir; ./sign $Tmpdir/reloc/$path`;
    135 	if ($? != 0) {
    136 		chomp($msg);
    137 		abnormal("WARNING $msg");
    138 		return;
    139 	}
    140 
    141 	# Pack output file(s) in ZIP archive and return
    142 	unlink("../out.zip");
    143 	system("/usr/bin/zip -qr ../out.zip .");
    144 	chdir($Tmpdir);
    145 	my $hash = `digest -a md5 $Tmpdir/reloc/$path`;
    146 	sendfile("$Tmpdir/out.zip", $path) || return;
    147 
    148 	# Audit successful signing
    149 	chomp($hash);
    150 	audit("SIGN $path $cred $hash");
    151 }
    152 
    153 #
    154 # sendfile(file, path)
    155 #
    156 # Send a ZIP archive to the client. This involves sending
    157 # an OK SIGN response that includes the file size, followed by
    158 # the contents of the archive itself.
    159 #
    160 sub sendfile {
    161 	my ($file, $path) = @_;
    162 	my ($size, $bytes);
    163 
    164 	$size = -s $file;
    165 	if (!open(F, "<$file")) {
    166 		abnormal("ERROR Internal read error");
    167 		return (0);
    168 	}
    169 	read(F, $bytes, $size);
    170 	close(F);
    171 	print "OK SIGN $size $path\n";
    172 	syswrite(STDOUT, $bytes, $size);
    173 	return (1);
    174 }
    175 
    176 #
    177 # recvfile(file, size)
    178 #
    179 # Receive a ZIP archive from the client. The caller
    180 # provides the size argument previously obtained from the 
    181 # client request.
    182 #
    183 sub recvfile {
    184 	my ($file, $size) = @_;
    185 	my $bytes;
    186 	
    187 	if (!read(STDIN, $bytes, $size)) {
    188 		abnormal("ERROR No input data");
    189 		return (0);
    190 	}
    191 	if (!open(F, ">$file")) {
    192 		abnormal("ERROR Internal write error");
    193 		return (0);
    194 	}
    195 	syswrite(F, $bytes, $size);
    196 	close(F);
    197 	return (1);
    198 }
    199 
    200 #
    201 # audit(msg)
    202 #
    203 # Create an audit record. All records have this format:
    204 #	[date] [time] [session] [keyword] [other parameters]
    205 # The keywords START and END mark the boundaries of a session.
    206 #
    207 sub audit {
    208 	my ($msg) = @_;
    209 	my ($sec, $min, $hr, $day, $mon, $yr) = localtime(time);
    210 	my $timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d",
    211 		$yr+1900, $mon+1, $day, $hr, $min, $sec);
    212 
    213 	print AUDIT "$timestamp $Session $msg\n";
    214 }
    215 
    216 #
    217 # abnormal(msg)
    218 #
    219 # Respond to an abnormal condition, which may be fatal (ERROR) or
    220 # non-fatal (WARNING). Send the message to the audit error log
    221 # and to the client program. Exit in case of fatal errors.
    222 #
    223 sub abnormal {
    224 	my $msg = shift;
    225 
    226 	audit($msg);
    227 	print("$msg\n");
    228 	exit(1) if ($msg =~ /^ERROR/);
    229 }
    230 
    231 #
    232 # END()
    233 #
    234 # Clean up prior to normal or abnormal exit.
    235 #
    236 sub END {
    237 	audit("END");
    238 	close(AUDIT);
    239 	chdir("");	# so $Tmpdir can be removed
    240 }
    241