auth/auth_handshake.cpp¶
Namespaces¶
| Name |
|---|
| rlpx |
| rlpx::auth |
Functions¶
| Name | |
|---|---|
| FrameSecrets | derive_frame_secrets(const AuthKeyMaterial & keys, bool is_initiator) Derive RLPx frame secrets from authenticated handshake key material. |
Functions Documentation¶
function derive_frame_secrets¶
Derive RLPx frame secrets from authenticated handshake key material.
Parameters:
- keys All ECDH, nonce, and wire bytes collected during the handshake.
- is_initiator True for the connection initiator (dialer), false for the responder.
Return: FrameSecrets containing AES/MAC keys and MAC seed byte strings.
Exposed as a free function for unit testing against go-ethereum test vectors.
Source code¶
// Copyright 2025 GeniusVentures
// SPDX-License-Identifier: Apache-2.0
#include <rlpx/auth/auth_handshake.hpp>
#include <rlpx/auth/ecies_cipher.hpp>
#include <rlpx/crypto/ecdh.hpp>
#include <rlpx/crypto/kdf.hpp>
#include <rlpx/crypto/hmac.hpp>
#include <base/rlp-logger.hpp>
#include <rlp/rlp_encoder.hpp>
#include <rlp/rlp_decoder.hpp>
#include <secp256k1.h>
#include <secp256k1_recovery.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <nil/crypto3/hash/algorithm/hash.hpp>
#include <nil/crypto3/hash/keccak.hpp>
#include <nil/crypto3/hash/accumulators/hash.hpp>
#include <cstring>
namespace rlpx::auth {
namespace asio = boost::asio;
namespace {
rlp::base::Logger& auth_log() {
static auto log = rlp::base::createLogger("rlpx.auth");
return log;
}
std::string pubkey_hex(gsl::span<const uint8_t, kPublicKeySize> pubkey)
{
static constexpr char kHex[] = "0123456789abcdef";
std::string out;
out.reserve(kPublicKeySize * 2U);
for (uint8_t b : pubkey)
{
out.push_back(kHex[(b >> 4U) & 0x0FU]);
out.push_back(kHex[b & 0x0FU]);
}
return out;
}
// Create auth message (initiator -> responder)
// Format: 2-byte-len-prefix || ECIES(RLP(authMsgV4) || random_padding)
// Matches go-ethereum sealEIP8 / makeAuthMsg exactly.
AuthResult<ByteBuffer> create_auth_message(
gsl::span<const uint8_t, kPrivateKeySize> local_private_key,
gsl::span<const uint8_t, kPublicKeySize> local_public_key,
const PublicKey& ephemeral_public_key,
const PrivateKey& ephemeral_private_key,
const Nonce& nonce,
gsl::span<const uint8_t, kPublicKeySize> remote_public_key
) noexcept {
// ── 1. static shared secret = ECDH(local_priv, remote_pub) ──
// go-ethereum: staticSharedSecret = GenerateShared(remote, sskLen=16, macLen=16)
// GenerateShared returns x.Bytes() zero-padded to skLen+macLen=32 bytes (raw x-coordinate).
auto* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
if (!ctx) { return AuthError::kSignatureInvalid; }
auto static_ss_result = rlpx::crypto::Ecdh::compute_shared_secret(remote_public_key, local_private_key);
if (!static_ss_result) {
auth_log()->debug("create_auth_message: compute_shared_secret failed (code {})",
static_cast<int>(static_ss_result.error()));
secp256k1_context_destroy(ctx);
return AuthError::kSharedSecretFailed;
}
// token = raw 32-byte x-coordinate (compute_shared_secret already returns x-coord)
const auto& token = static_ss_result.value();
// ── 2. signed = xor(token, initNonce) ──
std::array<uint8_t, kNonceSize> signed_msg;
for (size_t i = 0; i < kNonceSize; ++i) {
signed_msg[i] = token[i] ^ nonce[i];
}
// ── 3. signature = Sign(signed_msg, ephemeral_priv) — recoverable ──
secp256k1_ecdsa_recoverable_signature sig;
if (!secp256k1_ecdsa_sign_recoverable(ctx, &sig, signed_msg.data(),
ephemeral_private_key.data(), nullptr, nullptr)) {
auth_log()->debug("create_auth_message: sign_recoverable failed");
secp256k1_context_destroy(ctx);
return AuthError::kSignatureInvalid;
}
std::array<uint8_t, kEcdsaCompactSigSize> sig_compact;
int recid = 0;
secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, sig_compact.data(), &recid, &sig);
secp256k1_context_destroy(ctx);
// Full 65-byte signature: compact(64) || recid(1)
std::array<uint8_t, kEcdsaSigSize> signature;
std::memcpy(signature.data(), sig_compact.data(), kEcdsaCompactSigSize);
signature[kEcdsaCompactSigSize] = static_cast<uint8_t>(recid);
// ── 4. RLP-encode authMsgV4 ──
// go-ethereum struct: [Signature[65], InitiatorPubkey[64], Nonce[32], Version uint(=4)]
// RLP list of: bytes(65), bytes(64), bytes(32), uint(4)
rlp::RlpEncoder enc;
if (!enc.BeginList()) { return AuthError::kSignatureInvalid; }
if (!enc.add(rlp::ByteView(signature.data(), signature.size()))) { return AuthError::kSignatureInvalid; }
if (!enc.add(rlp::ByteView(local_public_key.data(), local_public_key.size()))) { return AuthError::kSignatureInvalid; }
if (!enc.add(rlp::ByteView(nonce.data(), nonce.size()))) { return AuthError::kSignatureInvalid; }
if (!enc.add(static_cast<uint64_t>(kAuthVersion))) { return AuthError::kSignatureInvalid; }
if (!enc.EndList()) { return AuthError::kSignatureInvalid; }
auto rlp_result = enc.MoveBytes();
if (!rlp_result) { return AuthError::kSignatureInvalid; }
ByteBuffer rlp_body(rlp_result.value().begin(), rlp_result.value().end());
// ── 5. Append fixed random padding (current implementation keeps 100 bytes) ──
{
ByteBuffer padding(kEip8AuthPaddingSize);
RAND_bytes(padding.data(), static_cast<int>(padding.size()));
rlp_body.insert(rlp_body.end(), padding.begin(), padding.end());
}
// ── 6. EIP-8 prefix = uint16_be(len(rlp_body) + eciesOverhead) ──
const auto prefix_val = static_cast<uint16_t>(rlp_body.size() + kEciesOverheadSize);
ByteBuffer prefix = { static_cast<uint8_t>(prefix_val >> 8U),
static_cast<uint8_t>(prefix_val & 0xFFU) };
// ── 7. ECIES encrypt with prefix as shared_mac_data ──
EciesEncryptParams params{
ByteView(rlp_body.data(), rlp_body.size()),
remote_public_key,
ByteView(prefix.data(), prefix.size())
};
auth_log()->debug("create_auth_message: RLP+padding body={} bytes, prefix=0x{:02x}{:02x}",
rlp_body.size(), prefix[0], prefix[1]);
auto ecies_result = EciesCipher::encrypt(params);
if (!ecies_result) {
auth_log()->debug("create_auth_message: EciesCipher::encrypt failed (code {})",
static_cast<int>(ecies_result.error()));
} else {
auth_log()->debug("create_auth_message: ECIES encrypt ok, ciphertext={} bytes", ecies_result.value().size());
}
return ecies_result;
}
// Parse auth message (responder)
AuthResult<AuthKeyMaterial> parse_auth_message(
ByteView encrypted_auth,
gsl::span<const uint8_t, kPrivateKeySize> local_private_key,
ByteView shared_mac_data
) noexcept {
// Decrypt with ECIES
EciesDecryptParams params{
encrypted_auth,
local_private_key,
shared_mac_data
};
auto auth_body_result = EciesCipher::decrypt(params);
if (!auth_body_result) {
return auth_body_result.error();
}
auto auth_body = std::move(auth_body_result.value());
// Parse auth body: signature(kEcdsaSigSize) || eph-pubk-hash(kNonceSize) || pubk(kPublicKeySize) || nonce(kNonceSize) || version(kAuthVersionSize)
if ( auth_body.size() < kEcdsaSigSize + kNonceSize + kPublicKeySize + kNonceSize + kAuthVersionSize ) {
return AuthError::kInvalidAuthMessage;
}
AuthKeyMaterial keys;
// Extract signature
size_t offset = 0;
std::array<uint8_t, kEcdsaSigSize> signature;
std::memcpy(signature.data(), auth_body.data() + offset, kEcdsaSigSize);
offset += kEcdsaSigSize;
// Extract ephemeral public key hash
std::array<uint8_t, kNonceSize> eph_pub_hash;
std::memcpy(eph_pub_hash.data(), auth_body.data() + offset, kNonceSize);
offset += kNonceSize;
// Extract initiator public key
std::memcpy(keys.peer_public_key.data(), auth_body.data() + offset, kPublicKeySize);
offset += kPublicKeySize;
// Extract nonce
std::memcpy(keys.initiator_nonce.data(), auth_body.data() + offset, kNonceSize);
offset += kNonceSize;
// Version byte (currently unused)
// uint8_t version = auth_body[offset];
// Store the auth message for MAC computation later
keys.initiator_auth_message = std::move(auth_body);
return keys;
}
// Create ack message (responder -> initiator)
// Format: ECIES(responder-ephemeral-pubk || responder-nonce || 0x00)
AuthResult<ByteBuffer> create_ack_message(
const PublicKey& ephemeral_public_key,
const Nonce& nonce,
gsl::span<const uint8_t, kPublicKeySize> remote_public_key
) noexcept {
ByteBuffer ack_body;
// ephemeral pubkey(kPublicKeySize) + nonce(kNonceSize) + version(kAuthVersionSize)
ack_body.reserve(kPublicKeySize + kNonceSize + kAuthVersionSize);
// Add ephemeral public key
ack_body.insert(ack_body.end(), ephemeral_public_key.begin(), ephemeral_public_key.end());
// Add nonce
ack_body.insert(ack_body.end(), nonce.begin(), nonce.end());
// Add version byte
ack_body.push_back(kAuthVersion);
// Encrypt with ECIES
EciesEncryptParams params{
ByteView(ack_body.data(), ack_body.size()),
remote_public_key,
ByteView{}
};
return EciesCipher::encrypt(params);
}
// Parse ack message (initiator) — plaintext is RLP-encoded authRespV4 + padding.
// go-ethereum authRespV4: [RandomPubkey[64], Nonce[32], Version uint]
AuthResult<void> parse_ack_message(
ByteView encrypted_ack,
gsl::span<const uint8_t, kPrivateKeySize> local_private_key,
ByteView shared_mac_data,
AuthKeyMaterial& keys
) noexcept {
// Decrypt with ECIES — shared_mac_data is the 2-byte EIP-8 length prefix
EciesDecryptParams params{
encrypted_ack,
local_private_key,
shared_mac_data
};
auto ack_body_result = EciesCipher::decrypt(params);
if (!ack_body_result)
{
return ack_body_result.error();
}
const auto& ack_plain = ack_body_result.value();
// RLP-decode: list header, then RandomPubkey(64 bytes), Nonce(32 bytes), Version
rlp::ByteView view(ack_plain.data(), ack_plain.size());
rlp::RlpDecoder dec(view);
auto list_result = dec.ReadListHeaderBytes();
if (!list_result) { return AuthError::kInvalidAckMessage; }
// RandomPubkey — 64 bytes
rlp::Bytes pubkey_bytes;
if (!dec.read(pubkey_bytes)) { return AuthError::kInvalidAckMessage; }
if (pubkey_bytes.size() != kPublicKeySize) { return AuthError::kInvalidAckMessage; }
std::memcpy(keys.peer_ephemeral_public_key.data(), pubkey_bytes.data(), kPublicKeySize);
// Nonce — 32 bytes
rlp::Bytes nonce_bytes;
if (!dec.read(nonce_bytes)) { return AuthError::kInvalidAckMessage; }
if (nonce_bytes.size() != kNonceSize) { return AuthError::kInvalidAckMessage; }
std::memcpy(keys.recipient_nonce.data(), nonce_bytes.data(), kNonceSize);
// Version and padding are intentionally ignored (forward-compat per EIP-8)
return outcome::success();
}
} // anonymous namespace
AuthHandshake::AuthHandshake(const HandshakeConfig& config,
socket::SocketTransport transport) noexcept
: config_(config)
, transport_(std::move(transport)) {
}
// Note: Uses Boost.Asio stackful coroutines (yield_context) for socket I/O — C++17 compatible.
Result<HandshakeResult> AuthHandshake::execute(asio::yield_context yield) noexcept {
const std::string remote_addr = transport_.remote_address();
const uint16_t remote_port = transport_.remote_port();
const std::string remote_pubkey_hex = config_.peer_public_key.has_value()
? pubkey_hex(config_.peer_public_key.value())
: std::string{};
// Generate ephemeral keypair
auto keypair_result = rlpx::crypto::Ecdh::generate_ephemeral_keypair();
if ( !keypair_result ) {
auth_log()->debug("execute: generate_ephemeral_keypair failed");
return SessionError::kAuthenticationFailed;
}
auto keypair = keypair_result.value();
// Generate nonce
Nonce local_nonce;
if ( RAND_bytes(local_nonce.data(), kNonceSize) != 1 ) {
auth_log()->debug("execute: RAND_bytes failed");
return SessionError::kAuthenticationFailed;
}
HandshakeResult result;
result.key_material.local_ephemeral_public_key = keypair.public_key;
result.key_material.local_ephemeral_private_key = keypair.private_key;
if ( is_initiator() ) {
// ── Initiator: send auth ────────────────────────────────────────────
result.key_material.initiator_nonce = local_nonce;
auth_log()->debug("execute: calling create_auth_message");
auto auth_msg_result = create_auth_message(
config_.local_private_key,
config_.local_public_key,
keypair.public_key,
keypair.private_key,
local_nonce,
config_.peer_public_key.value()
);
if ( !auth_msg_result ) {
auth_log()->debug("execute: create_auth_message failed (code {})",
static_cast<int>(auth_msg_result.error()));
return SessionError::kAuthenticationFailed;
}
auth_log()->debug("execute: auth message built ({} bytes), sending", auth_msg_result.value().size());
// Build full wire auth = prefix(2) || ciphertext
// Store full wire bytes for MAC derivation (go-ethereum uses authPacket in secrets())
const auto& auth_ciphertext = auth_msg_result.value();
const auto prefix_val = static_cast<uint16_t>(auth_ciphertext.size());
ByteBuffer auth_wire;
auth_wire.reserve(kEip8LengthPrefixSize + auth_ciphertext.size());
auth_wire.push_back(static_cast<uint8_t>(prefix_val >> 8U));
auth_wire.push_back(static_cast<uint8_t>(prefix_val & 0xFFU));
auth_wire.insert(auth_wire.end(), auth_ciphertext.begin(), auth_ciphertext.end());
// Store full wire bytes for MAC derivation
result.key_material.initiator_auth_message = auth_wire;
auto send_result = transport_.write_all(
ByteView(auth_wire.data(), auth_wire.size()), yield);
if ( !send_result ) {
auth_log()->debug("execute: write_all(auth) failed");
return SessionError::kAuthenticationFailed;
}
auth_log()->debug("execute: auth sent ({} bytes wire), waiting for ack length prefix", auth_wire.size());
// ── Initiator: receive ack ──────────────────────────────────────────
// EIP-8 ack wire: 2-byte len(ack_ciphertext) || ack_ciphertext
auto len_result = transport_.read_exact(kEip8LengthPrefixSize, yield);
if ( !len_result ) {
auth_log()->debug("execute: peer {}:{} pubkey={} read_exact(ack length prefix) failed",
remote_addr,
remote_port,
remote_pubkey_hex);
return SessionError::kAuthenticationFailed;
}
const auto& len_bytes = len_result.value();
const size_t ack_body_len = (static_cast<size_t>(len_bytes[0]) << 8U)
| static_cast<size_t>(len_bytes[1]);
if (ack_body_len > kMaxEip8HandshakePacketSize) {
auth_log()->debug("execute: peer {}:{} pubkey={} ack length {} exceeds EIP-8 max {}",
remote_addr,
remote_port,
remote_pubkey_hex,
ack_body_len,
kMaxEip8HandshakePacketSize);
return SessionError::kAuthenticationFailed;
}
auth_log()->debug("execute: ack length prefix received, ack_body_len={}", ack_body_len);
auto ack_result = transport_.read_exact(ack_body_len, yield);
if ( !ack_result ) {
auth_log()->debug("execute: read_exact(ack body) failed");
return SessionError::kAuthenticationFailed;
}
auth_log()->debug("execute: ack body received ({} bytes), parsing", ack_body_len);
// Store full wire ack = len_prefix(2) || ciphertext for MAC derivation
// go-ethereum uses authRespPacket (full wire bytes) in secrets()
ByteBuffer ack_wire_full;
ack_wire_full.reserve(len_bytes.size() + ack_result.value().size());
ack_wire_full.insert(ack_wire_full.end(), len_bytes.begin(), len_bytes.end());
ack_wire_full.insert(ack_wire_full.end(), ack_result.value().begin(), ack_result.value().end());
result.key_material.recipient_ack_message = ack_wire_full;
// Pass the 2-byte ack prefix as shared_mac_data so ECIES MAC matches
auto parse_result = parse_ack_message(
ByteView(ack_result.value().data(), ack_result.value().size()),
config_.local_private_key,
len_bytes,
result.key_material);
if ( !parse_result ) {
auth_log()->debug("execute: parse_ack_message failed (code {})",
static_cast<int>(parse_result.error()));
return SessionError::kAuthenticationFailed;
}
auth_log()->debug("execute: ack parsed successfully");
result.key_material.peer_public_key = config_.peer_public_key.value();
}
else
{
// ── Responder: receive auth ─────────────────────────────────────────
result.key_material.recipient_nonce = local_nonce;
// Read 2-byte length prefix
auto len_result = transport_.read_exact(kEip8LengthPrefixSize, yield);
if ( !len_result ) {
return SessionError::kAuthenticationFailed;
}
const auto& len_bytes = len_result.value();
const size_t auth_len = (static_cast<size_t>(len_bytes[0]) << 8U)
| static_cast<size_t>(len_bytes[1]);
if (auth_len > kMaxEip8HandshakePacketSize) {
auth_log()->debug("execute: peer {}:{} pubkey={} auth length {} exceeds EIP-8 max {}",
remote_addr,
remote_port,
remote_pubkey_hex,
auth_len,
kMaxEip8HandshakePacketSize);
return SessionError::kAuthenticationFailed;
}
auto auth_result = transport_.read_exact(auth_len, yield);
if ( !auth_result ) {
return SessionError::kAuthenticationFailed;
}
auto parse_result = parse_auth_message(
ByteView(auth_result.value().data(), auth_result.value().size()),
config_.local_private_key,
len_bytes);
if ( !parse_result ) {
return SessionError::kAuthenticationFailed;
}
result.key_material = parse_result.value();
result.key_material.initiator_auth_message = auth_result.value();
result.key_material.local_ephemeral_public_key = keypair.public_key;
result.key_material.local_ephemeral_private_key = keypair.private_key;
result.key_material.recipient_nonce = local_nonce;
// ── Responder: send ack ─────────────────────────────────────────────
auto ack_msg_result = create_ack_message(
keypair.public_key,
local_nonce,
result.key_material.peer_public_key
);
if ( !ack_msg_result ) {
return SessionError::kAuthenticationFailed;
}
result.key_material.recipient_ack_message = ack_msg_result.value();
const auto& ack_bytes = result.key_material.recipient_ack_message;
const auto ack_len = static_cast<uint16_t>(ack_bytes.size());
ByteBuffer ack_wire;
ack_wire.reserve(kEip8LengthPrefixSize + ack_bytes.size());
ack_wire.push_back(static_cast<uint8_t>(ack_len >> 8U));
ack_wire.push_back(static_cast<uint8_t>(ack_len & 0xFFU));
ack_wire.insert(ack_wire.end(), ack_bytes.begin(), ack_bytes.end());
auto send_result = transport_.write_all(
ByteView(ack_wire.data(), ack_wire.size()), yield);
if ( !send_result ) {
return SessionError::kAuthenticationFailed;
}
}
// ── Derive frame secrets ────────────────────────────────────────────────
result.frame_secrets = derive_frame_secrets(result.key_material, is_initiator());
// ── Hand transport back to caller via HandshakeResult ───────────────────
result.transport = std::move(transport_);
return result;
}
AuthResult<AuthKeyMaterial> AuthHandshake::perform_auth(asio::yield_context /*yield*/) noexcept {
// This method would contain the core auth logic
// Currently integrated into execute() above
return AuthError::kInvalidAuthMessage; // Placeholder
}
Result<void> AuthHandshake::exchange_hello(
ByteView aes_key,
ByteView mac_key,
asio::yield_context /*yield*/
) noexcept {
(void)aes_key;
(void)mac_key;
// This would perform the Hello message exchange
// Placeholder for now
return SessionError::kHandshakeFailed;
}
FrameSecrets AuthHandshake::derive_frame_secrets(
const AuthKeyMaterial& keys,
bool is_initiator
) noexcept {
FrameSecrets secrets;
// ── Step 1: ECDH between local ephemeral private key and peer ephemeral public key ──
// go-ethereum: ecdheSecret = randomPrivKey.GenerateShared(remoteRandomPub, 16, 16)
// Returns raw x-coordinate (32 bytes), same as our compute_shared_secret.
auto ecdhe_result = rlpx::crypto::Ecdh::compute_shared_secret(
keys.peer_ephemeral_public_key,
keys.local_ephemeral_private_key
);
if (!ecdhe_result) { return secrets; }
const auto& ecdhe_secret = ecdhe_result.value(); // 32-byte x-coordinate
// ── Step 2: sharedSecret = keccak256(ecdheSecret || keccak256(respNonce || initNonce)) ──
// go-ethereum: keccak256(h.respNonce, h.initNonce)
std::array<uint8_t, kNonceSize> nonce_hash{};
{
using Hasher = nil::crypto3::hashes::keccak_1600<256>;
nil::crypto3::accumulator_set<Hasher> acc;
nil::crypto3::hash<Hasher>(keys.recipient_nonce.begin(), keys.recipient_nonce.end(), acc);
nil::crypto3::hash<Hasher>(keys.initiator_nonce.begin(), keys.initiator_nonce.end(), acc);
auto digest = nil::crypto3::accumulators::extract::hash<Hasher>(acc);
std::copy(digest.begin(), digest.end(), nonce_hash.begin());
}
std::array<uint8_t, kNonceSize> shared_secret{};
{
using Hasher = nil::crypto3::hashes::keccak_1600<256>;
nil::crypto3::accumulator_set<Hasher> acc;
nil::crypto3::hash<Hasher>(ecdhe_secret.begin(), ecdhe_secret.end(), acc);
nil::crypto3::hash<Hasher>(nonce_hash.begin(), nonce_hash.end(), acc);
auto digest = nil::crypto3::accumulators::extract::hash<Hasher>(acc);
std::copy(digest.begin(), digest.end(), shared_secret.begin());
}
// ── Step 3: aesSecret = keccak256(ecdheSecret || sharedSecret) ──
std::array<uint8_t, kNonceSize> aes_secret{};
{
using Hasher = nil::crypto3::hashes::keccak_1600<256>;
nil::crypto3::accumulator_set<Hasher> acc;
nil::crypto3::hash<Hasher>(ecdhe_secret.begin(), ecdhe_secret.end(), acc);
nil::crypto3::hash<Hasher>(shared_secret.begin(), shared_secret.end(), acc);
auto digest = nil::crypto3::accumulators::extract::hash<Hasher>(acc);
std::copy(digest.begin(), digest.end(), aes_secret.begin());
}
std::copy(aes_secret.begin(), aes_secret.end(), secrets.aes_secret.begin());
// ── Step 4: mac_secret = keccak256(ecdheSecret || aesSecret) ──
std::array<uint8_t, kNonceSize> mac_secret{};
{
using Hasher = nil::crypto3::hashes::keccak_1600<256>;
nil::crypto3::accumulator_set<Hasher> acc;
nil::crypto3::hash<Hasher>(ecdhe_secret.begin(), ecdhe_secret.end(), acc);
nil::crypto3::hash<Hasher>(aes_secret.begin(), aes_secret.end(), acc);
auto digest = nil::crypto3::accumulators::extract::hash<Hasher>(acc);
std::copy(digest.begin(), digest.end(), mac_secret.begin());
}
std::copy(mac_secret.begin(), mac_secret.end(), secrets.mac_secret.begin());
// ── Step 5: MAC seeds ──
// go-ethereum:
// mac1.Write(xor(MAC, respNonce)); mac1.Write(auth) // = egress for initiator
// mac2.Write(xor(MAC, initNonce)); mac2.Write(authResp) // = ingress for initiator
// We store the full running-keccak state as a seed byte string.
auto mac_seed_bytes = [&](const std::array<uint8_t, kNonceSize>& nonce,
const ByteBuffer& msg) -> ByteBuffer
{
// xor_val = mac_secret XOR nonce
std::array<uint8_t, kNonceSize> xor_val{};
for (size_t i = 0; i < kNonceSize; ++i)
{
xor_val[i] = mac_secret[i] ^ nonce[i];
}
// Return raw bytes: xor_val || msg
// go-ethereum: mac.Write(xor_val); mac.Write(msg)
ByteBuffer seed;
seed.reserve(kNonceSize + msg.size());
seed.insert(seed.end(), xor_val.begin(), xor_val.end());
seed.insert(seed.end(), msg.begin(), msg.end());
return seed;
};
if (is_initiator)
{
// egress MAC: mac1.Write(xor(mac,respNonce)); mac1.Write(auth)
secrets.egress_mac_seed = mac_seed_bytes(keys.recipient_nonce,
keys.initiator_auth_message);
// ingress MAC: mac2.Write(xor(mac,initNonce)); mac2.Write(authResp)
secrets.ingress_mac_seed = mac_seed_bytes(keys.initiator_nonce,
keys.recipient_ack_message);
}
else
{
// Responder: roles swapped
secrets.ingress_mac_seed = mac_seed_bytes(keys.recipient_nonce,
keys.initiator_auth_message);
secrets.egress_mac_seed = mac_seed_bytes(keys.initiator_nonce,
keys.recipient_ack_message);
}
auth_log()->debug("derive_frame_secrets: aes_secret[0..3]={:02x}{:02x}{:02x}{:02x}",
secrets.aes_secret[0], secrets.aes_secret[1],
secrets.aes_secret[2], secrets.aes_secret[3]);
auth_log()->debug("derive_frame_secrets: mac_secret[0..3]={:02x}{:02x}{:02x}{:02x}",
secrets.mac_secret[0], secrets.mac_secret[1],
secrets.mac_secret[2], secrets.mac_secret[3]);
auth_log()->debug("derive_frame_secrets: egress_seed[0..3]={:02x}{:02x}{:02x}{:02x} len={}",
secrets.egress_mac_seed[0], secrets.egress_mac_seed[1],
secrets.egress_mac_seed[2], secrets.egress_mac_seed[3],
secrets.egress_mac_seed.size());
auth_log()->debug("derive_frame_secrets: ingress_seed[0..3]={:02x}{:02x}{:02x}{:02x} len={}",
secrets.ingress_mac_seed[0], secrets.ingress_mac_seed[1],
secrets.ingress_mac_seed[2], secrets.ingress_mac_seed[3],
secrets.ingress_mac_seed.size());
auth_log()->debug("derive_frame_secrets: auth_wire len={}, ack_wire len={}",
keys.initiator_auth_message.size(), keys.recipient_ack_message.size());
return secrets;
}
FrameSecrets derive_frame_secrets(const AuthKeyMaterial& keys, bool is_initiator) noexcept
{
return AuthHandshake::derive_frame_secrets(keys, is_initiator);
}
} // namespace rlpx::auth
Updated on 2026-04-13 at 23:22:46 -0700