#include #include "aptmethod.h" #include #include #include #include #include #include using std::string; using std::vector; class SQVMethod : public aptMethod { private: std::optional policy{}; void SetPolicy(); bool VerifyGetSigners(const char *file, const char *outfile, vector keyFiles, vector &signers); bool ExecuteSqv(const std::vector &args, std::vector &signers); protected: bool URIAcquire(std::string const &Message, FetchItem *Itm) override; public: SQVMethod(); }; SQVMethod::SQVMethod() : aptMethod("sqv", "1.1", SingleInstance | SendConfig | SendURIEncoded) { } void SQVMethod::SetPolicy() { constexpr const char *policies[] = { // APT overrides "APT_SEQUOIA_CRYPTO_POLICY", "/etc/crypto-policies/back-ends/apt-sequoia.config", "/var/lib/crypto-config/profiles/current/apt-sequoia.config", // Sequoia overrides "SEQUOIA_CRYPTO_POLICY", "/etc/crypto-policies/back-ends/sequoia.config", "/var/lib/crypto-config/profiles/current/sequoia.config", // Fallback APT defaults "/usr/share/apt/default-sequoia.config", }; if (policy) return; policy = ""; for (auto policy : policies) { if (not strchr(policy, '/')) { if (auto value = getenv(policy)) { this->policy = value; break; } } else if (FileExists(policy)) { this->policy = policy; break; } } if (not policy->empty()) { if (DebugEnabled()) std::clog << "Setting SEQUOIA_CRYPTO_POLICY=" << *policy << std::endl; setenv("SEQUOIA_CRYPTO_POLICY", policy->c_str(), 1); } } bool SQVMethod::VerifyGetSigners(const char *file, const char *outfile, vector keyFiles, vector &signers) { bool const Debug = DebugEnabled(); std::vector args; SetPolicy(); args.push_back(SQV_EXECUTABLE); auto dearmorKeyOrCheckFormat = [&](std::string const &k) -> bool { _error->PushToStack(); FileFd keyFd(k, FileFd::ReadOnly); _error->RevertToStack(); if (not keyFd.IsOpen()) return _error->Warning("The key(s) in the keyring %s are ignored as the file is not readable by user executing gpgv.\n", k.c_str()); else if (APT::String::Endswith(k, ".asc")) { std::string b64msg; int state = 0; for (std::string line; keyFd.ReadLine(line);) { line = APT::String::Strip(line); if (APT::String::Startswith(line, "-----BEGIN PGP PUBLIC KEY BLOCK-----")) state = 1; else if (state == 1 && line == "") state = 2; else if (state == 2 && line != "" && line[0] != '=' && line[0] != '-') b64msg += line; else if (APT::String::Startswith(line, "-----END")) state = 3; } if (state != 3) goto err; return true; } else { unsigned char c; if (not keyFd.Read(&c, sizeof(c))) goto err; // Identify the leading byte of an OpenPGP public key packet // 0x98 -- old-format OpenPGP public key packet, up to 255 octets // 0x99 -- old-format OpenPGP public key packet, 256-65535 octets // 0xc6 -- new-format OpenPGP public key packet, any length if (c != 0x98 && c != 0x99 && c != 0xc6) goto err; return true; } err: return _error->Warning("The key(s) in the keyring %s are ignored as the file has an unsupported filetype.", k.c_str()); }; if (keyFiles.empty()) { // Either trusted or trustedparts must exist _error->PushToStack(); auto Parts = GetListOfFilesInDir(_config->FindDir("Dir::Etc::TrustedParts"), std::vector{"gpg", "asc"}, true); if (auto trusted = _config->FindFile("Dir::Etc::Trusted"); not trusted.empty()) { std::string s; strprintf(s, "Loading %s from deprecated option Dir::Etc::Trusted\n", trusted.c_str()); Warning(std::move(s)); Parts.push_back(trusted); } if (Parts.empty()) _error->MergeWithStack(); else _error->RevertToStack(); for (auto &Part : Parts) { if (Debug) std::clog << "Trying TrustedPart: " << Part << std::endl; if (struct stat st; stat(Part.c_str(), &st) != 0 || st.st_size == 0) continue; if (not dearmorKeyOrCheckFormat(Part)) { std::string msg; _error->PopMessage(msg); if (not msg.empty()) Warning(std::move(msg)); continue; } keyFiles.push_back(Part); } } if (keyFiles.empty()) return _error->Error("The signatures couldn't be verified because no keyring is specified"); for (auto const &keyring : keyFiles) { args.push_back("--keyring"); args.push_back(keyring); } FileFd signatureFd; FileFd messageFd; DEFER([&] { if (signatureFd.IsOpen()) RemoveFile("RemoveSignature", signatureFd.Name()); if (messageFd.IsOpen()) RemoveFile("RemoveMessage", messageFd.Name()); }); if (strcmp(file, outfile) == 0) { if (GetTempFile("apt.sig", false, &signatureFd) == nullptr) return false; if (GetTempFile("apt.data", false, &messageFd) == nullptr) return false; // FIXME: The test suite only expects the final message. _error->PushToStack(); if (signatureFd.Failed() || messageFd.Failed() || not SplitClearSignedFile(file, &messageFd, nullptr, &signatureFd)) return _error->RevertToStack(), _error->Error("Splitting up %s into data and signature failed", file); _error->RevertToStack(); args.push_back(signatureFd.Name()); args.push_back(messageFd.Name()); } else { if (not VerifyDetachedSignatureFile(file)) return false; args.push_back(file); args.push_back(outfile); } bool res = false; std::vector aheadErrors; // Check the signature with a one-year-ahead policy first { _error->PushToStack(); auto time = std::time(nullptr); auto tm = std::localtime(&time); std::string yearAheadDate; strprintf(yearAheadDate, "%d-%d-%d", tm->tm_year + 1900 + 1, tm->tm_mon + 1, tm->tm_mday); args.push_back("--policy-as-of"); args.push_back(std::move(yearAheadDate)); res = ExecuteSqv(args, signers); args.pop_back(); args.pop_back(); // Preserve any warnings or whatnot on success if (res) _error->MergeWithStack(); else { while (not _error->empty()) { std::string msg; _error->PopMessage(msg); aheadErrors.push_back(msg); } _error->RevertToStack(); } } // The year-ahead-policy produced no valid signer, check if valid at current time. if (not res) { // clear signers, args have already been cleaned post-execution signers.clear(); res = ExecuteSqv(args, signers); if (res) { Warning(_("Policy will reject signature within a year, see --audit for details")); for (auto &&msg : aheadErrors) Audit(std::move(msg)); } } return res; } bool SQVMethod::ExecuteSqv(const std::vector &args, std::vector &signers) { bool const Debug = DebugEnabled(); // FIXME: Use a select() loop FileFd sqvout; FileFd sqverr; if (GetTempFile("apt.sqvout", false, &sqvout) == nullptr) return "Internal error: Cannot create temporary file"; DEFER([&] { RemoveFile("CleanSQVOut", sqvout.Name()); }); if (GetTempFile("apt.sqverr", false, &sqverr) == nullptr) return "Internal error: Cannot create temporary file"; DEFER([&] { RemoveFile("CleanSQVErr", sqverr.Name()); }); // Translate the argument list to a C array. This should happen before // the fork so we don't allocate money between fork() and execvp(). if (Debug) std::clog << "Executing " << APT::String::Join(args, " ") << std::endl; std::vector cArgs; cArgs.reserve(args.size() + 1); for (auto const &arg : args) cArgs.push_back(arg.c_str()); cArgs.push_back(nullptr); pid_t pid = ExecFork({sqvout.Fd(), sqverr.Fd()}); if (pid < 0) return _error->Errno("VerifyGetSigners", "Couldn't spawn new process"); else if (pid == 0) { dup2(sqvout.Fd(), STDOUT_FILENO); dup2(sqverr.Fd(), STDERR_FILENO); execvp(cArgs[0], (char **)&cArgs[0]); _exit(123); } int status; waitpid(pid, &status, 0); sqverr.Seek(0); sqvout.Seek(0); if (Debug == true) ioprintf(std::clog, "sqv exited with status %i\n", WEXITSTATUS(status)); if (WEXITSTATUS(status) != 0) { std::string msg; for (std::string err; sqverr.ReadLine(err);) msg.append(err).append("\n"); return _error->Error(_("Sub-process %s returned an error code (%u), error message is:\n%s"), cArgs[0], WEXITSTATUS(status), msg.c_str()); } for (std::string signer; sqvout.ReadLine(signer);) { if (Debug) std::clog << "Got GOODSIG " << signer << std::endl; signers.push_back(signer); } return true; } 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 SQVMethod::URIAcquire(std::string const &Message, FetchItem *Itm) { // Quick safety check: do we have left-over errors from a previous URL? if (unlikely(_error->PendingError())) return _error->Error("Internal error: Error set at start of verification"); URI const Get(Itm->Uri); std::string const Path = DecodeSendURI(Get.Host + Get.Path); // To account for relative paths std::vector Signers, 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)); } // Nothing should have failed here in the setup, if it did, don't bother verifying if (_error->PendingError()) return false; // Run sqv on file, extract contents and get the key ID of the signer VerifyGetSigners(Path.c_str(), Itm->DestFile.c_str(), keyFiles, Signers); if (Signers.empty()) return _error->PendingError() ? false : _error->Error("No good signature"); if (not keyFpts.empty()) { Signers.erase(std::remove_if(Signers.begin(), Signers.end(), [&](std::string const &signer) { bool allowedSigner = std::find(keyFpts.begin(), keyFpts.end(), signer) != keyFpts.end(); if (not allowedSigner && DebugEnabled()) std::cerr << "NoPubKey: GOODSIG " << signer << "\n"; return not allowedSigner; }), Signers.end()); if (Signers.empty()) { if (keyFpts.size() > 1) return _error->Error(_("No good signature from required signers: %s"), APT::String::Join(keyFpts, ", ").c_str()); return _error->Error(_("No good signature from required signer: %s"), APT::String::Join(keyFpts, ", ").c_str()); } } std::unordered_map fields; fields.emplace("URI", Itm->Uri); fields.emplace("Filename", Itm->DestFile); fields.emplace("Signed-By", APT::String::Join(Signers, "\n")); SendMessage("201 URI Done", std::move(fields)); Dequeue(); if (DebugEnabled()) std::clog << "sqv succeeded\n"; // If we have a pending error somehow, we should still fail here... return not _error->PendingError(); } int main() { return SQVMethod().Run(); }