el fuego

keyengine

A standalone easy-to-plug license issuer.

Key features:

Usage

System setup

License creation code snippets

License check code snippets

Configuration

You need to have keyengine.json configuration file in the current directory.

{
  "httpPort": 80,
  "secret": "1234567890",
  "key": "test.key",
  "pwd": "qazwsx",
  "driver": "sqlite3",
  "connString": "file:test.db?cache=shared&mode=memory",
  "allowTokens": true,
  "license": "ey...0ifX0="
}

httpPort

Sets up a port where the built-in HTTP listens.

secret

Specifies a secret key to access /upload/ page.

key

A path of a private key file. The key should be in an OpenSSH format, as generated by ssh-keygen.

pwd

The private key password, or an empty string, if the key isn't encrypted.

driver

Database driver, one of postgres, mysql, sqlite3.

connString

Database connection string.

For postgres it should be as follows:

postgres://username:password@host:5432/database?sslmode=disable

or see the docs here.

For mysql it should be as follows:

username:password@tcp(host:3306)/test?autocommit=true

or see the docs here.

For sqlite3 it should be as follows:

file:test.db?cache=shared

or see the docs here.

allowTokens

If true, unblocks /license/ page processing.

license

keyengine itself is a shareware so it requires license to be used. Use your license key or empty quotes for trial period. After the 30-day trial period you should buy a license at https://elfuego.biz.

Usage

Command line

gen

This command runs command line license generator, you may use it as follows:

keyengine gen -prod=PRODID -type=personal -name="Johnny Walker" [-start=2019-01-01] [-dur=1y]

daemon

This command runs the built-in HTTP server, you may use it as follows:

keyengine daemon

HTTP queries

keyengine has a built-in HTTP server and it is ready to be run as a linux service. The HTTP service processes 2 queries:

1. POST query to /upload/

The upload query is usually called from a successful payment page. Querying it you should specify licensing product info and, optionally, payer info (for informational purposes).

Headers
Authorization: Secret <secret>

where <secret> is secret value from keyengine.json,

Variables encoded into license

productId [mandatory] product id the license is generated for, used to be checked in licensed stuff to check if the license permits using a particular program

licensedto [mandatory] personal or company name the product is licensed to

lictype [mandatory] license type: personal, enterprise, or whatever you want, any text string

licduration license duration including years, months, and/or days, like: 1m15d, or 1y6m; empty means infinite license

test true/false, for test purposes: 1) test flag is stored to DB, 2) productId written to license is actually productId-test, just to avoid redundant checks

Variables only stored to DB

productName product name, informational field

name payer's name, informational field

addrLine1 payer's address, informational field

city payer's city, informational field

state payer's state, informational field

zipCode payer's zip code, informational field

country payer's country, informational field

email payer's email, informational field

phoneNumber payer's phone number, informational field

Reply
{
  "ok":true,
  "token":"bc7f80c1-46b5-11e9-a11c-f430b9a6e499",
  "hash":"a7873a910691c2594117998bdad3a9cf",
  "license":"eyJs...9fQ=="
}

token a license record token (should be used in license query to retrieve license)

hash a license record hash (should be used in license query to retrieve license)

license a license itself

2. GET query to /license/

license query is used to retrieve a previously generated license (for example, you may send a link in email to a user). It is built by template as follows: http://<license.server>/license/hash-token. For hash == a7873a910691c2594117998bdad3a9cf and token == bc7f80c1-46b5-11e9-a11c-f430b9a6e499 from the previous query example it should be http://<license.server>/license/a7873a910691c2594117998bdad3a9cf-bc7f80c1-46b5-11e9-a11c-f430b9a6e499.

Reply

Reply of this query is the same as for the previous query.

System setup

keyengine.service

[Unit]
Description=Keyengine Service

[Service]
Type=simple
WorkingDirectory=/etc/keyengine
ExecStart=/usr/local/bin/keyengine daemon &
ExecStop=killall keyengine
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

License creation code snippets

php

(tested on version 7.3)

$keHost = 'lic.example.com';
$keSecret = '12345';
$keTest = false;
$productDescr = 'Acme program';
$productId = 'ACME1';
$licType = 'enterprise';

function gen_license() {
    global $keHost, $keSecret, $keTest;
    global $productDescr, $productId, $licType;
    $data = array();
    $data['productId'] = $productId;
    $data['productName'] = $productDescr;
    $data['test'] = $keTest ? "true" : "false";
    $data['licensedto'] = $_POST['licensedto'];
    $data['lictype'] = $licType;
    $data['licduration'] = null;
    $data['name'] = $_POST['name'];
    $data['addrLine1'] = $_POST['addrLine1'];
    $data['city'] = $_POST['city'];
    $data['state'] = $_POST['state'];
    $data['zipCode'] = $_POST['zipCode'];
    $data['country'] = $_POST['country'];
    $data['email'] = $_POST['email'];
    $data['phoneNumber'] = $_POST['phoneNumber'];

    $url = $keHost;

    $options = array(
        'http' => array(
            'header'  => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Secret $keSecret\r\n",
            'method'  => 'POST',
            'content' => http_build_query($data)
        )
    );
    $context  = stream_context_create($options);
    $result = file_get_contents($url . '/upload/', false, $context);
    $error = error_get_last();
    if ($result === false)
        return array(false, $error['message']);
    return array(json_decode($result));
}

License check code snippets

golang

(tested on version 1.11.5)

package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"golang.org/x/crypto/ssh"
	"log"
	"time"
)

var pubKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4dK1cTSWe/6IkGpB8Xqc8YEFUKPLDFvl37Or4WacQhIhH+yb7dKAae0t866/ViZHf6qjwuWD4iXhmmQm5hDS9yiGGjEzmovhW8PiuJ7qhdkuliBbWmn5knOT9A4b9QSNubsVhk5qGExbK9hoFLR/97fqeCsuy67jvhXABPh7fBQ==`
var lic = "eyJsaWNlbnNlIjoie1wibmFtZVwiOlwiSm9obm55IFdhbGtlclwiLFwidHlwZVwiOlwicGVyc29uYWxcIixcInN0YXJ0XCI6XCIyMDE5LTAyLTI2VDEzOjUxOjIzKzAyOjAwXCIsXCJlbmRcIjpudWxsLFwiYXJ0aWNsZXNcIjpbe1wibmFtZVwiOlwiUFJPRElELXRlc3RcIixcInBheWxvYWRcIjpudWxsfV19Iiwic2lnbmF0dXJlIjp7IkZvcm1hdCI6InNzaC1yc2EiLCJCbG9iIjoiSFhqejE5bThuZktOMGtrMitkMHFFcGRWcUtDbWgvQytBdXNkMTJWditITE54YitiUWcwS1gvUndpaFA3WUdDRUlrNHlLbEZCZUl6Umk3VlQvaU1xNEFkaWY4Q3VQVzBUQWJLcGN4TDg1RS94YVFlM2ZJT0pteUNSSEFIbXIxdk56TUs2eDR2SC9DbFRuZVlveEhlTndNV2hmWG0yUURBUy85NHMrekgxLzhJPSJ9fQ=="

type signed struct {
	License   string        `json:"license"`
	Signature ssh.Signature `json:"signature"`
}

type article struct {
	Name string `json:"name"`
}

type license struct {
	LicensedTo string     `json:"name"`
	LicType    string     `json:"type"`
	Start      time.Time  `json:"start"`
	End        *time.Time `json:"end"`
	Articles   []article  `json:"articles"`
}

func checkLicense(prodId, pubKey, licString string) *license {
	pub, _, _, _, e := ssh.ParseAuthorizedKey([]byte(pubKey))
	die(e)
	var sl signed
	bytes, e := base64.StdEncoding.DecodeString(licString)
	die(e)
	e = json.Unmarshal(bytes, &sl)
	die(e)
	e = pub.Verify([]byte(sl.License), &sl.Signature)
	die(e)
	var lic1 license
	e = json.Unmarshal([]byte(sl.License), &lic1)
	if e == nil {
		if time.Now().After(lic1.Start) {
			if lic1.End == nil || time.Now().Before(*lic1.End) {
				for _, a := range lic1.Articles {
					if prodId == a.Name {
						return &lic1
					}
				}
			}
		}
	}
	return nil
}

func main() {
	const prodId = "PRODID-test"
	lic1 := checkLicense(prodId, pubKey, lic)
	fmt.Printf("License verified: %t\n", lic1 != nil)
	if lic1 != nil {
		fmt.Printf("You have %s license for product \"%s\" registered to %s.\n", lic1.LicType, prodId, lic1.LicensedTo)
	}
}

func die(e error) {
	if e != nil {
		log.Fatal(e)
	}
}

python 3

(tested on version 3.6.4)

import base64
import json
import re
from datetime import datetime, timezone

# pycryptodome
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

pub_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4dK1cTSWe/6IkGpB8Xqc8YEFUKPLDFvl37Or4WacQhIhH+yb7dKAae0t866/ViZHf6qjwuWD4iXhmmQm5hDS9yiGGjEzmovhW8PiuJ7qhdkuliBbWmn5knOT9A4b9QSNubsVhk5qGExbK9hoFLR/97fqeCsuy67jvhXABPh7fBQ=='
lic_str = 'eyJsaWNlbnNlIjoie1wibmFtZVwiOlwiSm9obm55IFdhbGtlclwiLFwidHlwZVwiOlwicGVyc29uYWxcIixcInN0YXJ0XCI6XCIyMDE5LTAyLTI2VDEzOjUxOjIzKzAyOjAwXCIsXCJlbmRcIjpudWxsLFwiYXJ0aWNsZXNcIjpbe1wibmFtZVwiOlwiUFJPRElELXRlc3RcIixcInBheWxvYWRcIjpudWxsfV19Iiwic2lnbmF0dXJlIjp7IkZvcm1hdCI6InNzaC1yc2EiLCJCbG9iIjoiSFhqejE5bThuZktOMGtrMitkMHFFcGRWcUtDbWgvQytBdXNkMTJWditITE54YitiUWcwS1gvUndpaFA3WUdDRUlrNHlLbEZCZUl6Umk3VlQvaU1xNEFkaWY4Q3VQVzBUQWJLcGN4TDg1RS94YVFlM2ZJT0pteUNSSEFIbXIxdk56TUs2eDR2SC9DbFRuZVlveEhlTndNV2hmWG0yUURBUy85NHMrekgxLzhJPSJ9fQ=='


def date_decode(s):
    if s is None:
        return None
    pat = re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{2}):(\d{2})$', re.M | re.I)
    m = pat.match(s)
    if m is not None:
        s = m.group(1) + m.group(2)
        return datetime.strptime(s, "%Y-%m-%dT%H:%M:%S%z")
    return s


def check_license(prod_id, pub_key, lic_string):
    sl = json.loads(base64.b64decode(lic_string))
    rsakey = RSA.importKey(pub_key)
    signer = PKCS1_v1_5.new(rsakey)
    digest = SHA256.new()
    digest.update(bytes(sl['license'], 'utf8'))
    sign = sl['signature']['Blob']
    try:
        signer.verify(digest, bytes(sign, 'utf8'))
    except ValueError:
        return None
    lic = json.loads(sl['license'])
    now = datetime.now(tz=timezone.utc)
    if now > date_decode(lic['start']):
        if lic['end'] is None or now < date_decode(lic['end']):
            arts = lic['articles']
            for i in range(len(arts)):
                a = arts[i]
                if prod_id == a['name']:
                    return lic
    return None


prod_id = "PRODID-test"
lic = check_license(prod_id, pub_key, lic_str)
print("License verified: %s" % (lic is not None))
if lic is not None:
    print("You have %s license for product \"%s\" registered to %s.\n" % (lic['type'], prod_id, lic['name']))

php

(tested on version 7.3)

<?php
// phpseclib 1.0
include('Crypt/RSA.php');


$pub_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4dK1cTSWe/6IkGpB8Xqc8YEFUKPLDFvl37Or4WacQhIhH+yb7dKAae0t866/ViZHf6qjwuWD4iXhmmQm5hDS9yiGGjEzmovhW8PiuJ7qhdkuliBbWmn5knOT9A4b9QSNubsVhk5qGExbK9hoFLR/97fqeCsuy67jvhXABPh7fBQ==";
$lic_str = "eyJsaWNlbnNlIjoie1wibmFtZVwiOlwiSm9obm55IFdhbGtlclwiLFwidHlwZVwiOlwicGVyc29uYWxcIixcInN0YXJ0XCI6XCIyMDE5LTAyLTI2VDEzOjUxOjIzKzAyOjAwXCIsXCJlbmRcIjpudWxsLFwiYXJ0aWNsZXNcIjpbe1wibmFtZVwiOlwiUFJPRElELXRlc3RcIixcInBheWxvYWRcIjpudWxsfV19Iiwic2lnbmF0dXJlIjp7IkZvcm1hdCI6InNzaC1yc2EiLCJCbG9iIjoiSFhqejE5bThuZktOMGtrMitkMHFFcGRWcUtDbWgvQytBdXNkMTJWditITE54YitiUWcwS1gvUndpaFA3WUdDRUlrNHlLbEZCZUl6Umk3VlQvaU1xNEFkaWY4Q3VQVzBUQWJLcGN4TDg1RS94YVFlM2ZJT0pteUNSSEFIbXIxdk56TUs2eDR2SC9DbFRuZVlveEhlTndNV2hmWG0yUURBUy85NHMrekgxLzhJPSJ9fQ==";

function check_license($prod_id, $pub_key, $lic_string) {
    $sl = json_decode(base64_decode($lic_string));
    $rsa = new Crypt_RSA();
    $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_OPENSSH);
    $key_created = $rsa->loadKey($pub_key);
    if (!$key_created)
	return null;
    $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
    $signature = base64_decode($sl->signature->Blob);
    $verified = $rsa->verify($sl->license, $signature);
    if (!$verified)
	return null;
    $lic = json_decode($sl->license);
    $now = time();
    if ($now > strtotime($lic->start)) {
        if ($lic->end === null || $now < strtotime($lic->end)) {
            $arts = $lic->articles;
            foreach($arts as $a) {
                if ($prod_id == $a->name)
                    return $lic;
            }
        }
    }
    return null;
}


$prod_id = "PRODID-test";
$lic = check_license($prod_id, $pub_key, $lic_str);
echo "License verified: " . ($lic !== null ? "true" : "false") . "\n";
if ($lic !== null)
    echo "You have " . $lic->type . " license for product \"$prod_id\" registered to " . $lic->name . ".\n";