mirror of
https://github.com/logos-co/logos-package.git
synced 2026-08-27 18:21:17 +00:00
* feat: add DID-based identity for package signing with comprehensive tests - Add did:jwk identity layer (base64url, publicKeyToDid, didToPublicKey) - Replace raw public keys with DID strings in manifest.sig - Add signer metadata (name, url) and linkedDids placeholder - JWK private key format (.jwk) replaces PEM (.secret) - JSON-based keyring with DID lookup - Make content hashes mandatory (recomputed on every content change) - Add keygen, keyring, sign commands with DID support - Add C API (lgx.h) with DID-based signature types - Add comprehensive test coverage for crypto, keyring, signing, hashes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace hashes_valid with package_valid in SignatureInfo verifySignature() now validates the package (structure + hashes) first via validatePackage(), removing duplicate hash verification. The hashes_valid field is replaced by package_valid which reflects the full package validation result. - Extract validatePackage() from verify() as non-static instance method - verify() now delegates to load() + validatePackage() - verifySignature() calls validatePackage() before checking signature - Remove duplicate Merkle tree verification from verifySignature() - Update C API, tests, and downstream consumers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: signPackage validates instead of recomputing hashes signPackage() now calls validatePackage() to ensure the package is valid (structure + hashes) before signing. It no longer recomputes hashes — hashes are already kept up to date by addVariant/removeVariant. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add lgx_keyring_list C API for listing trusted keys Adds lgx_keyring_list() and lgx_free_keyring_list() to the C API, enabling downstream consumers to enumerate trusted keys in the keyring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add custom directory options to keygen, keyring, sign, and verify CLI commands - keygen: --output-dir / -o to specify key output directory - keyring: --dir / -d to specify keyring directory - sign: --keys-dir / -d to specify keys directory - verify: --keyring-dir to specify keyring directory for trust lookup All default to the standard ~/.config/logos/ paths when not specified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update spec, project, and README for directory options and TOFU removal - Add --output-dir, --keys-dir, --dir, --keyring-dir options to CLI docs - Add signing/keyring C API functions to project.md - Add test_crypto.cpp and libsodium to project.md - Remove --tofu from install-time verification docs - Update lifecycle example with signing and trust management steps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback — security, robustness, and clarity Security: - Validate key names in keyring to prevent path traversal attacks - Add missing <cstdlib> include for std::getenv Robustness: - Detect malformed manifest.sig (report as invalid, not unsigned) - Fail validation when crypto::init() fails instead of skipping hashes - Check init() return value in sign() and verify() - Handle zero-length input in base64 encode functions - Check ensureDirectory() path is actually a directory Clarity: - Label signer name/URL as self-asserted in verify output - Update manifest.cpp comment: hashes for integrity, not just signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make recomputeHashes return Result to fail loudly on crypto errors Previously recomputeHashes() silently returned without setting hashes when crypto::init() failed. Now addVariant/removeVariant propagate the error so packages are never saved without hashes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
638 lines
19 KiB
C++
638 lines
19 KiB
C++
#include <gtest/gtest.h>
|
|
#include "crypto/signing.h"
|
|
#include "crypto/manifest_sig.h"
|
|
#include "crypto/keyring.h"
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
|
|
using namespace lgx;
|
|
using namespace lgx::crypto;
|
|
namespace fs = std::filesystem;
|
|
|
|
// =============================================================================
|
|
// Base64url Encode/Decode Tests
|
|
// =============================================================================
|
|
|
|
TEST(CryptoTest, Base64UrlEncode_RoundTrip) {
|
|
ASSERT_TRUE(init());
|
|
|
|
// Test with various byte patterns
|
|
std::vector<uint8_t> data = {0, 1, 2, 3, 255, 254, 128, 64};
|
|
std::string encoded = base64UrlEncode(data.data(), data.size());
|
|
EXPECT_FALSE(encoded.empty());
|
|
|
|
// Should not contain standard base64 characters that differ in URL variant
|
|
EXPECT_EQ(encoded.find('+'), std::string::npos);
|
|
EXPECT_EQ(encoded.find('/'), std::string::npos);
|
|
EXPECT_EQ(encoded.find('='), std::string::npos);
|
|
|
|
auto decoded = base64UrlDecode(encoded);
|
|
ASSERT_TRUE(decoded.has_value());
|
|
EXPECT_EQ(*decoded, data);
|
|
}
|
|
|
|
TEST(CryptoTest, Base64UrlEncode_EmptyInput) {
|
|
ASSERT_TRUE(init());
|
|
|
|
std::string encoded = base64UrlEncode(nullptr, 0);
|
|
EXPECT_TRUE(encoded.empty());
|
|
}
|
|
|
|
TEST(CryptoTest, Base64UrlEncode_StringOverload) {
|
|
ASSERT_TRUE(init());
|
|
|
|
std::string input = "Hello, World!";
|
|
std::string encoded = base64UrlEncode(input);
|
|
EXPECT_FALSE(encoded.empty());
|
|
|
|
auto decoded = base64UrlDecode(encoded);
|
|
ASSERT_TRUE(decoded.has_value());
|
|
std::string result(decoded->begin(), decoded->end());
|
|
EXPECT_EQ(result, input);
|
|
}
|
|
|
|
TEST(CryptoTest, Base64UrlDecode_InvalidInput) {
|
|
ASSERT_TRUE(init());
|
|
|
|
// Invalid base64url strings
|
|
auto result = base64UrlDecode("!!!invalid!!!");
|
|
EXPECT_FALSE(result.has_value());
|
|
}
|
|
|
|
TEST(CryptoTest, Base64UrlEncode_32ByteKey) {
|
|
ASSERT_TRUE(init());
|
|
|
|
// 32 bytes like an Ed25519 public key
|
|
std::vector<uint8_t> key(32);
|
|
for (size_t i = 0; i < 32; i++) key[i] = static_cast<uint8_t>(i);
|
|
|
|
std::string encoded = base64UrlEncode(key.data(), key.size());
|
|
auto decoded = base64UrlDecode(encoded);
|
|
|
|
ASSERT_TRUE(decoded.has_value());
|
|
EXPECT_EQ(decoded->size(), 32u);
|
|
EXPECT_EQ(*decoded, key);
|
|
}
|
|
|
|
// =============================================================================
|
|
// DID <-> PublicKey Conversion Tests
|
|
// =============================================================================
|
|
|
|
TEST(CryptoTest, PublicKeyToDid_Format) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
// Must start with did:jwk:
|
|
EXPECT_EQ(did.substr(0, 8), "did:jwk:");
|
|
|
|
// The rest should be base64url-encoded JWK
|
|
std::string jwkEncoded = did.substr(8);
|
|
EXPECT_FALSE(jwkEncoded.empty());
|
|
|
|
// Decode and verify JWK structure
|
|
auto jwkBytes = base64UrlDecode(jwkEncoded);
|
|
ASSERT_TRUE(jwkBytes.has_value());
|
|
std::string jwkStr(jwkBytes->begin(), jwkBytes->end());
|
|
|
|
// Should contain the required JWK fields
|
|
EXPECT_NE(jwkStr.find("\"crv\":\"Ed25519\""), std::string::npos);
|
|
EXPECT_NE(jwkStr.find("\"kty\":\"OKP\""), std::string::npos);
|
|
EXPECT_NE(jwkStr.find("\"x\":\""), std::string::npos);
|
|
}
|
|
|
|
TEST(CryptoTest, PublicKeyToDid_Deterministic) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp = generateKeypair();
|
|
std::string did1 = publicKeyToDid(kp.publicKey);
|
|
std::string did2 = publicKeyToDid(kp.publicKey);
|
|
|
|
EXPECT_EQ(did1, did2);
|
|
}
|
|
|
|
TEST(CryptoTest, PublicKeyToDid_DifferentKeysProduceDifferentDids) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp1 = generateKeypair();
|
|
auto kp2 = generateKeypair();
|
|
|
|
std::string did1 = publicKeyToDid(kp1.publicKey);
|
|
std::string did2 = publicKeyToDid(kp2.publicKey);
|
|
|
|
EXPECT_NE(did1, did2);
|
|
}
|
|
|
|
TEST(CryptoTest, DidToPublicKey_RoundTrip) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
auto extracted = didToPublicKey(did);
|
|
ASSERT_TRUE(extracted.has_value());
|
|
EXPECT_EQ(*extracted, kp.publicKey);
|
|
}
|
|
|
|
TEST(CryptoTest, DidToPublicKey_InvalidPrefix) {
|
|
auto result = didToPublicKey("did:key:z1234");
|
|
EXPECT_FALSE(result.has_value());
|
|
}
|
|
|
|
TEST(CryptoTest, DidToPublicKey_EmptyString) {
|
|
auto result = didToPublicKey("");
|
|
EXPECT_FALSE(result.has_value());
|
|
}
|
|
|
|
TEST(CryptoTest, DidToPublicKey_InvalidBase64) {
|
|
auto result = didToPublicKey("did:jwk:!!!invalid!!!");
|
|
EXPECT_FALSE(result.has_value());
|
|
}
|
|
|
|
TEST(CryptoTest, DidToPublicKey_InvalidJwk) {
|
|
ASSERT_TRUE(init());
|
|
|
|
// Valid base64url but not a valid JWK (missing x field)
|
|
std::string fakeJwk = "{\"kty\":\"OKP\",\"crv\":\"Ed25519\"}";
|
|
std::string encoded = base64UrlEncode(fakeJwk);
|
|
auto result = didToPublicKey("did:jwk:" + encoded);
|
|
EXPECT_FALSE(result.has_value());
|
|
}
|
|
|
|
TEST(CryptoTest, DidToPublicKey_WrongKeySize) {
|
|
ASSERT_TRUE(init());
|
|
|
|
// JWK with wrong-sized x value (16 bytes instead of 32)
|
|
std::vector<uint8_t> shortKey(16, 0x42);
|
|
std::string xValue = base64UrlEncode(shortKey.data(), shortKey.size());
|
|
std::string fakeJwk = "{\"crv\":\"Ed25519\",\"kty\":\"OKP\",\"x\":\"" + xValue + "\"}";
|
|
std::string encoded = base64UrlEncode(fakeJwk);
|
|
auto result = didToPublicKey("did:jwk:" + encoded);
|
|
EXPECT_FALSE(result.has_value());
|
|
}
|
|
|
|
TEST(CryptoTest, PublicKeyToDid_JwkKeysAlphabeticallySorted) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
// Decode the JWK
|
|
auto jwkBytes = base64UrlDecode(did.substr(8));
|
|
ASSERT_TRUE(jwkBytes.has_value());
|
|
std::string jwk(jwkBytes->begin(), jwkBytes->end());
|
|
|
|
// crv should come before kty, kty before x
|
|
auto crvPos = jwk.find("\"crv\"");
|
|
auto ktyPos = jwk.find("\"kty\"");
|
|
auto xPos = jwk.find("\"x\"");
|
|
|
|
EXPECT_LT(crvPos, ktyPos);
|
|
EXPECT_LT(ktyPos, xPos);
|
|
}
|
|
|
|
// =============================================================================
|
|
// ManifestSig Serialization Tests
|
|
// =============================================================================
|
|
|
|
TEST(ManifestSigTest, ToJson_Roundtrip) {
|
|
ManifestSig sig;
|
|
sig.version = 1;
|
|
sig.algorithm = "ed25519";
|
|
sig.did = "did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InRlc3QifQ";
|
|
sig.signature = "dGVzdHNpZw==";
|
|
sig.signerName = "Test Publisher";
|
|
sig.signerUrl = "https://example.com";
|
|
|
|
std::string json = sig.toJson();
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
|
|
ASSERT_TRUE(parsed.has_value());
|
|
EXPECT_EQ(parsed->version, 1);
|
|
EXPECT_EQ(parsed->algorithm, "ed25519");
|
|
EXPECT_EQ(parsed->did, sig.did);
|
|
EXPECT_EQ(parsed->signature, sig.signature);
|
|
EXPECT_EQ(parsed->signerName, "Test Publisher");
|
|
EXPECT_EQ(parsed->signerUrl, "https://example.com");
|
|
EXPECT_TRUE(parsed->linkedDids.empty());
|
|
}
|
|
|
|
TEST(ManifestSigTest, ToJson_WithLinkedDids) {
|
|
ManifestSig sig;
|
|
sig.version = 1;
|
|
sig.algorithm = "ed25519";
|
|
sig.did = "did:jwk:test";
|
|
sig.signature = "dGVzdA==";
|
|
sig.linkedDids = {"did:pkh:eip155:1:0xabc", "did:pkh:eip155:1:0xdef"};
|
|
|
|
std::string json = sig.toJson();
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
|
|
ASSERT_TRUE(parsed.has_value());
|
|
ASSERT_EQ(parsed->linkedDids.size(), 2u);
|
|
EXPECT_EQ(parsed->linkedDids[0], "did:pkh:eip155:1:0xabc");
|
|
EXPECT_EQ(parsed->linkedDids[1], "did:pkh:eip155:1:0xdef");
|
|
}
|
|
|
|
TEST(ManifestSigTest, ToJson_EmptySignerMetadata) {
|
|
ManifestSig sig;
|
|
sig.version = 1;
|
|
sig.algorithm = "ed25519";
|
|
sig.did = "did:jwk:test";
|
|
sig.signature = "dGVzdA==";
|
|
// signerName and signerUrl left empty
|
|
|
|
std::string json = sig.toJson();
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
|
|
ASSERT_TRUE(parsed.has_value());
|
|
EXPECT_TRUE(parsed->signerName.empty());
|
|
EXPECT_TRUE(parsed->signerUrl.empty());
|
|
}
|
|
|
|
TEST(ManifestSigTest, FromJson_MissingDid) {
|
|
const char* json = R"({
|
|
"version": 1,
|
|
"algorithm": "ed25519",
|
|
"signature": "dGVzdA==",
|
|
"signer": {},
|
|
"linkedDids": []
|
|
})";
|
|
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
EXPECT_FALSE(parsed.has_value());
|
|
}
|
|
|
|
TEST(ManifestSigTest, FromJson_MissingSignature) {
|
|
const char* json = R"({
|
|
"version": 1,
|
|
"algorithm": "ed25519",
|
|
"did": "did:jwk:test",
|
|
"signer": {},
|
|
"linkedDids": []
|
|
})";
|
|
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
EXPECT_FALSE(parsed.has_value());
|
|
}
|
|
|
|
TEST(ManifestSigTest, FromJson_UnsupportedVersion) {
|
|
const char* json = R"({
|
|
"version": 2,
|
|
"algorithm": "ed25519",
|
|
"did": "did:jwk:test",
|
|
"signature": "dGVzdA==",
|
|
"signer": {},
|
|
"linkedDids": []
|
|
})";
|
|
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
EXPECT_FALSE(parsed.has_value());
|
|
}
|
|
|
|
TEST(ManifestSigTest, FromJson_UnsupportedAlgorithm) {
|
|
const char* json = R"({
|
|
"version": 1,
|
|
"algorithm": "rsa",
|
|
"did": "did:jwk:test",
|
|
"signature": "dGVzdA==",
|
|
"signer": {},
|
|
"linkedDids": []
|
|
})";
|
|
|
|
auto parsed = ManifestSig::fromJson(json);
|
|
EXPECT_FALSE(parsed.has_value());
|
|
}
|
|
|
|
TEST(ManifestSigTest, FromJson_InvalidJson) {
|
|
auto parsed = ManifestSig::fromJson("{ not valid }");
|
|
EXPECT_FALSE(parsed.has_value());
|
|
EXPECT_FALSE(ManifestSig::getLastError().empty());
|
|
}
|
|
|
|
TEST(ManifestSigTest, ToJson_Deterministic) {
|
|
ManifestSig sig;
|
|
sig.version = 1;
|
|
sig.algorithm = "ed25519";
|
|
sig.did = "did:jwk:test";
|
|
sig.signature = "dGVzdA==";
|
|
sig.signerName = "Publisher";
|
|
sig.signerUrl = "https://example.com";
|
|
|
|
EXPECT_EQ(sig.toJson(), sig.toJson());
|
|
}
|
|
|
|
TEST(ManifestSigTest, ToJson_ContainsSignerObject) {
|
|
ManifestSig sig;
|
|
sig.version = 1;
|
|
sig.algorithm = "ed25519";
|
|
sig.did = "did:jwk:test";
|
|
sig.signature = "dGVzdA==";
|
|
sig.signerName = "Publisher";
|
|
sig.signerUrl = "https://example.com";
|
|
|
|
std::string json = sig.toJson();
|
|
// Should have a signer object with name and url
|
|
EXPECT_NE(json.find("\"signer\""), std::string::npos);
|
|
EXPECT_NE(json.find("\"name\""), std::string::npos);
|
|
EXPECT_NE(json.find("\"url\""), std::string::npos);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Keyring Tests
|
|
// =============================================================================
|
|
|
|
class KeyringTest : public ::testing::Test {
|
|
protected:
|
|
fs::path tempDir;
|
|
fs::path trustedKeysDir;
|
|
fs::path keysDir;
|
|
|
|
void SetUp() override {
|
|
ASSERT_TRUE(init());
|
|
tempDir = fs::temp_directory_path() / ("lgx_keyring_test_" + std::to_string(rand()));
|
|
trustedKeysDir = tempDir / "trusted-keys";
|
|
keysDir = tempDir / "keys";
|
|
fs::create_directories(tempDir);
|
|
}
|
|
|
|
void TearDown() override {
|
|
std::error_code ec;
|
|
fs::remove_all(tempDir, ec);
|
|
}
|
|
};
|
|
|
|
TEST_F(KeyringTest, AddKey_ValidDid) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
bool result = keyring.addKey("test-publisher", did, "Test Publisher", "https://example.com");
|
|
EXPECT_TRUE(result);
|
|
|
|
// File should exist
|
|
EXPECT_TRUE(fs::exists(trustedKeysDir / "test-publisher.json"));
|
|
}
|
|
|
|
TEST_F(KeyringTest, AddKey_InvalidDid) {
|
|
Keyring keyring(trustedKeysDir);
|
|
bool result = keyring.addKey("test", "not-a-valid-did");
|
|
EXPECT_FALSE(result);
|
|
}
|
|
|
|
TEST_F(KeyringTest, AddKey_EmptyName) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
bool result = keyring.addKey("", did);
|
|
EXPECT_FALSE(result);
|
|
}
|
|
|
|
TEST_F(KeyringTest, FindByDid) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
keyring.addKey("test-pub", did, "Test Publisher", "https://example.com");
|
|
|
|
auto found = keyring.findByDid(did);
|
|
ASSERT_TRUE(found.has_value());
|
|
EXPECT_EQ(found->name, "test-pub");
|
|
EXPECT_EQ(found->did, did);
|
|
EXPECT_EQ(found->displayName, "Test Publisher");
|
|
EXPECT_EQ(found->url, "https://example.com");
|
|
EXPECT_EQ(found->publicKey, kp.publicKey);
|
|
EXPECT_FALSE(found->addedAt.empty());
|
|
}
|
|
|
|
TEST_F(KeyringTest, FindByDid_NotFound) {
|
|
Keyring keyring(trustedKeysDir);
|
|
auto found = keyring.findByDid("did:jwk:nonexistent");
|
|
EXPECT_FALSE(found.has_value());
|
|
}
|
|
|
|
TEST_F(KeyringTest, FindByPublicKey) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
keyring.addKey("test-pub", did);
|
|
|
|
auto found = keyring.findByPublicKey(kp.publicKey);
|
|
ASSERT_TRUE(found.has_value());
|
|
EXPECT_EQ(found->name, "test-pub");
|
|
EXPECT_EQ(found->did, did);
|
|
}
|
|
|
|
TEST_F(KeyringTest, FindByName) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
keyring.addKey("my-publisher", did, "My Publisher");
|
|
|
|
auto found = keyring.findByName("my-publisher");
|
|
ASSERT_TRUE(found.has_value());
|
|
EXPECT_EQ(found->did, did);
|
|
EXPECT_EQ(found->displayName, "My Publisher");
|
|
}
|
|
|
|
TEST_F(KeyringTest, FindByName_NotFound) {
|
|
Keyring keyring(trustedKeysDir);
|
|
auto found = keyring.findByName("nonexistent");
|
|
EXPECT_FALSE(found.has_value());
|
|
}
|
|
|
|
TEST_F(KeyringTest, RemoveKey) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
keyring.addKey("to-remove", did);
|
|
|
|
EXPECT_TRUE(keyring.findByDid(did).has_value());
|
|
|
|
bool removed = keyring.removeKey("to-remove");
|
|
EXPECT_TRUE(removed);
|
|
|
|
EXPECT_FALSE(keyring.findByDid(did).has_value());
|
|
EXPECT_FALSE(fs::exists(trustedKeysDir / "to-remove.json"));
|
|
}
|
|
|
|
TEST_F(KeyringTest, RemoveKey_NonExistent) {
|
|
Keyring keyring(trustedKeysDir);
|
|
bool removed = keyring.removeKey("nonexistent");
|
|
EXPECT_FALSE(removed);
|
|
}
|
|
|
|
TEST_F(KeyringTest, ListKeys) {
|
|
auto kp1 = generateKeypair();
|
|
auto kp2 = generateKeypair();
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
keyring.addKey("alpha", publicKeyToDid(kp1.publicKey), "Alpha Publisher");
|
|
keyring.addKey("beta", publicKeyToDid(kp2.publicKey), "Beta Publisher");
|
|
|
|
auto keys = keyring.listKeys();
|
|
ASSERT_EQ(keys.size(), 2u);
|
|
|
|
// Sorted by name
|
|
EXPECT_EQ(keys[0].name, "alpha");
|
|
EXPECT_EQ(keys[1].name, "beta");
|
|
EXPECT_EQ(keys[0].displayName, "Alpha Publisher");
|
|
EXPECT_EQ(keys[1].displayName, "Beta Publisher");
|
|
}
|
|
|
|
TEST_F(KeyringTest, ListKeys_EmptyKeyring) {
|
|
Keyring keyring(trustedKeysDir);
|
|
auto keys = keyring.listKeys();
|
|
EXPECT_TRUE(keys.empty());
|
|
}
|
|
|
|
TEST_F(KeyringTest, AddKey_ReplaceExisting) {
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring keyring(trustedKeysDir);
|
|
keyring.addKey("test", did, "Original Name");
|
|
|
|
// Replace with updated metadata
|
|
keyring.addKey("test", did, "Updated Name", "https://new-url.com");
|
|
|
|
auto found = keyring.findByName("test");
|
|
ASSERT_TRUE(found.has_value());
|
|
EXPECT_EQ(found->displayName, "Updated Name");
|
|
EXPECT_EQ(found->url, "https://new-url.com");
|
|
}
|
|
|
|
// =============================================================================
|
|
// Keypair Save/Load Tests
|
|
// =============================================================================
|
|
|
|
TEST_F(KeyringTest, SaveKeypair_CreatesFiles) {
|
|
auto kp = generateKeypair();
|
|
|
|
bool saved = Keyring::saveKeypair(keysDir, "test-key", kp);
|
|
EXPECT_TRUE(saved);
|
|
|
|
EXPECT_TRUE(fs::exists(keysDir / "test-key.jwk"));
|
|
EXPECT_TRUE(fs::exists(keysDir / "test-key.pub"));
|
|
EXPECT_TRUE(fs::exists(keysDir / "test-key.did"));
|
|
}
|
|
|
|
TEST_F(KeyringTest, SaveKeypair_DidFileContainsCorrectDid) {
|
|
auto kp = generateKeypair();
|
|
std::string expectedDid = publicKeyToDid(kp.publicKey);
|
|
|
|
Keyring::saveKeypair(keysDir, "test-key", kp);
|
|
|
|
std::ifstream didFile(keysDir / "test-key.did");
|
|
std::string didContent;
|
|
std::getline(didFile, didContent);
|
|
|
|
EXPECT_EQ(didContent, expectedDid);
|
|
}
|
|
|
|
TEST_F(KeyringTest, SaveLoadSecretKey_RoundTrip) {
|
|
auto kp = generateKeypair();
|
|
|
|
bool saved = Keyring::saveKeypair(keysDir, "test-key", kp);
|
|
ASSERT_TRUE(saved);
|
|
|
|
auto loaded = Keyring::loadSecretKey(keysDir, "test-key");
|
|
ASSERT_TRUE(loaded.has_value());
|
|
|
|
// The loaded secret key should produce the same public key
|
|
PublicKey originalPk = extractPublicKey(kp.secretKey);
|
|
PublicKey loadedPk = extractPublicKey(*loaded);
|
|
EXPECT_EQ(originalPk, loadedPk);
|
|
}
|
|
|
|
TEST_F(KeyringTest, SaveLoadSecretKey_SignVerifyRoundTrip) {
|
|
auto kp = generateKeypair();
|
|
Keyring::saveKeypair(keysDir, "test-key", kp);
|
|
|
|
auto loadedSk = Keyring::loadSecretKey(keysDir, "test-key");
|
|
ASSERT_TRUE(loadedSk.has_value());
|
|
|
|
// Sign with loaded key and verify with original public key
|
|
std::vector<uint8_t> message = {'h', 'e', 'l', 'l', 'o'};
|
|
auto sig = sign(message, *loadedSk);
|
|
|
|
EXPECT_TRUE(verify(message, kp.publicKey, sig));
|
|
}
|
|
|
|
TEST_F(KeyringTest, LoadSecretKey_NonExistent) {
|
|
auto loaded = Keyring::loadSecretKey(keysDir, "nonexistent");
|
|
EXPECT_FALSE(loaded.has_value());
|
|
}
|
|
|
|
TEST_F(KeyringTest, SaveKeypair_JwkFileFormat) {
|
|
auto kp = generateKeypair();
|
|
Keyring::saveKeypair(keysDir, "test-key", kp);
|
|
|
|
// Read and parse the JWK file
|
|
std::ifstream file(keysDir / "test-key.jwk");
|
|
std::string content((std::istreambuf_iterator<char>(file)),
|
|
std::istreambuf_iterator<char>());
|
|
|
|
// Should be valid JSON containing the right fields
|
|
EXPECT_NE(content.find("\"crv\""), std::string::npos);
|
|
EXPECT_NE(content.find("\"Ed25519\""), std::string::npos);
|
|
EXPECT_NE(content.find("\"kty\""), std::string::npos);
|
|
EXPECT_NE(content.find("\"OKP\""), std::string::npos);
|
|
EXPECT_NE(content.find("\"x\""), std::string::npos);
|
|
EXPECT_NE(content.find("\"d\""), std::string::npos);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Ed25519 Sign/Verify with DID Round-trip
|
|
// =============================================================================
|
|
|
|
TEST(CryptoTest, SignVerify_WithDid) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp = generateKeypair();
|
|
std::string did = publicKeyToDid(kp.publicKey);
|
|
|
|
// Sign a message
|
|
std::vector<uint8_t> message = {'t', 'e', 's', 't', ' ', 'm', 'e', 's', 's', 'a', 'g', 'e'};
|
|
auto sig = sign(message, kp.secretKey);
|
|
|
|
// Extract public key from DID and verify
|
|
auto extractedPk = didToPublicKey(did);
|
|
ASSERT_TRUE(extractedPk.has_value());
|
|
|
|
EXPECT_TRUE(verify(message, *extractedPk, sig));
|
|
}
|
|
|
|
TEST(CryptoTest, SignVerify_WrongDid_Fails) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp1 = generateKeypair();
|
|
auto kp2 = generateKeypair();
|
|
|
|
std::vector<uint8_t> message = {'t', 'e', 's', 't'};
|
|
auto sig = sign(message, kp1.secretKey);
|
|
|
|
// Verify with wrong key's DID should fail
|
|
auto wrongPk = didToPublicKey(publicKeyToDid(kp2.publicKey));
|
|
ASSERT_TRUE(wrongPk.has_value());
|
|
EXPECT_FALSE(verify(message, *wrongPk, sig));
|
|
}
|
|
|
|
// =============================================================================
|
|
// ExtractPublicKey Tests
|
|
// =============================================================================
|
|
|
|
TEST(CryptoTest, ExtractPublicKey_MatchesKeypair) {
|
|
ASSERT_TRUE(init());
|
|
|
|
auto kp = generateKeypair();
|
|
PublicKey extracted = extractPublicKey(kp.secretKey);
|
|
EXPECT_EQ(extracted, kp.publicKey);
|
|
}
|