Skip to content

security/test_node_identity.cpp

Unit tests for NodeIdentity — key generation, sign/verify, encrypted save/load. More...

Functions

Name
TEST(NodeIdentity , DeterministicSignature )
TEST(NodeIdentity , DifferentMessagesDifferentSignatures )
TEST(NodeIdentity , SignAndVerifyRoundtrip )
TEST(NodeIdentity , SaveEncryptedLoadEncryptedRoundtrip )
TEST(NodeIdentity , LoadEncryptedWrongPassphrase )
TEST(NodeIdentity , LoadEncryptedTamperedFile )
TEST(NodeIdentity , SaveEncryptedWithoutKey )
TEST(NodeIdentity , LoadEncryptedNonexistentFile )
TEST(NodeIdentity , SaveEncryptedOverwrite )
TEST(NodeIdentity , PeerId_ConsistentForSameKey )
TEST(NodeIdentity , LoadEncrypted_TruncatedFile_ReturnsError )
TEST(NodeIdentity , PeerId_WithoutKey_ReturnsEmpty )
TEST(NodeIdentity , LoadFromFile_EmptyPath_ReturnsError )
TEST(NodeIdentity , SaveToFile_WithoutKey_ReturnsError )
TEST(NodeIdentity , GetPrivateKey_AfterGenerate_Returns32Bytes )
TEST(NodeIdentity , GetPrivateKey_AfterLoadFromFile_PreservesKey )
TEST(NodeIdentity , GetPrivateKey_AfterLoadEncrypted_PreservesKey )
TEST(NodeIdentity , PrivateKeyHexEncoding_Is66CharsWith0xPrefix )

Detailed Description

Unit tests for NodeIdentity — key generation, sign/verify, encrypted save/load.

Date: 2026-05-28 GSD Executor

Functions Documentation

function TEST

TEST(
    NodeIdentity ,
    DeterministicSignature 
)

function TEST

TEST(
    NodeIdentity ,
    DifferentMessagesDifferentSignatures 
)

function TEST

TEST(
    NodeIdentity ,
    SignAndVerifyRoundtrip 
)

function TEST

TEST(
    NodeIdentity ,
    SaveEncryptedLoadEncryptedRoundtrip 
)

function TEST

TEST(
    NodeIdentity ,
    LoadEncryptedWrongPassphrase 
)

function TEST

TEST(
    NodeIdentity ,
    LoadEncryptedTamperedFile 
)

function TEST

TEST(
    NodeIdentity ,
    SaveEncryptedWithoutKey 
)

function TEST

TEST(
    NodeIdentity ,
    LoadEncryptedNonexistentFile 
)

function TEST

TEST(
    NodeIdentity ,
    SaveEncryptedOverwrite 
)

function TEST

TEST(
    NodeIdentity ,
    PeerId_ConsistentForSameKey 
)

function TEST

TEST(
    NodeIdentity ,
    LoadEncrypted_TruncatedFile_ReturnsError 
)

function TEST

TEST(
    NodeIdentity ,
    PeerId_WithoutKey_ReturnsEmpty 
)

function TEST

TEST(
    NodeIdentity ,
    LoadFromFile_EmptyPath_ReturnsError 
)

function TEST

TEST(
    NodeIdentity ,
    SaveToFile_WithoutKey_ReturnsError 
)

function TEST

TEST(
    NodeIdentity ,
    GetPrivateKey_AfterGenerate_Returns32Bytes 
)

function TEST

TEST(
    NodeIdentity ,
    GetPrivateKey_AfterLoadFromFile_PreservesKey 
)

function TEST

TEST(
    NodeIdentity ,
    GetPrivateKey_AfterLoadEncrypted_PreservesKey 
)

function TEST

TEST(
    NodeIdentity ,
    PrivateKeyHexEncoding_Is66CharsWith0xPrefix 
)

Source code

#include "security/node_identity.hpp"
#include <gtest/gtest.h>

#include <cstdio>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <vector>

using namespace sgns::neoswarm;
using namespace sgns::neoswarm::security;

namespace
{
    const std::string kTestKeyPath = "/tmp/gnus_test_node.key";
    const std::string kTestPass = "test123";
    const std::string kWrongPass = "wrong456";

    void RemoveTestFile()
    {
        std::remove( kTestKeyPath.c_str() );
    }
} // namespace

// =======================================================================
// Key Generation & Identity
// =======================================================================

TEST( NodeIdentity, DeterministicSignature )
{
    NodeIdentity ident;
    ASSERT_TRUE( ident.Generate().has_value() );
    ASSERT_TRUE( ident.IsLoaded() );

    std::vector<uint8_t> msg1 = { 0x01, 0x02, 0x03, 0x04 };
    std::vector<uint8_t> msg2 = { 0x01, 0x02, 0x03, 0x04 };

    auto sig1 = ident.Sign( msg1 );
    auto sig2 = ident.Sign( msg2 );
    ASSERT_TRUE( sig1.has_value() );
    ASSERT_TRUE( sig2.has_value() );

    EXPECT_EQ( sig1.value().size(), sig2.value().size() );
    EXPECT_EQ( sig1.value(), sig2.value() );
}

TEST( NodeIdentity, DifferentMessagesDifferentSignatures )
{
    NodeIdentity ident;
    ASSERT_TRUE( ident.Generate().has_value() );

    std::vector<uint8_t> msgA = { 0xAA };
    std::vector<uint8_t> msgB = { 0xBB };

    auto sigA = ident.Sign( msgA );
    auto sigB = ident.Sign( msgB );
    ASSERT_TRUE( sigA.has_value() );
    ASSERT_TRUE( sigB.has_value() );

    EXPECT_NE( sigA.value(), sigB.value() );
}

TEST( NodeIdentity, SignAndVerifyRoundtrip )
{
    NodeIdentity ident;
    ASSERT_TRUE( ident.Generate().has_value() );

    std::vector<uint8_t> msg = { 0x01, 0x02, 0x03, 0x04, 0x05 };
    auto sig = ident.Sign( msg );
    ASSERT_TRUE( sig.has_value() );

    EXPECT_TRUE( ident.Verify( msg, sig.value() ) );
}

// =======================================================================
// AES-256-GCM Encrypted Key Storage
// =======================================================================

TEST( NodeIdentity, SaveEncryptedLoadEncryptedRoundtrip )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.IsLoaded() );

    auto saveResult = ident1.SaveEncrypted( kTestKeyPath, kTestPass );
    ASSERT_TRUE( saveResult.has_value() );

    NodeIdentity ident2;
    auto loadResult = ident2.LoadEncrypted( kTestKeyPath, kTestPass );
    ASSERT_TRUE( loadResult.has_value() );
    ASSERT_TRUE( ident2.IsLoaded() );

    EXPECT_EQ( ident1.GetPeerId(), ident2.GetPeerId() );

    RemoveTestFile();
}

TEST( NodeIdentity, LoadEncryptedWrongPassphrase )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.SaveEncrypted( kTestKeyPath, kTestPass ).has_value() );

    NodeIdentity ident2;
    auto result = ident2.LoadEncrypted( kTestKeyPath, kWrongPass );

    EXPECT_FALSE( result.has_value() );
    EXPECT_EQ( result.error(), Error::IdentityError );

    RemoveTestFile();
}

TEST( NodeIdentity, LoadEncryptedTamperedFile )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.SaveEncrypted( kTestKeyPath, kTestPass ).has_value() );

    {
        std::fstream f( kTestKeyPath, std::ios::binary | std::ios::in | std::ios::out );
        ASSERT_TRUE( f.is_open() );
        f.seekp( 48, std::ios::beg );
        char c = 0;
        f.get( c );
        f.seekp( 48, std::ios::beg );
        f.put( static_cast<char>( c ^ 0xFF ) );
        f.close();
    }

    NodeIdentity ident2;
    auto result = ident2.LoadEncrypted( kTestKeyPath, kTestPass );

    EXPECT_FALSE( result.has_value() );
    EXPECT_EQ( result.error(), Error::IdentityError );

    RemoveTestFile();
}

TEST( NodeIdentity, SaveEncryptedWithoutKey )
{
    RemoveTestFile();

    NodeIdentity ident;
    ASSERT_FALSE( ident.IsLoaded() );

    auto result = ident.SaveEncrypted( kTestKeyPath, kTestPass );

    EXPECT_FALSE( result.has_value() );
    EXPECT_EQ( result.error(), Error::IdentityError );

    RemoveTestFile();
}

TEST( NodeIdentity, LoadEncryptedNonexistentFile )
{
    RemoveTestFile();

    NodeIdentity ident;
    auto result = ident.LoadEncrypted( kTestKeyPath, kTestPass );

    EXPECT_FALSE( result.has_value() );
    EXPECT_EQ( result.error(), Error::IdentityError );
}

TEST( NodeIdentity, SaveEncryptedOverwrite )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.SaveEncrypted( kTestKeyPath, kTestPass ).has_value() );
    ASSERT_TRUE( ident1.SaveEncrypted( kTestKeyPath, kTestPass ).has_value() );

    NodeIdentity ident2;
    ASSERT_TRUE( ident2.LoadEncrypted( kTestKeyPath, kTestPass ).has_value() );
    EXPECT_EQ( ident1.GetPeerId(), ident2.GetPeerId() );

    RemoveTestFile();
}

TEST( NodeIdentity, PeerId_ConsistentForSameKey )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.SaveToFile( kTestKeyPath ).has_value() );
    std::string peerId1 = ident1.GetPeerId();

    NodeIdentity ident2;
    ASSERT_TRUE( ident2.LoadFromFile( kTestKeyPath ).has_value() );
    std::string peerId2 = ident2.GetPeerId();

    EXPECT_EQ( peerId1, peerId2 );
    EXPECT_FALSE( peerId1.empty() );
    RemoveTestFile();
}

TEST( NodeIdentity, LoadEncrypted_TruncatedFile_ReturnsError )
{
    RemoveTestFile();

    NodeIdentity ident;
    ASSERT_TRUE( ident.Generate().has_value() );
    ASSERT_TRUE( ident.SaveEncrypted( kTestKeyPath, kTestPass ).has_value() );

    {
        std::ofstream f( kTestKeyPath, std::ios::trunc | std::ios::binary );
        ASSERT_TRUE( f.is_open() );
        f.write( "short", 5 );
        f.close();
    }

    NodeIdentity ident2;
    auto result = ident2.LoadEncrypted( kTestKeyPath, kTestPass );
    EXPECT_FALSE( result.has_value() );

    RemoveTestFile();
}

TEST( NodeIdentity, PeerId_WithoutKey_ReturnsEmpty )
{
    NodeIdentity ident;
    EXPECT_TRUE( ident.GetPeerId().empty() );
}

TEST( NodeIdentity, LoadFromFile_EmptyPath_ReturnsError )
{
    NodeIdentity ident;
    auto result = ident.LoadFromFile( "" );
    EXPECT_FALSE( result.has_value() );
}

TEST( NodeIdentity, SaveToFile_WithoutKey_ReturnsError )
{
    NodeIdentity ident;
    auto result = ident.SaveToFile( kTestKeyPath );
    EXPECT_FALSE( result.has_value() );
}

// =======================================================================
// GetPrivateKey — Phase 2, Plan 05 (SDK wiring)
// =======================================================================

TEST( NodeIdentity, GetPrivateKey_AfterGenerate_Returns32Bytes )
{
    NodeIdentity ident;
    ASSERT_TRUE( ident.Generate().has_value() );
    ASSERT_TRUE( ident.IsLoaded() );

    const auto& privKey = ident.GetPrivateKey();
    EXPECT_EQ( privKey.size(), NodeIdentity::kPrivKeySize );

    // Key must not be all zeros — verify at least one non-zero byte
    bool hasNonZero = false;
    for ( const auto byte : privKey )
    {
        if ( byte != 0 )
        {
            hasNonZero = true;
            break;
        }
    }
    EXPECT_TRUE( hasNonZero );
}

TEST( NodeIdentity, GetPrivateKey_AfterLoadFromFile_PreservesKey )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.SaveToFile( kTestKeyPath ).has_value() );

    const auto& privKey1 = ident1.GetPrivateKey();

    NodeIdentity ident2;
    ASSERT_TRUE( ident2.LoadFromFile( kTestKeyPath ).has_value() );
    ASSERT_TRUE( ident2.IsLoaded() );

    const auto& privKey2 = ident2.GetPrivateKey();

    EXPECT_EQ( privKey1, privKey2 );
    EXPECT_EQ( ident1.GetPeerId(), ident2.GetPeerId() );

    RemoveTestFile();
}

TEST( NodeIdentity, GetPrivateKey_AfterLoadEncrypted_PreservesKey )
{
    RemoveTestFile();

    NodeIdentity ident1;
    ASSERT_TRUE( ident1.Generate().has_value() );
    ASSERT_TRUE( ident1.SaveEncrypted( kTestKeyPath, kTestPass ).has_value() );

    const auto& privKey1 = ident1.GetPrivateKey();

    NodeIdentity ident2;
    ASSERT_TRUE( ident2.LoadEncrypted( kTestKeyPath, kTestPass ).has_value() );
    ASSERT_TRUE( ident2.IsLoaded() );

    const auto& privKey2 = ident2.GetPrivateKey();

    EXPECT_EQ( privKey1, privKey2 );
    EXPECT_EQ( ident1.GetPeerId(), ident2.GetPeerId() );

    RemoveTestFile();
}

// =======================================================================
// Private key hex encoding — Phase 2, Plan 05 (GeniusSDK init)
// =======================================================================

TEST( NodeIdentity, PrivateKeyHexEncoding_Is66CharsWith0xPrefix )
{
    NodeIdentity ident;
    ASSERT_TRUE( ident.Generate().has_value() );

    const auto& privKey = ident.GetPrivateKey();
    EXPECT_EQ( privKey.size(), NodeIdentity::kPrivKeySize );

    // Simulate the hex encoding used by SGClient::Initialize()
    // Must produce "0x" + 64 hex chars (total 66) for GeniusSDKInitWithKey()
    std::ostringstream hexStream;
    hexStream << "0x";
    hexStream << std::hex << std::setfill( '0' );
    for ( const auto byte : privKey )
    {
        hexStream << std::setw( 2 ) << static_cast<int>( byte );
    }
    std::string hexKey = hexStream.str();

    EXPECT_EQ( hexKey.size(), 66U );
    EXPECT_EQ( hexKey.substr( 0, 2 ), "0x" );

    // All chars after prefix must be valid hex
    for ( size_t i = 2; i < hexKey.size(); ++i )
    {
        EXPECT_TRUE( std::isxdigit( static_cast<unsigned char>( hexKey[i] ) ) );
    }
}

Updated on 2026-07-25 at 22:56:57 +0000