OpenSolaris_b135/tools/codesign/codesign_server.pl

#!/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 (<STDIN>) {
	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
}