#include <config.h>

#include "aptmethod.h"
#include <apt-pkg/configuration.h>
#include <apt-pkg/error.h>
#include <apt-pkg/fileutl.h>
#include <apt-pkg/gpgv.h>
#include <apt-pkg/strutl.h>

#include <ctype.h>
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include <algorithm>
#include <array>
#include <iostream>
#include <iterator>
#include <map>
#include <sstream>
#include <string>
#include <vector>

#include <apti18n.h>

using std::string;
using std::vector;

#define GNUPGPREFIX "[GNUPG:]"
#define GNUPGBADSIG "[GNUPG:] BADSIG"
#define GNUPGERRSIG "[GNUPG:] ERRSIG"
#define GNUPGNOPUBKEY "[GNUPG:] NO_PUBKEY"
#define GNUPGVALIDSIG "[GNUPG:] VALIDSIG"
#define GNUPGGOODSIG "[GNUPG:] GOODSIG"
#define GNUPGEXPKEYSIG "[GNUPG:] EXPKEYSIG"
#define GNUPGEXPSIG "[GNUPG:] EXPSIG"
#define GNUPGREVKEYSIG "[GNUPG:] REVKEYSIG"
#define GNUPGNODATA "[GNUPG:] NODATA"
#define APTKEYWARNING "[APTKEY:] WARNING"
#define APTKEYERROR "[APTKEY:] ERROR"

struct Digest {
   enum class State {
      Untrusted,
      Weak,
      Trusted,
   } state;
   char name[32];

   State getState() const {
      std::string optionUntrusted;
      std::string optionWeak;
      strprintf(optionUntrusted, "APT::Hashes::%s::Untrusted", name);
      strprintf(optionWeak, "APT::Hashes::%s::Weak", name);
      if (_config->FindB(optionUntrusted, false) == true)
	 return State::Untrusted;
      if (_config->FindB(optionWeak, false) == true)
	 return State::Weak;

      return state;
   }
};

static constexpr Digest Digests[] = {
   {Digest::State::Untrusted, "Invalid digest"},
   {Digest::State::Untrusted, "MD5"},
   {Digest::State::Untrusted, "SHA1"},
   {Digest::State::Untrusted, "RIPE-MD/160"},
   {Digest::State::Trusted, "Reserved digest"},
   {Digest::State::Trusted, "Reserved digest"},
   {Digest::State::Trusted, "Reserved digest"},
   {Digest::State::Trusted, "Reserved digest"},
   {Digest::State::Trusted, "SHA256"},
   {Digest::State::Trusted, "SHA384"},
   {Digest::State::Trusted, "SHA512"},
   {Digest::State::Trusted, "SHA224"},
};

static Digest FindDigest(std::string const & Digest)
{
   int id = atoi(Digest.c_str());
   if (id >= 0 && static_cast<unsigned>(id) < _count(Digests)) {
      return Digests[id];
   } else {
      return Digests[0];
   }
}

struct Signer {
   std::string key;
   std::string note;
};
static bool IsTheSameKey(std::string const &validsig, std::string const &goodsig) {
   // VALIDSIG reports a fingerprint (40 = 24 + 16), GOODSIG can be longid (16) or
   // fingerprint according to documentation in DETAILS.gz
   if (goodsig.length() == 40 + strlen("GOODSIG "))
      return validsig.compare(0, 40, goodsig, strlen("GOODSIG "), 40) == 0;
   return validsig.compare(24, 16, goodsig, strlen("GOODSIG "), 16) == 0;
}

struct APT_HIDDEN SignersStorage {
   std::vector<std::string> Good;
   std::vector<std::string> Bad;
   std::vector<std::string> Worthless;
   // a worthless signature is a expired or revoked one
   std::vector<Signer> SoonWorthless;
   std::vector<std::string> NoPubKey;
   std::vector<std::string> Valid;
   std::vector<std::string> SignedBy;
};
class GPGVMethod : public aptMethod
{
   private:
   string VerifyGetSigners(const char *file, const char *outfile,
				vector<string> const &keyFpts,
				vector<string> const &keyFiles,
				SignersStorage &Signers);
   protected:
   virtual bool URIAcquire(std::string const &Message, FetchItem *Itm) APT_OVERRIDE;
   public:
   GPGVMethod() : aptMethod("gpgv", "1.1", SingleInstance | SendConfig){};
};
static void PushEntryWithKeyID(std::vector<std::string> &Signers, char * const buffer, bool const Debug)
{
   char * const msg = buffer + sizeof(GNUPGPREFIX);
   char *p = msg;
   // skip the message
   while (*p && !isspace(*p))
      ++p;
   // skip the separator whitespace
   ++p;
   // skip the hexdigit fingerprint
   while (*p && isxdigit(*p))
      ++p;
   // cut the rest from the message
   *p = '\0';
   if (Debug == true)
      std::clog << "Got " << msg << " !" << std::endl;
   Signers.push_back(msg);
}
static void PushEntryWithUID(std::vector<std::string> &Signers, char * const buffer, bool const Debug)
{
   std::string msg = buffer + sizeof(GNUPGPREFIX);
   auto const nuke = msg.find_last_not_of("\n\t\r");
   if (nuke != std::string::npos)
      msg.erase(nuke + 1);
   if (Debug == true)
      std::clog << "Got " << msg << " !" << std::endl;
   Signers.push_back(msg);
}
static void implodeVector(std::vector<std::string> const &vec, std::ostream &out, char const * const sep)
{
   if (vec.empty())
      return;
   std::copy(vec.begin(), std::prev(vec.end()), std::ostream_iterator<std::string>(out, sep));
   out << *vec.rbegin();
   return;
}
string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile,
					 vector<string> const &keyFpts,
					 vector<string> const &keyFiles,
					 SignersStorage &Signers)
{
   bool const Debug = DebugEnabled();

   if (Debug == true)
      std::clog << "inside VerifyGetSigners" << std::endl;

   int fd[2];

   if (pipe(fd) < 0)
      return "Couldn't create pipe";

   pid_t pid = fork();
   if (pid < 0)
      return string("Couldn't spawn new process") + strerror(errno);
   else if (pid == 0)
   {
      std::ostringstream keys;
      implodeVector(keyFiles, keys, ",");
      ExecGPGV(outfile, file, 3, fd, keys.str());
   }
   close(fd[1]);

   FILE *pipein = fdopen(fd[0], "r");

   // Loop over the output of apt-key (which really is gnupg), and check the signatures.
   std::vector<std::string> ErrSigners;
   std::map<std::string, std::vector<std::string>> SubKeyMapping;
   size_t buffersize = 0;
   char *buffer = NULL;
   bool gotNODATA = false;
   while (1)
   {
      if (getline(&buffer, &buffersize, pipein) == -1)
	 break;
      if (Debug == true)
         std::clog << "Read: " << buffer << std::endl;

      // Push the data into three separate vectors, which
      // we later concatenate.  They're kept separate so
      // if we improve the apt method communication stuff later
      // it will be better.
      if (strncmp(buffer, GNUPGBADSIG, sizeof(GNUPGBADSIG)-1) == 0)
	 PushEntryWithUID(Signers.Bad, buffer, Debug);
      else if (strncmp(buffer, GNUPGERRSIG, sizeof(GNUPGERRSIG)-1) == 0)
	 PushEntryWithKeyID(ErrSigners, buffer, Debug);
      else if (strncmp(buffer, GNUPGNOPUBKEY, sizeof(GNUPGNOPUBKEY)-1) == 0)
      {
	 PushEntryWithKeyID(Signers.NoPubKey, buffer, Debug);
	 ErrSigners.erase(std::remove_if(ErrSigners.begin(), ErrSigners.end(), [&](std::string const &errsig) {
		  return errsig.compare(strlen("ERRSIG "), 16, buffer, sizeof(GNUPGNOPUBKEY), 16) == 0;  }), ErrSigners.end());
      }
      else if (strncmp(buffer, GNUPGNODATA, sizeof(GNUPGNODATA)-1) == 0)
	 gotNODATA = true;
      else if (strncmp(buffer, GNUPGEXPKEYSIG, sizeof(GNUPGEXPKEYSIG)-1) == 0)
	 PushEntryWithUID(Signers.Worthless, buffer, Debug);
      else if (strncmp(buffer, GNUPGEXPSIG, sizeof(GNUPGEXPSIG)-1) == 0)
	 PushEntryWithUID(Signers.Worthless, buffer, Debug);
      else if (strncmp(buffer, GNUPGREVKEYSIG, sizeof(GNUPGREVKEYSIG)-1) == 0)
	 PushEntryWithUID(Signers.Worthless, buffer, Debug);
      else if (strncmp(buffer, GNUPGGOODSIG, sizeof(GNUPGGOODSIG)-1) == 0)
	 PushEntryWithKeyID(Signers.Good, buffer, Debug);
      else if (strncmp(buffer, GNUPGVALIDSIG, sizeof(GNUPGVALIDSIG)-1) == 0)
      {
         std::istringstream iss(buffer + sizeof(GNUPGVALIDSIG));
         vector<string> tokens{std::istream_iterator<string>{iss},
                               std::istream_iterator<string>{}};
         auto const sig = tokens[0];
         // Reject weak digest algorithms
         Digest digest = FindDigest(tokens[7]);
         switch (digest.getState()) {
         case Digest::State::Weak:
            // Treat them like an expired key: For that a message about expiry
            // is emitted, a VALIDSIG, but no GOODSIG.
            Signers.SoonWorthless.push_back({sig, digest.name});
	    if (Debug == true)
	       std::clog << "Got weak VALIDSIG, key ID: " << sig << std::endl;
            break;
         case Digest::State::Untrusted:
            // Treat them like an expired key: For that a message about expiry
            // is emitted, a VALIDSIG, but no GOODSIG.
            Signers.Worthless.push_back(sig);
            Signers.Good.erase(std::remove_if(Signers.Good.begin(), Signers.Good.end(), [&](std::string const &goodsig) {
		     return IsTheSameKey(sig, goodsig); }), Signers.Good.end());
	    if (Debug == true)
	       std::clog << "Got untrusted VALIDSIG, key ID: " << sig << std::endl;
            break;

	 case Digest::State::Trusted:
	    if (Debug == true)
	       std::clog << "Got trusted VALIDSIG, key ID: " << sig << std::endl;
            break;
         }

	 Signers.Valid.push_back(sig);

	 if (tokens.size() > 9 && sig != tokens[9])
	    SubKeyMapping[tokens[9]].emplace_back(sig);
      }
      else if (strncmp(buffer, APTKEYWARNING, sizeof(APTKEYWARNING)-1) == 0)
         Warning("%s", buffer + sizeof(APTKEYWARNING));
      else if (strncmp(buffer, APTKEYERROR, sizeof(APTKEYERROR)-1) == 0)
	 _error->Error("%s", buffer + sizeof(APTKEYERROR));
   }
   fclose(pipein);
   free(buffer);
   std::move(ErrSigners.begin(), ErrSigners.end(), std::back_inserter(Signers.Worthless));

   // apt-key has a --keyid parameter, but this requires gpg, so we call it without it
   // and instead check after the fact which keyids where used for verification
   if (keyFpts.empty() == false)
   {
      if (Debug == true)
      {
	 std::clog << "GoodSigs needs to be limited to keyid(s): ";
	 implodeVector(keyFpts, std::clog, ", ");
	 std::clog << "\n";
      }
      std::vector<std::string> filteredGood;
      for (auto &&good: Signers.Good)
      {
	 if (Debug == true)
	    std::clog << "Key " << good << " is good sig, is it also a valid and allowed one? ";
	 bool found = false;
	 for (auto l : keyFpts)
	 {
	    bool exactKey = false;
	    if (APT::String::Endswith(l, "!"))
	    {
	       exactKey = true;
	       l.erase(l.length() - 1);
	    }
	    if (IsTheSameKey(l, good))
	    {
	       // GOODSIG might be "just" a longid, so we check VALIDSIG which is always a fingerprint
	       if (std::find(Signers.Valid.cbegin(), Signers.Valid.cend(), l) == Signers.Valid.cend())
		  continue;
	       found = true;
	       Signers.SignedBy.push_back(l + "!");
	       break;
	    }
	    else if (exactKey == false)
	    {
	       auto const master = SubKeyMapping.find(l);
	       if (master == SubKeyMapping.end())
		  continue;
	       for (auto const &sub : master->second)
		  if (IsTheSameKey(sub, good))
		  {
		     if (std::find(Signers.Valid.cbegin(), Signers.Valid.cend(), sub) == Signers.Valid.cend())
			continue;
		     found = true;
		     Signers.SignedBy.push_back(l);
		     Signers.SignedBy.push_back(sub + "!");
		     break;
		  }
	       if (found)
		  break;
	    }
	 }
	 if (Debug)
	    std::clog << (found ? "yes" : "no") << "\n";
	 if (found)
	    filteredGood.emplace_back(std::move(good));
	 else
	    Signers.NoPubKey.emplace_back(std::move(good));
      }
      Signers.Good= std::move(filteredGood);
   }
   else
   {
      // for gpg an expired key is valid, too, but we want only the valid & good ones
      for (auto const &v : Signers.Valid)
		if (std::any_of(Signers.Good.begin(), Signers.Good.end(),
					 [&v](std::string const &g) { return IsTheSameKey(v, g); }))
		   Signers.SignedBy.push_back(v + "!");
      for (auto sub : SubKeyMapping)
	 if (std::any_of(sub.second.begin(), sub.second.end(),
			 [&](std::string const &s) {
			    if (std::find(Signers.Valid.begin(), Signers.Valid.end(), s) == Signers.Valid.end())
			       return false;
			    return std::any_of(Signers.Good.begin(), Signers.Good.end(),
					       [&s](std::string const &g) { return IsTheSameKey(s, g); });
			 }))
	    Signers.SignedBy.push_back(sub.first);
   }
   std::sort(Signers.SignedBy.begin(), Signers.SignedBy.end());

   int status;
   waitpid(pid, &status, 0);
   if (Debug == true)
   {
      ioprintf(std::clog, "gpgv exited with status %i\n", WEXITSTATUS(status));
   }

   if (Debug)
   {
      std::cerr << "Summary:\n  Good: ";
      implodeVector(Signers.Good, std::cerr, ", ");
      std::cerr << "\n  Valid: ";
      implodeVector(Signers.Valid, std::cerr, ", ");
      std::cerr << "\n  Bad: ";
      implodeVector(Signers.Bad, std::cerr, ", ");
      std::cerr << "\n  Worthless: ";
      implodeVector(Signers.Worthless, std::cerr, ", ");
      std::cerr << "\n  SoonWorthless: ";
      std::for_each(Signers.SoonWorthless.begin(), Signers.SoonWorthless.end(), [](Signer const &sig) { std::cerr << sig.key << ", "; });
      std::cerr << "\n  NoPubKey: ";
      implodeVector(Signers.NoPubKey, std::cerr, ", ");
      std::cerr << "\n  Signed-By: ";
      implodeVector(Signers.SignedBy, std::cerr, ", ");
      std::cerr << std::endl << "  NODATA: " << (gotNODATA ? "yes" : "no") << std::endl;
   }

   if (WEXITSTATUS(status) == 112)
   {
      // acquire system checks for "NODATA" to generate GPG errors (the others are only warnings)
      std::string errmsg;
      //TRANSLATORS: %s is a single techy word like 'NODATA'
      strprintf(errmsg, _("Clearsigned file isn't valid, got '%s' (does the network require authentication?)"), "NODATA");
      return errmsg;
   }
   else if (gotNODATA)
   {
      // acquire system checks for "NODATA" to generate GPG errors (the others are only warnings)
      std::string errmsg;
      //TRANSLATORS: %s is a single techy word like 'NODATA'
      strprintf(errmsg, _("Signed file isn't valid, got '%s' (does the network require authentication?)"), "NODATA");
      return errmsg;
   }
   else if (WEXITSTATUS(status) == 0)
   {
      if (keyFpts.empty() == false)
      {
	 // gpgv will report success, but we want to enforce a certain keyring
	 // so if we haven't found the key the valid we found is in fact invalid
	 if (Signers.Good.empty())
	    return _("At least one invalid signature was encountered.");
      }
      else
      {
	 if (Signers.Good.empty())
	    return _("Internal error: Good signature, but could not determine key fingerprint?!");
      }
      return "";
   }
   else if (WEXITSTATUS(status) == 1)
      return _("At least one invalid signature was encountered.");
   else if (WEXITSTATUS(status) == 111)
      return _("Could not execute 'apt-key' to verify signature (is gnupg installed?)");
   else
      return _("Unknown error executing apt-key");
}

bool GPGVMethod::URIAcquire(std::string const &Message, FetchItem *Itm)
{
   URI const Get = Itm->Uri;
   string const Path = Get.Host + Get.Path; // To account for relative paths
   SignersStorage Signers;

   std::vector<std::string> keyFpts, keyFiles;
   for (auto &&key : VectorizeString(LookupTag(Message, "Signed-By"), ','))
      if (key.empty() == false && key[0] == '/')
	 keyFiles.emplace_back(std::move(key));
      else
	 keyFpts.emplace_back(std::move(key));

   // Run apt-key on file, extract contents and get the key ID of the signer
   string const msg = VerifyGetSigners(Path.c_str(), Itm->DestFile.c_str(), keyFpts, keyFiles, Signers);
   if (_error->PendingError())
      return false;

   // Check if all good signers are soon worthless and warn in that case
   if (std::all_of(Signers.Good.begin(), Signers.Good.end(), [&](std::string const &good) {
	    return std::any_of(Signers.SoonWorthless.begin(), Signers.SoonWorthless.end(), [&](Signer const &weak) {
		  return IsTheSameKey(weak.key, good);
		  });
	    }))
   {
      for (auto const & Signer : Signers.SoonWorthless)
         // TRANSLATORS: The second %s is the reason and is untranslated for repository owners.
         Warning(_("Signature by key %s uses weak digest algorithm (%s)"), Signer.key.c_str(), Signer.note.c_str());
   }

   if (Signers.Good.empty() || !Signers.Bad.empty() || !Signers.NoPubKey.empty())
   {
      string errmsg;
      // In this case, something bad probably happened, so we just go
      // with what the other method gave us for an error message.
      if (Signers.Bad.empty() && Signers.Worthless.empty() && Signers.NoPubKey.empty())
         errmsg = msg;
      else
      {
         if (!Signers.Bad.empty())
         {
            errmsg += _("The following signatures were invalid:\n");
            for (auto const &I : Signers.Bad)
               errmsg.append(I).append("\n");
         }
         if (!Signers.Worthless.empty())
         {
            errmsg += _("The following signatures were invalid:\n");
            for (auto const &I : Signers.Worthless)
               errmsg.append(I).append("\n");
         }
         if (!Signers.NoPubKey.empty())
         {
             errmsg += _("The following signatures couldn't be verified because the public key is not available:\n");
            for (auto const &I : Signers.NoPubKey)
               errmsg.append(I).append("\n");
         }
      }
      // this is only fatal if we have no good sigs or if we have at
      // least one bad signature. good signatures and NoPubKey signatures
      // happen easily when a file is signed with multiple signatures
      if (Signers.Good.empty() or !Signers.Bad.empty())
	 return _error->Error("%s", errmsg.c_str());
   }

   std::unordered_map<std::string, std::string> fields;
   fields.emplace("URI", Itm->Uri);
   fields.emplace("Filename", Itm->DestFile);
   if (Signers.SignedBy.empty() == false)
   {
      std::ostringstream out;
      implodeVector(Signers.SignedBy, out, "\n");
      fields.emplace("Signed-By", out.str());
   }
   {
      // Just pass the raw output up, because passing it as a real data
      // structure is too difficult with the method stuff.  We keep it
      // as three separate vectors for future extensibility.
      std::vector<std::string> gpgvoutput;
      std::move(Signers.Good.begin(), Signers.Good.end(), std::back_inserter(gpgvoutput));
      std::move(Signers.Bad.begin(), Signers.Bad.end(), std::back_inserter(gpgvoutput));
      std::move(Signers.NoPubKey.begin(), Signers.NoPubKey.end(), std::back_inserter(gpgvoutput));
      if (gpgvoutput.empty() == false)
      {
	 std::ostringstream out;
	 implodeVector(gpgvoutput, out, "\n");
	 fields.emplace("GPGVOutput", out.str());
      }
   }
   SendMessage("201 URI Done", std::move(fields));
   Dequeue();

   if (DebugEnabled())
      std::clog << "apt-key succeeded\n";

   return true;
}


int main()
{
   return GPGVMethod().Run();
}
