The Network · Chapter 13.a
How do two strangers share a secret?
- The browser
- The network
- Software and languages
- The CPU
- Memory
- Arithmetic
- Logic gates
- The switch
Every message so far has crossed the network in the clear. The request of Chapter 13 went out as plain bytes, and anyone sitting on the wire between the two machines could read it. A secure channel must protect the bytes and establish who is at the other end. We will first assume a passive observer who can watch messages but cannot replace them, and model agreement on a shared secret followed by record encoding. Then we will remove that assumption and see why authentication is essential. The tiny arithmetic and cipher below demonstrate operations; they provide no real security, even against a passive observer.
13.a.1 Agreeing on a secret in the open
The first move sounds impossible: two strangers publish values yet agree on a number they never send. Diffie-Hellman uses powers with a remainder operation, called modular arithmetic. For example, 17 mod 5=2: divide 17 by 5 and keep the remainder. Working modulo a prime p keeps every result between 0 and p-1, even when the unreduced power is enormous.
Both sides publicly agree on a base g and a prime modulus p. Our example uses g=5 and p=2147483647. Alice keeps private exponent a=6; Bob keeps b=15. Their public values are A=ga mod p=15625, B=gb mod p=452807067. Bob’s ordinary power 515 is 30517578125; dividing by p and keeping the remainder gives the value he sends. Alice then computes Ba mod p, while Bob computes Ab mod p. Both reduce to the same value: Ba mod p=Ab mod p=gab mod p=1893334806. Reducing intermediate results modulo p preserves this equality. That explains agreement. Secrecy is a separate computational claim: with suitable large groups and private exponents, deriving this result from the public values is believed to be infeasible. Our small constants do not meet that requirement.1 Figure 13.a.2 traces the example.
Python’s three-argument pow(g, a, p) computes ga mod p without constructing the enormous ordinary power. In the code, _BASE and _PRIME hold the public g and p above.
Example 13.a-1. Publishing a value, then reaching the shared secret
def public_value(private: int) -> int: """Publish base**private modulo the shared prime.""" return pow(_BASE, private, _PRIME)
def shared_secret(their_public: int, my_private: int) -> int: """Raise the received value to my private exponent, modulo the prime. The result agrees with the other endpoint's, but authenticates neither peer. """ return pow(their_public, my_private, _PRIME)1
- raising the received public value to my private exponent, modulo the prime, reaches the common secret without sending it
13.a.2 Who sent that public value?
Agreement alone does not identify the other endpoint. Suppose an intermediary replaces both public values with its own, computed from private exponent 9. Alice derives 1493771129; Bob derives 849309108. The intermediary derives Alice’s key from Alice’s public value and its own 9, and derives Bob’s key the same way. It can read a message from Alice, encrypt it again with Bob’s key, and let Bob accept it. Neither endpoint detects that it agreed with the intermediary. This attack does not require recovering either private exponent.
An authenticated exchange binds the handshake to the intended peer. In a typical web TLS connection, the client validates a certificate for the expected server name and verifies a handshake signature made with the corresponding private key. A pre-shared key is another way to authenticate a peer and the handshake. TLS also checks the handshake transcript and derived keys before accepting the exchange.2 Enlarging the numbers or strengthening the record cipher would not fix our missing peer authentication. The code deliberately leaves that step out.
13.a.3 The encrypted record
Returning to the direct, unmodified exchange, both ends now hold the same secret. The toy record combines a message with a repeating keystream derived from that secret and adds a one-byte keyed checksum. The receiver checks it before decoding. A mismatch rejects the record, but many different records share a one-byte checksum: a matching tag establishes neither cryptographic integrity nor the sender’s identity. Figure 13.a.3 follows the encoding and decoding steps.
Example 13.a-2. Decrypting, checked first
def decrypt(secret: int, cipher: bytes, tag: int) -> bytes: """Check the ciphertext's checksum, then decrypt it. Raise if the checksum does not match, the sign of tampering or a wrong key.""" if _tag(secret, cipher) != tag:1 raise ValueError("checksum mismatch: the message was tampered with") keystream = _keystream(secret, len(cipher)) return bytes(encrypted ^ key for encrypted, key in zip(cipher, keystream))2
- a checksum mismatch fails before decoding; the example’s chosen one-bit edit produces such a mismatch
- only then is the keystream XORed back to recover the message
13.a.4 The toy exchange and record, run
Running ftb.tls carries out a handshake, sends one encrypted message, and then tampers with it.
Alice sends her public value: 15625 Bob sends his public value: 452807067 Alice derives the secret: 1893334806 Bob derives the secret: 1893334806 they match: True message: b'attack at dawn' ciphertext: 11ad8b7713b2df7704f99b7707b7 Bob reads: b'attack at dawn' a tampered message is caught: checksum mismatch: the message was tampered with
Both ends reach the same secret from their private exponents and the exchanged public values. The encoded message round-trips, and the selected edit is caught. Those checks demonstrate the toy operations. Its tiny parameters, four repeating key bytes, weak checksum, and unauthenticated exchange each prevent it from being a secure channel. Real TLS combines suitable key agreement and key derivation, authentication of the peer and handshake, and secure record protection.
We can now distinguish the jobs a private conversation needs from the operations our example actually supplies. The last part of the book, still in preparation, turns to the browser at our end: how a URL chooses a server, how the page is fetched, and how its text becomes a screen.
Exercises
- Give the two sides different private numbers, and confirm they still reach the same shared secret.
- Flip the low bit of the first ciphertext byte, as the run does, and watch the checksum reject it. Why does this one check not establish security?