wrote GnuPGKey_to_OpenSSH_pub
[monkeysphere-validation-agent.git] / msva-perl
index 3ace79f5b0a24ac88d28800c8f0ca29da7f61c46..befaa8d735d5f2114876e54e3390f7d00258f30b 100755 (executable)
--- a/msva-perl
+++ b/msva-perl
 use warnings;
 use strict;
 
 use warnings;
 use strict;
 
-{
-  package MSVA;
-
-  use parent qw(HTTP::Server::Simple::CGI);
-  require Crypt::X509;
-  use Convert::ASN1;
-  use MIME::Base64;
-  use IO::Socket;
-  use IO::File;
-  use Socket;
-  use Net::Server::Fork;
-
-  use JSON;
-  use POSIX qw(strftime);
-
-  my %dispatch = (
-                  '/' => { handler => \&noop,
-                           methods => { 'GET' => 1 },
-                         },
-                  '/reviewcert' => { handler => \&reviewcert,
-                                     methods => { 'POST' => 1 },
-                                   },
-                  '/extracerts' => { handler => \&extracerts,
-                                     methods => { 'POST' => 1 },
-                                   },
-                 );
-
-  my %loglevels = (
-                   'silent' => 1,
-                   'quiet' => 2,
-                   'fatal' => 3,
-                   'error' => 4,
-                   'info' => 5,
-                   'verbose' => 6,
-                   'debug' => 7,
-                   'debug1' => 7,
-                   'debug2' => 8,
-                   'debug3' => 9,
-                  );
-
-  my $rsa_decoder = Convert::ASN1->new;
-  $rsa_decoder->prepare(q<
-
-   SEQUENCE {
-        modulus INTEGER,
-        exponent INTEGER
-   }
-          >);
-
-  sub msvalog {
-    my $msglevel = shift;
-
-    my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
-    $level = $loglevels{info} if (! defined $level);
-
-    if ($loglevels{lc($msglevel)} <= $level) {
-      printf STDERR @_;
-    }
-  };
-
-  sub net_server {
-    return 'Net::Server::Fork';
-  };
-
-  sub new {
-    my $class = shift;
-
-    my $port = 8901;
-    if (exists $ENV{MSVA_PORT}) {
-      $port = $ENV{MSVA_PORT} + 0;
-      die sprintf("not a reasonable port %d", $port) if (($port >= 65536) || $port <= 0);
-    }
-    # start the server on port 8901
-    my $self = $class->SUPER::new($port);
-
-    $self->{allowed_uids} = {};
-    if (exists $ENV{MSVA_ALLOWED_USERS}) {
-      msvalog('verbose', "MSVA_ALLOWED_USERS environment variable is set.\nLimiting access to specified users.\n");
-      foreach my $user (split(/ +/, $ENV{MSVA_ALLOWED_USERS})) {
-        my ($name, $passwd, $uid);
-        if ($user =~ /^[0-9]+$/) {
-          $uid = $user + 0; # force to integer
-        } else {
-          ($name,$passwd,$uid) = getpwnam($user);
-        }
-        if (defined $uid) {
-          msvalog('verbose', "Allowing access from user ID %d\n", $uid);
-          $self->{allowed_uids}->{$uid} = $user;
-        } else {
-          msvalog('error', "Could not find user '%d'; not allowing\n", $user);
-        }
-      }
-    } else {
-      # default is to allow access only to the current user
-      $self->{allowed_uids}->{POSIX::getuid()} = 'self';
-    }
-
-    bless ($self, $class);
-    return $self;
-  }
-
-  sub noop {
-    my $self = shift;
-    my $cgi = shift;
-    return '200 OK', { available => JSON::true,
-                       protoversion => 1,
-                       server => "MSVA-Perl 0.1" };
-  }
-
-  # returns an empty list if bad key found.
-  sub parse_openssh_pubkey {
-    my $data = shift;
-    my ($label, $prop) = split(/ +/, $data);
-    $prop = decode_base64($prop) or return ();
-
-    msvalog('debug', "key properties: %s\n", unpack('H*', $prop));
-    my @out;
-    while (length($prop) > 4) {
-      my $size = unpack('N', substr($prop, 0, 4));
-      msvalog('debug', "size: 0x%08x\n", $size);
-      return () if (length($prop) < $size + 4);
-      push(@out, substr($prop, 4, $size));
-      $prop = substr($prop, 4 + $size);
-    }
-    return () if ($label ne $out[0]);
-    return @out;
-  }
-
-  # return the numeric ID of the peer on the other end of $socket,
-  # returning undef if unknown.
-  sub get_remote_peer_id {
-    my $socket = shift;
-
-    my $sock = IO::Socket->new_from_fd($socket, 'r');
-    # check SO_PEERCRED -- if this was a TCP socket, Linux
-    # might not be able to support SO_PEERCRED (even on the loopback),
-    # though apparently some kernels (Solaris?) are able to.
-
-    my $remotepeerid;
-    my $socktype = $sock->sockopt(SO_TYPE) or die "could not get SO_TYPE info";
-    if (defined $socktype) {
-      msvalog('debug', "sockopt(SO_TYPE) = %d\n", $socktype);
-    } else {
-      msvalog('verbose', "sockopt(SO_TYPE) returned undefined.\n");
-    }
-
-    my $peercred = $sock->sockopt(SO_PEERCRED) or die "could not get SO_PEERCRED info";
-    my $remotepeer = $sock->peername();
-    my $family = sockaddr_family($remotepeer); # should be AF_UNIX (a.k.a. AF_LOCAL) or AF_INET
-
-    msvalog('verbose', "socket family: %d\nsocket type: %d\n", $family, $socktype);
-
-    if ($peercred) {
-      # FIXME: on i386 linux, this appears to be three ints, according to
-      # /usr/include/linux/socket.h.  What about other platforms?
-      my ($pid, $uid, $gid) = unpack('iii', $peercred);
-
-      msvalog('verbose', "SO_PEERCRED: pid: %u, uid: %u, gid: %u\n",
-              $pid, $uid, $gid,
-             );
-      if ($pid != 0 && $uid != 0) { # then we can accept it:
-        $remotepeerid = $uid;
-      }
-    }
-
-    # another option in Linux would be to parse the contents of
-    # /proc/net/tcp to find the uid of the peer process based on that
-    # information.
-    if (! defined $remotepeerid) {
-      my $proto;
-      if ($family == AF_INET) {
-        $proto = '';
-      } elsif ($family == AF_INET6) {
-        $proto = '6';
-      }
-      if (defined $proto) {
-        if ($socktype == &SOCK_STREAM) {
-          $proto = 'tcp'.$proto;
-        } elsif ($socktype == &SOCK_DGRAM) {
-          $proto = 'udp'.$proto;
-        } else {
-          undef $proto;
-        }
-        if (defined $proto) {
-          my ($port, $iaddr) = unpack_sockaddr_in($remotepeer);
-          my $iaddrstring = unpack("H*", reverse($iaddr));
-          msvalog('verbose', "Port: %04x\nAddr: %s\n", $port, $iaddrstring);
-          my $remmatch = lc(sprintf("%s:%04x", $iaddrstring, $port));
-          my $infofile = '/proc/net/'.$proto;
-          my $f = new IO::File;
-          if ( $f->open('< '.$infofile)) {
-            my @header = split(/ +/, <$f>);
-            my ($localaddrix, $uidix);
-            my $ix = 0;
-            my $skipcount = 0;
-            while ($ix <= $#header) {
-              $localaddrix = $ix - $skipcount if (lc($header[$ix]) eq 'local_address');
-              $uidix = $ix - $skipcount if (lc($header[$ix]) eq 'uid');
-              $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
-              $ix++;
-            }
-            if (!defined $localaddrix) {
-              msvalog('info', "Could not find local_address field in %s; unable to determine peer UID\n",
-                      $infofile);
-            } elsif (!defined $uidix) {
-              msvalog('info', "Could not find uid field in %s; unable to determine peer UID\n",
-                      $infofile);
-            } else {
-              msvalog('debug', "local_address: %d; uid: %d\n", $localaddrix,$uidix);
-              while (my @line = split(/ +/,<$f>)) {
-                if (lc($line[$localaddrix]) eq $remmatch) {
-                  if (defined $remotepeerid) {
-                    msvalog('error', "Warning! found more than one remote uid! (%s and %s\n", $remotepeerid, $line[$uidix]);
-                  } else {
-                    $remotepeerid = $line[$uidix];
-                    msvalog('info', "remote peer is uid %d\n",
-                            $remotepeerid);
-                  }
-                }
-              }
-            msvalog('error', "Warning! could not find peer information in %s.  Not verifying.\n", $infofile) unless defined $remotepeerid;
-            }
-          } else { # FIXME: we couldn't read the file.  what should we
-                   # do besides warning?
-            msvalog('info', "Could not read %s; unable to determine peer UID\n",
-                    $infofile);
-          }
-        }
-      }
-    }
-    return $remotepeerid;
-  }
-
-  sub handle_request {
-    my $self = shift;
-    my $cgi  = shift;
-
-    my $remotepeerid =  get_remote_peer_id(select);
-
-    if (defined $remotepeerid) {
-      # test that this is an allowed user:
-      if (exists $self->{allowed_uids}->{$remotepeerid}) {
-        msvalog('verbose', "Allowing access from uid %d (%s)\n", $remotepeerid, $self->{allowed_uids}->{$remotepeerid});
-      } else {
-        msvalog('error', "MSVA client connection from uid %d, forbidden.\n", $remotepeerid);
-        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",
-               strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),);
-        return;
-      }
-    }
-
-    my $path = $cgi->path_info();
-    my $handler = $dispatch{$path};
-
-    if (ref($handler) eq "HASH") {
-      if (! exists $handler->{methods}->{$cgi->request_method()}) {
-        printf("HTTP/1.0 405 Method not allowed\r\nAllow: %s\r\nDate: %s\r\n",
-               join(', ', keys(%{$handler->{methods}})),
-               strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
-      } elsif (ref($handler->{handler}) ne "CODE") {
-        printf("HTTP/1.0 500 Server Error\r\nDate: %s\r\n",
-               strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
-      } else {
-        my $data = {};
-        my $ctype = $cgi->content_type();
-        msvalog('verbose', "Got %s %s (Content-Type: %s)\n", $cgi->request_method(), $path, defined $ctype ? $ctype : '**none supplied**');
-        if (defined $ctype) {
-          my @ctypes = split(/; */, $ctype);
-          $ctype = shift @ctypes;
-          if ($ctype eq 'application/json') {
-            $data = from_json($cgi->param('POSTDATA'));
-          }
-        };
-
-        my ($status, $object) = $handler->{handler}($data);
-        my $ret = to_json($object);
-        msvalog('info', "returning: %s\n", $ret);
-        printf("HTTP/1.0 %s\r\nDate: %s\r\nContent-Type: application/json\r\n\r\n%s",
-               $status,
-               strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
-               $ret);
-      }
-    } else {
-      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",
-             strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
-             $path, ' * '.join("\r\n * ", keys %dispatch) );
-    }
-  }
-
-
-
-  sub reviewcert {
-    my $data  = shift;
-    return if !ref $data;
-
-    my $uid = $data->{context}.'://'.$data->{peer};
-
-    my $rawdata = join('', map(chr, @{$data->{pkc}->{data}}));
-    my $cert = Crypt::X509->new(cert => $rawdata);
-    msvalog('verbose', "cert subject: %s\n", $cert->subject_cn());
-    msvalog('verbose', "cert issuer: %s\n", $cert->issuer_cn());
-    msvalog('verbose', "cert pubkey algo: %s\n", $cert->PubKeyAlg());
-    msvalog('verbose', "cert pubkey: %s\n", unpack('H*', $cert->pubkey()));
-
-    my $status = '200 OK';
-    my $ret =  { valid => JSON::false,
-                 message => 'Unknown failure',
-               };
-    if ($cert->PubKeyAlg() ne 'RSA') {
-      $ret->{message} = sprintf('public key was algo "%s" (OID %s).  MSVA.pl only supports RSA',
-                                $cert->PubKeyAlg(), $cert->pubkey_algorithm);
-    } else {
-      my $key = $rsa_decoder->decode($cert->pubkey());
-      if ($key) {
-        # make sure that the returned integers are Math::BigInts:
-        $key->{exponent} = Math::BigInt->new($key->{exponent}) unless (ref($key->{exponent}));
-        $key->{modulus} = Math::BigInt->new($key->{modulus}) unless (ref($key->{modulus}));
-        msvalog('debug', "cert info:\nmodulus: %s\nexponent: %s\n",
-                $key->{modulus}->as_hex(),
-                $key->{exponent}->as_hex(),
-               );
-
-        if ($key->{modulus}->copy()->blog(2) < 1000) { # FIXME: this appears to be the full pubkey, including DER overhead
-          $ret->{message} = sprintf('public key size is less than 1000 bits (was: %d bits)', $cert->pubkey_size());
-        } else {
-          $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
-
-          my $fh;
-          # clean up the path for taint-check mode:
-          $ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
-
-          open($fh, '-|', 'monkeysphere', 'keys-for-userid', $uid);
-          while(<$fh>) {
-            my @keyinfo = parse_openssh_pubkey($_);
-            if (scalar(@keyinfo) != 3 || $keyinfo[0] ne "ssh-rsa") {
-              msvalog('info', "got unknown or non-RSA key from monkeysphere\n");
-              next;
-            }
-            msvalog('verbose', "got good RSA key from monkeysphere: \nExponent: 0x%s\nModulus: 0x%s\n", unpack('H*', $keyinfo[1]), unpack('H*', $keyinfo[2]));
-            if ($key->{exponent}->bcmp(Math::BigInt->new('0x'.unpack('H*', $keyinfo[1]))) == 0 &&
-                $key->{modulus}->bcmp(Math::BigInt->new('0x'.unpack('H*', $keyinfo[2]))) == 0) {
-              msvalog('verbose', "...and it matches!\n");
-              $ret->{valid} = JSON::true;
-              $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
-            }
-          }
-        }
-      } else {
-        msvalog('error', "failed to decode %s\n", unpack('H*', $cert->pubkey()));
-        $ret->{message} = sprintf('failed to decode the public key', $uid);
-      }
-    }
-
-    return $status, $ret;
-  }
-
-  sub extracerts {
-    my $data = shift;
-
-    return '500 not yet implemented', { };
-  }
-
-  1;
-}
-
-my $server = MSVA->new();
-$server->run(host=>'localhost');
+use Crypt::Monkeysphere::MSVA;
+
+my $server = Crypt::Monkeysphere::MSVA->new();
+$server->run(host=>'localhost',
+             log_level=> $server->logger->get_log_level(),
+             user => POSIX::geteuid(),  # explicitly choose regular user and group (avoids spew)
+             group => POSIX::getegid(),
+             msva=>$server);
 __END__
 
 =head1 NAME
 __END__
 
 =head1 NAME
@@ -394,7 +35,7 @@ msva-perl - Perl implementation of a Monkeysphere Validation Agent
 
 =head1 SYNOPSIS
 
 
 =head1 SYNOPSIS
 
-  msva-perl
+  msva-perl [ COMMAND [ ARGS ... ] ]
 
 =head1 ABSTRACT
 
 
 =head1 ABSTRACT
 
@@ -415,8 +56,18 @@ The validation agent then tells the client whether it was able to
 successfully validate the peer's use of the public key in the given
 context.
 
 successfully validate the peer's use of the public key in the given
 context.
 
-msva-perl relies on monkeysphere(1), which uses the user's OpenPGP web
-of trust to validate the peer's use of public keys.
+=head1 USAGE
+
+Launched with no arguments, msva-perl simply runs and listens forever.
+
+Launched with arguments, it sets up a listener, spawns a subprocess
+using the supplied command and arguments, but with the
+MONKEYSPHERE_VALIDATION_AGENT_SOCKET environment variable set to refer
+to its listener.  When the subprocess terminates, msva-perl tears down
+the listener and exits as well, returning the same value as the
+subprocess.
+
+This is a similar invocation pattern to that of ssh-agent(1).
 
 =head1 ENVIRONMENT VARIABLES
 
 
 =head1 ENVIRONMENT VARIABLES
 
@@ -429,7 +80,7 @@ msva-perl is configured by means of environment variables.
 msva-perl logs messages about its operation to stderr.  MSVA_LOG_LEVEL
 controls its verbosity, and should be one of (in increasing
 verbosity): silent, quiet, fatal, error, info, verbose, debug, debug1,
 msva-perl logs messages about its operation to stderr.  MSVA_LOG_LEVEL
 controls its verbosity, and should be one of (in increasing
 verbosity): silent, quiet, fatal, error, info, verbose, debug, debug1,
-debug2, debug3.  Default is 'info'.
+debug2, debug3.  Default is 'error'.
 
 =item MSVA_ALLOWED_USERS
 
 
 =item MSVA_ALLOWED_USERS
 
@@ -442,7 +93,28 @@ of local users (by name or user ID) who are allowed to connect.
 =item MSVA_PORT
 
 msva-perl listens on a local TCP socket to facilitate access.  You can
 =item MSVA_PORT
 
 msva-perl listens on a local TCP socket to facilitate access.  You can
-choose what port to bind to by setting MSVA_PORT.  Default is 8901.
+choose what port to bind to by setting MSVA_PORT.  Default is to bind
+on an arbitrary open port.
+
+=item MSVA_KEYSERVER
+
+msva-perl will request information from OpenPGP keyservers.  Set
+MSVA_KEYSERVER to declare the keyserver you want it to check with.  If
+this variable is blank or unset, and your gpg.conf contains a
+keyserver declaration, it will use the GnuPG configuration.  Failing
+that, the default is 'hkp://pool.sks-keyservers.net'.
+
+=item MSVA_KEYSERVER_POLICY
+
+msva-perl must decide when to check with keyservers (for new keys,
+revocation certificates, new certifications, etc).  There are three
+possible options: 'always' means to check with the keyserver on every
+query it receives.  'never' means to never check with a
+keyserver. 'unlessvalid' will only check with the keyserver on a
+specific query if no keys are already locally known to be valid for
+the requested peer.  Default is 'unlessvalid'.
+
+=back
 
 =head1 COMMUNICATION PROTOCOL DETAILS
 
 
 =head1 COMMUNICATION PROTOCOL DETAILS
 
@@ -462,7 +134,7 @@ MSVA_ALLOWED_USERS above).
 
 =head1 SEE ALSO
 
 
 =head1 SEE ALSO
 
-monkeysphere(1), monkeysphere(7)
+monkeysphere(1), monkeysphere(7), ssh-agent(1)
 
 =head1 BUGS AND FEEDBACK
 
 
 =head1 BUGS AND FEEDBACK