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