[backend] do a checksig if the sign fails, automatically restart the job if the check...
[opensuse:build-service.git] / src / backend / bs_signer
1 #!/usr/bin/perl -w
2 #
3 # Copyright (c) 2009 Michael Schroeder, Novell Inc.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as
7 # published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program (see the file COPYING); if not, write to the
16 # Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18 #
19 ################################################################
20 #
21 # Sign the built packages
22 #
23
24 BEGIN {
25   my ($wd) = $0 =~ m-(.*)/- ;
26   $wd ||= '.';
27   unshift @INC,  "$wd/build";
28   unshift @INC,  "$wd";
29 }
30
31 use POSIX;
32 use Data::Dumper;
33 use Digest::MD5 ();
34 use Fcntl qw(:DEFAULT :flock);
35 use XML::Structured ':bytes';
36 use Build;
37 use Storable;
38
39 use BSConfig;
40 use BSRPC;
41 use BSUtil;
42 use BSXML;
43 use BSVerify;
44
45 use strict;
46
47 my $bsdir = $BSConfig::bsdir || "/srv/obs";
48
49 BSUtil::mkdir_p_chown($bsdir, $BSConfig::bsuser, $BSConfig::bsgroup);
50 BSUtil::drop_privs_to($BSConfig::bsuser, $BSConfig::bsgroup);
51
52 my $rundir = $BSConfig::rundir || "$BSConfig::bsdir/run";
53 my $jobsdir = "$BSConfig::bsdir/jobs";
54 my $eventdir = "$BSConfig::bsdir/events";
55 my $myeventdir = "$eventdir/signer";
56 my $uploaddir = "$BSConfig::bsdir/upload";
57
58 my $maxchild = 4;
59
60
61 sub readblk {
62   my ($fd, $blk, $num, $blksize) = @_;
63   $blksize ||= 2048;
64   sysseek($fd, $blk * $blksize, SEEK_SET) || die("sysseek: $!\n");
65   $num ||= 1;
66   $num *= $blksize;
67   my $ret = '';
68   (sysread($fd, $ret, $num) || 0) == $num || die("sysread: $!\n");
69   return $ret;
70 }
71
72 sub writeblk {
73   my ($fd, $blk, $cnt) = @_;
74   my $blksize = 2048;
75   sysseek($fd, $blk * $blksize, SEEK_SET) || die("sysseek: $!\n");
76   (syswrite($fd, $cnt) || 0) == length($cnt) || die("syswrite: $!\n");
77 }
78
79 sub signfilter {
80   my ($content, @signargs) = @_;
81   local (*RH, *WH);
82   local *P;
83   pipe(RH, WH) || die("pipe: $!\n");
84   my $pid;
85   $pid = open(P, '-|');
86   die("fork: $!\n") unless defined $pid;
87   if (!$pid) {
88     close WH;
89     open(STDIN, "<&RH");
90     exec('sign', '-d', @signargs);
91     die("sign: $!\n");
92   }
93   close RH;
94   print WH $content;
95   close WH;
96   my $ret = '';
97   $ret .= $_ while <P>;
98   close(P) || die("sign: $?\n");
99   return $ret;
100 }
101
102 sub readisodir {
103   my ($fd, $dirpos) = @_;
104
105   my $dirblk = readblk($fd, $dirpos);
106   my $dirlen = unpack('@10V', $dirblk);
107   die("bad directory len\n") if $dirlen & 0x7ff;
108   my $sp_bytes_skip = 0;
109   my @contents;
110   my $entryoff = 0;
111   while ($dirlen) {
112     if ($dirblk eq '' || unpack('C', $dirblk) == 0) {
113       $dirlen -= 0x800;
114       $dirblk = readblk($fd, ++$dirpos) if $dirlen;
115       $entryoff = 0;
116       next;
117     }
118     my ($l, $fpos, $flen, $f, $inter, $nl) = unpack('C@2V@10V@25Cv@32C', $dirblk);
119     die("bad dir entry\n") if $l > length($dirblk);
120     if ($f & 2) {
121       $dirblk = substr($dirblk, $l);
122       $entryoff += $l;
123       next;
124     }
125     die("associated file\n") if $f & 4;
126     die("interleaved file\n") if $inter;
127     die("bad dir entry\n") if !$nl || $nl + 33 > length($dirblk);
128     $nl++ unless $nl & 1;
129     my $e = substr($dirblk, $nl + 33, $l - $nl - 33);
130     if (length($e) >= 7 && substr($e, 0, 2) eq 'SP') {
131       ($sp_bytes_skip) = unpack('@6C', $e);
132     } else {
133       $e = substr($e, $sp_bytes_skip) if $sp_bytes_skip;
134     }
135     my ($ce_len, $ce_blk, $ce_off) = (0, 0, 0);
136     my $fname = '';
137     my $nmf = 0;
138     while ($e ne '') {
139       if (length($e) <= 2) {
140         last unless $ce_len;
141         $e = readblk($fd, $ce_blk);
142         $e = substr($e, $ce_off, $ce_len);
143         $ce_len = 0;
144         next;
145       }
146       if (substr($e, 0, 2) eq 'CE') {
147         ($ce_blk, $ce_off, $ce_len) = unpack('@4V@12V@20V', $e);
148       } elsif (substr($e, 0, 2) eq 'NM') {
149         my $nml = (unpack('@2C', $e))[0] - 5;
150         $fname = '' unless $nmf & 1;
151         ($nmf) = unpack('@4C', $e);
152         $fname .= substr($e, 5, $nml) if $nml > 0;
153       }
154       $e = substr($e, (unpack('@2C', $e))[0]);
155     }
156     push @contents, [$fname, $fpos, $flen, $dirpos, $entryoff];
157     $dirblk = substr($dirblk, $l);
158     $entryoff += $l;
159   }
160   return @contents;
161 }
162
163 sub signisofiles {
164   my ($fd, @signargs) = @_;
165
166   my $signed = 0;
167   my $vol = readblk($fd, 16);
168   die("primary volume descriptor missing\n") if substr($vol, 0, 6) ne "\001CD001";
169   my ($path_table_size, $path_table_pos) = unpack('@132V@140V', $vol);
170   my $path_table = readblk($fd, $path_table_pos * 2048, $path_table_size, 1);
171   while ($path_table ne '') {
172     my ($l, $dirpos) = unpack('C@2V', $path_table);
173     die("empty dir in path table\n") unless $l;
174     $path_table = substr($path_table, 8 + $l + ($l & 1));
175     my @c = readisodir($fd, $dirpos);
176     for my $e (@c) {
177       #print "$e->[0] $e->[1] $e->[2] $e->[3] $e->[4]\n";
178       if ($e->[0] =~ /^(.*).asc$/i && $e->[2] == 2048) {
179         my $n = $1;
180         my $signfile = readblk($fd, $e->[1]);
181         next if substr($signfile, 0, 8) ne "sIGnMe!\n";
182         my $len = hex(substr($signfile, 8, 8));
183         my $sum = hex(substr($signfile, 16, 8));
184         my @se = grep {$_->[0] =~ /^\Q$n\E$/i && $_->[2] == $len} @c;
185         die("don't know which file to sign: $e->[0]\n") unless @se == 1;
186         my $sf = readblk($fd, $se[0]->[1], ($len + 0x7ff) >> 11);
187         $sf = substr($sf, 0, $len);
188         die("selected wrong file\n") if $sum != unpack("%32C*", $sf);
189         my $sig = signfilter($sf);
190         die("returned signature is too big\n") if length($sig) > 2048;
191         # replace old content
192         writeblk($fd, $e->[1], $sig . ("\0" x (2048 - length($sig))));
193         my $dirblk = readblk($fd, $e->[3]);
194         # patch in new content len
195         substr($dirblk, $e->[4] + 10, 4) = pack('V', length($sig));
196         writeblk($fd, $e->[3], $dirblk);
197         $signed++;
198       }
199     }
200   }
201   return $signed;
202 }
203
204 sub retagmd5iso {
205   my ($fd) = @_;
206   my $blk = readblk($fd, 0, 17);
207   die("primary volume descriptor missing\n") if substr($blk, 0x8000, 6) ne "\001CD001";
208   my $tags = ';'.substr($blk, 0x8373, 0x200);
209   return unless $tags =~ /;md5sum=[0-9a-fA-F]{32}/;
210   print "updating md5sum tag\n";
211   substr($blk, 0x8373, 0x200) = ' ' x 0x200;
212   my $numblks = unpack("V", substr($blk, 0x8050, 4));
213   die("bad block number\n") if $numblks < 17;
214   my $md5 = Digest::MD5->new;
215   $md5->add($blk);
216   $numblks -= 17;
217   my $blkno = 16;
218   while ($numblks-- > 0) {
219     my $b = readblk($fd, ++$blkno);
220     $md5->add($b);
221   }
222   $md5 = $md5->hexdigest;
223   $tags =~ s/;md5sum=[0-9a-fA-F]{32}/;md5sum=$md5/;
224   substr($blk, 0x8373, 0x200) = substr($tags, 1);
225   writeblk($fd, 16, substr($blk, 0x8000, 0x800));
226 }
227
228 sub signiso {
229   my ($file, @signargs) = @_;
230   local *ISO;
231   open(ISO, '+<', $file) || die("$file: $!\n");
232   my $signed = signisofiles(\*ISO);
233   retagmd5iso(\*ISO) if $signed;
234   close(ISO) || die("close $file: $!\n");
235 }
236
237 sub signjob {
238   my ($job, $arch) = @_;
239
240   print "signing $arch/$job\n";
241   local *F;
242   if (! -e "$jobsdir/$arch/$job") {
243     print "no such job\n";
244     return undef;
245   }
246   if (! -e "$jobsdir/$arch/$job:status") {
247     print "job is not done\n";
248     return undef;
249   }
250   my $jobstatus = BSUtil::lockopenxml(\*F, '<', "$jobsdir/$arch/$job:status", $BSXML::jobstatus);
251   # finished can be removed here later, but running jobs shall not be lost on code update.
252   if ($jobstatus->{'code'} ne 'finished' && $jobstatus->{'code'} ne 'signing') {
253     print "job is not assigned for signing\n";
254     close F;
255     return undef;
256   }
257   my $jobdir = "$jobsdir/$arch/$job:dir";
258   die("jobdir does not exist\n") unless -d $jobdir;
259   my $info = readxml("$jobsdir/$arch/$job", $BSXML::buildinfo);
260   my $projid = $info->{'project'};
261   my @files = sort(ls($jobdir));
262   my @signfiles = grep {/\.(?:d?rpm|sha256|iso)$/} @files;
263   if (@signfiles) {
264     my @signargs;
265     push @signargs, '--project', $projid if $BSConfig::sign_project;
266     my $param = {
267       'uri' => "$BSConfig::srcserver/getsignkey",
268       'timeout' => 60,
269     };
270     my $signkey = BSRPC::rpc($param, undef, "project=$projid");
271     if ($signkey) {
272       mkdir_p($uploaddir);
273       writestr("$uploaddir/signer.$$", undef, $signkey);
274       push @signargs, '-P', "$uploaddir/signer.$$";
275     }
276     for my $signfile (@signfiles) {
277       if ($info->{'file'} eq '_aggregate' && ($signfile =~ /\.d?rpm$/)) {
278         # special aggregate handling: remove old sigs
279         system('rpm', '--delsign', "$jobdir/$signfile") && warn("delsign $jobdir/$signfile failed: $?\n");
280       }
281       if ($signfile =~ /\.iso$/) {
282         signiso("$jobdir/$signfile", @signargs);
283         next;
284       }
285       my @signmode;
286       @signmode = ('-r') if $signfile =~ /\.drpm$/;
287       if (system($BSConfig::sign, @signargs, @signmode, "$jobdir/$signfile")) {
288         unlink("$uploaddir/signer.$$") if $signkey;
289         close F;
290         print("sign failed: $? - checking digest\n");
291         if (system('rpm', '--checksig', '--nodigest', "$jobdir/$signfile")) {
292           print("rpm checksig failed: $? - restarting job\n");
293           BSUtil::cleandir($jobdir);
294           rmdir($jobdir);
295           unlink("$jobsdir/$arch/$job:status");
296           close F;
297           return undef;
298         }
299         die("sign $jobdir/$signfile failed\n");
300       }
301     }
302     unlink("$uploaddir/signer.$$") if $signkey;
303
304     # we have changed the file ids, thus we need to re-create
305     # the .bininfo file
306     my $bininfo = {};
307     for my $file (@files) {
308       next unless $file =~ /\.(?:rpm|deb)$/;
309       my @s = stat("$jobdir/$file");
310       next unless @s;
311       my $id = "$s[9]/$s[7]/$s[1]";
312       my $data = Build::query("$jobdir/$file", 'evra' => 1);
313       eval {
314         BSVerify::verify_nevraquery($data);
315       };
316       die("$jobdir/$file: $@") if $@;
317       $bininfo->{$id} = $data;
318     }
319     Storable::nstore($bininfo, "$jobdir/.bininfo") if %$bininfo;
320   }
321   
322   # write finished job status and release lock
323   $jobstatus->{'code'} = 'finished';
324   writexml("$jobsdir/$arch/.$job:status", "$jobsdir/$arch/$job:status", $jobstatus, $BSXML::jobstatus);
325   close F;
326
327   return 1;
328 }
329
330 sub ping {
331   my ($arch) = @_;
332   local *F;
333   if (sysopen(F, "$eventdir/$arch/.ping", POSIX::O_WRONLY|POSIX::O_NONBLOCK)) {
334     syswrite(F, 'x');
335     close(F);
336   }
337 }
338
339 sub signevent {
340   my ($event, $ev) = @_;
341
342   rename("$myeventdir/$event", "$myeventdir/${event}::inprogress");
343   my $job = $ev->{'job'};
344   my $arch = $ev->{'arch'};
345   my $res;
346   eval {
347     $res = signjob($job, $arch);
348   };
349   if ($@) {
350     warn("sign failed: $@");
351     rename("$myeventdir/${event}::inprogress", "$myeventdir/$event");
352     return;
353   } elsif ($res) {
354     my $ev = {'type' => 'built', 'arch' => $arch, 'job' => $job};
355     writexml("$eventdir/$arch/.finished:$job$$", "$eventdir/$arch/finished:$job", $ev, $BSXML::event);
356     ping($arch);
357   }
358   unlink("$myeventdir/${event}::inprogress");
359 }
360
361 $| = 1;
362 $SIG{'PIPE'} = 'IGNORE';
363 BSUtil::restartexit($ARGV[0], 'signer', "$rundir/bs_signer", "$myeventdir/.ping");
364 print "starting build service signer\n";
365
366 # get lock
367 mkdir_p($rundir);
368 open(RUNLOCK, '>>', "$rundir/bs_signer.lock") || die("$rundir/bs_signer.lock: $!\n");
369 flock(RUNLOCK, LOCK_EX | LOCK_NB) || die("signer is already running!\n");
370 utime undef, undef, "$rundir/bs_signer.lock";
371
372 die("sign program is not configured!\n") unless $BSConfig::sign;
373
374 mkdir_p($myeventdir);
375 if (!-p "$myeventdir/.ping") {
376   POSIX::mkfifo("$myeventdir/.ping", 0666) || die("$myeventdir/.ping: $!");
377   chmod(0666, "$myeventdir/.ping");
378 }
379 sysopen(PING, "$myeventdir/.ping", POSIX::O_RDWR) || die("$myeventdir/.ping: $!");
380
381 for my $event (grep {s/::inprogress$//s} ls($myeventdir)) {
382   rename("$myeventdir/${event}::inprogress", "$myeventdir/$event");
383 }
384
385 my %chld;
386 my $pid;
387
388 while (1) {
389   # drain ping pipe
390   my $dummy;
391   fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
392   1 while (sysread(PING, $dummy, 1024, 0) || 0) > 0; 
393   fcntl(PING,F_SETFL,0);
394
395   my @events = ls($myeventdir);
396   @events = grep {!/^\./} @events;
397
398   for my $event (@events) {
399     last if -e "$rundir/bs_signer.exit";
400     last if -e "$rundir/bs_signer.restart";
401
402     my $ev = readxml("$myeventdir/$event", $BSXML::event, 1);
403     if (!$ev) {
404       unlink("$myeventdir/$event");
405       next;
406     }
407     if ($ev->{'type'} ne 'built') {
408       print "unknown event type: $ev->{'type'}\n";
409       unlink("$myeventdir/$event");
410       next;
411     }
412     if (!$maxchild || $maxchild == 1) {
413       signevent($event, $ev);
414       next;
415     }
416     if (!($pid = xfork())) {
417       signevent($event, $ev);
418       exit(0);
419     }
420     $chld{$pid} = 1;
421     while (($pid = waitpid(-1, defined($maxchild) && keys(%chld) > $maxchild ? 0 : POSIX::WNOHANG)) > 0) {
422       delete $chld{$pid};
423     }
424   }
425
426   if (%chld) {
427     while (($pid = waitpid(-1, 0)) > 0) {
428       delete $chld{$pid};
429     }   
430   }
431
432   if (-e "$rundir/bs_signer.exit") {
433     unlink("$rundir/bs_signer.exit");
434     print "exiting...\n";
435     exit(0);
436   }
437   if (-e "$rundir/bs_signer.restart") {
438     unlink("$rundir/bs_signer.restart");
439     print "restarting...\n";
440     exec($0);
441     die("$0: $!\n");
442   }
443   print "waiting for an event...\n";
444   sysread(PING, $dummy, 1, 0);
445 }