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();
46 '/' => { handler => \&noop,
47 methods => { 'GET' => 1 },
49 '/reviewcert' => { handler => \&reviewcert,
50 methods => { 'POST' => 1 },
52 '/extracerts' => { handler => \&extracerts,
53 methods => { 'POST' => 1 },
57 # Net::Server log_level goes from 0 to 4
58 # this is scaled to match.
72 my $rsa_decoder = Convert::ASN1->new;
73 $rsa_decoder->prepare(q<
84 my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
85 $level = $loglevels{error} if (! defined $level);
87 if ($loglevels{lc($msglevel)} <= $level) {
93 my $level = $loglevels{lc($ENV{MSVA_LOG_LEVEL})};
94 $level = $loglevels{error} if (! defined $level);
99 return 'Net::Server::MSVA';
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);
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.
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
128 ($name,$passwd,$uid) = getpwnam($user);
131 msvalog('verbose', "Allowing access from user ID %d\n", $uid);
132 $self->{allowed_uids}->{$uid} = $user;
134 msvalog('error', "Could not find user '%d'; not allowing\n", $user);
138 # default is to allow access only to the current user
139 $self->{allowed_uids}->{POSIX::getuid()} = 'self';
142 bless ($self, $class);
149 return '200 OK', { available => JSON::true,
151 server => "MSVA-Perl ".$version };
154 # returns an empty list if bad key found.
155 sub parse_openssh_pubkey {
157 my ($label, $prop) = split(/ +/, $data);
158 $prop = decode_base64($prop) or return ();
160 msvalog('debug', "key properties: %s\n", unpack('H*', $prop));
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);
169 return () if ($label ne $out[0]);
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 {
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.
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);
188 msvalog('verbose', "sockopt(SO_TYPE) returned undefined.\n");
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
195 msvalog('verbose', "socket family: %d\nsocket type: %d\n", $family, $socktype);
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);
202 msvalog('verbose', "SO_PEERCRED: pid: %u, uid: %u, gid: %u\n",
205 if ($pid != 0 && $uid != 0) { # then we can accept it:
206 $remotepeerid = $uid;
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
213 if (! defined $remotepeerid) {
215 if ($family == AF_INET) {
217 } elsif ($family == AF_INET6) {
220 if (defined $proto) {
221 if ($socktype == &SOCK_STREAM) {
222 $proto = 'tcp'.$proto;
223 } elsif ($socktype == &SOCK_DGRAM) {
224 $proto = 'udp'.$proto;
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);
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
246 if (!defined $localaddrix) {
247 msvalog('info', "Could not find local_address field in %s; unable to determine peer UID\n",
249 } elsif (!defined $uidix) {
250 msvalog('info', "Could not find uid field in %s; unable to determine peer UID\n",
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]);
259 $remotepeerid = $line[$uidix];
260 msvalog('info', "remote peer is uid %d\n",
265 msvalog('error', "Warning! could not find peer information in %s. Not verifying.\n", $infofile) unless defined $remotepeerid;
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",
275 return $remotepeerid;
282 my $remotepeerid = get_remote_peer_id(select);
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});
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())),);
296 my $path = $cgi->path_info();
297 my $handler = $dispatch{$path};
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())));
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'));
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",
324 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
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) );
338 if ($gpgkey->algo_num != 1) {
339 msvalog('verbose', "Monkeysphere only does RSA keys. This key is algorithm #%d\n", $gpgkey->algo_num);
341 if ($rsakey->{exponent}->bcmp($gpgkey->pubkey_data->[1]) == 0 &&
342 $rsakey->{modulus}->bcmp($gpgkey->pubkey_data->[0]) == 0) {
351 if ($data->{context} =~ /^(https|ssh)$/) {
352 $data->{context} = $1;
353 if ($data->{peer} =~ /^($RE{net}{domain})$/) {
355 return $data->{context}.'://'.$data->{peer};
362 return if !ref $data;
364 my $status = '200 OK';
365 my $ret = { valid => JSON::false,
366 message => 'Unknown failure',
369 my $uid = getuid($data);
371 msvalog('error', "invalid peer/context: %s/%s\n", $data->{context}, $data->{peer});
372 $ret->{message} = sprintf('invalid peer/context');
373 return $status, $ret;
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()));
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);
387 my $key = $rsa_decoder->decode($cert->pubkey());
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(),
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());
400 $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
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)) {
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) {
414 if ($tryuid->validity eq 'f' ||
415 $tryuid->validity eq 'u');
417 if ($tryuid->validity eq 'm');
420 if ($marginal and $notvalid) {
422 } elsif ($notvalid) {
423 msvalog('verbose', "got a key that was not fully-valid for UID %s\n", $uid);
424 msvalog('debug', Dumper($gpgkey));
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);
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);
448 msvalog('error', "failed to decode %s\n", unpack('H*', $cert->pubkey()));
449 $ret->{message} = sprintf('failed to decode the public key', $uid);
453 return $status, $ret;
461 msvalog('debug', "Subprocess %d terminated.\n", $pid);
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();
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.
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();
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();
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();
505 my $argcount = @ARGV;
507 $self->{child_pid} = 0; # indicate that we are planning to fork.
509 if (! defined $fork) {
510 msvalog('error', "could not fork\n");
513 msvalog('debug', "Child process has PID %d\n", $fork);
514 $self->{child_pid} = $fork;
516 msvalog('verbose', "PID %d executing: \n", $$);
517 for my $arg (@ARGV) {
518 msvalog('verbose', " %s\n", $arg);
520 $ENV{PATH} = untaint($ENV{PATH});
523 push @args, untaint($_);
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;
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.
542 return '500 not yet implemented', { };
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(),
558 msva-perl - Perl implementation of a Monkeysphere Validation Agent
562 msva-perl [ COMMAND [ ARGS ... ] ]
566 msva-perl provides a Perl implementation of the Monkeysphere
567 Validation Agent, a certificate validation service.
571 The Monkeysphere Validation Agent offers a local service for tools to
572 validate certificates (both X.509 and OpenPGP) and other public keys.
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).
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
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.
588 Launched with no arguments, msva-perl simply runs and listens forever.
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
597 This is a similar invocation pattern to that of ssh-agent(1).
599 =head1 ENVIRONMENT VARIABLES
601 msva-perl is configured by means of environment variables.
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'.
612 =item MSVA_ALLOWED_USERS
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.
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.
628 =head1 COMMUNICATION PROTOCOL DETAILS
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
636 =head1 SECURITY CONSIDERATIONS
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).
646 monkeysphere(1), monkeysphere(7), ssh-agent(1)
648 =head1 BUGS AND FEEDBACK
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/
654 =head1 AUTHORS AND CONTRIBUTORS
656 Daniel Kahn Gillmor E<lt>dkg@fifthhorseman.net<gt>
658 The Monkeysphere Team http://web.monkeysphere.info/
660 =head1 COPYRIGHT AND LICENSE
662 Copyright © Daniel Kahn Gillmor and others from the Monkeysphere team.
663 msva-perl is free software, distributed under the GNU Public License,