You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OHTTP (Oblivious HTTP, RFC 9458) is the privacy layer in payjoin v2. Clients
encrypt requests using the gateway's public key. If a directory operator loses
their key material or needs to rotate keys, every client holding the old key is
broken with no standard signal to refresh.
Previously, the gateway served keys with no Cache-Control header and returned
HTTP 400 on decapsulation failure. Clients had no way to distinguish a stale key
from a malformed request, and operators had no mechanism to control how long
clients cached keys or to rotate them automatically.
A single configuration value, ohttp_keys_max_age, controls both client caching
and server-side key rotation. When set, it defines:
The Cache-Control: public, max-age=<seconds> header on the key endpoint,
telling clients and intermediaries how long to cache the key.
The interval at which the server automatically generates a new key.
The overlap window during which the previous key remains valid for
decapsulation, so clients with cached old keys continue to work.
When unset (the default), keys are served without cache headers and no rotation
occurs, preserving the previous behavior.
How It Works
Configuration
# Rotate keys every 7 days. Clients cache for the same duration.ohttp_keys_max_age = 604800
Or via environment variable: PJ_OHTTP_KEYS_MAX_AGE=604800
Key Lifecycle
Each OHTTP key has a key_id (a single byte, 0–255) embedded in the first byte
of every encrypted message. The server uses this to dispatch decapsulation to the
correct key.
When rotation is enabled, the server manages keys through three phases:
Active phase. The key is served to new clients on GET /.well-known/ohttp-gateway with a Cache-Control header. All incoming
requests encrypted with this key are decapsulated normally.
Overlap phase. A new key has been generated and is now being served to
clients. The old key is no longer advertised but still accepted for
decapsulation. This phase lasts ohttp_keys_max_age seconds — long enough for
all well-behaved caches to expire.
Retired. The old key is removed from memory and its .ikm file is deleted
from disk. Any request using this key now receives HTTP 422 with an application/problem+json body of type ohttp-key, signaling the client to
fetch a fresh key.
Timeline Example (7-day rotation)
Day 1: key_id=1 generated, served to clients with max-age=604800
Day 8: key_id=2 generated, now served. key_id=1 enters overlap.
Day 15: key_id=1 retired. key_id=3 generated, key_id=2 enters overlap.
Day 22: key_id=2 retired. And so on.
At any point, at most two keys are active simultaneously.
Client Perspective
A client that respects Cache-Control re-fetches the key when its cache expires,
naturally picking up the new key before the old one is retired. No disruption.
A client using a stale key (e.g. from an old BIP21 URI with an embedded OH1
fragment) sees:
POST to the gateway fails with 422 Unprocessable Entity and a problem+json
body: {"type":"https://iana.org/assignments/http-problem-types#ohttp-key", "title": "key identifier unknown"}.
Client fetches fresh keys from GET /.well-known/ohttp-gateway.
Client retries with the new key.
For payjoin-cli my intended caching approach is
Store the fetched key alongside the time it was fetched and the max-age value from the response header
Before each operation, check whether now - fetch_time > max_age
If the cache is still fresh, use the stored key without a network request
I4. if expired, re-fetch from the directory before proceeding
The 422 status code is mandated by RFC 9458 (sections 5.2, 6.4) for requests the
server cannot decapsulate due to an unknown key identifier.
Operator Guidance
Goal
Configuration
No rotation (static key)
Omit ohttp_keys_max_age
Weekly rotation
ohttp_keys_max_age = 604800
Daily rotation
ohttp_keys_max_age = 86400
paranoid (hourly)
ohttp_keys_max_age = 3600
Shorter intervals mean clients must refresh keys more frequently but reduce the
window of exposure if key material is compromised. The overlap window always
equals the configured max-age, so there is no disruption for clients that honor
the cache headers.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
OHTTP Key Rotation and Cache Control
Problem
OHTTP (Oblivious HTTP, RFC 9458) is the privacy layer in payjoin v2. Clients
encrypt requests using the gateway's public key. If a directory operator loses
their key material or needs to rotate keys, every client holding the old key is
broken with no standard signal to refresh.
Previously, the gateway served keys with no
Cache-Controlheader and returnedHTTP 400 on decapsulation failure. Clients had no way to distinguish a stale key
from a malformed request, and operators had no mechanism to control how long
clients cached keys or to rotate them automatically.
#445
#1035 this attempd to implement keys-catching, but would be retired for the solution below
Solution
#1449
A single configuration value,
ohttp_keys_max_age, controls both client cachingand server-side key rotation. When set, it defines:
Cache-Control: public, max-age=<seconds>header on the key endpoint,telling clients and intermediaries how long to cache the key.
decapsulation, so clients with cached old keys continue to work.
When unset (the default), keys are served without cache headers and no rotation
occurs, preserving the previous behavior.
How It Works
Configuration
Or via environment variable:
PJ_OHTTP_KEYS_MAX_AGE=604800Key Lifecycle
Each OHTTP key has a
key_id(a single byte, 0–255) embedded in the first byteof every encrypted message. The server uses this to dispatch decapsulation to the
correct key.
When rotation is enabled, the server manages keys through three phases:
Active phase. The key is served to new clients on
GET /.well-known/ohttp-gatewaywith aCache-Controlheader. All incomingrequests encrypted with this key are decapsulated normally.
Overlap phase. A new key has been generated and is now being served to
clients. The old key is no longer advertised but still accepted for
decapsulation. This phase lasts
ohttp_keys_max_ageseconds — long enough forall well-behaved caches to expire.
Retired. The old key is removed from memory and its
.ikmfile is deletedfrom disk. Any request using this key now receives HTTP 422 with an
application/problem+jsonbody of typeohttp-key, signaling the client tofetch a fresh key.
Timeline Example (7-day rotation)
At any point, at most two keys are active simultaneously.
Client Perspective
A client that respects
Cache-Controlre-fetches the key when its cache expires,naturally picking up the new key before the old one is retired. No disruption.
A client using a stale key (e.g. from an old BIP21 URI with an embedded
OH1fragment) sees:
body:
{"type":"https://iana.org/assignments/http-problem-types#ohttp-key", "title": "key identifier unknown"}.GET /.well-known/ohttp-gateway.For payjoin-cli my intended caching approach is
I4. if expired, re-fetch from the directory before proceeding
The 422 status code is mandated by RFC 9458 (sections 5.2, 6.4) for requests the
server cannot decapsulate due to an unknown key identifier.
Operator Guidance
ohttp_keys_max_ageohttp_keys_max_age = 604800ohttp_keys_max_age = 86400ohttp_keys_max_age = 3600Shorter intervals mean clients must refresh keys more frequently but reduce the
window of exposure if key material is compromised. The overlap window always
equals the configured max-age, so there is no disruption for clients that honor
the cache headers.
Beta Was this translation helpful? Give feedback.
All reactions