1 # Monkeysphere Validation Agent, Perl version
2 # Copyright © 2010 Daniel Kahn Gillmor <dkg@fifthhorseman.net>,
3 # Jameson Rollins <jrollins@finestructure.net>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 { package Crypt::Monkeysphere::MSVA;
22 use vars qw($VERSION);
24 use parent qw(HTTP::Server::Simple::CGI);
26 use Regexp::Common qw /net/;
35 use Crypt::Monkeysphere::MSVA::MarginalUI;
36 use Crypt::Monkeysphere::MSVA::Logger;
37 use Crypt::Monkeysphere::MSVA::Monitor;
40 use POSIX qw(strftime);
41 # we need the version of GnuPG::Interface that knows about pubkey_data, etc:
42 use GnuPG::Interface 0.42.02;
46 my $gnupg = GnuPG::Interface::->new();
47 $gnupg->options->quiet(1);
48 $gnupg->options->batch(1);
51 '/' => { handler => \&noop,
52 methods => { 'GET' => 1 },
54 '/reviewcert' => { handler => \&reviewcert,
55 methods => { 'POST' => 1 },
57 '/extracerts' => { handler => \&extracerts,
58 methods => { 'POST' => 1 },
62 my $default_keyserver = 'hkp://pool.sks-keyservers.net';
63 my $default_keyserver_policy = 'unlessvalid';
65 my $logger = Crypt::Monkeysphere::MSVA::Logger::->new($ENV{MSVA_LOG_LEVEL});
70 my $rsa_decoder = Convert::ASN1::->new();
71 $rsa_decoder->prepare(q<
80 return 'Net::Server::MSVA';
84 return $logger->log(@_);
91 if (exists $ENV{MSVA_PORT} and $ENV{MSVA_PORT} ne '') {
92 msvalog('debug', "MSVA_PORT set to %s\n", $ENV{MSVA_PORT});
93 $port = $ENV{MSVA_PORT} + 0;
94 die sprintf("not a reasonable port %d", $port) if (($port >= 65536) || $port <= 0);
96 # start the server on requested port
97 my $self = $class->SUPER::new($port);
98 if (! exists $ENV{MSVA_PORT}) {
99 # we can't pass port 0 to the constructor because it evaluates
100 # to false, so HTTP::Server::Simple just uses its internal
101 # default of 8080. But if we want to select an arbitrary open
102 # port, we *can* set it here.
106 $self->{allowed_uids} = {};
107 if (exists $ENV{MSVA_ALLOWED_USERS} and $ENV{MSVA_ALLOWED_USERS} ne '') {
108 msvalog('verbose', "MSVA_ALLOWED_USERS environment variable is set.\nLimiting access to specified users.\n");
109 foreach my $user (split(/ +/, $ENV{MSVA_ALLOWED_USERS})) {
110 my ($name, $passwd, $uid);
111 if ($user =~ /^[0-9]+$/) {
112 $uid = $user + 0; # force to integer
114 ($name,$passwd,$uid) = getpwnam($user);
117 msvalog('verbose', "Allowing access from user ID %d\n", $uid);
118 $self->{allowed_uids}->{$uid} = $user;
120 msvalog('error', "Could not find user '%d'; not allowing\n", $user);
124 # default is to allow access only to the current user
125 $self->{allowed_uids}->{POSIX::getuid()} = 'self';
128 bless ($self, $class);
135 return '200 OK', { available => JSON::true,
140 # return an arrayref of processes which we can detect that have the
141 # given socket open (the socket is specified with its inode)
142 sub getpidswithsocketinode {
145 if (! defined ($sockid)) {
146 msvalog('verbose', "No client socket ID to check. The MSVA is probably not running as a service.\n");
149 # this appears to be how Linux symlinks open sockets in /proc/*/fd,
150 # as of at least 2.6.26:
151 my $socktarget = sprintf('socket:[%d]', $sockid);
155 if (opendir($procfs, '/proc')) {
156 foreach my $pid (grep { /^\d+$/ } readdir($procfs)) {
157 my $procdir = sprintf('/proc/%d', $pid);
160 if (opendir($procfds, sprintf('/proc/%d/fd', $pid))) {
161 foreach my $procfd (grep { /^\d+$/ } readdir($procfds)) {
162 my $fd = sprintf('/proc/%d/fd/%d', $pid, $procfd);
164 #my ($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($fd);
165 my $targ = readlink($fd);
167 if ($targ eq $socktarget);
177 # FIXME: this whole business is very linux-specific, i think. i
178 # wonder how to get this info in other OSes?
183 # return {uid => X, inode => Y}, meaning the numeric ID of the peer
184 # on the other end of $socket, "socket inode" identifying the peer's
185 # open network socket. each value could be undef if unknown.
186 sub get_client_info {
189 my $sock = IO::Socket::->new_from_fd($socket, 'r');
190 # check SO_PEERCRED -- if this was a TCP socket, Linux
191 # might not be able to support SO_PEERCRED (even on the loopback),
192 # though apparently some kernels (Solaris?) are able to.
195 my $remotesocketinode;
196 my $socktype = $sock->sockopt(SO_TYPE) or die "could not get SO_TYPE info";
197 if (defined $socktype) {
198 msvalog('debug', "sockopt(SO_TYPE) = %d\n", $socktype);
200 msvalog('verbose', "sockopt(SO_TYPE) returned undefined.\n");
203 my $peercred = $sock->sockopt(SO_PEERCRED) or die "could not get SO_PEERCRED info";
204 my $client = $sock->peername();
205 my $family = sockaddr_family($client); # should be AF_UNIX (a.k.a. AF_LOCAL) or AF_INET
207 msvalog('verbose', "socket family: %d\nsocket type: %d\n", $family, $socktype);
210 # FIXME: on i386 linux, this appears to be three ints, according to
211 # /usr/include/linux/socket.h. What about other platforms?
212 my ($pid, $uid, $gid) = unpack('iii', $peercred);
214 msvalog('verbose', "SO_PEERCRED: pid: %u, uid: %u, gid: %u\n",
217 if ($pid != 0 && $uid != 0) { # then we can accept it:
220 # FIXME: can we get the socket inode as well this way?
223 # another option in Linux would be to parse the contents of
224 # /proc/net/tcp to find the uid of the peer process based on that
226 if (! defined $clientid) {
227 msvalog('verbose', "SO_PEERCRED failed, digging around in /proc/net/tcp\n");
229 if ($family == AF_INET) {
231 } elsif ($family == AF_INET6) {
234 if (defined $proto) {
235 if ($socktype == &SOCK_STREAM) {
236 $proto = 'tcp'.$proto;
237 } elsif ($socktype == &SOCK_DGRAM) {
238 $proto = 'udp'.$proto;
242 if (defined $proto) {
243 my ($port, $iaddr) = unpack_sockaddr_in($client);
244 my $iaddrstring = unpack("H*", reverse($iaddr));
245 msvalog('verbose', "Port: %04x\nAddr: %s\n", $port, $iaddrstring);
246 my $remmatch = lc(sprintf("%s:%04x", $iaddrstring, $port));
247 my $infofile = '/proc/net/'.$proto;
248 my $f = IO::File::->new();
249 if ( $f->open('< '.$infofile)) {
250 my @header = split(/ +/, <$f>);
251 my ($localaddrix, $uidix, $inodeix);
254 while ($ix <= $#header) {
255 $localaddrix = $ix - $skipcount if (lc($header[$ix]) eq 'local_address');
256 $uidix = $ix - $skipcount if (lc($header[$ix]) eq 'uid');
257 $inodeix = $ix - $skipcount if (lc($header[$ix]) eq 'inode');
258 $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
261 if (!defined $localaddrix) {
262 msvalog('info', "Could not find local_address field in %s; unable to determine peer UID\n",
264 } elsif (!defined $uidix) {
265 msvalog('info', "Could not find uid field in %s; unable to determine peer UID\n",
267 } elsif (!defined $inodeix) {
268 msvalog('info', "Could not find inode field in %s; unable to determine peer network socket inode\n",
271 msvalog('debug', "local_address: %d; uid: %d\n", $localaddrix,$uidix);
272 while (my @line = split(/ +/,<$f>)) {
273 if (lc($line[$localaddrix]) eq $remmatch) {
274 if (defined $clientid) {
275 msvalog('error', "Warning! found more than one remote uid! (%s and %s\n", $clientid, $line[$uidix]);
277 $clientid = $line[$uidix];
278 $remotesocketinode = $line[$inodeix];
279 msvalog('info', "remote peer is uid %d (inode %d)\n",
280 $clientid, $remotesocketinode);
284 msvalog('error', "Warning! could not find peer information in %s. Not verifying.\n", $infofile) unless defined $clientid;
286 } else { # FIXME: we couldn't read the file. what should we
287 # do besides warning?
288 msvalog('info', "Could not read %s; unable to determine peer UID\n",
294 return { 'uid' => $clientid,
295 'inode' => $remotesocketinode };
302 # This is part of a spawned child process. We don't want the
303 # child process to destroy the update monitor when it terminates.
304 $self->{updatemonitor}->forget();
305 my $clientinfo = get_client_info(select);
306 my $clientuid = $clientinfo->{uid};
308 if (defined $clientuid) {
309 # test that this is an allowed user:
310 if (exists $self->{allowed_uids}->{$clientuid}) {
311 msvalog('verbose', "Allowing access from uid %d (%s)\n", $clientuid, $self->{allowed_uids}->{$clientuid});
313 msvalog('error', "MSVA client connection from uid %d, forbidden.\n", $clientuid);
314 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",
315 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),);
320 my $path = $cgi->path_info();
321 my $handler = $dispatch{$path};
323 if (ref($handler) eq "HASH") {
324 if (! exists $handler->{methods}->{$cgi->request_method()}) {
325 printf("HTTP/1.0 405 Method not allowed\r\nAllow: %s\r\nDate: %s\r\n",
326 join(', ', keys(%{$handler->{methods}})),
327 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
328 } elsif (ref($handler->{handler}) ne "CODE") {
329 printf("HTTP/1.0 500 Server Error\r\nDate: %s\r\n",
330 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())));
333 my $ctype = $cgi->content_type();
334 msvalog('verbose', "Got %s %s (Content-Type: %s)\n", $cgi->request_method(), $path, defined $ctype ? $ctype : '**none supplied**');
335 if (defined $ctype) {
336 my @ctypes = split(/; */, $ctype);
337 $ctype = shift @ctypes;
338 if ($ctype eq 'application/json') {
339 $data = from_json($cgi->param('POSTDATA'));
343 my ($status, $object) = $handler->{handler}($data, $clientinfo);
344 if (ref($object) eq 'HASH' &&
345 ! defined $object->{server}) {
346 $object->{server} = sprintf("MSVA-Perl %s", $VERSION);
349 my $ret = to_json($object);
350 msvalog('info', "returning: %s\n", $ret);
351 printf("HTTP/1.0 %s\r\nDate: %s\r\nContent-Type: application/json\r\n\r\n%s",
353 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
357 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",
358 strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())),
359 $path, ' * '.join("\r\n * ", keys %dispatch) );
367 if ($gpgkey->algo_num != 1) {
368 msvalog('verbose', "Monkeysphere only does RSA keys. This key is algorithm #%d\n", $gpgkey->algo_num);
370 if ($rsakey->{exponent}->bcmp($gpgkey->pubkey_data->[1]) == 0 &&
371 $rsakey->{modulus}->bcmp($gpgkey->pubkey_data->[0]) == 0) {
378 sub get_keyserver_policy {
379 if (exists $ENV{MSVA_KEYSERVER_POLICY} and $ENV{MSVA_KEYSERVER_POLICY} ne '') {
380 if ($ENV{MSVA_KEYSERVER_POLICY} =~ /^(always|never|unlessvalid)$/) {
383 msvalog('error', "Not a valid MSVA_KEYSERVER_POLICY):\n %s\n", $ENV{MSVA_KEYSERVER_POLICY});
385 return $default_keyserver_policy;
389 # We should read from (first hit wins):
391 if (exists $ENV{MSVA_KEYSERVER} and $ENV{MSVA_KEYSERVER} ne '') {
392 if ($ENV{MSVA_KEYSERVER} =~ /^(((hkps?|hkpms|finger|ldap):\/\/)?$RE{net}{domain})$/) {
395 msvalog('error', "Not a valid keyserver (from MSVA_KEYSERVER):\n %s\n", $ENV{MSVA_KEYSERVER});
398 # FIXME: some msva.conf or monkeysphere.conf file (system and user?)
400 # or else read from the relevant gnupg.conf:
402 if (exists $ENV{GNUPGHOME} and $ENV{GNUPGHOME} ne '') {
403 $gpghome = untaint($ENV{GNUPGHOME});
405 $gpghome = File::Spec->catfile(File::HomeDir->my_home, '.gnupg');
407 my $gpgconf = File::Spec->catfile($gpghome, 'gpg.conf');
410 my %gpgconfig = Config::General::ParseConfig($gpgconf);
411 if ($gpgconfig{keyserver} =~ /^(((hkps?|hkpms|finger|ldap):\/\/)?$RE{net}{domain})$/) {
412 msvalog('debug', "Using keyserver %s from the GnuPG configuration file (%s)\n", $1, $gpgconf);
415 msvalog('error', "Not a valid keyserver (from gpg config %s):\n %s\n", $gpgconf, $gpgconfig{keyserver});
418 msvalog('error', "The GnuPG configuration file (%s) is not readable\n", $gpgconf);
421 msvalog('info', "Did not find GnuPG configuration file while looking for keyserver '%s'\n", $gpgconf);
424 # the default_keyserver
425 return $default_keyserver;
428 sub fetch_fpr_from_keyserver {
431 my $cmd = IO::Handle::->new();
432 my $nul = IO::File::->new("< /dev/null");
434 my $ks = get_keyserver();
435 msvalog('debug', "start ks query to %s for fingerprint: %s\n", $ks, $fpr);
436 my $pid = $gnupg->wrap_call
437 ( handles => GnuPG::Handles::->new( command => $cmd, stdout => $nul, stderr => $nul ),
438 command_args => [ '0x'.$fpr ],
439 commands => [ '--keyserver',
441 qw( --no-tty --recv-keys ) ]
443 # FIXME: can we do something to avoid hanging forever?
445 msvalog('debug', "ks query returns %d\n", POSIX::WEXITSTATUS($?));
448 sub fetch_uid_from_keyserver {
451 my $cmd = IO::Handle::->new();
452 my $out = IO::Handle::->new();
453 my $nul = IO::File::->new("< /dev/null");
455 my $ks = get_keyserver();
456 msvalog('debug', "start ks query to %s for UserID: %s\n", $ks, $uid);
457 my $pid = $gnupg->wrap_call
458 ( handles => GnuPG::Handles::->new( command => $cmd, stdout => $out, stderr => $nul ),
459 command_args => [ '='.$uid ],
460 commands => [ '--keyserver',
462 qw( --no-tty --with-colons --search ) ]
464 while (my $line = $out->getline()) {
465 msvalog('debug', "from ks query: (%d) %s", $cmd->fileno, $line);
466 if ($line =~ /^info:(\d+):(\d+)/ ) {
467 $cmd->print(join(' ', ($1..$2))."\n");
468 msvalog('debug', 'to ks query: '.join(' ', ($1..$2))."\n");
472 # FIXME: can we do something to avoid hanging forever?
474 msvalog('debug', "ks query returns %d\n", POSIX::WEXITSTATUS($?));
477 ##################################################
478 ## PKC KEY EXTRACTION ############################
484 if (lc($data->{pkc}->{type}) eq 'x509der') {
485 $key = der2key(join('', map(chr, @{$data->{pkc}->{data}})));
486 } elsif (lc($data->{pkc}->{type}) eq 'x509pem') {
487 $key = der2key(pem2der($data->{pkc}->{data}));
488 } elsif (lc($data->{pkc}->{type}) eq 'opensshpubkey') {
489 $key = opensshpubkey2key($data->{pkc}->{data});
490 } elsif (lc($data->{pkc}->{type}) eq 'rfc4716') {
491 $key = rfc47162key($data->{pkc}->{data});
493 $key->{error} = sprintf("Don't know this public key carrier type: %s", $data->{pkc}->{type});
496 if (exists $key->{error}) {
500 # make sure that the returned integers are Math::BigInts:
501 $key->{exponent} = Math::BigInt::->new($key->{exponent}) unless (ref($key->{exponent}));
502 $key->{modulus} = Math::BigInt::->new($key->{modulus}) unless (ref($key->{modulus}));
503 msvalog('debug', "pubkey info:\nmodulus: %s\nexponent: %s\n",
504 $key->{modulus}->as_hex(),
505 $key->{exponent}->as_hex(),
508 if ($key->{modulus}->copy()->blog(2) < 1000) {
509 $key->{error} = sprintf('Public key size is less than 1000 bits (was: %d bits)', $key->{modulus}->copy()->blog(2));
518 my $cert = Crypt::X509::->new(cert => $rawdata);
520 my $key = {error => 'I do not know what happened here'};
523 $key->{error} = sprintf("Error decoding X.509 certificate: %s", $cert->error);
525 msvalog('verbose', "cert subject: %s\n", $cert->subject_cn());
526 msvalog('verbose', "cert issuer: %s\n", $cert->issuer_cn());
527 msvalog('verbose', "cert pubkey algo: %s\n", $cert->PubKeyAlg());
528 msvalog('verbose', "cert pubkey: %s\n", unpack('H*', $cert->pubkey()));
530 if ($cert->PubKeyAlg() ne 'RSA') {
531 $key->{error} = sprintf('public key was algo "%s" (OID %s). MSVA.pl only supports RSA',
532 $cert->PubKeyAlg(), $cert->pubkey_algorithm);
534 msvalog('debug', "decoding ASN.1 pubkey\n");
535 $key = $rsa_decoder->decode($cert->pubkey());
536 if (! defined $key) {
537 msvalog('verbose', "failed to decode %s\n", unpack('H*', $cert->pubkey()));
538 $key = {error => 'failed to decode the public key'};
547 my @lines = split(/\n/, $pem);
550 foreach my $line (@lines) {
551 if ($line eq '-----END CERTIFICATE-----') {
554 push @goodlines, $line;
555 } elsif ($line eq '-----BEGIN CERTIFICATE-----') {
559 msvalog('debug', "%d lines of base64:\n%s\n", $#goodlines + 1, join("\n", @goodlines));
560 return decode_base64(join('', @goodlines));
563 sub opensshpubkey2key {
565 # FIXME: do we care that the label matches the type of key?
566 my ($label, $prop) = split(/ +/, $data);
568 my $out = parse_rfc4716body($prop);
577 my $continuation = '';
578 my $state = 'outside';
579 foreach my $line (split(/\n/, $data)) {
580 last if ($state eq 'body' && $line eq '---- END SSH2 PUBLIC KEY ----');
581 if ($state eq 'outside' && $line eq '---- BEGIN SSH2 PUBLIC KEY ----') {
585 if ($state eq 'header') {
586 $line = $continuation.$line;
588 if ($line =~ /^(.*)\\$/) {
592 if (! ($line =~ /:/)) {
596 push(@goodlines, $line) if ($state eq 'body');
599 msvalog('debug', "Found %d lines of RFC4716 body:\n%s\n",
601 join("\n", @goodlines));
602 my $out = parse_rfc4716body(join('', @goodlines));
607 sub parse_rfc4716body {
611 unless defined($data);
612 $data = decode_base64($data) or return undef;
614 msvalog('debug', "key properties: %s\n", unpack('H*', $data));
616 while (length($data) > 4) {
617 my $size = unpack('N', substr($data, 0, 4));
618 msvalog('debug', "size: 0x%08x\n", $size);
619 return undef if (length($data) < $size + 4);
620 push(@{$out}, substr($data, 4, $size));
621 $data = substr($data, 4 + $size);
624 if ($out->[0] ne "ssh-rsa") {
625 return {error => 'Not an RSA key'};
628 if (scalar(@{$out}) != 3) {
629 return {error => 'Does not contain the right number of bigints for RSA'};
632 return { exponent => Math::BigInt->from_hex('0x'.unpack('H*', $out->[1])),
633 modulus => Math::BigInt->from_hex('0x'.unpack('H*', $out->[2])),
637 ## PKC KEY EXTRACTION ############################
638 ##################################################
642 my $clientinfo = shift;
643 return if !ref $data;
645 msvalog('verbose', "reviewing data...\n");
647 my $status = '200 OK';
648 my $ret = { valid => JSON::false,
649 message => 'Unknown failure',
652 # check context string
653 if ($data->{context} =~ /^(https|ssh|smtp|ike|postgresql|imaps|imap|submission|e-mail)$/) {
654 $data->{context} = $1;
656 msvalog('error', "invalid context: %s\n", $data->{context});
657 $ret->{message} = sprintf("Invalid/unknown context: %s", $data->{context});
660 msvalog('verbose', "context: %s\n", $data->{context});
662 # checkout peer string
663 # old-style just passed a string as a peer, rather than
664 # peer: { name: 'whatever', 'type': 'client' }
665 $data->{peer} = { name => $data->{peer} }
666 if (ref($data->{peer}) ne 'HASH');
668 if (defined($data->{peer}->{type})) {
669 if ($data->{peer}->{type} =~ /^(client|server|peer)$/) {
670 $data->{peer}->{type} = $1;
672 msvalog('error', "invalid peer type string: %s\n", $data->{peer}->{type});
673 $ret->{message} = sprintf("Invalid peer type string: %s", $data->{peer}->{type});
678 my $prefix = $data->{context}.'://';
679 if ($data->{context} eq 'e-mail' ||
680 (defined $data->{peer}->{type} &&
681 $data->{peer}->{type} eq 'client' &&
682 # ike and smtp clients are effectively other servers, so we'll
684 $data->{context} !~ /^(ike|smtp)$/)) {
686 # clients can have any one-line User ID without NULL characters
687 # and leading or trailing whitespace
688 if ($data->{peer}->{name} =~ /^([^[:space:]][^\n\0]*[^[:space:]]|[^\0[:space:]])$/) {
689 $data->{peer}->{name} = $1;
691 msvalog('error', "invalid client peer name string: %s\n", $data->{peer}->{name});
692 $ret->{message} = sprintf("Invalid client peer name string: %s", $data->{peer}->{name});
695 } elsif ($data->{peer}->{name} =~ /^($RE{net}{domain}(:[[:digit:]]+)?)$/) {
696 $data->{peer}->{name} = $1;
698 msvalog('error', "invalid peer name string: %s\n", $data->{peer}->{name});
699 $ret->{message} = sprintf("Invalid peer name string: %s", $data->{peer}->{name});
703 msvalog('verbose', "peer: %s\n", $data->{peer}->{name});
705 # generate uid string
706 my $uid = $prefix.$data->{peer}->{name};
707 msvalog('verbose', "user ID: %s\n", $uid);
709 # extract key or openpgp fingerprint from PKC
713 if (lc($data->{pkc}->{type}) eq 'openpgp4fpr') {
714 if ($data->{pkc}->{data} =~ /^(0x)?([[:xdigit:]]+)$/) {
715 $data->{pkc}->{data} = $2;
716 $fpr = $data->{pkc}->{data};
717 msvalog('verbose', "OpenPGP v4 fingerprint: %s\n",$fpr);
719 msvalog('error', "invalid OpenPGP v4 fingerprint: %s\n",$data->{pkc}->{data});
720 $ret->{message} = sprintf("Invalid OpenPGP v4 fingerprint.");
723 $gpgquery = '0x'.$fpr;
725 # extract key from PKC
726 $key = pkcextractkey($data);
727 if (exists $key->{error}) {
728 $ret->{message} = $key->{error};
731 $gpgquery = '='.$uid;
735 $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
739 # determine keyserver policy
741 if (defined $data->{keyserverpolicy} &&
742 $data->{keyserverpolicy} =~ /^(always|never|unlessvalid)$/) {
744 msvalog("verbose", "using requested keyserver policy: %s\n", $1);
746 $kspolicy = get_keyserver_policy();
748 msvalog('debug', "keyserver policy: %s\n", $kspolicy);
749 # needed because $gnupg spawns child processes
750 $ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
751 if ($kspolicy eq 'always') {
753 fetch_fpr_from_keyserver($fpr);
755 fetch_uid_from_keyserver($uid);
758 } elsif ($kspolicy eq 'never') {
762 # fingerprints of keys that are not fully-valid for this User ID, but match
763 # the key from the queried certificate:
764 my @subvalid_key_fprs;
767 foreach my $gpgkey ($gnupg->get_public_keys($gpgquery)) {
769 foreach my $tryuid ($gpgkey->user_ids) {
770 if ($tryuid->as_string eq $uid) {
771 $validity = $tryuid->validity;
774 # treat primary keys just like subkeys:
775 foreach my $subkey ($gpgkey, @{$gpgkey->subkeys}) {
778 $primarymatch = keycomp($key, $subkey);
784 msvalog('verbose', "key 0x%s matches...\n",$subkey->hex_id);
785 if ($data->{context} eq 'e-mail') {
786 if ($subkey->usage_flags =~ /s/) {
788 msvalog('verbose', "...and is signing-capable...\n");
790 msvalog('verbose', "...but is not signing-capable (%s).\n",$subkey->usage_flags);
793 if ($subkey->usage_flags =~ /a/) {
795 msvalog('verbose', "...and is authentication-capable...\n");
797 msvalog('verbose', "...but is not authentication-capable (%s).\n",$subkey->usage_flags);
801 if ($validity =~ /^[fu]$/) {
803 msvalog('verbose', "...and is fully valid!\n");
804 $ret->{valid} = JSON::true;
805 $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
808 msvalog('verbose', "...but is not fully valid (%s).\n",$validity);
809 push(@subvalid_key_fprs, { fpr => $subkey->fingerprint, val => $validity }) if $lastloop;
814 last if ($foundvalid);
816 if ($lastloop || $foundvalid) {
821 fetch_fpr_from_keyserver($fpr);
823 fetch_uid_from_keyserver($uid);
830 # only show the marginal UI if the UID of the corresponding
831 # key is not fully valid.
833 my $resp = Crypt::Monkeysphere::MSVA::MarginalUI->ask_the_user($gnupg,
836 getpidswithsocketinode($clientinfo->{inode}),
838 msvalog('info', "response: %s\n", $resp);
840 $ret->{valid} = JSON::true;
841 $ret->{message} = sprintf('Manually validated "%s" through the OpenPGP Web of Trust.', $uid);
852 $self->spawn_master_subproc($server);
855 sub master_subprocess_died {
858 my $subproc_return = shift;
860 my $exitstatus = POSIX::WEXITSTATUS($subproc_return);
861 msvalog('verbose', "Subprocess %d terminated; exiting %d.\n", $self->{child_pid}, $exitstatus);
862 $server->set_exit_status($exitstatus);
863 $server->server_close();
871 msvalog('debug', "Subprocess %d terminated.\n", $pid);
873 if (exists $self->{updatemonitor} &&
874 defined $self->{updatemonitor}->getchildpid() &&
875 $self->{updatemonitor}->getchildpid() == $pid) {
876 my $exitstatus = POSIX::WEXITSTATUS($?);
877 msvalog('verbose', "Update monitoring process (%d) terminated with code %d.\n", $pid, $exitstatus);
878 if (0 == $exitstatus) {
879 msvalog('info', "Reloading MSVA due to update request.\n");
880 # sending self a SIGHUP:
883 msvalog('error', "Update monitoring process (%d) died unexpectedly with code %d.\nNo longer monitoring for updates; please send HUP manually.\n", $pid, $exitstatus);
884 # it died for some other weird reason; should we respawn it?
886 # FIXME: i'm worried that re-spawning would create a
887 # potentially abusive loop, if there are legit, repeatable
888 # reasons for the failure.
890 # $self->{updatemonitor}->spawn();
892 # instead, we'll just avoid trying to kill the next process with this PID:
893 $self->{updatemonitor}->forget();
895 } elsif (exists $self->{child_pid} &&
896 ($self->{child_pid} == 0 ||
897 $self->{child_pid} == $pid)) {
898 $self->master_subprocess_died($server, $?);
902 # use sparingly! We want to keep taint mode around for the data we
903 # get over the network. this is only here because we want to treat
904 # the command line arguments differently for the subprocess.
915 $server->{server}->{leave_children_open_on_hup} = 1;
917 my $socketcount = @{ $server->{server}->{sock} };
918 if ( $socketcount != 1 ) {
919 msvalog('error', "%d sockets open; should have been 1.\n", $socketcount);
920 $server->set_exit_status(10);
921 $server->server_close();
923 my $port = @{ $server->{server}->{sock} }[0]->sockport();
924 if ((! defined $port) || ($port < 1) || ($port >= 65536)) {
925 msvalog('error', "got nonsense port: %d.\n", $port);
926 $server->set_exit_status(11);
927 $server->server_close();
929 if ((exists $ENV{MSVA_PORT}) && (($ENV{MSVA_PORT} + 0) != $port)) {
930 msvalog('error', "Explicitly requested port %d, but got port: %d.", ($ENV{MSVA_PORT}+0), $port);
931 $server->set_exit_status(13);
932 $server->server_close();
935 $self->{updatemonitor} = Crypt::Monkeysphere::MSVA::Monitor::->new($logger);
938 sub spawn_master_subproc {
942 if ((exists $ENV{MSVA_CHILD_PID}) && ($ENV{MSVA_CHILD_PID} ne '')) {
943 # this is most likely a re-exec.
944 msvalog('info', "This appears to be a re-exec, continuing with child pid %d\n", $ENV{MSVA_CHILD_PID});
945 $self->{child_pid} = $ENV{MSVA_CHILD_PID} + 0;
946 } elsif ($#ARGV >= 0) {
947 $self->{child_pid} = 0; # indicate that we are planning to fork.
948 # avoid ignoring SIGCHLD right before we fork.
951 while (defined($val = POSIX::waitpid(-1, POSIX::WNOHANG)) && $val > 0) {
952 $self->child_dies($val, $server);
956 if (! defined $fork) {
957 msvalog('error', "could not fork\n");
960 msvalog('debug', "Child process has PID %d\n", $fork);
961 $self->{child_pid} = $fork;
962 $ENV{MSVA_CHILD_PID} = $fork;
964 msvalog('verbose', "PID %d executing: \n", $$);
965 for my $arg (@ARGV) {
966 msvalog('verbose', " %s\n", $arg);
968 # untaint the environment for the subprocess
969 # see: https://labs.riseup.net/code/issues/2461
970 foreach my $e (keys %ENV) {
971 $ENV{$e} = untaint($ENV{$e});
975 push @args, untaint($_);
977 # restore default SIGCHLD handling:
978 $SIG{CHLD} = 'DEFAULT';
979 $ENV{MONKEYSPHERE_VALIDATION_AGENT_SOCKET} = sprintf('http://localhost:%d', $self->port);
980 exec(@args) or exit 111;
984 printf("MONKEYSPHERE_VALIDATION_AGENT_SOCKET=http://localhost:%d;\nexport MONKEYSPHERE_VALIDATION_AGENT_SOCKET;\n", $self->port);
985 # FIXME: consider daemonizing here to behave more like
986 # ssh-agent. maybe avoid backgrounding by setting
987 # MSVA_NO_BACKGROUND.
994 return '500 not yet implemented', { };