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