3 # Monkeysphere Validation Agent, Perl version
4 # Copyright © 2010 Daniel Kahn Gillmor <dkg@fifthhorseman.net>
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.
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.
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/>.
26 use Crypt::Monkeysphere::MSVA::MarginalUI;
27 use parent qw(HTTP::Server::Simple::CGI);
29 use Regexp::Common qw /net/;
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;
43 my $gnupg = GnuPG::Interface->new();
44 $gnupg->options->quiet(1);
45 $gnupg->options->batch(1);
48 '/' => { handler => \&noop,
49 methods => { 'GET' => 1 },
51 '/reviewcert' => { handler => \&reviewcert,
52 methods => { 'POST' => 1 },
54 '/extracerts' => { handler => \&extracerts,
55 methods => { 'POST' => 1 },
59 my $default_keyserver = 'hkp://pool.sks-keyservers.net';
60 my $default_keyserver_policy = 'unlessvalid';
62 # Net::Server log_level goes from 0 to 4
63 # this is scaled to match.
77 my $rsa_decoder = Convert::ASN1->new;
78 $rsa_decoder->prepare(q<
89 my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
90 $level = $loglevels{error} if (! defined $level);
92 if ($loglevels{lc($msglevel)} <= $level) {
98 my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
99 $level = $loglevels{error} if (! defined $level);
104 return 'Net::Server::MSVA';
111 if (exists $ENV{MSVA_PORT}) {
112 $port = $ENV{MSVA_PORT} + 0;
113 die sprintf("not a reasonable port %d", $port) if (($port >= 65536) || $port <= 0);
115 # start the server on requested port
116 my $self = $class->SUPER::new($port);
117 if (! exists $ENV{MSVA_PORT}) {
118 # we can't pass port 0 to the constructor because it evaluates
119 # to false, so HTTP::Server::Simple just uses its internal
120 # default of 8080. But if we want to select an arbitrary open
121 # port, we *can* set it here.
125 $self->{allowed_uids} = {};
126 if (exists $ENV{MSVA_ALLOWED_USERS}) {
127 msvalog('verbose', "MSVA_ALLOWED_USERS environment variable is set.\nLimiting access to specified users.\n");
128 foreach my $user (split(/ +/, $ENV{MSVA_ALLOWED_USERS})) {
129 my ($name, $passwd, $uid);
130 if ($user =~ /^[0-9]+$/) {
131 $uid = $user + 0; # force to integer
133 ($name,$passwd,$uid) = getpwnam($user);
136 msvalog('verbose', "Allowing access from user ID %d\n", $uid);
137 $self->{allowed_uids}->{$uid} = $user;
139 msvalog('error', "Could not find user '%d'; not allowing\n", $user);
143 # default is to allow access only to the current user
144 $self->{allowed_uids}->{POSIX::getuid()} = 'self';
147 bless ($self, $class);
154 return '200 OK', { available => JSON::true,
156 server => "MSVA-Perl ".$version };
159 # returns an empty list if bad key found.
160 sub parse_openssh_pubkey {
162 my ($label, $prop) = split(/ +/, $data);
163 $prop = decode_base64($prop) or return ();
165 msvalog('debug', "key properties: %s\n", unpack('H*', $prop));
167 while (length($prop) > 4) {
168 my $size = unpack('N', substr($prop, 0, 4));
169 msvalog('debug', "size: 0x%08x\n", $size);
170 return () if (length($prop) < $size + 4);
171 push(@out, substr($prop, 4, $size));
172 $prop = substr($prop, 4 + $size);
174 return () if ($label ne $out[0]);
178 # return the numeric ID of the peer on the other end of $socket,
179 # returning undef if unknown.
180 sub get_remote_peer_id {
183 my $sock = IO::Socket->new_from_fd($socket, 'r');
184 # check SO_PEERCRED -- if this was a TCP socket, Linux
185 # might not be able to support SO_PEERCRED (even on the loopback),
186 # though apparently some kernels (Solaris?) are able to.
189 my $socktype = $sock->sockopt(SO_TYPE) or die "could not get SO_TYPE info";
190 if (defined $socktype) {
191 msvalog('debug', "sockopt(SO_TYPE) = %d\n", $socktype);
193 msvalog('verbose', "sockopt(SO_TYPE) returned undefined.\n");
196 my $peercred = $sock->sockopt(SO_PEERCRED) or die "could not get SO_PEERCRED info";
197 my $remotepeer = $sock->peername();
198 my $family = sockaddr_family($remotepeer); # should be AF_UNIX (a.k.a. AF_LOCAL) or AF_INET
200 msvalog('verbose', "socket family: %d\nsocket type: %d\n", $family, $socktype);
203 # FIXME: on i386 linux, this appears to be three ints, according to
204 # /usr/include/linux/socket.h. What about other platforms?
205 my ($pid, $uid, $gid) = unpack('iii', $peercred);
207 msvalog('verbose', "SO_PEERCRED: pid: %u, uid: %u, gid: %u\n",
210 if ($pid != 0 && $uid != 0) { # then we can accept it:
211 $remotepeerid = $uid;
215 # another option in Linux would be to parse the contents of
216 # /proc/net/tcp to find the uid of the peer process based on that
218 if (! defined $remotepeerid) {
220 if ($family == AF_INET) {
222 } elsif ($family == AF_INET6) {
225 if (defined $proto) {
226 if ($socktype == &SOCK_STREAM) {
227 $proto = 'tcp'.$proto;
228 } elsif ($socktype == &SOCK_DGRAM) {
229 $proto = 'udp'.$proto;
233 if (defined $proto) {
234 my ($port, $iaddr) = unpack_sockaddr_in($remotepeer);
235 my $iaddrstring = unpack("H*", reverse($iaddr));
236 msvalog('verbose', "Port: %04x\nAddr: %s\n", $port, $iaddrstring);
237 my $remmatch = lc(sprintf("%s:%04x", $iaddrstring, $port));
238 my $infofile = '/proc/net/'.$proto;
239 my $f = new IO::File;
240 if ( $f->open('< '.$infofile)) {
241 my @header = split(/ +/, <$f>);
242 my ($localaddrix, $uidix);
245 while ($ix <= $#header) {
246 $localaddrix = $ix - $skipcount if (lc($header[$ix]) eq 'local_address');
247 $uidix = $ix - $skipcount if (lc($header[$ix]) eq 'uid');
248 $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
251 if (!defined $localaddrix) {
252 msvalog('info', "Could not find local_address field in %s; unable to determine peer UID\n",
254 } elsif (!defined $uidix) {
255 msvalog('info', "Could not find uid field in %s; unable to determine peer UID\n",
258 msvalog('debug', "local_address: %d; uid: %d\n", $localaddrix,$uidix);
259 while (my @line = split(/ +/,<$f>)) {
260 if (lc($line[$localaddrix]) eq $remmatch) {
261 if (defined $remotepeerid) {
262 msvalog('error', "Warning! found more than one remote uid! (%s and %s\n", $remotepeerid, $line[$uidix]);
264 $remotepeerid = $line[$uidix];
265 msvalog('info', "remote peer is uid %d\n",
270 msvalog('error', "Warning! could not find peer information in %s. Not verifying.\n", $infofile) unless defined $remotepeerid;
272 } else { # FIXME: we couldn't read the file. what should we
273 # do besides warning?
274 msvalog('info', "Could not read %s; unable to determine peer UID\n",
280 return $remotepeerid;
287 my $remotepeerid = get_remote_peer_id(select);
289 if (defined $remotepeerid) {
290 # test that this is an allowed user:
291 if (exists $self->{allowed_uids}->{$remotepeerid}) {
292 msvalog('verbose', "Allowing access from uid %d (%s)\n", $remotepeerid, $self->{allowed_uids}->{$remotepeerid});
294 msvalog('error', "MSVA client connection from uid %d, forbidden.\n", $remotepeerid);
295 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",
296 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),);
301 my $path = $cgi->path_info();
302 my $handler = $dispatch{$path};
304 if (ref($handler) eq "HASH") {
305 if (! exists $handler->{methods}->{$cgi->request_method()}) {
306 printf("HTTP/1.0 405 Method not allowed\r\nAllow: %s\r\nDate: %s\r\n",
307 join(', ', keys(%{$handler->{methods}})),
308 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
309 } elsif (ref($handler->{handler}) ne "CODE") {
310 printf("HTTP/1.0 500 Server Error\r\nDate: %s\r\n",
311 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
314 my $ctype = $cgi->content_type();
315 msvalog('verbose', "Got %s %s (Content-Type: %s)\n", $cgi->request_method(), $path, defined $ctype ? $ctype : '**none supplied**');
316 if (defined $ctype) {
317 my @ctypes = split(/; */, $ctype);
318 $ctype = shift @ctypes;
319 if ($ctype eq 'application/json') {
320 $data = from_json($cgi->param('POSTDATA'));
324 my ($status, $object) = $handler->{handler}($data);
325 my $ret = to_json($object);
326 msvalog('info', "returning: %s\n", $ret);
327 printf("HTTP/1.0 %s\r\nDate: %s\r\nContent-Type: application/json\r\n\r\n%s",
329 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
333 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",
334 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
335 $path, ' * '.join("\r\n * ", keys %dispatch) );
343 if ($gpgkey->algo_num != 1) {
344 msvalog('verbose', "Monkeysphere only does RSA keys. This key is algorithm #%d\n", $gpgkey->algo_num);
346 if ($rsakey->{exponent}->bcmp($gpgkey->pubkey_data->[1]) == 0 &&
347 $rsakey->{modulus}->bcmp($gpgkey->pubkey_data->[0]) == 0) {
356 if ($data->{context} =~ /^(https|ssh)$/) {
357 $data->{context} = $1;
358 if ($data->{peer} =~ /^($RE{net}{domain})$/) {
360 return $data->{context}.'://'.$data->{peer};
365 sub get_keyserver_policy {
366 if (exists $ENV{MSVA_KEYSERVER_POLICY}) {
367 if ($ENV{MSVA_KEYSERVER_POLICY} =~ /^(always|never)$/) {
370 msvalog('error', "Not a valid MSVA_KEYSERVER_POLICY):\n %s\n", $ENV{MSVA_KEYSERVER_POLICY});
372 return $default_keyserver_policy;
376 # We should read from (first hit wins):
378 if (exists $ENV{MSVA_KEYSERVER}) {
379 if ($ENV{MSVA_KEYSERVER} =~ /^((hkps?|finger|ldap):\/\/)?$RE{net}{domain}$/) {
382 msvalog('error', "Not a valid keyserver (from MSVA_KEYSERVER):\n %s\n", $ENV{MSVA_KEYSERVER});
385 # FIXME: some msva.conf file (system and user?)
386 # FIXME: the relevant gnupg.conf instead?
388 # the default_keyserver
389 return $default_keyserver;
392 sub fetch_uid_from_keyserver {
395 my $cmd = IO::Handle->new();
396 my $out = IO::Handle->new();
397 my $nul = IO::File->new("< /dev/null");
399 msvalog('debug', "start ks query for UserID: %s", $uid);
400 my $pid = $gnupg->wrap_call
401 ( handles => GnuPG::Handles->new( command => $cmd, stdout => $out, stderr => $nul ),
402 command_args => [ '='.$uid ],
403 commands => [ '--keyserver',
405 qw( --no-tty --with-colons --search ) ]
407 while (my $line = $out->getline()) {
408 msvalog('debug', "from ks query: (%d) %s", $cmd->fileno, $line);
409 if ($line =~ /^info:(\d+):(\d+)/ ) {
410 $cmd->print(join(' ', ($1..$2))."\n");
411 msvalog('debug', 'to ks query: '.join(' ', ($1..$2))."\n");
414 # FIXME: can we do something to avoid hanging forever?
416 msvalog('debug', "ks query returns %d\n", POSIX::WEXITSTATUS($?));
421 return if !ref $data;
423 my $status = '200 OK';
424 my $ret = { valid => JSON::false,
425 message => 'Unknown failure',
428 my $uid = getuid($data);
430 msvalog('error', "invalid peer/context: %s/%s\n", $data->{context}, $data->{peer});
431 $ret->{message} = sprintf('invalid peer/context');
432 return $status, $ret;
435 my $rawdata = join('', map(chr, @{$data->{pkc}->{data}}));
436 my $cert = Crypt::X509->new(cert => $rawdata);
437 msvalog('verbose', "cert subject: %s\n", $cert->subject_cn());
438 msvalog('verbose', "cert issuer: %s\n", $cert->issuer_cn());
439 msvalog('verbose', "cert pubkey algo: %s\n", $cert->PubKeyAlg());
440 msvalog('verbose', "cert pubkey: %s\n", unpack('H*', $cert->pubkey()));
442 if ($cert->PubKeyAlg() ne 'RSA') {
443 $ret->{message} = sprintf('public key was algo "%s" (OID %s). MSVA.pl only supports RSA',
444 $cert->PubKeyAlg(), $cert->pubkey_algorithm);
446 my $key = $rsa_decoder->decode($cert->pubkey());
448 # make sure that the returned integers are Math::BigInts:
449 $key->{exponent} = Math::BigInt->new($key->{exponent}) unless (ref($key->{exponent}));
450 $key->{modulus} = Math::BigInt->new($key->{modulus}) unless (ref($key->{modulus}));
451 msvalog('debug', "cert info:\nmodulus: %s\nexponent: %s\n",
452 $key->{modulus}->as_hex(),
453 $key->{exponent}->as_hex(),
456 if ($key->{modulus}->copy()->blog(2) < 1000) { # FIXME: this appears to be the full pubkey, including DER overhead
457 $ret->{message} = sprintf('public key size is less than 1000 bits (was: %d bits)', $cert->pubkey_size());
459 $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
461 if (get_keyserver_policy() eq 'always') {
462 fetch_uid_from_keyserver($uid);
465 my $afterlocalpass = 0;
467 # needed because $gnupg spawns child processes
468 $ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
470 if ($afterlocalpass) {
471 # while loop termination condition:
472 last if ($foundvalid || $ks_checked || get_keyserver_policy() eq 'never');
473 fetch_uid_from_keyserver($uid);
476 foreach my $gpgkey ($gnupg->get_public_keys('='.$uid)) {
479 foreach my $tryuid ($gpgkey->user_ids) {
480 if ($tryuid->as_string eq $uid) {
482 if ($tryuid->validity eq 'f' ||
483 $tryuid->validity eq 'u');
485 if ($tryuid->validity eq 'm');
488 if ($marginal and $notvalid) {
490 } elsif ($notvalid) {
491 msvalog('verbose', "got a key that was not fully-valid for UID %s\n", $uid);
492 msvalog('debug', Dumper($gpgkey));
495 if ($gpgkey->usage_flags =~ /a/) {
496 msvalog('verbose', "primary key 0x%s is authentication-capable\n", $gpgkey->hex_id);
497 if (keycomp($key, $gpgkey)) {
498 msvalog('verbose', "...and it matches!\n");
499 $ret->{valid} = JSON::true;
500 $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
503 foreach my $subkey ($gpgkey->subkeys) {
504 msvalog('verbose', "subkey 0x%s is authentication-capable\n", $subkey->hex_id);
505 if (keycomp($key, $subkey)) {
506 msvalog('verbose', "...and it matches!\n");
507 $ret->{valid} = JSON::true;
508 $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
517 msvalog('error', "failed to decode %s\n", unpack('H*', $cert->pubkey()));
518 $ret->{message} = sprintf('failed to decode the public key', $uid);
522 return $status, $ret;
530 msvalog('debug', "Subprocess %d terminated.\n", $pid);
532 if (exists $self->{child_pid} &&
533 ($self->{child_pid} == 0 ||
534 $self->{child_pid} == $pid)) {
535 my $exitstatus = POSIX::WEXITSTATUS($?);
536 msvalog('verbose', "Subprocess %d terminated; exiting %d.\n", $pid, $exitstatus);
537 $server->set_exit_status($exitstatus);
538 $server->server_close();
542 # use sparingly! We want to keep taint mode around for the data we
543 # get over the network. this is only here because we want to treat
544 # the command line arguments differently for the subprocess.
555 my $socketcount = @{ $server->{server}->{sock} };
556 if ( $socketcount != 1 ) {
557 msvalog('error', "%d sockets open; should have been 1.", $socketcount);
558 $server->set_exit_status(10);
559 $server->server_close();
561 my $port = @{ $server->{server}->{sock} }[0]->sockport();
562 if ((! defined $port) || ($port < 1) || ($port >= 65536)) {
563 msvalog('error', "got nonsense port: %d.", $port);
564 $server->set_exit_status(11);
565 $server->server_close();
567 if ((exists $ENV{MSVA_PORT}) && (($ENV{MSVA_PORT} + 0) != $port)) {
568 msvalog('error', "Explicitly requested port %d, but got port: %d.", ($ENV{MSVA_PORT}+0), $port);
569 $server->set_exit_status(13);
570 $server->server_close();
574 my $argcount = @ARGV;
576 $self->{child_pid} = 0; # indicate that we are planning to fork.
578 if (! defined $fork) {
579 msvalog('error', "could not fork\n");
582 msvalog('debug', "Child process has PID %d\n", $fork);
583 $self->{child_pid} = $fork;
585 msvalog('verbose', "PID %d executing: \n", $$);
586 for my $arg (@ARGV) {
587 msvalog('verbose', " %s\n", $arg);
589 $ENV{PATH} = untaint($ENV{PATH});
592 push @args, untaint($_);
594 # restore default SIGCHLD handling:
595 $SIG{CHLD} = 'DEFAULT';
596 $ENV{MONKEYSPHERE_VALIDATION_AGENT_SOCKET} = sprintf('http://localhost:%d', $self->port);
597 exec(@args) or exit 111;
601 printf("MONKEYSPHERE_VALIDATION_AGENT_SOCKET=http://localhost:%d;\nexport MONKEYSPHERE_VALIDATION_AGENT_SOCKET;\n", $self->port);
602 # FIXME: consider daemonizing here to behave more like
603 # ssh-agent. maybe avoid backgrounding by setting
604 # MSVA_NO_BACKGROUND.
611 return '500 not yet implemented', { };
617 my $server = MSVA->new();
618 $server->run(host=>'localhost',
619 log_level=>MSVA::get_log_level(),
620 user => POSIX::geteuid(), # explicitly choose regular user and group (avoids spew)
621 group => POSIX::getegid(),
627 msva-perl - Perl implementation of a Monkeysphere Validation Agent
631 msva-perl [ COMMAND [ ARGS ... ] ]
635 msva-perl provides a Perl implementation of the Monkeysphere
636 Validation Agent, a certificate validation service.
640 The Monkeysphere Validation Agent offers a local service for tools to
641 validate certificates (both X.509 and OpenPGP) and other public keys.
643 Clients of the validation agent query it with a public key carrier (a
644 raw public key, or some flavor of certificate), the supposed name of
645 the remote peer offering the pubkey, and the context in which the
646 validation check is relevant (e.g. ssh, https, etc).
648 The validation agent then tells the client whether it was able to
649 successfully validate the peer's use of the public key in the given
652 msva-perl relies on monkeysphere(1), which uses the user's OpenPGP web
653 of trust to validate the peer's use of public keys.
657 Launched with no arguments, msva-perl simply runs and listens forever.
659 Launched with arguments, it sets up a listener, spawns a subprocess
660 using the supplied command and arguments, but with the
661 MONKEYSPHERE_VALIDATION_AGENT_SOCKET environment variable set to refer
662 to its listener. When the subprocess terminates, msva-perl tears down
663 the listener and exits as well, returning the same value as the
666 This is a similar invocation pattern to that of ssh-agent(1).
668 =head1 ENVIRONMENT VARIABLES
670 msva-perl is configured by means of environment variables.
676 msva-perl logs messages about its operation to stderr. MSVA_LOG_LEVEL
677 controls its verbosity, and should be one of (in increasing
678 verbosity): silent, quiet, fatal, error, info, verbose, debug, debug1,
679 debug2, debug3. Default is 'error'.
681 =item MSVA_ALLOWED_USERS
683 If your system is capable of it, msva-perl tries to figure out the
684 owner of the connecting client. If MSVA_ALLOWED_USERS is unset,
685 msva-perl will only permit connections from the user msva is running
686 as. If you set MSVA_ALLOWED_USERS, msva-perl will treat it as a list
687 of local users (by name or user ID) who are allowed to connect.
691 msva-perl listens on a local TCP socket to facilitate access. You can
692 choose what port to bind to by setting MSVA_PORT. Default is to bind
693 on an arbitrary open port.
697 msva-perl will request information from OpenPGP keyservers. Set
698 MSVA_KEYSERVER to declare the keyserver you want it to check with.
699 Default is 'hkp://pool.sks-keyservers.net'.
701 =item MSVA_KEYSERVER_POLICY
703 msva-perl must decide when to check with keyservers (for new keys,
704 revocation certificates, new certifications, etc). There are three
705 possible options: 'always' means to check with the keyserver on every
706 query it receives. 'never' means to never check with a
707 keyserver. 'unlessvalid' will only check with the keyserver on a
708 specific query if no keys are already locally known to be valid for
709 the requested peer. Default is 'unlessvalid'.
713 =head1 COMMUNICATION PROTOCOL DETAILS
715 Communications with the Monkeysphere Validation Agent are in the form
716 of JSON requests over plain HTTP. Responses from the agent are also
717 JSON objects. For details on the structure of the requests and
718 responses, please see
719 http://web.monkeysphere.info/validation-agent/protocol
721 =head1 SECURITY CONSIDERATIONS
723 msva-perl deliberately binds to the loopback adapter (via named lookup
724 of "localhost") so that remote users do not get access to the daemon.
725 On systems (like Linux) which report ownership of TCP sockets in
726 /proc/net/tcp, msva-perl will refuse access from random users (see
727 MSVA_ALLOWED_USERS above).
731 monkeysphere(1), monkeysphere(7), ssh-agent(1)
733 =head1 BUGS AND FEEDBACK
735 Bugs or feature requests for msva-perl should be filed with the
736 Monkeysphere project's bug tracker at
737 https://labs.riseup.net/code/projects/monkeysphere/issues/
739 =head1 AUTHORS AND CONTRIBUTORS
741 Daniel Kahn Gillmor E<lt>dkg@fifthhorseman.net<gt>
743 The Monkeysphere Team http://web.monkeysphere.info/
745 =head1 COPYRIGHT AND LICENSE
747 Copyright © Daniel Kahn Gillmor and others from the Monkeysphere team.
748 msva-perl is free software, distributed under the GNU Public License,