Factor out userid validation from MSVA.pm into Crypto::Monkeysphere::Validator.
[monkeysphere-validation-agent.git] / Crypt / Monkeysphere / MSVA.pm
index ff1631675a5bb8b3d9e57a2e69f876bbf3d29adf..e134758fec51faf4e070c4894d5a68cceb5997a3 100755 (executable)
@@ -22,6 +22,9 @@
   use vars qw($VERSION);
 
   use parent qw(HTTP::Server::Simple::CGI);
+
+  use Crypt::Monkeysphere::Validator;
+
   require Crypt::X509;
   use Regexp::Common qw /net/;
   use Convert::ASN1;
@@ -33,7 +36,7 @@
   use File::HomeDir;
   use Config::General;
   use Crypt::Monkeysphere::MSVA::MarginalUI;
-  use Crypt::Monkeysphere::MSVA::Logger;
+  use Crypt::Monkeysphere::Logger;
   use Crypt::Monkeysphere::MSVA::Monitor;
 
   use JSON;
@@ -41,7 +44,7 @@
   # we need the version of GnuPG::Interface that knows about pubkey_data, etc:
   use GnuPG::Interface 0.42.02;
 
-  $VERSION = '0.8';
+  $VERSION = '0.9~pre';
 
   my $gnupg = GnuPG::Interface::->new();
   $gnupg->options->quiet(1);
                                    },
                  );
 
-  my $default_keyserver = 'hkp://pool.sks-keyservers.net';
   my $default_keyserver_policy = 'unlessvalid';
 
-  my $logger = Crypt::Monkeysphere::MSVA::Logger::->new($ENV{MSVA_LOG_LEVEL});
+  my $logger = Crypt::Monkeysphere::Logger->new($ENV{MSVA_LOG_LEVEL});
   sub logger {
     return $logger;
   }
     }
   }
 
-  sub keycomp {
-    my $rsakey = shift;
-    my $gpgkey = shift;
-
-    if ($gpgkey->algo_num != 1) {
-      msvalog('verbose', "Monkeysphere only does RSA keys.  This key is algorithm #%d\n", $gpgkey->algo_num);
-    } else {
-      if ($rsakey->{exponent}->bcmp($gpgkey->pubkey_data->[1]) == 0 &&
-          $rsakey->{modulus}->bcmp($gpgkey->pubkey_data->[0]) == 0) {
-        return 1;
-      }
-    }
-    return 0;
-  }
-
   sub get_keyserver_policy {
     if (exists $ENV{MSVA_KEYSERVER_POLICY} and $ENV{MSVA_KEYSERVER_POLICY} ne '') {
       if ($ENV{MSVA_KEYSERVER_POLICY} =~ /^(always|never|unlessvalid)$/) {
 
     # FIXME: some msva.conf or monkeysphere.conf file (system and user?)
 
-    # or else read from the relevant gnupg.conf:
-    my $gpghome;
-    if (exists $ENV{GNUPGHOME} and $ENV{GNUPGHOME} ne '') {
-      $gpghome = untaint($ENV{GNUPGHOME});
-    } else {
-      $gpghome = File::Spec->catfile(File::HomeDir->my_home, '.gnupg');
-    }
-    my $gpgconf = File::Spec->catfile($gpghome, 'gpg.conf');
-    if (-f $gpgconf) {
-      if (-r $gpgconf) {
-        my %gpgconfig = Config::General::ParseConfig($gpgconf);
-        if ($gpgconfig{keyserver} =~ /^(((hkps?|hkpms|finger|ldap):\/\/)?$RE{net}{domain})$/) {
-          msvalog('debug', "Using keyserver %s from the GnuPG configuration file (%s)\n", $1, $gpgconf);
-          return $1;
-        } else {
-          msvalog('error', "Not a valid keyserver (from gpg config %s):\n  %s\n", $gpgconf, $gpgconfig{keyserver});
-        }
-      } else {
-        msvalog('error', "The GnuPG configuration file (%s) is not readable\n", $gpgconf);
-      }
-    } else {
-      msvalog('info', "Did not find GnuPG configuration file while looking for keyserver '%s'\n", $gpgconf);
-    }
-
-    # the default_keyserver
-    return $default_keyserver;
+    # let the keyserver routines choose.
+    return undef;
   }
 
-  sub fetch_fpr_from_keyserver {
-    my $fpr = shift;
-
-    my $cmd = IO::Handle::->new();
-    my $nul = IO::File::->new("< /dev/null");
-
-    my $ks = get_keyserver();
-    msvalog('debug', "start ks query to %s for fingerprint: %s\n", $ks, $fpr);
-    my $pid = $gnupg->wrap_call
-      ( handles => GnuPG::Handles::->new( command => $cmd, stdout => $nul, stderr => $nul ),
-       command_args => [ '0x'.$fpr ],
-       commands => [ '--keyserver',
-                     $ks,
-                     qw( --no-tty --recv-keys ) ]
-                                       );
-    # FIXME: can we do something to avoid hanging forever?
-    waitpid($pid, 0);
-    msvalog('debug', "ks query returns %d\n", POSIX::WEXITSTATUS($?));
-  }
-
-  sub fetch_uid_from_keyserver {
-    my $uid = shift;
-
-    my $cmd = IO::Handle::->new();
-    my $out = IO::Handle::->new();
-    my $nul = IO::File::->new("< /dev/null");
-
-    my $ks = get_keyserver();
-    msvalog('debug', "start ks query to %s for UserID: %s\n", $ks, $uid);
-    my $pid = $gnupg->wrap_call
-      ( handles => GnuPG::Handles::->new( command => $cmd, stdout => $out, stderr => $nul ),
-        command_args => [ '='.$uid ],
-        commands => [ '--keyserver',
-                      $ks,
-                      qw( --no-tty --with-colons --search ) ]
-      );
-    while (my $line = $out->getline()) {
-      msvalog('debug', "from ks query: (%d) %s", $cmd->fileno, $line);
-      if ($line =~ /^info:(\d+):(\d+)/ ) {
-        $cmd->print(join(' ', ($1..$2))."\n");
-        msvalog('debug', 'to ks query: '.join(' ', ($1..$2))."\n");
-        last;
-      }
-    }
-    # FIXME: can we do something to avoid hanging forever?
-    waitpid($pid, 0);
-    msvalog('debug', "ks query returns %d\n", POSIX::WEXITSTATUS($?));
-  }
 
 ##################################################
 ## PKC KEY EXTRACTION ############################
       $key->{error} = sprintf("Don't know this public key carrier type: %s", $data->{pkc}->{type});
     }
 
+    if (exists $key->{error}) {
+      return $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}));
     if ($key->{modulus}->copy()->blog(2) < 1000) {
       $key->{error} = sprintf('Public key size is less than 1000 bits (was: %d bits)', $key->{modulus}->copy()->blog(2));
     }
+
     return $key;
   }
 
                  message => 'Unknown failure',
                };
 
+    # check that there actually is key data
+    if ($data->{pkc}->{data} eq '') {
+      $ret->{message} = sprintf("Key data empty.");
+      return $status,$ret;
+    }
+
     # check context string
-    if ($data->{context} =~ /^(https|ssh|smtp|ike|postgresql|imaps|imap|submission)$/) {
+    if ($data->{context} =~ /^(https|ssh|smtp|ike|postgresql|imaps|imap|submission|e-mail)$/) {
        $data->{context} = $1;
     } else {
        msvalog('error', "invalid context: %s\n", $data->{context});
     }
 
     my $prefix = $data->{context}.'://';
-    if (defined $data->{peer}->{type} &&
+    if ($data->{context} eq 'e-mail' ||
+       (defined $data->{peer}->{type} &&
         $data->{peer}->{type} eq 'client' &&
         # ike and smtp clients are effectively other servers, so we'll
         # exclude them:
-        $data->{context} !~ /^(ike|smtp)$/) {
+        $data->{context} !~ /^(ike|smtp)$/)) {
       $prefix = '';
       # clients can have any one-line User ID without NULL characters
       # and leading or trailing whitespace
     # extract key or openpgp fingerprint from PKC
     my $fpr;
     my $key;
-    my $gpgquery;
     if (lc($data->{pkc}->{type}) eq 'openpgp4fpr') {
-      if ($data->{pkc}->{data} =~ /^([[:xdigit:]]+)$/) {
-       $data->{pkc}->{data} = $1;
+      if ($data->{pkc}->{data} =~ /^(0x)?([[:xdigit:]]{40})$/) {
+       $data->{pkc}->{data} = uc($2);
        $fpr = $data->{pkc}->{data};
        msvalog('verbose', "OpenPGP v4 fingerprint: %s\n",$fpr);
       } else {
        msvalog('error', "invalid OpenPGP v4 fingerprint: %s\n",$data->{pkc}->{data});
-       $ret->{message} = sprintf("Invalid OpengPGP v4 fingerprint.");
+       $ret->{message} = sprintf("Invalid OpenPGP v4 fingerprint.");
        return $status,$ret;
       }
-      $gpgquery = '0x'.$fpr;
     } else {
       # extract key from PKC
       $key = pkcextractkey($data);
        $ret->{message} = $key->{error};
        return $status,$ret;
       }
-      $gpgquery = '='.$uid;
     }
 
-    # setup variables
-    $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
-    my $lastloop = 0;
-    my $foundvalid = 0;
-
     # determine keyserver policy
     my $kspolicy;
     if (defined $data->{keyserverpolicy} &&
     msvalog('debug', "keyserver policy: %s\n", $kspolicy);
     # needed because $gnupg spawns child processes
     $ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
-    if ($kspolicy eq 'always') {
-      if (defined $fpr) {
-       fetch_fpr_from_keyserver($fpr);
-      } else {
-       fetch_uid_from_keyserver($uid);
-      }
-      $lastloop = 1;
-    } elsif ($kspolicy eq 'never') {
-      $lastloop = 1;
-    }
-
-    # fingerprints of keys that are not fully-valid for this User ID, but match
-    # the key from the queried certificate:
-    my @subvalid_key_fprs;
-
-    while (1) {
-      foreach my $gpgkey ($gnupg->get_public_keys($gpgquery)) {
-       my $validity = '-';
-       foreach my $tryuid ($gpgkey->user_ids) {
-         if ($tryuid->as_string eq $uid) {
-           $validity = $tryuid->validity;
-         }
-       }
-       # treat primary keys just like subkeys:
-       foreach my $subkey ($gpgkey, @{$gpgkey->subkeys}) {
-         my $primarymatch;
-         if (defined $key) {
-           $primarymatch = keycomp($key, $subkey);
-         } else {
-           $primarymatch = 1;
-         }
-         if ($primarymatch) {
-           if ($subkey->usage_flags =~ /a/) {
-             msvalog('verbose', "key matches, and 0x%s is authentication-capable\n", $subkey->hex_id);
-             if ($validity =~ /^[fu]$/) {
-               $foundvalid = 1;
-               msvalog('verbose', "...and it's fully valid!\n");
-               $ret->{valid} = JSON::true;
-               $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
-             } else {
-               push(@subvalid_key_fprs, { fpr => $subkey->fingerprint, val => $validity }) if $lastloop;
-             }
-           } else {
-             msvalog('verbose', "key matches, but 0x%s is not authentication-capable\n", $subkey->hex_id);
-           }
-         }
-       }
-      }
-      if ($lastloop) {
-       last;
-      } else {
-       if (!$foundvalid) {
-         if (defined $fpr) {
-           fetch_fpr_from_keyserver($fpr);
-         } else {
-           fetch_uid_from_keyserver($uid);
-         }
-       }
-       $lastloop = 1;
-      }
-    }
+
+    $ret->{message} = sprintf('Failed to validate "%s" through the OpenPGP Web of Trust.', $uid);
+
+    my $validator=new Crypt::Monkeysphere::Validator(kspolicy=>$kspolicy,
+                                                    context=>$data->{context},
+                                                    keyserver=>get_keyserver(),
+                                                    gnupg=>$gnupg,
+                                                    logger=>$logger);
+
+    my $uid_query=$validator->query(uid=>$uid,fpr=>$fpr, key=>$key );
 
     # only show the marginal UI if the UID of the corresponding
     # key is not fully valid.
-    if (!$foundvalid) {
+    if (scalar(@{$uid_query->{valid_keys}}) > 0) {
+      $ret->{valid} = JSON::true;
+      $ret->{message} = sprintf('Successfully validated "%s" through the OpenPGP Web of Trust.', $uid);
+    } else
+      my @subvalid_key_fprs= map { $_->{fingerprint} }   @{$uid_query->{subvalid_keys}};
+
       my $resp = Crypt::Monkeysphere::MSVA::MarginalUI->ask_the_user($gnupg,
                                                                     $uid,
                                                                     \@subvalid_key_fprs,