mirror of
https://github.com/logos-co/logos-package.git
synced 2026-08-27 18:21:17 +00:00
* feat(sig): let a consumer carry a package's signature, and check it against a DID of their own
lgx_extract writes variants/<v>/ and assets/ and nothing else, so a consumer
that installs a package keeps the SIGNED BYTES -- manifest.json, written from
lgx_get_manifest_json(), which returns getManifest().toJson(), the same
expression Package::signPackage() signs -- and throws away the signature over
them. An installed package therefore held evidence it could not check, and
whoever wanted to know who published it had to record their own answer and
then defend that record from everything that can write a file.
Two additions close that:
lgx_get_manifest_sig_json() the counterpart to lgx_get_manifest_json(),
so the signature can travel with the bytes.
lgx_check_manifest_signature() does `expected_did`'s key sign these bytes?
The second takes the DID as a PARAMETER, and that is the whole point of it.
Reading the DID out of the signature document, comparing it to a pin, and then
verifying with that same document's key proves only that the file agrees with
itself, which any attacker can arrange: replace the signature, replace the DID
beside it, sign with a key you own. Taking the key from the caller instead
means a match requires an Ed25519 signature under a key the attacker does not
have. `sig_json` contributes the signature bytes and nothing else.
A did:jwk embeds its public key, so this needs no keyring. The keyring answers
a different question -- is this key ANCHORED -- and lgx_verify_signature still
answers that one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(sig): pin the property the expected-DID parameter exists for
The interesting cases are the ones a self-checking implementation would
accept. A signature document supplies both a DID and a signature, so it can
always be made to agree with itself; these tests hand the checker documents
that agree with themselves perfectly and a DID that says otherwise.
AnotherKeysGenuineSignatureIsAMismatch verifies under its own DID, and is
still a mismatch for the publisher
RelabellingTheDidDoesNotChangeTheAnswer the attacker's genuine signature,
relabelled to name the publisher --
accepted by anything that compares
the document's DID to the expected
one, refused here
AGenuineSignatureOverOtherBytesIsAMismatch replay: a real signature by the
right key over a different package
Plus the boundaries the two failure classes have to keep apart: a bad expected
DID is the CALLER's error (BAD_DID), absent or malformed evidence refutes
nothing (UNUSABLE), and an empty message is a message rather than a pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
658 lines
24 KiB
C++
658 lines
24 KiB
C++
#include <gtest/gtest.h>
|
|
#include "lgx.h"
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <cstring>
|
|
|
|
class LibraryTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
// Create a temporary directory for test files
|
|
test_dir_ = std::filesystem::temp_directory_path() / "lgx_test_lib";
|
|
std::filesystem::create_directories(test_dir_);
|
|
}
|
|
|
|
void TearDown() override {
|
|
// Clean up test files
|
|
std::filesystem::remove_all(test_dir_);
|
|
}
|
|
|
|
std::filesystem::path test_dir_;
|
|
};
|
|
|
|
TEST_F(LibraryTest, VersionTest) {
|
|
const char* version = lgx_version();
|
|
ASSERT_NE(version, nullptr);
|
|
EXPECT_STREQ(version, "0.1.0");
|
|
}
|
|
|
|
TEST_F(LibraryTest, CreatePackage) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
lgx_result_t result = lgx_create(output_path.c_str(), "testpkg");
|
|
EXPECT_TRUE(result.success);
|
|
EXPECT_EQ(result.error, nullptr);
|
|
|
|
// Verify file was created
|
|
EXPECT_TRUE(std::filesystem::exists(output_path));
|
|
}
|
|
|
|
TEST_F(LibraryTest, CreatePackageInvalidArgs) {
|
|
lgx_result_t result = lgx_create(nullptr, "testpkg");
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_NE(result.error, nullptr);
|
|
|
|
result = lgx_create("test.lgx", nullptr);
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_NE(result.error, nullptr);
|
|
}
|
|
|
|
TEST_F(LibraryTest, LoadPackage) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
// Create package first
|
|
lgx_result_t result = lgx_create(output_path.c_str(), "testpkg");
|
|
ASSERT_TRUE(result.success);
|
|
|
|
// Load it
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Cleanup
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, LoadPackageInvalidPath) {
|
|
lgx_package_t pkg = lgx_load("/nonexistent/path.lgx");
|
|
EXPECT_EQ(pkg, nullptr);
|
|
|
|
const char* error = lgx_get_last_error();
|
|
EXPECT_NE(error, nullptr);
|
|
EXPECT_NE(strlen(error), 0);
|
|
}
|
|
|
|
TEST_F(LibraryTest, LoadPackageNullArg) {
|
|
lgx_package_t pkg = lgx_load(nullptr);
|
|
EXPECT_EQ(pkg, nullptr);
|
|
|
|
const char* error = lgx_get_last_error();
|
|
EXPECT_NE(error, nullptr);
|
|
}
|
|
|
|
TEST_F(LibraryTest, GetPackageMetadata) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
// Create and load package
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Get name
|
|
const char* name = lgx_get_name(pkg);
|
|
ASSERT_NE(name, nullptr);
|
|
EXPECT_STREQ(name, "testpkg");
|
|
|
|
// Get version
|
|
const char* version = lgx_get_version(pkg);
|
|
ASSERT_NE(version, nullptr);
|
|
EXPECT_STREQ(version, "0.0.1");
|
|
|
|
// Get description (should be empty initially)
|
|
const char* desc = lgx_get_description(pkg);
|
|
ASSERT_NE(desc, nullptr);
|
|
|
|
// Get icon (should be empty initially)
|
|
const char* icon = lgx_get_icon(pkg);
|
|
ASSERT_NE(icon, nullptr);
|
|
EXPECT_STREQ(icon, "");
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, SetPackageMetadata) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
// Create and load package
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Set version
|
|
lgx_result_t result = lgx_set_version(pkg, "1.2.3");
|
|
EXPECT_TRUE(result.success);
|
|
|
|
const char* version = lgx_get_version(pkg);
|
|
EXPECT_STREQ(version, "1.2.3");
|
|
|
|
// Set description
|
|
lgx_set_description(pkg, "Test package");
|
|
const char* desc = lgx_get_description(pkg);
|
|
EXPECT_STREQ(desc, "Test package");
|
|
|
|
// Set icon
|
|
lgx_set_icon(pkg, "icon.png");
|
|
const char* icon = lgx_get_icon(pkg);
|
|
EXPECT_STREQ(icon, "icon.png");
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, SavePackage) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto output_path2 = (test_dir_ / "test2.lgx").string();
|
|
|
|
// Create and load package
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Modify it
|
|
lgx_set_version(pkg, "1.0.0");
|
|
lgx_set_description(pkg, "Modified package");
|
|
|
|
// Save to new location
|
|
lgx_result_t result = lgx_save(pkg, output_path2.c_str());
|
|
EXPECT_TRUE(result.success);
|
|
EXPECT_TRUE(std::filesystem::exists(output_path2));
|
|
|
|
lgx_free_package(pkg);
|
|
|
|
// Verify changes persisted
|
|
lgx_package_t pkg2 = lgx_load(output_path2.c_str());
|
|
ASSERT_NE(pkg2, nullptr);
|
|
|
|
EXPECT_STREQ(lgx_get_version(pkg2), "1.0.0");
|
|
EXPECT_STREQ(lgx_get_description(pkg2), "Modified package");
|
|
|
|
lgx_free_package(pkg2);
|
|
}
|
|
|
|
TEST_F(LibraryTest, VerifyPackage) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
// Create package
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
|
|
// Verify it
|
|
lgx_verify_result_t result = lgx_verify(output_path.c_str());
|
|
EXPECT_TRUE(result.valid);
|
|
|
|
// Should have no errors
|
|
if (result.errors) {
|
|
for (int i = 0; result.errors[i]; i++) {
|
|
// Print any errors for debugging
|
|
printf("Error: %s\n", result.errors[i]);
|
|
}
|
|
}
|
|
|
|
lgx_free_verify_result(result);
|
|
}
|
|
|
|
TEST_F(LibraryTest, VerifyInvalidPackage) {
|
|
lgx_verify_result_t result = lgx_verify("/nonexistent/path.lgx");
|
|
EXPECT_FALSE(result.valid);
|
|
EXPECT_NE(result.errors, nullptr);
|
|
|
|
// Should have at least one error
|
|
EXPECT_NE(result.errors[0], nullptr);
|
|
|
|
lgx_free_verify_result(result);
|
|
}
|
|
|
|
TEST_F(LibraryTest, AddVariantSingleFile) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto file_path = (test_dir_ / "test.txt").string();
|
|
|
|
// Create a test file
|
|
std::ofstream(file_path) << "test content";
|
|
|
|
// Create and load package
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Add variant with single file
|
|
lgx_result_t result = lgx_add_variant(pkg, "test-variant", file_path.c_str(), "test.txt");
|
|
EXPECT_TRUE(result.success) << (result.error ? result.error : "");
|
|
|
|
// Check variant exists
|
|
EXPECT_TRUE(lgx_has_variant(pkg, "test-variant"));
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, HasVariant) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Should not have any variants initially
|
|
EXPECT_FALSE(lgx_has_variant(pkg, "nonexistent"));
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, GetVariants) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto file_path = (test_dir_ / "test.txt").string();
|
|
|
|
// Create a test file
|
|
std::ofstream(file_path) << "test content";
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Initially no variants
|
|
const char** variants = lgx_get_variants(pkg);
|
|
ASSERT_NE(variants, nullptr);
|
|
EXPECT_EQ(variants[0], nullptr); // Empty array
|
|
lgx_free_string_array(variants);
|
|
|
|
// Add a variant
|
|
lgx_add_variant(pkg, "test-variant", file_path.c_str(), "test.txt");
|
|
|
|
// Now should have one variant
|
|
variants = lgx_get_variants(pkg);
|
|
ASSERT_NE(variants, nullptr);
|
|
ASSERT_NE(variants[0], nullptr);
|
|
EXPECT_STREQ(variants[0], "test-variant");
|
|
EXPECT_EQ(variants[1], nullptr); // NULL-terminated
|
|
lgx_free_string_array(variants);
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, RemoveVariant) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto file_path = (test_dir_ / "test.txt").string();
|
|
|
|
// Create a test file
|
|
std::ofstream(file_path) << "test content";
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Add variant
|
|
lgx_add_variant(pkg, "test-variant", file_path.c_str(), "test.txt");
|
|
EXPECT_TRUE(lgx_has_variant(pkg, "test-variant"));
|
|
|
|
// Remove variant
|
|
lgx_result_t result = lgx_remove_variant(pkg, "test-variant");
|
|
EXPECT_TRUE(result.success);
|
|
EXPECT_FALSE(lgx_has_variant(pkg, "test-variant"));
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, RemoveNonexistentVariant) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
// Try to remove non-existent variant
|
|
lgx_result_t result = lgx_remove_variant(pkg, "nonexistent");
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_NE(result.error, nullptr);
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, NullPackageHandles) {
|
|
// All functions should handle NULL package gracefully
|
|
EXPECT_FALSE(lgx_save(nullptr, "test.lgx").success);
|
|
EXPECT_FALSE(lgx_add_variant(nullptr, "variant", "path", nullptr).success);
|
|
EXPECT_FALSE(lgx_remove_variant(nullptr, "variant").success);
|
|
EXPECT_FALSE(lgx_has_variant(nullptr, "variant"));
|
|
EXPECT_EQ(lgx_get_variants(nullptr), nullptr);
|
|
EXPECT_EQ(lgx_get_name(nullptr), nullptr);
|
|
EXPECT_EQ(lgx_get_version(nullptr), nullptr);
|
|
EXPECT_EQ(lgx_get_description(nullptr), nullptr);
|
|
EXPECT_EQ(lgx_get_icon(nullptr), nullptr);
|
|
|
|
// These should not crash with NULL
|
|
lgx_free_package(nullptr);
|
|
}
|
|
|
|
TEST_F(LibraryTest, FreeStringArray) {
|
|
// Test that freeing NULL is safe
|
|
lgx_free_string_array(nullptr);
|
|
|
|
// Test freeing an empty array
|
|
const char** empty = static_cast<const char**>(malloc(sizeof(char*)));
|
|
empty[0] = nullptr;
|
|
lgx_free_string_array(empty);
|
|
}
|
|
|
|
TEST_F(LibraryTest, FreeVerifyResult) {
|
|
// Test that freeing empty result is safe
|
|
lgx_verify_result_t result = {true, nullptr, nullptr};
|
|
lgx_free_verify_result(result);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Extract Tests
|
|
// =============================================================================
|
|
|
|
TEST_F(LibraryTest, ExtractVariant) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto file_path = (test_dir_ / "test.txt").string();
|
|
auto extract_dir = (test_dir_ / "extracted").string();
|
|
|
|
std::ofstream(file_path) << "test content";
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
lgx_result_t result = lgx_add_variant(pkg, "test-variant", file_path.c_str(), "test.txt");
|
|
ASSERT_TRUE(result.success);
|
|
|
|
lgx_save(pkg, output_path.c_str());
|
|
|
|
result = lgx_extract(pkg, "test-variant", extract_dir.c_str());
|
|
EXPECT_TRUE(result.success) << (result.error ? result.error : "");
|
|
|
|
auto extracted_file = std::filesystem::path(extract_dir) / "test-variant" / "test.txt";
|
|
EXPECT_TRUE(std::filesystem::exists(extracted_file)) << "Expected: " << extracted_file.string();
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, ExtractAllVariants) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto file_path = (test_dir_ / "test.txt").string();
|
|
auto extract_dir = (test_dir_ / "extracted").string();
|
|
|
|
std::ofstream(file_path) << "test content";
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
lgx_add_variant(pkg, "variant1", file_path.c_str(), "test.txt");
|
|
lgx_add_variant(pkg, "variant2", file_path.c_str(), "test.txt");
|
|
lgx_save(pkg, output_path.c_str());
|
|
|
|
lgx_result_t result = lgx_extract(pkg, nullptr, extract_dir.c_str());
|
|
EXPECT_TRUE(result.success) << (result.error ? result.error : "");
|
|
|
|
EXPECT_TRUE(std::filesystem::exists(std::filesystem::path(extract_dir) / "variant1" / "test.txt"));
|
|
EXPECT_TRUE(std::filesystem::exists(std::filesystem::path(extract_dir) / "variant2" / "test.txt"));
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, ExtractNonexistentVariant) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
auto extract_dir = (test_dir_ / "extracted").string();
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
lgx_result_t result = lgx_extract(pkg, "nonexistent", extract_dir.c_str());
|
|
EXPECT_FALSE(result.success);
|
|
EXPECT_NE(result.error, nullptr);
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(LibraryTest, ExtractNullArgs) {
|
|
auto output_path = (test_dir_ / "test.lgx").string();
|
|
|
|
lgx_create(output_path.c_str(), "testpkg");
|
|
lgx_package_t pkg = lgx_load(output_path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
|
|
lgx_result_t result = lgx_extract(nullptr, "variant", "/tmp");
|
|
EXPECT_FALSE(result.success);
|
|
|
|
result = lgx_extract(pkg, "variant", nullptr);
|
|
EXPECT_FALSE(result.success);
|
|
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Carrying a signature out of a package, and checking it against a caller's DID
|
|
//
|
|
// lgx_extract() writes variants/<v>/ and assets/ only, so a consumer that
|
|
// installs a package holds the signed bytes without the signature over them.
|
|
// These two functions are what let the signature travel and be checked later,
|
|
// offline, against a key the ASKER chooses.
|
|
// ============================================================================
|
|
|
|
class ManifestSignatureTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
dir_ = std::filesystem::temp_directory_path() / "lgx_test_manifest_sig";
|
|
std::filesystem::remove_all(dir_);
|
|
std::filesystem::create_directories(dir_ / "content");
|
|
std::ofstream(dir_ / "content" / "payload.txt") << "hello";
|
|
}
|
|
void TearDown() override { std::filesystem::remove_all(dir_); }
|
|
|
|
// A real, structurally valid package.
|
|
std::string makePackage(const std::string& name, const std::string& version) {
|
|
auto path = (dir_ / (name + ".lgx")).string();
|
|
if (!lgx_create(path.c_str(), name.c_str()).success) return {};
|
|
lgx_package_t pkg = lgx_load(path.c_str());
|
|
if (!pkg) return {};
|
|
lgx_set_version(pkg, version.c_str());
|
|
auto res = lgx_add_variant(pkg, "linux-x86_64",
|
|
(dir_ / "content").string().c_str(), "payload.txt");
|
|
if (!res.success) { lgx_free_package(pkg); return {}; }
|
|
res = lgx_save(pkg, path.c_str());
|
|
lgx_free_package(pkg);
|
|
return res.success ? path : std::string{};
|
|
}
|
|
std::string makeKey(const std::string& name) {
|
|
if (!lgx_keygen(name.c_str(), dir_.string().c_str()).success) return {};
|
|
return (dir_ / (name + ".jwk")).string();
|
|
}
|
|
std::string didOf(const std::string& keyName) {
|
|
std::ifstream f(dir_ / (keyName + ".did"));
|
|
std::string did; std::getline(f, did); return did;
|
|
}
|
|
// The two documents a consumer would carry into an install tree.
|
|
bool carry(const std::string& lgxPath, std::string& manifest, std::string& sig) {
|
|
lgx_package_t pkg = lgx_load(lgxPath.c_str());
|
|
if (!pkg) return false;
|
|
const char* m = lgx_get_manifest_json(pkg);
|
|
const char* s = lgx_get_manifest_sig_json(pkg);
|
|
manifest = m ? m : "";
|
|
sig = s ? s : "";
|
|
lgx_free_package(pkg);
|
|
return !manifest.empty();
|
|
}
|
|
std::filesystem::path dir_;
|
|
};
|
|
|
|
TEST_F(ManifestSignatureTest, UnsignedPackageHasNoSignatureDocument) {
|
|
auto path = makePackage("plain", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
lgx_package_t pkg = lgx_load(path.c_str());
|
|
ASSERT_NE(pkg, nullptr);
|
|
EXPECT_EQ(lgx_get_manifest_sig_json(pkg), nullptr);
|
|
lgx_free_package(pkg);
|
|
}
|
|
|
|
TEST_F(ManifestSignatureTest, NullPackageIsRejected) {
|
|
EXPECT_EQ(lgx_get_manifest_sig_json(nullptr), nullptr);
|
|
}
|
|
|
|
// THE PREMISE. lgx_get_manifest_json() returns getManifest().toJson(), the
|
|
// same expression signPackage() signs, so the manifest a consumer carries away
|
|
// is byte-for-byte the message the signature covers.
|
|
TEST_F(ManifestSignatureTest, TheCarriedManifestIsTheSignedMessage) {
|
|
auto path = makePackage("signed", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
auto key = makeKey("pub");
|
|
ASSERT_FALSE(key.empty());
|
|
ASSERT_TRUE(lgx_sign(path.c_str(), key.c_str(), nullptr, nullptr).success);
|
|
|
|
std::string manifest, sig;
|
|
ASSERT_TRUE(carry(path, manifest, sig));
|
|
ASSERT_FALSE(sig.empty());
|
|
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
sig.c_str(), didOf("pub").c_str()),
|
|
LGX_SIG_CHECK_OK);
|
|
}
|
|
|
|
// THE PROPERTY THE PARAMETER EXISTS FOR. The document is entirely
|
|
// self-consistent -- a genuine signature by a real key, naming that key's DID
|
|
// -- and it is still refused when the caller asks about a DIFFERENT key.
|
|
TEST_F(ManifestSignatureTest, AnotherKeysGenuineSignatureIsAMismatch) {
|
|
auto path = makePackage("signed", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
auto attackerKey = makeKey("attacker");
|
|
ASSERT_FALSE(attackerKey.empty());
|
|
ASSERT_FALSE(makeKey("publisher").empty());
|
|
ASSERT_TRUE(lgx_sign(path.c_str(), attackerKey.c_str(), nullptr, nullptr).success);
|
|
|
|
std::string manifest, sig;
|
|
ASSERT_TRUE(carry(path, manifest, sig));
|
|
|
|
// Self-consistent: it verifies under its own DID.
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
sig.c_str(), didOf("attacker").c_str()),
|
|
LGX_SIG_CHECK_OK);
|
|
// ...and that buys it nothing when the caller names the publisher.
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
sig.c_str(), didOf("publisher").c_str()),
|
|
LGX_SIG_CHECK_MISMATCH);
|
|
}
|
|
|
|
// The DID inside the document is never consulted for the key, so relabelling
|
|
// it changes nothing. This is the case a "compare the DID to the expected one,
|
|
// then verify with it" implementation would accept.
|
|
TEST_F(ManifestSignatureTest, RelabellingTheDidDoesNotChangeTheAnswer) {
|
|
auto path = makePackage("signed", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
auto attackerKey = makeKey("attacker");
|
|
ASSERT_FALSE(attackerKey.empty());
|
|
ASSERT_FALSE(makeKey("publisher").empty());
|
|
ASSERT_TRUE(lgx_sign(path.c_str(), attackerKey.c_str(), nullptr, nullptr).success);
|
|
|
|
std::string manifest, sig;
|
|
ASSERT_TRUE(carry(path, manifest, sig));
|
|
|
|
// Rewrite ONLY the did field to name the publisher, leaving the attacker's
|
|
// genuine signature in place. Textual, so the test does not depend on the
|
|
// JSON library the caller happens to use.
|
|
const std::string publisherDid = didOf("publisher");
|
|
const std::string attackerDid = didOf("attacker");
|
|
auto at = sig.find(attackerDid);
|
|
ASSERT_NE(at, std::string::npos);
|
|
std::string relabelled = sig.substr(0, at) + publisherDid
|
|
+ sig.substr(at + attackerDid.size());
|
|
ASSERT_NE(relabelled.find(publisherDid), std::string::npos);
|
|
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
relabelled.c_str(), publisherDid.c_str()),
|
|
LGX_SIG_CHECK_MISMATCH)
|
|
<< "a signature wearing the expected DID's name was accepted";
|
|
}
|
|
|
|
// A signature covers a MESSAGE, not a package name. The manifest carries the
|
|
// Merkle root over the payload, so a signature lifted from another package
|
|
// cannot describe these bytes.
|
|
TEST_F(ManifestSignatureTest, AGenuineSignatureOverOtherBytesIsAMismatch) {
|
|
auto key = makeKey("pub");
|
|
ASSERT_FALSE(key.empty());
|
|
|
|
auto a = makePackage("pkg_a", "1.0.0");
|
|
auto b = makePackage("pkg_b", "2.0.0");
|
|
ASSERT_FALSE(a.empty());
|
|
ASSERT_FALSE(b.empty());
|
|
ASSERT_TRUE(lgx_sign(a.c_str(), key.c_str(), nullptr, nullptr).success);
|
|
ASSERT_TRUE(lgx_sign(b.c_str(), key.c_str(), nullptr, nullptr).success);
|
|
|
|
std::string manifestA, sigA, manifestB, sigB;
|
|
ASSERT_TRUE(carry(a, manifestA, sigA));
|
|
ASSERT_TRUE(carry(b, manifestB, sigB));
|
|
|
|
const std::string did = didOf("pub");
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifestA.data(), manifestA.size(),
|
|
sigA.c_str(), did.c_str()),
|
|
LGX_SIG_CHECK_OK);
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifestA.data(), manifestA.size(),
|
|
sigB.c_str(), did.c_str()),
|
|
LGX_SIG_CHECK_MISMATCH);
|
|
}
|
|
|
|
// A caller's DID that is not a did:jwk Ed25519 key is reported as the CALLER's
|
|
// error, distinctly from unusable evidence. Callers rank the two differently:
|
|
// missing evidence is commonly tolerated, an unsatisfiable expectation is not,
|
|
// and collapsing them would let a typo'd DID be waved through.
|
|
TEST_F(ManifestSignatureTest, ABadExpectedDidIsItsOwnAnswer) {
|
|
auto path = makePackage("signed", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
auto key = makeKey("pub");
|
|
ASSERT_FALSE(key.empty());
|
|
ASSERT_TRUE(lgx_sign(path.c_str(), key.c_str(), nullptr, nullptr).success);
|
|
std::string manifest, sig;
|
|
ASSERT_TRUE(carry(path, manifest, sig));
|
|
|
|
for (const char* bad : {"", "did:jwk:!!!!", "did:web:example.com", "nonsense"}) {
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
sig.c_str(), bad),
|
|
LGX_SIG_CHECK_BAD_DID) << bad;
|
|
}
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
sig.c_str(), nullptr),
|
|
LGX_SIG_CHECK_BAD_DID);
|
|
}
|
|
|
|
// Evidence that is absent or unreadable refutes nothing, and is reported
|
|
// separately from evidence that refutes.
|
|
TEST_F(ManifestSignatureTest, UnusableEvidenceIsNotAMismatch) {
|
|
auto path = makePackage("signed", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
auto key = makeKey("pub");
|
|
ASSERT_FALSE(key.empty());
|
|
ASSERT_TRUE(lgx_sign(path.c_str(), key.c_str(), nullptr, nullptr).success);
|
|
std::string manifest, sig;
|
|
ASSERT_TRUE(carry(path, manifest, sig));
|
|
const std::string did = didOf("pub");
|
|
|
|
const char* unusable[] = {
|
|
nullptr, // no document
|
|
"", // empty
|
|
"not json at all", // unparseable
|
|
R"({"version":2,"algorithm":"ed25519","did":"x","signature":"AA"})", // version
|
|
R"({"version":1,"algorithm":"rsa","did":"x","signature":"AA"})", // algorithm
|
|
R"({"version":1,"algorithm":"ed25519","did":"x","signature":"AA"})", // short sig
|
|
};
|
|
for (const char* u : unusable) {
|
|
EXPECT_EQ(lgx_check_manifest_signature(manifest.data(), manifest.size(),
|
|
u, did.c_str()),
|
|
LGX_SIG_CHECK_UNUSABLE) << (u ? u : "(null)");
|
|
}
|
|
}
|
|
|
|
// An empty message is a message. It must not be mistaken for "nothing to
|
|
// check": a caller handing over a truncated manifest gets a mismatch, not a
|
|
// pass.
|
|
TEST_F(ManifestSignatureTest, AnEmptyMessageIsAMismatchNotAPass) {
|
|
auto path = makePackage("signed", "1.0.0");
|
|
ASSERT_FALSE(path.empty());
|
|
auto key = makeKey("pub");
|
|
ASSERT_FALSE(key.empty());
|
|
ASSERT_TRUE(lgx_sign(path.c_str(), key.c_str(), nullptr, nullptr).success);
|
|
std::string manifest, sig;
|
|
ASSERT_TRUE(carry(path, manifest, sig));
|
|
|
|
EXPECT_EQ(lgx_check_manifest_signature("", 0, sig.c_str(), didOf("pub").c_str()),
|
|
LGX_SIG_CHECK_MISMATCH);
|
|
EXPECT_EQ(lgx_check_manifest_signature(nullptr, 99, sig.c_str(), didOf("pub").c_str()),
|
|
LGX_SIG_CHECK_MISMATCH);
|
|
}
|