Skip to content

Commit d76da43

Browse files
committed
Ed25519 public-key verification
1 parent 334013f commit d76da43

4 files changed

Lines changed: 223 additions & 446 deletions

File tree

README.md

Lines changed: 117 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,118 @@
1-
# AuthForge Python SDK
2-
3-
Official Python SDK for [AuthForge](https://authforge.cc) — credit-based license key authentication with HMAC-verified heartbeats.
4-
5-
**Zero dependencies.** Standard library only. Works on Python 3.9+.
6-
7-
## Quick Start
8-
9-
Copy `authforge.py` into your project, then:
10-
11-
```python
12-
from authforge import AuthForgeClient
13-
14-
client = AuthForgeClient(
15-
app_id="YOUR_APP_ID", # from your AuthForge dashboard
16-
app_secret="YOUR_APP_SECRET", # from your AuthForge dashboard
17-
heartbeat_mode="SERVER", # "SERVER" or "LOCAL"
18-
)
19-
20-
license_key = input("Enter license key: ")
21-
22-
if client.login(license_key):
23-
print("Authenticated!")
24-
# Your app logic here — heartbeats run automatically in the background
25-
else:
26-
print("Invalid license key.")
27-
exit(1)
28-
```
29-
30-
## Configuration
31-
32-
| Parameter | Type | Default | Description |
33-
|---|---|---|---|
34-
| `app_id` | str | required | Your application ID from the AuthForge dashboard |
35-
| `app_secret` | str | required | Your application secret from the AuthForge dashboard |
36-
| `heartbeat_mode` | str | required | `"SERVER"` or `"LOCAL"` (see below) |
37-
| `heartbeat_interval` | int | `900` | Seconds between heartbeat checks (default 15 min) |
38-
| `api_base_url` | str | `https://auth.authforge.cc` | API endpoint |
39-
| `on_failure` | callable | `None` | Callback `(reason: str, exc: Exception | None)` on auth failure |
40-
| `request_timeout` | int | `15` | HTTP request timeout in seconds |
41-
42-
## Methods
43-
44-
| Method | Returns | Description |
45-
|---|---|---|
46-
| `login(license_key)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
47-
| `logout()` | `None` | Stops heartbeat and clears all session/auth state |
48-
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
49-
| `get_session_data()` | `dict \| None` | Full decoded payload map |
50-
| `get_app_variables()` | `dict \| None` | App-scoped variables map |
51-
| `get_license_variables()` | `dict \| None` | License-scoped variables map |
52-
53-
## Heartbeat Modes
54-
55-
**SERVER** — The SDK calls `/auth/heartbeat` every `heartbeat_interval` seconds with a fresh nonce, verifies signature + nonce, and triggers failure on invalid session state.
56-
57-
**LOCAL** — No network calls. The SDK re-verifies stored signature state and checks expiry timestamp locally. If expired, it triggers failure with `session_expired`.
58-
59-
## Failure Handling
60-
61-
If authentication fails (login rejected, heartbeat fails, signature mismatch, etc.), the SDK calls your `on_failure` callback if one is provided. If no callback is set, **the SDK calls `os._exit(1)` to terminate the process.** This is intentional — it prevents your app from running without a valid license.
62-
63-
Recognized server errors:
64-
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `bad_request`, `checksum_required`, `checksum_mismatch`
65-
66-
Request retries are automatic inside the internal HTTP layer:
67-
- `rate_limited`: retry after 2s, then 5s (max 3 attempts total)
68-
- network failure: retry once after 2s
69-
- every retry regenerates a fresh nonce
70-
71-
```python
72-
def handle_auth_failure(reason, exception):
73-
print(f"Auth failed: {reason}")
74-
if exception:
75-
print(f"Details: {exception}")
76-
# Clean up and exit gracefully
77-
sys.exit(1)
78-
79-
client = AuthForgeClient(
80-
app_id="YOUR_APP_ID",
81-
app_secret="YOUR_APP_SECRET",
82-
heartbeat_mode="SERVER",
83-
on_failure=handle_auth_failure,
84-
)
85-
```
86-
87-
## How It Works
88-
89-
1. **Login** — Collects a hardware fingerprint (MAC, CPU, disk serial), generates a random nonce, and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload. The SDK verifies the HMAC-SHA256 signature and nonce to prevent replay attacks.
90-
91-
2. **Heartbeat** — A background daemon thread checks in at the configured interval. In SERVER mode, it sends a fresh nonce and verifies the response. In LOCAL mode, it re-verifies the stored signature and checks expiry without network calls.
92-
93-
3. **Crypto** — The `/validate` response is signed with a key derived from `SHA256(appSecret + nonce)`. That response carries a per-session `sigKey` (32-byte random hex) embedded in the signed session token. Every `/heartbeat` response is then signed with a key derived from `SHA256(sigKey + nonce)`. This keeps `appSecret` out of the heartbeat path while still rotating the signing key on every nonce, making replay and MITM attacks impractical.
94-
95-
## Hardware ID
96-
97-
The SDK generates a deterministic hardware fingerprint by hashing:
98-
- MAC address
99-
- CPU identifier
100-
- Disk serial number
101-
102-
Each component falls back gracefully if it can't be read (e.g. permissions issues). The HWID is sent with every auth request so the server can enforce per-device license limits.
103-
104-
## Test Vectors
105-
106-
The `generate_vectors.py` script and `test_vectors.json` file are provided for cross-SDK verification. If you're porting this SDK to another language, your implementation must produce identical `derivedKeyHex` and `signatureHex` values for the same inputs.
107-
108-
## Requirements
109-
110-
- Python 3.9+
111-
- No external packages
112-
113-
## License
114-
1+
# AuthForge Python SDK
2+
3+
Official Python SDK for [AuthForge](https://authforge.cc) — credit-based license key authentication with Ed25519-verified responses.
4+
5+
Uses `cryptography` for Ed25519 verification. Works on Python 3.9+.
6+
7+
## Quick Start
8+
9+
Copy `authforge.py` into your project, then:
10+
11+
```python
12+
from authforge import AuthForgeClient
13+
14+
client = AuthForgeClient(
15+
app_id="YOUR_APP_ID", # from your AuthForge dashboard
16+
app_secret="YOUR_APP_SECRET", # from your AuthForge dashboard
17+
public_key="YOUR_PUBLIC_KEY", # from your AuthForge dashboard
18+
heartbeat_mode="SERVER", # "SERVER" or "LOCAL"
19+
)
20+
21+
license_key = input("Enter license key: ")
22+
23+
if client.login(license_key):
24+
print("Authenticated!")
25+
# Your app logic here — heartbeats run automatically in the background
26+
else:
27+
print("Invalid license key.")
28+
exit(1)
29+
```
30+
31+
## Configuration
32+
33+
| Parameter | Type | Default | Description |
34+
|---|---|---|---|
35+
| `app_id` | str | required | Your application ID from the AuthForge dashboard |
36+
| `app_secret` | str | required | Your application secret from the AuthForge dashboard |
37+
| `public_key` | str | required | App Ed25519 public key (base64) from dashboard |
38+
| `heartbeat_mode` | str | required | `"SERVER"` or `"LOCAL"` (see below) |
39+
| `heartbeat_interval` | int | `900` | Seconds between heartbeat checks (default 15 min) |
40+
| `api_base_url` | str | `https://auth.authforge.cc` | API endpoint |
41+
| `on_failure` | callable | `None` | Callback `(reason: str, exc: Exception | None)` on auth failure |
42+
| `request_timeout` | int | `15` | HTTP request timeout in seconds |
43+
44+
## Methods
45+
46+
| Method | Returns | Description |
47+
|---|---|---|
48+
| `login(license_key)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
49+
| `logout()` | `None` | Stops heartbeat and clears all session/auth state |
50+
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
51+
| `get_session_data()` | `dict \| None` | Full decoded payload map |
52+
| `get_app_variables()` | `dict \| None` | App-scoped variables map |
53+
| `get_license_variables()` | `dict \| None` | License-scoped variables map |
54+
55+
## Heartbeat Modes
56+
57+
**SERVER** — The SDK calls `/auth/heartbeat` every `heartbeat_interval` seconds with a fresh nonce, verifies signature + nonce, and triggers failure on invalid session state.
58+
59+
**LOCAL** — No network calls. The SDK re-verifies stored signature state and checks expiry timestamp locally. If expired, it triggers failure with `session_expired`.
60+
61+
## Failure Handling
62+
63+
If authentication fails (login rejected, heartbeat fails, signature mismatch, etc.), the SDK calls your `on_failure` callback if one is provided. If no callback is set, **the SDK calls `os._exit(1)` to terminate the process.** This is intentional — it prevents your app from running without a valid license.
64+
65+
Recognized server errors:
66+
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `bad_request`
67+
68+
Request retries are automatic inside the internal HTTP layer:
69+
- `rate_limited`: retry after 2s, then 5s (max 3 attempts total)
70+
- network failure: retry once after 2s
71+
- every retry regenerates a fresh nonce
72+
73+
```python
74+
def handle_auth_failure(reason, exception):
75+
print(f"Auth failed: {reason}")
76+
if exception:
77+
print(f"Details: {exception}")
78+
# Clean up and exit gracefully
79+
sys.exit(1)
80+
81+
client = AuthForgeClient(
82+
app_id="YOUR_APP_ID",
83+
app_secret="YOUR_APP_SECRET",
84+
public_key="YOUR_PUBLIC_KEY",
85+
heartbeat_mode="SERVER",
86+
on_failure=handle_auth_failure,
87+
)
88+
```
89+
90+
## How It Works
91+
92+
1. **Login** — Collects a hardware fingerprint (MAC, CPU, disk serial), generates a random nonce, and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.
93+
94+
2. **Heartbeat** — A background daemon thread checks in at the configured interval. In SERVER mode, it sends a fresh nonce and verifies the response. In LOCAL mode, it re-verifies the stored signature and checks expiry without network calls.
95+
96+
3. **Crypto** — Both `/validate` and `/heartbeat` responses are signed by AuthForge with your app's Ed25519 private key. The SDK verifies every signed `payload` using your configured `public_key` and rejects tampered responses.
97+
98+
## Hardware ID
99+
100+
The SDK generates a deterministic hardware fingerprint by hashing:
101+
- MAC address
102+
- CPU identifier
103+
- Disk serial number
104+
105+
Each component falls back gracefully if it can't be read (e.g. permissions issues). The HWID is sent with every auth request so the server can enforce per-device license limits.
106+
107+
## Test Vectors
108+
109+
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior.
110+
111+
## Requirements
112+
113+
- Python 3.9+
114+
- Dependency: `cryptography`
115+
116+
## License
117+
115118
MIT

0 commit comments

Comments
 (0)