-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauthforge_sdk.h
More file actions
412 lines (371 loc) · 17.2 KB
/
Copy pathauthforge_sdk.h
File metadata and controls
412 lines (371 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
#pragma once
#include <cstdint>
#include <exception>
#include <functional>
#include <mutex>
#include <optional>
#include <unordered_set>
#include <string>
#include <utility>
#include <vector>
namespace authforge {
struct ValidateLicenseResult {
bool valid = false;
std::string errorCode;
std::string sessionToken;
long long expiresIn = 0;
std::string sessionDataJson;
std::string appVariablesJson;
std::string licenseVariablesJson;
std::string keyId;
std::optional<std::string> sessionExpiresAt;
bool licenseExpirationKnown = false;
/// ISO 8601 when `licenseExpirationKnown` and key is dated; empty when lifetime.
std::string licenseExpiresAt;
std::optional<int> maxHwidSlots;
std::optional<int> hwidCount;
std::optional<std::string> licenseLabel;
};
/// Opt-in policy for online check-ins after a successful activation.
///
/// Off (the default): after the client activates online via /auth/validate it
/// keeps running on the Ed25519-signed session for the duration of the grace
/// period (the session TTL: default 24h, server clamps 1h to 7d) without
/// contacting AuthForge. The background loop re-verifies the signed session
/// locally and fails once the grace period ends.
///
/// On: the client additionally performs online check-ins, periodic calls to
/// POST /auth/heartbeat every heartbeatInterval seconds, for fast revocation
/// and concurrent-use detection.
///
/// Deliberately an enum class rather than bool: a bool fourth parameter would
/// let legacy calls such as AuthForgeClient(a, b, key, "LOCAL") silently bind
/// through the const char* to bool standard conversion.
enum class OnlineHeartbeat { Off, On };
/// Kind of session the client currently holds: an online server session
/// (Login), an offline license file (LoginFromFile), or none.
enum class SessionKind { None, Online, Offline };
// ---------------------------------------------------------------------------
// Offline license files (`.authforge`)
//
// A cloud-minted, Ed25519-signed document for machines that never phone home.
// This is a SEPARATE mode from the grace period: the grace period continues a
// signed session after one online activation, while an offline file is
// verified locally with only the app public key and the machine HWID. Nothing
// here performs network I/O or starts online check-ins.
//
// Format (version 1): PEM-style armor with informational headers, a base64
// JSON payload wrapped at 64 columns, and a detached Ed25519 signature over
// the UTF-8 bytes of the base64 payload string (body lines joined, whitespace
// removed) - the same contract as /auth/validate.
// ---------------------------------------------------------------------------
/// The only `.authforge` format version this SDK accepts.
constexpr int kOfflineLicenseFileVersion = 1;
/// HWID binding policy embedded in a `.authforge` file.
struct OfflineHwidPolicy {
/// "bound" (verify only when the local HWID is in `hwids`) or "any".
std::string mode;
std::vector<std::string> hwids;
};
/// Verified content of a `.authforge` file.
struct OfflineLicense {
std::string appId;
std::string licenseKey;
/// Unique id of this minted file.
std::string jti;
/// App signing key id that signed the file.
std::string keyId;
std::string issuedAt;
/// ISO 8601 file expiry; std::nullopt for a lifetime file.
std::optional<std::string> expiresAt;
OfflineHwidPolicy hwidPolicy;
std::optional<std::string> label;
/// True when the payload carried `licenseExpiresAt` (lifetime licenses use JSON null).
bool licenseExpirationKnown = false;
/// License expiry when `licenseExpirationKnown` and dated; empty when lifetime.
std::string licenseExpiresAt;
/// Raw JSON object text (empty when absent).
std::string licenseVariablesJson;
std::string appVariablesJson;
/// Full decoded payload JSON text (unknown fields preserved).
std::string payloadJson;
/// Canonical signed string and its signature.
std::string payloadBase64;
std::string signatureBase64;
};
/// Raw armor split into parts.
struct ParsedLicenseFile {
std::vector<std::pair<std::string, std::string>> headers;
/// Exactly the string the signature covers.
std::string payloadBase64;
std::string signatureBase64;
};
/// Result of VerifyLicenseFile.
struct VerifyLicenseFileResult {
bool ok = false;
/// Cross-SDK error code when !ok: bad_armor, bad_signature,
/// unsupported_version, malformed_payload, wrong_app, expired,
/// hwid_mismatch (or "read_error: ..." from the client path helpers).
std::string error;
OfflineLicense license;
};
/// Parse armored `.authforge` text. Returns std::nullopt when the armor is
/// malformed. Tolerates CRLF, a UTF-8 BOM, any re-wrapping of the base64 body
/// and text before/after the armor.
std::optional<ParsedLicenseFile> ParseLicenseFile(const std::string &text);
/// Verify armored `.authforge` text with NO network access. Check order
/// (fixed across every SDK): bad_armor -> bad_signature -> unsupported_version
/// -> malformed_payload -> wrong_app -> expired -> hwid_mismatch. The
/// signature is checked before the payload JSON is decoded.
///
/// `publicKeys` are raw-32-byte Ed25519 keys in standard base64 (entries may
/// themselves be comma-separated). `hwid` is the local machine id (required
/// for bound files). `nowEpochMs` overrides the clock (tests); 0 means now.
VerifyLicenseFileResult VerifyLicenseFile(
const std::string &file,
const std::string &appId,
const std::vector<std::string> &publicKeys,
const std::string &hwid,
long long nowEpochMs = 0);
/// Parse `YYYY-MM-DDTHH:MM:SS[.fff][Z|+HH:MM]` into epoch milliseconds.
std::optional<long long> ParseIso8601Ms(const std::string &value);
/// Optional fields for AuthForgeClient::CreateActivationRequest.
/// machineName is omitted unless includeMachineName is true.
struct ActivationRequestOptions {
bool includeMachineName = false;
bool omitOs = false;
bool omitSdk = false;
std::string machineName;
std::string os;
std::string sdk;
std::string licenseKey;
std::string createdAt;
};
/// Armored `.authforge-request` text from explicit fields. Empty optional
/// strings are omitted from the payload.
std::string FormatActivationRequest(const std::string &appId, const std::string &hwid,
const std::string &createdAt, const std::string &machineName = "",
const std::string &os = "", const std::string &sdk = "",
const std::string &licenseKey = "");
class AuthForgeClient {
public:
static constexpr const char *kDefaultApiBaseUrl = "https://auth.authforge.cc";
/// Single-key constructor. The provided string may also be a
/// comma-separated trust list (current,previous) so callers can roll a key
/// by re-deploying with an env var change.
///
/// appSecret may be empty when the client will only call LoginFromFile
/// (air-gapped builds should not ship the secret). Online APIs still
/// require a non-empty secret.
///
/// ttlSeconds requests the grace period duration in seconds for
/// /auth/validate (how long the app keeps running on the signed session
/// without contacting AuthForge). 0 means the server default (24h today);
/// the server clamps requested values to 1h..7d.
AuthForgeClient(
std::string appId,
std::string appSecret,
std::string publicKey,
OnlineHeartbeat onlineHeartbeat = OnlineHeartbeat::Off,
int heartbeatInterval = 900,
std::string apiBaseUrl = kDefaultApiBaseUrl,
std::function<void(const std::string &, const std::exception *)> onFailure = nullptr,
int requestTimeout = 15,
int ttlSeconds = 0,
std::string hwidOverride = "");
/// Rotation-aware constructor. The first entry should be the *current* key
/// (it is reflected back through GetPublicKey() for diagnostics);
/// subsequent entries are still trusted to support overlap windows.
///
/// See the single-key constructor for the onlineHeartbeat and ttlSeconds
/// (grace period duration) semantics.
AuthForgeClient(
std::string appId,
std::string appSecret,
std::vector<std::string> publicKeys,
OnlineHeartbeat onlineHeartbeat = OnlineHeartbeat::Off,
int heartbeatInterval = 900,
std::string apiBaseUrl = kDefaultApiBaseUrl,
std::function<void(const std::string &, const std::exception *)> onFailure = nullptr,
int requestTimeout = 15,
int ttlSeconds = 0,
std::string hwidOverride = "");
/// Legacy string-mode constructor kept for source compatibility. Validates
/// heartbeatMode exactly as before (std::invalid_argument unless LOCAL or
/// SERVER, case-insensitive) and delegates to the OnlineHeartbeat overload.
[[deprecated("heartbeatMode strings are deprecated: LOCAL maps to the default grace period behavior (drop the argument) and SERVER maps to OnlineHeartbeat::On")]]
AuthForgeClient(
std::string appId,
std::string appSecret,
std::string publicKey,
std::string heartbeatMode,
int heartbeatInterval = 900,
std::string apiBaseUrl = kDefaultApiBaseUrl,
std::function<void(const std::string &, const std::exception *)> onFailure = nullptr,
int requestTimeout = 15,
int ttlSeconds = 0,
std::string hwidOverride = "");
/// Legacy string-mode rotation-aware constructor. See the deprecation note
/// above; delegates to the OnlineHeartbeat overload.
[[deprecated("heartbeatMode strings are deprecated: LOCAL maps to the default grace period behavior (drop the argument) and SERVER maps to OnlineHeartbeat::On")]]
AuthForgeClient(
std::string appId,
std::string appSecret,
std::vector<std::string> publicKeys,
std::string heartbeatMode,
int heartbeatInterval = 900,
std::string apiBaseUrl = kDefaultApiBaseUrl,
std::function<void(const std::string &, const std::exception *)> onFailure = nullptr,
int requestTimeout = 15,
int ttlSeconds = 0,
std::string hwidOverride = "");
/// Returns the configured trust list. Useful for tests and observability.
const std::vector<std::string> &GetPublicKeys() const noexcept { return publicKeys_; }
bool Login(const std::string &licenseKey);
/// Same cryptographic validation as Login without persisting session state or starting heartbeats.
ValidateLicenseResult ValidateLicense(const std::string &licenseKey);
bool SelfBan(const std::string &licenseKey = "",
const std::string &sessionToken = "",
bool revokeLicense = true,
bool blacklistHwid = true,
bool blacklistIp = true);
void Logout();
bool IsAuthenticated() const;
/// SessionKind::Online after Login, SessionKind::Offline after
/// LoginFromFile, SessionKind::None when logged out.
SessionKind GetSessionKind() const;
std::optional<std::string> GetSessionDataJson() const;
std::optional<std::string> GetAppVariablesJson() const;
std::optional<std::string> GetLicenseVariablesJson() const;
/// The HWID this client sends to AuthForge (or hwidOverride). Customers on
/// air-gapped machines report this value to the operator so an offline
/// `.authforge` file can be bound to it.
const std::string &GetHwid() const noexcept { return hwid_; }
/// Build an activation request (`.authforge-request`) for this machine.
/// No network, no session, no app secret. machineName is omitted unless
/// options.includeMachineName is true.
std::string CreateActivationRequest(const ActivationRequestOptions &options = ActivationRequestOptions()) const;
/// Authorize from a cloud-minted offline license file (`.authforge`) with
/// NO network access. Accepts a filesystem path or the armored text.
///
/// On success the client is authenticated (IsAuthenticated,
/// GetSessionDataJson, GetAppVariablesJson, GetLicenseVariablesJson work)
/// and GetOfflineLicense() describes the file. No grace-period thread and
/// no online check-ins are started - the file's own expiresAt is the only
/// clock. Online Login is untouched.
///
/// Failures are reported through onFailure("offline_login_failed", &exc)
/// (exc.what() is the error code) and return false; unlike Login this never
/// calls std::exit.
bool LoginFromFile(const std::string &pathOrText);
/// Verify a `.authforge` file (filesystem path or armored text) with this
/// client's app id, public key(s) and HWID, without touching session state.
VerifyLicenseFileResult VerifyLicenseFile(const std::string &pathOrText, long long nowEpochMs = 0) const;
/// Details of the offline file the client authenticated with, if any.
std::optional<OfflineLicense> GetOfflineLicense() const;
private:
// Test-only: lets tests/offline_vectors_test.cpp drive the private
// heartbeat entry point to prove it is a no-op for offline sessions.
friend struct OfflineVectorsTestAccess;
void ApplyOfflineLicense(const OfflineLicense &license);
static std::string ReadLicenseFileInput(const std::string &pathOrText);
struct JsonValue {
bool exists = false;
bool isString = false;
std::string value;
};
enum class SigningContext { Validate, Heartbeat };
void StartHeartbeatOnce();
void HeartbeatLoop() noexcept;
void ServerHeartbeat();
/// Grace period check: re-verifies the stored signed session locally (no
/// network) and fails with session_expired once the grace period ends.
void GracePeriodCheck();
void ValidateAndStore(const std::string &licenseKey);
void ApplySignedResponse(
const std::string &responseJson,
const std::string &expectedNonce,
const std::optional<std::string> &licenseKey,
SigningContext context,
bool persistToSession = true,
ValidateLicenseResult *validateOnlyOut = nullptr);
std::string PostJson(const std::string &path, const std::string &bodyJson, std::string *usedNonce = nullptr) const;
std::string ExtractServerError(const std::string &responseJson) const;
void Fail(const std::string &reason, const std::exception *exc = nullptr) const noexcept;
std::string ComputeHwid() const;
std::string SafeMacAddress() const;
std::string SafeCpuInfo() const;
std::string SafeDiskSerial() const;
std::string RunCommand(const std::string &command) const;
static bool ExtractJsonValue(const std::string &json, const std::string &key, JsonValue &outValue);
static std::optional<std::string> ExtractJsonString(const std::string &json, const std::string &key);
static std::optional<long long> ExtractJsonInt(const std::string &json, const std::string &key);
static std::string BuildJsonBody(const std::vector<std::pair<std::string, std::string>> &pairs);
static std::string EscapeJsonString(const std::string &value);
static std::string UnescapeJsonString(const std::string &value, bool &ok);
static std::string Trim(const std::string &value);
static std::string ToLower(std::string value);
static std::string GenerateNonceHex32();
static std::vector<unsigned char> Sha256Bytes(const std::string &input);
static std::string Sha256Hex(const std::string &input);
static std::string BytesToHexLower(const std::vector<unsigned char> &bytes);
static std::vector<unsigned char> DecodeBase64Any(const std::string &value);
static std::vector<unsigned char> DecodeBase64WithAlphabet(const std::string &value, bool urlSafe);
static std::string AddBase64Padding(const std::string &value);
static bool IsSuccessStatus(const JsonValue &status);
static std::optional<long long> ExtractExpiresInFromSessionToken(const std::string &sessionToken);
static std::optional<std::string> DecodeSessionTokenBody(const std::string &sessionToken);
void VerifySignature(const std::string &rawPayloadB64, const std::string &signature) const;
static std::vector<std::string> SplitCommaTrustList(const std::string &value);
std::string appId_;
std::string appSecret_;
std::vector<std::string> publicKeys_;
// True when online check-ins (periodic POST /auth/heartbeat) are enabled.
bool onlineHeartbeat_;
int heartbeatInterval_;
std::string apiBaseUrl_;
std::function<void(const std::string &, const std::exception *)> onFailure_;
int requestTimeout_;
// Requested grace period duration in seconds for /auth/validate (the
// session token lifetime). 0 means "let the server pick its default"
// (24h today). Server clamps to [3600, 604800]; preserved across
// heartbeat refreshes.
int ttlSeconds_;
mutable std::mutex lock_;
bool heartbeatStarted_;
std::string licenseKey_;
std::string sessionToken_;
std::optional<long long> sessionExpiresIn_;
std::string lastNonce_;
std::string rawPayloadB64_;
std::string signature_;
std::string keyId_;
std::vector<std::vector<unsigned char>> verifyPublicKeysBytes_;
std::string sessionDataJson_;
std::string appVariablesJson_;
std::string licenseVariablesJson_;
bool authenticated_ = false;
SessionKind sessionKind_ = SessionKind::None;
bool heartbeatStop_ = false;
std::string hwid_;
std::optional<OfflineLicense> offlineLicense_;
std::unordered_set<std::string> knownServerErrors_ = {
"invalid_app",
"invalid_key",
"expired",
"revoked",
"hwid_mismatch",
"no_credits",
"app_burn_cap_reached",
"blocked",
"rate_limited",
"replay_detected",
"app_disabled",
"session_expired",
"revoke_requires_session",
"bad_request",
"malformed_request",
"system_error",
};
};
} // namespace authforge