b92821b362af28bb9f57f5a783aee366cf119705
[monkeysphere-validation-agent.git] / Crypt / Monkeysphere / MSVA.pm
1 # Monkeysphere Validation Agent, Perl version
2 # Copyright © 2010 Daniel Kahn Gillmor <dkg@fifthhorseman.net>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
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.  If not, see <http://www.gnu.org/licenses/>.
16
17 { package Crypt::Monkeysphere::MSVA;
18
19   use strict;
20   use warnings;
21
22   use parent qw(HTTP::Server::Simple::CGI);
23   require Crypt::X509;
24   use Regexp::Common qw /net/;
25   use Convert::ASN1;
26   use MIME::Base64;
27   use IO::Socket;
28   use IO::File;
29   use Socket;
30   use File::Spec;
31   use File::HomeDir;
32   use Config::General;
33   use Crypt::Monkeysphere::MSVA::MarginalUI;
34   use Crypt::Monkeysphere::MSVA::Logger;
35   use Crypt::Monkeysphere::MSVA::Monitor;
36
37   use JSON;
38   use POSIX qw(strftime);
39   # we need the version of GnuPG::Interface that knows about pubkey_data, etc:
40   use GnuPG::Interface 0.42.02;
41
42   my $VERSION = '0.6';
43
44   my $gnupg = GnuPG::Interface->new();
45   $gnupg->options->quiet(1);
46   $gnupg->options->batch(1);
47
48   my %dispatch = (
49                   '/' => { handler => \&noop,
50                            methods => { 'GET' => 1 },
51                          },
52                   '/reviewcert' => { handler => \&reviewcert,
53                                      methods => { 'POST' => 1 },
54                                    },
55                   '/extracerts' => { handler => \&extracerts,
56                                      methods => { 'POST' => 1 },
57                                    },
58                  );
59
60   my $default_keyserver = 'hkp://pool.sks-keyservers.net';
61   my $default_keyserver_policy = 'unlessvalid';
62
63   my $logger = Crypt::Monkeysphere::MSVA::Logger->new($ENV{MSVA_LOG_LEVEL});
64   sub logger {
65     return $logger;
66   }
67
68   my $rsa_decoder = Convert::ASN1->new;
69   $rsa_decoder->prepare(q<
70
71    SEQUENCE {
72         modulus INTEGER,
73         exponent INTEGER
74    }
75           >);
76
77   sub net_server {
78     return 'Net::Server::MSVA';
79   };
80
81   sub msvalog {
82     return $logger->log(@_);
83   };
84
85   sub new {
86     my $class = shift;
87
88     my $port = 0;
89     if (exists $ENV{MSVA_PORT} and $ENV{MSVA_PORT} ne '') {
90       msvalog('debug', "MSVA_PORT set to %s\n", $ENV{MSVA_PORT});
91       $port = $ENV{MSVA_PORT} + 0;
92       die sprintf("not a reasonable port %d", $port) if (($port >= 65536) || $port <= 0);
93     }
94     # start the server on requested port
95     my $self = $class->SUPER::new($port);
96     if (! exists $ENV{MSVA_PORT}) {
97       # we can't pass port 0 to the constructor because it evaluates
98       # to false, so HTTP::Server::Simple just uses its internal
99       # default of 8080.  But if we want to select an arbitrary open
100       # port, we *can* set it here.
101       $self->port(0);
102     }
103
104     $self->{allowed_uids} = {};
105     if (exists $ENV{MSVA_ALLOWED_USERS} and $ENV{MSVA_ALLOWED_USERS} ne '') {
106       msvalog('verbose', "MSVA_ALLOWED_USERS environment variable is set.\nLimiting access to specified users.\n");
107       foreach my $user (split(/ +/, $ENV{MSVA_ALLOWED_USERS})) {
108         my ($name, $passwd, $uid);
109         if ($user =~ /^[0-9]+$/) {
110           $uid = $user + 0; # force to integer
111         } else {
112           ($name,$passwd,$uid) = getpwnam($user);
113         }
114         if (defined $uid) {
115           msvalog('verbose', "Allowing access from user ID %d\n", $uid);
116           $self->{allowed_uids}->{$uid} = $user;
117         } else {
118           msvalog('error', "Could not find user '%d'; not allowing\n", $user);
119         }
120       }
121     } else {
122       # default is to allow access only to the current user
123       $self->{allowed_uids}->{POSIX::getuid()} = 'self';
124     }
125
126     bless ($self, $class);
127     return $self;
128   }
129
130   sub noop {
131     my $self = shift;
132     my $cgi = shift;
133     return '200 OK', { available => JSON::true,
134                        protoversion => 1,
135                      };
136   }
137
138   sub opensshpubkey2key {
139     my $data = shift;
140     # FIXME: do we care that the label matches the type of key?
141     my ($label, $prop) = split(/ +/, $data);
142
143     my $out = parse_rfc4716body($prop);
144
145     return $out;
146   }
147
148   sub rfc47162key {
149     my $data = shift;
150
151     my @goodlines;
152     my $continuation = '';
153     my $state = 'outside';
154     foreach my $line (split(/\n/, $data)) {
155       last if ($state eq 'body' && $line eq '---- END SSH2 PUBLIC KEY ----');
156       if ($state eq 'outside' && $line eq '---- BEGIN SSH2 PUBLIC KEY ----') {
157         $state = 'header';
158         next;
159       }
160       if ($state eq 'header') {
161         $line = $continuation.$line;
162         $continuation = '';
163         if ($line =~ /^(.*)\\$/) {
164           $continuation = $1;
165           next;
166         }
167         if (! ($line =~ /:/)) {
168           $state = 'body';
169         }
170       }
171       push(@goodlines, $line) if ($state eq 'body');
172     }
173
174     msvalog('debug', "Found %d lines of RFC4716 body:\n%s\n", 
175             scalar(@goodlines),
176             join("\n", @goodlines));
177     my $out = parse_rfc4716body(join('', @goodlines));
178
179     return $out;
180   }
181
182   sub parse_rfc4716body {
183     my $data = shift;
184     $data = decode_base64($data) or return undef;
185
186     msvalog('debug', "key properties: %s\n", unpack('H*', $data));
187     my $out = [ ];
188     while (length($data) > 4) {
189       my $size = unpack('N', substr($data, 0, 4));
190       msvalog('debug', "size: 0x%08x\n", $size);
191       return undef if (length($data) < $size + 4);
192       push(@{$out}, substr($data, 4, $size));
193       $data = substr($data, 4 + $size);
194     }
195
196     if ($out->[0] ne "ssh-rsa") {
197       return {error => 'Not an RSA key'};
198     }
199
200     if (scalar(@{$out}) != 3) {
201       return {error => 'Does not contain the right number of bigints for RSA'};
202     }
203
204     return { exponent => Math::BigInt->from_hex('0x'.unpack('H*', $out->[1])),
205              modulus => Math::BigInt->from_hex('0x'.unpack('H*', $out->[2])),
206            } ;
207   }
208
209
210   # return an arrayref of processes which we can detect that have the
211   # given socket open (the socket is specified with its inode)
212   sub getpidswithsocketinode {
213     my $sockid = shift;
214
215     # this appears to be how Linux symlinks open sockets in /proc/*/fd,
216     # as of at least 2.6.26:
217     my $socktarget = sprintf('socket:[%d]', $sockid);
218     my @pids;
219
220     my $procfs;
221     if (opendir($procfs, '/proc')) {
222       foreach my $pid (grep { /^\d+$/ } readdir($procfs)) {
223         my $procdir = sprintf('/proc/%d', $pid);
224         if (-d $procdir) {
225           my $procfds;
226           if (opendir($procfds, sprintf('/proc/%d/fd', $pid))) {
227             foreach my $procfd (grep { /^\d+$/ } readdir($procfds)) {
228               my $fd = sprintf('/proc/%d/fd/%d', $pid, $procfd);
229               if (-l $fd) {
230                 #my ($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($fd);
231                 my $targ = readlink($fd);
232                 push @pids, $pid
233                   if ($targ eq $socktarget);
234               }
235             }
236             closedir($procfds);
237           }
238         }
239       }
240       closedir($procfs);
241     }
242
243     # FIXME: this whole business is very linux-specific, i think.  i
244     # wonder how to get this info in other OSes?
245
246     return \@pids;
247   }
248
249   # return {uid => X, inode => Y}, meaning the numeric ID of the peer
250   # on the other end of $socket, "socket inode" identifying the peer's
251   # open network socket.  each value could be undef if unknown.
252   sub get_client_info {
253     my $socket = shift;
254
255     my $sock = IO::Socket->new_from_fd($socket, 'r');
256     # check SO_PEERCRED -- if this was a TCP socket, Linux
257     # might not be able to support SO_PEERCRED (even on the loopback),
258     # though apparently some kernels (Solaris?) are able to.
259
260     my $clientid;
261     my $remotesocketinode;
262     my $socktype = $sock->sockopt(SO_TYPE) or die "could not get SO_TYPE info";
263     if (defined $socktype) {
264       msvalog('debug', "sockopt(SO_TYPE) = %d\n", $socktype);
265     } else {
266       msvalog('verbose', "sockopt(SO_TYPE) returned undefined.\n");
267     }
268
269     my $peercred = $sock->sockopt(SO_PEERCRED) or die "could not get SO_PEERCRED info";
270     my $client = $sock->peername();
271     my $family = sockaddr_family($client); # should be AF_UNIX (a.k.a. AF_LOCAL) or AF_INET
272
273     msvalog('verbose', "socket family: %d\nsocket type: %d\n", $family, $socktype);
274
275     if ($peercred) {
276       # FIXME: on i386 linux, this appears to be three ints, according to
277       # /usr/include/linux/socket.h.  What about other platforms?
278       my ($pid, $uid, $gid) = unpack('iii', $peercred);
279
280       msvalog('verbose', "SO_PEERCRED: pid: %u, uid: %u, gid: %u\n",
281               $pid, $uid, $gid,
282              );
283       if ($pid != 0 && $uid != 0) { # then we can accept it:
284         $clientid = $uid;
285       }
286       # FIXME: can we get the socket inode as well this way?
287     }
288
289     # another option in Linux would be to parse the contents of
290     # /proc/net/tcp to find the uid of the peer process based on that
291     # information.
292     if (! defined $clientid) {
293       msvalog('verbose', "SO_PEERCRED failed, digging around in /proc/net/tcp\n");
294       my $proto;
295       if ($family == AF_INET) {
296         $proto = '';
297       } elsif ($family == AF_INET6) {
298         $proto = '6';
299       }
300       if (defined $proto) {
301         if ($socktype == &SOCK_STREAM) {
302           $proto = 'tcp'.$proto;
303         } elsif ($socktype == &SOCK_DGRAM) {
304           $proto = 'udp'.$proto;
305         } else {
306           undef $proto;
307         }
308         if (defined $proto) {
309           my ($port, $iaddr) = unpack_sockaddr_in($client);
310           my $iaddrstring = unpack("H*", reverse($iaddr));
311           msvalog('verbose', "Port: %04x\nAddr: %s\n", $port, $iaddrstring);
312           my $remmatch = lc(sprintf("%s:%04x", $iaddrstring, $port));
313           my $infofile = '/proc/net/'.$proto;
314           my $f = new IO::File;
315           if ( $f->open('< '.$infofile)) {
316             my @header = split(/ +/, <$f>);
317             my ($localaddrix, $uidix, $inodeix);
318             my $ix = 0;
319             my $skipcount = 0;
320             while ($ix <= $#header) {
321               $localaddrix = $ix - $skipcount if (lc($header[$ix]) eq 'local_address');
322               $uidix = $ix - $skipcount if (lc($header[$ix]) eq 'uid');
323               $inodeix = $ix - $skipcount if (lc($header[$ix]) eq 'inode');
324               $skipcount++ if (lc($header[$ix]) eq 'tx_queue') or (lc($header[$ix]) eq 'tr'); # these headers don't actually result in a new column during the data rows
325               $ix++;
326             }
327             if (!defined $localaddrix) {
328               msvalog('info', "Could not find local_address field in %s; unable to determine peer UID\n",
329                       $infofile);
330             } elsif (!defined $uidix) {
331               msvalog('info', "Could not find uid field in %s; unable to determine peer UID\n",
332                       $infofile);
333             } elsif (!defined $inodeix) {
334               msvalog('info', "Could not find inode field in %s; unable to determine peer network socket inode\n",
335                       $infofile);
336             } else {
337               msvalog('debug', "local_address: %d; uid: %d\n", $localaddrix,$uidix);
338               while (my @line = split(/ +/,<$f>)) {
339                 if (lc($line[$localaddrix]) eq $remmatch) {
340                   if (defined $clientid) {
341                     msvalog('error', "Warning! found more than one remote uid! (%s and %s\n", $clientid, $line[$uidix]);
342                   } else {
343                     $clientid = $line[$uidix];
344                     $remotesocketinode = $line[$inodeix];
345                     msvalog('info', "remote peer is uid %d (inode %d)\n",
346                             $clientid, $remotesocketinode);
347                   }
348                 }
349               }
350             msvalog('error', "Warning! could not find peer information in %s.  Not verifying.\n", $infofile) unless defined $clientid;
351             }
352           } else { # FIXME: we couldn't read the file.  what should we
353                    # do besides warning?
354             msvalog('info', "Could not read %s; unable to determine peer UID\n",
355                     $infofile);
356           }
357         }
358       }
359     }
360     return { 'uid' => $clientid,
361              'inode' => $remotesocketinode };
362   }
363
364   sub handle_request {
365     my $self = shift;
366     my $cgi  = shift;
367
368     # This is part of a spawned child process.  We don't want the
369     # child process to destroy the update monitor when it terminates.
370     $self->{updatemonitor}->forget();
371     my $clientinfo = get_client_info(select);
372     my $clientuid = $clientinfo->{uid};
373
374     if (defined $clientuid) {
375       # test that this is an allowed user:
376       if (exists $self->{allowed_uids}->{$clientuid}) {
377         msvalog('verbose', "Allowing access from uid %d (%s)\n", $clientuid, $self->{allowed_uids}->{$clientuid});
378       } else {
379         msvalog('error', "MSVA client connection from uid %d, forbidden.\n", $clientuid);
380         printf("HTTP/1.0 403 Forbidden -- peer does not match local user ID\r\nContent-Type: text/plain\r\nDate: %s\r\n\r\nHTTP/1.1 403 Not Found -- peer does not match the local user ID.  Are you sure the agent is running as the same user?\r\n",
381                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),);
382         return;
383       }
384     }
385
386     my $path = $cgi->path_info();
387     my $handler = $dispatch{$path};
388
389     if (ref($handler) eq "HASH") {
390       if (! exists $handler->{methods}->{$cgi->request_method()}) {
391         printf("HTTP/1.0 405 Method not allowed\r\nAllow: %s\r\nDate: %s\r\n",
392                join(', ', keys(%{$handler->{methods}})),
393                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
394       } elsif (ref($handler->{handler}) ne "CODE") {
395         printf("HTTP/1.0 500 Server Error\r\nDate: %s\r\n",
396                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
397       } else {
398         my $data = {};
399         my $ctype = $cgi->content_type();
400         msvalog('verbose', "Got %s %s (Content-Type: %s)\n", $cgi->request_method(), $path, defined $ctype ? $ctype : '**none supplied**');
401         if (defined $ctype) {
402           my @ctypes = split(/; */, $ctype);
403           $ctype = shift @ctypes;
404           if ($ctype eq 'application/json') {
405             $data = from_json($cgi->param('POSTDATA'));
406           }
407         };
408
409         my ($status, $object) = $handler->{handler}($data, $clientinfo);
410         if (ref($object) eq 'HASH' &&
411             ! defined $object->{server}) {
412           $object->{server} = sprintf("MSVA-Perl %s", $VERSION);
413         }
414
415         my $ret = to_json($object);
416         msvalog('info', "returning: %s\n", $ret);
417         printf("HTTP/1.0 %s\r\nDate: %s\r\nContent-Type: application/json\r\n\r\n%s",
418                $status,
419                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
420                $ret);
421       }
422     } else {
423       printf("HTTP/1.0 404 Not Found -- not handled by Monkeysphere validation agent\r\nContent-Type: text/plain\r\nDate: %s\r\n\r\nHTTP/1.0 404 Not Found -- the path:\r\n   %s\r\nis not handled by the MonkeySphere validation agent.\r\nPlease try one of the following paths instead:\r\n\r\n%s\r\n",
424              strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
425              $path, ' * '.join("\r\n * ", keys %dispatch) );
426     }
427   }
428
429   sub keycomp {
430     my $rsakey = shift;
431     my $gpgkey = shift;
432
433     if ($gpgkey->algo_num != 1) {
434       msvalog('verbose', "Monkeysphere only does RSA keys.  This key is algorithm #%d\n", $gpgkey->algo_num);
435     } else {
436       if ($rsakey->{exponent}->bcmp($gpgkey->pubkey_data->[1]) == 0 &&
437           $rsakey->{modulus}->bcmp($gpgkey->pubkey_data->[0]) == 0) {
438         return 1;
439       }
440     }
441     return 0;
442   }
443
444   sub pem2der {
445     my $pem = shift;
446     my @lines = split(/\n/, $pem);
447     my @goodlines = ();
448     my $ready = 0;
449     foreach my $line (@lines) {
450       if ($line eq '-----END CERTIFICATE-----') {
451         last;
452       } elsif ($ready) {
453         push @goodlines, $line;
454       } elsif ($line eq '-----BEGIN CERTIFICATE-----') {
455         $ready = 1;
456       }
457     }
458     msvalog('debug', "%d lines of base64:\n%s\n", $#goodlines + 1, join("\n", @goodlines));
459     return decode_base64(join('', @goodlines));
460   }
461
462   sub der2key {
463     my $rawdata = shift;
464
465     my $cert = Crypt::X509->new(cert => $rawdata);
466
467     my $key = {error => 'I do not know what happened here'};
468
469     if ($cert->error) {
470       $key->{error} = sprintf("Error decoding X.509 certificate: %s", $cert->error);
471     } else {
472       msvalog('verbose', "cert subject: %s\n", $cert->subject_cn());
473       msvalog('verbose', "cert issuer: %s\n", $cert->issuer_cn());
474       msvalog('verbose', "cert pubkey algo: %s\n", $cert->PubKeyAlg());
475       msvalog('verbose', "cert pubkey: %s\n", unpack('H*', $cert->pubkey()));
476
477       if ($cert->PubKeyAlg() ne 'RSA') {
478         $key->{error} = sprintf('public key was algo "%s" (OID %s).  MSVA.pl only supports RSA',
479                                 $cert->PubKeyAlg(), $cert->pubkey_algorithm);
480       } else {
481         msvalog('debug', "decoding ASN.1 pubkey\n");
482         $key = $rsa_decoder->decode($cert->pubkey());
483         if (! defined $key) {
484           msvalog('verbose', "failed to decode %s\n", unpack('H*', $cert->pubkey()));
485           $key = {error => 'failed to decode the public key'};
486         }
487       }
488     }
489     return $key;
490   }
491
492   sub getuid {
493     my $data = shift;
494     if ($data->{context} =~ /^(https|ssh)$/) {
495       $data->{context} = $1;
496       if ($data->{peer} =~ /^($RE{net}{domain})$/) {
497         $data->{peer} = $1;
498         return $data->{context}.'://'.$data->{peer};
499       }
500     }
501   }
502
503   sub get_keyserver_policy {
504     if (exists $ENV{MSVA_KEYSERVER_POLICY} and $ENV{MSVA_KEYSERVER_POLICY} ne '') {
505       if ($ENV{MSVA_KEYSERVER_POLICY} =~ /^(always|never|unlessvalid)$/) {
506         return $1;
507       }
508       msvalog('error', "Not a valid MSVA_KEYSERVER_POLICY):\n  %s\n", $ENV{MSVA_KEYSERVER_POLICY});
509     }
510     return $default_keyserver_policy;
511   }
512
513   sub get_keyserver {
514     # We should read from (first hit wins):
515     # the environment
516     if (exists $ENV{MSVA_KEYSERVER} and $ENV{MSVA_KEYSERVER} ne '') {
517       if ($ENV{MSVA_KEYSERVER} =~ /^(((hkps?|finger|ldap):\/\/)?$RE{net}{domain})$/) {
518         return $1;
519       }
520       msvalog('error', "Not a valid keyserver (from MSVA_KEYSERVER):\n  %s\n", $ENV{MSVA_KEYSERVER});
521     }
522
523     # FIXME: some msva.conf or monkeysphere.conf file (system and user?)
524
525     # or else read from the relevant gnupg.conf:
526     my $gpghome;
527     if (exists $ENV{GNUPGHOME} and $ENV{GNUPGHOME} ne '') {
528       $gpghome = untaint($ENV{GNUPGHOME});
529     } else {
530       $gpghome = File::Spec->catfile(File::HomeDir->my_home, '.gnupg');
531     }
532     my $gpgconf = File::Spec->catfile($gpghome, 'gpg.conf');
533     if (-f $gpgconf) {
534       if (-r $gpgconf) {
535         my %gpgconfig = Config::General::ParseConfig($gpgconf);
536         if ($gpgconfig{keyserver} =~ /^(((hkps?|finger|ldap):\/\/)?$RE{net}{domain})$/) {
537           msvalog('debug', "Using keyserver %s from the GnuPG configuration file (%s)\n", $1, $gpgconf);
538           return $1;
539         } else {
540           msvalog('error', "Not a valid keyserver (from gpg config %s):\n  %s\n", $gpgconf, $gpgconfig{keyserver});
541         }
542       } else {
543         msvalog('error', "The GnuPG configuration file (%s) is not readable\n", $gpgconf);
544       }
545     } else {
546       msvalog('info', "Did not find GnuPG configuration file while looking for keyserver '%s'\n", $gpgconf);
547     }
548
549     # the default_keyserver
550     return $default_keyserver;
551   }
552
553   sub fetch_uid_from_keyserver {
554     my $uid = shift;
555
556     my $cmd = IO::Handle->new();
557     my $out = IO::Handle->new();
558     my $nul = IO::File->new("< /dev/null");
559
560     my $ks = get_keyserver();
561     msvalog('debug', "start ks query to %s for UserID: %s\n", $ks, $uid);
562     my $pid = $gnupg->wrap_call
563       ( handles => GnuPG::Handles->new( command => $cmd, stdout => $out, stderr => $nul ),
564         command_args => [ '='.$uid ],
565         commands => [ '--keyserver',
566                       $ks,
567                       qw( --no-tty --with-colons --search ) ]
568       );
569     while (my $line = $out->getline()) {
570       msvalog('debug', "from ks query: (%d) %s", $cmd->fileno, $line);
571       if ($line =~ /^info:(\d+):(\d+)/ ) {
572         $cmd->print(join(' ', ($1..$2))."\n");
573         msvalog('debug', 'to ks query: '.join(' ', ($1..$2))."\n");
574         last;
575       }
576     }
577     # FIXME: can we do something to avoid hanging forever?
578     waitpid($pid, 0);
579     msvalog('debug', "ks query returns %d\n", POSIX::WEXITSTATUS($?));
580   }
581
582   sub reviewcert {
583     my $data  = shift;
584     my $clientinfo  = shift;
585     return if !ref $data;
586
587     msvalog('verbose', "reviewing data...\n");
588
589     my $status = '200 OK';
590     my $ret =  { valid => JSON::false,
591                  message => 'Unknown failure',
592                };
593
594     my $uid = getuid($data);
595     if ($uid eq []) {
596         msvalog('error', "invalid peer/context: %s/%s\n", $data->{context}, $data->{peer});
597         $ret->{message} = sprintf('invalid peer/context');
598         return $status, $ret;
599     }
600     msvalog('verbose', "context: %s\n", $data->{context});
601     msvalog('verbose', "peer: %s\n", $data->{peer});
602
603     my $key;
604     if (lc($data->{pkc}->{type}) eq 'x509der') {
605       $key = der2key(join('', map(chr, @{$data->{pkc}->{data}})));
606     } elsif (lc($data->{pkc}->{type}) eq 'x509pem') {
607       $key = der2key(pem2der($data->{pkc}->{data}));
608     } elsif (lc($data->{pkc}->{type}) eq 'opensshpubkey') {
609       $key = opensshpubkey2key($data->{pkc}->{data});
610     } elsif (lc($data->{pkc}->{type}) eq 'rfc4716') {
611       $key = rfc47162key($data->{pkc}->{data});
612     } else {
613       $ret->{message} = sprintf("Don't know this public key carrier type: %s", $data->{pkc}->{type});
614       return $status,$ret;
615     }
616
617     if (exists $key->{error}) {
618       $ret->{message} = $key->{error};
619       return $status,$ret;
620     }
621
622     # make sure that the returned integers are Math::BigInts:
623     $key->{exponent} = Math::BigInt->new($key->{exponent}) unless (ref($key->{exponent}));
624     $key->{modulus} = Math::BigInt->new($key->{modulus}) unless (ref($key->{modulus}));
625     msvalog('debug', "pubkey info:\nmodulus: %s\nexponent: %s\n",
626             $key->{modulus}->as_hex(),
627             $key->{exponent}->as_hex(),
628            );
629
630     if ($key->{modulus}->copy()->blog(2) < 1000) {
631       $ret->{message} = sprintf('Public key size is less than 1000 bits (was: %d bits)', $key->{modulus}->copy()->blog(2));
632     } else {
633       $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
634       my $lastloop = 0;
635       my $kspolicy;
636       if (defined $data->{keyserverpolicy} &&
637           $data->{keyserverpolicy} =~ /^(always|never|unlessvalid)$/) {
638         $kspolicy = $1;
639         msvalog("verbose", "using requested keyserver policy: %s\n", $1);
640       } else {
641         $kspolicy = get_keyserver_policy();
642       }
643       msvalog('debug', "keyserver policy: %s\n", $kspolicy);
644       # needed because $gnupg spawns child processes
645       $ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
646       if ($kspolicy eq 'always') {
647         fetch_uid_from_keyserver($uid);
648         $lastloop = 1;
649       } elsif ($kspolicy eq 'never') {
650         $lastloop = 1;
651       }
652       my $foundvalid = 0;
653
654       # fingerprints of keys that are not fully-valid for this User ID, but match
655       # the key from the queried certificate:
656       my @subvalid_key_fprs;
657
658       while (1) {
659         foreach my $gpgkey ($gnupg->get_public_keys('='.$uid)) {
660           my $validity = '-';
661           foreach my $tryuid ($gpgkey->user_ids) {
662             if ($tryuid->as_string eq $uid) {
663               $validity = $tryuid->validity;
664             }
665           }
666           # treat primary keys just like subkeys:
667           foreach my $subkey ($gpgkey, @{$gpgkey->subkeys}) {
668             my $primarymatch = keycomp($key, $subkey);
669             if ($primarymatch) {
670               if ($subkey->usage_flags =~ /a/) {
671                 msvalog('verbose', "key matches, and 0x%s is authentication-capable\n", $subkey->hex_id);
672                 if ($validity =~ /^[fu]$/) {
673                   $foundvalid = 1;
674                   msvalog('verbose', "...and it matches!\n");
675                   $ret->{valid} = JSON::true;
676                   $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
677                 } else {
678                   push(@subvalid_key_fprs, { fpr => $subkey->fingerprint, val => $validity }) if $lastloop;
679                 }
680               } else {
681                 msvalog('verbose', "key matches, but 0x%s is not authentication-capable\n", $subkey->hex_id);
682               }
683             }
684           }
685         }
686         if ($lastloop) {
687           last;
688         } else {
689           fetch_uid_from_keyserver($uid) if (!$foundvalid);
690           $lastloop = 1;
691         }
692       }
693
694       # only show the marginal UI if the UID of the corresponding
695       # key is not fully valid.
696       if (!$foundvalid) {
697         my $resp = Crypt::Monkeysphere::MSVA::MarginalUI->ask_the_user($gnupg,
698                                                                        $uid,
699                                                                        \@subvalid_key_fprs,
700                                                                        getpidswithsocketinode($clientinfo->{inode}),
701                                                                        $logger);
702         msvalog('info', "response: %s\n", $resp);
703         if ($resp) {
704           $ret->{valid} = JSON::true;
705           $ret->{message} = sprintf('Manually validated "%s" through the OpenPGP Web of Trust.', $uid);
706         }
707       }
708     }
709     return $status, $ret;
710   }
711
712   sub pre_loop_hook {
713     my $self = shift;
714     my $server = shift;
715
716     $self->spawn_master_subproc($server);
717   }
718
719   sub master_subprocess_died {
720     my $self = shift;
721     my $server = shift;
722     my $subproc_return = shift;
723
724     my $exitstatus = POSIX::WEXITSTATUS($subproc_return);
725     msvalog('verbose', "Subprocess %d terminated; exiting %d.\n", $self->{child_pid}, $exitstatus);
726     $server->set_exit_status($exitstatus);
727     $server->server_close();
728   }
729
730   sub child_dies {
731     my $self = shift;
732     my $pid = shift;
733     my $server = shift;
734
735     msvalog('debug', "Subprocess %d terminated.\n", $pid);
736
737     if (exists $self->{updatemonitor} &&
738         defined $self->{updatemonitor}->getchildpid() &&
739         $self->{updatemonitor}->getchildpid() == $pid) {
740       my $exitstatus = POSIX::WEXITSTATUS($?);
741       msvalog('verbose', "Update monitoring process (%d) terminated with code %d.\n", $pid, $exitstatus);
742       if (0 == $exitstatus) {
743         msvalog('info', "Reloading MSVA due to update request.\n");
744         # sending self a SIGHUP:
745         kill(1, $$);
746       } else {
747         msvalog('error', "Update monitoring process (%d) died unexpectedly with code %d.\nNo longer monitoring for updates; please send HUP manually.\n", $pid, $exitstatus);
748         # it died for some other weird reason; should we respawn it?
749
750         # FIXME: i'm worried that re-spawning would create a
751         # potentially abusive loop, if there are legit, repeatable
752         # reasons for the failure.
753
754 #        $self->{updatemonitor}->spawn();
755
756         # instead, we'll just avoid trying to kill the next process with this PID:
757         $self->{updatemonitor}->forget();
758       }
759     } elsif (exists $self->{child_pid} &&
760              ($self->{child_pid} == 0 ||
761               $self->{child_pid} == $pid)) {
762       $self->master_subprocess_died($server, $?);
763     }
764   }
765
766   # use sparingly!  We want to keep taint mode around for the data we
767   # get over the network.  this is only here because we want to treat
768   # the command line arguments differently for the subprocess.
769   sub untaint {
770     my $x = shift;
771     $x =~ /^(.*)$/ ;
772     return $1;
773   }
774
775   sub post_bind_hook {
776     my $self = shift;
777     my $server = shift;
778
779     $server->{server}->{leave_children_open_on_hup} = 1;
780
781     my $socketcount = @{ $server->{server}->{sock} };
782     if ( $socketcount != 1 ) {
783       msvalog('error', "%d sockets open; should have been 1.\n", $socketcount);
784       $server->set_exit_status(10);
785       $server->server_close();
786     }
787     my $port = @{ $server->{server}->{sock} }[0]->sockport();
788     if ((! defined $port) || ($port < 1) || ($port >= 65536)) {
789       msvalog('error', "got nonsense port: %d.\n", $port);
790       $server->set_exit_status(11);
791       $server->server_close();
792     }
793     if ((exists $ENV{MSVA_PORT}) && (($ENV{MSVA_PORT} + 0) != $port)) {
794       msvalog('error', "Explicitly requested port %d, but got port: %d.", ($ENV{MSVA_PORT}+0), $port);
795       $server->set_exit_status(13);
796       $server->server_close();
797     }
798     $self->port($port);
799     $self->{updatemonitor} = Crypt::Monkeysphere::MSVA::Monitor->new($logger);
800   }
801
802   sub spawn_master_subproc {
803     my $self = shift;
804     my $server = shift;
805
806     if ((exists $ENV{MSVA_CHILD_PID}) && ($ENV{MSVA_CHILD_PID} ne '')) {
807       # this is most likely a re-exec.
808       msvalog('info', "This appears to be a re-exec, continuing with child pid %d\n", $ENV{MSVA_CHILD_PID});
809       $self->{child_pid} = $ENV{MSVA_CHILD_PID} + 0;
810     } elsif ($#ARGV >= 0) {
811       $self->{child_pid} = 0; # indicate that we are planning to fork.
812       # avoid ignoring SIGCHLD right before we fork.
813       $SIG{CHLD} = sub {
814         my $val;
815         while (defined($val = POSIX::waitpid(-1, POSIX::WNOHANG)) && $val > 0) {
816           $self->child_dies($val, $server);
817         }
818       };
819       my $fork = fork();
820       if (! defined $fork) {
821         msvalog('error', "could not fork\n");
822       } else {
823         if ($fork) {
824           msvalog('debug', "Child process has PID %d\n", $fork);
825           $self->{child_pid} = $fork;
826           $ENV{MSVA_CHILD_PID} = $fork;
827         } else {
828           msvalog('verbose', "PID %d executing: \n", $$);
829           for my $arg (@ARGV) {
830             msvalog('verbose', " %s\n", $arg);
831           }
832           # untaint the environment for the subprocess
833           # see: https://labs.riseup.net/code/issues/2461
834           foreach my $e (keys %ENV) {
835             $ENV{$e} = untaint($ENV{$e});
836           }
837           my @args;
838           foreach (@ARGV) {
839             push @args, untaint($_);
840           }
841           # restore default SIGCHLD handling:
842           $SIG{CHLD} = 'DEFAULT';
843           $ENV{MONKEYSPHERE_VALIDATION_AGENT_SOCKET} = sprintf('http://localhost:%d', $self->port);
844           exec(@args) or exit 111;
845         }
846       }
847     } else {
848       printf("MONKEYSPHERE_VALIDATION_AGENT_SOCKET=http://localhost:%d;\nexport MONKEYSPHERE_VALIDATION_AGENT_SOCKET;\n", $self->port);
849       # FIXME: consider daemonizing here to behave more like
850       # ssh-agent.  maybe avoid backgrounding by setting
851       # MSVA_NO_BACKGROUND.
852     };
853   }
854
855   sub extracerts {
856     my $data = shift;
857
858     return '500 not yet implemented', { };
859   }
860
861   1;
862 }