xref: /illumos-gate/usr/src/tools/codesign/codesign_server.pl (revision a28480febf31f0e61debac062a55216a98a05a92)
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
49use strict;
50use File::Temp 'tempdir';
51use File::Path;
52
53my $Base = "/opt/signing";
54my $Tmpdir = tempdir(CLEANUP => 1);	# Temporary directory
55my $Session = $$;
56
57#
58# Main program
59#
60
61# Set up
62open(AUDIT, ">>$Base/audit/log");
63$| = 1;	# Flush output on every write
64
65# Record user and client system
66my $user = `/usr/ucb/whoami`;
67chomp($user);
68my ($client) = split(/\s/, $ENV{SSH_CLIENT});
69audit("START User=$user Client=$client");
70
71# Process signing requests
72while (<STDIN>) {
73	if (/^SIGN (\d+) (\S+) (\S+)/) {
74		sign($1, $2, $3);
75	} else {
76		abnormal("WARNING Unknown command");
77	}
78}
79exit(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#
88sub 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#
106sub 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#
160sub 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#
183sub 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#
207sub 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#
223sub 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#
236sub END {
237	audit("END");
238	close(AUDIT);
239	chdir("");	# so $Tmpdir can be removed
240}
241