docs(pusher): document v2 onion delegation and legacy ping defaults

- Correct WELIST_ALLOW_LEGACY_PING: legacy pings are accepted by default,
  the flag only disables them.
- Describe onion delegation: v2-only inclusion, service-side host
  normalization, strict expiry check, deployment order and rollback.
- Warn that disabled delegation rejects onion pings when SSL_KEY_PATH is
  the dedicated ping key.
- Document certificate lifetime and revocation limits, offline master
  key requirements, helper --days bounds, and a delegation verification
  checklist.
- Fix the bal-pusher.sh comment about the service-side defaults.
This commit is contained in:
2026-11-09 00:15:00 -04:00
parent 4af1ca8220
commit 78e4f72255
4 changed files with 127 additions and 42 deletions

View File

@@ -204,12 +204,15 @@ This is the async daemon that monitors the blockchain and pushes pending transac
fetches `info`, `base_fee` and `version` from the local
`/{chain}/info` endpoint and signs them together with the block metadata.
The WeTime service discovers the public key from a DNSSEC-validated
`_welist-key.<host>` TXT record or from a v3 onion address, so no HTTP
callback is needed. The `url` is trimmed of trailing slashes to match the
service-side normalization; absent metadata is signed as `null`. When
onion delegation is configured, the pusher verifies at startup that the
certificate is bound to the configured onion host, has not expired, and
delegates to the key at `SSL_KEY_PATH`.
`_welist-key.<host>` TXT record, from a v3 onion address, or from a ping key
delegated by that onion identity key, so no HTTP callback is needed. The
`url` is trimmed of trailing slashes to match the service-side
normalization; absent metadata is signed as `null`. When onion delegation
is configured, the pusher sends the certificate only with v2 pings and
verifies at startup that it is bound to the configured onion host, has not
expired, and delegates to the key at `SSL_KEY_PATH`. If that check fails the
pusher disables delegation and signs directly, which the service rejects
unless `SSL_KEY_PATH` is the onion identity key.
- `bal-pusher.sh` pins `WELIST_PING_SIG_VERSION=1` for the development setup:
a `localhost` URL cannot use DNSSEC, so the dev pusher uses the legacy
scheme verified against `/.pub_key.pem`. v2 requires a public DNS hostname

View File

@@ -169,14 +169,19 @@ scheme is selected with `WELIST_PING_SIG_VERSION`:
or from a v3 `.onion` address. No HTTP callback to `/.pub_key.pem` is made.
For `.onion` URLs the pusher includes a `delegation` object when
`WELIST_PING_DELEGATION_FILE` is set. The onion identity key signs that
certificate offline; `SSL_KEY_PATH` is then the dedicated ping key, and the
service verifies the certificate before checking the ping signature. The
pusher validates the certificate at startup: `onion` must match the configured
host, `valid_until` must be in the future, and `key` must match `SSL_KEY_PATH`.
If the file is missing or malformed, or any check fails, the pusher logs a
warning and sends no `delegation` object. It then signs with `SSL_KEY_PATH`
directly, which works only when that key is the onion identity key.
`WELIST_PING_DELEGATION_FILE` is set and `WELIST_PING_SIG_VERSION=2`. The onion
identity key signs that certificate offline; `SSL_KEY_PATH` is then the
dedicated ping key, and the service verifies the certificate before checking
the ping signature. The service normalizes `onion` (lowercase, trailing dot
removed) before comparing it with the ping host; the pusher's startup check uses
the exact configured host, so issue lowercase certificates.
The pusher validates the certificate at startup: `onion` must match the
configured host, `valid_until` must be in the future, and `key` must match
`SSL_KEY_PATH`. If the file is missing or malformed, or any check fails, the
pusher logs a warning and sends no `delegation` object. It then signs with
`SSL_KEY_PATH` directly; in the recommended setup that key is the dedicated
ping key, so the service rejects the pings until delegation is restored.
The `url` is trimmed of surrounding whitespace and trailing slashes, and the
`chain` is lowercase, matching the service-side canonicalization.

View File

@@ -1,7 +1,8 @@
# DNSSEC-authenticated WeTime pings (v2)
# WeTime v2 ping keys: DNSSEC TXT and onion delegation
This document explains how to publish the Ed25519 public key used by the
`bal-pusher` v2 WeTime ping and how to verify that the record is usable.
`bal-pusher` v2 WeTime ping, how to delegate onion ping signing to a dedicated,
rotatable key, and how to verify the configuration.
In v2 the WeTime service discovers the executor public key from:
@@ -198,8 +199,9 @@ use the legacy scheme and let the service verify the published public key:
- Keep `WELIST_PING_SIG_VERSION` unset (or `1`), so the pusher signs with
`SSL_KEY_PATH` and the service verifies against `GET /.pub_key.pem`.
- Start the service with `WELIST_ALLOW_LEGACY_PING=true` and, for loopback or
private addresses, `WELIST_ALLOW_LOCALHOST_PING=true`.
- For loopback or private addresses, start the service with
`WELIST_ALLOW_LOCALHOST_PING=true`. Legacy pings are accepted by default;
only set `WELIST_ALLOW_LEGACY_PING=false` if the service must require v2.
No TXT record and no DNSSEC setup are required in this case.
@@ -278,8 +280,8 @@ export WELIST_PING_SIG_VERSION=2
- `BAL_SERVER_URL` must be the public DNS hostname. The pusher normalizes it by
trimming whitespace and trailing slashes before signing.
- `WELIST_PING_SIG_VERSION=2` activates the canonical JSON signature. Leaving it
unset (or `1`) keeps the legacy v1 scheme, which the service rejects unless
`WELIST_ALLOW_LEGACY_PING=true`.
unset (or `1`) keeps the legacy v1 scheme, which the service accepts by
default (set `WELIST_ALLOW_LEGACY_PING=false` to require v2).
For onion hosts, also configure the delegation certificate (see
[Onion URLs](#onion-urls)):
@@ -365,6 +367,23 @@ values are persisted.
A response of `"ko"` means the signature was rejected. Check the service logs
for the specific `PING REJECTED` reason.
### Onion delegation
For an onion pusher with delegation configured:
1. At startup, the pusher must not log `disabling onion delegation`.
2. After the next block, look for `Report to welist(...) status=200 body="ok"`
and `PING ADDED` in the service log.
3. Negative check: send a crafted v2 ping with a tampered certificate (change
`key` or `valid_until` after signing) directly to the WeTime service. It must
answer `"ko"` and log `PING REJECTED invalid onion delegation`. Do not run
the pusher with a tampered certificate: it disables delegation locally
instead of sending it.
`bal-welist/scripts/test_v2_ping.py` is a local end-to-end reference: it
generates a throwaway onion identity key, issues a delegation certificate, and
checks the accepted and rejected paths on a throwaway database.
---
## 9. Key rotation
@@ -398,10 +417,10 @@ keys for that host.
| `v2 DNS lookup requires a hostname` | `BAL_SERVER_URL` uses an IP address | Use a DNS hostname |
| `invalid v2 signature` | Key mismatch or non-canonical URL | Ensure the TXT key matches `SSL_KEY_PATH` and the URL has no trailing slash |
| `invalid onion delegation` | Certificate signature does not verify against the onion identity key | Re-issue the certificate with the correct onion key; do not edit `key`/`valid_until` after signing |
| `delegation does not match the ping onion host` | `onion` field differs from `BAL_SERVER_URL` | Re-issue the certificate for the exact host |
| `delegation expired` | `valid_until` is in the past | Re-issue the certificate and restart the pusher |
| `delegation does not match the ping onion host` | `onion` field differs from `BAL_SERVER_URL` after normalization | Re-issue the certificate for the exact host |
| `delegation expired` | `valid_until` is at or before the current time | Re-issue the certificate and restart the pusher |
| `delegation is only supported for onion hosts` | `delegation` sent with a DNS URL | Unset `WELIST_PING_DELEGATION_FILE` for DNS hosts |
| `legacy ping disabled` | v1 ping sent while the service requires v2 | Set `WELIST_PING_SIG_VERSION=2` (or enable legacy on the service) |
| `legacy ping disabled` | v1 ping sent while the service requires v2 | Set `WELIST_PING_SIG_VERSION=2` (or set `WELIST_ALLOW_LEGACY_PING=true` if it was disabled) |
Useful diagnostics:
@@ -440,7 +459,7 @@ The request carries:
{
"onion": "<address>.onion",
"key": "<base64 SPKI DER of the delegated Ed25519 key>",
"valid_until": 1767225600,
"valid_until": 1800000000,
"signature": "<base64 Ed25519 signature by the onion identity key>"
}
```
@@ -448,13 +467,16 @@ The request carries:
The identity signature covers exactly:
```text
WELIST-PING-DELEGATION-V1\n{"key":"...","onion":"...","valid_until":1767225600}
WELIST-PING-DELEGATION-V1\n{"key":"...","onion":"...","valid_until":1800000000}
```
Rules enforced by the service:
- `onion` must exactly equal the ping URL host (lowercase, no trailing dot).
- `valid_until` is a Unix timestamp in seconds and must be in the future.
- `onion` is normalized (lowercase, trailing dot removed) before it is compared
with the ping URL host. The pusher's startup check is stricter (exact string),
so issue certificates with the lowercase form; the helper does this.
- `valid_until` is a Unix timestamp in seconds and must be strictly in the
future. A certificate expiring exactly now is already rejected.
- `key` must be a valid Ed25519 SubjectPublicKeyInfo DER in standard base64.
- The identity signature must verify against the key embedded in the onion
address.
@@ -482,7 +504,7 @@ contrib/make_onion_delegation.py \
--onion "$ONION" \
--identity-key "$ONION_KEY" \
--ping-key /etc/bal/ping_private_key.pem \
--days 365 \
--days 90 \
--out /etc/bal/onion-delegation.json
```
@@ -493,10 +515,20 @@ pip install cryptography pynacl
```
If the onion identity key is a PKCS#8 PEM file, point `--identity-key` at it and
`pynacl` is not needed. Run this step on a machine that holds the identity key
(ideally offline), then copy only `onion-delegation.json` and
`ping_private_key.pem` to the executor. The certificate contains public data;
the ping private key must remain secret.
`pynacl` is not needed. `--days` accepts 1–3650 (default 365); prefer short
lifetimes as explained in [Revocation and lifetime](#revocation-and-lifetime).
Run this step on a machine that holds the identity key (ideally offline), then
copy only `onion-delegation.json` and `ping_private_key.pem` to the executor.
The certificate contains public data (`0644` is fine); only
`ping_private_key.pem` must stay `0600` and never leave the executor.
> **Tor offline master keys:** when the onion service runs with
> `OfflineMasterKey 1`, the key file on the Tor host is a blinded signing key,
> not the v3 identity key, and the helper fails with `onion secret key does not
> match the onion address`. Issue the certificate on a machine that holds the
> original `hs_ed25519_secret_key` (keep an offline copy from when the service
> was created). The blinded signing key cannot issue a delegation certificate.
### Configure the pusher
@@ -507,11 +539,32 @@ export BAL_SERVER_URL="http://<address>.onion"
export WELIST_PING_SIG_VERSION=2
```
Delegation is only sent with v2 pings. With `WELIST_PING_SIG_VERSION=1` (or
unset) the certificate is loaded and validated but never included in the
request.
The pusher sends `delegation` only when `BAL_SERVER_URL` is an `.onion` host. It
validates the certificate at startup against the onion host, its expiry, and
`SSL_KEY_PATH`. If the file is missing or malformed, or validation fails,
delegation is disabled and the pusher signs with `SSL_KEY_PATH` directly; that
works only when `SSL_KEY_PATH` is the onion identity key.
delegation is disabled and the pusher signs with `SSL_KEY_PATH` directly.
> **Warning — disabled delegation means onion pings fail.** In this setup
> `SSL_KEY_PATH` is the dedicated ping key, so when delegation is disabled the
> service verifies the ping against the onion identity key and rejects every
> request (`invalid v2 signature`). The executor then stops earning WeTime
> points. Watch for the pusher warning `disabling onion delegation` and
> re-issue before `valid_until`.
### Deployment order
1. Upgrade the WeTime service (`bal-welist`) to a version that supports v2
onion delegation.
2. Then set `WELIST_PING_DELEGATION_FILE` on the pusher and restart it.
A pusher that sends delegation to an older service receives `"ko"`: the old
service ignores the `delegation` field and verifies the ping with the onion
identity key. When rolling back the service, unset
`WELIST_PING_DELEGATION_FILE` first.
For local development, the `WELIST_ALLOW_LOCALHOST_PING` and legacy v1 notes
above still apply; delegation itself needs no network access.
@@ -526,13 +579,32 @@ above still apply; delegation itself needs no network access.
The onion address does not change, and the identity key stays offline. There is
no DNS cache to wait for; the service checks the certificate on every ping.
Rotation does **not** revoke the previous certificate; see
[Revocation and lifetime](#revocation-and-lifetime).
### Revocation and lifetime
The service has no revocation list. It accepts any certificate that is bound to
the onion host and not expired, so rotating to a new ping key does not stop the
old key from working until the old certificate's `valid_until` passes.
Treat the certificate lifetime as the exposure window:
- Prefer short lifetimes (for example 30–90 days) and re-issue before expiry.
- Keep `--days` well below the helper maximum (3650).
- If a ping key leaks, the only immediate revocation is changing the onion
identity, which also changes the `.onion` address. Otherwise the leaked key
remains usable until the certificate expires.
### Security notes
- Delegation limits the blast radius: a compromised online ping key forges
pings only until rotation, not the onion service identity.
- A long `valid_until` increases the impact of a leaked delegated key. Prefer
one year or less and rotate before expiry.
- Delegation limits the blast radius: a compromised online ping key can publish
pings and metadata for that onion host until the certificate expires, but it
cannot take over the onion service identity or sign for another host.
- The delegated key authorizes the full ping capability for that host, including
the `info`, `base_fee` and `version` metadata.
- The certificate is public data and can be copied freely; only the ping private
key is secret.
- The onion identity key should never be copied to the executor.
### Metadata for onion pings
@@ -563,9 +635,14 @@ authenticated and stored.
For onion hosts, replace the TXT/DNSSEC items with:
- [ ] The WeTime service (`bal-welist`) runs a version with onion delegation support.
- [ ] A dedicated `ping_private_key.pem` exists and is `chmod 600`.
- [ ] `onion-delegation.json` was issued offline by the onion identity key.
- [ ] `onion-delegation.json` was issued offline by the v3 **master** identity
key (not a blinded signing key).
- [ ] `SSL_KEY_PATH` points at the delegated ping key, not the identity key.
- [ ] `WELIST_PING_DELEGATION_FILE` points at the certificate.
- [ ] `valid_until` is in the future and rotation is scheduled.
- [ ] `WELIST_PING_SIG_VERSION=2` is set.
- [ ] `valid_until` is strictly in the future, monitored, and rotation is
scheduled before it.
- [ ] The pusher starts without a `disabling onion delegation` warning.
- [ ] Pusher restarted and reports `body="ok"`.