From 78e4f7225542e7b8c5422ca1015f5887af8c8acd Mon Sep 17 00:00:00 2001 From: svatantrya Date: Mon, 9 Nov 2026 00:15:00 -0400 Subject: [PATCH] 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. --- bal-pusher.sh | 4 +- docs/04_modules_detail.md | 15 ++-- docs/05_api_reference.md | 21 +++-- docs/10_dnssec_key_publishing.md | 129 ++++++++++++++++++++++++------- 4 files changed, 127 insertions(+), 42 deletions(-) diff --git a/bal-pusher.sh b/bal-pusher.sh index 3503b3c..351c45d 100755 --- a/bal-pusher.sh +++ b/bal-pusher.sh @@ -30,8 +30,8 @@ export WELIST_SKIP_URL_VALIDATION=true # Local development uses the legacy v1 signature, verified by the service # against GET /.pub_key.pem: a localhost URL cannot use DNSSEC key discovery. # v2 requires a public DNS hostname (or a v3 onion URL) with a published key. -# The WeTime service must run with WELIST_ALLOW_LEGACY_PING=true and, for -# loopback addresses, WELIST_ALLOW_LOCALHOST_PING=true. +# The WeTime service accepts legacy pings by default; for loopback addresses it +# also needs WELIST_ALLOW_LOCALHOST_PING=true. export WELIST_PING_SIG_VERSION=1 export BAL_SERVER_URL="http://127.0.0.1:9133" export SSL_KEY_PATH="$WORKING_DIR/private_key.pem" diff --git a/docs/04_modules_detail.md b/docs/04_modules_detail.md index 8208b7a..96a79fc 100644 --- a/docs/04_modules_detail.md +++ b/docs/04_modules_detail.md @@ -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.` 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.` 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 diff --git a/docs/05_api_reference.md b/docs/05_api_reference.md index d082d0d..412989d 100644 --- a/docs/05_api_reference.md +++ b/docs/05_api_reference.md @@ -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. diff --git a/docs/10_dnssec_key_publishing.md b/docs/10_dnssec_key_publishing.md index 10324c1..55e6604 100644 --- a/docs/10_dnssec_key_publishing.md +++ b/docs/10_dnssec_key_publishing.md @@ -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": "
.onion", "key": "", - "valid_until": 1767225600, + "valid_until": 1800000000, "signature": "" } ``` @@ -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://
.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"`.