Split validate.[ch]pp (#3041)

* Split validation errors

* Split validation keys

* Fix header include

* Split validation versioned namespaces

* Trailing return types

* Split update framework implementation

* Finish validation split

* Fix missing header
This commit is contained in:
Antoine Prouvost 2023-12-06 19:34:19 +01:00 committed by GitHub
parent 3cb488bb61
commit c529199eef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 3313 additions and 3114 deletions

View File

@ -178,6 +178,14 @@ set(
${LIBMAMBA_SOURCE_DIR}/specs/repo_data.cpp
${LIBMAMBA_SOURCE_DIR}/specs/version.cpp
${LIBMAMBA_SOURCE_DIR}/specs/version_spec.cpp
# Artifacts validation
${LIBMAMBA_SOURCE_DIR}/validation/tools.cpp
${LIBMAMBA_SOURCE_DIR}/validation/errors.cpp
${LIBMAMBA_SOURCE_DIR}/validation/keys.cpp
${LIBMAMBA_SOURCE_DIR}/validation/update_framework.cpp
${LIBMAMBA_SOURCE_DIR}/validation/update_framework_v0_6.cpp
${LIBMAMBA_SOURCE_DIR}/validation/update_framework_v1.cpp
${LIBMAMBA_SOURCE_DIR}/validation/repo_checker.cpp
# C++ wrapping of libcurl
${LIBMAMBA_SOURCE_DIR}/core/curl.cpp
# C++ wrapping of compression libs (zstd and bzlib)
@ -220,7 +228,6 @@ set(
${LIBMAMBA_SOURCE_DIR}/core/util.cpp
${LIBMAMBA_SOURCE_DIR}/core/fsutil.cpp
${LIBMAMBA_SOURCE_DIR}/core/util_os.cpp
${LIBMAMBA_SOURCE_DIR}/core/validate.cpp
${LIBMAMBA_SOURCE_DIR}/core/virtual_packages.cpp
${LIBMAMBA_SOURCE_DIR}/core/env_lockfile.cpp
${LIBMAMBA_SOURCE_DIR}/core/execution.cpp
@ -283,6 +290,14 @@ set(
${LIBMAMBA_INCLUDE_DIR}/mamba/specs/repo_data.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/specs/version.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/specs/version_spec.hpp
# Artifacts validation
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/tools.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/errors.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/keys.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/update_framework.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/update_framework_v0_6.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/update_framework_v1.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/validation/repo_checker.hpp
# Core API (low-level)
${LIBMAMBA_INCLUDE_DIR}/mamba/core/activation.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/channel_context.hpp
@ -323,7 +338,6 @@ set(
${LIBMAMBA_INCLUDE_DIR}/mamba/core/util_os.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/util_random.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/util_scope.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/validate.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/virtual_packages.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/env_lockfile.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/core/tasksync.hpp

View File

@ -1,807 +0,0 @@
// Copyright (c) 2019, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_CORE_VALIDATE_HPP
#define MAMBA_CORE_VALIDATE_HPP
#include <set>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "mamba/core/timeref.hpp"
#include "mamba/core/util.hpp"
#include "mamba/fs/filesystem.hpp"
namespace mamba::validation
{
[[nodiscard]] auto sha256sum(const fs::u8path& path) -> std::string_view;
[[nodiscard]] auto md5sum(const fs::u8path& path) -> std::string_view;
bool file_size(const fs::u8path& path, std::uintmax_t validation);
inline constexpr std::size_t MAMBA_SHA256_SIZE_HEX = 64;
inline constexpr std::size_t MAMBA_SHA256_SIZE_BYTES = 32;
inline constexpr std::size_t MAMBA_MD5_SIZE_HEX = 32;
inline constexpr std::size_t MAMBA_MD5_SIZE_BYTES = 16;
inline constexpr std::size_t MAMBA_ED25519_KEYSIZE_HEX = 64;
inline constexpr std::size_t MAMBA_ED25519_KEYSIZE_BYTES = 32;
inline constexpr std::size_t MAMBA_ED25519_SIGSIZE_HEX = 128;
inline constexpr std::size_t MAMBA_ED25519_SIGSIZE_BYTES = 64;
int generate_ed25519_keypair(std::byte* pk, std::byte* sk);
std::pair<std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>, std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>>
generate_ed25519_keypair();
std::pair<std::string, std::string> generate_ed25519_keypair_hex();
int sign(const std::string& data, const std::byte* sk, std::byte* signature);
int sign(const std::string& data, const std::string& sk, std::string& signature);
std::array<std::byte, MAMBA_ED25519_SIGSIZE_BYTES>
ed25519_sig_hex_to_bytes(const std::string& sig_hex, int& error_code) noexcept;
std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>
ed25519_key_hex_to_bytes(const std::string& key_hex, int& error_code) noexcept;
int
verify(const std::byte* data, std::size_t data_len, const std::byte* pk, const std::byte* signature);
int verify(const std::string& data, const std::byte* pk, const std::byte* signature);
int verify(const std::string& data, const std::string& pk_hex, const std::string& signature_hex);
/**
* Verify a GPG/PGP signature against the hash of the binary data and
* the additional trailer added in V4 signature.
* See RFC4880, section 5.2.4 https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.4
* This method assumes hash function to be SHA-256
*/
int verify_gpg_hashed_msg(const std::byte* data, const std::byte* pk, const std::byte* signature);
int
verify_gpg_hashed_msg(const std::string& data, const std::byte* pk, const std::byte* signature);
int
verify_gpg_hashed_msg(const std::string& data, const std::string& pk, const std::string& signature);
/**
* Verify a GPG/PGP signature against the binary data and
* the additional trailer added in V4 signature.
* See RFC4880, section 5.2.4 https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.4
* This method assumes hash function to be SHA-256
*/
int verify_gpg(
const std::string& data,
const std::string& gpg_v4_trailer,
const std::string& pk,
const std::string& signature
);
/**
* Base class for artifact/package verification error.
*/
class trust_error : public std::exception
{
public:
trust_error(std::string_view message);
~trust_error() override = default;
[[nodiscard]] auto what() const noexcept -> const char* override;
private:
std::string m_message;
};
/**
* Error raised when a threshold of signatures is
* not met. This can be due to wrong signatures,
* wrong or missing public keys.
*/
class threshold_error : public trust_error
{
public:
threshold_error();
~threshold_error() override = default;
};
/**
* Error raised when wrong metadata are spotted
* in a role file.
*/
class role_metadata_error : public trust_error
{
public:
role_metadata_error();
~role_metadata_error() override = default;
};
/**
* Error raised when a wrong file name is
* detected for role metadata.
*/
class role_file_error : public trust_error
{
public:
role_file_error();
~role_file_error() override = default;
};
/**
* Error raised when a possible rollback
* attack is detected.
*/
class rollback_error : public trust_error
{
public:
rollback_error();
~rollback_error() override = default;
};
/**
* Error raised when a possible freeze
* attack is detected.
*/
class freeze_error : public trust_error
{
public:
freeze_error();
~freeze_error() override = default;
};
/**
* Error raised when a spec version is either
* wrong/invalid or not supported by the client.
*/
class spec_version_error : public trust_error
{
public:
spec_version_error();
~spec_version_error() override = default;
};
/**
* Error raised when a role metadata file
* fetching process fails.
*/
class fetching_error : public trust_error
{
public:
fetching_error();
~fetching_error() override = default;
};
/**
* Error raised when signatures threshold
* is not met for a package.
*/
class package_error : public trust_error
{
public:
package_error();
~package_error() override = default;
};
/**
* Error raised when signatures threshold
* is not met for a trust role.
*/
class role_error : public trust_error
{
public:
role_error();
~role_error() override = default;
};
/**
* Error raised when an invalid package
* index is met.
*/
class index_error : public trust_error
{
public:
index_error();
~index_error() override = default;
};
void check_timestamp_metadata_format(const std::string& ts);
/**
* Representation of the public part of a
* cryptographic key pair.
*/
struct Key
{
std::string keytype = "";
std::string scheme = "";
std::string keyval = "";
static Key from_ed25519(std::string keyval)
{
return { "ed25519", "ed25519", keyval };
}
};
void to_json(nlohmann::json& j, const Key& k);
void from_json(const nlohmann::json& j, Key& k);
/**
* Representation of a role signature.
* Optional 'pgp_trailer' will trigger special
* handling during verification to conform to
* OpenPGP RFC4880.
*/
struct RoleSignature
{
std::string keyid = "";
std::string sig = "";
std::string pgp_trailer = "";
};
void to_json(nlohmann::json& j, const RoleSignature& rs);
void from_json(const nlohmann::json& j, RoleSignature& rs);
bool operator<(const RoleSignature& rs1, const RoleSignature& rs2);
/**
* Store key IDs and threshold for a role.
* Key ID can be a hash of Key, or just
* its public key value.
*/
struct RoleKeys
{
std::vector<std::string> keyids;
std::size_t threshold;
};
void to_json(nlohmann::json& j, const RoleKeys& rk);
void from_json(const nlohmann::json& j, RoleKeys& rk);
/**
* Store key values and threshold for role.
* Assumes key scheme/type is `ed25519`.
*/
struct RolePubKeys
{
std::vector<std::string> pubkeys;
std::size_t threshold;
RoleKeys to_role_keys() const;
};
void to_json(nlohmann::json& j, const RolePubKeys& rk);
void from_json(const nlohmann::json& j, RolePubKeys& rk);
/**
* Store full keys and threshold for role.
*/
struct RoleFullKeys
{
RoleFullKeys() = default;
RoleFullKeys(const std::map<std::string, Key>& keys_, const std::size_t& threshold_);
std::map<std::string, Key> keys;
std::size_t threshold;
std::map<std::string, Key> to_keys() const;
RoleKeys to_roles() const;
};
void to_json(nlohmann::json& j, const RoleFullKeys& r);
void from_json(const nlohmann::json& j, RoleFullKeys& r);
/**
* Base class for spec implementations.
*/
class SpecBase
{
public:
virtual ~SpecBase() = default;
std::string version_str() const;
virtual std::string canonicalize(const nlohmann::json& j) const;
std::string compatible_prefix() const;
std::vector<std::string> upgrade_prefix() const;
bool is_compatible(const fs::u8path& p) const;
bool is_compatible(const nlohmann::json& j) const;
bool is_compatible(const std::string& version) const;
bool is_upgrade(const nlohmann::json& j) const;
bool is_upgrade(const std::string& version) const;
virtual bool upgradable() const;
virtual std::string json_key() const = 0;
virtual std::string expiration_json_key() const = 0;
virtual std::set<RoleSignature> signatures(const nlohmann::json& j) const = 0;
protected:
SpecBase(const std::string& spec_version);
SpecBase() = delete;
std::string get_json_value(const nlohmann::json& j) const;
private:
std::string m_spec_version;
};
bool operator==(const SpecBase& sv1, const SpecBase& sv2);
bool operator!=(const SpecBase& sv1, const SpecBase& sv2);
/**
* Base class for role implementation.
*/
class RoleBase
{
public:
RoleBase(const std::string& type, std::shared_ptr<SpecBase> sv);
virtual ~RoleBase() = 0;
std::string type() const;
SpecBase& spec_version() const;
std::size_t version() const;
std::string file_ext() const;
std::string expires() const;
bool expired(const TimeRef& time_reference) const;
std::set<std::string> roles() const;
std::set<RoleSignature> signatures(const nlohmann::json& j) const;
virtual RoleFullKeys self_keys() const = 0;
std::map<std::string, RoleFullKeys> all_keys() const;
friend void to_json(nlohmann::json& j, const RoleBase* r);
friend void from_json(const nlohmann::json& j, RoleBase* r);
protected:
nlohmann::json read_json_file(const fs::u8path& p, bool update = false) const;
/**
* Check that a threshold of valid signatures is met
* for the signed metadata of a role, using another
* role keys (possibly the same).
* Both signed and signatures metadata are contained
* in 'data'.
*/
void check_role_signatures(const nlohmann::json& data, const RoleBase& role);
/**
* Check that a threshold of valid signatures is met
* for the signed metadata, using a set of keys.
*/
void check_signatures(
const std::string& signed_data,
const std::set<RoleSignature>& signatures,
const RoleFullKeys& keyring
) const;
void set_spec_version(std::shared_ptr<SpecBase> sv);
void set_expiration(const std::string& expires);
// Forwarding to spec implementation
std::string canonicalize(const nlohmann::json& j) const;
// Return the spec implementation
std::shared_ptr<SpecBase> spec_impl() const;
// Mandatory roles defined by the current role
virtual std::set<std::string> mandatory_defined_roles() const;
// Optional roles defined by the current role
virtual std::set<std::string> optionally_defined_roles() const;
// Check role
void check_expiration_format() const;
void check_defined_roles(bool allow_any = false) const;
std::map<std::string, RoleFullKeys> m_defined_roles;
private:
std::string m_internal_type;
std::string m_type;
std::shared_ptr<SpecBase> p_spec;
std::size_t m_version = 1;
std::string m_expires;
std::string m_ext = "json";
};
// Forward declaration of RepoIndexChecker.
class RepoIndexChecker;
/**
* 'root' role interface.
*/
class RootRole : public RoleBase
{
public:
virtual ~RootRole() = default;
std::unique_ptr<RootRole> update(fs::u8path path);
std::unique_ptr<RootRole> update(nlohmann::json j);
std::vector<fs::u8path> possible_update_files();
virtual std::unique_ptr<RepoIndexChecker>
build_index_checker(Context& context, const TimeRef& time_reference, const std::string& url, const fs::u8path& cache_path) const = 0;
protected:
RootRole(std::shared_ptr<SpecBase> spec);
private:
virtual std::unique_ptr<RootRole> create_update(const nlohmann::json& j) = 0;
};
/**
* Interface that performs validity checks
* on a repository packages index.
*/
class RepoIndexChecker
{
public:
virtual ~RepoIndexChecker() = default;
virtual void verify_index(const nlohmann::json& j) const = 0;
virtual void verify_index(const fs::u8path& p) const = 0;
virtual void verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const = 0;
protected:
RepoIndexChecker() = default;
};
/**
* Perform security check against a repository
* package index using cryptographic signatures.
* Relies on multiple roles defined in TUF specification.
*/
class RepoChecker
{
public:
/**
* 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(
Context& context,
const std::string& base_url,
const fs::u8path& ref_path,
const fs::u8path& cache_path = ""
);
// Forwarding to a ``RepoIndexChecker`` implementation
void verify_index(const nlohmann::json& j) const;
void verify_index(const fs::u8path& p) const;
void
verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const;
void generate_index_checker();
const fs::u8path& cache_path();
std::size_t root_version();
private:
std::string m_base_url;
std::size_t m_root_version = 0;
fs::u8path m_ref_path;
fs::u8path m_cache_path;
Context& m_context;
fs::u8path initial_trusted_root();
fs::u8path ref_root();
fs::u8path cached_root();
void persist_file(const fs::u8path& file_path);
std::unique_ptr<RepoIndexChecker> p_index_checker;
std::unique_ptr<RootRole> get_root_role(const TimeRef& time_reference);
};
namespace v1
{
/**
* TUF v1 specific implementation.
*/
class SpecImpl final : public SpecBase
{
public:
SpecImpl(const std::string& sv = "1.0.17");
std::string json_key() const override;
std::string expiration_json_key() const override;
std::set<RoleSignature> signatures(const nlohmann::json& j) const override;
};
/**
* 'root' role implementation.
* TUF v1.0.17 §2.1.1
* https://theupdateframework.github.io/specification/latest/#root
*/
class RootImpl final : public RootRole
{
public:
RootImpl(const fs::u8path& p);
RootImpl(const nlohmann::json& j);
RoleFullKeys self_keys() const override;
std::unique_ptr<RepoIndexChecker> build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const override;
friend void to_json(nlohmann::json& j, const RootImpl& r);
friend void from_json(const nlohmann::json& j, RootImpl& r);
private:
RootImpl() = delete;
void load_from_json(const nlohmann::json& j);
std::unique_ptr<RootRole> create_update(const nlohmann::json& j) override;
std::set<std::string> mandatory_defined_roles() const override;
std::set<std::string> optionally_defined_roles() const override;
void
set_defined_roles(std::map<std::string, Key> keys, std::map<std::string, RoleKeys> roles);
};
}
namespace v06
{
/**
* ``conda-content-trust`` v0.6.0 specific implementation.
* This is a variation of TUF specification.
*/
class SpecImpl final : public SpecBase
{
public:
SpecImpl(const std::string& sv = "0.6.0");
std::string json_key() const override;
std::string expiration_json_key() const override;
std::set<RoleSignature> signatures(const nlohmann::json& j) const override;
std::string canonicalize(const nlohmann::json& j) const override;
bool upgradable() const override;
};
class V06RoleBaseExtension
{
public:
void set_timestamp(const std::string& ts);
std::string timestamp() const;
protected:
std::string m_timestamp;
void check_timestamp_format() const;
};
// Forward declaration of KeyMgrRole.
class KeyMgrRole;
/**
* 'root' role implementation.
*/
class RootImpl final
: public RootRole
, public V06RoleBaseExtension
{
public:
RootImpl(const fs::u8path& p);
RootImpl(const nlohmann::json& j);
RootImpl(const std::string& json_str);
/**
* Return a ``RepoIndexChecker`` implementation (derived class)
* from repository base URL.
*/
std::unique_ptr<RepoIndexChecker> build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const override;
RoleFullKeys self_keys() const override;
nlohmann::json upgraded_signable() const;
RoleSignature
upgraded_signature(const nlohmann::json& j, const std::string& pk, const std::byte* sk) const;
KeyMgrRole create_key_mgr(const fs::u8path& p) const;
KeyMgrRole create_key_mgr(const nlohmann::json& j) const;
friend void to_json(nlohmann::json& j, const RootImpl& r);
friend void from_json(const nlohmann::json& j, RootImpl& r);
private:
RootImpl() = delete;
void load_from_json(const nlohmann::json& j);
std::unique_ptr<RootRole> create_update(const nlohmann::json& j) override;
std::set<std::string> mandatory_defined_roles() const override;
std::set<std::string> optionally_defined_roles() const override;
void set_defined_roles(std::map<std::string, RolePubKeys> keys);
};
// Forward declaration of KeyMgrRole.
class PkgMgrRole;
/**
* 'key_mgr' role implementation.
*/
class KeyMgrRole final
: public RoleBase
, public V06RoleBaseExtension
{
public:
KeyMgrRole(const fs::u8path& p, const RoleFullKeys& keys, const std::shared_ptr<SpecBase> spec);
KeyMgrRole(
const nlohmann::json& j,
const RoleFullKeys& keys,
const std::shared_ptr<SpecBase> spec
);
KeyMgrRole(
const std::string& json_str,
const RoleFullKeys& keys,
const std::shared_ptr<SpecBase> spec
);
// std::set<std::string> roles() const override;
RoleFullKeys self_keys() const override;
PkgMgrRole create_pkg_mgr(const fs::u8path& p) const;
PkgMgrRole create_pkg_mgr(const nlohmann::json& j) const;
/**
* Return a ``RepoIndexChecker`` implementation (derived class)
* from repository base URL.
*/
std::unique_ptr<RepoIndexChecker> build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const;
friend void to_json(nlohmann::json& j, const KeyMgrRole& r);
friend void from_json(const nlohmann::json& j, KeyMgrRole& r);
private:
KeyMgrRole() = delete;
void load_from_json(const nlohmann::json& j);
RoleFullKeys m_keys;
std::map<std::string, RolePubKeys> m_delegations;
std::set<std::string> mandatory_defined_roles() const override;
std::set<std::string> optionally_defined_roles() const override;
void set_defined_roles(std::map<std::string, RolePubKeys> keys);
};
/**
* 'pkg_mgr' role implementation.
* This role inherits from ``RepoIndexChecker`` and
* will be used by ``RepoChecker`` to perform the
* repository index verification.
*/
class PkgMgrRole final
: public RoleBase
, public V06RoleBaseExtension
, public RepoIndexChecker
{
public:
PkgMgrRole(const RoleFullKeys& keys, const std::shared_ptr<SpecBase> spec);
PkgMgrRole(const fs::u8path& p, const RoleFullKeys& keys, const std::shared_ptr<SpecBase> spec);
PkgMgrRole(
const nlohmann::json& j,
const RoleFullKeys& keys,
const std::shared_ptr<SpecBase> spec
);
PkgMgrRole(
const std::string& json_str,
const RoleFullKeys& keys,
const std::shared_ptr<SpecBase> spec
);
void verify_index(const fs::u8path& p) const override;
void verify_index(const nlohmann::json& j) const override;
void verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures)
const override;
friend void to_json(nlohmann::json& j, const PkgMgrRole& r);
friend void from_json(const nlohmann::json& j, PkgMgrRole& r);
private:
PkgMgrRole() = delete;
void load_from_json(const nlohmann::json& j);
RoleFullKeys self_keys() const override;
std::set<RoleSignature> pkg_signatures(const nlohmann::json& j) const;
void
check_pkg_signatures(const nlohmann::json& signed_data, const nlohmann::json& signatures) const;
void set_defined_roles(std::map<std::string, RolePubKeys> keys);
RoleFullKeys m_keys;
friend class KeyMgrRole;
};
}
} // namespace validate
#endif // MAMBA_VALIDATE_HPP

View File

@ -0,0 +1,146 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_ERRORS_HPP
#define MAMBA_VALIDATION_ERRORS_HPP
#include <exception>
#include <string>
#include <string_view>
namespace mamba::validation
{
/**
* Base class for artifact/package verification error.
*/
class trust_error : public std::exception
{
public:
trust_error(std::string_view message);
~trust_error() override = default;
[[nodiscard]] auto what() const noexcept -> const char* override;
private:
std::string m_message;
};
/**
* Error raised when a threshold of signatures is not met.
*
* This can be due to wrong signatures, wrong or missing public keys.
*/
class threshold_error : public trust_error
{
public:
threshold_error();
~threshold_error() override = default;
};
/**
* Error raised when wrong metadata are spotted
* in a role file.
*/
class role_metadata_error : public trust_error
{
public:
role_metadata_error();
~role_metadata_error() override = default;
};
/**
* Error raised when a wrong file name is
* detected for role metadata.
*/
class role_file_error : public trust_error
{
public:
role_file_error();
~role_file_error() override = default;
};
/**
* Error raised when a possible rollback attack is detected.
*/
class rollback_error : public trust_error
{
public:
rollback_error();
~rollback_error() override = default;
};
/**
* Error raised when a possible freeze attack is detected.
*/
class freeze_error : public trust_error
{
public:
freeze_error();
~freeze_error() override = default;
};
/**
* Error raised when a spec version is either wrong/invalid or not supported by the client.
*/
class spec_version_error : public trust_error
{
public:
spec_version_error();
~spec_version_error() override = default;
};
/**
* Error raised when a role metadata file fetching process fails.
*/
class fetching_error : public trust_error
{
public:
fetching_error();
~fetching_error() override = default;
};
/**
* Error raised when signatures threshold is not met for a package.
*/
class package_error : public trust_error
{
public:
package_error();
~package_error() override = default;
};
/**
* Error raised when signatures threshold is not met for a trust role.
*/
class role_error : public trust_error
{
public:
role_error();
~role_error() override = default;
};
/**
* Error raised when an invalid package index is met.
*/
class index_error : public trust_error
{
public:
index_error();
~index_error() override = default;
};
}
#endif

View File

@ -0,0 +1,100 @@
// Copyright (c) 2019, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_UPDATE_FRAMEWORK_ROLES_HPP
#define MAMBA_VALIDATION_UPDATE_FRAMEWORK_ROLES_HPP
#include <map>
#include <string>
#include <nlohmann/json_fwd.hpp>
namespace mamba::validation
{
/**
* Representation of the public part of a cryptographic key pair.
*/
struct Key
{
std::string keytype = "";
std::string scheme = "";
std::string keyval = "";
[[nodiscard]] static auto from_ed25519(std::string keyval) -> Key;
};
void to_json(nlohmann::json& j, const Key& k);
void from_json(const nlohmann::json& j, Key& k);
/**
* Representation of a role signature.
*
* Optional 'pgp_trailer' will trigger special handling during verification to conform to
* OpenPGP RFC4880.
*/
struct RoleSignature
{
std::string keyid = "";
std::string sig = "";
std::string pgp_trailer = "";
};
void to_json(nlohmann::json& j, const RoleSignature& rs);
void from_json(const nlohmann::json& j, RoleSignature& rs);
[[nodiscard]] auto operator<(const RoleSignature& rs1, const RoleSignature& rs2) -> bool;
/**
* Store key IDs and threshold for a role.
*
* Key ID can be a hash of Key, or just its public key value.
*/
struct RoleKeys
{
std::vector<std::string> keyids;
std::size_t threshold;
};
void to_json(nlohmann::json& j, const RoleKeys& rk);
void from_json(const nlohmann::json& j, RoleKeys& rk);
/**
* Store key values and threshold for role. Assumes key scheme/type is `ed25519`.
*/
struct RolePubKeys
{
std::vector<std::string> pubkeys;
std::size_t threshold;
[[nodiscard]] auto to_role_keys() const -> RoleKeys;
};
void to_json(nlohmann::json& j, const RolePubKeys& rk);
void from_json(const nlohmann::json& j, RolePubKeys& rk);
/**
* Store full keys and threshold for role.
*/
struct RoleFullKeys
{
RoleFullKeys() = default;
RoleFullKeys(const std::map<std::string, Key>& keys_, const std::size_t& threshold_);
std::map<std::string, Key> keys;
std::size_t threshold;
[[nodiscard]] auto to_keys() const -> std::map<std::string, Key>;
[[nodiscard]] auto to_roles() const -> RoleKeys;
};
void to_json(nlohmann::json& j, const RoleFullKeys& r);
void from_json(const nlohmann::json& j, RoleFullKeys& r);
}
#endif

View File

@ -0,0 +1,77 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_REPO_CHECKER_HPP
#define MAMBA_VALIDATION_REPO_CHECKER_HPP
#include <memory>
#include <string>
#include <nlohmann/json_fwd.hpp>
#include "mamba/fs/filesystem.hpp"
namespace mamba
{
class Context;
}
namespace mamba::validation
{
class RepoIndexChecker;
class RootRole;
class TimeRef;
/**
* Perform security check against a repository package index using cryptographic signatures.
*
* Relies on multiple roles defined in The Update Framework specification.
*/
class RepoChecker
{
public:
/**
* 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(Context& context, std::string base_url, fs::u8path ref_path, fs::u8path cache_path = "");
~RepoChecker();
// Forwarding to a ``RepoIndexChecker`` implementation
void verify_index(const nlohmann::json& j) const;
void verify_index(const fs::u8path& p) const;
void
verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const;
void generate_index_checker();
auto cache_path() -> const fs::u8path&;
auto root_version() -> std::size_t;
private:
std::string m_base_url;
std::size_t m_root_version = 0;
fs::u8path m_ref_path;
fs::u8path m_cache_path;
Context& m_context;
auto initial_trusted_root() -> fs::u8path;
auto ref_root() -> fs::u8path;
auto cached_root() -> fs::u8path;
void persist_file(const fs::u8path& file_path);
std::unique_ptr<RepoIndexChecker> p_index_checker;
auto get_root_role(const TimeRef& time_reference) -> std::unique_ptr<RootRole>;
};
}
#endif

View File

@ -0,0 +1,88 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_TOOLS_HPP
#define MAMBA_VALIDATION_TOOLS_HPP
#include <array>
#include <cstddef>
#include <string>
namespace mamba::fs
{
class u8path;
}
namespace mamba::validation
{
[[nodiscard]] auto sha256sum(const fs::u8path& path) -> std::string_view;
[[nodiscard]] auto md5sum(const fs::u8path& path) -> std::string_view;
auto file_size(const fs::u8path& path, std::uintmax_t validation) -> bool;
inline constexpr std::size_t MAMBA_SHA256_SIZE_HEX = 64;
inline constexpr std::size_t MAMBA_SHA256_SIZE_BYTES = 32;
inline constexpr std::size_t MAMBA_MD5_SIZE_HEX = 32;
inline constexpr std::size_t MAMBA_MD5_SIZE_BYTES = 16;
inline constexpr std::size_t MAMBA_ED25519_KEYSIZE_HEX = 64;
inline constexpr std::size_t MAMBA_ED25519_KEYSIZE_BYTES = 32;
inline constexpr std::size_t MAMBA_ED25519_SIGSIZE_HEX = 128;
inline constexpr std::size_t MAMBA_ED25519_SIGSIZE_BYTES = 64;
auto generate_ed25519_keypair(std::byte* pk, std::byte* sk) -> int;
auto generate_ed25519_keypair() -> std::pair<
std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>,
std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>>;
auto generate_ed25519_keypair_hex() -> std::pair<std::string, std::string>;
auto sign(const std::string& data, const std::byte* sk, std::byte* signature) -> int;
auto sign(const std::string& data, const std::string& sk, std::string& signature) -> int;
auto ed25519_sig_hex_to_bytes(const std::string& sig_hex, int& error_code) noexcept
-> std::array<std::byte, MAMBA_ED25519_SIGSIZE_BYTES>;
auto ed25519_key_hex_to_bytes(const std::string& key_hex, int& error_code) noexcept
-> std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>;
auto
verify(const std::byte* data, std::size_t data_len, const std::byte* pk, const std::byte* signature)
-> int;
auto verify(const std::string& data, const std::byte* pk, const std::byte* signature) -> int;
auto verify(const std::string& data, const std::string& pk_hex, const std::string& signature_hex)
-> int;
/**
* Verify a GPG/PGP signature against the hash of the binary data and
* the additional trailer added in V4 signature.
* See RFC4880, section 5.2.4 https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.4
* This method assumes hash function to be SHA-256
*/
auto verify_gpg_hashed_msg(const std::byte* data, const std::byte* pk, const std::byte* signature)
-> int;
auto
verify_gpg_hashed_msg(const std::string& data, const std::byte* pk, const std::byte* signature)
-> int;
auto
verify_gpg_hashed_msg(const std::string& data, const std::string& pk, const std::string& signature)
-> int;
/**
* Verify a GPG/PGP signature against the binary data and
* the additional trailer added in V4 signature.
* See RFC4880, section 5.2.4 https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.4
* This method assumes hash function to be SHA-256
*/
auto verify_gpg(
const std::string& data,
const std::string& gpg_v4_trailer,
const std::string& pk,
const std::string& signature
) -> int;
void check_timestamp_metadata_format(const std::string& ts);
}
#endif

View File

@ -0,0 +1,214 @@
// Copyright (c) 2019, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_UPDATE_FRAMEWORK_HPP
#define MAMBA_VALIDATION_UPDATE_FRAMEWORK_HPP
#include <memory>
#include <set>
#include <string>
#include <vector>
#include <nlohmann/json_fwd.hpp>
#include "mamba/core/timeref.hpp"
#include "mamba/validation/keys.hpp"
namespace mamba
{
class Context;
namespace fs
{
class u8path;
}
}
namespace mamba::validation
{
class RepoIndexChecker;
/**
* Base class for spec implementations.
*/
class SpecBase
{
public:
virtual ~SpecBase() = default;
[[nodiscard]] auto version_str() const -> std::string;
[[nodiscard]] virtual auto canonicalize(const nlohmann::json& j) const -> std::string;
[[nodiscard]] auto compatible_prefix() const -> std::string;
[[nodiscard]] auto upgrade_prefix() const -> std::vector<std::string>;
[[nodiscard]] auto is_compatible(const fs::u8path& p) const -> bool;
[[nodiscard]] auto is_compatible(const nlohmann::json& j) const -> bool;
[[nodiscard]] auto is_compatible(const std::string& version) const -> bool;
[[nodiscard]] auto is_upgrade(const nlohmann::json& j) const -> bool;
[[nodiscard]] auto is_upgrade(const std::string& version) const -> bool;
[[nodiscard]] virtual auto upgradable() const -> bool;
[[nodiscard]] virtual auto json_key() const -> std::string = 0;
[[nodiscard]] virtual auto expiration_json_key() const -> std::string = 0;
[[nodiscard]] virtual auto signatures(const nlohmann::json& j) const
-> std::set<RoleSignature>
= 0;
protected:
SpecBase(std::string spec_version);
[[nodiscard]] auto get_json_value(const nlohmann::json& j) const -> std::string;
private:
std::string m_spec_version;
};
auto operator==(const SpecBase& sv1, const SpecBase& sv2) -> bool;
auto operator!=(const SpecBase& sv1, const SpecBase& sv2) -> bool;
/**
* Base class for role implementation.
*/
class RoleBase
{
public:
RoleBase(std::string type, std::shared_ptr<SpecBase> sv);
virtual ~RoleBase() = 0;
[[nodiscard]] auto type() const -> std::string;
[[nodiscard]] auto spec_version() const -> SpecBase&;
[[nodiscard]] auto version() const -> std::size_t;
[[nodiscard]] auto file_ext() const -> std::string;
[[nodiscard]] auto expires() const -> std::string;
[[nodiscard]] auto expired(const TimeRef& time_reference) const -> bool;
[[nodiscard]] auto roles() const -> std::set<std::string>;
[[nodiscard]] auto signatures(const nlohmann::json& j) const -> std::set<RoleSignature>;
[[nodiscard]] virtual auto self_keys() const -> RoleFullKeys = 0;
[[nodiscard]] auto all_keys() const -> std::map<std::string, RoleFullKeys>;
protected:
[[nodiscard]] auto read_json_file(const fs::u8path& p, bool update = false) const
-> nlohmann::json;
/**
* Check that a threshold of valid signatures is met
* for the signed metadata of a role, using another
* role keys (possibly the same).
* Both signed and signatures metadata are contained
* in 'data'.
*/
void check_role_signatures(const nlohmann::json& data, const RoleBase& role);
/**
* Check that a threshold of valid signatures is met
* for the signed metadata, using a set of keys.
*/
void check_signatures(
const std::string& signed_data,
const std::set<RoleSignature>& signatures,
const RoleFullKeys& keyring
) const;
void set_spec_version(std::shared_ptr<SpecBase> sv);
void set_expiration(const std::string& expires);
// Forwarding to spec implementation
[[nodiscard]] auto canonicalize(const nlohmann::json& j) const -> std::string;
// Return the spec implementation
[[nodiscard]] auto spec_impl() const -> std::shared_ptr<SpecBase>;
// Mandatory roles defined by the current role
[[nodiscard]] virtual auto mandatory_defined_roles() const -> std::set<std::string>;
// Optional roles defined by the current role
[[nodiscard]] virtual auto optionally_defined_roles() const -> std::set<std::string>;
// Check role
void check_expiration_format() const;
void check_defined_roles(bool allow_any = false) const;
std::map<std::string, RoleFullKeys> m_defined_roles;
private:
std::string m_internal_type;
std::string m_type;
std::shared_ptr<SpecBase> p_spec;
std::size_t m_version = 1;
std::string m_expires;
std::string m_ext = "json";
friend void to_json(nlohmann::json& j, const RoleBase& r);
friend void from_json(const nlohmann::json& j, RoleBase& r);
};
void to_json(nlohmann::json& j, const RoleBase& role);
void from_json(const nlohmann::json& j, RoleBase& role);
/**
* 'root' role interface.
*/
class RootRole : public RoleBase
{
public:
~RootRole() override = default;
auto update(fs::u8path path) -> std::unique_ptr<RootRole>;
auto update(nlohmann::json j) -> std::unique_ptr<RootRole>;
auto possible_update_files() -> std::vector<fs::u8path>;
virtual auto build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const -> std::unique_ptr<RepoIndexChecker>
= 0;
protected:
RootRole(std::shared_ptr<SpecBase> spec);
private:
virtual auto create_update(const nlohmann::json& j) -> std::unique_ptr<RootRole> = 0;
};
/**
* Interface that performs validity checks
* on a repository packages index.
*/
class RepoIndexChecker
{
public:
virtual ~RepoIndexChecker() = default;
virtual void verify_index(const nlohmann::json& j) const = 0;
virtual void verify_index(const fs::u8path& p) const = 0;
virtual void verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const = 0;
protected:
RepoIndexChecker() = default;
};
}
#endif

View File

@ -0,0 +1,200 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_UPDATE_FRAMEWORK_V0_6_HPP
#define MAMBA_VALIDATION_UPDATE_FRAMEWORK_V0_6_HPP
#include <memory>
#include <set>
#include <string>
#include <nlohmann/json_fwd.hpp>
#include "mamba/validation/keys.hpp"
#include "mamba/validation/update_framework.hpp"
namespace mamba::validation::v0_6
{
/**
* The Update Frameworkd ``conda-content-trust`` v0.6.0 specific implementation.
*
* This is a variation of TUF specification.
*/
class SpecImpl final : public SpecBase
{
public:
SpecImpl(std::string sv = "0.6.0");
[[nodiscard]] auto json_key() const -> std::string override;
[[nodiscard]] auto expiration_json_key() const -> std::string override;
[[nodiscard]] auto signatures(const nlohmann::json& j) const
-> std::set<RoleSignature> override;
[[nodiscard]] auto canonicalize(const nlohmann::json& j) const -> std::string override;
[[nodiscard]] auto upgradable() const -> bool override;
};
class V06RoleBaseExtension
{
public:
void set_timestamp(const std::string& ts);
[[nodiscard]] auto timestamp() const -> std::string;
protected:
std::string m_timestamp;
void check_timestamp_format() const;
};
// Forward declaration of KeyMgrRole.
class KeyMgrRole;
/**
* 'root' role implementation.
*/
class RootImpl final
: public RootRole
, public V06RoleBaseExtension
{
public:
RootImpl(const fs::u8path& p);
RootImpl(const nlohmann::json& j);
RootImpl(const std::string& json_str);
/**
* Return a ``RepoIndexChecker`` implementation (derived class) from repository base URL.
*/
auto build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const -> std::unique_ptr<RepoIndexChecker> override;
[[nodiscard]] auto self_keys() const -> RoleFullKeys override;
[[nodiscard]] auto upgraded_signable() const -> nlohmann::json;
auto
upgraded_signature(const nlohmann::json& j, const std::string& pk, const std::byte* sk) const
-> RoleSignature;
[[nodiscard]] auto create_key_mgr(const fs::u8path& p) const -> KeyMgrRole;
[[nodiscard]] auto create_key_mgr(const nlohmann::json& j) const -> KeyMgrRole;
friend void to_json(nlohmann::json& j, const RootImpl& r);
friend void from_json(const nlohmann::json& j, RootImpl& r);
private:
void load_from_json(const nlohmann::json& j);
auto create_update(const nlohmann::json& j) -> std::unique_ptr<RootRole> override;
[[nodiscard]] auto mandatory_defined_roles() const -> std::set<std::string> override;
[[nodiscard]] auto optionally_defined_roles() const -> std::set<std::string> override;
void set_defined_roles(std::map<std::string, RolePubKeys> keys);
};
class PkgMgrRole;
/**
* The Update Framework 'key_mgr' role implementation.
*/
class KeyMgrRole final
: public RoleBase
, public V06RoleBaseExtension
{
public:
KeyMgrRole(const fs::u8path& p, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
KeyMgrRole(const nlohmann::json& j, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
KeyMgrRole(const std::string& json_str, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
// std::set<std::string> roles() const override;
[[nodiscard]] auto self_keys() const -> RoleFullKeys override;
[[nodiscard]] auto create_pkg_mgr(const fs::u8path& p) const -> PkgMgrRole;
[[nodiscard]] auto create_pkg_mgr(const nlohmann::json& j) const -> PkgMgrRole;
/**
* Return a ``RepoIndexChecker`` implementation (derived class) from repository base URL.
*/
auto build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const -> std::unique_ptr<RepoIndexChecker>;
friend void to_json(nlohmann::json& j, const KeyMgrRole& r);
friend void from_json(const nlohmann::json& j, KeyMgrRole& r);
private:
void load_from_json(const nlohmann::json& j);
RoleFullKeys m_keys;
std::map<std::string, RolePubKeys> m_delegations;
[[nodiscard]] auto mandatory_defined_roles() const -> std::set<std::string> override;
[[nodiscard]] auto optionally_defined_roles() const -> std::set<std::string> override;
void set_defined_roles(std::map<std::string, RolePubKeys> keys);
};
/**
* The Update Framework 'pkg_mgr' role implementation.
*
* This role inherits from ``RepoIndexChecker`` and will be used by ``RepoChecker`` to
* perform the repository index verification.
*/
class PkgMgrRole final
: public RoleBase
, public V06RoleBaseExtension
, public RepoIndexChecker
{
public:
PkgMgrRole(RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
PkgMgrRole(const fs::u8path& p, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
PkgMgrRole(const nlohmann::json& j, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
PkgMgrRole(const std::string& json_str, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec);
void verify_index(const fs::u8path& p) const override;
void verify_index(const nlohmann::json& j) const override;
void
verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const override;
friend void to_json(nlohmann::json& j, const PkgMgrRole& r);
friend void from_json(const nlohmann::json& j, PkgMgrRole& r);
private:
void load_from_json(const nlohmann::json& j);
[[nodiscard]] auto self_keys() const -> RoleFullKeys override;
[[nodiscard]] auto pkg_signatures(const nlohmann::json& j) const -> std::set<RoleSignature>;
void
check_pkg_signatures(const nlohmann::json& signed_data, const nlohmann::json& signatures) const;
void set_defined_roles(std::map<std::string, RolePubKeys> keys);
RoleFullKeys m_keys;
friend class KeyMgrRole;
};
}
#endif

View File

@ -0,0 +1,81 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#ifndef MAMBA_VALIDATION_UPDATE_FRAMEWORK_V1_HPP
#define MAMBA_VALIDATION_UPDATE_FRAMEWORK_V1_HPP
#include <set>
#include <string>
#include <nlohmann/json_fwd.hpp>
#include "mamba/validation/keys.hpp"
#include "mamba/validation/update_framework.hpp"
namespace mamba::fs
{
class u8path;
}
namespace mamba::validation::v1
{
/**
* TUF v1 specific implementation.
*/
class SpecImpl final : public SpecBase
{
public:
SpecImpl(std::string sv = "1.0.17");
[[nodiscard]] auto json_key() const -> std::string override;
[[nodiscard]] auto expiration_json_key() const -> std::string override;
[[nodiscard]] auto signatures(const nlohmann::json& j) const
-> std::set<RoleSignature> override;
};
/**
* The Update Frameworkd 'root' role implementation.
*
* TUF v1.0.17 §2.1.1
* https://theupdateframework.github.io/specification/latest/#root
*/
class RootImpl final : public RootRole
{
public:
RootImpl(const fs::u8path& p);
RootImpl(const nlohmann::json& j);
[[nodiscard]] auto self_keys() const -> RoleFullKeys override;
auto build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& url,
const fs::u8path& cache_path
) const -> std::unique_ptr<RepoIndexChecker> override;
friend void to_json(nlohmann::json& j, const RootImpl& r);
friend void from_json(const nlohmann::json& j, RootImpl& r);
private:
void load_from_json(const nlohmann::json& j);
auto create_update(const nlohmann::json& j) -> std::unique_ptr<RootRole> override;
[[nodiscard]] auto mandatory_defined_roles() const -> std::set<std::string> override;
[[nodiscard]] auto optionally_defined_roles() const -> std::set<std::string> override;
void set_defined_roles(
const std::map<std::string, Key>& keys,
const std::map<std::string, RoleKeys>& roles
);
};
}
#endif

View File

@ -18,10 +18,10 @@
#include "mamba/core/menuinst.hpp"
#include "mamba/core/output.hpp"
#include "mamba/core/transaction_context.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/util/build.hpp"
#include "mamba/util/environment.hpp"
#include "mamba/util/string.hpp"
#include "mamba/validation/tools.hpp"
#ifdef __APPLE__
#include "mamba/core/util_os.hpp"

View File

@ -10,10 +10,10 @@
#include "mamba/core/output.hpp"
#include "mamba/core/package_cache.hpp"
#include "mamba/core/package_handling.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/core/util.hpp"
#include "mamba/specs/conda_url.hpp"
#include "mamba/util/string.hpp"
#include "mamba/util/url_manip.hpp"
#include "mamba/validation/tools.hpp"
namespace mamba
{

View File

@ -1,8 +1,8 @@
#include "mamba/core/invoke.hpp"
#include "mamba/core/package_fetcher.hpp"
#include "mamba/core/util.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/util/string.hpp"
#include "mamba/validation/tools.hpp"
namespace mamba
{

View File

@ -5,8 +5,6 @@
// The full license is in the file LICENSE, distributed with this software.
#include <sstream>
#include <archive.h>
#include <archive_entry.h>
#include <reproc++/run.hpp>
@ -17,8 +15,8 @@
#include "mamba/core/package_paths.hpp"
#include "mamba/core/thread_utils.hpp"
#include "mamba/core/util_os.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/util/string.hpp"
#include "mamba/validation/tools.hpp"
#include "nlohmann/json.hpp"

View File

@ -1,24 +1,22 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <atomic>
#include <cassert>
#include <mutex>
#include <regex>
#include "mamba/util/build.hpp"
extern "C"
{
#include <curl/urlapi.h>
}
#include "mamba/api/configuration.hpp"
#include "mamba/core/channel_context.hpp"
#include "mamba/core/context.hpp"
#include "mamba/core/execution.hpp"
#include "mamba/core/output.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/util/build.hpp"
#include "spdlog/spdlog.h"
namespace mamba
{
@ -181,5 +179,4 @@ namespace mamba
{
main_console = nullptr;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include "mamba/util/string.hpp"
#include "mamba/validation/errors.hpp"
namespace mamba::validation
{
trust_error::trust_error(std::string_view message)
: m_message(util::concat("Content trust error. ", message, ". Aborting."))
{
}
auto trust_error::what() const noexcept -> const char*
{
return this->m_message.c_str();
}
threshold_error::threshold_error()
: trust_error("Signatures threshold not met")
{
}
role_metadata_error::role_metadata_error()
: trust_error("Invalid role metadata")
{
}
rollback_error::rollback_error()
: trust_error("Possible rollback attack")
{
}
freeze_error::freeze_error()
: trust_error("Possible freeze attack")
{
}
role_file_error::role_file_error()
: trust_error("Invalid role file")
{
}
spec_version_error::spec_version_error()
: trust_error("Unsupported specification version")
{
}
fetching_error::fetching_error()
: trust_error("Failed to fetch role metadata")
{
}
package_error::package_error()
: trust_error("Invalid package")
{
}
role_error::role_error()
: trust_error("Invalid role")
{
}
index_error::index_error()
: trust_error("Invalid package index metadata")
{
}
}

View File

@ -0,0 +1,112 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <utility>
#include <nlohmann/json.hpp>
#include "mamba/validation/keys.hpp"
namespace mamba::validation
{
auto Key::from_ed25519(std::string keyval) -> Key
{
return { "ed25519", "ed25519", std::move(keyval) };
}
void to_json(nlohmann::json& j, const Key& key)
{
j = { { "keytype", key.keytype }, { "scheme", key.scheme }, { "keyval", key.keyval } };
}
void from_json(const nlohmann::json& j, Key& key)
{
j.at("keytype").get_to(key.keytype);
j.at("scheme").get_to(key.scheme);
j.at("keyval").get_to(key.keyval);
}
void to_json(nlohmann::json& j, const RoleSignature& role_sig)
{
j = { { "keyid", role_sig.keyid }, { "sig", role_sig.sig } };
if (!role_sig.pgp_trailer.empty())
{
j["other_headers"] = role_sig.pgp_trailer;
}
}
void from_json(const nlohmann::json& j, RoleSignature& role_sig)
{
j.at("keyid").get_to(role_sig.keyid);
j.at("sig").get_to(role_sig.sig);
if (j.find("other_headers") != j.end())
{
j.at("other_headers").get_to(role_sig.pgp_trailer);
}
}
auto operator<(const RoleSignature& rs1, const RoleSignature& rs2) -> bool
{
return rs1.keyid < rs2.keyid;
};
void to_json(nlohmann::json& j, const RoleKeys& role_keys)
{
j = { { "keyids", role_keys.keyids }, { "threshold", role_keys.threshold } };
}
void from_json(const nlohmann::json& j, RoleKeys& role_keys)
{
j.at("keyids").get_to(role_keys.keyids);
j.at("threshold").get_to(role_keys.threshold);
}
auto RolePubKeys::to_role_keys() const -> RoleKeys
{
return { pubkeys, threshold };
}
void to_json(nlohmann::json& j, const RolePubKeys& role_keys)
{
j = { { "pubkeys", role_keys.pubkeys }, { "threshold", role_keys.threshold } };
}
void from_json(const nlohmann::json& j, RolePubKeys& role_keys)
{
j.at("pubkeys").get_to(role_keys.pubkeys);
j.at("threshold").get_to(role_keys.threshold);
}
RoleFullKeys::RoleFullKeys(const std::map<std::string, Key>& keys_, const std::size_t& threshold_)
: keys(keys_)
, threshold(threshold_){};
auto RoleFullKeys::to_keys() const -> std::map<std::string, Key>
{
return keys;
}
auto RoleFullKeys::to_roles() const -> RoleKeys
{
std::vector<std::string> keyids;
for (auto& k : keys)
{
keyids.push_back(k.first);
}
return { keyids, threshold };
}
void to_json(nlohmann::json& j, const RoleFullKeys& k)
{
j = { { "keys", k.keys }, { "threshold", k.threshold } };
}
void from_json(const nlohmann::json& j, RoleFullKeys& k)
{
j.at("keys").get_to(k.keys);
j.at("threshold").get_to(k.threshold);
}
}

View File

@ -0,0 +1,219 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <string>
#include "mamba/core/download.hpp"
#include "mamba/core/output.hpp"
#include "mamba/core/timeref.hpp"
#include "mamba/core/util.hpp"
#include "mamba/util/string.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/repo_checker.hpp"
#include "mamba/validation/update_framework.hpp"
#include "mamba/validation/update_framework_v0_6.hpp"
#include "mamba/validation/update_framework_v1.hpp"
namespace mamba::validation
{
RepoChecker::RepoChecker(Context& context, std::string base_url, fs::u8path ref_path, fs::u8path cache_path)
: m_base_url(std::move(base_url))
, m_ref_path(std::move(ref_path))
, m_cache_path(std::move(cache_path))
, m_context(context)
{
}
RepoChecker::~RepoChecker() = default;
auto RepoChecker::cache_path() -> const fs::u8path&
{
return m_cache_path;
}
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
const TimeRef time_reference;
auto root = get_root_role(time_reference);
p_index_checker = root->build_index_checker(
m_context,
time_reference,
m_base_url,
cache_path()
);
LOG_INFO << "Index checker successfully generated for '" << m_base_url << "'";
}
}
void RepoChecker::verify_index(const nlohmann::json& j) const
{
p_index_checker->verify_index(j);
}
void RepoChecker::verify_index(const fs::u8path& p) const
{
p_index_checker->verify_index(p);
}
void
RepoChecker::verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const
{
p_index_checker->verify_package(signed_data, signatures);
}
auto RepoChecker::root_version() -> std::size_t
{
return m_root_version;
}
auto RepoChecker::ref_root() -> fs::u8path
{
return m_ref_path / "root.json";
}
auto RepoChecker::cached_root() -> fs::u8path
{
if (cache_path().empty())
{
return "";
}
else
{
return cache_path() / "root.json";
}
}
void RepoChecker::persist_file(const fs::u8path& file_path)
{
if (fs::exists(cached_root()))
{
fs::remove(cached_root());
}
if (!cached_root().empty())
{
fs::copy(file_path, cached_root());
}
}
auto RepoChecker::initial_trusted_root() -> fs::u8path
{
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();
}
}
auto RepoChecker::get_root_role(const TimeRef& time_reference) -> std::unique_ptr<RootRole>
{
// 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 (v0_6::SpecImpl().is_compatible(trusted_root))
{
updated_root = std::make_unique<v0_6::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 = TemporaryDirectory();
auto tmp_dir_path = tmp_dir.path();
// do chained updates
LOG_DEBUG << "Starting updates of 'root' metadata";
do
{
fs::u8path tmp_file_path;
// Update from the most recent spec supported by this client
for (auto& f : update_files)
{
auto url = util::concat(m_base_url, "/", f.string());
tmp_file_path = tmp_dir_path / f;
if (check_resource_exists(url, m_context))
{
DownloadRequest request(f.string(), url, tmp_file_path.string());
DownloadResult res = download(std::move(request), m_context);
if (res)
{
break;
}
}
tmp_file_path = "";
}
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);
// Set the next possible files
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;
// TUF spec 5.3.10 - Check for a freeze attack
// Updated 'root' role should not be expired
// https://theupdateframework.github.io/specification/latest/#update-root
if (updated_root->expired(time_reference))
{
LOG_ERROR << "Possible freeze attack of 'root' metadata.\nExpired: "
<< updated_root->expires();
throw freeze_error();
}
return updated_root;
};
}

View File

@ -0,0 +1,397 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <regex>
#include <utility>
#include <openssl/evp.h>
#include "mamba/core/output.hpp"
#include "mamba/core/util.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/util/cryptography.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/tools.hpp"
namespace mamba::validation
{
auto sha256sum(const fs::u8path& path) -> std::string_view
{
std::ifstream infile = mamba::open_ifstream(path);
thread_local auto hasher = util::Sha256Hasher();
thread_local auto hash = hasher.file_hex(infile);
return { hash.data(), hash.size() };
}
auto md5sum(const fs::u8path& path) -> std::string_view
{
std::ifstream infile = mamba::open_ifstream(path);
thread_local auto hasher = util::Md5Hasher();
thread_local auto hash = hasher.file_hex(infile);
return { hash.data(), hash.size() };
}
auto file_size(const fs::u8path& path, std::uintmax_t validation) -> bool
{
return fs::file_size(path) == validation;
}
namespace
{
template <size_t S, class B>
[[nodiscard]] auto hex_to_bytes_arr(const B& buffer, int& error_code) noexcept
-> std::array<std::byte, S>
{
auto out = std::array<std::byte, S>{};
auto err = util::EncodingError::Ok;
util::hex_to_bytes_to(buffer, out.data(), err);
error_code = err != util::EncodingError::Ok;
return out;
}
template <class B>
[[nodiscard]] auto hex_to_bytes_vec(const B& buffer, int& error_code) noexcept
-> std::vector<std::byte>
{
auto out = std::vector<std::byte>(buffer.size() / 2);
auto err = util::EncodingError::Ok;
util::hex_to_bytes_to(buffer, out.data(), err);
error_code = err != util::EncodingError::Ok;
return out;
}
}
auto ed25519_sig_hex_to_bytes(const std::string& sig_hex, int& error_code) noexcept
-> std::array<std::byte, MAMBA_ED25519_SIGSIZE_BYTES>
{
return hex_to_bytes_arr<MAMBA_ED25519_SIGSIZE_BYTES>(sig_hex, error_code);
}
auto ed25519_key_hex_to_bytes(const std::string& key_hex, int& error_code) noexcept
-> std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>
{
return hex_to_bytes_arr<MAMBA_ED25519_KEYSIZE_BYTES>(key_hex, error_code);
}
auto generate_ed25519_keypair(std::byte* pk, std::byte* sk) -> int
{
std::size_t key_len = MAMBA_ED25519_KEYSIZE_BYTES;
::EVP_PKEY* pkey = nullptr;
struct EVPContext
{
::EVP_PKEY_CTX* pctx = ::EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr);
~EVPContext()
{
::EVP_PKEY_CTX_free(pctx);
}
} evp_context;
int gen_status = ::EVP_PKEY_keygen_init(evp_context.pctx);
if (gen_status != 1)
{
LOG_DEBUG << "Failed to initialize ED25519 key pair generation";
return gen_status;
}
gen_status = ::EVP_PKEY_keygen(evp_context.pctx, &pkey);
if (gen_status != 1)
{
LOG_DEBUG << "Failed to generate ED25519 key pair";
return gen_status;
}
int storage_status = ::EVP_PKEY_get_raw_public_key(
pkey,
reinterpret_cast<unsigned char*>(pk),
&key_len
);
if (storage_status != 1)
{
LOG_DEBUG << "Failed to store public key of generated ED25519 key pair";
return storage_status;
}
storage_status = ::EVP_PKEY_get_raw_private_key(
pkey,
reinterpret_cast<unsigned char*>(sk),
&key_len
);
if (storage_status != 1)
{
LOG_DEBUG << "Failed to store private key of generated ED25519 key pair";
return storage_status;
}
return 1;
}
auto generate_ed25519_keypair() -> std::pair<
std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>,
std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>>
{
std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES> pk, sk;
generate_ed25519_keypair(pk.data(), sk.data());
return { pk, sk };
}
auto generate_ed25519_keypair_hex() -> std::pair<std::string, std::string>
{
auto [first, second] = generate_ed25519_keypair();
// TODO change function signature to use std::byte
const auto first_data = reinterpret_cast<const std::byte*>(first.data());
const auto second_data = reinterpret_cast<const std::byte*>(second.data());
return {
util::bytes_to_hex_str(first_data, first_data + first.size()),
util::bytes_to_hex_str(second_data, second_data + second.size()),
};
}
auto sign(const std::string& data, const std::byte* sk, std::byte* signature) -> int
{
::EVP_PKEY* ed_key = ::EVP_PKEY_new_raw_private_key(
EVP_PKEY_ED25519,
nullptr,
reinterpret_cast<const unsigned char*>(sk),
MAMBA_ED25519_KEYSIZE_BYTES
);
::EVP_MD_CTX* md_ctx = ::EVP_MD_CTX_new();
if (ed_key == nullptr)
{
LOG_DEBUG << "Failed to read secret key raw buffer during signing step";
return 0;
}
int init_status, sign_status;
init_status = ::EVP_DigestSignInit(md_ctx, nullptr, nullptr, nullptr, ed_key);
if (init_status != 1)
{
LOG_DEBUG << "Failed to init signing step";
return init_status;
}
std::size_t sig_len = MAMBA_ED25519_SIGSIZE_BYTES;
sign_status = ::EVP_DigestSign(
md_ctx,
reinterpret_cast<unsigned char*>(signature),
&sig_len,
reinterpret_cast<const unsigned char*>(data.data()),
data.size()
);
if (sign_status != 1)
{
LOG_DEBUG << "Failed to sign the data";
return sign_status;
}
::EVP_MD_CTX_free(md_ctx);
return 1;
}
auto sign(const std::string& data, const std::string& sk, std::string& signature) -> int
{
int error_code = 0;
auto bin_sk = ed25519_key_hex_to_bytes(sk, error_code);
if (error_code != 0)
{
LOG_DEBUG << "Invalid secret key";
return 0;
}
std::array<std::byte, MAMBA_ED25519_SIGSIZE_BYTES> sig;
error_code = sign(data, bin_sk.data(), sig.data());
const auto sig_data = reinterpret_cast<const std::byte*>(sig.data());
signature = util::bytes_to_hex_str(sig_data, sig_data + sig.size());
return error_code;
}
auto
verify(const std::byte* data, std::size_t data_len, const std::byte* pk, const std::byte* signature)
-> int
{
::EVP_PKEY* ed_key = ::EVP_PKEY_new_raw_public_key(
EVP_PKEY_ED25519,
nullptr,
reinterpret_cast<const unsigned char*>(pk),
MAMBA_ED25519_KEYSIZE_BYTES
);
::EVP_MD_CTX* md_ctx = ::EVP_MD_CTX_new();
if (ed_key == nullptr)
{
LOG_DEBUG << "Failed to read public key raw buffer during verification step";
return 0;
}
int init_status, verif_status;
init_status = ::EVP_DigestVerifyInit(md_ctx, nullptr, nullptr, nullptr, ed_key);
if (init_status != 1)
{
LOG_DEBUG << "Failed to init verification step";
return init_status;
}
std::size_t sig_len = MAMBA_ED25519_SIGSIZE_BYTES;
verif_status = ::EVP_DigestVerify(
md_ctx,
reinterpret_cast<const unsigned char*>(signature),
sig_len,
reinterpret_cast<const unsigned char*>(data),
data_len
);
if (verif_status != 1)
{
LOG_DEBUG << "Failed to verify the data signature";
return verif_status;
}
::EVP_MD_CTX_free(md_ctx);
return 1;
}
auto verify(const std::string& data, const std::byte* pk, const std::byte* signature) -> int
{
unsigned long long data_len = data.size();
auto raw_data = reinterpret_cast<const std::byte*>(data.data());
return verify(raw_data, data_len, pk, signature);
}
auto verify(const std::string& data, const std::string& pk, const std::string& signature) -> int
{
int error_code = 0;
auto bin_signature = ed25519_sig_hex_to_bytes(signature, error_code);
if (error_code != 0)
{
LOG_DEBUG << "Invalid signature '" << signature << "' for public key '" << pk << "'";
return 0;
}
auto bin_pk = ed25519_key_hex_to_bytes(pk, error_code);
if (error_code != 0)
{
LOG_DEBUG << "Invalid public key '" << pk << "'";
return 0;
}
return verify(data, bin_pk.data(), bin_signature.data());
}
auto verify_gpg_hashed_msg(const std::byte* data, const std::byte* pk, const std::byte* signature)
-> int
{
return verify(data, MAMBA_SHA256_SIZE_BYTES, pk, signature);
}
auto
verify_gpg_hashed_msg(const std::string& data, const std::byte* pk, const std::byte* signature)
-> int
{
int error = 0;
auto data_bin = hex_to_bytes_arr<MAMBA_SHA256_SIZE_BYTES>(data, error);
return verify(data_bin.data(), MAMBA_SHA256_SIZE_BYTES, pk, signature) + error;
}
auto
verify_gpg_hashed_msg(const std::string& data, const std::string& pk, const std::string& signature)
-> int
{
int error = 0;
auto signature_bin = ed25519_sig_hex_to_bytes(signature, error);
if (error)
{
return error;
}
auto pk_bin = ed25519_key_hex_to_bytes(pk, error);
if (error)
{
return error;
}
return verify_gpg_hashed_msg(data, pk_bin.data(), signature_bin.data());
}
auto verify_gpg(
const std::string& data,
const std::string& pgp_v4_trailer,
const std::string& pk,
const std::string& signature
) -> int
{
unsigned long long data_len = data.size();
auto data_bin = reinterpret_cast<const std::byte*>(data.data());
int error = 0;
auto signature_bin = ed25519_sig_hex_to_bytes(signature, error);
if (error)
{
return error;
}
auto pk_bin = ed25519_key_hex_to_bytes(pk, error);
if (error)
{
return error;
}
std::size_t trailer_hex_size = pgp_v4_trailer.size();
if (trailer_hex_size % 2 != 0)
{
LOG_DEBUG << "PGP V4 trailer size is not even: " << pgp_v4_trailer;
return 0;
}
auto pgp_trailer_bin = hex_to_bytes_vec(pgp_v4_trailer, error);
if (error)
{
return error;
}
auto final_trailer_bin = hex_to_bytes_arr<2>(std::string_view("04ff"), error);
assert(!error);
auto trailer_bin_len_big_endian = static_cast<uint32_t>(pgp_trailer_bin.size());
#ifdef _WIN32
trailer_bin_len_big_endian = _byteswap_ulong(trailer_bin_len_big_endian);
#else
trailer_bin_len_big_endian = __builtin_bswap32(trailer_bin_len_big_endian);
#endif
std::array<std::byte, MAMBA_SHA256_SIZE_BYTES> hash;
auto digester = util::Sha256Digester();
digester.digest_start();
digester.digest_update(data_bin, data_len);
digester.digest_update(pgp_trailer_bin.data(), pgp_trailer_bin.size());
digester.digest_update(final_trailer_bin.data(), final_trailer_bin.size());
digester.digest_update(reinterpret_cast<const std::byte*>(&trailer_bin_len_big_endian), 4);
digester.digest_finalize_to(hash.data());
return verify_gpg_hashed_msg(hash.data(), pk_bin.data(), signature_bin.data()) + error;
}
void check_timestamp_metadata_format(const std::string& ts)
{
std::regex timestamp_re("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$");
if (!std::regex_match(ts, timestamp_re))
{
mamba::Console::stream() << "Invalid timestamp in content trust metadata";
LOG_ERROR << "Invalid timestamp format '" << ts
<< "', should be UTC ISO8601 ('<YYYY>-<MM>-<DD>T<HH>:<MM>:<SS>Z')";
throw role_metadata_error();
}
}
}

View File

@ -0,0 +1,601 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <fstream>
#include <regex>
#include <set>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "mamba/core/output.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/util/string.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/tools.hpp"
#include "mamba/validation/update_framework.hpp"
namespace mamba::validation
{
SpecBase::SpecBase(std::string spec_version)
: m_spec_version(std::move(spec_version))
{
}
auto SpecBase::version_str() const -> std::string
{
return m_spec_version;
}
auto SpecBase::compatible_prefix() const -> std::string
{
auto split_spec_version = util::split(m_spec_version, ".", 2);
auto spec_version_major = std::stoi(split_spec_version[0]);
if (spec_version_major == 0)
{
return split_spec_version[0] + "." + split_spec_version[1];
}
else
{
return split_spec_version[0];
}
}
auto SpecBase::upgrade_prefix() const -> std::vector<std::string>
{
auto split_spec_version = util::split(m_spec_version, ".", 2);
auto spec_version_major = std::stoi(split_spec_version[0]);
auto spec_version_minor = std::stoi(split_spec_version[1]);
if (spec_version_major == 0)
{
// Return the most recent possible upgrade first
return { "1", split_spec_version[0] + "." + std::to_string(spec_version_minor + 1) };
}
else
{
return { std::to_string(spec_version_major + 1) };
}
}
auto SpecBase::is_compatible(const fs::u8path& p) const -> bool
{
std::regex name_re;
std::smatch matches;
std::size_t min_match_size;
std::string f_name = p.filename().string();
std::string f_spec_version_str, f_version_str, f_type, f_ext;
name_re = R"(^(?:[1-9]+\d*.)?(?:sv([1-9]\d*|0\.[1-9]\d*).)?(\w+)\.(\w+)$)";
min_match_size = 3;
if (std::regex_search(f_name, matches, name_re) && (min_match_size <= matches.size()))
{
f_spec_version_str = matches[1].str();
if (!f_spec_version_str.empty())
{
return is_compatible(matches[1].str() + ".");
}
else
{
std::ifstream i(p.std_path());
nlohmann::json j;
i >> j;
return is_compatible(j);
}
}
else
{
return false;
}
}
auto SpecBase::is_compatible(const std::string& version) const -> bool
{
return util::starts_with(version, compatible_prefix() + ".");
}
auto SpecBase::is_compatible(const nlohmann::json& j) const -> bool
{
auto spec_version = get_json_value(j);
if (!spec_version.empty())
{
return is_compatible(spec_version);
}
else
{
return false;
}
}
auto SpecBase::is_upgrade(const std::string& version) const -> bool
{
auto upgrade_prefixes = upgrade_prefix();
std::vector<std::string_view> possible_upgrades;
for (auto& s : upgrade_prefixes)
{
s += '.';
possible_upgrades.push_back(s);
}
return util::starts_with_any(version, possible_upgrades);
}
auto SpecBase::is_upgrade(const nlohmann::json& j) const -> bool
{
auto spec_version = get_json_value(j);
if (!spec_version.empty())
{
return is_upgrade(spec_version);
}
else
{
return false;
}
}
auto SpecBase::get_json_value(const nlohmann::json& j) const -> std::string
{
try
{
return j.at("signed").at(json_key()).get<std::string>();
}
catch (const nlohmann::json::exception& e)
{
LOG_DEBUG << "Invalid 'root' metadata, impossible to check spec version compatibility: "
<< e.what();
return "";
}
}
auto SpecBase::canonicalize(const nlohmann::json& j) const -> std::string
{
return j.dump();
}
auto SpecBase::upgradable() const -> bool
{
return false;
};
auto operator==(const SpecBase& sv1, const SpecBase& sv2) -> bool
{
return sv1.version_str() == sv2.version_str();
}
auto operator!=(const SpecBase& sv1, const SpecBase& sv2) -> bool
{
return sv1.version_str() != sv2.version_str();
}
RoleBase::RoleBase(std::string type, std::shared_ptr<SpecBase> sv)
: m_type(std::move(type))
{
p_spec = std::move(sv);
};
RoleBase::~RoleBase() = default;
auto RoleBase::type() const -> std::string
{
return m_type;
}
auto RoleBase::version() const -> std::size_t
{
return m_version;
}
auto RoleBase::expires() const -> std::string
{
return m_expires;
}
auto RoleBase::file_ext() const -> std::string
{
return m_ext;
}
auto RoleBase::spec_version() const -> SpecBase&
{
return *p_spec;
}
auto RoleBase::spec_impl() const -> std::shared_ptr<SpecBase>
{
return p_spec;
}
auto RoleBase::expired(const TimeRef& time_reference) const -> bool
{
return time_reference.timestamp().compare(m_expires) < 0 ? false : true;
}
auto RoleBase::canonicalize(const nlohmann::json& j) const -> std::string
{
return p_spec->canonicalize(j);
}
auto RoleBase::signatures(const nlohmann::json& j) const -> std::set<RoleSignature>
{
return p_spec->signatures(j);
}
auto RoleBase::roles() const -> std::set<std::string>
{
std::set<std::string> r;
for (auto& it : m_defined_roles)
{
r.insert(it.first);
}
return r;
}
auto RoleBase::mandatory_defined_roles() const -> std::set<std::string>
{
return {};
}
auto RoleBase::optionally_defined_roles() const -> std::set<std::string>
{
return {};
}
void RoleBase::check_expiration_format() const
{
check_timestamp_metadata_format(m_expires);
}
void RoleBase::check_defined_roles(bool allow_any) const
{
auto mandatory_roles = mandatory_defined_roles();
auto optional_roles = optionally_defined_roles();
auto all_roles = mandatory_roles;
all_roles.insert(optional_roles.cbegin(), optional_roles.cend());
if (!allow_any)
{
for (const auto& r : roles())
{
if (all_roles.find(r) == all_roles.end())
{
LOG_ERROR << "Invalid role defined in '" << type() << "' metadata: '" << r << "'";
throw role_metadata_error();
}
}
}
auto current_roles = roles();
if (!std::includes(
current_roles.begin(),
current_roles.end(),
mandatory_roles.begin(),
mandatory_roles.end()
))
{
std::vector<std::string> diff;
std::set_difference(
mandatory_roles.begin(),
mandatory_roles.end(),
current_roles.begin(),
current_roles.end(),
std::inserter(diff, diff.end())
);
LOG_ERROR << "Missing roles while loading '" << type() << "' metadata: '"
<< ::mamba::util::join(", ", diff) << "'";
throw role_metadata_error();
}
for (const auto& r : all_keys())
{
auto r_keys = r.second;
if (r_keys.keys.empty())
{
LOG_ERROR << "'" << type()
<< "' metadata should declare at least one key ID for role: '" << r.first
<< "'";
throw role_metadata_error();
}
if (r_keys.threshold == 0)
{
LOG_ERROR << "'" << type()
<< "' metadata should declare at least a 'threshold' of 1 for role: '"
<< r.first << "'";
throw role_metadata_error();
}
}
}
auto RoleBase::all_keys() const -> std::map<std::string, RoleFullKeys>
{
return m_defined_roles;
}
void RoleBase::set_spec_version(std::shared_ptr<SpecBase> new_sv)
{
auto& current_sv = spec_version();
if (!current_sv.is_compatible(new_sv->version_str()))
{
LOG_ERROR << "Incompatible 'spec_version' found in 'root' metadata, should start with '"
<< current_sv.compatible_prefix() << "' but is: '" << new_sv->version_str()
<< "'";
throw spec_version_error();
}
p_spec = std::move(new_sv);
}
void RoleBase::set_expiration(const std::string& expires)
{
m_expires = expires;
}
auto RoleBase::read_json_file(const fs::u8path& p, bool update) const -> nlohmann::json
{
if (!fs::exists(p))
{
LOG_ERROR << "File not found for '" << type() << "' role: " << p.string();
throw role_file_error();
}
std::regex name_re;
std::smatch matches;
std::size_t min_match_size;
std::string f_name = p.filename().string();
std::string f_spec_version_str, f_version_str, f_type, f_ext;
// Files should be named using one of the following structures:
// - Trusted (reference) file:
// - FILENAME.EXT
// - svSPEC_VERSION_MAJOR.FILENAME.EXT
// - Update file:
// - VERSION_NUMBER.FILENAME.EXT
// - VERSION_NUMBER.svSPEC_VERSION_MAJOR.FILENAME.EXT
if (update)
{
name_re = R"(^(?:([1-9]+\d*).)(?:sv([1-9]\d*|0\.[1-9]\d*).)?(\w+)\.(\w+)$)";
min_match_size = 4;
}
else
{
name_re = R"(^(?:[1-9]+\d*.)?(?:sv([1-9]\d*|0\.[1-9]\d*).)?(\w+)\.(\w+)$)";
min_match_size = 3;
}
if (std::regex_search(f_name, matches, name_re) && (min_match_size <= matches.size()))
{
auto match_size = matches.size();
if (update)
{
f_version_str = matches[1].str();
}
f_type = matches[match_size - 2].str();
f_ext = matches[match_size - 1].str();
if ((min_match_size + 1) == match_size)
{
f_spec_version_str = matches[match_size - 3].str();
}
}
else
{
LOG_ERROR << "Invalid file name for 'root' metadata update: " << f_name;
throw role_file_error();
}
if (f_ext != file_ext())
{
LOG_ERROR << "'root' metadata file should have 'json' extension, not: '" << f_ext << "'";
throw role_file_error();
}
if (f_type != type())
{
LOG_ERROR << "'root' metadata file should have 'root' type, not: '" << f_type << "'";
throw role_file_error();
}
if (!f_spec_version_str.empty())
{
auto new_spec_version_str = f_spec_version_str + ".";
if (update && spec_version().is_upgrade(new_spec_version_str)
&& !spec_version().upgradable())
{
LOG_ERROR << "Please check for a client update, unsupported spec version: '"
<< f_spec_version_str << "'";
throw spec_version_error();
}
else if (!((!update && spec_version().is_compatible(new_spec_version_str))
|| (update && spec_version().is_upgrade(new_spec_version_str))))
{
LOG_ERROR << "Invalid spec version specified in file name: '" << f_spec_version_str
<< "'";
throw role_file_error();
}
}
if (update)
{
// Check version number in filename is N+1
unsigned long f_version;
try
{
f_version = std::stoul(f_version_str);
}
catch (...)
{
LOG_ERROR << "Invalid version in file name for 'root' metadata update: "
<< f_version_str;
throw role_file_error();
}
if (f_version != (version() + 1))
{
LOG_ERROR << "'root' metadata file name should start with N+1 version ("
<< version() + 1 << "), but starts with: " << f_version;
throw role_file_error();
}
}
std::ifstream i(p.std_path());
nlohmann::json j;
i >> j;
return j;
}
void RoleBase::check_role_signatures(const nlohmann::json& data, const RoleBase& role)
{
std::string signed_data = role.canonicalize(data["signed"]);
auto signatures = role.signatures(data);
auto k = self_keys();
try
{
check_signatures(signed_data, signatures, k);
}
catch (const threshold_error& e)
{
LOG_ERROR << "Validation failed on role '" << type() << "' : " << e.what();
throw role_error();
}
}
void RoleBase::check_signatures(
const std::string& signed_data,
const std::set<RoleSignature>& signatures,
const RoleFullKeys& keyring
) const
{
std::size_t valid_sig = 0;
for (auto& s : signatures)
{
auto it = keyring.keys.find(s.keyid);
if (it != keyring.keys.end())
{
auto& pk = it->second.keyval;
int status;
if (s.pgp_trailer.empty())
{
status = verify(signed_data, pk, s.sig);
}
else
{
status = verify_gpg(signed_data, s.pgp_trailer, pk, s.sig);
}
if (status == 1)
{
++valid_sig;
}
else
{
LOG_WARNING << "Invalid signature of metadata using keyid: " << s.keyid;
}
}
else
{
LOG_WARNING << "Invalid keyid: " << s.keyid;
}
if (valid_sig >= keyring.threshold)
{
break;
}
}
if (valid_sig < keyring.threshold)
{
LOG_ERROR << "Threshold of valid signatures is not met (" << valid_sig << "/"
<< keyring.threshold << ")";
throw threshold_error();
}
}
void to_json(nlohmann::json& j, const RoleBase& role)
{
j = { { "version", role.version() }, { "expires", role.expires() } };
}
void from_json(const nlohmann::json& j, RoleBase& role)
{
role.m_version = j.at("version");
role.set_expiration(j.at(role.spec_version().expiration_json_key()));
}
RootRole::RootRole(std::shared_ptr<SpecBase> spec)
: RoleBase("root", spec)
{
}
auto RootRole::possible_update_files() -> std::vector<fs::u8path>
{
auto new_v = std::to_string(version() + 1);
auto compat_spec = spec_impl()->compatible_prefix();
auto upgrade_spec = spec_impl()->upgrade_prefix();
std::vector<fs::u8path> files;
// upgrade first
for (auto& s : upgrade_spec)
{
files.emplace_back(
::mamba::util::join(".", std::vector<std::string>({ new_v, "sv" + s, "root.json" }))
);
}
// compatible next
files.emplace_back(::mamba::util::join(
".",
std::vector<std::string>({ new_v, "sv" + compat_spec, "root.json" })
));
// then finally undefined spec
files.emplace_back(::mamba::util::join(".", std::vector<std::string>({ new_v, "root.json" })));
return files;
}
auto RootRole::update(fs::u8path path) -> std::unique_ptr<RootRole>
{
auto j = read_json_file(path, true);
return update(j);
}
// `create_update` currently catch a possible spec version update by testing
// and extracting spec version from JSON. It could be done upstream (in
// `update(fs::u8path)`) if we decide to specify the spec version in the file name.
// The filename would take the form VERSION_NUMBER.SPECVERSION.FILENAME.EXT
// To disambiguate version and spec version: 1.sv0.6.root.json or 1.sv1.root.json
auto RootRole::update(nlohmann::json j) -> std::unique_ptr<RootRole>
{
// TUF spec 5.3.4 - Check for an arbitrary software attack
// Check signatures against current keyids and threshold in 'RootImpl' constructor
auto root_update = create_update(j);
// Check signatures against new keyids and threshold
// check_role_signatures(j, *root_update);
// TUF spec 5.3.5 - Check for a rollback attack
// Version number has to be N+1
if (root_update->version() != (version() + 1))
{
if (root_update->version() > (version() + 1))
{
LOG_ERROR << "Invalid 'root' metadata version, should be exactly N+1";
throw role_metadata_error();
}
else
{
LOG_ERROR << "Possible rollback attack of 'root' metadata";
throw rollback_error();
}
}
return root_update;
}
}

View File

@ -0,0 +1,677 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <fstream>
#include <utility>
#include "mamba/core/download.hpp"
#include "mamba/core/output.hpp"
#include "mamba/core/util.hpp"
#include "mamba/specs/conda_url.hpp"
#include "mamba/util/encoding.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/tools.hpp"
#include "mamba/validation/update_framework_v0_6.hpp"
#include "mamba/validation/update_framework_v1.hpp"
namespace mamba::validation::v0_6
{
SpecImpl::SpecImpl(std::string sv)
: SpecBase(std::move(sv))
{
}
auto SpecImpl::json_key() const -> std::string
{
return "metadata_spec_version";
}
auto SpecImpl::expiration_json_key() const -> std::string
{
return "expiration";
}
auto SpecImpl::signatures(const nlohmann::json& j) const -> std::set<RoleSignature>
{
auto sigs = j.at("signatures").get<std::map<std::string, std::map<std::string, std::string>>>();
std::set<RoleSignature> unique_sigs;
for (auto& s : sigs)
{
std::string pgp_trailer = "";
if (s.second.find("other_headers") != s.second.end())
{
pgp_trailer = s.second["other_headers"];
}
unique_sigs.insert(RoleSignature({ s.first, s.second.at("signature"), pgp_trailer }));
}
return unique_sigs;
}
auto SpecImpl::upgradable() const -> bool
{
return true;
};
auto SpecImpl::canonicalize(const nlohmann::json& j) const -> std::string
{
return j.dump(2);
}
void V06RoleBaseExtension::check_timestamp_format() const
{
check_timestamp_metadata_format(m_timestamp);
}
void V06RoleBaseExtension::set_timestamp(const std::string& ts)
{
m_timestamp = ts;
}
auto V06RoleBaseExtension::timestamp() const -> std::string
{
return m_timestamp;
}
RootImpl::RootImpl(const fs::u8path& path)
: RootRole(std::make_shared<SpecImpl>())
{
auto j = read_json_file(path);
load_from_json(j);
}
RootImpl::RootImpl(const nlohmann::json& j)
: RootRole(std::make_shared<SpecImpl>())
{
load_from_json(j);
}
RootImpl::RootImpl(const std::string& json_str)
: RootRole(std::make_shared<SpecImpl>())
{
load_from_json(nlohmann::json::parse(json_str));
}
auto RootImpl::create_update(const nlohmann::json& j) -> std::unique_ptr<RootRole>
{
if (SpecImpl().is_compatible(j))
{
return std::make_unique<RootImpl>(j);
}
else if (v1::SpecImpl().is_compatible(j))
{
LOG_DEBUG << "Updating 'root' role spec version";
return std::make_unique<v1::RootImpl>(j);
}
{
LOG_ERROR << "Invalid spec version for 'root' update";
throw spec_version_error();
}
}
void RootImpl::load_from_json(const nlohmann::json& j)
{
from_json(j, *this);
// TUF spec 5.3.4 - Check for an arbitrary software attack
// Check signatures against current keyids and threshold
check_role_signatures(j, *this);
}
auto RootImpl::upgraded_signable() const -> nlohmann::json
{
nlohmann::json v1_equivalent_root;
v1_equivalent_root["roles"]["root"] = m_defined_roles.at("root").to_roles();
v1_equivalent_root["roles"]["targets"] = m_defined_roles.at("key_mgr").to_roles();
v1_equivalent_root["roles"]["snapshot"] = RoleKeys({ std::vector<std::string>(), 1 });
v1_equivalent_root["roles"]["timestamp"] = RoleKeys({ std::vector<std::string>(), 1 });
std::map<std::string, Key> v1_keys = m_defined_roles.at("root").to_keys();
auto key_mgr_keys = m_defined_roles.at("key_mgr").to_keys();
v1_keys.insert(key_mgr_keys.cbegin(), key_mgr_keys.cend());
v1_equivalent_root["keys"] = v1_keys;
v1_equivalent_root["_type"] = "root";
v1_equivalent_root["version"] = version();
v1_equivalent_root["spec_version"] = "1.0.17";
v1_equivalent_root["expires"] = expires();
return v1_equivalent_root;
}
auto
RootImpl::upgraded_signature(const nlohmann::json& j, const std::string& pk, const std::byte* sk) const
-> RoleSignature
{
std::array<std::byte, MAMBA_ED25519_SIGSIZE_BYTES> sig_bin;
sign(j.dump(), sk, sig_bin.data());
const auto sig_bin_data = sig_bin.data();
auto sig_hex = util::bytes_to_hex_str(sig_bin_data, sig_bin_data + sig_bin.size());
return { pk, sig_hex };
}
auto RootImpl::self_keys() const -> RoleFullKeys
{
return m_defined_roles.at("root");
}
auto RootImpl::mandatory_defined_roles() const -> std::set<std::string>
{
return { "root", "key_mgr" };
}
auto RootImpl::optionally_defined_roles() const -> std::set<std::string>
{
return {};
}
void RootImpl::set_defined_roles(std::map<std::string, RolePubKeys> keys)
{
m_defined_roles.clear();
for (auto& it : keys)
{
std::map<std::string, Key> role_keys;
for (auto& key : it.second.pubkeys)
{
role_keys.insert({ key, Key::from_ed25519(key) });
}
m_defined_roles.insert({ it.first, { role_keys, it.second.threshold } });
}
}
auto RootImpl::build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& base_url,
const fs::u8path& cache_path
) const -> std::unique_ptr<RepoIndexChecker>
{
fs::u8path metadata_path = cache_path / "key_mgr.json";
auto tmp_dir = TemporaryDirectory();
auto tmp_metadata_path = tmp_dir.path() / "key_mgr.json";
const auto url = specs::CondaURL::parse(base_url) / "key_mgr.json";
if (check_resource_exists(url.pretty_str(), context))
{
DownloadRequest request(
"key_mgr.json",
url.str(util::URL::Credentials::Show),
tmp_metadata_path.string()
);
DownloadResult res = download(std::move(request), context);
if (res)
{
KeyMgrRole key_mgr = create_key_mgr(tmp_metadata_path);
// TUF spec 5.6.5 - Check for a freeze attack
// 'key_mgr' (equivalent of 'targets') role should not be expired
// https://theupdateframework.github.io/specification/latest/#update-targets
if (key_mgr.expired(time_reference))
{
LOG_ERROR << "Possible freeze attack of 'key_mgr' metadata.\nExpired: "
<< key_mgr.expires();
throw freeze_error();
}
// 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(context, time_reference, base_url, cache_path);
}
}
// Fallback to local cached-copy if existing
if (fs::exists(metadata_path))
{
KeyMgrRole key_mgr = create_key_mgr(metadata_path);
return key_mgr.build_index_checker(context, time_reference, base_url, cache_path);
}
LOG_ERROR << "Error while fetching 'key_mgr' metadata";
throw fetching_error();
}
auto RootImpl::create_key_mgr(const fs::u8path& p) const -> KeyMgrRole
{
return { p, all_keys()["key_mgr"], spec_impl() };
}
auto RootImpl::create_key_mgr(const nlohmann::json& j) const -> KeyMgrRole
{
return { j, all_keys()["key_mgr"], spec_impl() };
}
void to_json(nlohmann::json& j, const RootImpl& r)
{
to_json(j, static_cast<const RoleBase&>(r));
}
void from_json(const nlohmann::json& j, RootImpl& role)
{
auto j_signed = j.at("signed");
try
{
from_json(j_signed, static_cast<RoleBase&>(role));
role.set_timestamp(j_signed.at("timestamp").get<std::string>());
auto type = j_signed.at("type").get<std::string>();
if (type != role.type())
{
LOG_ERROR << "Wrong 'type' found in 'root' metadata, should be 'root': '" << type
<< "'";
throw role_metadata_error();
}
role.set_spec_version(
std::make_shared<SpecImpl>(j_signed.at("metadata_spec_version").get<std::string>())
);
role.set_defined_roles(
j_signed.at("delegations").get<std::map<std::string, RolePubKeys>>()
);
}
catch (const nlohmann::json::exception& e)
{
LOG_ERROR << "Invalid 'root' metadata: " << e.what();
throw role_metadata_error();
}
role.check_expiration_format();
role.check_timestamp_format();
role.check_defined_roles();
}
KeyMgrRole::KeyMgrRole(const fs::u8path& p, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec)
: RoleBase("key_mgr", spec)
, m_keys(std::move(keys))
{
auto j = read_json_file(p);
load_from_json(j);
}
KeyMgrRole::KeyMgrRole(const nlohmann::json& j, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec)
: RoleBase("key_mgr", spec)
, m_keys(std::move(keys))
{
load_from_json(j);
}
KeyMgrRole::KeyMgrRole(
const std::string& json_str,
RoleFullKeys keys,
const std::shared_ptr<SpecBase> spec
)
: RoleBase("key_mgr", spec)
, m_keys(std::move(keys))
{
load_from_json(nlohmann::json::parse(json_str));
}
void KeyMgrRole::load_from_json(const nlohmann::json& j)
{
from_json(j, *this);
// Check signatures against keyids and threshold
check_role_signatures(j, *this);
}
auto KeyMgrRole::self_keys() const -> RoleFullKeys
{
return m_keys;
}
auto KeyMgrRole::create_pkg_mgr(const fs::u8path& p) const -> PkgMgrRole
{
return { p, all_keys()["pkg_mgr"], spec_impl() };
}
auto KeyMgrRole::create_pkg_mgr(const nlohmann::json& j) const -> PkgMgrRole
{
return { j, all_keys()["pkg_mgr"], spec_impl() };
}
auto KeyMgrRole::build_index_checker(
Context& context,
const TimeRef& time_reference,
const std::string& base_url,
const fs::u8path& cache_path
) const -> std::unique_ptr<RepoIndexChecker>
{
fs::u8path metadata_path = cache_path / "pkg_mgr.json";
auto tmp_dir = TemporaryDirectory();
auto tmp_metadata_path = tmp_dir.path() / "pkg_mgr.json";
const auto url = mamba::util::URL::parse(base_url + "/pkg_mgr.json");
if (check_resource_exists(url.pretty_str(), context))
{
DownloadRequest request("pkg_mgr.json", url.pretty_str(), tmp_metadata_path.string());
DownloadResult res = download(std::move(request), context);
if (res)
{
PkgMgrRole pkg_mgr = create_pkg_mgr(tmp_metadata_path);
// TUF spec 5.6.5 - Check for a freeze attack
// 'pkg_mgr' (equivalent of delegated 'targets') role should not be expired
// https://theupdateframework.github.io/specification/latest/#update-targets
if (pkg_mgr.expired(time_reference))
{
LOG_ERROR << "Possible freeze attack of 'pkg_mgr' metadata.\nExpired: "
<< pkg_mgr.expires();
throw freeze_error();
}
// 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 std::make_unique<PkgMgrRole>(pkg_mgr);
}
}
// Fallback to local cached-copy if existing
if (fs::exists(metadata_path))
{
return std::make_unique<PkgMgrRole>(create_pkg_mgr(metadata_path));
}
LOG_ERROR << "Error while fetching 'pkg_mgr' metadata";
throw fetching_error();
}
auto KeyMgrRole::mandatory_defined_roles() const -> std::set<std::string>
{
return { "pkg_mgr" };
}
auto KeyMgrRole::optionally_defined_roles() const -> std::set<std::string>
{
return {};
}
void KeyMgrRole::set_defined_roles(std::map<std::string, RolePubKeys> keys)
{
m_defined_roles.clear();
for (auto& it : keys)
{
std::map<std::string, Key> role_keys;
for (auto& key : it.second.pubkeys)
{
role_keys.insert({ key, Key::from_ed25519(key) });
}
m_defined_roles.insert({ it.first, { role_keys, it.second.threshold } });
}
}
void to_json(nlohmann::json& j, const KeyMgrRole& r)
{
to_json(j, static_cast<const RoleBase&>(r));
}
void from_json(const nlohmann::json& j, KeyMgrRole& role)
{
auto j_signed = j.at("signed");
try
{
from_json(j_signed, static_cast<RoleBase&>(role));
role.set_timestamp(j_signed.at("timestamp").get<std::string>());
auto type = j_signed.at("type").get<std::string>();
if (type != role.type())
{
LOG_ERROR << "Wrong 'type' found in 'key_mgr' metadata, should be 'key_mgr': '"
<< type << "'";
throw role_metadata_error();
}
auto new_spec_version = j_signed.at(role.spec_version().json_key()).get<std::string>();
if (role.spec_version() != SpecImpl(new_spec_version))
{
LOG_ERROR << "Invalid spec version '" << new_spec_version
<< "' in 'key_mgr' metadata, it should match exactly 'root' spec version: '"
<< role.spec_version().version_str() << "'";
throw spec_version_error();
}
role.set_defined_roles(
j_signed.at("delegations").get<std::map<std::string, RolePubKeys>>()
);
}
catch (const nlohmann::json::exception& e)
{
LOG_ERROR << "Invalid 'key_mgr' metadata: " << e.what();
throw role_metadata_error();
}
role.check_expiration_format();
role.check_timestamp_format();
role.check_defined_roles();
}
PkgMgrRole::PkgMgrRole(RoleFullKeys keys, const std::shared_ptr<SpecBase> spec)
: RoleBase("pkg_mgr", spec)
, m_keys(std::move(keys))
{
}
PkgMgrRole::PkgMgrRole(const fs::u8path& p, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec)
: RoleBase("pkg_mgr", spec)
, m_keys(std::move(keys))
{
auto j = read_json_file(p);
load_from_json(j);
}
PkgMgrRole::PkgMgrRole(const nlohmann::json& j, RoleFullKeys keys, const std::shared_ptr<SpecBase> spec)
: RoleBase("pkg_mgr", spec)
, m_keys(std::move(keys))
{
load_from_json(j);
}
PkgMgrRole::PkgMgrRole(
const std::string& json_str,
RoleFullKeys keys,
const std::shared_ptr<SpecBase> spec
)
: RoleBase("pkg_mgr", spec)
, m_keys(std::move(keys))
{
load_from_json(nlohmann::json::parse(json_str));
}
void PkgMgrRole::load_from_json(const nlohmann::json& j)
{
from_json(j, *this);
// Check signatures against keyids and threshold
check_role_signatures(j, *this);
}
void PkgMgrRole::set_defined_roles(std::map<std::string, RolePubKeys> keys)
{
m_defined_roles.clear();
for (auto& it : keys)
{
std::map<std::string, Key> role_keys;
for (auto& key : it.second.pubkeys)
{
role_keys.insert({ key, Key::from_ed25519(key) });
}
m_defined_roles.insert({ it.first, { role_keys, it.second.threshold } });
}
}
void to_json(nlohmann::json& j, const PkgMgrRole& r)
{
to_json(j, static_cast<const RoleBase&>(r));
}
void from_json(const nlohmann::json& j, PkgMgrRole& role)
{
auto j_signed = j.at("signed");
try
{
from_json(j_signed, static_cast<RoleBase&>(role));
role.set_timestamp(j_signed.at("timestamp").get<std::string>());
auto type = j_signed.at("type").get<std::string>();
if (type != role.type())
{
LOG_ERROR << "Wrong 'type' found in 'pkg_mgr' metadata, should be 'pkg_mgr': '"
<< type << "'";
throw role_metadata_error();
}
auto new_spec_version = j_signed.at(role.spec_version().json_key()).get<std::string>();
if (role.spec_version() != SpecImpl(new_spec_version))
{
LOG_ERROR << "Invalid spec version '" << new_spec_version
<< "' in 'pkg_mgr' metadata, it should match exactly 'root' spec version: '"
<< role.spec_version().version_str() << "'";
throw spec_version_error();
}
role.set_defined_roles(
j_signed.at("delegations").get<std::map<std::string, RolePubKeys>>()
);
}
catch (const nlohmann::json::exception& e)
{
LOG_ERROR << "Invalid 'pkg_mgr' metadata: " << e.what();
throw role_metadata_error();
}
role.check_expiration_format();
role.check_timestamp_format();
role.check_defined_roles();
}
auto PkgMgrRole::self_keys() const -> RoleFullKeys
{
return m_keys;
}
auto PkgMgrRole::pkg_signatures(const nlohmann::json& j) const -> std::set<RoleSignature>
{
auto sigs = j.get<std::map<std::string, std::map<std::string, std::string>>>();
std::set<RoleSignature> unique_sigs;
for (auto& s : sigs)
{
std::string pgp_trailer = "";
if (s.second.find("other_headers") != s.second.end())
{
pgp_trailer = s.second["other_headers"];
}
unique_sigs.insert(RoleSignature({ s.first, s.second.at("signature"), pgp_trailer }));
}
return unique_sigs;
}
void
PkgMgrRole::check_pkg_signatures(const nlohmann::json& metadata, const nlohmann::json& signatures) const
{
std::string signed_data = canonicalize(metadata);
auto sigs = pkg_signatures(signatures);
auto k = self_keys();
check_signatures(signed_data, sigs, k);
}
void PkgMgrRole::verify_index(const nlohmann::json& j) const
{
try
{
auto packages = j.at("packages").get<nlohmann::json::object_t>();
auto sigs = j.at("signatures").get<nlohmann::json::object_t>();
for (auto& it : packages)
{
auto pkg_name = it.first;
auto pkg_meta = it.second;
auto pkg_sigs = sigs.at(pkg_name).get<nlohmann::json::object_t>();
try
{
check_pkg_signatures(pkg_meta, pkg_sigs);
}
catch (const threshold_error& e)
{
LOG_ERROR << "Validation failed on package: '" << pkg_name << "' : " << e.what();
throw package_error();
}
}
}
catch (const nlohmann::json::exception& e)
{
LOG_ERROR << "Invalid package index metadata: " << e.what();
throw index_error();
}
}
void PkgMgrRole::verify_index(const fs::u8path& p) const
{
if (!fs::exists(p))
{
LOG_ERROR << "'repodata' file not found at: " << p.string();
throw index_error();
}
std::ifstream i(p.std_path());
nlohmann::json j;
i >> j;
try
{
verify_index(j);
}
catch (const package_error& e)
{
LOG_ERROR << "Validation failed on package index: '" << p.string() << "' : " << e.what();
throw index_error();
}
}
void
PkgMgrRole::verify_package(const nlohmann::json& signed_data, const nlohmann::json& signatures) const
{
try
{
check_pkg_signatures(signed_data, signatures);
}
catch (const threshold_error& e)
{
LOG_ERROR << "Validation failed on package: '" << signed_data.at("name")
<< "' : " << e.what();
throw package_error();
}
}
} // namespace v06

View File

@ -0,0 +1,165 @@
// Copyright (c) 2023, QuantStack and Mamba Contributors
//
// Distributed under the terms of the BSD 3-Clause License.
//
// The full license is in the file LICENSE, distributed with this software.
#include <nlohmann/json.hpp>
#include "mamba/core/output.hpp"
#include "mamba/fs/filesystem.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/update_framework_v1.hpp"
namespace mamba::validation::v1
{
SpecImpl::SpecImpl(std::string sv)
: SpecBase(std::move(sv))
{
}
auto SpecImpl::json_key() const -> std::string
{
return "spec_version";
}
auto SpecImpl::expiration_json_key() const -> std::string
{
return "expires";
}
auto SpecImpl::signatures(const nlohmann::json& j) const -> std::set<RoleSignature>
{
auto sigs = j.at("signatures").get<std::vector<RoleSignature>>();
std::set<RoleSignature> unique_sigs(sigs.cbegin(), sigs.cend());
return unique_sigs;
}
RootImpl::RootImpl(const nlohmann::json& j)
: RootRole(std::make_shared<SpecImpl>())
{
load_from_json(j);
}
RootImpl::RootImpl(const fs::u8path& path)
: RootRole(std::make_shared<SpecImpl>())
{
auto j = read_json_file(path);
load_from_json(j);
}
auto RootImpl::create_update(const nlohmann::json& j) -> std::unique_ptr<RootRole>
{
if (v1::SpecImpl().is_compatible(j))
{
return std::make_unique<RootImpl>(j);
}
else
{
LOG_ERROR << "Invalid spec version for 'root' update";
throw spec_version_error();
}
}
void RootImpl::load_from_json(const nlohmann::json& j)
{
from_json(j, *this);
// TUF spec 5.3.4 - Check for an arbitrary software attack
// Check signatures against current keyids and threshold
check_role_signatures(j, *this);
}
auto RootImpl::self_keys() const -> RoleFullKeys
{
return m_defined_roles.at("root");
}
auto RootImpl::mandatory_defined_roles() const -> std::set<std::string>
{
return { "root", "snapshot", "targets", "timestamp" };
}
auto RootImpl::optionally_defined_roles() const -> std::set<std::string>
{
return { "mirrors" };
}
void RootImpl::set_defined_roles(
const std::map<std::string, Key>& keys,
const std::map<std::string, RoleKeys>& roles
)
{
m_defined_roles.clear();
for (auto& it : roles)
{
std::map<std::string, Key> role_keys;
for (auto& keyid : it.second.keyids)
{
try
{
role_keys.insert({ keyid, keys.at(keyid) });
}
catch (const std::out_of_range&)
{
LOG_ERROR << "Missing key in 'keys' is used in '" << it.first
<< "' delegation: '" << keyid << "'";
throw role_metadata_error();
}
}
m_defined_roles.insert({ it.first, { role_keys, it.second.threshold } });
}
}
auto RootImpl::build_index_checker(
Context&,
const TimeRef& /*time_reference*/,
const std::string& /*url*/,
const fs::u8path& /*cache_path*/
) const -> std::unique_ptr<RepoIndexChecker>
{
std::unique_ptr<RepoIndexChecker> ptr;
return ptr;
}
void to_json(nlohmann::json& j, const RootImpl& r)
{
to_json(j, static_cast<const RoleBase&>(r));
}
void from_json(const nlohmann::json& j, RootImpl& role)
{
auto j_signed = j.at("signed");
try
{
from_json(j_signed, static_cast<RoleBase&>(role));
auto type = j_signed.at("_type").get<std::string>();
if (type != role.type())
{
LOG_ERROR << "Wrong '_type' found in 'root' metadata, should be 'root': '" << type
<< "'";
throw role_metadata_error();
}
role.set_spec_version(
std::make_shared<SpecImpl>(j_signed.at("spec_version").get<std::string>())
);
auto keys = j_signed.at("keys").get<std::map<std::string, Key>>();
auto roles = j_signed.at("roles").get<std::map<std::string, RoleKeys>>();
role.set_defined_roles(keys, roles);
}
catch (const nlohmann::json::exception& e)
{
LOG_ERROR << "Invalid 'root' metadata: " << e.what();
throw role_metadata_error();
}
role.check_expiration_format();
role.check_defined_roles();
}
}

View File

@ -49,6 +49,10 @@ set(
src/specs/test_repo_data.cpp
src/specs/test_version.cpp
src/specs/test_version_spec.cpp
# Artifacts validation
src/validation/test_tools.cpp
src/validation/test_update_framework_v0_6.cpp
src/validation/test_update_framework_v1.cpp
# The un-triaged rest
../longpath.manifest
src/core/test_activation.cpp
@ -66,9 +70,6 @@ set(
src/core/test_progress_bar.cpp
src/core/test_shell_init.cpp
src/core/test_thread_utils.cpp
src/core/test_validate_common.cpp
src/core/test_validate_v06.cpp
src/core/test_validate_v1.cpp
src/core/test_virtual_packages.cpp
src/core/test_util.cpp
src/core/test_env_lockfile.cpp

View File

@ -8,23 +8,22 @@
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include "mamba/core/validate.hpp"
#include "mamba/core/util.hpp"
#include "mamba/util/encoding.hpp"
#include "mamba/validation/tools.hpp"
using namespace mamba;
using namespace mamba::validation;
namespace nl = nlohmann;
// TODO validate API should move to std::byte
template <std::size_t size>
auto
hex_str(const std::array<std::byte, size>& bytes)
{
auto data = reinterpret_cast<const std::byte*>(bytes.data());
return util::bytes_to_hex_str(data, data + bytes.size());
return util::bytes_to_hex_str(bytes.data(), bytes.data() + bytes.size());
}
TEST_SUITE("Validate")
TEST_SUITE("validation::tools")
{
TEST_CASE("sha256sum")
{
@ -116,7 +115,7 @@ protected:
std::array<std::byte, MAMBA_ED25519_SIGSIZE_BYTES> signature;
};
TEST_SUITE("VerifyMsg")
TEST_SUITE("validation::VerifyMsg")
{
TEST_CASE_FIXTURE(VerifyMsg, "from_bytes")
{
@ -189,7 +188,7 @@ protected:
std::string data;
};
TEST_SUITE("VerifyGPGMsg")
TEST_SUITE("validation::VerifyGPGMsg")
{
TEST_CASE_FIXTURE(VerifyGPGMsg, "verify_gpg_hashed_msg_from_bin")
{

View File

@ -11,9 +11,14 @@
#include <spdlog/spdlog.h>
#include "mamba/core/fsutil.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/core/util.hpp"
#include "mamba/util/encoding.hpp"
#include "mamba/util/path_manip.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/repo_checker.hpp"
#include "mamba/validation/tools.hpp"
#include "mamba/validation/update_framework_v0_6.hpp"
#include "mamba/validation/update_framework_v1.hpp"
#include "mambatests.hpp"
@ -21,14 +26,14 @@ using namespace mamba;
using namespace mamba::validation;
namespace nl = nlohmann;
class RootImplT_v06
class RootImplT_v0_6
{
public:
using role_secrets_type = std::map<std::string, std::array<std::byte, MAMBA_ED25519_KEYSIZE_BYTES>>;
using secrets_type = std::map<std::string, role_secrets_type>;
RootImplT_v06()
RootImplT_v0_6()
{
channel_dir = std::make_unique<mamba::TemporaryDirectory>();
@ -132,7 +137,7 @@ public:
return signatures;
}
auto upgrade_to_v1(const v06::RootImpl& root, const nl::json& patch = nl::json()) -> nl::json
auto upgrade_to_v1(const v0_6::RootImpl& root, const nl::json& patch = nl::json()) -> nl::json
{
auto root_meta = root.upgraded_signable();
if (!patch.empty())
@ -183,59 +188,59 @@ protected:
}
};
TEST_SUITE("RootImplT_v06")
TEST_SUITE("validation::v0_6::RootImpl")
{
TEST_CASE_FIXTURE(RootImplT_v06, "ctor_from_path")
TEST_CASE_FIXTURE(RootImplT_v0_6, "ctor_from_path")
{
v06::RootImpl root(trusted_root_file_raw_key());
v0_6::RootImpl root(trusted_root_file_raw_key());
CHECK_EQ(root.type(), "root");
CHECK_EQ(root.file_ext(), "json");
CHECK_EQ(root.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(root.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(root.version(), 1);
}
TEST_CASE_FIXTURE(RootImplT_v06, "ctor_from_path_pgp_signed")
TEST_CASE_FIXTURE(RootImplT_v0_6, "ctor_from_path_pgp_signed")
{
v06::RootImpl root(trusted_root_file_pgp());
v0_6::RootImpl root(trusted_root_file_pgp());
CHECK_EQ(root.type(), "root");
CHECK_EQ(root.file_ext(), "json");
CHECK_EQ(root.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(root.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(root.version(), 1);
}
TEST_CASE_FIXTURE(RootImplT_v06, "ctor_from_json")
TEST_CASE_FIXTURE(RootImplT_v0_6, "ctor_from_json")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
CHECK_EQ(root.type(), "root");
CHECK_EQ(root.file_ext(), "json");
CHECK_EQ(root.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(root.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(root.version(), 1);
}
TEST_CASE_FIXTURE(RootImplT_v06, "ctor_from_json_str")
TEST_CASE_FIXTURE(RootImplT_v0_6, "ctor_from_json_str")
{
v06::RootImpl root(root1_json.dump());
v0_6::RootImpl root(root1_json.dump());
CHECK_EQ(root.type(), "root");
CHECK_EQ(root.file_ext(), "json");
CHECK_EQ(root.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(root.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(root.version(), 1);
}
TEST_CASE_FIXTURE(RootImplT_v06, "ctor_from_json_pgp_signed")
TEST_CASE_FIXTURE(RootImplT_v0_6, "ctor_from_json_pgp_signed")
{
v06::RootImpl root(root1_pgp_json);
v0_6::RootImpl root(root1_pgp_json);
CHECK_EQ(root.type(), "root");
CHECK_EQ(root.file_ext(), "json");
CHECK_EQ(root.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(root.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(root.version(), 1);
}
TEST_CASE_FIXTURE(RootImplT_v06, "ctor_wrong_filename_spec_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "ctor_wrong_filename_spec_version")
{
fs::u8path p = channel_dir->path() / "2.sv1.root.json";
@ -244,31 +249,31 @@ TEST_SUITE("RootImplT_v06")
out_file.close();
// "2.sv1.root.json" is not compatible spec version (spec version N)
CHECK_THROWS_AS(v06::RootImpl root(p), role_file_error);
CHECK_THROWS_AS(v0_6::RootImpl root(p), role_file_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "update_from_path")
TEST_CASE_FIXTURE(RootImplT_v0_6, "update_from_path")
{
using namespace mamba;
auto f = trusted_root_file_raw_key();
v06::RootImpl root(f);
v0_6::RootImpl root(f);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 2 }
])"_json;
auto updated_root = root.update(create_root_update("2.root.json", patch));
auto testing_root = static_cast<v06::RootImpl*>(updated_root.get());
auto testing_root = static_cast<v0_6::RootImpl*>(updated_root.get());
CHECK_EQ(testing_root->type(), "root");
CHECK_EQ(testing_root->file_ext(), "json");
CHECK_EQ(testing_root->spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(testing_root->spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(testing_root->version(), 2);
}
TEST_CASE_FIXTURE(RootImplT_v06, "wrong_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "wrong_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 3 }
@ -277,9 +282,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "spec_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "spec_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 2 },
@ -287,15 +292,15 @@ TEST_SUITE("RootImplT_v06")
])"_json;
auto updated_root = root.update(create_root_update("2.root.json", patch));
auto testing_root = static_cast<v06::RootImpl*>(updated_root.get());
CHECK_EQ(testing_root->spec_version(), v06::SpecImpl("0.6.1"));
auto testing_root = static_cast<v0_6::RootImpl*>(updated_root.get());
CHECK_EQ(testing_root->spec_version(), v0_6::SpecImpl("0.6.1"));
CHECK_EQ(testing_root->version(), 2);
CHECK_EQ(testing_root->expires(), root.expires());
}
TEST_CASE_FIXTURE(RootImplT_v06, "upgraded_spec_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "upgraded_spec_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 2 },
@ -318,14 +323,14 @@ TEST_SUITE("RootImplT_v06")
auto testing_root = dynamic_cast<v1::RootImpl*>(updated_root.get());
REQUIRE_NE(testing_root, nullptr);
CHECK_EQ(testing_root->spec_version(), v06::SpecImpl("1.0.17"));
CHECK_EQ(testing_root->spec_version(), v0_6::SpecImpl("1.0.17"));
CHECK_EQ(testing_root->version(), 2);
CHECK_LT(testing_root->expires(), root.expires());
}
TEST_CASE_FIXTURE(RootImplT_v06, "equivalent_upgraded_spec_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "equivalent_upgraded_spec_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json signable_patch = R"([
{ "op": "add", "path": "/keys/dummy_value", "value": { "keytype": "ed25519", "scheme": "ed25519", "keyval": "dummy_value" } },
@ -338,9 +343,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_EQ(updated_root.version(), 1);
}
TEST_CASE_FIXTURE(RootImplT_v06, "wrong_spec_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "wrong_spec_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 2 },
@ -357,23 +362,23 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), spec_version_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "wrong_filename_role")
TEST_CASE_FIXTURE(RootImplT_v0_6, "wrong_filename_role")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
CHECK_THROWS_AS(root.update(create_root_update("2.rooot.json")), role_file_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "wrong_filename_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "wrong_filename_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
CHECK_THROWS_AS(root.update(create_root_update("3.root.json")), role_file_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "wrong_filename_spec_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "wrong_filename_spec_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
// "2.sv1.root.json" is upgradable spec version (spec version N+1)
nl::json signable_patch = R"([
@ -386,7 +391,7 @@ TEST_SUITE("RootImplT_v06")
auto updated_root = root.update(upgrade_to_v1(root, signable_patch));
auto testing_root = dynamic_cast<v1::RootImpl*>(updated_root.get());
REQUIRE_NE(testing_root, nullptr);
CHECK_EQ(testing_root->spec_version(), v06::SpecImpl("1.0.0"));
CHECK_EQ(testing_root->spec_version(), v0_6::SpecImpl("1.0.0"));
// "2.sv2.root.json" is not upgradable spec version (spec version N+1)
nl::json patch = R"([
@ -395,18 +400,18 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.sv2.root.json", patch)), role_file_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "illformed_filename_version")
TEST_CASE_FIXTURE(RootImplT_v0_6, "illformed_filename_version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
CHECK_THROWS_AS(root.update(create_root_update("wrong.root.json")), role_file_error);
CHECK_THROWS_AS(root.update(create_root_update("2..root.json")), role_file_error);
CHECK_THROWS_AS(root.update(create_root_update("2.sv04.root.json")), role_file_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "rollback_attack")
TEST_CASE_FIXTURE(RootImplT_v0_6, "rollback_attack")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 1 }
@ -415,9 +420,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), rollback_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "wrong_type")
TEST_CASE_FIXTURE(RootImplT_v0_6, "wrong_type")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/type", "value": "timestamp" },
@ -427,9 +432,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "missing_type")
TEST_CASE_FIXTURE(RootImplT_v0_6, "missing_type")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "remove", "path": "/signed/type" },
@ -439,9 +444,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "missing_delegations")
TEST_CASE_FIXTURE(RootImplT_v0_6, "missing_delegations")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "remove", "path": "/signed/delegations" },
@ -451,9 +456,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "missing_delegation")
TEST_CASE_FIXTURE(RootImplT_v0_6, "missing_delegation")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "remove", "path": "/signed/delegations/root" },
@ -463,9 +468,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "empty_delegation_pubkeys")
TEST_CASE_FIXTURE(RootImplT_v0_6, "empty_delegation_pubkeys")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/delegations/root/pubkeys", "value": [] },
@ -475,9 +480,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "null_role_threshold")
TEST_CASE_FIXTURE(RootImplT_v0_6, "null_role_threshold")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/delegations/root/threshold", "value": 0 },
@ -487,9 +492,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "extra_roles")
TEST_CASE_FIXTURE(RootImplT_v0_6, "extra_roles")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "add", "path": "/signed/delegations/some_wrong_role",
@ -514,9 +519,9 @@ TEST_SUITE("RootImplT_v06")
CHECK(mirrors_role_found);
}
*/
TEST_CASE_FIXTURE(RootImplT_v06, "threshold_not_met")
TEST_CASE_FIXTURE(RootImplT_v0_6, "threshold_not_met")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch = R"([
{ "op": "replace", "path": "/signed/version", "value": 2 },
@ -526,9 +531,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "expires")
TEST_CASE_FIXTURE(RootImplT_v0_6, "expires")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
// expiration is set to now+3600s in 'sign_root'
TimeRef time_ref;
@ -546,13 +551,13 @@ TEST_SUITE("RootImplT_v06")
);
auto updated_root = root.update(create_root_update("2.root.json", patch));
auto testing_root = static_cast<v06::RootImpl*>(updated_root.get());
auto testing_root = static_cast<v0_6::RootImpl*>(updated_root.get());
CHECK_FALSE(testing_root->expired(time_ref));
}
TEST_CASE_FIXTURE(RootImplT_v06, "timestamp")
TEST_CASE_FIXTURE(RootImplT_v0_6, "timestamp")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch;
@ -575,9 +580,9 @@ TEST_SUITE("RootImplT_v06")
CHECK_THROWS_AS(root.update(create_root_update("2.root.json", patch)), role_metadata_error);
}
TEST_CASE_FIXTURE(RootImplT_v06, "possible_update_files")
TEST_CASE_FIXTURE(RootImplT_v0_6, "possible_update_files")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
auto update_f = root.possible_update_files();
CHECK(update_f[0].string().c_str() == doctest::Contains("2.sv1.root.json"));
@ -605,14 +610,14 @@ public:
protected:
v06::SpecImpl spec;
v0_6::SpecImpl spec;
};
TEST_SUITE("SpecImplT_v06")
TEST_SUITE("validation::v0_6::SpecImpl")
{
TEST_CASE_FIXTURE(SpecImplT_v06, "ctor")
{
v06::SpecImpl new_spec("0.6.1");
v0_6::SpecImpl new_spec("0.6.1");
CHECK_EQ(new_spec.version_str(), "0.6.1");
}
@ -700,12 +705,12 @@ TEST_SUITE("SpecImplT_v06")
}
class KeyMgrT_v06 : public RootImplT_v06
class KeyMgrT_v06 : public RootImplT_v0_6
{
public:
KeyMgrT_v06()
: RootImplT_v06()
: RootImplT_v0_6()
{
sign_key_mgr();
}
@ -780,33 +785,33 @@ protected:
}
};
TEST_SUITE("KeyMgrT_v06")
TEST_SUITE("validation::v0_6::KeyMgr")
{
TEST_CASE_FIXTURE(KeyMgrT_v06, "ctor_from_json")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
auto key_mgr = root.create_key_mgr(key_mgr_json);
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 1);
}
TEST_CASE_FIXTURE(KeyMgrT_v06, "ctor_from_json_str")
{
v06::RootImpl root(root1_json);
auto key_mgr = v06::KeyMgrRole(
v0_6::RootImpl root(root1_json);
auto key_mgr = v0_6::KeyMgrRole(
key_mgr_json.dump(),
root.all_keys()["key_mgr"],
std::make_shared<v06::SpecImpl>(v06::SpecImpl())
std::make_shared<v0_6::SpecImpl>(v0_6::SpecImpl())
);
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 1);
}
TEST_CASE_FIXTURE(KeyMgrT_v06, "version")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
{
nl::json key_mgr_patch = R"([
@ -814,7 +819,7 @@ TEST_SUITE("KeyMgrT_v06")
])"_json;
auto key_mgr = root.create_key_mgr(patched_key_mgr_json(key_mgr_patch));
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 2);
}
@ -824,14 +829,14 @@ TEST_SUITE("KeyMgrT_v06")
])"_json;
auto key_mgr = root.create_key_mgr(patched_key_mgr_json(key_mgr_patch));
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 20);
}
}
TEST_CASE_FIXTURE(KeyMgrT_v06, "spec_version")
{ // spec version as to match exactly 'root' spec version
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
{
nl::json key_mgr_patch = R"([
@ -839,7 +844,7 @@ TEST_SUITE("KeyMgrT_v06")
])"_json;
auto key_mgr = root.create_key_mgr(patched_key_mgr_json(key_mgr_patch));
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 1);
}
@ -865,15 +870,15 @@ TEST_SUITE("KeyMgrT_v06")
TEST_CASE_FIXTURE(KeyMgrT_v06, "ctor_from_path")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
auto key_mgr = root.create_key_mgr(write_key_mgr_file(key_mgr_json));
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 1);
// TODO: enforce consistency between spec version in filename and metadata
key_mgr = root.create_key_mgr(write_key_mgr_file(key_mgr_json, "20.sv0.6.key_mgr.json"));
CHECK_EQ(key_mgr.spec_version(), v06::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.spec_version(), v0_6::SpecImpl("0.6.0"));
CHECK_EQ(key_mgr.version(), 1);
@ -897,7 +902,7 @@ TEST_SUITE("KeyMgrT_v06")
TEST_CASE_FIXTURE(KeyMgrT_v06, "expires")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
auto key_mgr = root.create_key_mgr(key_mgr_json);
// expiration is set to now+3600s in 'sign_key_mgr'
@ -923,7 +928,7 @@ TEST_SUITE("KeyMgrT_v06")
TEST_CASE_FIXTURE(KeyMgrT_v06, "timestamp")
{
v06::RootImpl root(root1_json);
v0_6::RootImpl root(root1_json);
nl::json patch;
@ -957,7 +962,7 @@ public:
{
sign_pkg_mgr();
generate_index_checkerdata();
root = std::make_unique<v06::RootImpl>(root1_json);
root = std::make_unique<v0_6::RootImpl>(root1_json);
};
auto sign_repodata(const nl::json& patch = nl::json()) -> nl::json
@ -1035,7 +1040,7 @@ protected:
nl::json pkg_mgr_json, repodata_json, signed_repodata_json;
std::unique_ptr<v06::RootImpl> root;
std::unique_ptr<v0_6::RootImpl> root;
auto sign_pkg_mgr_meta(const nl::json& meta) -> nl::json
{
@ -1103,7 +1108,7 @@ protected:
}
};
TEST_SUITE("PkgMgrT_v06")
TEST_SUITE("validation::v0_6::PkgMgr")
{
TEST_CASE_FIXTURE(PkgMgrT_v06, "verify_index")
{
@ -1182,7 +1187,7 @@ protected:
}
};
TEST_SUITE("RepoCheckerT")
TEST_SUITE("validation::v0_6::RepoChecker")
{
TEST_CASE_FIXTURE(RepoCheckerT, "ctor")
{

View File

@ -11,8 +11,11 @@
#include <spdlog/spdlog.h>
#include "mamba/core/fsutil.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/core/util.hpp"
#include "mamba/util/encoding.hpp"
#include "mamba/validation/errors.hpp"
#include "mamba/validation/tools.hpp"
#include "mamba/validation/update_framework_v1.hpp"
#include "mambatests.hpp"
@ -149,7 +152,7 @@ protected:
}
};
TEST_SUITE("RootImplT_v1")
TEST_SUITE("validation::v1::RootImpl")
{
TEST_CASE_FIXTURE(RootImplT_v1, "ctor_from_path")
{
@ -495,7 +498,7 @@ protected:
v1::SpecImpl spec;
};
TEST_SUITE("SpecImplT_v1")
TEST_SUITE("validation::v1::SpecImpl")
{
TEST_CASE_FIXTURE(SpecImplT_v1, "ctore")
{
@ -581,7 +584,7 @@ TEST_SUITE("SpecImplT_v1")
}
}
TEST_SUITE("RoleSignature")
TEST_SUITE("validation::v1::RoleSignature")
{
// Test serialization/deserialization
TEST_CASE("to_json")

View File

@ -33,10 +33,11 @@
#include "mamba/core/subdirdata.hpp"
#include "mamba/core/transaction.hpp"
#include "mamba/core/util_os.hpp"
#include "mamba/core/validate.hpp"
#include "mamba/core/virtual_packages.hpp"
#include "mamba/specs/version.hpp"
#include "mamba/util/string.hpp"
#include "mamba/validation/tools.hpp"
#include "mamba/validation/update_framework_v0_6.hpp"
#include "bindings.hpp"
#include "flat_set_caster.hpp"
@ -1150,53 +1151,53 @@ bind_submodule_impl(pybind11::module_ m)
.def_property_readonly("expired", &validation::RoleBase::expired)
.def("all_keys", &validation::RoleBase::all_keys);
py::class_<validation::v06::V06RoleBaseExtension, std::shared_ptr<validation::v06::V06RoleBaseExtension>>(
py::class_<validation::v0_6::V06RoleBaseExtension, std::shared_ptr<validation::v0_6::V06RoleBaseExtension>>(
m,
"RoleBaseExtension"
)
.def_property_readonly("timestamp", &validation::v06::V06RoleBaseExtension::timestamp);
.def_property_readonly("timestamp", &validation::v0_6::V06RoleBaseExtension::timestamp);
py::class_<validation::v06::SpecImpl, validation::SpecBase, std::shared_ptr<validation::v06::SpecImpl>>(
py::class_<validation::v0_6::SpecImpl, validation::SpecBase, std::shared_ptr<validation::v0_6::SpecImpl>>(
m,
"SpecImpl"
)
.def(py::init<>());
py::class_<
validation::v06::KeyMgrRole,
validation::v0_6::KeyMgrRole,
validation::RoleBase,
validation::v06::V06RoleBaseExtension,
std::shared_ptr<validation::v06::KeyMgrRole>>(m, "KeyMgr")
validation::v0_6::V06RoleBaseExtension,
std::shared_ptr<validation::v0_6::KeyMgrRole>>(m, "KeyMgr")
.def(py::init<
const std::string&,
const validation::RoleFullKeys&,
const std::shared_ptr<validation::SpecBase>>());
py::class_<
validation::v06::PkgMgrRole,
validation::v0_6::PkgMgrRole,
validation::RoleBase,
validation::v06::V06RoleBaseExtension,
std::shared_ptr<validation::v06::PkgMgrRole>>(m, "PkgMgr")
validation::v0_6::V06RoleBaseExtension,
std::shared_ptr<validation::v0_6::PkgMgrRole>>(m, "PkgMgr")
.def(py::init<
const std::string&,
const validation::RoleFullKeys&,
const std::shared_ptr<validation::SpecBase>>());
py::class_<
validation::v06::RootImpl,
validation::v0_6::RootImpl,
validation::RoleBase,
validation::v06::V06RoleBaseExtension,
std::shared_ptr<validation::v06::RootImpl>>(m, "RootImpl")
validation::v0_6::V06RoleBaseExtension,
std::shared_ptr<validation::v0_6::RootImpl>>(m, "RootImpl")
.def(py::init<const std::string&>(), py::arg("json_str"))
.def(
"update",
[](validation::v06::RootImpl& role, const std::string& json_str)
[](validation::v0_6::RootImpl& role, const std::string& json_str)
{ return role.update(nlohmann::json::parse(json_str)); },
py::arg("json_str")
)
.def(
"create_key_mgr",
[](validation::v06::RootImpl& role, const std::string& json_str)
[](validation::v0_6::RootImpl& role, const std::string& json_str)
{ return role.create_key_mgr(nlohmann::json::parse(json_str)); },
py::arg("json_str")
);