From 851b6a097165af4d51c0db01b5e05256e5006896 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:00:48 +0200 Subject: Adding upstream version 2.6.1. Signed-off-by: Daniel Baumann --- methods/gpgv.cc | 597 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 methods/gpgv.cc (limited to 'methods/gpgv.cc') diff --git a/methods/gpgv.cc b/methods/gpgv.cc new file mode 100644 index 0000000..267b43e --- /dev/null +++ b/methods/gpgv.cc @@ -0,0 +1,597 @@ +#include + +#include "aptmethod.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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(id) < APT_ARRAY_SIZE(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 Good; + std::vector Bad; + std::vector Worthless; + // a worthless signature is a expired or revoked one + std::vector SoonWorthless; + std::vector NoPubKey; + std::vector Valid; + std::vector SignedBy; +}; +class GPGVMethod : public aptMethod +{ + private: + string VerifyGetSigners(const char *file, const char *outfile, + vector const &keyFpts, + vector const &keyFiles, + SignersStorage &Signers); + string VerifyGetSignersWithLegacy(const char *file, const char *outfile, + vector const &keyFpts, + vector const &keyFiles, + SignersStorage &Signers); + + protected: + virtual bool URIAcquire(std::string const &Message, FetchItem *Itm) APT_OVERRIDE; + public: + GPGVMethod() : aptMethod("gpgv", "1.1", SingleInstance | SendConfig | SendURIEncoded){}; +}; +static void PushEntryWithKeyID(std::vector &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 &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 const &vec, std::ostream &out, char const * const sep) +{ + if (vec.empty()) + return; + std::copy(vec.begin(), std::prev(vec.end()), std::ostream_iterator(out, sep)); + out << *vec.rbegin(); + return; +} +string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile, + vector const &keyFpts, + vector 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, ","); + setenv("APT_KEY_NO_LEGACY_KEYRING", "1", true); + 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 ErrSigners; + std::map> 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 tokens{std::istream_iterator{iss}, + std::istream_iterator{}}; + 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(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 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 primary = SubKeyMapping.find(l); + if (primary == SubKeyMapping.end()) + continue; + auto const validsubkeysig = std::find_if(primary->second.cbegin(), primary->second.cend(), [&](auto const subkey) { + return IsTheSameKey(subkey, good) && std::find(Signers.Valid.cbegin(), Signers.Valid.cend(), subkey) != Signers.Valid.cend(); + }); + if (validsubkeysig != primary->second.cend()) + { + found = true; + Signers.SignedBy.push_back(l); + Signers.SignedBy.push_back(*validsubkeysig + "!"); + 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"); +} +string GPGVMethod::VerifyGetSignersWithLegacy(const char *file, const char *outfile, + vector const &keyFpts, + vector const &keyFiles, + SignersStorage &Signers) +{ + string const msg = VerifyGetSigners(file, outfile, keyFpts, keyFiles, Signers); + if (_error->PendingError()) + return msg; + + // Bad signature always remains bad, no need to retry against trusted.gpg + if (!Signers.Bad.empty()) + return msg; + + // We do not have a key file pinned, did not find a good signature, but found + // missing keys - let's retry with trusted.gpg + if (keyFiles.empty() && Signers.Valid.empty() && !Signers.NoPubKey.empty()) + { + std::vector legacyKeyFiles{_config->FindFile("Dir::Etc::trusted")}; + if (legacyKeyFiles[0].empty()) + return msg; + if (DebugEnabled()) + std::clog << "Retrying against " << legacyKeyFiles[0] << "\n"; + + SignersStorage legacySigners; + + string const legacyMsg = VerifyGetSigners(file, outfile, keyFpts, legacyKeyFiles, legacySigners); + if (_error->PendingError()) + return legacyMsg; + // Hooray, we found a key apparently, something verified as good or bad + if (!legacySigners.Valid.empty() || !legacySigners.Bad.empty()) + { + std::string warning; + strprintf(warning, + _("Key is stored in legacy trusted.gpg keyring (%s), see the DEPRECATION section in apt-key(8) for details."), + legacyKeyFiles[0].c_str()); + Warning(std::move(warning)); + Signers = std::move(legacySigners); + return legacyMsg; + } + + } + return msg; +} +static std::string GenerateKeyFile(std::string const key) +{ + FileFd fd; + GetTempFile("apt-key.XXXXXX.asc", false, &fd); + fd.Write(key.data(), key.size()); + return fd.Name(); +} + +bool GPGVMethod::URIAcquire(std::string const &Message, FetchItem *Itm) +{ + URI const Get(Itm->Uri); + std::string const Path = DecodeSendURI(Get.Host + Get.Path); // To account for relative paths + SignersStorage Signers; + + std::vector keyFpts, keyFiles; + struct TemporaryFile + { + std::string name = ""; + ~TemporaryFile() { RemoveFile("~TemporaryFile", name); } + } tmpKey; + + std::string SignedBy = DeQuoteString(LookupTag(Message, "Signed-By")); + + if (SignedBy.find("-----BEGIN PGP PUBLIC KEY BLOCK-----") != std::string::npos) + { + tmpKey.name = GenerateKeyFile(SignedBy); + keyFiles.emplace_back(tmpKey.name); + } + else + { + for (auto &&key : VectorizeString(SignedBy, ',')) + 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 = VerifyGetSignersWithLegacy(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) + { + std::string msg; + // TRANSLATORS: The second %s is the reason and is untranslated for repository owners. + strprintf(msg, _("Signature by key %s uses weak digest algorithm (%s)"), Signer.key.c_str(), Signer.note.c_str()); + Warning(std::move(msg)); + } + } + + 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 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 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(); +} -- cgit v1.2.3