mirror of https://github.com/mamba-org/mamba.git
1989 lines
78 KiB
C++
1989 lines
78 KiB
C++
#include "mamba/core/environment.hpp"
|
|
#include "mamba/core/fsutil.hpp"
|
|
#include "mamba/core/validate.hpp"
|
|
#include "mamba/core/util.hpp"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <gmock/gmock.h>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
|
|
#include "spdlog/spdlog.h"
|
|
|
|
namespace validate
|
|
{
|
|
namespace testing
|
|
{
|
|
using nlohmann::json;
|
|
|
|
TEST(Validate, sha256sum)
|
|
{
|
|
auto f = mamba::open_ofstream("sometestfile.txt");
|
|
f << "test";
|
|
f.close();
|
|
auto sha256 = sha256sum("sometestfile.txt");
|
|
EXPECT_EQ(sha256, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
|
|
|
|
auto md5 = md5sum("sometestfile.txt");
|
|
EXPECT_EQ(md5, "098f6bcd4621d373cade4e832627b4f6");
|
|
}
|
|
|
|
|
|
TEST(Validate, ed25519_key_hex_to_bytes)
|
|
{
|
|
std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES> pk, sk;
|
|
generate_ed25519_keypair(pk.data(), sk.data());
|
|
|
|
auto pk_hex = ::mamba::hex_string(pk);
|
|
auto pk_bytes = ed25519_key_hex_to_bytes(pk_hex);
|
|
EXPECT_EQ(pk_hex, ::mamba::hex_string(pk_bytes));
|
|
|
|
spdlog::set_level(spdlog::level::debug);
|
|
|
|
std::array<unsigned char, 5> not_even_key;
|
|
pk_hex = ::mamba::hex_string(not_even_key);
|
|
pk_bytes = ed25519_key_hex_to_bytes(pk_hex);
|
|
EXPECT_FALSE(pk_hex == ::mamba::hex_string(pk_bytes));
|
|
|
|
std::array<unsigned char, 6> wrong_size_key;
|
|
pk_hex = ::mamba::hex_string(wrong_size_key);
|
|
pk_bytes = ed25519_key_hex_to_bytes(pk_hex);
|
|
EXPECT_FALSE(pk_hex == ::mamba::hex_string(pk_bytes));
|
|
|
|
spdlog::set_level(spdlog::level::info);
|
|
}
|
|
|
|
TEST(Validate, ed25519_sig_hex_to_bytes)
|
|
{
|
|
std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES> pk, sk;
|
|
generate_ed25519_keypair(pk.data(), sk.data());
|
|
|
|
std::array<unsigned char, MAMBA_ED25519_SIGSIZE_BYTES> sig;
|
|
sign("Some text.", sk.data(), sig.data());
|
|
|
|
auto sig_hex = ::mamba::hex_string(sig);
|
|
auto sig_bytes = ed25519_sig_hex_to_bytes(sig_hex);
|
|
EXPECT_EQ(sig_hex, ::mamba::hex_string(sig_bytes));
|
|
|
|
spdlog::set_level(spdlog::level::debug);
|
|
|
|
std::array<unsigned char, 5> not_even_sig;
|
|
sig_hex = ::mamba::hex_string(not_even_sig);
|
|
sig_bytes = ed25519_sig_hex_to_bytes(sig_hex);
|
|
EXPECT_FALSE(sig_hex == ::mamba::hex_string(sig_bytes));
|
|
|
|
std::array<unsigned char, 6> wrong_size_sig;
|
|
sig_hex = ::mamba::hex_string(wrong_size_sig);
|
|
sig_bytes = ed25519_sig_hex_to_bytes(sig_hex);
|
|
EXPECT_FALSE(sig_hex == ::mamba::hex_string(sig_bytes));
|
|
|
|
spdlog::set_level(spdlog::level::info);
|
|
}
|
|
|
|
|
|
class VerifyMsg : public ::testing::Test
|
|
{
|
|
public:
|
|
VerifyMsg()
|
|
{
|
|
generate_ed25519_keypair(pk, sk);
|
|
sign("Some text.", sk, signature);
|
|
}
|
|
|
|
protected:
|
|
unsigned char pk[MAMBA_ED25519_KEYSIZE_BYTES];
|
|
unsigned char sk[MAMBA_ED25519_KEYSIZE_BYTES];
|
|
unsigned char signature[MAMBA_ED25519_SIGSIZE_BYTES];
|
|
};
|
|
|
|
TEST_F(VerifyMsg, from_bytes)
|
|
{
|
|
EXPECT_EQ(verify("Some text.", pk, signature), 1);
|
|
}
|
|
|
|
TEST_F(VerifyMsg, from_hex)
|
|
{
|
|
auto signature_hex = ::mamba::hex_string(signature, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
auto pk_hex = ::mamba::hex_string(pk, MAMBA_ED25519_KEYSIZE_BYTES);
|
|
|
|
EXPECT_EQ(verify("Some text.", pk_hex, signature_hex), 1);
|
|
}
|
|
|
|
TEST_F(VerifyMsg, wrong_signature)
|
|
{
|
|
spdlog::set_level(spdlog::level::debug);
|
|
auto pk_hex = ::mamba::hex_string(pk, MAMBA_ED25519_KEYSIZE_BYTES);
|
|
|
|
EXPECT_EQ(verify("Some text.", pk_hex, "signature_hex"), 0);
|
|
spdlog::set_level(spdlog::level::info);
|
|
}
|
|
|
|
TEST_F(VerifyMsg, wrong_public_key)
|
|
{
|
|
spdlog::set_level(spdlog::level::debug);
|
|
auto signature_hex = ::mamba::hex_string(signature, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
|
|
EXPECT_EQ(verify("Some text.", "pk_hex", signature_hex), 0);
|
|
spdlog::set_level(spdlog::level::info);
|
|
}
|
|
|
|
class VerifyGPGMsg : public ::testing::Test
|
|
{
|
|
public:
|
|
VerifyGPGMsg()
|
|
{
|
|
json j = R"({
|
|
"delegations": {
|
|
"key_mgr": {
|
|
"pubkeys": [
|
|
"013ddd714962866d12ba5bae273f14d48c89cf0773dee2dbf6d4561e521c83f7"
|
|
],
|
|
"threshold": 1
|
|
},
|
|
"root": {
|
|
"pubkeys": [
|
|
"2b920f88531576643ada0a632915d1dcdd377557647093f29cbe251ba8c33724"
|
|
],
|
|
"threshold": 1
|
|
}
|
|
},
|
|
"expiration": "2022-05-19T14:44:35Z",
|
|
"metadata_spec_version": "0.6.0",
|
|
"timestamp": "2021-05-19T14:44:35Z",
|
|
"type": "root",
|
|
"version": 1
|
|
})"_json;
|
|
data = j.dump(2);
|
|
}
|
|
|
|
protected:
|
|
std::string pk = "2b920f88531576643ada0a632915d1dcdd377557647093f29cbe251ba8c33724";
|
|
std::string signature
|
|
= "d891de3fc102a2ff7b96559ff2f4d81a8e25b5d51a44e10a9fbc5bdc3febf22120582f30e26f6dfe9450ca8100566af7cbc286bf7f52c700d074acd3d4a01603";
|
|
std::string trailer
|
|
= "04001608001d1621040673d781a8b80bcb7b002040ac7bc8bcf821360d050260a52453";
|
|
std::string hash = "5ad6a0995a537a5fc728ead2dda546972607c5ac235945f7c6c66f90eae1b326";
|
|
std::string data;
|
|
};
|
|
|
|
TEST_F(VerifyGPGMsg, verify_gpg_hashed_msg_from_bin)
|
|
{
|
|
auto bin_signature = ed25519_sig_hex_to_bytes(signature);
|
|
auto bin_pk = ed25519_key_hex_to_bytes(pk);
|
|
|
|
EXPECT_EQ(verify_gpg_hashed_msg(hash, bin_pk.data(), bin_signature.data()), 1);
|
|
}
|
|
|
|
TEST_F(VerifyGPGMsg, verify_gpg_hashed_msg_from_hex)
|
|
{
|
|
EXPECT_EQ(verify_gpg_hashed_msg(hash, pk, signature), 1);
|
|
}
|
|
|
|
TEST_F(VerifyGPGMsg, verify_gpg)
|
|
{
|
|
EXPECT_EQ(verify_gpg(data, trailer, pk, signature), 1);
|
|
}
|
|
} // namespace testing
|
|
|
|
namespace v06
|
|
{
|
|
namespace testing
|
|
{
|
|
using namespace mamba;
|
|
|
|
class RootImplT_v06 : public ::testing::Test
|
|
{
|
|
public:
|
|
using role_secrets_type
|
|
= std::map<std::string, std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES>>;
|
|
using secrets_type = std::map<std::string, role_secrets_type>;
|
|
|
|
RootImplT_v06()
|
|
{
|
|
channel_dir = std::make_unique<TemporaryDirectory>();
|
|
|
|
generate_secrets();
|
|
sign_root();
|
|
}
|
|
|
|
fs::u8path trusted_root_file(const json& j)
|
|
{
|
|
fs::u8path p = channel_dir->path() / "root.json";
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << j;
|
|
out_file.close();
|
|
|
|
return p;
|
|
}
|
|
|
|
fs::u8path trusted_root_file_raw_key()
|
|
{
|
|
return trusted_root_file(root1_json);
|
|
}
|
|
|
|
fs::u8path trusted_root_file_pgp()
|
|
{
|
|
return trusted_root_file(root1_pgp_json);
|
|
}
|
|
|
|
json create_root_update_json(const json& patch)
|
|
{
|
|
json new_root = root1_json;
|
|
|
|
if (!patch.empty())
|
|
new_root = new_root.patch(patch);
|
|
|
|
json sig_patch
|
|
= json::parse(R"([
|
|
{ "op": "replace", "path": "/signatures", "value":)"
|
|
+ sign_root_meta(new_root.at("signed")).dump() + R"( }
|
|
])");
|
|
return new_root.patch(sig_patch);
|
|
}
|
|
|
|
fs::u8path create_root_update(const fs::u8path& name, const json& patch = json())
|
|
{
|
|
fs::u8path p = channel_dir->path() / name;
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << create_root_update_json(patch);
|
|
out_file.close();
|
|
return p;
|
|
}
|
|
|
|
void generate_secrets(int root = 1, int key_mgr = 1, int pkg_mgr = 1)
|
|
{
|
|
secrets.insert({ "root", generate_role_secrets(root) });
|
|
secrets.insert({ "key_mgr", generate_role_secrets(key_mgr) });
|
|
secrets.insert({ "pkg_mgr", generate_role_secrets(pkg_mgr) });
|
|
}
|
|
|
|
void sign_root()
|
|
{
|
|
std::vector<std::string> mandatory_roles({ "root", "key_mgr" });
|
|
for (auto& r : mandatory_roles)
|
|
{
|
|
std::vector<std::string> role_public_keys;
|
|
for (auto& secret : secrets.at(r))
|
|
{
|
|
role_public_keys.push_back(secret.first);
|
|
}
|
|
root1_json["signed"]["delegations"][r]
|
|
= RolePubKeys({ role_public_keys, 1 });
|
|
}
|
|
|
|
root1_json["signed"]["version"] = 1;
|
|
root1_json["signed"]["metadata_spec_version"] = "0.6.0";
|
|
root1_json["signed"]["type"] = "root";
|
|
root1_json["signed"]["timestamp"] = timestamp(utc_time_now());
|
|
root1_json["signed"]["expiration"] = timestamp(utc_time_now() + 3600);
|
|
root1_json["signatures"] = sign_root_meta(root1_json["signed"]);
|
|
|
|
std::ifstream i(root1_pgp.std_path());
|
|
i >> root1_pgp_json;
|
|
}
|
|
|
|
json sign_root_meta(const json& root_meta)
|
|
{
|
|
std::map<std::string, std::map<std::string, std::string>> signatures;
|
|
|
|
unsigned char sig_bin[MAMBA_ED25519_SIGSIZE_BYTES];
|
|
|
|
for (auto& secret : secrets.at("root"))
|
|
{
|
|
sign(root_meta.dump(2), secret.second.data(), sig_bin);
|
|
|
|
auto sig_hex = ::mamba::hex_string(sig_bin, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
signatures[secret.first].insert({ "signature", sig_hex });
|
|
}
|
|
|
|
return signatures;
|
|
}
|
|
|
|
json upgrade_to_v1(const RootImpl& root, const json& patch = json())
|
|
{
|
|
auto root_meta = root.upgraded_signable();
|
|
if (!patch.empty())
|
|
root_meta = root_meta.patch(patch);
|
|
|
|
std::vector<RoleSignature> signatures;
|
|
for (auto& secret : secrets.at("root"))
|
|
{
|
|
signatures.push_back(
|
|
root.upgraded_signature(root_meta, secret.first, secret.second.data()));
|
|
}
|
|
|
|
json upgraded_root;
|
|
upgraded_root["signed"] = root_meta;
|
|
upgraded_root["signatures"] = signatures;
|
|
|
|
return upgraded_root;
|
|
}
|
|
|
|
protected:
|
|
fs::u8path root1_pgp = "validation_data/1.sv0.6.root.json";
|
|
json root1_json, root1_pgp_json;
|
|
|
|
secrets_type secrets;
|
|
|
|
std::unique_ptr<TemporaryDirectory> channel_dir;
|
|
|
|
std::map<std::string, std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES>>
|
|
generate_role_secrets(int count)
|
|
{
|
|
std::map<std::string, std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES>>
|
|
role_secrets;
|
|
|
|
unsigned char pk[MAMBA_ED25519_KEYSIZE_BYTES];
|
|
std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES> sk;
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
generate_ed25519_keypair(pk, sk.data());
|
|
auto pk_hex = ::mamba::hex_string(pk, MAMBA_ED25519_KEYSIZE_BYTES);
|
|
|
|
role_secrets.insert({ pk_hex, sk });
|
|
}
|
|
return role_secrets;
|
|
}
|
|
};
|
|
|
|
TEST_F(RootImplT_v06, ctor_from_path)
|
|
{
|
|
RootImpl root(trusted_root_file_raw_key());
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, ctor_from_path_pgp_signed)
|
|
{
|
|
RootImpl root(trusted_root_file_pgp());
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, ctor_from_json)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, ctor_from_json_str)
|
|
{
|
|
RootImpl root(root1_json.dump());
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, ctor_from_json_pgp_signed)
|
|
{
|
|
RootImpl root(root1_pgp_json);
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, ctor_wrong_filename_spec_version)
|
|
{
|
|
fs::u8path p = channel_dir->path() / "2.sv1.root.json";
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << root1_json;
|
|
out_file.close();
|
|
|
|
// "2.sv1.root.json" is not compatible spec version (spec version N)
|
|
EXPECT_THROW(RootImpl root(p), role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, update_from_path)
|
|
{
|
|
using namespace mamba;
|
|
|
|
auto f = trusted_root_file_raw_key();
|
|
RootImpl root(f);
|
|
|
|
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<RootImpl*>(updated_root.get());
|
|
EXPECT_EQ(testing_root->type(), "root");
|
|
EXPECT_EQ(testing_root->file_ext(), "json");
|
|
EXPECT_EQ(testing_root->spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(testing_root->version(), 2);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, wrong_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 3 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": "0.6.1" }
|
|
])"_json;
|
|
auto updated_root = root.update(create_root_update("2.root.json", patch));
|
|
|
|
auto testing_root = static_cast<RootImpl*>(updated_root.get());
|
|
EXPECT_EQ(testing_root->spec_version(), SpecImpl("0.6.1"));
|
|
EXPECT_EQ(testing_root->version(), 2);
|
|
EXPECT_EQ(testing_root->expires(), root.expires());
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, upgraded_spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": "1.0.0" }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
spec_version_error);
|
|
|
|
json signable_patch
|
|
= json::parse(R"([
|
|
{ "op": "replace", "path": "/version", "value": 2 },
|
|
{ "op": "replace", "path": "/expires", "value": ")"
|
|
+ timestamp(utc_time_now() + 1) /* force +1s */ + R"(" },
|
|
{ "op": "add", "path": "/keys/dummy_value", "value": { "keytype": "ed25519", "scheme": "ed25519", "keyval": "dummy_value" } },
|
|
{ "op": "add", "path": "/roles/snapshot/keyids", "value": ["dummy_value"] },
|
|
{ "op": "add", "path": "/roles/timestamp/keyids", "value": ["dummy_value"] }
|
|
])");
|
|
auto updated_root = root.update(upgrade_to_v1(root, signable_patch));
|
|
|
|
auto testing_root = static_cast<RootImpl*>(updated_root.get());
|
|
EXPECT_EQ(testing_root->spec_version(), SpecImpl("1.0.17"));
|
|
EXPECT_EQ(testing_root->version(), 2);
|
|
EXPECT_LT(testing_root->expires(), root.expires());
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, equivalent_upgraded_spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json signable_patch = R"([
|
|
{ "op": "add", "path": "/keys/dummy_value", "value": { "keytype": "ed25519", "scheme": "ed25519", "keyval": "dummy_value" } },
|
|
{ "op": "add", "path": "/roles/snapshot/keyids", "value": ["dummy_value"] },
|
|
{ "op": "add", "path": "/roles/timestamp/keyids", "value": ["dummy_value"] }
|
|
])"_json;
|
|
v1::RootImpl updated_root(upgrade_to_v1(root, signable_patch));
|
|
|
|
EXPECT_EQ(updated_root.spec_version(), v1::SpecImpl("1.0.17"));
|
|
EXPECT_EQ(updated_root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, wrong_spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": "1.0.0" }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
spec_version_error);
|
|
|
|
patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": "wrong" }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
spec_version_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, wrong_filename_role)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.rooot.json")), role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, wrong_filename_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
EXPECT_THROW(root.update(create_root_update("3.root.json")), role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, wrong_filename_spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
// "2.sv1.root.json" is upgradable spec version (spec version N+1)
|
|
json signable_patch = R"([
|
|
{ "op": "replace", "path": "/version", "value": 2 },
|
|
{ "op": "replace", "path": "/spec_version", "value": "1.0.0" },
|
|
{ "op": "add", "path": "/keys/dummy_value", "value": { "keytype": "ed25519", "scheme": "ed25519", "keyval": "dummy_value" } },
|
|
{ "op": "add", "path": "/roles/snapshot/keyids", "value": ["dummy_value"] },
|
|
{ "op": "add", "path": "/roles/timestamp/keyids", "value": ["dummy_value"] }
|
|
])"_json;
|
|
auto updated_root = root.update(upgrade_to_v1(root, signable_patch));
|
|
auto testing_root = static_cast<RootImpl*>(updated_root.get());
|
|
EXPECT_EQ(testing_root->spec_version(), SpecImpl("1.0.0"));
|
|
|
|
// "2.sv2.root.json" is not upgradable spec version (spec version N+1)
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
EXPECT_THROW(root.update(create_root_update("2.sv2.root.json", patch)),
|
|
role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, illformed_filename_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
EXPECT_THROW(root.update(create_root_update("wrong.root.json")), role_file_error);
|
|
EXPECT_THROW(root.update(create_root_update("2..root.json")), role_file_error);
|
|
EXPECT_THROW(root.update(create_root_update("2.sv04.root.json")), role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, rollback_attack)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 1 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)), rollback_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, wrong_type)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/type", "value": "timestamp" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, missing_type)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/type" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, missing_delegations)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/delegations" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, missing_delegation)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/delegations/root" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, empty_delegation_pubkeys)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/delegations/root/pubkeys", "value": [] },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, null_role_threshold)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/delegations/root/threshold", "value": 0 },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, extra_roles)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "add", "path": "/signed/delegations/some_wrong_role",
|
|
"value": { "pubkeys": ["c"], "threshold": 1 } },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
/*
|
|
TEST_F(RootImplT_v06, mirrors_role)
|
|
{
|
|
json patch = R"([
|
|
{ "op": "add", "path":
|
|
"/signed/roles/mirrors", "value": { "keyids":
|
|
["c"], "threshold": 1 } }, { "op": "replace", "path":
|
|
"/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
RootImpl root(create_root_update("2.root.json", patch));
|
|
bool mirrors_role_found = (root.roles().find("mirrors") != root.roles().end());
|
|
EXPECT_TRUE(mirrors_role_found);
|
|
}
|
|
*/
|
|
TEST_F(RootImplT_v06, threshold_not_met)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/delegations/root/threshold", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)), role_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, expires)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
// expiration is set to now+3600s in 'sign_root'
|
|
TimeRef::instance().set(utc_time_now());
|
|
EXPECT_FALSE(root.expired());
|
|
|
|
TimeRef::instance().set(utc_time_now() + 7200);
|
|
EXPECT_TRUE(root.expired());
|
|
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expiration", "value": ")"
|
|
+ timestamp(utc_time_now() + 10800) + R"(" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
auto updated_root = root.update(create_root_update("2.root.json", patch));
|
|
|
|
auto testing_root = static_cast<RootImpl*>(updated_root.get());
|
|
EXPECT_FALSE(testing_root->expired());
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, timestamp)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch;
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/timestamp", "value": "2021-09-20T07:07:09+0030" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/timestamp", "value": "2021-09-20T07:07:09D" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/timestamp", "value": "2021-09-20T07:07:09.000" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v06, possible_update_files)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
auto update_f = root.possible_update_files();
|
|
EXPECT_THAT(update_f,
|
|
::testing::ElementsAre("2.sv1.root.json",
|
|
"2.sv0.7.root.json",
|
|
"2.sv0.6.root.json",
|
|
"2.root.json"));
|
|
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
auto updated_root = root.update(create_root_update("2.root.json", patch));
|
|
update_f = updated_root->possible_update_files();
|
|
EXPECT_THAT(update_f,
|
|
::testing::ElementsAre("3.sv1.root.json",
|
|
"3.sv0.7.root.json",
|
|
"3.sv0.6.root.json",
|
|
"3.root.json"));
|
|
}
|
|
|
|
|
|
class SpecImplT_v06 : public ::testing::Test
|
|
{
|
|
public:
|
|
SpecImplT_v06() = default;
|
|
|
|
protected:
|
|
SpecImpl spec;
|
|
};
|
|
|
|
TEST_F(SpecImplT_v06, ctor)
|
|
{
|
|
SpecImpl new_spec("0.6.1");
|
|
EXPECT_EQ(new_spec.version_str(), "0.6.1");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, version_str)
|
|
{
|
|
EXPECT_EQ(spec.version_str(), "0.6.0");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, is_compatible)
|
|
{
|
|
EXPECT_TRUE(spec.is_compatible(std::string("0.6.0")));
|
|
EXPECT_TRUE(spec.is_compatible(std::string("0.6.1")));
|
|
EXPECT_TRUE(spec.is_compatible(std::string("0.6.10")));
|
|
|
|
// minor version change with major version '0' may be
|
|
// backward incompatible
|
|
EXPECT_FALSE(spec.is_compatible(std::string("0.7.0")));
|
|
EXPECT_FALSE(spec.is_compatible(std::string("1.0.0")));
|
|
EXPECT_FALSE(spec.is_compatible(std::string("2.0.0")));
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, is_upgrade)
|
|
{
|
|
EXPECT_TRUE(spec.is_upgrade(std::string("0.7.0")));
|
|
EXPECT_TRUE(spec.is_upgrade(std::string("1.0.0")));
|
|
EXPECT_TRUE(spec.is_upgrade(std::string("1.1.0")));
|
|
EXPECT_TRUE(spec.is_upgrade(std::string("1.0.17")));
|
|
|
|
// 2 possible backward incompatible updates
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("0.8.0")));
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("2.0.0")));
|
|
// not an upgrade, compatible version
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("0.6.1")));
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, upgradable)
|
|
{
|
|
EXPECT_TRUE(spec.upgradable());
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, compatible_prefix)
|
|
{
|
|
EXPECT_EQ(spec.compatible_prefix(), "0.6");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, upgrade_prefix)
|
|
{
|
|
EXPECT_THAT(spec.upgrade_prefix(), ::testing::ElementsAre("1", "0.7"));
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, json_key)
|
|
{
|
|
EXPECT_EQ(spec.json_key(), "metadata_spec_version");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, expiration_json_key)
|
|
{
|
|
EXPECT_EQ(spec.expiration_json_key(), "expiration");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, canonicalize)
|
|
{
|
|
EXPECT_EQ(spec.canonicalize(R"({"foo":"bar"})"_json), "{\n \"foo\": \"bar\"\n}");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v06, signatures)
|
|
{
|
|
json j = R"({
|
|
"signatures":
|
|
{
|
|
"foo":
|
|
{
|
|
"other_headers": "bar",
|
|
"signature": "baz"
|
|
}
|
|
}
|
|
})"_json;
|
|
auto sigs = spec.signatures(j);
|
|
EXPECT_EQ(sigs.size(), 1);
|
|
EXPECT_EQ(sigs.begin()->keyid, "foo");
|
|
EXPECT_EQ(sigs.begin()->sig, "baz");
|
|
EXPECT_EQ(sigs.begin()->pgp_trailer, "bar");
|
|
}
|
|
|
|
|
|
class KeyMgrT_v06 : public RootImplT_v06
|
|
{
|
|
public:
|
|
KeyMgrT_v06()
|
|
: RootImplT_v06()
|
|
{
|
|
sign_key_mgr();
|
|
}
|
|
|
|
void sign_key_mgr()
|
|
{
|
|
std::vector<std::string> pkg_mgr_pks;
|
|
for (auto& secret : secrets.at("pkg_mgr"))
|
|
{
|
|
pkg_mgr_pks.push_back(secret.first);
|
|
}
|
|
key_mgr_json["signed"]["delegations"]["pkg_mgr"]
|
|
= RolePubKeys({ pkg_mgr_pks, 1 });
|
|
|
|
key_mgr_json["signed"]["version"] = 1;
|
|
key_mgr_json["signed"]["metadata_spec_version"] = "0.6.0";
|
|
key_mgr_json["signed"]["type"] = "key_mgr";
|
|
|
|
key_mgr_json["signed"]["timestamp"] = timestamp(utc_time_now());
|
|
key_mgr_json["signed"]["expiration"] = timestamp(utc_time_now() + 3600);
|
|
key_mgr_json["signatures"] = sign_key_mgr_meta(key_mgr_json["signed"]);
|
|
}
|
|
|
|
json patched_key_mgr_json(const json& patch = json())
|
|
{
|
|
json update_key_mgr = key_mgr_json;
|
|
|
|
if (!patch.empty())
|
|
update_key_mgr = update_key_mgr.patch(patch);
|
|
|
|
json sig_patch = json::parse(
|
|
R"([
|
|
{ "op": "replace", "path": "/signatures", "value": )"
|
|
+ sign_key_mgr_meta(update_key_mgr.at("signed")).dump() + R"( }
|
|
])");
|
|
return update_key_mgr.patch(sig_patch);
|
|
}
|
|
|
|
fs::u8path write_key_mgr_file(const json& j,
|
|
const std::string& filename = "key_mgr.json")
|
|
{
|
|
fs::u8path p = channel_dir->path() / filename;
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << j;
|
|
out_file.close();
|
|
|
|
return p;
|
|
}
|
|
|
|
protected:
|
|
json key_mgr_json;
|
|
|
|
json sign_key_mgr_meta(const json& meta)
|
|
{
|
|
std::map<std::string, std::map<std::string, std::string>> signatures;
|
|
|
|
unsigned char sig_bin[MAMBA_ED25519_SIGSIZE_BYTES];
|
|
|
|
for (auto& secret : secrets.at("key_mgr"))
|
|
{
|
|
sign(meta.dump(2), secret.second.data(), sig_bin);
|
|
|
|
auto sig_hex = ::mamba::hex_string(sig_bin, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
signatures[secret.first].insert({ "signature", sig_hex });
|
|
}
|
|
|
|
return signatures;
|
|
}
|
|
};
|
|
|
|
TEST_F(KeyMgrT_v06, ctor_from_json)
|
|
{
|
|
RootImpl root(root1_json);
|
|
auto key_mgr = root.create_key_mgr(key_mgr_json);
|
|
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(key_mgr.version(), 1);
|
|
}
|
|
|
|
TEST_F(KeyMgrT_v06, ctor_from_json_str)
|
|
{
|
|
RootImpl root(root1_json);
|
|
auto key_mgr = KeyMgrRole(key_mgr_json.dump(),
|
|
root.all_keys()["key_mgr"],
|
|
std::make_shared<SpecImpl>(SpecImpl()));
|
|
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(key_mgr.version(), 1);
|
|
}
|
|
|
|
TEST_F(KeyMgrT_v06, version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
{
|
|
json key_mgr_patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
auto key_mgr = root.create_key_mgr(patched_key_mgr_json(key_mgr_patch));
|
|
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(key_mgr.version(), 2);
|
|
}
|
|
|
|
{ // Any version is valid, without chaining required
|
|
json key_mgr_patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 20 }
|
|
])"_json;
|
|
auto key_mgr = root.create_key_mgr(patched_key_mgr_json(key_mgr_patch));
|
|
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(key_mgr.version(), 20);
|
|
}
|
|
}
|
|
|
|
TEST_F(KeyMgrT_v06, spec_version)
|
|
{ // spec version as to match exactly 'root' spec version
|
|
RootImpl root(root1_json);
|
|
|
|
{
|
|
json key_mgr_patch = R"([
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": "0.6.0" }
|
|
])"_json;
|
|
auto key_mgr = root.create_key_mgr(patched_key_mgr_json(key_mgr_patch));
|
|
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(key_mgr.version(), 1);
|
|
}
|
|
|
|
{ // is compatible but not strictly the same as 'root' one
|
|
json key_mgr_patch = R"([
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": "0.6.1" }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.create_key_mgr(patched_key_mgr_json(key_mgr_patch)),
|
|
spec_version_error);
|
|
}
|
|
|
|
{ // wrong type
|
|
json key_mgr_patch = R"([
|
|
{ "op": "replace", "path": "/signed/metadata_spec_version", "value": 0.6 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.create_key_mgr(patched_key_mgr_json(key_mgr_patch)),
|
|
role_metadata_error);
|
|
}
|
|
}
|
|
|
|
TEST_F(KeyMgrT_v06, ctor_from_path)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
auto key_mgr = root.create_key_mgr(write_key_mgr_file(key_mgr_json));
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_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"));
|
|
EXPECT_EQ(key_mgr.spec_version(), SpecImpl("0.6.0"));
|
|
EXPECT_EQ(key_mgr.version(), 1);
|
|
|
|
|
|
EXPECT_THROW(root.create_key_mgr(fs::u8path("not_existing")), role_file_error);
|
|
|
|
EXPECT_THROW(root.create_key_mgr(write_key_mgr_file(key_mgr_json, "wrong.json")),
|
|
role_file_error);
|
|
|
|
EXPECT_THROW(
|
|
root.create_key_mgr(write_key_mgr_file(key_mgr_json, "sv1.key_mgr.json")),
|
|
role_file_error);
|
|
|
|
EXPECT_THROW(root.create_key_mgr(
|
|
write_key_mgr_file(key_mgr_json, "wrong.sv0.6.key_mgr.json")),
|
|
role_file_error);
|
|
}
|
|
|
|
TEST_F(KeyMgrT_v06, expires)
|
|
{
|
|
RootImpl root(root1_json);
|
|
auto key_mgr = root.create_key_mgr(key_mgr_json);
|
|
|
|
// expiration is set to now+3600s in 'sign_key_mgr'
|
|
TimeRef::instance().set(utc_time_now());
|
|
EXPECT_FALSE(key_mgr.expired());
|
|
EXPECT_FALSE(root.expired());
|
|
|
|
TimeRef::instance().set(utc_time_now() + 7200);
|
|
EXPECT_TRUE(key_mgr.expired());
|
|
EXPECT_TRUE(root.expired());
|
|
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expiration", "value": ")"
|
|
+ timestamp(utc_time_now() + 10800) + R"(" }
|
|
])");
|
|
|
|
key_mgr = root.create_key_mgr(patched_key_mgr_json(patch));
|
|
EXPECT_FALSE(key_mgr.expired());
|
|
EXPECT_TRUE(root.expired());
|
|
}
|
|
|
|
TEST_F(KeyMgrT_v06, timestamp)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch;
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/timestamp", "value": "2021-09-20T07:07:09+0030" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 1 }
|
|
])");
|
|
|
|
EXPECT_THROW(root.create_key_mgr(patched_key_mgr_json(patch)), role_metadata_error);
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/timestamp", "value": "2021-09-20T07:07:09D" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 1 }
|
|
])");
|
|
EXPECT_THROW(root.create_key_mgr(patched_key_mgr_json(patch)), role_metadata_error);
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/timestamp", "value": "2021-09-20T07:07:09.000" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 1 }
|
|
])");
|
|
EXPECT_THROW(root.create_key_mgr(patched_key_mgr_json(patch)), role_metadata_error);
|
|
}
|
|
|
|
class PkgMgrT_v06 : public KeyMgrT_v06
|
|
{
|
|
public:
|
|
PkgMgrT_v06()
|
|
: KeyMgrT_v06()
|
|
{
|
|
sign_pkg_mgr();
|
|
generate_index_checkerdata();
|
|
root = std::make_unique<RootImpl>(root1_json);
|
|
};
|
|
|
|
json sign_repodata(const json& patch = json())
|
|
{
|
|
json updated_repodata = repodata_json;
|
|
|
|
if (!patch.empty())
|
|
updated_repodata = updated_repodata.patch(patch);
|
|
|
|
for (auto& it : updated_repodata.at("packages").get<json::object_t>())
|
|
{
|
|
json sig_patch = json::parse(
|
|
R"({
|
|
"signatures": { ")"
|
|
+ it.first + "\":" + sign_repodata_meta(it.second).dump() + R"(
|
|
}
|
|
})");
|
|
updated_repodata.merge_patch(sig_patch);
|
|
}
|
|
return updated_repodata;
|
|
}
|
|
|
|
void sign_pkg_mgr()
|
|
{
|
|
std::vector<std::string> pkg_mgr_pks;
|
|
for (auto& secret : secrets.at("pkg_mgr"))
|
|
{
|
|
pkg_mgr_pks.push_back(secret.first);
|
|
}
|
|
pkg_mgr_json["signed"]["delegations"] = json::object();
|
|
|
|
pkg_mgr_json["signed"]["version"] = 1;
|
|
pkg_mgr_json["signed"]["metadata_spec_version"] = "0.6.0";
|
|
pkg_mgr_json["signed"]["type"] = "pkg_mgr";
|
|
|
|
pkg_mgr_json["signed"]["timestamp"] = timestamp(utc_time_now());
|
|
pkg_mgr_json["signed"]["expiration"] = timestamp(utc_time_now() + 3600);
|
|
pkg_mgr_json["signatures"] = sign_pkg_mgr_meta(pkg_mgr_json["signed"]);
|
|
}
|
|
|
|
json patched_pkg_mgr_json(const json& patch = json())
|
|
{
|
|
json update_pkg_mgr = pkg_mgr_json;
|
|
|
|
if (!patch.empty())
|
|
update_pkg_mgr = update_pkg_mgr.patch(patch);
|
|
|
|
json sig_patch = json::parse(
|
|
R"([
|
|
{ "op": "replace", "path": "/signatures", "value": )"
|
|
+ sign_pkg_mgr_meta(update_pkg_mgr.at("signed")).dump() + R"( }
|
|
])");
|
|
return update_pkg_mgr.patch(sig_patch);
|
|
}
|
|
|
|
fs::u8path write_pkg_mgr_file(const json& j,
|
|
const std::string& filename = "pkg_mgr.json")
|
|
{
|
|
fs::u8path p = channel_dir->path() / filename;
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << j;
|
|
out_file.close();
|
|
|
|
return p;
|
|
}
|
|
|
|
protected:
|
|
json pkg_mgr_json, repodata_json, signed_repodata_json;
|
|
|
|
std::unique_ptr<RootImpl> root;
|
|
|
|
json sign_pkg_mgr_meta(const json& meta)
|
|
{
|
|
std::map<std::string, std::map<std::string, std::string>> signatures;
|
|
|
|
unsigned char sig_bin[MAMBA_ED25519_SIGSIZE_BYTES];
|
|
|
|
for (auto& secret : secrets.at("pkg_mgr"))
|
|
{
|
|
sign(meta.dump(2), secret.second.data(), sig_bin);
|
|
|
|
auto sig_hex = ::mamba::hex_string(sig_bin, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
signatures[secret.first].insert({ "signature", sig_hex });
|
|
}
|
|
|
|
return signatures;
|
|
}
|
|
|
|
void generate_index_checkerdata()
|
|
{
|
|
repodata_json = R"({
|
|
"info": {
|
|
"subdir": "noarch"
|
|
},
|
|
"packages": {
|
|
"test-package1-0.1-0.tar.bz2": {
|
|
"build": "0",
|
|
"build_number": 0,
|
|
"depends": [],
|
|
"license": "BSD",
|
|
"license_family": "BSD",
|
|
"md5": "2a8595f37faa2950e1b433acbe91d481",
|
|
"name": "test-package",
|
|
"noarch": "generic",
|
|
"sha256": "b908ffce2d26d94c58c968abf286568d4bcf87d1cfe6c994958351724a6f6988",
|
|
"size": 5719,
|
|
"subdir": "noarch",
|
|
"timestamp": 1613117294885,
|
|
"version": "0.1"
|
|
},
|
|
"test-package2-0.1-0.tar.bz2": {
|
|
"build": "0"
|
|
}
|
|
}
|
|
})"_json;
|
|
|
|
signed_repodata_json = sign_repodata();
|
|
}
|
|
|
|
json sign_repodata_meta(const json& meta)
|
|
{
|
|
std::map<std::string, std::map<std::string, std::string>> signatures;
|
|
|
|
unsigned char sig_bin[MAMBA_ED25519_SIGSIZE_BYTES];
|
|
|
|
for (auto& secret : secrets.at("pkg_mgr"))
|
|
{
|
|
sign(meta.dump(2), secret.second.data(), sig_bin);
|
|
|
|
auto sig_hex = ::mamba::hex_string(sig_bin, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
signatures[secret.first].insert({ "signature", sig_hex });
|
|
}
|
|
|
|
return signatures;
|
|
}
|
|
};
|
|
|
|
TEST_F(PkgMgrT_v06, verify_index)
|
|
{
|
|
auto key_mgr = root->create_key_mgr(key_mgr_json);
|
|
auto pkg_mgr = key_mgr.create_pkg_mgr(pkg_mgr_json);
|
|
|
|
pkg_mgr.verify_index(signed_repodata_json);
|
|
}
|
|
|
|
TEST_F(PkgMgrT_v06, corrupted_repodata)
|
|
{
|
|
auto key_mgr = root->create_key_mgr(key_mgr_json);
|
|
auto pkg_mgr = key_mgr.create_pkg_mgr(pkg_mgr_json);
|
|
|
|
json wrong_pkg_patch = R"([
|
|
{ "op": "replace", "path": "/packages/test-package1-0.1-0.tar.bz2/version", "value": "0.1.1" }
|
|
])"_json;
|
|
EXPECT_THROW(pkg_mgr.verify_index(signed_repodata_json.patch(wrong_pkg_patch)),
|
|
package_error);
|
|
}
|
|
|
|
TEST_F(PkgMgrT_v06, illformed_repodata)
|
|
{
|
|
auto key_mgr = root->create_key_mgr(key_mgr_json);
|
|
auto pkg_mgr = key_mgr.create_pkg_mgr(pkg_mgr_json);
|
|
|
|
json illformed_pkg_patch = R"([
|
|
{ "op": "remove", "path": "/signatures"}
|
|
])"_json;
|
|
EXPECT_THROW(pkg_mgr.verify_index(signed_repodata_json.patch(illformed_pkg_patch)),
|
|
index_error);
|
|
}
|
|
|
|
|
|
class RepoCheckerT : public PkgMgrT_v06
|
|
{
|
|
public:
|
|
RepoCheckerT()
|
|
: PkgMgrT_v06()
|
|
{
|
|
m_repo_base_url = "file://" + channel_dir->path().string();
|
|
m_ref_path = channel_dir->path().string();
|
|
|
|
write_role(root1_json, channel_dir->path() / "root.json");
|
|
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
write_role(create_root_update_json(patch), channel_dir->path() / "2.root.json");
|
|
|
|
write_role(key_mgr_json, channel_dir->path() / "key_mgr.json");
|
|
write_role(pkg_mgr_json, channel_dir->path() / "pkg_mgr.json");
|
|
|
|
spdlog::set_level(spdlog::level::debug);
|
|
}
|
|
|
|
~RepoCheckerT()
|
|
{
|
|
spdlog::set_level(spdlog::level::warn);
|
|
}
|
|
|
|
protected:
|
|
std::string m_ref_path, m_repo_base_url;
|
|
|
|
void write_role(const json& j, const fs::u8path& p)
|
|
{
|
|
fs::u8path expanded_p = env::expand_user(p);
|
|
path::touch(expanded_p, true);
|
|
std::ofstream out_file(expanded_p.std_path(),
|
|
std::ofstream::out | std::ofstream::trunc);
|
|
out_file << j.dump(2);
|
|
out_file.close();
|
|
}
|
|
};
|
|
|
|
TEST_F(RepoCheckerT, ctor)
|
|
{
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
checker.generate_index_checker();
|
|
EXPECT_EQ(checker.root_version(), 2);
|
|
}
|
|
|
|
TEST_F(RepoCheckerT, verify_index)
|
|
{
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
checker.generate_index_checker();
|
|
checker.verify_index(signed_repodata_json);
|
|
}
|
|
|
|
TEST_F(RepoCheckerT, root_freeze_attack)
|
|
{
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/expiration", "value": ")"
|
|
+ timestamp(utc_time_now() - 10) + R"(" }
|
|
])");
|
|
write_role(create_root_update_json(patch), channel_dir->path() / "2.root.json");
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
EXPECT_THROW(checker.generate_index_checker(), freeze_error);
|
|
}
|
|
|
|
TEST_F(RepoCheckerT, key_mgr_freeze_attack)
|
|
{
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expiration", "value": ")"
|
|
+ timestamp(utc_time_now() - 10) + R"(" }
|
|
])");
|
|
write_role(patched_key_mgr_json(patch), channel_dir->path() / "key_mgr.json");
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
EXPECT_THROW(checker.generate_index_checker(), freeze_error);
|
|
}
|
|
|
|
TEST_F(RepoCheckerT, missing_key_mgr_file)
|
|
{
|
|
fs::remove(channel_dir->path() / "key_mgr.json");
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
EXPECT_THROW(checker.generate_index_checker(), fetching_error);
|
|
}
|
|
|
|
TEST_F(RepoCheckerT, corrupted_repodata)
|
|
{
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
|
|
json wrong_pkg_patch = R"([
|
|
{ "op": "replace", "path": "/packages/test-package1-0.1-0.tar.bz2/version", "value": "0.1.1" }
|
|
])"_json;
|
|
checker.generate_index_checker();
|
|
EXPECT_THROW(checker.verify_index(signed_repodata_json.patch(wrong_pkg_patch)),
|
|
package_error);
|
|
}
|
|
|
|
TEST_F(RepoCheckerT, illformed_repodata)
|
|
{
|
|
RepoChecker checker(m_repo_base_url, m_ref_path);
|
|
|
|
json illformed_pkg_patch = R"([
|
|
{ "op": "remove", "path": "/signatures"}
|
|
])"_json;
|
|
checker.generate_index_checker();
|
|
EXPECT_THROW(checker.verify_index(signed_repodata_json.patch(illformed_pkg_patch)),
|
|
index_error);
|
|
}
|
|
} // namespace testing
|
|
} // namespace v06
|
|
|
|
namespace v1
|
|
{
|
|
namespace testing
|
|
{
|
|
using namespace mamba;
|
|
|
|
class RootImplT_v1 : public ::testing::Test
|
|
{
|
|
public:
|
|
using role_secrets_type
|
|
= std::map<std::string, std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES>>;
|
|
using secrets_type = std::map<std::string, role_secrets_type>;
|
|
|
|
RootImplT_v1()
|
|
{
|
|
channel_dir = std::make_unique<TemporaryDirectory>();
|
|
|
|
generate_secrets();
|
|
sign_root();
|
|
}
|
|
|
|
fs::u8path trusted_root_file()
|
|
{
|
|
fs::u8path p = channel_dir->path() / "root.json";
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << root1_json;
|
|
out_file.close();
|
|
|
|
return p;
|
|
}
|
|
|
|
fs::u8path create_root_update(const fs::u8path& name, const json& patch = json())
|
|
{
|
|
fs::u8path p = channel_dir->path() / name;
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
|
|
json new_root = root1_json;
|
|
|
|
if (!patch.empty())
|
|
new_root = new_root.patch(patch);
|
|
|
|
json sig_patch
|
|
= json::parse(R"([
|
|
{ "op": "replace", "path": "/signatures", "value":)"
|
|
+ sign_root_meta(new_root.at("signed")).dump() + R"(}
|
|
])");
|
|
out_file << new_root.patch(sig_patch);
|
|
out_file.close();
|
|
|
|
return p;
|
|
}
|
|
|
|
void generate_secrets(int root = 1,
|
|
int targets = 1,
|
|
int snapshot = 1,
|
|
int timestamp = 1)
|
|
{
|
|
secrets.insert({ "root", generate_role_secrets(root) });
|
|
secrets.insert({ "targets", generate_role_secrets(targets) });
|
|
secrets.insert({ "snapshot", generate_role_secrets(snapshot) });
|
|
secrets.insert({ "timestamp", generate_role_secrets(timestamp) });
|
|
}
|
|
|
|
void sign_root()
|
|
{
|
|
std::ifstream i(root1.std_path());
|
|
i >> root1_json;
|
|
|
|
std::map<std::string, RoleKeys> all_roles;
|
|
std::map<std::string, Key> all_keys;
|
|
|
|
for (auto& it : secrets)
|
|
{
|
|
auto& r = it.first;
|
|
std::vector<std::string> r_keys;
|
|
for (auto& s : it.second)
|
|
{
|
|
r_keys.push_back(s.first);
|
|
all_keys.insert({ s.first, Key::from_ed25519(s.first) });
|
|
}
|
|
all_roles[r] = { r_keys, 1 };
|
|
}
|
|
root1_json.at("signed").at("roles") = all_roles;
|
|
root1_json.at("signed").at("keys") = all_keys;
|
|
root1_json.at("signed")["expires"] = timestamp(utc_time_now() + 3600);
|
|
|
|
root1_json["signatures"] = sign_root_meta(root1_json["signed"]);
|
|
}
|
|
|
|
json sign_root_meta(const json& root_meta)
|
|
{
|
|
std::vector<RoleSignature> signatures;
|
|
unsigned char sig_bin[MAMBA_ED25519_SIGSIZE_BYTES];
|
|
|
|
for (auto& secret : secrets.at("root"))
|
|
{
|
|
sign(root_meta.dump(), secret.second.data(), sig_bin);
|
|
|
|
auto sig_hex = ::mamba::hex_string(sig_bin, MAMBA_ED25519_SIGSIZE_BYTES);
|
|
signatures.push_back({ secret.first, sig_hex });
|
|
}
|
|
|
|
return signatures;
|
|
}
|
|
|
|
protected:
|
|
fs::u8path root1 = "validation_data/root.json";
|
|
json root1_json;
|
|
|
|
std::unique_ptr<TemporaryDirectory> channel_dir;
|
|
|
|
secrets_type secrets;
|
|
|
|
std::map<std::string, std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES>>
|
|
generate_role_secrets(int count)
|
|
{
|
|
std::map<std::string, std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES>>
|
|
role_secrets;
|
|
|
|
unsigned char pk[MAMBA_ED25519_KEYSIZE_BYTES];
|
|
std::array<unsigned char, MAMBA_ED25519_KEYSIZE_BYTES> sk;
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
generate_ed25519_keypair(pk, sk.data());
|
|
|
|
auto pk_hex = ::mamba::hex_string(pk, MAMBA_ED25519_KEYSIZE_BYTES);
|
|
role_secrets.insert({ pk_hex, sk });
|
|
}
|
|
return role_secrets;
|
|
}
|
|
};
|
|
|
|
TEST_F(RootImplT_v1, ctor_from_path)
|
|
{
|
|
RootImpl root(trusted_root_file());
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("1.0.17"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, ctor_from_json)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
EXPECT_EQ(root.type(), "root");
|
|
EXPECT_EQ(root.file_ext(), "json");
|
|
EXPECT_EQ(root.spec_version(), SpecImpl("1.0.17"));
|
|
EXPECT_EQ(root.version(), 1);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, update_from_path)
|
|
{
|
|
using namespace mamba;
|
|
|
|
RootImpl root(trusted_root_file());
|
|
|
|
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<RootImpl*>(updated_root.get());
|
|
EXPECT_EQ(testing_root->type(), "root");
|
|
EXPECT_EQ(testing_root->file_ext(), "json");
|
|
EXPECT_EQ(testing_root->spec_version(), SpecImpl("1.0.17"));
|
|
EXPECT_EQ(testing_root->version(), 2);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, ctor_wrong_filename_spec_version)
|
|
{
|
|
fs::u8path p = channel_dir->path() / "2.sv0.6.root.json";
|
|
|
|
std::ofstream out_file(p.std_path(), std::ofstream::out | std::ofstream::trunc);
|
|
out_file << root1_json;
|
|
out_file.close();
|
|
|
|
// "2.sv0.6.root.json" is not compatible spec version (spec version N)
|
|
EXPECT_THROW(RootImpl root(p), role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, wrong_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 3 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/spec_version", "value": "1.30.10" }
|
|
])"_json;
|
|
|
|
auto updated_root = root.update(create_root_update("2.root.json", patch));
|
|
|
|
auto testing_root = static_cast<RootImpl*>(updated_root.get());
|
|
EXPECT_EQ(testing_root->spec_version(), SpecImpl("1.30.10"));
|
|
EXPECT_EQ(testing_root->version(), 2);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, wrong_spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/spec_version", "value": "2.0.0" }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
spec_version_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, wrong_filename_role)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.rooot.json", patch)),
|
|
role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, wrong_filename_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("3.root.json", patch)),
|
|
role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, wrong_filename_spec_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
// "2.sv2.root.json" is upgradable spec version (spec version N+1)
|
|
// but v2 is NOT implemented yet, so v1::RootImpl is not upgradable
|
|
EXPECT_THROW(root.update(create_root_update("2.sv2.root.json")),
|
|
spec_version_error);
|
|
// "2.sv3.root.json" is NOT upgradable spec version (spec version N+1)
|
|
EXPECT_THROW(root.update(create_root_update("2.sv3.root.json")), role_file_error);
|
|
EXPECT_THROW(root.update(create_root_update("2.sv0.6.root.json")), role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, illformed_filename_version)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("wrong.root.json", patch)),
|
|
role_file_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, rollback_attack)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 1 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)), rollback_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, wrong_type)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/_type", "value": "timestamp" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, missing_type)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/_type" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, missing_keys)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/keys" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, missing_roles)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/roles" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, missing_role)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "remove", "path": "/signed/roles/timestamp" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, empty_role_keyids)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/roles/snapshot/keyids", "value": [] },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, null_role_threshold)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/roles/snapshot/threshold", "value": 0 },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, extra_roles)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "add", "path": "/signed/roles/some_wrong_role", "value": { "keyids": ["c"], "threshold": 1 } },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, key_not_found)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "add", "path": "/signed/roles/snapshot/keyids/-", "value": "c" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, mirrors_role)
|
|
{
|
|
json patch = R"([
|
|
{ "op": "add", "path": "/signed/roles/mirrors", "value": { "keyids": ["c"], "threshold": 1 } },
|
|
{ "op": "add", "path": "/signed/keys/c", "value": { "scheme": "ed25519", "keytype": "ed25519", "keyval": "c"} },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])"_json;
|
|
|
|
const RootImpl root(create_root_update("2.root.json", patch));
|
|
bool mirrors_role_found = contains(root.roles(), "mirrors");
|
|
EXPECT_TRUE(mirrors_role_found);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, threshold_not_met)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
json patch = R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 },
|
|
{ "op": "replace", "path": "/signed/roles/root/threshold", "value": 2 }
|
|
])"_json;
|
|
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)), role_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, expires)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
// expiration is set to now+3600s in 'sign_root'
|
|
TimeRef::instance().set(utc_time_now());
|
|
EXPECT_FALSE(root.expired());
|
|
|
|
TimeRef::instance().set(utc_time_now() + 7200);
|
|
EXPECT_TRUE(root.expired());
|
|
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expires", "value": ")"
|
|
+ timestamp(utc_time_now() + 10800) + R"(" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
auto updated_root = root.update(create_root_update("2.root.json", patch));
|
|
|
|
auto testing_root = static_cast<RootImpl*>(updated_root.get());
|
|
EXPECT_FALSE(testing_root->expired());
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expires", "value": "2051-10-08T07:07:09+0030" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expires", "value": "2051-10-08T07:07:09D" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
|
|
patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/expires", "value": "2051-10-08T07:07:09.000" },
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
EXPECT_THROW(root.update(create_root_update("2.root.json", patch)),
|
|
role_metadata_error);
|
|
}
|
|
|
|
TEST_F(RootImplT_v1, possible_update_files)
|
|
{
|
|
RootImpl root(root1_json);
|
|
|
|
auto update_f = root.possible_update_files();
|
|
EXPECT_THAT(
|
|
update_f,
|
|
::testing::ElementsAre("2.sv2.root.json", "2.sv1.root.json", "2.root.json"));
|
|
|
|
json patch = json::parse(R"([
|
|
{ "op": "replace", "path": "/signed/version", "value": 2 }
|
|
])");
|
|
auto updated_root = root.update(create_root_update("2.root.json", patch));
|
|
update_f = updated_root->possible_update_files();
|
|
EXPECT_THAT(
|
|
update_f,
|
|
::testing::ElementsAre("3.sv2.root.json", "3.sv1.root.json", "3.root.json"));
|
|
}
|
|
|
|
|
|
class SpecImplT_v1 : public ::testing::Test
|
|
{
|
|
public:
|
|
SpecImplT_v1() = default;
|
|
|
|
protected:
|
|
SpecImpl spec;
|
|
};
|
|
|
|
TEST_F(SpecImplT_v1, ctor)
|
|
{
|
|
SpecImpl new_spec("1.0.0");
|
|
EXPECT_EQ(new_spec.version_str(), "1.0.0");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, version_str)
|
|
{
|
|
EXPECT_EQ(spec.version_str(), "1.0.17");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, is_compatible)
|
|
{
|
|
EXPECT_TRUE(spec.is_compatible(std::string("1.0.0")));
|
|
EXPECT_TRUE(spec.is_compatible(std::string("1.0.17")));
|
|
EXPECT_TRUE(spec.is_compatible(std::string("1.25.10")));
|
|
|
|
EXPECT_FALSE(spec.is_compatible(std::string("2.0.0")));
|
|
EXPECT_FALSE(spec.is_compatible(std::string("2.0.17")));
|
|
EXPECT_FALSE(spec.is_compatible(std::string("0.6.0")));
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, is_upgrade)
|
|
{
|
|
EXPECT_TRUE(spec.is_upgrade(std::string("2.0.0")));
|
|
EXPECT_TRUE(spec.is_upgrade(std::string("2.1.10")));
|
|
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("0.6.0")));
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("3.0.0")));
|
|
// not an upgrade, compatible version
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("1.0.17")));
|
|
EXPECT_FALSE(spec.is_upgrade(std::string("1.0.0")));
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, upgradable)
|
|
{
|
|
EXPECT_FALSE(spec.upgradable());
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, compatible_prefix)
|
|
{
|
|
EXPECT_EQ(spec.compatible_prefix(), "1");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, upgrade_prefix)
|
|
{
|
|
EXPECT_THAT(spec.upgrade_prefix(), ::testing::ElementsAre("2"));
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, json_key)
|
|
{
|
|
EXPECT_EQ(spec.json_key(), "spec_version");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, expiration_json_key)
|
|
{
|
|
EXPECT_EQ(spec.expiration_json_key(), "expires");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, canonicalize)
|
|
{
|
|
EXPECT_EQ(spec.canonicalize(R"({"foo":"bar"})"_json), "{\"foo\":\"bar\"}");
|
|
}
|
|
|
|
TEST_F(SpecImplT_v1, signatures)
|
|
{
|
|
json j = R"({
|
|
"signatures":
|
|
[
|
|
{
|
|
"keyid": "foo",
|
|
"sig": "baz",
|
|
"other_headers": "bar"
|
|
}
|
|
]
|
|
})"_json;
|
|
auto sigs = spec.signatures(j);
|
|
EXPECT_EQ(sigs.size(), 1);
|
|
EXPECT_EQ(sigs.begin()->keyid, "foo");
|
|
EXPECT_EQ(sigs.begin()->sig, "baz");
|
|
EXPECT_EQ(sigs.begin()->pgp_trailer, "bar");
|
|
}
|
|
|
|
// Test serialization/deserialization
|
|
TEST(RoleSignature, to_json)
|
|
{
|
|
RoleSignature s{ "some_key_id", "some_signature", "" };
|
|
json j = R"({"keyid": "some_key_id", "sig": "some_signature"})"_json;
|
|
EXPECT_EQ(j, json(s));
|
|
|
|
s = { "some_key_id", "some_signature", "some_pgp_trailer" };
|
|
j = R"({"keyid": "some_key_id", "other_headers": "some_pgp_trailer", "sig": "some_signature"})"_json;
|
|
EXPECT_EQ(j, json(s));
|
|
}
|
|
} // namespace testing
|
|
} // namespace v1
|
|
} // namespace validate
|