Instance Verification and Licensing

Date: 2026-05-20 Status: Accepted

Decision

The mobile app supports connecting to any Range Day Pro server via a user-configurable server URL. Connections are verified using a cryptographic license system: T.REX ARMS signs a license per deployment with a private Ed25519 key, the app verifies it with an embedded public key, and no authentication or user data is sent until verification passes. This serves three purposes simultaneously: phishing protection, white-label licensing/DRM, and developer/staging environment switching.

Context

Range Day Pro has multiple deployment contexts:

  • T.REX ARMS SaaS — the default, at rangedaypro.com
  • White-label deployments — military, LE, private organizations running their own server on private or air-gapped networks
  • Staging/development — internal environments for testing before production releases

All of these need to be reachable from the same App Store binary. The app can’t be rebuilt per customer. White-label customers need a way to point the stock app at their server. Developers need to switch between staging and production. And in all cases, the app must not send credentials to an unverified server.

License Issuance

T.REX ARMS holds an Ed25519 private signing key. This key never leaves T.REX infrastructure. The corresponding public key is embedded in the app binary (compiled into the Rust core crate).

When onboarding a white-label customer, T.REX generates a license file:

{
  "customer": "Springfield PD",
  "domains": ["training.springfieldpd.gov"],
  "issued": "2026-06-01",
  "expires": "2027-06-01",
  "capabilities": {
    "max_users": 500,
    "public_signup": false,
    "billing": false,
    "drill_catalog": false
  }
}

The claims are signed with the private key, producing a license file. The customer places this file alongside their config.yaml on the server. The T.REX SaaS instance has its own license (signed for rangedaypro.com) — no special cases in the app.

A small CLI tool or rangeday license issue command handles generation. Ed25519 signing is straightforward with ed25519-dalek.

Handshake Protocol

When a user enters a server URL, the app performs a verification handshake before showing any login UI:

User enters URL
       |
       v
App calls GetInstanceInfo (unauthenticated, no user data)
       |
       v
Server returns InstanceInfo:
  - instance_name, org_name
  - branding (colors, logo_url)
  - capability flags
  - license claims (customer, domains, expires, capabilities)
  - license signature (Ed25519 bytes)
       |
       v
App verifies (all must pass):
  1. Signature valid (using embedded public key)
  2. URL user entered matches a domain in the license claims
  3. License not expired
       |
  pass |         fail
       v           v
Show login      "Not a verified Range Day Pro
with custom     server" — no auth UI shown
branding

No user credentials, tokens, or identifying information are sent before verification passes. The handshake is a single unauthenticated RPC.

Proto Definition

New service in instance.v1:

service InstanceService {
  rpc GetInstanceInfo(GetInstanceInfoRequest) returns (GetInstanceInfoResponse);
}
 
message GetInstanceInfoRequest {}
 
message GetInstanceInfoResponse {
  InstanceInfo instance = 1;
}
 
message InstanceInfo {
  string instance_name = 1;       // "Range Day Pro" or "Springfield PD Training"
  string org_name = 2;            // "T.REX ARMS" or "Springfield Police Department"
  BrandingConfig branding = 3;
  CapabilityFlags capabilities = 4;
  string api_version = 5;         // proto compatibility check
  bytes license_claims = 6;       // raw JSON bytes of the license claims
  bytes license_signature = 7;    // Ed25519 signature over license_claims
}
 
message BrandingConfig {
  string primary_color = 1;
  string accent_color = 2;
  string logo_url = 3;            // relative to server base URL
  string favicon_url = 4;
}
 
message CapabilityFlags {
  bool public_signup = 1;
  bool billing_enabled = 2;
  bool drill_catalog = 3;
}

The server reads its config.yaml and license file at startup and serves the response from memory. The license_claims field is the raw signed bytes (not re-serialized), ensuring the signature verification is exact.

App UI

The server URL selector is minimal — visible but not prominent. It sits at the bottom of the login screen as small, muted text:

+-----------------------------+
|                             |
|      [ T.REX ARMS LOGO ]   |
|        Range Day Pro        |
|                             |
|  Email:    [              ] |
|  Password: [              ] |
|                             |
|        [ Sign In ]          |
|        [ Register ]         |
|                             |
|                             |
|  rangedaypro.com  -  Change |
|                             |
+-----------------------------+

Behavior:

  • Default state: rangedaypro.com — 95% of users never touch this.
  • “Change” tap: Opens a sheet/modal with a URL input field. User types a URL, app runs the handshake.
  • On success: Login screen re-renders with the server’s branding (name, colors, logo fetched from logo_url). The URL persists in local storage.
  • On failure: Error message in the modal (“Not a verified Range Day Pro server” or “License expired”). No navigation to login.
  • Switching servers: Clears local auth tokens (old tokens are meaningless for a different server). If unsynced scores exist in the offline queue, prompts the user before clearing.

For white-label IT admins, the instruction is: “Download Range Day Pro from the App Store. Tap ‘Change’ at the bottom of the login screen. Enter training.springfieldpd.gov.”

The capability flags from the handshake also control what UI is shown. If public_signup is false, the “Register” button is hidden. If billing_enabled is false, no payment UI anywhere in the app.

Security Properties

ThreatMitigation
Phishing server harvests credentialsApp verifies Ed25519 signature before showing login. Attacker cannot forge a signature without T.REX’s private key.
Attacker stands up fake serverWithout a valid license signed by T.REX, the app refuses to connect.
License sharing between customersDomain binding — the license is only valid for the domains listed in its claims.
Expired white-label customerLicense has an expiration date. App and server both reject expired licenses.
Man-in-the-middleTLS required (app rejects HTTP URLs). Combined with license verification, both transport and application layers are secured.
Reverse-engineering the app binaryOnly the public key is embedded. Attacker can verify signatures but not forge them.
Binary patching to skip verificationNon-trivial, breaks on every app update, and is a license violation. Proportionate deterrent for the threat model.

DRM Properties

The license system doubles as white-label DRM:

  • T.REX controls distribution. Only servers with a T.REX-issued license are accepted by the app. Running the server binary without a valid license means no mobile app can connect.
  • Domain-bound. A license for training.springfieldpd.gov cannot be reused on training.otherpd.gov.
  • Time-bound. Expiration dates enable subscription-based white-label licensing. Customer renews, receives a new license file, drops it on their server.
  • Capability-bound. The license claims can restrict features (max users, billing, drill catalog). The server enforces these server-side; the app uses them for UI adaptation.
  • Offline-compatible. Verification is purely cryptographic — no phone-home to T.REX servers. Works on air-gapped military networks.

All Clients Protected

The verification logic lives in libs/core, which all four client apps import:

ClientImport methodNotes
iOSUniFFI (Swift binding)Native binary, same as any compiled app
AndroidUniFFI (Kotlin binding)Native binary, same as any compiled app
PWADirect crate import (Rust WASM)WASM binary format, not human-readable source
DashboardDirect crate import (Rust WASM)WASM binary format, not human-readable source

All four clients run the same handshake before showing any auth UI. No platform-specific verification code.

The embedded public key is discoverable by a determined reverse engineer on any platform (native disassembly, WASM decompilation). This is fine — the public key only allows verification of signatures, not forgery. The security model assumes all client binaries are public. The private signing key never leaves T.REX infrastructure.

Implementation

Verification lives in core

Crate: ed25519-dalek (pure Rust, no-std compatible, works on WASM/iOS/Android).

Server serves the response

The server loads config.yaml + license file at startup. GetInstanceInfo is an unauthenticated ConnectRPC handler that returns the pre-loaded response. The license file bytes are served as-is (not re-serialized) to preserve the exact signed payload.

The server may also validate its own license at startup and refuse to start if expired, providing a server-side enforcement layer independent of the app.

License generation tool

A CLI command (rangeday license issue) or standalone script that takes customer details and the private key, produces a signed license file. Used during white-label customer onboarding.

Consequences

  • A new instance.v1 proto service is needed
  • The Rust core crate gains a dependency on ed25519-dalek
  • T.REX ARMS must securely store and manage the Ed25519 private signing key
  • White-label customer onboarding includes a license generation step
  • The app login screen gains a small server URL selector
  • Server URL and verified instance info are persisted in local storage on all clients
  • The T.REX SaaS instance needs its own license file (no special-casing)