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:
{ "payload": "<compact-json-string>", "sig": "<base64-p1363-sig>" }payloadis a compact JSON string. It is the signed content. The client treats it as an opaque UTF-8 string and verifies the signature over those exact bytes.sigis the signature, base64-encoded.
Every payload object contains at minimum:
{ "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
- Algorithm: ECDSA.
- Curve: P-256 (a.k.a.
prime256v1/secp256r1). - Hash: SHA-256.
- Signature encoding: raw IEEE P1363
r || s, exactly 64 bytes, then base64 (standard, with padding). This is *not* DER. The C++ SDK converts P1363 to DER internally for OpenSSL; the C# SDK usesDSASignatureFormat.IeeeP1363directly. - Public key the developer embeds: SPKI DER, base64. C# loads it with
ImportSubjectPublicKeyInfo; OpenSSL loads it withd2i_PUBKEY.
The exact verification sequence
Apply this to every signed response before you trust any field:
- Read
payloadas an opaque UTF-8 string. Do not re-serialize it. Re-serialization reorders keys or changes whitespace and breaks the signature. - base64-decode
sig→ exactly 64 bytes. - Verify
ECDSA-P256-SHA256(sig, utf8(payload), embeddedPublicKey). If invalid → treat the whole call as failed/hostile and abort. Do not parse. - Only then
JSON.parse(payload). - Check
payload.nonceequals the nonce the client sent. If not → replay/tamper → abort. - (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:
- The client generates a fresh random nonce (≥16 bytes, hex or base64url) for every request and stores it until the matching response is verified.
- The server echoes that exact nonce inside the signed payload.
- After verifying the signature, the client checks
payload.nonce == the nonce it sent. A mismatch means the response is stale or belongs to a different request → abort.
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:
- Use a CSPRNG (
RandomNumberGeneratorin C#,RAND_bytesin OpenSSL). Never a non-cryptographic PRNG. - One nonce per request; never reuse. The heartbeat loop generates a new nonce on every
check. - Hold the sent nonce until its response is verified, then discard it.
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:
abs(localUnixTime - payload.t) <= 60 // API_MAX_SKEW_SECONDSThis 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:
- Confidentiality: credentials, license keys, and HWIDs are not exposed on the wire.
- Server authentication: the certificate authenticates
atlasauth.cc. - Integrity in transit: TLS detects tampering.
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
| Secret | Location | Touches the client? |
|---|---|---|
| App private key (ECDSA P-256) | Cloudflare Worker (server) | Never |
| Supabase service key | Cloudflare Worker (server) | Never |
| App public key (SPKI DER) | Embedded in the SDK | Yes, public, safe to embed |
app_id (UUID) | Embedded in the SDK | Yes, 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:
- Default:
base64( SHA256( ...stable hardware ids... ) ). - Seeded: if you set a secret seed, the SDK returns
base64( SHA256("atlasauth-hwid-seed|" + seed) ). This is reproducible on any machine that knows the seed (so a legitimate owner can move their own key between their machines) and unguessable to anyone who does not.
Server behavior:
- The server binds the first HWID it sees for a key/user, then requires an exact match on later activations. A mismatch returns
code:"hwid_mismatch". - HWID bans match the full hwid string with exact equality only. There is no prefix, fuzzy, or partial matching, so a ban can never produce a false positive against an unrelated machine.
- Resetting a HWID clears the binding so the key re-binds on its next activation (use this when a user legitimately changes hardware).
- Per-app
hwid_lockcan be off (a key works anywhere) or on; an individual key can override the app default.
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:
valid == false→ session killed server-side (reason: "killed").key_valid == false→ bound key expired or banned.banned == true→ user/HWID/IP banned since login.expiry/remaining_seconds→ time-based access running out.
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)
| Threat | Defense |
|---|---|
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 proxy | Signature verification fails on any payload not signed by the private key |
| Replay of a previously captured valid response | Per-request fresh nonce echoed inside the signed payload |
| Stale response / clock-roll tricks | Advisory t skew check (≤60 s) backing up the nonce |
| Eavesdropping / credential theft on the wire | HTTPS / TLS |
| Sharing one key across many machines | HWID binding (exact match) + per-app/key HWID lock |
| Continued use after revocation/expiry/ban | Signed check heartbeat re-evaluated every interval |
| Remotely disabling a compromised release | App-status kill switch delivered via signed init/check |
| Binary secret extraction | There 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:
- 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
isAuthenticatedflag *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. - 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.
- Bypassing the network. Blocking the API endpoint stops heartbeats. If your client keeps full functionality when
checkstops succeeding, the attacker just firewalls the domain. - 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:
- Make the server the source of truth. Deliver real secrets, premium content, server-side computation, or licensed data only after a verified, authenticated
check. A cracked client that flips a boolean then has nothing to unlock, because the thing of value was never in the binary. This is the single most effective mitigation. - Tie functionality to live, verified heartbeats, not just to a one-time
init. A client cut off from the server should degrade, not run free. - Fail closed. On any verification failure, nonce mismatch, or unverifiable response, deny access. Never fall back to trusting unsigned data.
- Use the kill switch and bans. When you detect abuse, disable the app or ban the key/user/HWID; signed heartbeats propagate the decision within one interval.
- Defense in depth on native clients (integrity checks, anti-debugging, obfuscation) raises the cost of patching. Treat these as speed bumps layered on top of server-side enforcement, not as the primary control.
- Rotate keys server-side if a private key is ever suspected compromised; ship clients with the new public key.
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.
