πŸ›οΈ The Byzantine Problem: When You Can't Trust Your Own System

The name comes from the
Byzantine Empire’s complex
military structure, where
generals communicated
via unreliable messengers.
The paper was published
by Lamport, Shostak,
and Pease in 1982.

In 1982, Leslie Lamport posed a deceptively simple puzzle:

Four generals surround a city. To succeed they must all attack at the same moment, or all retreat. They can only communicate by messenger. One of the generals is a traitor. Can they still reach agreement?

This became one of the most important problems in distributed systems β€” and one of the most important motivations for cryptography in software. The answer is: yes, but only if the messages are signed.

This is not a theoretical curiosity. It is the exact problem your web server faces every time it processes a session token, a JWT, or a signed cookie. The traitor is not necessarily a general β€” it might be an attacker who has compromised one node, a tampered packet, or a man-in-the-middle sitting between your load balancer and your auth service.


The Generals Problem, Visualised

With 3f + 1 nodes you
can tolerate f Byzantine
traitors. So with 4 nodes
you survive 1 traitor.
This is the minimum bound
proven by Lamport et al.

Before code, build intuition. The simulation below lets you run the two-round broadcast protocol with and without digital signatures. Toggle the Commander as traitor and watch what happens:

Press "Next step" to begin the simulation.

The key difference: without signatures, the traitor can tell each general a different story and no one can prove it. With digital signatures, the traitor’s contradiction becomes cryptographic evidence β€” a general who claims to have received an order can attach the commander’s signature, and everyone can verify it.


Why This Maps Exactly to Web Security

Byzantine failures in
web systems: any node
(server, client, proxy)
that sends conflicting
or forged messages
to different parties
to cause inconsistency.

The generals are your servers. The messengers are HTTP requests. The traitor is an attacker (or a compromised node). Here are the real-world manifestations:

🔑

JWT Algorithm Confusion

Attacker changes the header from alg: RS256 to alg: none, drops the signature, and the server accepts it β€” because it never checked. The traitor general claiming "I have the commander's seal" on a blank document.
auth bypass
🍪

Session Cookie Tampering

A cookie stores role=user without a signature or HMAC. Attacker edits it to role=admin. Server trusts it. The traitor relaying a forged order with no way to verify its origin.
privilege esc.
📋

MITM on API Calls

A proxy between your frontend and auth service intercepts and modifies messages. Without mutual TLS or signed payloads, neither side can tell. A traitor intercepting messages between two loyal generals.
interception

The JWT Attack β€” Live Demo

The alg: none vulnerability
was discovered in 2015 and
affected dozens of JWT
libraries. Never trust
the algorithm declared
in the header β€” always
enforce it server-side.

A JWT has three base64url-encoded parts: header.payload.signature. Edit the payload below to see when the signature becomes invalid:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . ... . ...
Change the payload. Signature will fail unless you also know the secret.
✓ Signature valid

The signature is an HMAC-SHA256 over base64(header) + "." + base64(payload) using the secret. Change a single byte of the payload and the signature no longer matches. That is cryptographic integrity: the loyal general’s message cannot be tampered with in transit.


The Real Code: PHP

❌ Vulnerable β€” unsigned session data

PHP — dangerous pattern
// ❌ The role is stored in a cookie without any integrity check.
// An attacker edits the cookie in their browser: role=admin
setcookie('user', json_encode([
    'id'   => 42,
    'role' => 'user',
]), time() + 3600);

// Later: reading it back β€” blindly trusted
$data = json_decode($_COOKIE['user'], true);
if ($data['role'] === 'admin') {
    // 🚨 attacker is now admin
}
PHP — HMAC integrity check
define('COOKIE_SECRET', getenv('COOKIE_SECRET'));  // 32+ byte random key

function sign_cookie(array $data): string
{
    $payload = base64_encode(json_encode($data));
    $mac     = hash_hmac('sha256', $payload, COOKIE_SECRET);
    return $payload . '.' . $mac;
}

function verify_cookie(string $cookie): ?array
{
    $parts = explode('.', $cookie, 2);
    if (count($parts) !== 2) return null;

    [$payload, $mac] = $parts;
    $expected = hash_hmac('sha256', $payload, COOKIE_SECRET);

    // βœ… constant-time comparison prevents timing attacks
    if (!hash_equals($expected, $mac)) {
        return null;  // tampered β€” reject
    }

    return json_decode(base64_decode($payload), true);
}

// Usage
setcookie('user', sign_cookie(['id' => 42, 'role' => 'user']), ...);

$data = verify_cookie($_COOKIE['user'] ?? '');
if ($data === null) {
    // tampered or missing β€” force re-login
    header('Location: /login');
    exit;
}

❌ Vulnerable β€” JWT with alg: none

PHP — the alg:none trap
// ❌ Trusting the algorithm from the token's own header
$header = json_decode(base64_decode($parts[0]), true);
$alg    = $header['alg'];  // attacker sends "none"

if ($alg === 'none') {
    // skips verification entirely β€” 🚨 auth bypass
    return json_decode(base64_decode($parts[1]), true);
}

βœ… Secure β€” enforce the algorithm server-side

PHP — safe JWT verification
function verify_jwt(string $token, string $secret): ?array
{
    $parts = explode('.', $token);
    if (count($parts) !== 3) return null;

    [$b64h, $b64p, $b64s] = $parts;

    // βœ… Never read alg from the token β€” always enforce HS256
    $expected_sig = hash_hmac(
        'sha256',
        $b64h . '.' . $b64p,
        $secret,
        binary: true
    );

    $provided_sig = base64_decode(
        strtr($b64s, '-_', '+/')
    );

    // βœ… Constant-time compare
    if (!hash_equals($expected_sig, $provided_sig)) {
        return null;
    }

    $payload = json_decode(base64_decode(
        strtr($b64p, '-_', '+/')
    ), true);

    // βœ… Check expiry
    if (($payload['exp'] ?? 0) < time()) {
        return null;
    }

    return $payload;
}

Timing Attacks: The Subtle Byzantine

A timing attack is a
Byzantine failure where the
β€œtraitor” is time itself.
If your MAC comparison
short-circuits on the
first wrong byte, an
attacker can infer the
correct MAC byte-by-byte
in ~256 Γ— n requests.

There is one more Byzantine failure that pure logic cannot fix: the timing attack. A naive string comparison like $a === $b short-circuits the moment it finds the first differing byte. An attacker can measure response times to infer how many leading bytes of their forged MAC are correct.

The fix is hash_equals() in PHP β€” which always compares every byte regardless of where they first differ. This is called constant-time comparison and it is mandatory for any MAC or signature check.

PHP — timing safe vs unsafe
// ❌ Leaks timing information
if ($provided_mac === $expected_mac) { ... }

// ❌ Also leaks: early exit on mismatch
if (strcmp($provided_mac, $expected_mac) === 0) { ... }

// βœ… Always runs in the same time regardless of input
if (hash_equals($expected_mac, $provided_mac)) { ... }

// Note: argument order matters for hash_equals β€” known value first!

The Full Checklist

  • Sign every cookie that carries authoritative data. Use hash_hmac('sha256', $payload, $secret) and compare with hash_equals().
  • Never read alg from a JWT header. Enforce the algorithm on the server side β€” always.
  • Use hash_equals() for every MAC, signature, or token comparison. Never === or strcmp.
  • Store secrets in environment variables or a vault. Never hardcode them. Never commit them. Never log them.
  • Validate JWT exp, iss, and aud claims explicitly. Signed does not mean authorised.
  • Use HTTPS everywhere β€” TLS ensures the channel, but it does not replace payload signing (the channel can be terminated at a proxy).
  • For distributed systems: require signatures on inter-service messages. A compromised internal node should not be trusted just because it is inside the VPC.

The Insight That Lamport Gave Us

In blockchain, PBFT
(Practical BFT) and its
descendants (Tendermint,
HotStuff) are what make
validators agree on
a chain despite traitors.
It all traces back to
Lamport’s 1982 paper.

The Byzantine Generals Problem sounds ancient and abstract. But its resolution β€” you cannot achieve reliable consensus without authenticated messages β€” is the foundation of every security primitive in use today:

  • HMAC says: β€œonly someone with the shared secret can produce this signature”
  • RSA/ECDSA says: β€œonly someone with the private key can produce this signature”
  • TLS says: β€œonly the certificate’s true owner could have completed this handshake”
  • JWT verification says: β€œonly the auth server that issued this token could have signed it”

Every time you call hash_equals() instead of ===, every time you enforce alg: HS256 instead of trusting the header, you are implementing the lesson from 1982: a message without a verifiable signature is worthless in an adversarial environment.

The traitor general cannot lie if every message carries a proof that only a loyal general could have created.


Further reading: Lamport, Shostak, Pease β€” Byzantine Generals (1982), OWASP JWT Cheat Sheet, PHP hash_equals.