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 }