|
#define _GNU_SOURCE |
|
#include "license.h" |
|
|
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdint.h> |
|
#include <ctype.h> |
|
#include <time.h> |
|
#include <unistd.h> |
|
#include <errno.h> |
|
#include <sys/stat.h> |
|
|
|
/* Fixed license path for the demo */ |
|
#ifndef LICENSE_PATH |
|
#define LICENSE_PATH "/tmp/demo_license.txt" |
|
#endif |
|
|
|
/* DEMO ONLY: embedded secret used for HMAC */ |
|
static const uint8_t EMBEDDED_SECRET[] = |
|
"DEMO-ONLY-SECRET-CHANGE-ME-KEEP-PRIVATE"; |
|
|
|
/* Thread-local last error */ |
|
static __thread char g_last_error[256]; |
|
|
|
/* Cached canonical string from last successful check (for derive_key) */ |
|
static __thread char g_last_canonical[1024]; |
|
static __thread int g_canonical_ok = 0; |
|
|
|
static void set_err(const char *msg) { |
|
snprintf(g_last_error, sizeof(g_last_error), "%s", msg); |
|
} |
|
|
|
const char *license_last_error(void) { |
|
if (g_last_error[0] == '\0') return "no error"; |
|
return g_last_error; |
|
} |
|
|
|
/* ---------------- Minimal SHA-256 + HMAC-SHA256 (glibc-only) ---------------- */ |
|
|
|
typedef struct { |
|
uint32_t h[8]; |
|
uint64_t len_bits; |
|
uint8_t buf[64]; |
|
size_t buf_len; |
|
} sha256_ctx; |
|
|
|
static uint32_t rotr32(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } |
|
static uint32_t ch(uint32_t x, uint32_t y, uint32_t z) { return (x & y) ^ (~x & z); } |
|
static uint32_t maj(uint32_t x, uint32_t y, uint32_t z) { return (x & y) ^ (x & z) ^ (y & z); } |
|
static uint32_t bsig0(uint32_t x) { return rotr32(x, 2) ^ rotr32(x,13) ^ rotr32(x,22); } |
|
static uint32_t bsig1(uint32_t x) { return rotr32(x, 6) ^ rotr32(x,11) ^ rotr32(x,25); } |
|
static uint32_t ssig0(uint32_t x) { return rotr32(x, 7) ^ rotr32(x,18) ^ (x >> 3); } |
|
static uint32_t ssig1(uint32_t x) { return rotr32(x,17) ^ rotr32(x,19) ^ (x >> 10); } |
|
|
|
static const uint32_t K[64] = { |
|
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u, |
|
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u, |
|
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau, |
|
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u, |
|
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u, |
|
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u, |
|
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u, |
|
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u |
|
}; |
|
|
|
static void sha256_init(sha256_ctx *c) { |
|
c->h[0]=0x6a09e667u; c->h[1]=0xbb67ae85u; c->h[2]=0x3c6ef372u; c->h[3]=0xa54ff53au; |
|
c->h[4]=0x510e527fu; c->h[5]=0x9b05688cu; c->h[6]=0x1f83d9abu; c->h[7]=0x5be0cd19u; |
|
c->len_bits = 0; |
|
c->buf_len = 0; |
|
} |
|
|
|
static void sha256_transform(sha256_ctx *c, const uint8_t block[64]) { |
|
uint32_t w[64]; |
|
for (int i=0;i<16;i++) { |
|
w[i] = ((uint32_t)block[i*4+0] << 24) | |
|
((uint32_t)block[i*4+1] << 16) | |
|
((uint32_t)block[i*4+2] << 8) | |
|
((uint32_t)block[i*4+3] << 0); |
|
} |
|
for (int i=16;i<64;i++) w[i] = ssig1(w[i-2]) + w[i-7] + ssig0(w[i-15]) + w[i-16]; |
|
|
|
uint32_t a=c->h[0], b=c->h[1], cc=c->h[2], d=c->h[3]; |
|
uint32_t e=c->h[4], f=c->h[5], g=c->h[6], h=c->h[7]; |
|
|
|
for (int i=0;i<64;i++) { |
|
uint32_t t1 = h + bsig1(e) + ch(e,f,g) + K[i] + w[i]; |
|
uint32_t t2 = bsig0(a) + maj(a,b,cc); |
|
h = g; g = f; f = e; e = d + t1; |
|
d = cc; cc = b; b = a; a = t1 + t2; |
|
} |
|
|
|
c->h[0] += a; c->h[1] += b; c->h[2] += cc; c->h[3] += d; |
|
c->h[4] += e; c->h[5] += f; c->h[6] += g; c->h[7] += h; |
|
} |
|
|
|
static void sha256_update(sha256_ctx *c, const void *data, size_t len) { |
|
const uint8_t *p = (const uint8_t*)data; |
|
c->len_bits += (uint64_t)len * 8; |
|
while (len) { |
|
size_t take = 64 - c->buf_len; |
|
if (take > len) take = len; |
|
memcpy(c->buf + c->buf_len, p, take); |
|
c->buf_len += take; |
|
p += take; |
|
len -= take; |
|
if (c->buf_len == 64) { |
|
sha256_transform(c, c->buf); |
|
c->buf_len = 0; |
|
} |
|
} |
|
} |
|
|
|
static void sha256_final(sha256_ctx *c, uint8_t out[32]) { |
|
c->buf[c->buf_len++] = 0x80; |
|
if (c->buf_len > 56) { |
|
while (c->buf_len < 64) c->buf[c->buf_len++] = 0x00; |
|
sha256_transform(c, c->buf); |
|
c->buf_len = 0; |
|
} |
|
while (c->buf_len < 56) c->buf[c->buf_len++] = 0x00; |
|
|
|
uint64_t L = c->len_bits; |
|
for (int i=7;i>=0;i--) c->buf[c->buf_len++] = (uint8_t)((L >> (i*8)) & 0xff); |
|
sha256_transform(c, c->buf); |
|
|
|
for (int i=0;i<8;i++) { |
|
out[i*4+0] = (uint8_t)((c->h[i] >> 24) & 0xff); |
|
out[i*4+1] = (uint8_t)((c->h[i] >> 16) & 0xff); |
|
out[i*4+2] = (uint8_t)((c->h[i] >> 8) & 0xff); |
|
out[i*4+3] = (uint8_t)((c->h[i] >> 0) & 0xff); |
|
} |
|
} |
|
|
|
static void hmac_sha256(const uint8_t *key, size_t key_len, |
|
const uint8_t *msg, size_t msg_len, |
|
uint8_t out[32]) { |
|
uint8_t k0[64]; |
|
memset(k0, 0, sizeof(k0)); |
|
|
|
if (key_len > 64) { |
|
sha256_ctx c; |
|
uint8_t kh[32]; |
|
sha256_init(&c); |
|
sha256_update(&c, key, key_len); |
|
sha256_final(&c, kh); |
|
memcpy(k0, kh, 32); |
|
} else { |
|
memcpy(k0, key, key_len); |
|
} |
|
|
|
uint8_t ipad[64], opad[64]; |
|
for (int i=0;i<64;i++) { |
|
ipad[i] = (uint8_t)(k0[i] ^ 0x36); |
|
opad[i] = (uint8_t)(k0[i] ^ 0x5c); |
|
} |
|
|
|
sha256_ctx inner; |
|
uint8_t inner_hash[32]; |
|
sha256_init(&inner); |
|
sha256_update(&inner, ipad, 64); |
|
sha256_update(&inner, msg, msg_len); |
|
sha256_final(&inner, inner_hash); |
|
|
|
sha256_ctx outer; |
|
sha256_init(&outer); |
|
sha256_update(&outer, opad, 64); |
|
sha256_update(&outer, inner_hash, 32); |
|
sha256_final(&outer, out); |
|
} |
|
|
|
/* ---------------- Helpers ---------------- */ |
|
|
|
static int hexval(int c) { |
|
if (c>='0' && c<='9') return c-'0'; |
|
if (c>='a' && c<='f') return 10 + (c-'a'); |
|
if (c>='A' && c<='F') return 10 + (c-'A'); |
|
return -1; |
|
} |
|
|
|
static int hex_decode_32(const char *hex, uint8_t out[32]) { |
|
size_t n = strlen(hex); |
|
if (n != 64) return -1; |
|
for (size_t i=0;i<32;i++) { |
|
int hi = hexval(hex[i*2]); |
|
int lo = hexval(hex[i*2+1]); |
|
if (hi < 0 || lo < 0) return -1; |
|
out[i] = (uint8_t)((hi<<4) | lo); |
|
} |
|
return 0; |
|
} |
|
|
|
static int secure_memeq(const uint8_t *a, const uint8_t *b, size_t n) { |
|
uint8_t r = 0; |
|
for (size_t i=0;i<n;i++) r |= (uint8_t)(a[i] ^ b[i]); |
|
return r == 0; |
|
} |
|
|
|
static char *trim(char *s) { |
|
while (*s && isspace((unsigned char)*s)) s++; |
|
char *e = s + strlen(s); |
|
while (e > s && isspace((unsigned char)e[-1])) e--; |
|
*e = '\0'; |
|
return s; |
|
} |
|
|
|
static int read_file_all(const char *path, char **out_buf, size_t *out_len) { |
|
FILE *f = fopen(path, "rb"); |
|
if (!f) return -1; |
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; } |
|
long sz = ftell(f); |
|
if (sz < 0) { fclose(f); return -1; } |
|
if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return -1; } |
|
char *buf = (char*)malloc((size_t)sz + 1); |
|
if (!buf) { fclose(f); return -1; } |
|
size_t rd = fread(buf, 1, (size_t)sz, f); |
|
fclose(f); |
|
if (rd != (size_t)sz) { free(buf); return -1; } |
|
buf[sz] = '\0'; |
|
*out_buf = buf; |
|
*out_len = (size_t)sz; |
|
return 0; |
|
} |
|
|
|
static int read_machine_id(char out[128]) { |
|
FILE *f = fopen("/etc/machine-id", "rb"); |
|
if (!f) return -1; |
|
if (!fgets(out, 128, f)) { fclose(f); return -1; } |
|
fclose(f); |
|
char *t = trim(out); |
|
memmove(out, t, strlen(t)+1); |
|
return (out[0] ? 0 : -1); |
|
} |
|
|
|
static int read_hostname(char out[256]) { |
|
if (gethostname(out, 256) != 0) return -1; |
|
out[255] = '\0'; |
|
char *t = trim(out); |
|
memmove(out, t, strlen(t)+1); |
|
return (out[0] ? 0 : -1); |
|
} |
|
|
|
static int parse_date_yyyy_mm_dd(const char *s, time_t *out_end_local) { |
|
if (strlen(s) != 10 || s[4] != '-' || s[7] != '-') return -1; |
|
for (int i=0;i<10;i++) { |
|
if (i==4 || i==7) continue; |
|
if (!isdigit((unsigned char)s[i])) return -1; |
|
} |
|
int y = atoi(s); |
|
int m = atoi(s+5); |
|
int d = atoi(s+8); |
|
if (m < 1 || m > 12 || d < 1 || d > 31) return -1; |
|
|
|
struct tm tmv; |
|
memset(&tmv, 0, sizeof(tmv)); |
|
tmv.tm_year = y - 1900; |
|
tmv.tm_mon = m - 1; |
|
tmv.tm_mday = d; |
|
tmv.tm_hour = 23; |
|
tmv.tm_min = 59; |
|
tmv.tm_sec = 59; |
|
tmv.tm_isdst = -1; |
|
|
|
time_t t = mktime(&tmv); /* local time end-of-day */ |
|
if (t == (time_t)-1) return -1; |
|
*out_end_local = t; |
|
return 0; |
|
} |
|
|
|
/* ---------------- Anti-tamper best-effort checks ---------------- */ |
|
|
|
static int anti_tamper_env(void) { |
|
/* Refuse common dynamic loader injection knobs */ |
|
if (getenv("LD_PRELOAD") && getenv("LD_PRELOAD")[0]) { |
|
set_err("license: tamper detected (LD_PRELOAD set)"); |
|
return -1; |
|
} |
|
if (getenv("LD_AUDIT") && getenv("LD_AUDIT")[0]) { |
|
set_err("license: tamper detected (LD_AUDIT set)"); |
|
return -1; |
|
} |
|
return 0; |
|
} |
|
|
|
static int anti_tamper_tracer(void) { |
|
/* Refuse if being debugged (simple check) */ |
|
FILE *f = fopen("/proc/self/status", "rb"); |
|
if (!f) return 0; /* if unavailable, don't hard-fail in demo */ |
|
char line[256]; |
|
while (fgets(line, sizeof(line), f)) { |
|
if (strncmp(line, "TracerPid:", 10) == 0) { |
|
char *p = line + 10; |
|
while (*p && isspace((unsigned char)*p)) p++; |
|
int tpid = atoi(p); |
|
fclose(f); |
|
if (tpid != 0) { |
|
set_err("license: tamper detected (debugger/tracer attached)"); |
|
return -1; |
|
} |
|
return 0; |
|
} |
|
} |
|
fclose(f); |
|
return 0; |
|
} |
|
|
|
static int anti_tamper_exe_perms(void) { |
|
/* Refuse if the running executable is group/other writable */ |
|
struct stat st; |
|
if (stat("/proc/self/exe", &st) != 0) return 0; /* best-effort */ |
|
if ((st.st_mode & S_IWGRP) || (st.st_mode & S_IWOTH)) { |
|
set_err("license: tamper detected (executable is group/other-writable)"); |
|
return -1; |
|
} |
|
return 0; |
|
} |
|
|
|
/* ---------------- License format and validation ---------------- |
|
License file (order doesn't matter): |
|
expires=2030-12-31 |
|
bind_type=hostname # hostname | machineid |
|
bind_value=<hostname or machine-id> |
|
sig=<64 hex chars> # HMAC-SHA256(secret, canonical) |
|
|
|
Canonical string for signing (exactly): |
|
expires=<expires>\n |
|
bind_type=<bind_type>\n |
|
bind_value=<bind_value>\n |
|
------------------------------------------------------------------ */ |
|
|
|
int license_check(void) { |
|
g_last_error[0] = '\0'; |
|
g_canonical_ok = 0; |
|
|
|
if (anti_tamper_env() != 0) return 101; |
|
if (anti_tamper_tracer() != 0) return 102; |
|
if (anti_tamper_exe_perms() != 0) return 103; |
|
|
|
char *buf = NULL; |
|
size_t len = 0; |
|
if (read_file_all(LICENSE_PATH, &buf, &len) != 0) { |
|
set_err("license: license file not found or unreadable at " LICENSE_PATH); |
|
return 1; |
|
} |
|
|
|
char expires[32] = {0}; |
|
char bind_type[32] = {0}; |
|
char bind_value[256] = {0}; |
|
char sig_hex[128] = {0}; |
|
|
|
char *save = NULL; |
|
for (char *line = strtok_r(buf, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { |
|
char *t = trim(line); |
|
if (*t == '\0' || *t == '#') continue; |
|
char *eq = strchr(t, '='); |
|
if (!eq) continue; |
|
*eq = '\0'; |
|
char *k = trim(t); |
|
char *v = trim(eq+1); |
|
|
|
if (strcmp(k, "expires") == 0) snprintf(expires, sizeof(expires), "%s", v); |
|
else if (strcmp(k, "bind_type") == 0) snprintf(bind_type, sizeof(bind_type), "%s", v); |
|
else if (strcmp(k, "bind_value") == 0) snprintf(bind_value, sizeof(bind_value), "%s", v); |
|
else if (strcmp(k, "sig") == 0) snprintf(sig_hex, sizeof(sig_hex), "%s", v); |
|
} |
|
|
|
if (!expires[0] || !bind_type[0] || !bind_value[0] || !sig_hex[0]) { |
|
free(buf); |
|
set_err("license: missing required fields (expires/bind_type/bind_value/sig)"); |
|
return 2; |
|
} |
|
|
|
time_t exp_end = 0; |
|
if (parse_date_yyyy_mm_dd(expires, &exp_end) != 0) { |
|
free(buf); |
|
set_err("license: invalid expires date (expected YYYY-MM-DD)"); |
|
return 3; |
|
} |
|
|
|
time_t now = time(NULL); |
|
if (now == (time_t)-1) { |
|
free(buf); |
|
set_err("license: unable to read system time"); |
|
return 4; |
|
} |
|
if (now > exp_end) { |
|
free(buf); |
|
set_err("license: license expired"); |
|
return 5; |
|
} |
|
|
|
/* Verify binding */ |
|
char actual[256] = {0}; |
|
if (strcmp(bind_type, "hostname") == 0) { |
|
if (read_hostname(actual) != 0) { |
|
free(buf); |
|
set_err("license: unable to read hostname"); |
|
return 6; |
|
} |
|
} else if (strcmp(bind_type, "machineid") == 0) { |
|
char mid[128] = {0}; |
|
if (read_machine_id(mid) != 0) { |
|
free(buf); |
|
set_err("license: unable to read /etc/machine-id"); |
|
return 7; |
|
} |
|
snprintf(actual, sizeof(actual), "%s", mid); |
|
} else { |
|
free(buf); |
|
set_err("license: unknown bind_type (expected hostname|machineid)"); |
|
return 8; |
|
} |
|
|
|
if (strcmp(actual, bind_value) != 0) { |
|
free(buf); |
|
set_err("license: bind_value does not match this system"); |
|
return 9; |
|
} |
|
|
|
/* Build canonical (also cached for derive_key) */ |
|
int n = snprintf(g_last_canonical, sizeof(g_last_canonical), |
|
"expires=%s\nbind_type=%s\nbind_value=%s\n", |
|
expires, bind_type, bind_value); |
|
if (n <= 0 || (size_t)n >= sizeof(g_last_canonical)) { |
|
free(buf); |
|
set_err("license: internal error building canonical string"); |
|
return 10; |
|
} |
|
|
|
uint8_t expected[32]; |
|
hmac_sha256(EMBEDDED_SECRET, sizeof(EMBEDDED_SECRET)-1, |
|
(const uint8_t*)g_last_canonical, (size_t)n, |
|
expected); |
|
|
|
uint8_t provided[32]; |
|
if (hex_decode_32(sig_hex, provided) != 0) { |
|
free(buf); |
|
set_err("license: sig is not valid hex (expected 64 hex chars)"); |
|
return 11; |
|
} |
|
|
|
if (!secure_memeq(expected, provided, 32)) { |
|
free(buf); |
|
set_err("license: signature mismatch (invalid license)"); |
|
return 12; |
|
} |
|
|
|
g_canonical_ok = 1; |
|
free(buf); |
|
return 0; |
|
} |
|
|
|
int license_derive_key(uint8_t out_key32[32]) { |
|
if (!out_key32) { |
|
set_err("license: derive_key called with NULL buffer"); |
|
return 1; |
|
} |
|
if (!g_canonical_ok) { |
|
set_err("license: derive_key called before successful license_check()"); |
|
return 2; |
|
} |
|
|
|
/* Key derivation: HMAC(secret, "derive-key-v1\n" + canonical) */ |
|
char msg[1200]; |
|
int n = snprintf(msg, sizeof(msg), "derive-key-v1\n%s", g_last_canonical); |
|
if (n <= 0 || (size_t)n >= sizeof(msg)) { |
|
set_err("license: internal error building derive message"); |
|
return 3; |
|
} |
|
|
|
hmac_sha256(EMBEDDED_SECRET, sizeof(EMBEDDED_SECRET)-1, |
|
(const uint8_t*)msg, (size_t)n, |
|
out_key32); |
|
return 0; |
|
} |