You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Customer's running deployment will fail its next heartbeat (within 24h) and refuse to serve.
135
+
Customer's running deployment will fail its next heartbeat and refuse to serve.
136
+
137
+
## Signed heartbeat contract
138
+
139
+
The client sends a fresh unpadded-base64url 16-byte `request_nonce`. The Worker
140
+
returns an Ed25519-signed `tether.license.heartbeat` v1 attestation containing
141
+
the echoed nonce, `license_id`, active signing `key_id`, Unix `issued_at`,
142
+
`valid_until`, and status (`active`, `expired`, or `revoked`; clients also
143
+
understand `suspended`). Active validity is capped at 24 hours and at the
144
+
license expiry. Released clients accept five minutes of clock skew, persist
145
+
only verified attestations, and fail paid requests closed when the signed
146
+
deadline plus skew elapses.
110
147
111
148
## Privacy posture
112
149
@@ -142,14 +179,22 @@ WHERE l.revoked_at IS NULL
142
179
);
143
180
```
144
181
145
-
## Key rotation (Phase 2 — not implemented yet)
182
+
## Key rotation
146
183
147
184
The schema supports key rotation via the `master_keys.retired_at` column, but the rotation endpoint (`POST /admin/rotate`) isn't built yet. When you need it:
148
185
149
-
1. Generate a new Ed25519 keypair (new POST /admin/init variant)
150
-
2. New licenses get signed with the new key
151
-
3. Old key stays valid for verification (grace period)
152
-
4. Customer-side bundled key gets a list of N trusted keys instead of one
153
-
5. Eventually retire the old key when no licenses signed with it remain
186
+
1. Generate the new Ed25519 pair through an audited rotation procedure and
187
+
insert its public half as a new non-retired `master_keys` row. Keep the old
188
+
row active.
189
+
2. Ship both old and new public keys in `TRUSTED_PUBLIC_KEYS_B64` and release
190
+
that client trust overlap before changing the signer.
191
+
3. Set `PRIVATE_KEY` and `SIGNING_KEY_ID` to the new coupled pair in one
192
+
controlled deployment window, then require authenticated `/admin/signer`
193
+
to return the new id with `verified: true`. Roll back both secrets together
194
+
if verification fails.
195
+
4. Keep the old public key trusted throughout the maximum license/attestation
196
+
overlap.
197
+
5. Retire the old D1 row and remove its client trust only after that overlap
198
+
has elapsed.
154
199
155
200
Plan to revisit when you have ~50 active licenses or a security incident requires rotation.
err "Existing-signer migration requires REFLEX_ADMIN_TOKEN so /admin/signer can verify the binding immediately after deploy. SIGNING_KEY_ID and Worker code were not changed."
175
+
exit 1
176
+
fi
177
+
fi
178
+
if! wrangler secret list 2>/dev/null | grep -q '"PRIVATE_KEY"';then
179
+
err "D1 has active key ${KEY_ID}, but PRIVATE_KEY is missing. Restore the matching private key; do not generate a replacement for the existing public row."
180
+
exit 1
181
+
fi
182
+
info "Binding existing active D1 key ${KEY_ID} before deploying signer enforcement..."
183
+
echo -n "$KEY_ID"| wrangler secret put SIGNING_KEY_ID
184
+
ok "Existing SIGNING_KEY_ID set before deploy"
185
+
elif [ "$KEY_SELECT_STATUS"-eq 3 ];then
186
+
info "No active D1 signing key; fresh install will initialize one after deploy"
187
+
else
188
+
err "Could not select the existing signing key safely. If rotation overlap leaves multiple active rows, set TETHER_SIGNING_KEY_ID to the row that matches PRIVATE_KEY."
189
+
exit 1
190
+
fi
191
+
192
+
# ─── 9. Deploy the worker ────────────────────────────────────────────────────
0 commit comments