Merge remote branch 'enw/marginalUI-perl' into marginalUI-perl
[monkeysphere-validation-agent.git] / msva-perl
1 #!/usr/bin/perl -wT
2
3 # Monkeysphere Validation Agent, Perl version
4 # Copyright © 2010 Daniel Kahn Gillmor <dkg@fifthhorseman.net>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19 use warnings;
20 use strict;
21
22
23 { package MSVA;
24
25 use Data::Dumper;
26   use parent qw(HTTP::Server::Simple::CGI);
27   require Crypt::X509;
28   use Regexp::Common qw /net/;
29   use Convert::ASN1;
30   use MIME::Base64;
31   use IO::Socket;
32   use IO::File;
33   use Socket;
34
35   use JSON;
36   use POSIX qw(strftime);
37   # we need the version of GnuPG::Interface that knows about pubkey_data, etc:
38   use GnuPG::Interface 0.42.02;
39
40   my $version = '0.1';
41
42   my $gnupg = GnuPG::Interface->new();
43
44   my %dispatch = (
45                   '/' => { handler => \&noop,
46                            methods => { 'GET' => 1 },
47                          },
48                   '/reviewcert' => { handler => \&reviewcert,
49                                      methods => { 'POST' => 1 },
50                                    },
51                   '/extracerts' => { handler => \&extracerts,
52                                      methods => { 'POST' => 1 },
53                                    },
54                  );
55
56 # Net::Server log_level goes from 0 to 4
57 # this is scaled to match.
58   my %loglevels = (
59                    'silent' => 0,
60                    'quiet' => 0.25,
61                    'fatal' => 0.5,
62                    'error' => 1,
63                    'info' => 2,
64                    'verbose' => 3,
65                    'debug' => 4,
66                    'debug1' => 4,
67                    'debug2' => 5,
68                    'debug3' => 6,
69                   );
70
71   my $rsa_decoder = Convert::ASN1->new;
72   $rsa_decoder->prepare(q<
73
74    SEQUENCE {
75         modulus INTEGER,
76         exponent INTEGER
77    }
78           >);
79
80   sub msvalog {
81     my $msglevel = shift;
82
83     my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
84     $level = $loglevels{error} if (! defined $level);
85
86     if ($loglevels{lc($msglevel)} <= $level) {
87       printf STDERR @_;
88     }
89   };
90
91   sub get_log_level {
92     my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
93     $level = $loglevels{error} if (! defined $level);
94     return $level;
95   }
96
97   sub net_server {
98     return 'Net::Server::MSVA';
99   };
100
101   sub new {
102     my $class = shift;
103
104     my $port = 0;
105     if (exists $ENV{MSVA_PORT}) {
106       $port = $ENV{MSVA_PORT} + 0;
107       die sprintf("not a reasonable port %d", $port) if (($port >= 65536) || $port <= 0);
108     }
109     # start the server on requested port
110     my $self = $class->SUPER::new($port);
111     if (! exists $ENV{MSVA_PORT}) {
112       # we can't pass port 0 to the constructor because it evaluates
113       # to false, so HTTP::Server::Simple just uses its internal
114       # default of 8080.  But if we want to select an arbitrary open
115       # port, we *can* set it here.
116       $self->port(0);
117     }
118
119     $self->{allowed_uids} = {};
120     if (exists $ENV{MSVA_ALLOWED_USERS}) {
121       msvalog('verbose', "MSVA_ALLOWED_USERS environment variable is set.\nLimiting access to specified users.\n");
122       foreach my $user (split(/ +/, $ENV{MSVA_ALLOWED_USERS})) {
123         my ($name, $passwd, $uid);
124         if ($user =~ /^[0-9]+$/) {
125           $uid = $user + 0; # force to integer
126         } else {
127           ($name,$passwd,$uid) = getpwnam($user);
128         }
129         if (defined $uid) {
130           msvalog('verbose', "Allowing access from user ID %d\n", $uid);
131           $self->{allowed_uids}->{$uid} = $user;
132         } else {
133           msvalog('error', "Could not find user '%d'; not allowing\n", $user);
134         }
135       }
136     } else {
137       # default is to allow access only to the current user
138       $self->{allowed_uids}->{POSIX::getuid()} = 'self';
139     }
140
141     bless ($self, $class);
142     return $self;
143   }
144
145   sub noop {
146     my $self = shift;
147     my $cgi = shift;
148     return '200 OK', { available => JSON::true,
149                        protoversion => 1,
150                        server => "MSVA-Perl ".$version };
151   }
152
153   # returns an empty list if bad key found.
154   sub parse_openssh_pubkey {
155     my $data = shift;
156     my ($label, $prop) = split(/ +/, $data);
157     $prop = decode_base64($prop) or return ();
158
159     msvalog('debug', "key properties: %s\n", unpack('H*', $prop));
160     my @out;
161     while (length($prop) > 4) {
162       my $size = unpack('N', substr($prop, 0, 4));
163       msvalog('debug', "size: 0x%08x\n", $size);
164       return () if (length($prop) < $size + 4);
165       push(@out, substr($prop, 4, $size));
166       $prop = substr($prop, 4 + $size);
167     }
168     return () if ($label ne $out[0]);
169     return @out;
170   }
171
172   # return the numeric ID of the peer on the other end of $socket,
173   # returning undef if unknown.
174   sub get_remote_peer_id {
175     my $socket = shift;
176
177     my $sock = IO::Socket->new_from_fd($socket, 'r');
178     # check SO_PEERCRED -- if this was a TCP socket, Linux
179     # might not be able to support SO_PEERCRED (even on the loopback),
180     # though apparently some kernels (Solaris?) are able to.
181
182     my $remotepeerid;
183     my $socktype = $sock->sockopt(SO_TYPE) or die "could not get SO_TYPE info";
184     if (defined $socktype) {
185       msvalog('debug', "sockopt(SO_TYPE) = %d\n", $socktype);
186     } else {
187       msvalog('verbose', "sockopt(SO_TYPE) returned undefined.\n");
188     }
189
190     my $peercred = $sock->sockopt(SO_PEERCRED) or die "could not get SO_PEERCRED info";
191     my $remotepeer = $sock->peername();
192     my $family = sockaddr_family($remotepeer); # should be AF_UNIX (a.k.a. AF_LOCAL) or AF_INET
193
194     msvalog('verbose', "socket family: %d\nsocket type: %d\n", $family, $socktype);
195
196     if ($peercred) {
197       # FIXME: on i386 linux, this appears to be three ints, according to
198       # /usr/include/linux/socket.h.  What about other platforms?
199       my ($pid, $uid, $gid) = unpack('iii', $peercred);
200
201       msvalog('verbose', "SO_PEERCRED: pid: %u, uid: %u, gid: %u\n",
202               $pid, $uid, $gid,
203              );
204       if ($pid != 0 && $uid != 0) { # then we can accept it:
205         $remotepeerid = $uid;
206       }
207     }
208
209     # another option in Linux would be to parse the contents of
210     # /proc/net/tcp to find the uid of the peer process based on that
211     # information.
212     if (! defined $remotepeerid) {
213       my $proto;
214       if ($family == AF_INET) {
215         $proto = '';
216       } elsif ($family == AF_INET6) {
217         $proto = '6';
218       }
219       if (defined $proto) {
220         if ($socktype == &SOCK_STREAM) {
221           $proto = 'tcp'.$proto;
222         } elsif ($socktype == &SOCK_DGRAM) {
223           $proto = 'udp'.$proto;
224         } else {
225           undef $proto;
226         }
227         if (defined $proto) {
228           my ($port, $iaddr) = unpack_sockaddr_in($remotepeer);
229           my $iaddrstring = unpack("H*", reverse($iaddr));
230           msvalog('verbose', "Port: %04x\nAddr: %s\n", $port, $iaddrstring);
231           my $remmatch = lc(sprintf("%s:%04x", $iaddrstring, $port));
232           my $infofile = '/proc/net/'.$proto;
233           my $f = new IO::File;
234           if ( $f->open('< '.$infofile)) {
235             my @header = split(/ +/, <$f>);
236             my ($localaddrix, $uidix);
237             my $ix = 0;
238             my $skipcount = 0;
239             while ($ix <= $#header) {
240               $localaddrix = $ix - $skipcount if (lc($header[$ix]) eq 'local_address');
241               $uidix = $ix - $skipcount if (lc($header[$ix]) eq 'uid');
242               $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
243               $ix++;
244             }
245             if (!defined $localaddrix) {
246               msvalog('info', "Could not find local_address field in %s; unable to determine peer UID\n",
247                       $infofile);
248             } elsif (!defined $uidix) {
249               msvalog('info', "Could not find uid field in %s; unable to determine peer UID\n",
250                       $infofile);
251             } else {
252               msvalog('debug', "local_address: %d; uid: %d\n", $localaddrix,$uidix);
253               while (my @line = split(/ +/,<$f>)) {
254                 if (lc($line[$localaddrix]) eq $remmatch) {
255                   if (defined $remotepeerid) {
256                     msvalog('error', "Warning! found more than one remote uid! (%s and %s\n", $remotepeerid, $line[$uidix]);
257                   } else {
258                     $remotepeerid = $line[$uidix];
259                     msvalog('info', "remote peer is uid %d\n",
260                             $remotepeerid);
261                   }
262                 }
263               }
264             msvalog('error', "Warning! could not find peer information in %s.  Not verifying.\n", $infofile) unless defined $remotepeerid;
265             }
266           } else { # FIXME: we couldn't read the file.  what should we
267                    # do besides warning?
268             msvalog('info', "Could not read %s; unable to determine peer UID\n",
269                     $infofile);
270           }
271         }
272       }
273     }
274     return $remotepeerid;
275   }
276
277   sub handle_request {
278     my $self = shift;
279     my $cgi  = shift;
280
281     my $remotepeerid =  get_remote_peer_id(select);
282
283     if (defined $remotepeerid) {
284       # test that this is an allowed user:
285       if (exists $self->{allowed_uids}->{$remotepeerid}) {
286         msvalog('verbose', "Allowing access from uid %d (%s)\n", $remotepeerid, $self->{allowed_uids}->{$remotepeerid});
287       } else {
288         msvalog('error', "MSVA client connection from uid %d, forbidden.\n", $remotepeerid);
289         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",
290                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),);
291         return;
292       }
293     }
294
295     my $path = $cgi->path_info();
296     my $handler = $dispatch{$path};
297
298     if (ref($handler) eq "HASH") {
299       if (! exists $handler->{methods}->{$cgi->request_method()}) {
300         printf("HTTP/1.0 405 Method not allowed\r\nAllow: %s\r\nDate: %s\r\n",
301                join(', ', keys(%{$handler->{methods}})),
302                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
303       } elsif (ref($handler->{handler}) ne "CODE") {
304         printf("HTTP/1.0 500 Server Error\r\nDate: %s\r\n",
305                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
306       } else {
307         my $data = {};
308         my $ctype = $cgi->content_type();
309         msvalog('verbose', "Got %s %s (Content-Type: %s)\n", $cgi->request_method(), $path, defined $ctype ? $ctype : '**none supplied**');
310         if (defined $ctype) {
311           my @ctypes = split(/; */, $ctype);
312           $ctype = shift @ctypes;
313           if ($ctype eq 'application/json') {
314             $data = from_json($cgi->param('POSTDATA'));
315           }
316         };
317
318         my ($status, $object) = $handler->{handler}($data);
319         my $ret = to_json($object);
320         msvalog('info', "returning: %s\n", $ret);
321         printf("HTTP/1.0 %s\r\nDate: %s\r\nContent-Type: application/json\r\n\r\n%s",
322                $status,
323                strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
324                $ret);
325       }
326     } else {
327       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",
328              strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
329              $path, ' * '.join("\r\n * ", keys %dispatch) );
330     }
331   }
332
333   sub keycomp {
334     my $rsakey = shift;
335     my $gpgkey = shift;
336
337     if ($gpgkey->algo_num != 1) {
338       msvalog('verbose', "Monkeysphere only does RSA keys.  This key is algorithm #%d\n", $gpgkey->algo_num);
339     } else {
340       if ($rsakey->{exponent}->bcmp($gpgkey->pubkey_data->[1]) == 0 &&
341           $rsakey->{modulus}->bcmp($gpgkey->pubkey_data->[0]) == 0) {
342         return 1;
343       }
344     }
345     return 0;
346   }
347
348   sub getuid {
349     my $data = shift;
350     if ($data->{context} =~ /^(https|ssh)$/) {
351       $data->{context} = $1;
352       if ($data->{peer} =~ /^($RE{net}{domain})$/) {
353         $data->{peer} = $1;
354         return $data->{context}.'://'.$data->{peer};
355       }
356     }
357   }
358
359   sub reviewcert {
360     my $data  = shift;
361     return if !ref $data;
362
363     my $status = '200 OK';
364     my $ret =  { valid => JSON::false,
365                  message => 'Unknown failure',
366                };
367
368     my $uid = getuid($data);
369     if ($uid eq []) {
370         msvalog('error', "invalid peer/context: %s/%s\n", $data->{context}, $data->{peer});
371         $ret->{message} = sprintf('invalid peer/context');
372         return $status, $ret;
373     }
374
375     my $rawdata = join('', map(chr, @{$data->{pkc}->{data}}));
376     my $cert = Crypt::X509->new(cert => $rawdata);
377     msvalog('verbose', "cert subject: %s\n", $cert->subject_cn());
378     msvalog('verbose', "cert issuer: %s\n", $cert->issuer_cn());
379     msvalog('verbose', "cert pubkey algo: %s\n", $cert->PubKeyAlg());
380     msvalog('verbose', "cert pubkey: %s\n", unpack('H*', $cert->pubkey()));
381
382     if ($cert->PubKeyAlg() ne 'RSA') {
383       $ret->{message} = sprintf('public key was algo "%s" (OID %s).  MSVA.pl only supports RSA',
384                                 $cert->PubKeyAlg(), $cert->pubkey_algorithm);
385     } else {
386       my $key = $rsa_decoder->decode($cert->pubkey());
387       if ($key) {
388         # make sure that the returned integers are Math::BigInts:
389         $key->{exponent} = Math::BigInt->new($key->{exponent}) unless (ref($key->{exponent}));
390         $key->{modulus} = Math::BigInt->new($key->{modulus}) unless (ref($key->{modulus}));
391         msvalog('debug', "cert info:\nmodulus: %s\nexponent: %s\n",
392                 $key->{modulus}->as_hex(),
393                 $key->{exponent}->as_hex(),
394                );
395
396         if ($key->{modulus}->copy()->blog(2) < 1000) { # FIXME: this appears to be the full pubkey, including DER overhead
397           $ret->{message} = sprintf('public key size is less than 1000 bits (was: %d bits)', $cert->pubkey_size());
398         } else {
399           $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
400
401           # needed because $gnupg spawns child processes
402           $ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
403           # FIXME: check keyservers?
404           foreach my $gpgkey ($gnupg->get_public_keys('='.$uid)) {
405             my $notvalid = 1;
406             my $marginal = 0;
407             if ($gpgkey->usage_flags =~ /A/) {
408               # we're only interested in keys that might have a valid
409               # authentication key/subkey:
410               foreach my $tryuid ($gpgkey->user_ids) {
411                 if ($tryuid->as_string eq $uid) {
412                   $notvalid = 0
413                     if ($tryuid->validity eq 'f' ||
414                         $tryuid->validity eq 'u');
415                   $marginal = 1
416                     if ($tryuid->validity eq 'm');
417                 }
418               }
419               if ($marginal and $notvalid) {
420                 # MJGOINS
421               } elsif ($notvalid) {
422                 msvalog('verbose', "got a key that was not fully-valid for UID %s\n", $uid);
423                 msvalog('debug', Dumper($gpgkey));
424               } else {
425                 if ($gpgkey->usage_flags =~ /a/) {
426                   msvalog('verbose', "primary key 0x%s is authentication-capable\n", $gpgkey->hex_id);
427                   if (keycomp($key, $gpgkey)) {
428                     msvalog('verbose', "...and it matches!\n");
429                     $ret->{valid} = JSON::true;
430                     $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
431                   }
432                 }
433                 foreach my $subkey ($gpgkey->subkeys) {
434                   msvalog('verbose', "subkey 0x%s is authentication-capable\n", $subkey->hex_id);
435                   if (keycomp($key, $subkey)) {
436                     msvalog('verbose', "...and it matches!\n");
437                     $ret->{valid} = JSON::true;
438                     $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
439                   }
440                 }
441               }
442             }
443           }
444
445         }
446       } else {
447         msvalog('error', "failed to decode %s\n", unpack('H*', $cert->pubkey()));
448         $ret->{message} = sprintf('failed to decode the public key', $uid);
449       }
450     }
451
452     return $status, $ret;
453   }
454
455   sub child_dies {
456     my $self = shift;
457     my $pid = shift;
458     my $server = shift;
459
460     msvalog('debug', "Subprocess %d terminated.\n", $pid);
461
462     if (exists $self->{child_pid} &&
463         ($self->{child_pid} == 0 ||
464          $self->{child_pid} == $pid)) {
465       my $exitstatus = POSIX::WEXITSTATUS($?);
466       msvalog('verbose', "Subprocess %d terminated; exiting %d.\n", $pid, $exitstatus);
467       $server->set_exit_status($exitstatus);
468       $server->server_close();
469     }
470   }
471
472   # use sparingly!  We want to keep taint mode around for the data we
473   # get over the network.  this is only here because we want to treat
474   # the command line arguments differently for the subprocess.
475   sub untaint {
476     my $x = shift;
477     $x =~ /^(.*)$/ ;
478     return $1;
479   }
480
481   sub post_bind_hook {
482     my $self = shift;
483     my $server = shift;
484
485     my $socketcount = @{ $server->{server}->{sock} };
486     if ( $socketcount != 1 ) {
487       msvalog('error', "%d sockets open; should have been 1.", $socketcount);
488       $server->set_exit_status(10);
489       $server->server_close();
490     }
491     my $port = @{ $server->{server}->{sock} }[0]->sockport();
492     if ((! defined $port) || ($port < 1) || ($port >= 65536)) {
493       msvalog('error', "got nonsense port: %d.", $port);
494       $server->set_exit_status(11);
495       $server->server_close();
496     }
497     if ((exists $ENV{MSVA_PORT}) && (($ENV{MSVA_PORT} + 0) != $port)) {
498       msvalog('error', "Explicitly requested port %d, but got port: %d.", ($ENV{MSVA_PORT}+0), $port);
499       $server->set_exit_status(13);
500       $server->server_close();
501     }
502     $self->port($port);
503
504     my $argcount = @ARGV;
505     if ($argcount) {
506       $self->{child_pid} = 0; # indicate that we are planning to fork.
507       my $fork = fork();
508       if (! defined $fork) {
509         msvalog('error', "could not fork\n");
510       } else {
511         if ($fork) {
512           msvalog('debug', "Child process has PID %d\n", $fork);
513           $self->{child_pid} = $fork;
514         } else {
515           msvalog('verbose', "PID %d executing: \n", $$);
516           for my $arg (@ARGV) {
517             msvalog('verbose', " %s\n", $arg);
518           }
519           $ENV{PATH} = untaint($ENV{PATH});
520           my @args;
521           foreach (@ARGV) {
522             push @args, untaint($_);
523           }
524           # restore default SIGCHLD handling:
525           $SIG{CHLD} = 'DEFAULT';
526           $ENV{MONKEYSPHERE_VALIDATION_AGENT_SOCKET} = sprintf('http://localhost:%d', $self->port);
527           exec(@args) or exit 111;
528         }
529       }
530     } else {
531       printf("MONKEYSPHERE_VALIDATION_AGENT_SOCKET=http://localhost:%d;\nexport MONKEYSPHERE_VALIDATION_AGENT_SOCKET;\n", $self->port);
532       # FIXME: consider daemonizing here to behave more like
533       # ssh-agent.  maybe avoid backgrounding by setting
534       # MSVA_NO_BACKGROUND.
535     };
536   }
537
538   sub extracerts {
539     my $data = shift;
540
541     return '500 not yet implemented', { };
542   }
543
544   1;
545 }
546
547 my $server = MSVA->new();
548 $server->run(host=>'localhost',
549              log_level=>MSVA::get_log_level(),
550              user => POSIX::geteuid(),  # explicitly choose regular user and group (avoids spew)
551              group => POSIX::getegid(),
552              msva=>$server);
553 __END__
554
555 =head1 NAME
556
557 msva-perl - Perl implementation of a Monkeysphere Validation Agent
558
559 =head1 SYNOPSIS
560
561   msva-perl [ COMMAND [ ARGS ... ] ]
562
563 =head1 ABSTRACT
564
565 msva-perl provides a Perl implementation of the Monkeysphere
566 Validation Agent, a certificate validation service.
567
568 =head1 INTRODUCTION
569
570 The Monkeysphere Validation Agent offers a local service for tools to
571 validate certificates (both X.509 and OpenPGP) and other public keys.
572
573 Clients of the validation agent query it with a public key carrier (a
574 raw public key, or some flavor of certificate), the supposed name of
575 the remote peer offering the pubkey, and the context in which the
576 validation check is relevant (e.g. ssh, https, etc).
577
578 The validation agent then tells the client whether it was able to
579 successfully validate the peer's use of the public key in the given
580 context.
581
582 msva-perl relies on monkeysphere(1), which uses the user's OpenPGP web
583 of trust to validate the peer's use of public keys.
584
585 =head1 USAGE
586
587 Launched with no arguments, msva-perl simply runs and listens forever.
588
589 Launched with arguments, it sets up a listener, spawns a subprocess
590 using the supplied command and arguments, but with the
591 MONKEYSPHERE_VALIDATION_AGENT_SOCKET environment variable set to refer
592 to its listener.  When the subprocess terminates, msva-perl tears down
593 the listener and exits as well, returning the same value as the
594 subprocess.
595
596 This is a similar invocation pattern to that of ssh-agent(1).
597
598 =head1 ENVIRONMENT VARIABLES
599
600 msva-perl is configured by means of environment variables.
601
602 =over 4
603
604 =item MSVA_LOG_LEVEL
605
606 msva-perl logs messages about its operation to stderr.  MSVA_LOG_LEVEL
607 controls its verbosity, and should be one of (in increasing
608 verbosity): silent, quiet, fatal, error, info, verbose, debug, debug1,
609 debug2, debug3.  Default is 'error'.
610
611 =item MSVA_ALLOWED_USERS
612
613 If your system is capable of it, msva-perl tries to figure out the
614 owner of the connecting client.  If MSVA_ALLOWED_USERS is unset,
615 msva-perl will only permit connections from the user msva is running
616 as.  If you set MSVA_ALLOWED_USERS, msva-perl will treat it as a list
617 of local users (by name or user ID) who are allowed to connect.
618
619 =item MSVA_PORT
620
621 msva-perl listens on a local TCP socket to facilitate access.  You can
622 choose what port to bind to by setting MSVA_PORT.  Default is to bind
623 on an arbitrary open port.
624
625 =back
626
627 =head1 COMMUNICATION PROTOCOL DETAILS
628
629 Communications with the Monkeysphere Validation Agent are in the form
630 of JSON requests over plain HTTP.  Responses from the agent are also
631 JSON objects.  For details on the structure of the requests and
632 responses, please see
633 http://web.monkeysphere.info/validation-agent/protocol
634
635 =head1 SECURITY CONSIDERATIONS
636
637 msva-perl deliberately binds to the loopback adapter (via named lookup
638 of "localhost") so that remote users do not get access to the daemon.
639 On systems (like Linux) which report ownership of TCP sockets in
640 /proc/net/tcp, msva-perl will refuse access from random users (see
641 MSVA_ALLOWED_USERS above).
642
643 =head1 SEE ALSO
644
645 monkeysphere(1), monkeysphere(7), ssh-agent(1)
646
647 =head1 BUGS AND FEEDBACK
648
649 Bugs or feature requests for msva-perl should be filed with the
650 Monkeysphere project's bug tracker at
651 https://labs.riseup.net/code/projects/monkeysphere/issues/
652
653 =head1 AUTHORS AND CONTRIBUTORS
654
655 Daniel Kahn Gillmor E<lt>dkg@fifthhorseman.net<gt>
656
657 The Monkeysphere Team http://web.monkeysphere.info/
658
659 =head1 COPYRIGHT AND LICENSE
660
661 Copyright © Daniel Kahn Gillmor and others from the Monkeysphere team.
662 msva-perl is free software, distributed under the GNU Public License,
663 version 3 or later.
664