Peer Handshake Protocol

Introduction

When two rippled nodes connect over the internet, they can't trust each other initially. How does node A know that the node claiming to be B really controls B's private key? How do they prevent man-in-the-middle attacks? How do they avoid accidentally connecting to themselves?

The peer handshake protocol solves all these problems through careful cryptographic design. This chapter explores how XRPL nodes establish secure, authenticated connections.

The Challenge

What We Need to Prove

Node A                                    Node B
  |                                         |
  |  "I am Node A with public key PK_A"    |
  |  "Prove you have secret key SK_A"      |
  |────────────────────────────────────────>|
  |                                         |
  |  "I am Node B with public key PK_B"    |
  |  "Prove you have secret key SK_B"      |
  |<────────────────────────────────────────|
  |                                         |
  |  Both nodes must prove:                |
  |  1. Identity (I own this key)          |
  |  2. Liveness (I'm here NOW, not replay)|
  |  3. Session binding (THIS connection)  |

Attack Scenarios to Prevent

1. Man-in-the-Middle (MITM)

Node A ──┐         ┌── Node B
         │         │
         └─> Evil <┘

Evil intercepts and relays messages
A thinks it's talking to B
B thinks it's talking to A

2. Replay Attack

Evil records handshake messages from previous session
Replays them to impersonate Node A

3. Self-Connection

Node A tries to connect to itself through network loop
Could cause infinite recursion/waste resources

4. Network Mismatch

Mainnet node connects to testnet node
Could cause confusion/invalid transactions

The Solution: Cryptographic Handshake

High-Level Flow

1. SSL/TLS Connection Established
   ├─ Provides encryption (confidentiality)
   ├─ Provides basic authentication
   └─ Creates shared session state

2. Extract Shared Value from SSL Session
   ├─ Unique to THIS specific SSL connection
   ├─ Both nodes can compute it independently
   └─ Cannot be predicted beforehand

3. Sign the Shared Value
   ├─ Node A signs with SK_A
   ├─ Node B signs with SK_B
   └─ Proves possession of private keys

4. Exchange Signatures in HTTP Headers
   ├─ Verify each other's signatures
   ├─ Check network IDs match
   └─ Prevent self-connection

5. Connection Authenticated!

The Shared Value: Session Binding

Why We Need It

Signatures alone aren't enough:

// ❌ INSECURE: Sign static message
auto sig = sign(pk, sk, "I am Node A");
// Problem: Can be replayed in future connections!

We need something unique to THIS specific connection:

// ✅ SECURE: Sign session-specific value
auto sharedValue = deriveFromSSL(session);
auto sig = sign(pk, sk, sharedValue);
// Can only be used for THIS session

Implementation

// From src/xrpld/overlay/detail/Handshake.cpp

std::optional<uint256>
makeSharedValue(stream_type& ssl, beast::Journal journal)
{
    // Get our "Finished" message from SSL handshake
    auto const cookie1 = hashLastMessage(
        ssl.native_handle(),
        SSL_get_finished);

    // Get peer's "Finished" message from SSL handshake
    auto const cookie2 = hashLastMessage(
        ssl.native_handle(),
        SSL_get_peer_finished);

    if (!cookie1 || !cookie2)
        return std::nullopt;

    // XOR the two hashes together
    auto const result = (*cookie1 ^ *cookie2);

    // Ensure they're not identical (would result in zero)
    if (result == beast::zero)
    {
        JLOG(journal.error()) << "Identical finished messages";
        return std::nullopt;
    }

    // Hash the XOR result to get final shared value
    return sha512Half(Slice(result.data(), result.size()));
}

Hashing the SSL Finished Messages

static std::optional<base_uint<512>>
hashLastMessage(
    SSL const* ssl,
    size_t (*get)(const SSL*, void*, size_t))
{
    // Buffer for SSL finished message
    unsigned char buf[1024];
    size_t len = get(ssl, buf, sizeof(buf));

    if (len < 12)  // Minimum valid length
        return std::nullopt;

    // Hash it with SHA-512
    base_uint<512> cookie;
    SHA512(buf, len, cookie.data());
    return cookie;
}

What are "Finished" messages?

In the SSL/TLS handshake, both parties send a "Finished" message that contains:

  • A hash of all previous handshake messages

  • A MAC (Message Authentication Code) proving they know the session keys

These messages are:

  • Unique per session: Different for every SSL connection

  • Unpredictable: Depend on random values exchanged during handshake

  • Authenticated: Part of SSL's own security

Properties of the Shared Value

Properties:
1. Session-specific:  Different for every connection
2. Unpredictable:     Cannot be known before handshake completes
3. Mutual:            Both nodes contribute (via XOR)
4. Verifiable:        Both nodes can compute independently
5. Binding:           Tied to THIS specific SSL session

Building the Handshake

// From src/xrpld/overlay/detail/Handshake.cpp

void buildHandshake(
    boost::beast::http::fields& h,
    ripple::uint256 const& sharedValue,
    std::optional<std::uint32_t> networkID,
    beast::IP::Address public_ip,
    beast::IP::Address remote_ip,
    Application& app)
{
    // 1. Network identification
    if (networkID)
        h.insert("Network-ID", std::to_string(*networkID));

    // 2. Timestamp (freshness, prevent replay)
    h.insert("Network-Time",
        std::to_string(app.timeKeeper().now().time_since_epoch().count()));

    // 3. Node's public key
    h.insert("Public-Key",
        toBase58(TokenType::NodePublic, app.nodeIdentity().first));

    // 4. CRITICAL: Session signature
    auto const sig = signDigest(
        app.nodeIdentity().first,   // Public key
        app.nodeIdentity().second,  // Secret key
        sharedValue);                // Session-specific value

    h.insert("Session-Signature", base64_encode(sig));

    // 5. Instance cookie (prevent self-connection)
    h.insert("Instance-Cookie",
        std::to_string(app.getInstanceCookie()));

    // 6. Optional: Server domain
    auto const domain = app.config().SERVER_DOMAIN;
    if (!domain.empty())
        h.insert("Server-Domain", domain);

    // 7. Ledger information
    if (auto closed = app.getLedgerMaster().getClosedLedger())
        h.insert("Closed-Ledger", to_string(closed->info().hash));
}

Header Fields Explained

Network-ID:

// Mainnet: 0
// Testnet: 1
// Devnet:  2, etc.

// Prevents nodes from different networks connecting

Network-Time:

// Current time in milliseconds since epoch
// Helps detect replayed handshakes (timestamps too old)
// Not strictly enforced (clocks may be slightly off)

Public-Key:

// Node's public key in Base58 format
// Example: nHUpcmNsxAw47yt2ADDoNoQrzLyTJPgnyq5o3xTmMcgV8X3iVVa7
// Used to verify the signature

Session-Signature:

// Signature of the shared value
// Proves: "I have the secret key for this public key"
//     AND "I'm participating in THIS specific SSL session"

Instance-Cookie:

// Random value generated on node startup
// If we receive our own cookie back → we're connecting to ourselves!

Server-Domain (optional):

// Domain name like "ripple.com"
// Can be verified against validator list
// Helps with node identification

Verifying the Handshake

std::optional<PublicKey>
verifyHandshake(
    http_request_type const& request,
    uint256 const& sharedValue,
    std::optional<std::uint32_t> networkID,
    uint64_t instanceCookie,
    beast::Journal journal)
{
    // 1. Extract and parse public key
    auto const pkStr = request["Public-Key"];
    auto const pk = parseBase58<PublicKey>(
        TokenType::NodePublic,
        pkStr);

    if (!pk)
    {
        JLOG(journal.warn()) << "Invalid public key";
        return std::nullopt;
    }

    // 2. Check network ID matches
    if (networkID)
    {
        auto const theirNetworkID = request["Network-ID"];
        if (theirNetworkID.empty() ||
            std::to_string(*networkID) != theirNetworkID)
        {
            JLOG(journal.warn()) << "Network ID mismatch";
            return std::nullopt;
        }
    }

    // 3. Check for self-connection
    auto const theirCookie = request["Instance-Cookie"];
    if (theirCookie == std::to_string(instanceCookie))
    {
        JLOG(journal.warn()) << "Detected self-connection";
        return std::nullopt;
    }

    // 4. Verify session signature
    auto const sigStr = request["Session-Signature"];
    auto const sig = base64_decode(sigStr);

    if (!verifyDigest(*pk, sharedValue, sig, true))
    {
        JLOG(journal.warn()) << "Invalid session signature";
        return std::nullopt;
    }

    // 5. Optional: Validate server domain
    auto const domain = request["Server-Domain"];
    if (!domain.empty() && !isProperlyFormedTomlDomain(domain))
    {
        JLOG(journal.warn()) << "Invalid server domain";
        return std::nullopt;
    }

    // Success! Return authenticated public key
    JLOG(journal.info()) << "Handshake verified for " << toBase58(*pk);
    return pk;
}

Complete Handshake Flow

Node A                                                Node B
  |                                                     |
  |  1. TCP connection established                     |
  |<--------------------------------------------------->|
  |                                                     |
  |  2. SSL/TLS handshake                              |
  |     (Both send "Finished" messages)                |
  |<--------------------------------------------------->|
  |                                                     |
  |  3. Both compute shared value                      |
  |     shared = sha512Half(finishedA XOR finishedB)   |
  |                                                     |
  |  4. HTTP Upgrade Request                           |
  |     Headers:                                        |
  |       Public-Key: PK_A                             |
  |       Session-Signature: sign(SK_A, shared)        |
  |       Network-ID: 0                                |
  |       Instance-Cookie: COOKIE_A                    |
  |---------------------------------------------------->|
  |                                                     |
  |                                     5. Node B:      |
  |                                        - Verify PK_A|
  |                                        - Check sig  |
  |                                        - Check net  |
  |                                        - Check cookie|
  |                                                     |
  |  6. HTTP Upgrade Response                          |
  |     Headers:                                        |
  |       Public-Key: PK_B                             |
  |       Session-Signature: sign(SK_B, shared)        |
  |       Network-ID: 0                                |
  |       Instance-Cookie: COOKIE_B                    |
  |<----------------------------------------------------|
  |                                                     |
  |  7. Node A verifies Node B                         |
  |                                                     |
  |  8. Begin XRPL protocol                            |
  |<--------------------------------------------------->|

Security Properties

1. Mutual Authentication

Both nodes prove they possess their private keys:

Node A proves: "I have SK_A"
Node B proves: "I have SK_B"

2. Session Binding

Signatures are specific to this connection:

Signature valid ONLY for THIS SSL session
Cannot be replayed in different session

3. Replay Prevention

sharedValue = derived from THIS session's SSL handshake
Old signatures from previous sessions won't verify

4. MITM Prevention

Attacker cannot forge signatures without private keys
SSL provides encryption, handshake provides authentication

5. Self-Connection Prevention

if (theirCookie == myCookie) {
    // We're talking to ourselves!
    reject();
}

6. Network Segregation

if (theirNetwork != myNetwork) {
    // Different networks (mainnet vs testnet)
    reject();
}

Attack Analysis

Can an attacker impersonate Node A?

No:

Attacker needs to:
1. Know Node A's secret key (impossible - properly secured)
2. Sign the shared value (requires secret key)
Without SK_A, cannot create valid signature

Can an attacker replay old handshakes?

No:

Shared value is different for each SSL session
Old signature: sign(SK, oldSharedValue)
New session:   verify(PK, newSharedValue, oldSignature)
Result: Verification fails (different shared values)

Can an attacker perform MITM?

Very difficult:

SSL provides:
- Encryption (attacker can't read/modify)
- Certificate validation (can detect impersonation)

Application handshake provides:
- Signature verification (requires private keys)
- Session binding (tied to SSL session)

Attacker would need to:
1. Break SSL (extremely difficult)
2. AND forge signatures (impossible without keys)

Implementation Best Practices

✅ DO:

// 1. Always verify the shared value
auto sharedValue = makeSharedValue(ssl, journal);
if (!sharedValue) {
    disconnect("Failed to create shared value");
}

// 2. Always require canonical signatures
if (!verifyDigest(pk, sharedValue, sig, true)) {
    disconnect("Invalid signature");
}

// 3. Always check network ID
if (theirNetwork != myNetwork) {
    disconnect("Network mismatch");
}

// 4. Always check instance cookie
if (theirCookie == myCookie) {
    disconnect("Self-connection detected");
}

❌ DON'T:

// ❌ Don't skip signature verification
if (config.TRUSTED_NODE) {
    // Skip verification - WRONG!
}

// ❌ Don't ignore network ID
// connect();  // Oops, might be wrong network

// ❌ Don't allow self-connections
// They waste resources and can cause issues

Performance Considerations

// Handshake happens once per connection
// Not a performance bottleneck

Typical handshake time:
- SSL/TLS handshake:      50-100ms
- Shared value computation: <1ms
- Signature creation:      <1ms
- Signature verification:  <1ms
Total:                    ~50-100ms

// Amortized over connection lifetime (hours/days)
// Cost is negligible

Summary

The peer handshake protocol provides:

  1. Mutual authentication: Both nodes prove identity

  2. Session binding: Signatures tied to specific SSL session

  3. Replay prevention: Old signatures don't work

  4. MITM protection: Requires private keys to forge

  5. Self-connection prevention: Instance cookies detect loops

  6. Network segregation: Network IDs prevent cross-network connections

Key components:

  • Shared value: Derived from SSL session, unique per connection

  • Signatures: Prove possession of private keys

  • Headers: Exchange identities and verification data

  • Validation: Multiple checks ensure security

This cryptographic handshake enables XRPL to operate as a decentralized network where nodes don't need to trust each other—they can verify everything cryptographically.

Last updated