4.3 KiB
Game provisioning and signing-key lifecycle
Tracking: #5
Rendezvous treats game and environment scope as provisioned policy, not caller input. Production starts only when it can build an enabled policy registry and load at least one currently active signing key from an external secret provider. Unknown and disabled scopes fail closed.
Policy boundary
Each GamePolicy fixes the allowed:
- game/environment pair and regions;
- exact gameplay protocol versions;
- publisher trust and listing visibility modes;
- metadata keys, required keys, per-value limits, total bytes, and key count;
- listing, anonymous-host, and active-attempt quotas; and
- dedicated fallback feature policy.
Publisher authorization first authenticates a typed principal, then derives the authoritative game/environment from that principal. Request fields are compared for mismatch detection but never replace the authenticated scope. Dedicated workloads, short-lived player-host grants, anonymous unlisted publishers, and operators are distinct principal types. Operator credentials cannot be used as publisher credentials, and anonymous publishers cannot escalate to public visibility.
Signed credentials
Signed principal credentials use the compact form
rv1.<key-id>.<base64url-payload>.<base64url-HMAC-SHA256>. The signed payload
contains version, issuer, audience, subject, principal kind, bounded scope,
issued/not-before/expiry times, and a random nonce. It contains no signing key,
reusable publisher secret, player identity, or gameplay state.
Validation is deliberately ordered and bounded:
- enforce the v1 opaque-credential length and four-segment grammar;
- resolve a known, non-revoked key in its verification window;
- compare the HMAC in fixed time;
- parse canonical bounded JSON;
- require exact version, issuer, and audience;
- enforce clock skew, expiry, key lifetime, principal kind, and scope shape.
Failures return typed internal reasons without echoing the credential. Logs and metrics must record only allowlisted tenant/principal/result dimensions; token, key, secret-reference value, and raw key material are excluded.
Rotation and revocation
A key is bound either to operator credentials only or to allowed publisher credential kinds for exactly one game/environment. The verifier checks this authority after the signature, so even a compromised game grant issuer cannot mint a valid cross-game or operator credential.
A key also has three times: NotBefore, SignUntil, and VerifyUntil. Issuance
picks the newest authorized non-revoked key inside its signing window. Older credentials continue
to verify until the old key's verification window ends, providing an explicit
overlap. After VerifyUntil they fail as retired. Configuration revocation and
runtime revocation both reject immediately. A configured revoked key retains
only its public key ID/lifecycle metadata and does not require retired secret
material to remain available.
Key IDs are non-secret base64url identifiers. Secret references are resolved
through ISecretProvider; production supports base64 env:<VARIABLE> and raw
file:/absolute/path references to bounded non-symlink files. The interface is
replaceable by a deployment-specific vault/KMS adapter. The
committed development profile uses an in-memory random key identified by a
development:ephemeral/... reference. It never writes key material to disk and
all credentials become invalid when the process exits.
Production configuration
Rendezvous:Provisioning supplies issuer, audience, clock skew, signing-key
descriptors, and game policies. A production key reference such as
env:RENDEZVOUS_SIGNING_KEY_2026_01 expects that environment variable to hold at
least 32 random bytes encoded as base64. file:/run/secrets/rendezvous-signing
expects the raw bytes in a read-only, absolute, non-symlink file. Missing,
malformed, short, inactive, or duplicate keys stop startup with a key-ID-only
diagnostic. No game-wide secret
belongs in appsettings, source control, examples, the Client package, URLs,
responses, logs, metrics, exceptions, or diagnostic dumps.
Readiness becomes true only after provisioning and UDP startup both succeed. OpenAPI generation uses a pinned build-only host and does not start listeners or bypass provisioning in a deployed server process.