security/message_signing.cpp¶
Message signing implementation. More...
Namespaces¶
| Name |
|---|
| sgns |
| sgns::neoswarm |
| sgns::neoswarm::security |
Detailed Description¶
Message signing implementation.
Date: 2026-05-08
Source code¶
#include "message_signing.hpp"
#include "common/logging.hpp"
#include <chrono>
#include <iomanip>
#include <sstream>
#include <secp256k1.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
namespace sgns::neoswarm::security
{
namespace
{
auto SigningLogger()
{
return neoswarm::CreateLogger( "MessageSigning" );
}
std::string ToHex( const std::vector<uint8_t>& data )
{
std::ostringstream oss;
for ( auto b : data )
{
oss << std::hex << std::setw( 2 ) << std::setfill( '0' ) << static_cast<int>( b );
}
return oss.str();
}
std::vector<uint8_t> FromHex( const std::string& hex )
{
std::vector<uint8_t> bytes;
for ( size_t i = 0; i + 1 < hex.size(); i += 2 )
{
bytes.push_back( static_cast<uint8_t>( std::stoul( hex.substr( i, 2 ), nullptr, 16 ) ) );
}
return bytes;
}
} // namespace
MessageSigning::MessageSigning( const NodeIdentity& identity )
: m_identity( identity )
{
}
// -----------------------------------------------------------------------
// Sign
// -----------------------------------------------------------------------
outcome::result<std::vector<uint8_t>> MessageSigning::Sign( const std::string& payload ) const
{
std::vector<uint8_t> bytes( payload.begin(), payload.end() );
return m_identity.Sign( bytes );
}
// -----------------------------------------------------------------------
// Verify
// -----------------------------------------------------------------------
bool MessageSigning::Verify( const std::string& payload,
const std::vector<uint8_t>& signature,
const std::string& m_pubKeyhex )
{
// Validate inputs
if ( payload.empty() || signature.empty() || m_pubKeyhex.empty() )
{
return false;
}
// Parse public key from hex
auto pubBytes = FromHex( m_pubKeyhex );
if ( pubBytes.size() != NodeIdentity::kPubKeySize )
{
return false;
}
// Create verify-only secp256k1 context
auto ctx = secp256k1_context_create( SECP256K1_CONTEXT_VERIFY );
if ( !ctx )
{
SigningLogger()->error( "MessageSigning::Verify — failed to create secp256k1 context" );
return false;
}
// Parse public key
secp256k1_pubkey pubkey;
if ( !secp256k1_ec_pubkey_parse( ctx, &pubkey, pubBytes.data(), pubBytes.size() ) )
{
secp256k1_context_destroy( ctx );
return false;
}
// Parse DER signature
secp256k1_ecdsa_signature sig;
if ( !secp256k1_ecdsa_signature_parse_der( ctx, &sig, signature.data(), signature.size() ) )
{
secp256k1_context_destroy( ctx );
return false;
}
// Normalize to low-S to prevent signature malleability
secp256k1_ecdsa_signature_normalize( ctx, nullptr, &sig );
// Hash payload with SHA-256
uint8_t hash[32];
SHA256( reinterpret_cast<const uint8_t*>( payload.data() ), payload.size(), hash );
// Verify
int result = secp256k1_ecdsa_verify( ctx, &sig, hash, &pubkey );
secp256k1_context_destroy( ctx );
return result == 1;
}
// -----------------------------------------------------------------------
// GenerateNonce / CurrentTimestampMs
// -----------------------------------------------------------------------
std::string MessageSigning::GenerateNonce()
{
std::vector<uint8_t> nonceBytes( 32 );
if ( !RAND_bytes( nonceBytes.data(), static_cast<int>( nonceBytes.size() ) ) )
{
// Fallback: use time-based nonce if RAND_bytes fails
auto ts = CurrentTimestampMs();
std::memcpy( nonceBytes.data(), &ts, sizeof( ts ) );
}
return ToHex( nonceBytes );
}
uint64_t MessageSigning::CurrentTimestampMs()
{
auto now = std::chrono::system_clock::now();
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>( now.time_since_epoch() ).count() );
}
// -----------------------------------------------------------------------
// AttachSignature
// -----------------------------------------------------------------------
std::string MessageSigning::AttachSignature( const std::string& payload ) const
{
// Generate nonce and timestamp
const std::string nonce = GenerateNonce();
const uint64_t ts = CurrentTimestampMs();
// Build signed payload: inject nonce + ts into JSON before signing
std::string signedPayload = payload;
if ( !signedPayload.empty() && signedPayload.back() == '}' )
{
signedPayload.pop_back();
signedPayload += ",\"nonce\":\"" + nonce + "\"";
signedPayload += ",\"ts\":" + std::to_string( ts );
signedPayload += "}";
}
// Sign the payload WITH nonce+ts included
auto sig_res = Sign( signedPayload );
if ( !sig_res.has_value() )
{
SigningLogger()->warn( "MessageSigning: failed to sign payload" );
return payload;
}
// Append signature field
std::string result = signedPayload;
if ( !result.empty() && result.back() == '}' )
{
result.pop_back();
result += ",\"sig\":\"" + ToHex( sig_res.value() ) + "\"}";
}
return result;
}
// -----------------------------------------------------------------------
// VerifyAndStrip
// -----------------------------------------------------------------------
bool MessageSigning::VerifyAndStrip( std::string& payload, const std::string& m_pubKeyhex )
{
// Step 1: Extract sig field (last field appended by AttachSignature)
auto sigPos = payload.rfind( ",\"sig\":\"" );
if ( sigPos == std::string::npos )
{
return false;
}
auto sigStart = sigPos + 8; // strlen(",\"sig\":\"")
auto sigEnd = payload.find( "\"", sigStart );
if ( sigEnd == std::string::npos )
{
return false;
}
auto sigBytes = FromHex( payload.substr( sigStart, sigEnd - sigStart ) );
if ( sigBytes.empty() )
{
return false;
}
// Step 2: Build the verify payload (everything before ",sig" + "}")
std::string verifyPayload = payload.substr( 0, sigPos ) + "}";
// Step 3: Extract ts from verifyPayload
auto tsPos = verifyPayload.rfind( ",\"ts\":" );
if ( tsPos == std::string::npos )
{
SigningLogger()->warn( "MessageSigning: missing timestamp in signed payload" );
return false;
}
auto tsStart = tsPos + 6; // strlen(",\"ts\":")
auto tsEnd = verifyPayload.find_first_of( ",}", tsStart );
uint64_t msgTs = 0;
try
{
msgTs = std::stoull( verifyPayload.substr( tsStart, tsEnd - tsStart ) );
}
catch ( ... )
{
return false;
}
// Step 4: Validate replay window
uint64_t nowMs = CurrentTimestampMs();
int64_t ageMs = static_cast<int64_t>( nowMs - msgTs );
if ( ageMs < 0 || ageMs > ( kReplayWindowSec * 1000 ) )
{
SigningLogger()->warn( "MessageSigning: replay detected — message age {}ms exceeds {}s window", ageMs,
kReplayWindowSec );
return false;
}
// Step 5: Verify signature on the full payload (includes nonce+ts)
if ( !Verify( verifyPayload, sigBytes, m_pubKeyhex ) )
{
return false;
}
// Step 6: Strip injected fields to recover original payload
// Remove ",sig":"<hex>" (keep trailing '}')
payload.erase( sigPos, sigEnd - sigPos + 1 ); // +1 to include closing '"'
// Remove ",ts":<num>
tsPos = payload.rfind( ",\"ts\":" );
if ( tsPos != std::string::npos )
{
auto tsEnd2 = payload.find_first_of( ",}", tsPos + 6 );
payload.erase( tsPos, tsEnd2 - tsPos );
}
// Remove ",nonce":"<hex>"
auto noncePos = payload.rfind( ",\"nonce\":\"" );
if ( noncePos != std::string::npos )
{
auto nonceEnd = payload.find( "\"", noncePos + 10 );
payload.erase( noncePos, nonceEnd - noncePos + 1 );
}
return true;
}
} // namespace sgns::neoswarm::security
Updated on 2026-07-25 at 22:56:57 +0000