TRENDING
Close-up of the Rosetta Stone showing the Demotic script above and the Greek script below, the same text written in two different scripts
October 6, 2026
How to Prepare Your Python Code for the Python 3.15 UTF-8 Default and Fix Windows Encoding Bugs
A row of green and grey fibre broadband street cabinets on a pavement beside a fence in Iver, England
October 6, 2026
BT’s TalkTalk Rescue Turns Telecom Continuity Into a New Merger-Control Ground
An ornate cast-iron wall mailbox with its door hanging open, stuffed with colorful flyers and a yellow flyer bulging out of the top slot
October 6, 2026
Google Stops Accepting Product Bug Reports for Its Open-Source Bounty, Citing Automated Submissions
Chronophotograph by Étienne-Jules Marey of a man riding a bicycle, showing five snapshots of the same ride taken at regular intervals
October 6, 2026
How to Find Slow Python Code With the Python 3.15 Tachyon Sampling Profiler
Close-up of an airport baggage tag reading Stockholm Arlanda and ARN
October 6, 2026
Cloudflare Traces Turns Distributed Tracing Into a Trust Decision at the Edge
06 Oct 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
Shelves of old books fastened by iron chains in the Francis Trigge Chained Library in Grantham, England, a picture of data that can be read but not changed
How to Use frozendict in Python 3.15 to Freeze Config and Cache Dictionary Arguments
October 5, 2026
Row of capsule hotel pods with white pillows and folded blankets, each capsule an idle sleeper packed into a shared rack
Kubernetes Node Swap Turns Idle Agent Memory Into a Density Bet With No Wake-Up Test
October 5, 2026
Denmark’s oldest church book, from Holmens parish, open on a stack of books; its handwritten pages record births between 1617 and 1639
Denmark Says 8.8 Million Population Register Records Were Pulled Through One Company’s Lawful Access
October 5, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 226 Posts
News 228 Posts
Learning Hub 198 Posts
Home/Articles/Cloudflare’s OHTTP Gateway Turns Request Privacy Into a Question of Who Runs the Other Hop
Articles

Cloudflare’s OHTTP Gateway Turns Request Privacy Into a Question of Who Runs the Other Hop

Cloudflare’s new OHTTP Gateway lets apps already behind its network adopt Oblivious HTTP, but the privacy still depends on a relay run by someone else, and a short Python model of both hops shows...

October 2, 2026 16 Min Read
38

Cloudflare now offers both halves of Oblivious HTTP. On October 2 the company announced a closed beta of a self-serve OHTTP Gateway, a paid zone add-on that it says is launching this fall, and renamed its 2022 Privacy Gateway to Cloudflare OHTTP Relay. Oblivious HTTP, or OHTTP, is the IETF standard in RFC 9458 for hiding a client’s IP address from the server it talks to. It works only when a relay and a gateway are run by different organizations, which makes the rules in the announcement more interesting than the features.

Table Of Content

  • What OHTTP promises, and what it leaves out
  • The announcement: two products and one rule
  • Relay or gateway, and who runs which
  • The refusal rule
  • What else the post says about the gateway
  • Where the trust actually sits
  • Two logs and a promise
  • How the large deployments chose their other hop
  • “Bring your own relay” is the weak spot
  • What the wire format leaks
  • A check against the RFC’s own example
  • What a relay can still learn
  • Size
  • Keys
  • Forward secrecy and the post-quantum question
  • Questions to ask before enabling it
  • The bottom line
  • The lab script

Cloudflare says its gateway “will refuse to decrypt requests sent from Cloudflare Workers or from proxied hosts on Cloudflare.” That is a technical guard around what is, underneath, an organizational promise. This piece looks at what the guard protects, what the design leaves to the customer, and what I measured after building both hops in a short Python script (the full listing is at the end).

What OHTTP promises, and what it leaves out

The RFC’s abstract describes a protocol that “allows a client to make multiple requests to an origin server without that server being able to link those requests to the client or to identify the requests as having come from the same client, while placing only limited trust in the nodes used to forward the messages.” RFC 9458, published in January 2024, was co-authored by Martin Thomson of Mozilla and Christopher A. Wood of Cloudflare. The mechanism is simple to state. The client encrypts an HTTP request to the gateway’s public key with HPKE (Hybrid Public Key Encryption) and sends the ciphertext to a relay. The relay forwards it to the gateway, which decrypts it, hands it to the application and encrypts the response on the way back.

Each party gets half of the picture. Cloudflare’s announcement calls this a double-blind model: “the relay sees only client identifiers; the gateway and app server see only request contents; no party sees both.” The RFC describes the relay’s side as one that “knows the origin and destination of an Encapsulated Request and Response, yet it does not know the decrypted contents.” The post names two users of the protocol: Flo Health’s Anonymous Mode, and Apple’s Private Cloud Compute, which uses OHTTP “to disassociate AI inference requests from user identities.”

Party Sees Does not see
Relay The client’s IP address and connection metadata, the gateway it forwards to, the size and timing of each ciphertext The request or the response
Gateway The plaintext request and the relay’s address The client’s IP address
Anyone holding both logs Both columns above, joinable on the ciphertext bytes, the size or the timing Nothing

Two limits matter before any product comparison. First, OHTTP offers unlinkability, not anonymity: the community explainer ohttp.info says the protocol “hides who made a specific request, not that requests were made,” and a cookie or login inside the request still identifies the user to the application. Cloudflare’s post says the same about the body: “it’s up to you not to send identifying information (e.g. a user’s email address or username) in the request body.” Its relay documentation adds that Cloudflare “cannot ensure that websites and services will not send identifying user data from requests forwarded through Cloudflare OHTTP Relay.”

Second, the guarantee needs two independent operators. Cloudflare’s own 2022 formal analysis of the protocol, by Christopher Wood and Jonathan Hoyland, states the assumption plainly: “OHTTP assumes that R and G do not collude, and so we assume only one of R and G is compromised.”

The announcement: two products and one rule

Relay or gateway, and who runs which

Until now a customer could buy Cloudflare’s relay and had to run its own gateway. The new product reverses the arrangement.

Product Cloudflare runs The customer supplies Cloudflare says it fits when
Cloudflare OHTTP Relay (formerly Privacy Gateway, launched 2022) The relay The gateway The app servers are hosted off Cloudflare and the customer can run its own gateway
Cloudflare OHTTP Gateway (closed beta, paid zone add-on) The gateway, bound to the customer’s zone A relay, from a third party The app servers are already behind Cloudflare, the customer accepts OHTTP requests from a third party such as Apple’s LiveCallerID, or wants a managed gateway to cut latency and operational work

The reason for two products is the rule at the center of the protocol. RFC 9458 is explicit: “To achieve the stated privacy goals, the Oblivious Relay Resource cannot be operated by the same entity as the Oblivious Gateway Resource.” Cloudflare’s post spells out what that meant for its own customers. Developers who protected their app servers behind Cloudflare “weren’t able to use our OHTTP Relay, because Cloudflare would see both client metadata and the decrypted contents of requests, breaking OHTTP’s privacy model.” With the Gateway those customers can adopt OHTTP, provided the relay is somebody else.

The refusal rule

Cloudflare expects mistakes. It “anticipated that customers might accidentally break OHTTP’s privacy model by running both their relay and gateway on Cloudflare,” and its answer is a refusal built into the product: the gateway “will refuse to decrypt requests sent from Cloudflare Workers or from proxied hosts on Cloudflare.” A guard in the product is the right shape of safeguard, and it is worth noticing what it does not cover. The post does not say how the gateway recognizes those senders, or whether the refusal also covers Cloudflare’s own relay product. Those are questions for documentation, and there is none yet. When I checked on October 2, Cloudflare’s documentation index listed only Cloudflare OHTTP Relay pages for OHTTP, and the relay’s Legal page, marked as updated that day, still opened by describing the relay as “a managed gateway service,” a small sign of how easily the two roles blur.

What else the post says about the gateway

  • It is bound to a zone. Clients send OHTTP to the /.well-known/ohttp-gateway path that RFC 9540 defines, on the customer’s own domain, and non-OHTTP requests pass straight through. A client may reach foo.example.com or bar.example.com through a gateway on example.com but not an unrelated domain, which is the kind of mechanism RFC 9458 asks for when it says a gateway should ensure it “is not misused as a relay for HTTP messages to an arbitrary Target Resource,” and it suggests an allowlist as one way.
  • Cloudflare manages the keys. The public key configuration is served on a GET to the same path, and “for stronger privacy, clients can download keys over a different IP than they request the gateway.”
  • The relay is authenticated before decryption. Because the gateway knows little about the client, “it places trust in the relay to authenticate clients and forward traffic responsibly,” so Cloudflare Access runs first, with mutual TLS, static service credentials or custom logic. Google’s Safe Browsing gateway takes the same approach and requires the relay to authenticate with OAuth 2.0.
  • Chunked OHTTP is supported. Cloudflare says it handles “both standard and chunked OHTTP” and recommends chunked for speed. Chunked OHTTP is still an Internet-Draft (draft-ietf-ohai-chunked-ohttp, revision 08, last updated May 20, 2026 on the IETF datatracker), so the lab below covers the standard format only.

Where the trust actually sits

Two logs and a promise

The relay holds the identity half. Cloudflare’s relay documentation says Cloudflare sees “the connecting device’s IP address, the application service they are using, including its DNS name and IP address, and metadata associated with the request, including the type of browser, device operating system, hardware configuration, and timestamp of the request,” and keeps those logs “for the most recent quarter plus one month (approximately 124 days).” The gateway side holds the content half. Nothing in the cryptography stops the two from being joined, and Cloudflare’s post says what the separation depends on. The reason to use a dedicated relay provider is “to verifiably promise to your users that you won’t inspect logs with client identifiers. Otherwise, you’d be able to correlate clients at the relay with decrypted requests at your app servers.”

How hard would the join be? The lab below answers with a model of both hops. The relay forwards the encapsulated request unchanged, so the same bytes reach both operators, and a short fingerprint of them is a perfect join key:

== 2. A gateway key configuration
key id 1, KEM 0x0020, KDF 0x0001 + AEAD 0x0001, 41 bytes in all

== 3. What each hop logs for three clients
relay   : client=198.51.100.21  ciphertext=95 bytes  fingerprint=26984a2ff7
relay   : client=198.51.100.22  ciphertext=106 bytes  fingerprint=2ed72e9d0b
relay   : client=198.51.100.23  ciphertext=110 bytes  fingerprint=2eab93e319
gateway : peer=203.0.113.7  request=GET https://api.example.com/v1/health  fingerprint=26984a2ff7
gateway : peer=203.0.113.7  request=GET https://api.example.com/v1/records/42/export  fingerprint=2ed72e9d0b
gateway : peer=203.0.113.7  request=GET https://api.example.com/v1/search?q=insulin+pump  fingerprint=2eab93e319

Pooling those two logs takes a dictionary lookup:

== 4. If the two operators pooled those logs
198.51.100.21 asked for GET https://api.example.com/v1/health
198.51.100.22 asked for GET https://api.example.com/v1/records/42/export
198.51.100.23 asked for GET https://api.example.com/v1/search?q=insulin+pump

This illustrates a property of the format, not a claim about what any operator logs. The Legal page lists the connecting address, the service and request metadata such as browser, operating system, hardware configuration and timestamp, and it does not mention ciphertext or fingerprints. The point is that collusion costs one extra column, which is why the protocol leans on independent operators and why a relay’s retention period matters.

How the large deployments chose their other hop

Mozilla. In an October 2023 post, Bobby Holley said Mozilla had engaged Fastly to operate an OHTTP relay for Firefox and Divvi Up to operate an aggregator for a related protocol. His argument for independence was incentive: “Both Fastly and ISRG (the nonprofit behind Divvi Up and Let’s Encrypt) have excellent reputations for acting with integrity, and they have staked those reputations on the faithful operation of these services.” Even if Mozilla tried to persuade them to cheat, he wrote, “they have a strong incentive to hold the line.”

Apple. Apple’s Private Cloud Compute write-up says requests pass through an OHTTP relay “operated by a third party,” so that “an attacker would have to compromise both the third-party relay and our load balancer to steer traffic based on the source IP address.”

Google. The Safe Browsing gateway documentation, last updated in September 2024 and still marked as under development, says “Clients can choose any Relay provider” and names Fastly as an example, while requiring that relay to authenticate itself.

The pattern is a different company with its own reputation at stake, and a gateway that checks who is allowed to relay. Cloudflare’s post names no relay providers. It says relays “can run on any infrastructure provider,” links to sample code, and points to dedicated relay providers as the way to make a no-logging promise verifiable.

“Bring your own relay” is the weak spot

Cloudflare’s getting-started advice is to “bring your own relay.” A customer that runs that relay itself holds the identity half and, through its app servers, the content half, which is the situation Cloudflare’s own warning describes. The RFC adds a traffic-analysis point: “A relay that forwards large volumes of exchanges can provide better privacy by providing larger sets of messages that need to be matched,” and “A malicious Oblivious Relay Resource could use traffic analysis to learn information about otherwise encrypted requests and responses relayed between Clients and gateways.” By that logic a relay that carries one application’s traffic gives an observer fewer messages to match, and if the application owner runs it there is no separation at all.

What the wire format leaks

To make these properties concrete I wrote a model of the request direction of RFC 9458 in Python 3.13 with cryptography 50.0.1 (HPKE support arrived in version 47.0.0). A client function encapsulates a Binary HTTP request, a relay function logs what a relay would see, and a gateway function decrypts. Both hops are plain functions with documentation addresses, so nothing leaves the machine except the last step, which fetches one live key configuration. The library’s HPKE interface offers single-shot encrypt and decrypt, while RFC 9458 derives response keys from an exported secret, so the model covers requests only.

A check against the RFC’s own example

Before trusting the model, I decrypted the Encapsulated Request in RFC 9458 Appendix A with the secret key printed beside it:

== 1. Decapsulate the example in RFC 9458 Appendix A
Encapsulated Request bytes: 80 (the RFC's sample POST says Content-Length: 78)
decrypted: 00034745540568747470730b6578616d706c652e636f6d012f
matches the RFC's Binary HTTP request: True
what the gateway reads from it: GET https://example.com/

The decrypted bytes equal the Binary HTTP request in the RFC, which validates the decapsulation code. One discrepancy turned up while counting: the RFC’s sample POST shows Content-Length: 78, but the hex it shows is 80 bytes (a 7-byte header, a 32-byte encapsulated key, 25 bytes of Binary HTTP and a 16-byte authentication tag). Size buffers from the hex, not the prose.

What a relay can still learn

Size

The relay can read the 7-byte header, so it knows the key identifier and algorithms, and it can measure the rest. The ciphertext is the Binary HTTP request plus 55 bytes, so three different requests produce three different sizes. Padding to a multiple of 128 bytes makes them identical:

== 5. Request size is visible to the relay
/v1/health                   binary HTTP  40 bytes -> on the wire  95 (7 + 32 + 40 + 16)  padded to 128: 183
/v1/records/42/export        binary HTTP  51 bytes -> on the wire 106 (7 + 32 + 51 + 16)  padded to 128: 183
/v1/search?q=insulin+pump    binary HTTP  55 bytes -> on the wire 110 (7 + 32 + 55 + 16)  padded to 128: 183

The RFC says “Clients and Oblivious Gateway Resources can use padding to reduce the effectiveness of traffic analysis.” For requests, only the client can add that padding inside the encrypted message, so a managed gateway cannot pad requests on a customer’s behalf. Timing is the sibling leak: “The time at which Encapsulated Request or Response messages are sent can reveal information to a network observer.”

Keys

The key configuration is where linkability can hide. RFC 9540 notes that “client requests using Oblivious HTTP can only be linked by recognizing the key configuration,” so a gateway that handed each client a unique key could track them. Fetching keys directly also costs privacy: “If a client fetches the key configuration directly from the gateway, it will expose identifiers like a client IP address to the gateway.” Cloudflare suggests fetching keys from a different IP, and Google tells client vendors “to set up a centralized key distribution infrastructure” so that every client receives the same key, refreshed “once per day.”

On October 2 I fetched Google’s live configuration without an API key, which its page says is allowed:

== 7. A live gateway's key configuration
200 application/ohttp-keys public, max-age=3600
key id 30, KEM 0x0020, KDF 0x0001 + AEAD 0x0002, 41 bytes in all
public key starts e9049e7a68ec2a5a

That is one 41-byte configuration with key identifier 30, the X25519 key-encapsulation method, HKDF-SHA256 and AES-256-GCM, served as application/ohttp-keys with a one-hour cache lifetime. The 41 bytes are one identifier byte, a two-byte method ID, a 32-byte public key and a six-byte algorithm list (a length field and one pair of algorithm IDs).

Forward secrecy and the post-quantum question

RFC 9458 is blunt about one limit: “This document does not provide forward secrecy for either requests or responses during the lifetime of the key configuration.” The explainer ohttp.info draws the consequence: “If the gateway’s private key leaks, recorded ciphertexts can be decrypted.” The relay is the one party that sees every ciphertext next to the sender’s address, so a relay archive combined with a later key compromise would unmask past requests. That is my inference from the RFC’s statement, and it describes a malicious or compelled relay rather than a passive network observer.

The key configuration carries a 16-bit method identifier, so a registered post-quantum method fits the existing format. The IANA HPKE registry lists X-Wing, a hybrid of ML-KEM-768 and X25519, at 0x647A with a 1,120-byte encapsulated key and a 1,216-byte public key, and the cryptography library ships it as KEM.MLKEM768_X25519. The model shows what it costs on this wire format:

== 6. The same request with a post-quantum hybrid KEM (X-Wing)
public key 1216 bytes (X25519: 32), key configuration 1225 bytes (X25519: 41)
encapsulated request 1183 bytes (X25519: 95), 1088 bytes more, round trip ok

The configuration grows from 41 to 1,225 bytes and every request grows by 1,088 bytes. Cloudflare’s post does not say which key-encapsulation methods the Gateway will offer, and the one live gateway I decoded advertises X25519. I make no claim about what is deployed elsewhere. For the hybrid construction itself, see our Python tutorial on X25519 and ML-KEM, and for the same per-hop habit applied to TLS, our look at Cloudflare’s post-quantum visibility fields.

Questions to ask before enabling it

  1. Who runs the relay? It must be independent of Cloudflare and of your own company. The refusal rule blocks Cloudflare-hosted senders, but it cannot judge a relay you operate yourself.
  2. What does the relay log, and for how long? Cloudflare’s own relay keeps the address, service and request metadata for about 124 days. Ask any other relay provider for the same list.
  3. Which Cloudflare products see the decrypted request? The relay documentation lists API Shield, Cache and WAF as incompatible, and its WAF entry explains that “Cloudflare cannot see the request or response content.” The Gateway decrypts and issues a subrequest to your server, and until its documentation exists it is unclear which of those products apply to that subrequest.
  4. Which key methods and rotation schedule? Ask for the algorithm list, whether a post-quantum method is planned, and how often keys change.
  5. How will clients check they all received the same key? RFC 9540 expects clients to “employ some technique to mitigate key targeting attacks,” such as comparing keys through a shared proxy.
  6. Are requests padded, free of identifiers and safe to replay? The RFC warns that “The threat model for Oblivious HTTP allows the possibility that an Oblivious Relay Resource might replay requests,” so the application must reject replays or tolerate them.

The bottom line

The new Gateway removes a real obstacle. Until now an app behind Cloudflare could not use Cloudflare’s relay without Cloudflare seeing both halves, and the alternative was to build and run a gateway at scale, which Cloudflare says can be tough for customers. A managed gateway that refuses Cloudflare-hosted senders is a sensible guard against the most likely self-inflicted mistake. What it cannot supply is the other hop. The relay has to be a different organization with its own incentives and a log policy you can read, and the cryptography will not tell you if it is not. As of October 2 the product is a waitlist and a blog post with no gateway documentation, so the useful questions now are about the relay, not the box. For another recent Cloudflare launch built around a trust question, see our piece on the agent billing betas and who holds the meter.

The lab script

Run it with pip install "cryptography>=50" (version 47 or newer has HPKE; 50.0.1 is what I used). The hashes in the log lines change on every run because each request uses a fresh ephemeral key, and the last section needs network access.

# ohttp_lab.py
# What an OHTTP relay and gateway each see (RFC 9458, request direction only).
# Needs cryptography 47.0.0 or newer (HPKE). The two hops are plain functions and every address is a
# documentation address, so only the optional live key fetch at the end touches the network.
import hashlib
import struct
import urllib.request

from cryptography.hazmat.primitives.asymmetric.mlkem import MLKEM768PrivateKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
from cryptography.hazmat.primitives.hpke import AEAD, KDF, KEM, MLKEM768X25519PrivateKey, Suite
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

KEM_X25519, KEM_XWING, KDF_SHA256, AEAD_AES128 = 0x0020, 0x647A, 0x0001, 0x0001
SUITE = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM)
LABEL = b"message/bhttp request\x00"


def raw(public_key):
    return public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)


def key_config(key_id, kem_id, public_key_bytes):
    # RFC 9458 section 3.1: key id, KEM id, public key, then a list of (KDF id, AEAD id) pairs
    return struct.pack(">BH", key_id, kem_id) + public_key_bytes + struct.pack(">HHH", 4, KDF_SHA256, AEAD_AES128)


def describe(cfg, npk=32):
    key_id, kem_id = struct.unpack(">BH", cfg[:3])
    n = struct.unpack(">H", cfg[3 + npk : 5 + npk])[0]
    pairs = [struct.unpack(">HH", cfg[5 + npk + i : 9 + npk + i]) for i in range(0, n, 4)]
    algs = ", ".join(f"KDF 0x{k:04x} + AEAD 0x{a:04x}" for k, a in pairs)
    return f"key id {key_id}, KEM 0x{kem_id:04x}, {algs}, {len(cfg)} bytes in all"


def encapsulate(suite, kem_id, key_id, public_key, bhttp):
    hdr = struct.pack(">BHHH", key_id, kem_id, KDF_SHA256, AEAD_AES128)  # 7 bytes the relay can read
    return hdr + suite.encrypt(bhttp, public_key, info=LABEL + hdr)


def decapsulate(suite, private_key, message):
    hdr = message[:7]
    return suite.decrypt(message[7:], private_key, info=LABEL + hdr)


def bhttp_get(authority, path, pad_to=0):
    # A known-length Binary HTTP request (RFC 9292): GET, https, authority, path, no headers, no content.
    fields = (b"GET", b"https", authority.encode(), path.encode())
    assert all(len(f) < 64 for f in fields)  # one-byte lengths are enough for this lab
    msg = b"\x00" + b"".join(bytes([len(f)]) + f for f in fields) + b"\x00\x00"
    return msg + b"\x00" * (-len(msg) % pad_to if pad_to else 0)  # trailing zeros are Binary HTTP padding


def bhttp_target(msg):
    i, parts = 1, []
    for _ in range(4):
        parts.append(msg[i + 1 : i + 1 + msg[i]].decode())
        i += 1 + msg[i]
    return f"{parts[0]} {parts[1]}://{parts[2]}{parts[3]}"


print("== 1. Decapsulate the example in RFC 9458 Appendix A")
rfc_secret = X25519PrivateKey.from_private_bytes(
    bytes.fromhex("3c168975674b2fa8e465970b79c8dcf09f1c741626480bd4c6162fc5b6a98e1a")
)
rfc_request = bytes.fromhex(
    "010020000100014b28f881333e7c164ffc499ad9796f877f4e1051ee6d31bad19dec96c208b4726374e46913"
    "5906992e1268c594d2a10c695d858c40a026e7965e7d86b83dd440b2c0185204b4d63525"
)
plain = decapsulate(SUITE, rfc_secret, rfc_request)
print("Encapsulated Request bytes:", len(rfc_request), "(the RFC's sample POST says Content-Length: 78)")
print("decrypted:", plain.hex())
print("matches the RFC's Binary HTTP request:", plain.hex() == "00034745540568747470730b6578616d706c652e636f6d012f")
print("what the gateway reads from it:", bhttp_target(plain))

print("\n== 2. A gateway key configuration")
GATEWAY_KEY = X25519PrivateKey.generate()
CONFIG = key_config(1, KEM_X25519, raw(GATEWAY_KEY.public_key()))
print(describe(CONFIG))

RELAY_ADDR = "203.0.113.7"
relay_log, gateway_log = [], []


def gateway(peer, message):  # sees the relay's address and the plaintext request
    request = decapsulate(SUITE, GATEWAY_KEY, message)
    # The fingerprint column is something a gateway COULD log: it receives the same bytes the relay forwarded.
    gateway_log.append((peer, bhttp_target(request), hashlib.sha256(message).hexdigest()[:10]))


def relay(client, message):  # sees the client's address, the gateway, and ciphertext
    relay_log.append((client, len(message), hashlib.sha256(message).hexdigest()[:10]))
    gateway(RELAY_ADDR, message)


gateway_pub = X25519PublicKey.from_public_bytes(CONFIG[3:35])
traffic = [
    ("198.51.100.21", "/v1/health"),
    ("198.51.100.22", "/v1/records/42/export"),
    ("198.51.100.23", "/v1/search?q=insulin+pump"),
]
for client, path in traffic:
    relay(client, encapsulate(SUITE, KEM_X25519, 1, gateway_pub, bhttp_get("api.example.com", path)))

print("\n== 3. What each hop logs for three clients")
for client, size, fp in relay_log:
    print(f"relay   : client={client}  ciphertext={size} bytes  fingerprint={fp}")
for peer, target, fp in gateway_log:
    print(f"gateway : peer={peer}  request={target}  fingerprint={fp}")

print("\n== 4. If the two operators pooled those logs")
by_fingerprint = {fp: client for client, _, fp in relay_log}
for _, target, fp in gateway_log:
    print(f"{by_fingerprint[fp]} asked for {target}")

print("\n== 5. Request size is visible to the relay")
for _, path in traffic:
    plain_len = len(bhttp_get("api.example.com", path))
    wire = len(encapsulate(SUITE, KEM_X25519, 1, gateway_pub, bhttp_get("api.example.com", path)))
    padded = len(encapsulate(SUITE, KEM_X25519, 1, gateway_pub, bhttp_get("api.example.com", path, pad_to=128)))
    print(f"{path:<28} binary HTTP {plain_len:>3} bytes -> on the wire {wire:>3} (7 + 32 + {plain_len} + 16)  padded to 128: {padded}")

print("\n== 6. The same request with a post-quantum hybrid KEM (X-Wing)")
mlkem, x25519 = MLKEM768PrivateKey.generate(), X25519PrivateKey.generate()
xwing_private = MLKEM768X25519PrivateKey(mlkem, x25519)
xwing_suite = Suite(KEM.MLKEM768_X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM)
xwing_pub_len = len(mlkem.public_key().public_bytes_raw()) + len(raw(x25519.public_key()))
msg = bhttp_get("api.example.com", "/v1/health")
wire = encapsulate(xwing_suite, KEM_XWING, 2, xwing_private.public_key(), msg)
assert decapsulate(xwing_suite, xwing_private, wire) == msg
classic = len(encapsulate(SUITE, KEM_X25519, 1, gateway_pub, msg))
print(f"public key {xwing_pub_len} bytes (X25519: 32), key configuration {2 + 1 + xwing_pub_len + 6} bytes (X25519: {len(CONFIG)})")
print(f"encapsulated request {len(wire)} bytes (X25519: {classic}), {len(wire) - classic} bytes more, round trip ok")

print("\n== 7. A live gateway's key configuration")
URL = "https://safebrowsingohttpgateway.googleapis.com/v1/ohttp/hpkekeyconfig"
req = urllib.request.Request(URL, headers={"Accept": "application/ohttp-keys", "User-Agent": "Mozilla/5.0"})
try:
    with urllib.request.urlopen(req, timeout=15) as r:
        body = r.read()
        print(r.status, r.headers["Content-Type"], r.headers["Cache-Control"])
    print(describe(body))
    print("public key starts", body[3:11].hex())
except OSError as exc:
    print("skipped (offline):", exc)

Tags:

CloudflareCryptographyData PrivacyNetwork SecurityOblivious HTTP

Share

Red 1970s drafting stencil with cut-out symbols and a ruler edge, standing in for a prompt template
Previous Post

GitLab Patches a Second Critical Template Flaw in Its Self-Hosted AI Gateway Within Eight Months

An open smoke sensor showing its green circuit board, black optical detection chamber and battery clip
Next Post

How to Catch a Malicious Python Package at Import Time With Audit Hooks

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
05 Oct
How to Use frozendict in Python 3.15 to Freeze Config and Cache Dictionary Arguments
05 Oct
Kubernetes Node Swap Turns Idle Agent Memory Into a Density Bet With No Wake-Up Test
Trending
October 5, 2026
How to Use frozendict in Python 3.15 to Freeze Config and Cache Dictionary Arguments
October 5, 2026
Kubernetes Node Swap Turns Idle Agent Memory Into a Density Bet With No Wake-Up Test
October 5, 2026
Denmark Says 8.8 Million Population Register Records Were Pulled Through One Company’s Lawful Access
October 5, 2026
How to Prepare Your Python Code for the Python 3.15 UTF-8 Default and Fix Windows Encoding Bugs
October 5, 2026
BT’s TalkTalk Rescue Turns Telecom Continuity Into a New Merger-Control Ground
October 5, 2026
Google Stops Accepting Product Bug Reports for Its Open-Source Bounty, Citing Automated Submissions

Related Posts

Blue-lit server racks in a modern data center, illustrating the compute infrastructure behind the AI boom.
Articles

The AI Boom Is Spending Real Money Before Proving Real Returns

June 7, 2026
Technician working with a laptop beside server racks, representing enterprise AI retrieval infrastructure
Articles

Google’s Agentic RAG Push Makes Enterprise AI Less of a One-Shot Guess

June 7, 2026
A person with a laptop and smartphone, representing digital attention and AI-assisted work
Articles

AI Chatbots Are Making Attention a Design Problem

June 7, 2026
A customer-support representative wearing a headset against a dark studio background.
Articles

The Meta AI Support Hack Was a Plain Old Authorization Failure

June 7, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026