feat(push): add iOS APNs relay gateway (#43369)

* feat(push): add ios apns relay gateway

* fix(shared): avoid oslog string concatenation

# Conflicts:
#	apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift

* fix(push): harden relay validation and invalidation

* fix(push): persist app attest state before relay registration

* fix(push): harden relay invalidation and url handling

* feat(push): use scoped relay send grants

* feat(push): configure ios relay through gateway config

* feat(push): bind relay registration to gateway identity

* fix(push): tighten ios relay trust flow

* fix(push): bound APNs registration fields (#43369) (thanks @ngutman)
This commit is contained in:
Nimrod Gutman
2026-03-12 18:15:35 +02:00
committed by GitHub
parent 9342739d71
commit b77b7485e0
36 changed files with 3249 additions and 203 deletions

View File

@@ -2447,6 +2447,14 @@ See [Plugins](/tools/plugin).
// Remove tools from the default HTTP deny list
allow: ["gateway"],
},
push: {
apns: {
relay: {
baseUrl: "https://relay.example.com",
timeoutMs: 10000,
},
},
},
},
}
```
@@ -2472,6 +2480,11 @@ See [Plugins](/tools/plugin).
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
- `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`: client-side break-glass override that allows plaintext `ws://` to trusted private-network IPs; default remains loopback-only for plaintext.
- `gateway.remote.token` / `.password` are remote-client credential fields. They do not configure gateway auth by themselves.
- `gateway.push.apns.relay.baseUrl`: base HTTPS URL for the external APNs relay used by official/TestFlight iOS builds after they publish relay-backed registrations to the gateway. This URL must match the relay URL compiled into the iOS build.
- `gateway.push.apns.relay.timeoutMs`: gateway-to-relay send timeout in milliseconds. Defaults to `10000`.
- Relay-backed registrations are delegated to a specific gateway identity. The paired iOS app fetches `gateway.identity.get`, includes that identity in the relay registration, and forwards a registration-scoped send grant to the gateway. Another gateway cannot reuse that stored registration.
- `OPENCLAW_APNS_RELAY_BASE_URL` / `OPENCLAW_APNS_RELAY_TIMEOUT_MS`: temporary env overrides for the relay config above.
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true`: development-only escape hatch for loopback HTTP relay URLs. Production relay URLs should stay on HTTPS.
- Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.

View File

@@ -225,6 +225,63 @@ When validation fails:
</Accordion>
<Accordion title="Enable relay-backed push for official iOS builds">
Relay-backed push is configured in `openclaw.json`.
Set this in gateway config:
```json5
{
gateway: {
push: {
apns: {
relay: {
baseUrl: "https://relay.example.com",
// Optional. Default: 10000
timeoutMs: 10000,
},
},
},
},
}
```
CLI equivalent:
```bash
openclaw config set gateway.push.apns.relay.baseUrl https://relay.example.com
```
What this does:
- Lets the gateway send `push.test`, wake nudges, and reconnect wakes through the external relay.
- Uses a registration-scoped send grant forwarded by the paired iOS app. The gateway does not need a deployment-wide relay token.
- Binds each relay-backed registration to the gateway identity that the iOS app paired with, so another gateway cannot reuse the stored registration.
- Keeps local/manual iOS builds on direct APNs. Relay-backed sends apply only to official distributed builds that registered through the relay.
- Must match the relay base URL baked into the official/TestFlight iOS build, so registration and send traffic reach the same relay deployment.
End-to-end flow:
1. Install an official/TestFlight iOS build that was compiled with the same relay base URL.
2. Configure `gateway.push.apns.relay.baseUrl` on the gateway.
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
Operational notes:
- If you switch the iOS app to a different gateway, reconnect the app so it can publish a new relay registration bound to that gateway.
- If you ship a new iOS build that points at a different relay deployment, the app refreshes its cached relay registration instead of reusing the old relay origin.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.
</Accordion>
<Accordion title="Set up heartbeat (periodic check-ins)">
```json5
{

View File

@@ -49,6 +49,114 @@ openclaw nodes status
openclaw gateway call node.list --params "{}"
```
## Relay-backed push for official builds
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
token to the gateway.
Gateway-side requirement:
```json5
{
gateway: {
push: {
apns: {
relay: {
baseUrl: "https://relay.example.com",
},
},
},
},
}
```
How the flow works:
- The iOS app registers with the relay using App Attest and the app receipt.
- The relay returns an opaque relay handle plus a registration-scoped send grant.
- The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway.
- The app forwards that relay-backed registration to the paired gateway with `push.apns.register`.
- The gateway uses that stored relay handle for `push.test`, background wakes, and wake nudges.
- The gateway relay base URL must match the relay URL baked into the official/TestFlight iOS build.
- If the app later connects to a different gateway or a build with a different relay base URL, it refreshes the relay registration instead of reusing the old binding.
What the gateway does **not** need for this path:
- No deployment-wide relay token.
- No direct APNs key for official/TestFlight relay-backed sends.
Expected operator flow:
1. Install the official/TestFlight iOS build.
2. Set `gateway.push.apns.relay.baseUrl` on the gateway.
3. Pair the app to the gateway and let it finish connecting.
4. The app publishes `push.apns.register` automatically after it has an APNs token, the operator session is connected, and relay registration succeeds.
5. After that, `push.test`, reconnect wakes, and wake nudges can use the stored relay-backed registration.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
## Authentication and trust flow
The relay exists to enforce two constraints that direct APNs-on-gateway cannot provide for
official iOS builds:
- Only genuine OpenClaw iOS builds distributed through Apple can use the hosted relay.
- A gateway can send relay-backed pushes only for iOS devices that paired with that specific
gateway.
Hop by hop:
1. `iOS app -> gateway`
- The app first pairs with the gateway through the normal Gateway auth flow.
- That gives the app an authenticated node session plus an authenticated operator session.
- The operator session is used to call `gateway.identity.get`.
2. `iOS app -> relay`
- The app calls the relay registration endpoints over HTTPS.
- Registration includes App Attest proof plus the app receipt.
- The relay validates the bundle ID, App Attest proof, and Apple receipt, and requires the
official/production distribution path.
- This is what blocks local Xcode/dev builds from using the hosted relay. A local build may be
signed, but it does not satisfy the official Apple distribution proof the relay expects.
3. `gateway identity delegation`
- Before relay registration, the app fetches the paired gateway identity from
`gateway.identity.get`.
- The app includes that gateway identity in the relay registration payload.
- The relay returns a relay handle and a registration-scoped send grant that are delegated to
that gateway identity.
4. `gateway -> relay`
- The gateway stores the relay handle and send grant from `push.apns.register`.
- On `push.test`, reconnect wakes, and wake nudges, the gateway signs the send request with its
own device identity.
- The relay verifies both the stored send grant and the gateway signature against the delegated
gateway identity from registration.
- Another gateway cannot reuse that stored registration, even if it somehow obtains the handle.
5. `relay -> APNs`
- The relay owns the production APNs credentials and the raw APNs token for the official build.
- The gateway never stores the raw APNs token for relay-backed official builds.
- The relay sends the final push to APNs on behalf of the paired gateway.
Why this design was created:
- To keep production APNs credentials out of user gateways.
- To avoid storing raw official-build APNs tokens on the gateway.
- To allow hosted relay usage only for official/TestFlight OpenClaw builds.
- To prevent one gateway from sending wake pushes to iOS devices owned by a different gateway.
Local/manual builds remain on direct APNs. If you are testing those builds without the relay, the
gateway still needs direct APNs credentials:
```bash
export OPENCLAW_APNS_TEAM_ID="TEAMID"
export OPENCLAW_APNS_KEY_ID="KEYID"
export OPENCLAW_APNS_PRIVATE_KEY_P8='-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
```
## Discovery paths
### Bonjour (LAN)