#!/usr/perl5/bin/perl # # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (the "License"). # You may not use this file except in compliance with the License. # # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE # or http://www.opensolaris.org/os/licensing. # See the License for the specific language governing permissions # and limitations under the License. # # When distributing Covered Code, include this CDDL HEADER in each # file and include the License file at usr/src/OPENSOLARIS.LICENSE. # If applicable, add the following below this CDDL HEADER, with the # fields enclosed by brackets "[]" replaced with your own identifying # information: Portions Copyright [yyyy] [name of copyright owner] # # CDDL HEADER END # # # ident "%Z%%M% %I% %E% SMI" # # Copyright 2007 Sun Microsystems, Inc. All rights reserved. # Use is subject to license terms. # # Server program for code signing server # # This program implements an ssh-based service to add digital # signatures to files. The sshd_config file on the server # contains an entry like the following to invoke this program: # # Subsystem codesign /opt/signing/bin/server # # The client program sends a ZIP archive of the file to be # signed along with the name of a signing credential stored # on the server. Each credential is a directory containing # a public-key certificate, private key, and a script to # perform the appropriate signing operation. # # This program unpacks the input ZIP archive, invokes the # signing script for the specified credential, and sends # back an output ZIP archive, which typically contains the # (modified) input file but may also contain additional # files created by the signing script. use strict; use File::Temp 'tempdir'; use File::Path; my $Base = "/opt/signing"; my $Tmpdir = tempdir(CLEANUP => 1); # Temporary directory my $Session = $$; # # Main program # # Set up open(AUDIT, ">>$Base/audit/log"); $| = 1; # Flush output on every write # Record user and client system my $user = `/usr/ucb/whoami`; chomp($user); my ($client) = split(/\s/, $ENV{SSH_CLIENT}); audit("START User=$user Client=$client"); # Process signing requests while () { if (/^SIGN (\d+) (\S+) (\S+)/) { sign($1, $2, $3); } else { abnormal("WARNING Unknown command"); } } exit(0); # # get_credential(name) # # Verify that the user is allowed to use the named credential and # return the path to the credential directory. If the user is not # authorized to use the credential, return undef. # sub get_credential { my $name = shift; my $dir; $dir = "$Base/cred/$2"; if (!open(F, "<$dir/private")) { abnormal("WARNING Credential $name not available"); $dir = undef; } close(F); return $dir; } # # sign(size, cred, path) # # Sign an individual file. # sub sign { my ($size, $cred, $path) = @_; my ($cred_dir, $msg); # Read input file recvfile("$Tmpdir/in.zip", $size) || return; # Check path for use of .. or absolute pathname my @comp = split(m:/:, $path); foreach my $elem (@comp) { if ($elem eq "" || $elem eq "..") { abnormal("WARNING Invalid path $path"); return; } } # Get credential directory $cred_dir = get_credential($cred) || return; # Create work area rmtree("$Tmpdir/reloc"); mkdir("$Tmpdir/reloc"); chdir("$Tmpdir/reloc"); # Read and unpack input ZIP archive system("/usr/bin/unzip -qo ../in.zip $path"); # Sign input file using credential-specific script $msg = `cd $cred_dir; ./sign $Tmpdir/reloc/$path`; if ($? != 0) { chomp($msg); abnormal("WARNING $msg"); return; } # Pack output file(s) in ZIP archive and return unlink("../out.zip"); system("/usr/bin/zip -qr ../out.zip ."); chdir($Tmpdir); my $hash = `digest -a md5 $Tmpdir/reloc/$path`; sendfile("$Tmpdir/out.zip", $path) || return; # Audit successful signing chomp($hash); audit("SIGN $path $cred $hash"); } # # sendfile(file, path) # # Send a ZIP archive to the client. This involves sending # an OK SIGN response that includes the file size, followed by # the contents of the archive itself. # sub sendfile { my ($file, $path) = @_; my ($size, $bytes); $size = -s $file; if (!open(F, "<$file")) { abnormal("ERROR Internal read error"); return (0); } read(F, $bytes, $size); close(F); print "OK SIGN $size $path\n"; syswrite(STDOUT, $bytes, $size); return (1); } # # recvfile(file, size) # # Receive a ZIP archive from the client. The caller # provides the size argument previously obtained from the # client request. # sub recvfile { my ($file, $size) = @_; my $bytes; if (!read(STDIN, $bytes, $size)) { abnormal("ERROR No input data"); return (0); } if (!open(F, ">$file")) { abnormal("ERROR Internal write error"); return (0); } syswrite(F, $bytes, $size); close(F); return (1); } # # audit(msg) # # Create an audit record. All records have this format: # [date] [time] [session] [keyword] [other parameters] # The keywords START and END mark the boundaries of a session. # sub audit { my ($msg) = @_; my ($sec, $min, $hr, $day, $mon, $yr) = localtime(time); my $timestamp = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $yr+1900, $mon+1, $day, $hr, $min, $sec); print AUDIT "$timestamp $Session $msg\n"; } # # abnormal(msg) # # Respond to an abnormal condition, which may be fatal (ERROR) or # non-fatal (WARNING). Send the message to the audit error log # and to the client program. Exit in case of fatal errors. # sub abnormal { my $msg = shift; audit($msg); print("$msg\n"); exit(1) if ($msg =~ /^ERROR/); } # # END() # # Clean up prior to normal or abnormal exit. # sub END { audit("END"); close(AUDIT); chdir(""); # so $Tmpdir can be removed }