mirror of https://github.com/mamba-org/mamba.git
add docstrings
add RepoChecker to Channel add lazy loading of the RepoChecker add artefact verif flag to micromamba CLI get index_file from a MRepo refactor Channel to avoid circular dependency in base_url add cache_name_from_url free function add index_error to disambiguate with package_error add RepoIndexChecker and RepoChecker verify_package implement verify_package on v0.6.0 spec PkgMgr define reference and cache path for top-level content trust roles use cached root metadata if any add missing virtual destructor to base class RootRole prefer artifact vs artefact
This commit is contained in:
parent
a1f7f9b835
commit
58b486d737
|
@ -88,7 +88,7 @@ To build ``micromamba``, just activate the ``BUILD_EXE`` flag in ``cmake`` comma
|
|||
- ``-DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX\\Library`` [win]
|
||||
|
||||
| Doing so, you have built the dynamically linked version of ``micromamba``.
|
||||
| It's well fitted for development but is not the artefact shipped when installing ``micromamba``.
|
||||
| It's well fitted for development but is not the artifact shipped when installing ``micromamba``.
|
||||
|
||||
|
||||
Statically linked
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
#ifndef MAMBA_CORE_CHANNEL_HPP
|
||||
#define MAMBA_CORE_CHANNEL_HPP
|
||||
|
||||
#include "mamba/core/validate.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
@ -36,6 +38,7 @@ namespace mamba
|
|||
const std::string& platform() const;
|
||||
const std::string& package_filename() const;
|
||||
const std::string& canonical_name() const;
|
||||
const validate::RepoChecker& repo_checker();
|
||||
|
||||
std::string base_url() const;
|
||||
std::string url(bool with_credential = true) const;
|
||||
|
@ -70,6 +73,7 @@ namespace mamba
|
|||
std::string m_platform;
|
||||
std::string m_package_filename;
|
||||
mutable std::string m_canonical_name;
|
||||
validate::RepoChecker m_repo_checker;
|
||||
};
|
||||
|
||||
Channel& make_channel(const std::string& value);
|
||||
|
|
|
@ -134,6 +134,7 @@ namespace mamba
|
|||
|
||||
VerificationLevel safety_checks = VerificationLevel::kWarn;
|
||||
bool extra_safety_checks = false;
|
||||
bool artifact_verif = false;
|
||||
|
||||
// debug helpers
|
||||
bool keep_temp_files = std::getenv("MAMBA_KEEP_TEMP") ? 1 : 0;
|
||||
|
|
|
@ -24,6 +24,10 @@ extern "C"
|
|||
|
||||
namespace mamba
|
||||
{
|
||||
/**
|
||||
* Represents a channel subdirectory
|
||||
* index.
|
||||
*/
|
||||
struct RepoMetadata
|
||||
{
|
||||
std::string url;
|
||||
|
@ -38,20 +42,53 @@ namespace mamba
|
|||
&& lhs.mod == rhs.mod;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper class of libsolv Repo.
|
||||
* Represents a channel subdirectory and
|
||||
* is built using a ready-to-use index/metadata
|
||||
* file (see ``MSubdirData``).
|
||||
*/
|
||||
class MRepo
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Constructor.
|
||||
* @param pool ``libsolv`` pool wrapper
|
||||
* @param prefix_data prefix data
|
||||
*/
|
||||
MRepo(MPool& pool, const PrefixData& prefix_data);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param pool ``libsolv`` pool wrapper
|
||||
* @param name Name of the subdirectory (<channel>/<subdir>)
|
||||
* @param index Path to the index file
|
||||
* @param url Subdirectory URL
|
||||
*/
|
||||
MRepo(MPool& pool,
|
||||
const std::string& name,
|
||||
const std::string& filename,
|
||||
const std::string& url);
|
||||
MRepo(MPool& pool, const std::string& name, const fs::path& path, const RepoMetadata& meta);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param pool ``libsolv`` pool wrapper
|
||||
* @param name Name of the subdirectory (<channel>/<subdir>)
|
||||
* @param index Path to the index file
|
||||
* @param meta Metadata of the repo
|
||||
*/
|
||||
MRepo(MPool& pool,
|
||||
const std::string& name,
|
||||
const fs::path& filename,
|
||||
const RepoMetadata& meta);
|
||||
|
||||
~MRepo();
|
||||
|
||||
void set_installed();
|
||||
void set_priority(int priority, int subpriority);
|
||||
|
||||
const std::string& index_file();
|
||||
|
||||
std::string name() const;
|
||||
bool write() const;
|
||||
const std::string& url() const;
|
||||
|
|
|
@ -28,23 +28,37 @@ namespace decompress
|
|||
|
||||
namespace mamba
|
||||
{
|
||||
/**
|
||||
* Represents a channel subdirectory (i.e. a platform)
|
||||
* packages index. Handles downloading of the index
|
||||
* from the server and cache generation as well.
|
||||
*/
|
||||
class MSubdirData
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Constructor.
|
||||
* @param name Name of the subdirectory (<channel>/<subdir>)
|
||||
* @param repodata_url URL of the repodata file
|
||||
* @param repodata_fn Local path of the repodata file
|
||||
*/
|
||||
MSubdirData(const std::string& name,
|
||||
const std::string& url,
|
||||
const std::string& repodata_url,
|
||||
const std::string& repodata_fn);
|
||||
|
||||
// TODO return seconds as double
|
||||
fs::file_time_type::duration check_cache(const fs::path& cache_file,
|
||||
const fs::file_time_type::clock::time_point& ref);
|
||||
bool load();
|
||||
bool loaded();
|
||||
|
||||
bool forbid_cache();
|
||||
void clear_cache();
|
||||
bool load();
|
||||
|
||||
std::string cache_path() const;
|
||||
DownloadTarget* target();
|
||||
const std::string& name() const;
|
||||
|
||||
DownloadTarget* target();
|
||||
bool finalize_transfer();
|
||||
|
||||
MRepo create_repo(MPool& pool);
|
||||
|
@ -66,7 +80,7 @@ namespace mamba
|
|||
|
||||
bool m_loaded;
|
||||
bool m_download_complete;
|
||||
std::string m_url;
|
||||
std::string m_repodata_url;
|
||||
std::string m_name;
|
||||
std::string m_json_fn;
|
||||
std::string m_solv_fn;
|
||||
|
|
|
@ -66,6 +66,8 @@ namespace mamba
|
|||
std::string unc_url(const std::string& url);
|
||||
std::string encode_url(const std::string& url);
|
||||
std::string decode_url(const std::string& url);
|
||||
// Only returns a cache name without extension
|
||||
std::string cache_name_from_url(const std::string& url);
|
||||
|
||||
class URLHandler
|
||||
{
|
||||
|
|
|
@ -88,7 +88,7 @@ namespace validate
|
|||
const std::string& signature);
|
||||
|
||||
/**
|
||||
* Base class for artefact/package verification error.
|
||||
* Base class for artifact/package verification error.
|
||||
*/
|
||||
class trust_error : public std::exception
|
||||
{
|
||||
|
@ -198,6 +198,18 @@ namespace validate
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Error raised when an invalid package
|
||||
* index is met.
|
||||
*/
|
||||
class index_error : public trust_error
|
||||
{
|
||||
public:
|
||||
index_error() noexcept;
|
||||
virtual ~index_error() = default;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Representation of the public part of a
|
||||
* cryptographic key pair.
|
||||
|
@ -430,22 +442,20 @@ namespace validate
|
|||
class RootRole : public RoleBase
|
||||
{
|
||||
public:
|
||||
virtual ~RootRole() = default;
|
||||
|
||||
std::unique_ptr<RootRole> update(fs::path path);
|
||||
std::unique_ptr<RootRole> update(json j);
|
||||
|
||||
std::vector<fs::path> possible_update_files();
|
||||
|
||||
virtual std::unique_ptr<RepoIndexChecker> build_index_checker(
|
||||
const std::string& url) const = 0;
|
||||
const std::string& url, const fs::path& cache_path) const = 0;
|
||||
|
||||
protected:
|
||||
RootRole(std::shared_ptr<SpecBase> spec);
|
||||
|
||||
private:
|
||||
// virtual json read_root_file(const fs::path& p, bool update = false) const = 0;
|
||||
// virtual std::size_t root_version() const = 0;
|
||||
// virtual SpecBase* spec_impl() = 0;
|
||||
|
||||
virtual std::unique_ptr<RootRole> create_update(const json& j) = 0;
|
||||
};
|
||||
|
||||
|
@ -460,6 +470,8 @@ namespace validate
|
|||
virtual ~RepoIndexChecker() = default;
|
||||
virtual void verify_index(const json& j) const = 0;
|
||||
virtual void verify_index(const fs::path& p) const = 0;
|
||||
virtual void verify_package(const fs::path& index_path,
|
||||
const std::string& pkg_name) const = 0;
|
||||
|
||||
protected:
|
||||
RepoIndexChecker() = default;
|
||||
|
@ -474,19 +486,38 @@ namespace validate
|
|||
class RepoChecker
|
||||
{
|
||||
public:
|
||||
RepoChecker(const std::string& url, const fs::path& local_trusted_root);
|
||||
/**
|
||||
* Constructor.
|
||||
* @param base_url Repository base URL
|
||||
* @param ref_path Path to the reference directory, hosting trusted root metadata
|
||||
* @param cache_path Path to the cache directory
|
||||
*/
|
||||
RepoChecker(const std::string& base_url,
|
||||
const fs::path& ref_path,
|
||||
const fs::path& cache_path = "");
|
||||
|
||||
// Forwarding to a ``RepoIndexChecker`` implementation
|
||||
void verify_index(const json& j);
|
||||
void verify_index(const fs::path& p);
|
||||
void verify_index(const json& j) const;
|
||||
void verify_index(const fs::path& p) const;
|
||||
void verify_package(const fs::path& index_path, const std::string& pkg_name) const;
|
||||
|
||||
void generate_index_checker();
|
||||
|
||||
const fs::path& cache_path();
|
||||
|
||||
std::size_t root_version();
|
||||
|
||||
private:
|
||||
std::string m_base_url;
|
||||
std::size_t m_root_version = 0;
|
||||
fs::path m_ref_path;
|
||||
fs::path m_cache_path;
|
||||
|
||||
fs::path m_local_trusted_root;
|
||||
fs::path initial_trusted_root();
|
||||
fs::path ref_root();
|
||||
fs::path cached_root();
|
||||
|
||||
void persist_file(const fs::path& file_path);
|
||||
|
||||
std::unique_ptr<RepoIndexChecker> p_index_checker;
|
||||
|
||||
|
@ -525,7 +556,7 @@ namespace validate
|
|||
RoleFullKeys self_keys() const override;
|
||||
|
||||
std::unique_ptr<RepoIndexChecker> build_index_checker(
|
||||
const std::string& url) const override;
|
||||
const std::string& url, const fs::path& cache_path) const override;
|
||||
|
||||
friend void to_json(json& j, const RootImpl& r);
|
||||
friend void from_json(const json& j, RootImpl& r);
|
||||
|
@ -582,7 +613,7 @@ namespace validate
|
|||
* from repository base URL.
|
||||
*/
|
||||
std::unique_ptr<RepoIndexChecker> build_index_checker(
|
||||
const std::string& url) const override;
|
||||
const std::string& url, const fs::path& cache_path) const override;
|
||||
|
||||
RoleFullKeys self_keys() const override;
|
||||
|
||||
|
@ -629,7 +660,8 @@ namespace validate
|
|||
* Return a ``RepoIndexChecker`` implementation (derived class)
|
||||
* from repository base URL.
|
||||
*/
|
||||
std::unique_ptr<RepoIndexChecker> build_index_checker(const std::string& url) const;
|
||||
std::unique_ptr<RepoIndexChecker> build_index_checker(const std::string& url,
|
||||
const fs::path& cache_path) const;
|
||||
|
||||
friend void to_json(json& j, const KeyMgrRole& r);
|
||||
friend void from_json(const json& j, KeyMgrRole& r);
|
||||
|
@ -670,6 +702,8 @@ namespace validate
|
|||
public:
|
||||
void verify_index(const fs::path& p) const override;
|
||||
void verify_index(const json& j) const override;
|
||||
void verify_package(const fs::path& index_path,
|
||||
const std::string& pkg_name) const override;
|
||||
|
||||
private:
|
||||
PkgMgrRole() = delete;
|
||||
|
|
|
@ -696,6 +696,15 @@ namespace mamba
|
|||
Spend extra time validating package contents. Currently, runs sha256
|
||||
verification on every file within each package during installation.)")));
|
||||
|
||||
insert(Configurable("artifact_verif", &ctx.artifact_verif)
|
||||
.group("Link & Install")
|
||||
.set_rc_configurable()
|
||||
.set_env_var_name()
|
||||
.description("Run verifications on packages signatures")
|
||||
.long_description(unindent(R"(
|
||||
Spend extra time validating package contents. It consists of running
|
||||
cryptographic verifications on channels and packages metadata.)")));
|
||||
|
||||
// Output, Prompt and Flow
|
||||
insert(Configurable("always_yes", &ctx.always_yes)
|
||||
.group("Output, Prompt and Flow Control")
|
||||
|
|
|
@ -347,12 +347,12 @@ namespace mamba
|
|||
for (auto& url : channel_urls)
|
||||
{
|
||||
auto& channel = make_channel(url);
|
||||
std::string full_url = concat(channel.url(true), "/repodata.json");
|
||||
std::string repodata_full_url = concat(channel.url(true), "/repodata.json");
|
||||
|
||||
auto sdir
|
||||
= std::make_shared<MSubdirData>(concat(channel.name(), "/", channel.platform()),
|
||||
full_url,
|
||||
cache_dir / cache_fn_url(full_url));
|
||||
repodata_full_url,
|
||||
cache_dir / cache_fn_url(repodata_full_url));
|
||||
|
||||
sdir->load();
|
||||
multi_dl.add(sdir->target());
|
||||
|
|
|
@ -69,7 +69,7 @@ namespace mamba
|
|||
|
||||
if (regex.empty() || std::regex_search(pkg_info.name, spec_pat))
|
||||
{
|
||||
auto channel = make_channel(pkg_info.url);
|
||||
auto& channel = make_channel(pkg_info.url);
|
||||
obj["base_url"] = channel.base_url();
|
||||
obj["build_number"] = pkg_info.build_number;
|
||||
obj["build_string"] = pkg_info.build_string;
|
||||
|
|
|
@ -14,9 +14,12 @@
|
|||
#include "mamba/core/channel.hpp"
|
||||
#include "mamba/core/environment.hpp"
|
||||
#include "mamba/core/context.hpp"
|
||||
#include "mamba/core/fsutil.hpp"
|
||||
#include "mamba/core/package_handling.hpp"
|
||||
#include "mamba/core/url.hpp"
|
||||
#include "mamba/core/util.hpp"
|
||||
#include "mamba/core/validate.hpp"
|
||||
|
||||
|
||||
namespace mamba
|
||||
{
|
||||
|
@ -61,7 +64,13 @@ namespace mamba
|
|||
, m_platform(platform)
|
||||
, m_package_filename(package_filename)
|
||||
, m_canonical_name(multi_name)
|
||||
, m_repo_checker(base_url(),
|
||||
Context::instance().root_prefix / "etc" / "trusted-repos"
|
||||
/ cache_name_from_url(base_url()),
|
||||
Context::instance().root_prefix / "pkgs" / "cache"
|
||||
/ cache_name_from_url(base_url()))
|
||||
{
|
||||
fs::create_directories(m_repo_checker.cache_path());
|
||||
}
|
||||
|
||||
void Channel::set_token(const std::string& token)
|
||||
|
@ -104,6 +113,12 @@ namespace mamba
|
|||
return m_package_filename;
|
||||
}
|
||||
|
||||
const validate::RepoChecker& Channel::repo_checker()
|
||||
{
|
||||
m_repo_checker.generate_index_checker();
|
||||
return m_repo_checker;
|
||||
}
|
||||
|
||||
const std::string& Channel::canonical_name() const
|
||||
{
|
||||
if (m_canonical_name == "")
|
||||
|
@ -131,7 +146,7 @@ namespace mamba
|
|||
|
||||
std::string Channel::base_url() const
|
||||
{
|
||||
if (canonical_name() == UNKNOWN_CHANNEL)
|
||||
if (name() == UNKNOWN_CHANNEL)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ namespace mamba
|
|||
LOG_INFO << "need to expand path!";
|
||||
spec_str = path_to_url(fs::absolute(env::expand_user(spec_str)));
|
||||
}
|
||||
auto parsed_channel = make_channel(spec_str);
|
||||
auto& parsed_channel = make_channel(spec_str);
|
||||
|
||||
if (!parsed_channel.platform().empty())
|
||||
{
|
||||
|
|
|
@ -30,23 +30,23 @@ namespace mamba
|
|||
|
||||
MRepo::MRepo(MPool& pool,
|
||||
const std::string& name,
|
||||
const fs::path& filename,
|
||||
const fs::path& index,
|
||||
const RepoMetadata& metadata)
|
||||
: m_metadata(metadata)
|
||||
{
|
||||
m_url = rsplit(metadata.url, "/", 1)[0];
|
||||
m_repo = repo_create(pool, m_url.c_str());
|
||||
read_file(filename);
|
||||
read_file(index);
|
||||
}
|
||||
|
||||
MRepo::MRepo(MPool& pool,
|
||||
const std::string& name,
|
||||
const std::string& filename,
|
||||
const std::string& index,
|
||||
const std::string& url)
|
||||
: m_url(url)
|
||||
{
|
||||
m_repo = repo_create(pool, name.c_str());
|
||||
read_file(filename);
|
||||
read_file(index);
|
||||
}
|
||||
|
||||
MRepo::MRepo(MPool& pool, const PrefixData& prefix_data)
|
||||
|
@ -158,6 +158,11 @@ namespace mamba
|
|||
return m_repo->nsolvables;
|
||||
}
|
||||
|
||||
const std::string& MRepo::index_file()
|
||||
{
|
||||
return m_json_file;
|
||||
}
|
||||
|
||||
bool MRepo::read_file(const std::string& filename)
|
||||
{
|
||||
LOG_INFO << m_repo->name << ": reading repo file " << filename;
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
//
|
||||
// The full license is in the file LICENSE, distributed with this software.
|
||||
|
||||
#include "openssl/md5.h"
|
||||
|
||||
#include "mamba/core/mamba_fs.hpp"
|
||||
#include "mamba/core/output.hpp"
|
||||
#include "mamba/core/package_cache.hpp"
|
||||
#include "mamba/core/subdirdata.hpp"
|
||||
#include "mamba/core/url.hpp"
|
||||
|
||||
|
||||
namespace decompress
|
||||
{
|
||||
|
@ -64,11 +64,11 @@ namespace decompress
|
|||
namespace mamba
|
||||
{
|
||||
MSubdirData::MSubdirData(const std::string& name,
|
||||
const std::string& url,
|
||||
const std::string& repodata_url,
|
||||
const std::string& repodata_fn)
|
||||
: m_loaded(false)
|
||||
, m_download_complete(false)
|
||||
, m_url(url)
|
||||
, m_repodata_url(repodata_url)
|
||||
, m_name(name)
|
||||
, m_json_fn(repodata_fn)
|
||||
, m_solv_fn(repodata_fn.substr(0, repodata_fn.size() - 4) + "solv")
|
||||
|
@ -98,7 +98,7 @@ namespace mamba
|
|||
|
||||
bool MSubdirData::forbid_cache()
|
||||
{
|
||||
return starts_with(m_url, "file://");
|
||||
return starts_with(m_repodata_url, "file://");
|
||||
}
|
||||
|
||||
bool MSubdirData::load()
|
||||
|
@ -128,8 +128,8 @@ namespace mamba
|
|||
if ((max_age > cache_age_seconds || Context::instance().offline))
|
||||
{
|
||||
// cache valid!
|
||||
LOG_INFO << "Using cache " << m_url << " age in seconds: " << cache_age_seconds
|
||||
<< " / " << max_age;
|
||||
LOG_INFO << "Using cache " << m_repodata_url
|
||||
<< " age in seconds: " << cache_age_seconds << " / " << max_age;
|
||||
std::string prefix = m_name;
|
||||
prefix.resize(PREFIX_LENGTH - 1, ' ');
|
||||
Console::stream() << prefix << " Using cache";
|
||||
|
@ -158,7 +158,7 @@ namespace mamba
|
|||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO << "No cache found " << m_url;
|
||||
LOG_INFO << "No cache found " << m_repodata_url;
|
||||
if (!Context::instance().offline || forbid_cache())
|
||||
{
|
||||
create_target(m_mod_etag);
|
||||
|
@ -196,7 +196,7 @@ namespace mamba
|
|||
if (m_target->result != 0 || m_target->http_status >= 400)
|
||||
{
|
||||
LOG_INFO << "Unable to retrieve repodata (response: " << m_target->http_status
|
||||
<< ") for " << m_url;
|
||||
<< ") for " << m_repodata_url;
|
||||
m_progress_bar.set_postfix(std::to_string(m_target->http_status) + " Failed");
|
||||
m_progress_bar.set_full();
|
||||
m_progress_bar.mark_as_completed();
|
||||
|
@ -247,24 +247,26 @@ namespace mamba
|
|||
return true;
|
||||
}
|
||||
|
||||
LOG_INFO << "Finalized transfer: " << m_url;
|
||||
LOG_INFO << "Finalized transfer: " << m_repodata_url;
|
||||
|
||||
m_mod_etag.clear();
|
||||
m_mod_etag["_url"] = m_url;
|
||||
m_mod_etag["_url"] = m_repodata_url;
|
||||
m_mod_etag["_etag"] = m_target->etag;
|
||||
m_mod_etag["_mod"] = m_target->mod;
|
||||
m_mod_etag["_cache_control"] = m_target->cache_control;
|
||||
|
||||
LOG_INFO << "Opening: " << m_json_fn;
|
||||
std::ofstream final_file(m_json_fn);
|
||||
// TODO make sure that cache directory exists!
|
||||
|
||||
create_cache_dir();
|
||||
|
||||
if (!final_file.is_open())
|
||||
{
|
||||
LOG_ERROR << "Could not open file " << m_json_fn;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (ends_with(m_url, ".bz2"))
|
||||
if (ends_with(m_repodata_url, ".bz2"))
|
||||
{
|
||||
m_progress_bar.set_postfix("Decomp...");
|
||||
decompress();
|
||||
|
@ -287,8 +289,8 @@ namespace mamba
|
|||
|
||||
if (!temp_file)
|
||||
{
|
||||
LOG_ERROR << "Could not write out repodata file " << m_json_fn << ": "
|
||||
<< strerror(errno);
|
||||
LOG_ERROR << "Could not write out repodata file '" << m_json_fn
|
||||
<< "': " << strerror(errno);
|
||||
fs::remove(m_json_fn);
|
||||
exit(1);
|
||||
}
|
||||
|
@ -326,7 +328,7 @@ namespace mamba
|
|||
{
|
||||
m_temp_file = std::make_unique<TemporaryFile>();
|
||||
m_progress_bar = Console::instance().add_progress_bar(m_name);
|
||||
m_target = std::make_unique<DownloadTarget>(m_name, m_url, m_temp_file->path());
|
||||
m_target = std::make_unique<DownloadTarget>(m_name, m_repodata_url, m_temp_file->path());
|
||||
m_target->set_progress_bar(m_progress_bar);
|
||||
// if we get something _other_ than the noarch, we DO NOT throw if the file
|
||||
// can't be retrieved
|
||||
|
@ -405,14 +407,7 @@ namespace mamba
|
|||
|
||||
std::string cache_fn_url(const std::string& url)
|
||||
{
|
||||
std::vector<unsigned char> hash(MD5_DIGEST_LENGTH);
|
||||
MD5_CTX md5;
|
||||
MD5_Init(&md5);
|
||||
MD5_Update(&md5, url.c_str(), url.size());
|
||||
MD5_Final(hash.data(), &md5);
|
||||
|
||||
std::string hex_digest = hex_string(hash);
|
||||
return hex_digest.substr(0u, 8u) + ".json";
|
||||
return cache_name_from_url(url) + ".json";
|
||||
}
|
||||
|
||||
std::string create_cache_dir()
|
||||
|
@ -428,7 +423,7 @@ namespace mamba
|
|||
|
||||
MRepo MSubdirData::create_repo(MPool& pool)
|
||||
{
|
||||
RepoMetadata meta{ m_url,
|
||||
RepoMetadata meta{ m_repodata_url,
|
||||
Context::instance().add_pip_as_python_dependency,
|
||||
m_mod_etag["_etag"],
|
||||
m_mod_etag["_mod"] };
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#include <stack>
|
||||
#include <thread>
|
||||
|
||||
#include "mamba/core/channel.hpp"
|
||||
#include "mamba/core/context.hpp"
|
||||
#include "mamba/core/transaction.hpp"
|
||||
#include "mamba/core/link.hpp"
|
||||
#include "mamba/core/match_spec.hpp"
|
||||
|
@ -745,9 +747,27 @@ namespace mamba
|
|||
}
|
||||
if (mamba_repo->url() == "")
|
||||
{
|
||||
// TODO: comment which use case it represents
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& ctx = Context::instance();
|
||||
if (ctx.experimental && ctx.artifact_verif)
|
||||
{
|
||||
const auto& repo_checker
|
||||
= Channel::make_cached_channel(mamba_repo->url()).repo_checker();
|
||||
|
||||
auto pkg_info = PackageInfo(s);
|
||||
|
||||
// TODO: avoid parsing again the index file by storing
|
||||
// keyid/signatures into libsolv Solvable
|
||||
repo_checker.verify_package(fs::path(mamba_repo->index_file()),
|
||||
pkg_info.str() + ".tar.bz2");
|
||||
|
||||
LOG_DEBUG << "Package '" << pkg_info.name << "' trusted from channel '"
|
||||
<< mamba_repo->url() << "' metadata";
|
||||
}
|
||||
|
||||
targets.emplace_back(std::make_unique<PackageDownloadExtractTarget>(s));
|
||||
multi_dl.add(targets[targets.size() - 1]->target(m_cache_path, m_multi_cache));
|
||||
}
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
//
|
||||
// The full license is in the file LICENSE, distributed with this software.
|
||||
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
|
||||
#include "mamba/core/mamba_fs.hpp"
|
||||
#include "mamba/core/url.hpp"
|
||||
#include "mamba/core/util.hpp"
|
||||
|
||||
#include "openssl/md5.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
|
||||
|
||||
namespace mamba
|
||||
{
|
||||
bool has_scheme(const std::string& url)
|
||||
|
@ -171,6 +174,17 @@ namespace mamba
|
|||
throw std::runtime_error("Could not url-unescape string.");
|
||||
}
|
||||
|
||||
std::string cache_name_from_url(const std::string& url)
|
||||
{
|
||||
std::vector<unsigned char> hash(MD5_DIGEST_LENGTH);
|
||||
MD5_CTX md5;
|
||||
MD5_Init(&md5);
|
||||
MD5_Update(&md5, url.c_str(), url.size());
|
||||
MD5_Final(hash.data(), &md5);
|
||||
|
||||
std::string hex_digest = hex_string(hash);
|
||||
return hex_digest.substr(0u, 8u);
|
||||
}
|
||||
|
||||
URLHandler::URLHandler(const std::string& url)
|
||||
: m_url(url)
|
||||
|
|
|
@ -73,6 +73,11 @@ namespace validate
|
|||
}
|
||||
|
||||
package_error::package_error() noexcept
|
||||
: trust_error("Invalid package metadata")
|
||||
{
|
||||
}
|
||||
|
||||
index_error::index_error() noexcept
|
||||
: trust_error("Invalid package index metadata")
|
||||
{
|
||||
}
|
||||
|
@ -1070,7 +1075,7 @@ namespace validate
|
|||
}
|
||||
|
||||
std::unique_ptr<RepoIndexChecker> RootImpl::build_index_checker(
|
||||
const std::string& url) const
|
||||
const std::string& url, const fs::path& cache_path) const
|
||||
{
|
||||
std::unique_ptr<RepoIndexChecker> ptr;
|
||||
return ptr;
|
||||
|
@ -1262,17 +1267,17 @@ namespace validate
|
|||
}
|
||||
|
||||
std::unique_ptr<RepoIndexChecker> RootImpl::build_index_checker(
|
||||
const std::string& base_url) const
|
||||
const std::string& base_url, const fs::path& cache_path) const
|
||||
{
|
||||
fs::path metadata_path = cache_path / "key_mgr.json";
|
||||
|
||||
auto tmp_dir = std::make_unique<mamba::TemporaryDirectory>();
|
||||
auto tmp_dir_path = tmp_dir->path();
|
||||
auto tmp_metadata_path = tmp_dir->path() / "key_mgr.json";
|
||||
|
||||
mamba::URLHandler url(base_url + "key_mgr.json");
|
||||
fs::path tmp_file_path = tmp_dir_path / "key_mgr.json";
|
||||
|
||||
auto dl_target
|
||||
= std::make_unique<mamba::DownloadTarget>("key_mgr.json", url.url(), tmp_file_path);
|
||||
mamba::URLHandler url(base_url + "/key_mgr.json");
|
||||
|
||||
auto dl_target = std::make_unique<mamba::DownloadTarget>(
|
||||
"key_mgr.json", url.url(), tmp_metadata_path);
|
||||
|
||||
if (dl_target->resource_exists())
|
||||
{
|
||||
|
@ -1281,7 +1286,7 @@ namespace validate
|
|||
|
||||
if ((result == CURLE_OK) && dl_target->finalize())
|
||||
{
|
||||
KeyMgrRole key_mgr(tmp_file_path, all_keys()["key_mgr"], spec_impl());
|
||||
KeyMgrRole key_mgr(tmp_metadata_path, all_keys()["key_mgr"], spec_impl());
|
||||
|
||||
// TUF spec 5.6.5 - Check for a freeze attack
|
||||
// 'key_mgr' (equivalent of 'targets') role should not be expired
|
||||
|
@ -1293,10 +1298,25 @@ namespace validate
|
|||
throw freeze_error();
|
||||
}
|
||||
|
||||
return key_mgr.build_index_checker(base_url);
|
||||
// TUF spec 5.6.6 - Persist targets metadata
|
||||
if (!cache_path.empty())
|
||||
{
|
||||
if (fs::exists(metadata_path))
|
||||
fs::remove(metadata_path);
|
||||
fs::copy(tmp_metadata_path, metadata_path);
|
||||
}
|
||||
|
||||
return key_mgr.build_index_checker(base_url, cache_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to local cached-copy if existing
|
||||
if (fs::exists(metadata_path))
|
||||
{
|
||||
KeyMgrRole key_mgr(metadata_path, all_keys()["key_mgr"], spec_impl());
|
||||
return key_mgr.build_index_checker(base_url, cache_path);
|
||||
}
|
||||
|
||||
LOG_ERROR << "Error while fetching 'key_mgr' metadata";
|
||||
throw fetching_error();
|
||||
}
|
||||
|
@ -1383,7 +1403,7 @@ namespace validate
|
|||
}
|
||||
|
||||
std::unique_ptr<RepoIndexChecker> KeyMgrRole::build_index_checker(
|
||||
const std::string& url) const
|
||||
const std::string& url, const fs::path& cache_path) const
|
||||
{
|
||||
auto pkg_mgr = std::make_unique<PkgMgrRole>(create_pkg_mgr());
|
||||
return pkg_mgr;
|
||||
|
@ -1521,7 +1541,7 @@ namespace validate
|
|||
catch (const json::exception& e)
|
||||
{
|
||||
LOG_ERROR << "Invalid package index metadata: " << e.what();
|
||||
throw package_error();
|
||||
throw index_error();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1530,7 +1550,7 @@ namespace validate
|
|||
if (!fs::exists(p))
|
||||
{
|
||||
LOG_ERROR << "'repodata' file not found at: " << p.string();
|
||||
throw role_file_error();
|
||||
throw index_error();
|
||||
}
|
||||
|
||||
std::ifstream i(p);
|
||||
|
@ -1544,7 +1564,40 @@ namespace validate
|
|||
catch (const package_error& e)
|
||||
{
|
||||
LOG_ERROR << "Validation failed on package index: '" << p.string() << "'";
|
||||
throw e;
|
||||
throw index_error();
|
||||
}
|
||||
}
|
||||
void PkgMgrRole::verify_package(const fs::path& index_path,
|
||||
const std::string& pkg_name) const
|
||||
{
|
||||
if (!fs::exists(index_path))
|
||||
{
|
||||
LOG_ERROR << "'repodata' file not found at: " << index_path.string();
|
||||
throw index_error();
|
||||
}
|
||||
|
||||
std::ifstream i(index_path);
|
||||
json j;
|
||||
i >> j;
|
||||
|
||||
try
|
||||
{
|
||||
auto pkg_meta = j.at("packages").at(pkg_name).get<json::object_t>();
|
||||
auto pkg_sigs = j.at("signatures").at(pkg_name).get<json::object_t>();
|
||||
try
|
||||
{
|
||||
check_pkg_signatures(pkg_meta, pkg_sigs);
|
||||
}
|
||||
catch (const threshold_error& e)
|
||||
{
|
||||
LOG_ERROR << "Validation failed on package: '" << pkg_name << "'";
|
||||
throw package_error();
|
||||
}
|
||||
}
|
||||
catch (const json::exception& e)
|
||||
{
|
||||
LOG_ERROR << "Invalid package index metadata: " << e.what();
|
||||
throw index_error();
|
||||
}
|
||||
}
|
||||
} // namespace v06
|
||||
|
@ -1620,71 +1673,146 @@ namespace validate
|
|||
role->set_expiration(j.at(role->spec_version().expiration_json_key()));
|
||||
}
|
||||
|
||||
RepoChecker::RepoChecker(const std::string& url, const fs::path& local_trusted_root)
|
||||
: m_base_url(url)
|
||||
, m_local_trusted_root(local_trusted_root)
|
||||
RepoChecker::RepoChecker(const std::string& base_url,
|
||||
const fs::path& ref_path,
|
||||
const fs::path& cache_path)
|
||||
: m_base_url(base_url)
|
||||
, m_ref_path(ref_path)
|
||||
, m_cache_path(cache_path){};
|
||||
|
||||
const fs::path& RepoChecker::cache_path()
|
||||
{
|
||||
// TUF spec 5.1 - Record fixed update start time
|
||||
// Expiration computations will be done against
|
||||
// this reference
|
||||
// https://theupdateframework.github.io/specification/latest/#fix-time
|
||||
TimeRef::instance().set_now();
|
||||
return m_cache_path;
|
||||
}
|
||||
|
||||
auto root = get_root_role();
|
||||
p_index_checker = root->build_index_checker(url);
|
||||
};
|
||||
void RepoChecker::generate_index_checker()
|
||||
{
|
||||
if (p_index_checker == nullptr)
|
||||
{
|
||||
// TUF spec 5.1 - Record fixed update start time
|
||||
// Expiration computations will be done against
|
||||
// this reference
|
||||
// https://theupdateframework.github.io/specification/latest/#fix-time
|
||||
TimeRef::instance().set_now();
|
||||
|
||||
void RepoChecker::verify_index(const json& j)
|
||||
auto root = get_root_role();
|
||||
p_index_checker = root->build_index_checker(m_base_url, cache_path());
|
||||
|
||||
LOG_INFO << "Index checker successfully generated for '" << m_base_url << "'";
|
||||
}
|
||||
}
|
||||
|
||||
void RepoChecker::verify_index(const json& j) const
|
||||
{
|
||||
p_index_checker->verify_index(j);
|
||||
}
|
||||
|
||||
void RepoChecker::verify_index(const fs::path& p)
|
||||
void RepoChecker::verify_index(const fs::path& p) const
|
||||
{
|
||||
p_index_checker->verify_index(p);
|
||||
}
|
||||
|
||||
void RepoChecker::verify_package(const fs::path& index_path, const std::string& pkg_name) const
|
||||
{
|
||||
p_index_checker->verify_package(index_path, pkg_name);
|
||||
}
|
||||
|
||||
std::size_t RepoChecker::root_version()
|
||||
{
|
||||
return m_root_version;
|
||||
}
|
||||
|
||||
std::unique_ptr<RootRole> RepoChecker::get_root_role()
|
||||
fs::path RepoChecker::ref_root()
|
||||
{
|
||||
std::unique_ptr<RootRole> updated_root;
|
||||
return m_ref_path / "root.json";
|
||||
}
|
||||
|
||||
if (v06::SpecImpl().is_compatible(m_local_trusted_root))
|
||||
fs::path RepoChecker::cached_root()
|
||||
{
|
||||
if (cache_path().empty())
|
||||
{
|
||||
updated_root = std::make_unique<v06::RootImpl>(m_local_trusted_root);
|
||||
}
|
||||
else if (v1::SpecImpl().is_compatible(m_local_trusted_root))
|
||||
{
|
||||
updated_root = std::make_unique<v1::RootImpl>(m_local_trusted_root);
|
||||
return "";
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR << "Invalid spec version for 'root' initial trusted file";
|
||||
throw spec_version_error();
|
||||
return cache_path() / "root.json";
|
||||
}
|
||||
}
|
||||
|
||||
void RepoChecker::persist_file(const fs::path& file_path)
|
||||
{
|
||||
if (fs::exists(cached_root()))
|
||||
fs::remove(cached_root());
|
||||
if (!cached_root().empty())
|
||||
fs::copy(file_path, cached_root());
|
||||
}
|
||||
|
||||
fs::path RepoChecker::initial_trusted_root()
|
||||
{
|
||||
if (fs::exists(cached_root()))
|
||||
{
|
||||
LOG_DEBUG << "Using cache for 'root' initial trusted file";
|
||||
return cached_root();
|
||||
}
|
||||
|
||||
if (!fs::exists(m_ref_path))
|
||||
{
|
||||
LOG_ERROR << "'root' initial trusted file not found at '" << m_ref_path.string()
|
||||
<< "' for repo '" << m_base_url << "'";
|
||||
throw role_file_error();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ref_root();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<RootRole> RepoChecker::get_root_role()
|
||||
{
|
||||
// TUF spec 5.3 - Update the root role
|
||||
// https://theupdateframework.github.io/specification/latest/#update-root
|
||||
|
||||
std::unique_ptr<RootRole> updated_root;
|
||||
|
||||
LOG_DEBUG << "Loading 'root' metadata for repo '" << m_base_url << "'";
|
||||
auto trusted_root = initial_trusted_root();
|
||||
|
||||
if (v06::SpecImpl().is_compatible(trusted_root))
|
||||
{
|
||||
updated_root = std::make_unique<v06::RootImpl>(trusted_root);
|
||||
}
|
||||
else if (v1::SpecImpl().is_compatible(trusted_root))
|
||||
{
|
||||
updated_root = std::make_unique<v1::RootImpl>(trusted_root);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR << "Invalid 'root' initial trusted file '" << trusted_root.string()
|
||||
<< "' for repo '" << m_base_url << "'";
|
||||
throw role_file_error();
|
||||
}
|
||||
|
||||
if (trusted_root != cached_root())
|
||||
persist_file(trusted_root);
|
||||
|
||||
auto update_files = updated_root->possible_update_files();
|
||||
auto tmp_dir = std::make_unique<mamba::TemporaryDirectory>();
|
||||
auto tmp_dir_path = tmp_dir->path();
|
||||
|
||||
// do chained updates
|
||||
while (true)
|
||||
LOG_DEBUG << "Starting updates of 'root' metadata";
|
||||
do
|
||||
{
|
||||
fs::path tmp_file_path;
|
||||
|
||||
// Update from the most recent spec supported by this client
|
||||
for (auto& f : update_files)
|
||||
{
|
||||
mamba::URLHandler url(m_base_url + f.string());
|
||||
auto url = mamba::concat(m_base_url, "/", f.string());
|
||||
tmp_file_path = tmp_dir_path / f;
|
||||
|
||||
auto dl_target
|
||||
= std::make_unique<mamba::DownloadTarget>(f.string(), url.url(), tmp_file_path);
|
||||
// dl_target->set_ignore_failure(true);
|
||||
= std::make_unique<mamba::DownloadTarget>(f.string(), url, tmp_file_path);
|
||||
|
||||
if (dl_target->resource_exists())
|
||||
{
|
||||
|
@ -1693,8 +1821,6 @@ namespace validate
|
|||
|
||||
if ((result == CURLE_OK) && dl_target->finalize())
|
||||
{
|
||||
updated_root = updated_root->update(tmp_file_path);
|
||||
update_files = updated_root->possible_update_files();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1703,7 +1829,16 @@ namespace validate
|
|||
|
||||
if (tmp_file_path.empty())
|
||||
break;
|
||||
|
||||
updated_root = updated_root->update(tmp_file_path);
|
||||
// TUF spec 5.3.8 - Persist root metadata
|
||||
// Updated 'root' metadata are persisted in a cache directory
|
||||
persist_file(tmp_file_path);
|
||||
|
||||
update_files = updated_root->possible_update_files();
|
||||
}
|
||||
// TUF spec 5.3.9 - Repeat steps 5.3.2 to 5.3.9
|
||||
while (true);
|
||||
|
||||
m_root_version = updated_root->version();
|
||||
LOG_DEBUG << "Latest 'root' metadata has version " << m_root_version;
|
||||
|
|
|
@ -326,4 +326,7 @@ init_install_options(CLI::App* subcom)
|
|||
safety_checks.set_cli_config(""),
|
||||
{ "enabled", "warn", "disabled" },
|
||||
safety_checks.description());
|
||||
|
||||
auto& av = config.at("artifact_verif").get_wrapped<bool>();
|
||||
subcom->add_flag("--artifact-verif", av.set_cli_config(0), av.description());
|
||||
}
|
||||
|
|
|
@ -1026,7 +1026,7 @@ namespace validate
|
|||
PkgMgrT_v06()
|
||||
: KeyMgrT_v06()
|
||||
{
|
||||
init_repodata();
|
||||
generate_index_checkerdata();
|
||||
root = std::make_unique<RootImpl>(root1_json);
|
||||
};
|
||||
|
||||
|
@ -1055,7 +1055,7 @@ namespace validate
|
|||
|
||||
std::unique_ptr<RootImpl> root;
|
||||
|
||||
void init_repodata()
|
||||
void generate_index_checkerdata()
|
||||
{
|
||||
repodata_json = R"({
|
||||
"info": {
|
||||
|
@ -1133,7 +1133,7 @@ namespace validate
|
|||
{ "op": "remove", "path": "/signatures"}
|
||||
])"_json;
|
||||
EXPECT_THROW(pkg_mgr.verify_index(signed_repodata_json.patch(hillformed_pkg_patch)),
|
||||
package_error);
|
||||
index_error);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1143,8 +1143,8 @@ namespace validate
|
|||
RepoCheckerT()
|
||||
: PkgMgrT_v06()
|
||||
{
|
||||
m_repo_base_url = "file://" + channel_dir->path().string() + "/";
|
||||
m_trusted_root = channel_dir->path() / "root.json";
|
||||
m_repo_base_url = "file://" + channel_dir->path().string();
|
||||
m_ref_path = channel_dir->path();
|
||||
|
||||
write_role(root1_json, channel_dir->path() / "root.json");
|
||||
|
||||
|
@ -1164,7 +1164,7 @@ namespace validate
|
|||
}
|
||||
|
||||
protected:
|
||||
std::string m_trusted_root, m_repo_base_url;
|
||||
std::string m_ref_path, m_repo_base_url;
|
||||
|
||||
void write_role(const json& j, const fs::path& p)
|
||||
{
|
||||
|
@ -1178,13 +1178,15 @@ namespace validate
|
|||
|
||||
TEST_F(RepoCheckerT, ctor)
|
||||
{
|
||||
RepoChecker checker(m_repo_base_url, m_trusted_root);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
checker.generate_index_checker();
|
||||
EXPECT_EQ(checker.root_version(), 2);
|
||||
}
|
||||
|
||||
TEST_F(RepoCheckerT, verify_index)
|
||||
{
|
||||
RepoChecker checker(m_repo_base_url, m_trusted_root);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
checker.generate_index_checker();
|
||||
checker.verify_index(signed_repodata_json);
|
||||
}
|
||||
|
||||
|
@ -1196,8 +1198,8 @@ namespace validate
|
|||
+ timestamp(utc_time_now() - 10) + R"(" }
|
||||
])");
|
||||
write_role(create_root_update_json(patch), channel_dir->path() / "2.root.json");
|
||||
|
||||
EXPECT_THROW(RepoChecker checker(m_repo_base_url, m_trusted_root), freeze_error);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
EXPECT_THROW(checker.generate_index_checker(), freeze_error);
|
||||
}
|
||||
|
||||
TEST_F(RepoCheckerT, key_mgr_freeze_attack)
|
||||
|
@ -1207,36 +1209,39 @@ namespace validate
|
|||
+ timestamp(utc_time_now() - 10) + R"(" }
|
||||
])");
|
||||
write_role(patched_key_mgr_json(patch), channel_dir->path() / "key_mgr.json");
|
||||
|
||||
EXPECT_THROW(RepoChecker checker(m_repo_base_url, m_trusted_root), freeze_error);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
EXPECT_THROW(checker.generate_index_checker(), freeze_error);
|
||||
}
|
||||
|
||||
TEST_F(RepoCheckerT, missing_key_mgr_file)
|
||||
{
|
||||
fs::remove(channel_dir->path() / "key_mgr.json");
|
||||
EXPECT_THROW(RepoChecker checker(m_repo_base_url, m_trusted_root), fetching_error);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
EXPECT_THROW(checker.generate_index_checker(), fetching_error);
|
||||
}
|
||||
|
||||
TEST_F(RepoCheckerT, corrupted_repodata)
|
||||
{
|
||||
RepoChecker checker(m_repo_base_url, m_trusted_root);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
|
||||
json wrong_pkg_patch = R"([
|
||||
{ "op": "replace", "path": "/packages/test-package1-0.1-0.tar.bz2/version", "value": "0.1.1" }
|
||||
])"_json;
|
||||
checker.generate_index_checker();
|
||||
EXPECT_THROW(checker.verify_index(signed_repodata_json.patch(wrong_pkg_patch)),
|
||||
package_error);
|
||||
}
|
||||
|
||||
TEST_F(RepoCheckerT, hillformed_repodata)
|
||||
{
|
||||
RepoChecker checker(m_repo_base_url, m_trusted_root);
|
||||
RepoChecker checker(m_repo_base_url, m_ref_path);
|
||||
|
||||
json hillformed_pkg_patch = R"([
|
||||
{ "op": "remove", "path": "/signatures"}
|
||||
])"_json;
|
||||
checker.generate_index_checker();
|
||||
EXPECT_THROW(checker.verify_index(signed_repodata_json.patch(hillformed_pkg_patch)),
|
||||
package_error);
|
||||
index_error);
|
||||
}
|
||||
} // namespace testing
|
||||
} // namespace v06
|
||||
|
|
Loading…
Reference in New Issue