AtlasAuth

AtlasAuth Security

This page explains, in full, how AtlasAuth resists spoofing and what it does and does not protect against. If you read only one page before shipping, read this one. It assumes you have skimmed overview.md.

The short version: every security-relevant response is signed by the server with an ECDSA P-256 private key that never leaves the server, and your client embeds only the public key. A forged or replayed response cannot pass verification. What signing *cannot* do is make code honest on a machine its owner controls. The threat-model section at the end is candid about that and lists mitigations.


1. The signed envelope

Every security-relevant response is HTTP 200 with exactly this body:

json
{ "payload": "<compact-json-string>", "sig": "<base64-p1363-sig>" }

Every payload object contains at minimum:

json
{ "v": 1, "t": 1751299200, "nonce": "<echo of request nonce>", "ok": true }

ok:false is still a signed truth (wrong password, expired key, app disabled). It is trustworthy and should drive your user-facing messaging through the error/code fields. Only an unsigned { "error": "...", "code": "..." } body with a 4xx/5xx status comes back for transport-level failures (unknown app, malformed body, rate limited). The client must treat any unverifiable response as a failed, untrusted call.


2. ECDSA P-256 signing

The exact verification sequence

Apply this to every signed response before you trust any field:

  1. Read payload as an opaque UTF-8 string. Do not re-serialize it. Re-serialization reorders keys or changes whitespace and breaks the signature.
  2. base64-decode sig → exactly 64 bytes.
  3. Verify ECDSA-P256-SHA256(sig, utf8(payload), embeddedPublicKey). If invalid → treat the whole call as failed/hostile and abort. Do not parse.
  4. Only then JSON.parse(payload).
  5. Check payload.nonce equals the nonce the client sent. If not → replay/tamper → abort.
  6. (Advisory) Check abs(localUnixTime - payload.t) <= 60.

The single most common integration bug is re-serializing the payload (parsing it to an object and re-stringifying) before verifying. Sign and verify the raw string bytes. A different key order or a single changed space makes a valid signature look invalid.

Why this defeats forgery and MITM

The verifying key is public. It can confirm that a signature was produced by the matching private key, but it cannot produce signatures. An attacker who extracts everything from your binary gets the app_id and the public key. Neither one lets them sign. To return a response your client accepts, they would need the private key, which exists only on the server. A fake server, a patched host entry, a local proxy, or a man in the middle can all *send* bytes to your client, but none can sign them, so step 3 rejects them.


3. Nonce / replay defense

A signature alone proves a response is *genuine*, not that it is *fresh*. Without freshness, an attacker could capture one real "you are licensed" response and replay it forever.

AtlasAuth ties every response to a one-time challenge:

Because the nonce is inside the signed bytes, an attacker cannot swap in the nonce of the current request without breaking the signature. So a captured response cannot be replayed: its baked-in nonce will not match any future request's fresh nonce.

Implementation requirements:


4. Timestamp / clock skew

Each signed payload carries t, the server's unix time at signing. After verifying the signature and nonce, the client runs an advisory check:

text
abs(localUnixTime - payload.t) <= 60   // API_MAX_SKEW_SECONDS

This is a sanity signal, not the primary defense. The nonce check is what makes replay infeasible, and it does so no matter what the clocks say. Skew is advisory because legitimate clients sometimes have a badly set clock. Treat a violation as a warning (log it, optionally surface it) rather than a hard failure, since blocking on it would lock out users whose only mistake is a wrong system clock. The nonce, which does not depend on time, stays authoritative against replay.


5. HTTPS / TLS

All API traffic is HTTPS to https://atlasauth.cc/api/v1. TLS gives you:

TLS and signing are complementary, not redundant. TLS protects the channel to the *real* endpoint, but an attacker who controls the client machine can redirect the hostname, install a custom root CA, or run a local proxy, which defeats TLS's server-authentication guarantee from the client's point of view. Response signing is the backstop: even if the attacker fully owns the TLS channel, they still cannot produce a validly signed payload. Do not disable certificate validation, and do not "fall back" to trusting an unsigned response when verification fails.


6. Key handling - what lives where

SecretLocationTouches the client?
App private key (ECDSA P-256)Cloudflare Worker (server)Never
Supabase service keyCloudflare Worker (server)Never
App public key (SPKI DER)Embedded in the SDKYes, public, safe to embed
app_id (UUID)Embedded in the SDKYes, public identifier

The SDK contains no secret. The two values you paste in, app_id and the public key, are both public. The private key (used to sign) and the Supabase service key (used by the Worker to reach the database) stay server-side. That is the whole point: there is nothing in the binary whose extraction lets an attacker forge a response or read the database.

If a private key is ever suspected compromised, rotate it server-side and ship clients with the new public key. Because clients hold only the public half, a public-key value being known is never itself a compromise.


7. HWID binding and exact-match bans

The SDK computes an opaque hwid per machine:

Server behavior:

HWID is an abuse-control tool (one key, one machine; ban a specific machine), not a cryptographic identity. A determined attacker can spoof hardware identifiers, which is exactly why bans are exact-match (precise, no collateral) and why HWID is one layer among several rather than the only gate.


8. App-status kill switch via heartbeat

Every init and every check returns app_status (active | maintenance | disabled) and a status_message. The client re-reads it every heartbeat and must kick the user immediately if status is not active.

This is a remote kill switch. Setting an app to disabled or maintenance in the dashboard drops every running client within one heartbeat interval (default 10 seconds). The same heartbeat also enforces, per beat:

The client kicks if any of ok==false, valid==false, app_status!="active", key_valid==false, or banned==true. Because each check is itself a signed, nonce-bound response, a fake server cannot suppress the kill switch by feeding fake "all good" replies. Those replies cannot be signed.

Make the heartbeat meaningful: if your client can keep running forever after init without ever depending on a verified check, an attacker can simply block network access. Tie real functionality to recent, verified heartbeats so a silenced client degrades rather than runs free.


9. Threat model

What AtlasAuth defends against (and how)

ThreatDefense
Forged server response ({"ok":true} minted by attacker)ECDSA P-256 signature; private key is server-only
Fake / MITM server, DNS or hosts redirect, local proxySignature verification fails on any payload not signed by the private key
Replay of a previously captured valid responsePer-request fresh nonce echoed inside the signed payload
Stale response / clock-roll tricksAdvisory t skew check (≤60 s) backing up the nonce
Eavesdropping / credential theft on the wireHTTPS / TLS
Sharing one key across many machinesHWID binding (exact match) + per-app/key HWID lock
Continued use after revocation/expiry/banSigned check heartbeat re-evaluated every interval
Remotely disabling a compromised releaseApp-status kill switch delivered via signed init/check
Binary secret extractionThere is no secret in the binary to extract

Residual risks - what client-side auth cannot guarantee

No client-side system can make a program honest on hardware the attacker fully controls. Be clear-eyed about these:

  1. Native-app memory tampering. An attacker can patch your compiled binary to skip the verification branch, or edit process memory at runtime to flip an isAuthenticated flag *after* a legitimate check passed. The signature was valid; the attacker simply ignored the result. Signing protects the conversation with the server, not the integrity of your process against its owner.
  2. Code lifting. If a protected feature is fully present in the client and never truly needs the server, an attacker can rip it out and run it standalone. No bypass of AtlasAuth is required, because nothing was gated on the server.
  3. Bypassing the network. Blocking the API endpoint stops heartbeats. If your client keeps full functionality when check stops succeeding, the attacker just firewalls the domain.
  4. HWID spoofing. Hardware identifiers can be faked, which weakens one-machine-per-key enforcement (bans stay precise because they are exact-match, but a spoofed HWID is still a spoofed HWID).

Mitigations for the residual risks

These reduce, but cannot eliminate, the client-trust problem:

Bottom line

AtlasAuth gives you a cryptographically strong guarantee that your client is talking to the real server and the server's answers are genuine and fresh. That defeats the forgery, MITM, and replay attacks that break naive licensing. It does not guarantee honest execution on a hostile machine. So put what truly matters behind a verified, authenticated server response, and treat the client as the convenient-but-untrusted edge it inherently is.