Files
Dario LipicarandClaude Opus 5 49151f0036 feat(sig): let a consumer carry a package's signature and check it against a DID of their own (#36)
* 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>
2026-08-27 09:31:04 -03:00

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