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:
Adrien DELSALLE 2021-06-02 17:02:13 +02:00
parent a1f7f9b835
commit 58b486d737
No known key found for this signature in database
GPG Key ID: 639D9226C33B92BB
19 changed files with 409 additions and 116 deletions

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
{

View File

@ -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;

View File

@ -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")

View File

@ -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());

View File

@ -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;

View File

@ -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 "";
}

View File

@ -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())
{

View File

@ -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;

View File

@ -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"] };

View File

@ -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));
}

View File

@ -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)

View File

@ -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;

View File

@ -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());
}

View File

@ -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