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 }