feat(pusher): add opt-in v2 WeTime ping signing

- Add WELIST_PING_SIG_VERSION (1 legacy, 2 canonical JSON); default 1
- Sign WELIST-PING-V2\n + compact sorted JSON covering url, chain, height,
  median_time, last_block_hash, info, base_fee, version and sig_version
- Fetch info/base_fee/version from the local /{chain}/info endpoint and sign them
- Fall back to null metadata on fetch failure; skip the fetch for onion URLs
- Normalize the advertised URL and bound metadata to match the WeTime service
- Activate v2 in bal-pusher.sh and document the TXT record setup
- Add unit tests for canonical payloads, URL normalization, metadata and onion detection

Includes previously uncommitted local deployment hardening:
- pg-server.sh configurable loopback bind address, volume and recreate handling
- .env loading in bal-server.sh and bal-pusher.sh
- second bal-server/bal-pusher development scripts
This commit is contained in:
2026-11-07 20:37:11 -04:00
parent b8d87ecabd
commit 0eff7a649a
12 changed files with 614 additions and 32 deletions

View File

@@ -147,11 +147,34 @@ The client tries username/password auth first, then falls back to cookie file au
---
## Ed25519 Stats Signing
## Ed25519 WeTime Ping Signing
The pusher signs the statistics payload before sending it to the `welist` server:
1. Collects statistics from the database.
2. Serializes the stats as JSON.
3. Signs the JSON payload with the Ed25519 private key (`privkey.pem`).
4. Sends the payload with the base64-encoded signature in the `X-Signature` header.
5. The `welist` server can verify the signature using the public key served at `GET /.pub_key.pem`.
The pusher signs the report before sending it to the `welist` server. The
scheme is selected with `WELIST_PING_SIG_VERSION`:
### Version 2 (canonical JSON, preferred)
1. Fetches `info`, `base_fee` and `version` from the local
`GET /{chain}/info` endpoint (failure falls back to `null`).
2. Builds a compact JSON object with lexicographically sorted keys, every field
present, and `null` for absent optional values:
`base_fee`, `chain`, `height`, `info`, `last_block_hash`, `median_time`,
`sig_version`, `url`, `version`.
3. Signs `WELIST-PING-V2\n` followed by that JSON with the Ed25519 private key
(`SSL_KEY_PATH`).
4. POSTs the same fields plus the base64 signature and `"sig_version": 2` to
`{WELIST_SERVER_URL}/ping`.
5. The `welist` server discovers the public key from a DNSSEC-validated
`_welist-key.<host>` TXT record (`v=we1; alg=ed25519; key=<base64 SPKI DER>`)
or from a v3 `.onion` address. No HTTP callback to `/.pub_key.pem` is made.
The `url` is trimmed of surrounding whitespace and trailing slashes, and the
`chain` is lowercase, matching the service-side canonicalization.
### Version 1 (legacy)
1. Concatenates `url + chain + height + median_time + last_block_hash`.
2. Signs that string with the Ed25519 private key.
3. POSTs the fields plus the base64 signature to `{WELIST_SERVER_URL}/ping`.
4. The `welist` server verifies the signature with the public key served at
`GET /.pub_key.pem`; this scheme only refreshes `last_update`.