feat(contrib): add ping key revocation helper
- Add make_ping_key_revocation.py: onion revocations are signed by the identity key, DNS TXT revocations are self-signed by the revoked key - Document the revocation format, endpoint, helper usage, lifetime and no-revocation-of-identity-key limits - Update the security audit, references and env docs for the new flow and the v2/onion-only scope
This commit is contained in:
@@ -10,10 +10,10 @@ In v2 the WeTime service discovers the executor public key from:
|
||||
- the Ed25519 identity key embedded in a v3 `.onion` address, or a dedicated
|
||||
ping key delegated by that identity key.
|
||||
|
||||
This guide covers the DNS TXT record. Onion authentication needs no DNS record
|
||||
and should use delegation so the onion identity key stays offline (see
|
||||
[Onion URLs](#onion-urls)). For local development no record is needed either
|
||||
(see [Local development](#local-development)).
|
||||
Sections 1–10 below cover the DNS TXT record; onion delegation is documented in
|
||||
[Onion URLs](#onion-urls). Onion authentication needs no DNS record and should
|
||||
use delegation so the onion identity key stays offline. For local development
|
||||
no record is needed either (see [Local development](#local-development)).
|
||||
|
||||
All values below use `we.example.com` as a placeholder for the hostname in
|
||||
`BAL_SERVER_URL`. Replace it with your real executor hostname.
|
||||
@@ -290,6 +290,7 @@ For onion hosts, also configure the delegation certificate (see
|
||||
export SSL_KEY_PATH="/absolute/path/to/ping_private_key.pem"
|
||||
export WELIST_PING_DELEGATION_FILE="/absolute/path/to/onion-delegation.json"
|
||||
export BAL_SERVER_URL="http://<address>.onion"
|
||||
export WELIST_PING_SIG_VERSION=2
|
||||
```
|
||||
|
||||
- `SSL_KEY_PATH` is the **dedicated ping key**, not the onion identity key.
|
||||
@@ -304,6 +305,9 @@ export BAL_SERVER_URL="http://<address>.onion"
|
||||
|
||||
Restart the pusher after changing the key or environment.
|
||||
|
||||
If a ping key leaks, issue and submit a revocation before rotating; see
|
||||
[Revocation and lifetime](#revocation-and-lifetime).
|
||||
|
||||
With v2, the pusher also fetches `info`, `base_fee`, and `version` from the
|
||||
local `/{chain}/info` endpoint and signs those values. If the fetch fails, the
|
||||
fields are signed as `null`; the ping is still valid. Metadata is also signed
|
||||
@@ -379,6 +383,12 @@ For an onion pusher with delegation configured:
|
||||
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.
|
||||
4. Revocation check (destructive; use a test executor): issue a revocation with
|
||||
`contrib/make_ping_key_revocation.py --certificate ...`, submit it with
|
||||
`curl`, then send the delegated ping again. The service must log
|
||||
`PING KEY REVOKED` on submission, `PING REJECTED revoked onion delegation`
|
||||
on the ping, and answer `"ko"`. Generate a new ping key and certificate
|
||||
afterwards: a revoked key stays revoked.
|
||||
|
||||
`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
|
||||
@@ -415,13 +425,21 @@ keys for that host.
|
||||
| `malformed record` | Wrong version, algorithm, or syntax | Use `v=we1; alg=ed25519; key=...` |
|
||||
| `invalid key` | Not a base64 Ed25519 SPKI DER | Regenerate with the `openssl` command above |
|
||||
| `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 v2 signature` | Key mismatch or non-canonical URL | Ensure the TXT key matches `SSL_KEY_PATH` and the URL has no trailing slash; for onion delegation, ensure `SSL_KEY_PATH` is the delegated ping key |
|
||||
| `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` 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 |
|
||||
| `invalid revocation` | Signature does not verify, the host/key is invalid, or an onion identity key was targeted | Re-issue with the helper; onion revocations must be signed by the identity key and cannot target it |
|
||||
| `revoked_at is in the future` | Issuer clock is more than 24 h ahead | Use the current time or fix the issuer clock |
|
||||
| `revoked onion delegation` | The delegated key is on the revocation list | Generate a new ping key and certificate; a revoked key cannot be re-enabled |
|
||||
| `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) |
|
||||
|
||||
Pusher warning `disabling onion delegation` (not a service log): the
|
||||
certificate is missing, expired, bound to another host, or does not match
|
||||
`SSL_KEY_PATH`. In the recommended setup onion pings will then be rejected;
|
||||
re-issue the certificate before restarting the pusher.
|
||||
|
||||
Useful diagnostics:
|
||||
|
||||
```bash
|
||||
@@ -515,8 +533,8 @@ 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. `--days` accepts 1–3650 (default 365); prefer short
|
||||
lifetimes as explained in [Revocation and lifetime](#revocation-and-lifetime).
|
||||
`pynacl` is not needed. `--days` accepts 1–3650 (default 365); prefer 30–90
|
||||
days 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.
|
||||
@@ -579,28 +597,97 @@ 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).
|
||||
Rotation does **not** revoke the previous certificate; use the revocation helper
|
||||
below before or after rotating.
|
||||
|
||||
### 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.
|
||||
The WeTime service keeps a permanent, key-scoped revocation list
|
||||
(`POST /ping-keys/revoke`). A revoked key is rejected for both the onion
|
||||
delegation path and the DNSSEC TXT path, even while a cached TXT record or a
|
||||
still-valid certificate lists it.
|
||||
|
||||
Treat the certificate lifetime as the exposure window:
|
||||
#### Onion delegation (identity-signed)
|
||||
|
||||
- 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.
|
||||
The onion identity key signs the revocation, so only the onion owner can revoke
|
||||
a delegated key. The identity key itself cannot be revoked; changing it changes
|
||||
the `.onion` address.
|
||||
|
||||
```bash
|
||||
contrib/make_ping_key_revocation.py \
|
||||
--onion "$ONION" \
|
||||
--identity-key "$ONION_KEY" \
|
||||
--certificate /etc/bal/onion-delegation.json \
|
||||
--service-url "https://<service>" \
|
||||
--out /tmp/onion-revocation.json
|
||||
|
||||
curl -sS -X POST -H 'Content-Type: application/json' \
|
||||
--data @/tmp/onion-revocation.json "https://<service>/ping-keys/revoke"
|
||||
```
|
||||
|
||||
#### DNSSEC TXT key (self-signed)
|
||||
|
||||
For a TXT key the only available authority is the key itself, so the helper
|
||||
signs the revocation with the key being revoked:
|
||||
|
||||
```bash
|
||||
contrib/make_ping_key_revocation.py \
|
||||
--host we.example.com \
|
||||
--key /etc/bal/private_key.pem \
|
||||
--service-url "https://<service>" \
|
||||
--out /tmp/dns-revocation.json
|
||||
|
||||
curl -sS -X POST -H 'Content-Type: application/json' \
|
||||
--data @/tmp/dns-revocation.json "https://<service>/ping-keys/revoke"
|
||||
```
|
||||
|
||||
Also remove the key from `_welist-key.<host>`: revocation makes the stop
|
||||
immediate, but the TXT record should stop advertising the key to new resolvers.
|
||||
If the private key was destroyed and cannot sign the revocation, rotating the
|
||||
TXT record and waiting for the DNS cache TTL is the only remaining path.
|
||||
|
||||
#### Certificate format
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "<address>.onion or a DNS hostname",
|
||||
"key": "<base64 SPKI DER of the revoked Ed25519 key>",
|
||||
"revoked_at": 1800000000,
|
||||
"signature": "<base64 signature>"
|
||||
}
|
||||
```
|
||||
|
||||
The signature covers:
|
||||
|
||||
```text
|
||||
WELIST-PING-REVOCATION-V1\n{"host":"...","key":"...","revoked_at":1800000000}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `host` is normalized (lowercase, no trailing dot).
|
||||
- Onion hosts require the onion identity signature; DNS hosts require a
|
||||
self-signature from the revoked key.
|
||||
- `revoked_at` may be at most 24 hours in the future.
|
||||
- The key id is the hex of the raw public key, so re-encoded base64 cannot
|
||||
bypass the list.
|
||||
- Entries never expire and submissions are idempotent (`"ok"`); at most 64
|
||||
distinct keys are kept per host.
|
||||
- Revoking a key is permanent; a later certificate with the same key is also
|
||||
rejected.
|
||||
|
||||
Prefer short certificate lifetimes (for example 30–90 days) anyway: revocation
|
||||
requires the onion identity key (onion) or the revoked key (DNS), so a key that
|
||||
is both leaked and lost without a remaining signer cannot be revoked.
|
||||
|
||||
### Security notes
|
||||
|
||||
- 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.
|
||||
pings and metadata for that onion host until it is revoked or the certificate
|
||||
expires, but it cannot take over the onion service identity or sign for
|
||||
another host.
|
||||
- A leaked key should be revoked immediately; rotation alone is not a
|
||||
revocation.
|
||||
- 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
|
||||
@@ -631,6 +718,8 @@ authenticated and stored.
|
||||
- [ ] `dig +dnssec` / `delv` shows a validated answer.
|
||||
- [ ] `BAL_SERVER_URL` is the public DNS hostname.
|
||||
- [ ] `WELIST_PING_SIG_VERSION=2` is set.
|
||||
- [ ] `contrib/make_ping_key_revocation.py --host ... --key ...` can produce a
|
||||
self-signed revocation for the TXT key.
|
||||
- [ ] Pusher restarted and reports `body="ok"`.
|
||||
|
||||
For onion hosts, replace the TXT/DNSSEC items with:
|
||||
@@ -644,5 +733,7 @@ For onion hosts, replace the TXT/DNSSEC items with:
|
||||
- [ ] `WELIST_PING_SIG_VERSION=2` is set.
|
||||
- [ ] `valid_until` is strictly in the future, monitored, and rotation is
|
||||
scheduled before it.
|
||||
- [ ] The identity key is available offline so a leaked ping key can be revoked
|
||||
with `contrib/make_ping_key_revocation.py`.
|
||||
- [ ] The pusher starts without a `disabling onion delegation` warning.
|
||||
- [ ] Pusher restarted and reports `body="ok"`.
|
||||
|
||||
Reference in New Issue
Block a user