Skip to main content

Pure-Python ECDSA license tokens for PyArmor outer keys (bind-data), with plan/feature policy helpers.

Project description

licensekit

Pure-Python ECDSA license tokens for PyArmor outer keys (bind-data), with plan/feature policy helpers. Designed to work nicely with PyArmor --outer + --bind-data

Flow

graph TD
    A["1. Vendor generates<br/>keys"] -->|license_signing_private.pem<br/>license_signing_public.pem| B["2. Issue tokens<br/>per customer<br/>can create with multiple products allowed"]
    B -->|Token:<br/>payload.signature| C["3. Put token into<br/>PyArmor runtime license key<br/>--bind-data"]
    C -->|Ship<br/> license_signing_public.pem<br/> + pyarmor.rkey <br/> separately<br/> which can hold multiple<br/>products in 1 key | F["5b. Customer receives<br/>license key"]
    D["4. Obfuscate app<br/>with PyArmor<br/>pyarmor gen --outer"]
    D -->|Ship obfuscated app<br/>separately this could have trial period key | E["5a. Customer receives<br/>obfuscated app"]
    E --> G["6. App verifies<br/>token signature"]
    F --> G
    G -->|Extract claims<br/>at runtime| H["7. Enforce plan<br/>& features"]
    H -->|Allow/Deny| I["App runs<br/>or blocks"]
    
    style A fill:#e1f5ff
    style B fill:#e1f5ff
    style C fill:#e1f5ff
    style D fill:#fff3e0
    style E fill:#fff3e0
    style F fill:#fff3e0
    style G fill:#f3e5f5
    style H fill:#f3e5f5
    style I fill:#c8e6c9

This package issues and verifies small signed license tokens using pure-Python ECDSA (ecdsa package). It is designed to work nicely with PyArmor --outer + --bind-data:

  • Vendor generates a signed token (payload JSON + signature).
  • Vendor stores that token inside pyarmor.rkey using pyarmor gen key --bind-data.
  • App reads the token from the runtime key (__pyarmor__(..., b"keyinfo", 1)), verifies it, and enforces claims.

It also provides:

  • Feature flags helper (ctx.feature("export"))
  • Plan gating (ctx.require_plan("pro"))
  • Optional public-key file loading + SHA-256 fingerprint pinning to reduce key-swap risk.

Vendor workflow

  1. Generate keys (once)

Using installed CLI:

licensekit-make-keys --out-dir .

This writes:

</absolute/path/to/>/license_signing_private.pem
</absolute/path/to/>/license_signing_public.pem
  • license_signing_private.pem (keep secret)
  • license_signing_public.pem (safe to ship)
  1. Issue a token (per customer)
licensekit-issue-license  --product my_product  --customer customerA  --plan pro  --days 60  --features export,sync,api  --private-key license_signing_private.pem

This prints a token like:

<base64url(payload_json)>.<base64url(signature)>
  1. Put token into PyArmor runtime key (outer key)
licensekit-make-pyarmor-key --token "PASTE_TOKEN_HERE" --customer customerA --expire-days 60
# outputs: </absolute/path/to/>licenses/customerA/pyarmor.rkey
  1. LicenseContext in the python Entrypoint

Fingerprint pinning (recommended)

If you load the public key from disk, an attacker could try to replace it with their own. To reduce that risk, pin an allowlist of public-key fingerprints in your app.

To compute fingerprint:

licensekit-show-public-key-fingerprint

Then embed that hex digest string in your app as PINNED = ["..."].

App usage (PyArmor hook / earliest startup) Load public key from file + fingerprint pinning

from licensekit import LicenseContext

print("Started Test.py")

PINNED = ["<sha256 hex fingerprint>"]

print("Getting License Context")
ctx = LicenseContext.from_pyarmor_files(
    pubkey_path="license_signing_public.pem",
    expected_product="my_product",
    require_customer=True,
    require_plan=True,
    pinned_fingerprints_sha256=PINNED,
    search=True,
    base_file=__file__,
)

print("Checking Pro plan")
ctx.require_plan("pro")
print("has Pro plan")

if ctx.feature("export"):
    print("Export enabled")

Use embedded public key bytes (no file loading)

from licensekit import LicenseContext

PUBLIC_KEY_PEM = b\"\"\"-----BEGIN PUBLIC KEY-----
... contents ...
-----END PUBLIC KEY-----\"\"\"

ctx = LicenseContext.from_pyarmor(
    public_key_pem=PUBLIC_KEY_PEM,
    expected_product="my_product",
    require_customer=True,
    require_plan=True,
)
  1. Obfuscate with PyArmor (outer key required)
pyarmor gen --outer -O dist test.py

Ship dist/ without the key. Provide customer pyarmor.rkey.

Shipping a public key file next to the runtime key

You may also ship:

  • license_pub.pem (public key PEM)
  • pyarmor.rkey

in the same folder, then load the public key from disk at runtime.

Payload schema (suggested)

Typical payload fields:

  • product (string) required
  • customer (string) optional but recommended
  • plan ("free" | "pro" | "enterprise") optional but recommended
  • issued_at (unix epoch int) recommended
  • expires_at (unix epoch int; 0 means no expiry) recommended
  • features (list of strings) optional

You can add extra claims (like seat counts, feature bundles, etc.) and enforce them in your app.

Development

Install the local environment

python -m venv venv

Windows

venv/scripts/activate

Mac/Linux

source venv/bin/activate

Install the local Plugo project

Install poetry package manager

pip install poetry

Lock poetry dependencies

poetry cache clear pypi --all -n
poetry lock

Install plugo package via poetry (including dependencies)

poetry install --all-extras

Test

pytest
coverage run -m pytest
coverage report
coverage html
mypy --html-report mypy_report .
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --format=html --htmldir="flake8_report/basic" --exclude=venv
flake8 . --count --exit-zero --max-complexity=11 --max-line-length=127 --statistics --format=html --htmldir="flake8_report/complexity" --exclude=venv

BumpVer

With the CLI command bumpver, you can search for and update version strings in your project files. It has a flexible pattern syntax to support many version schemes (SemVer, CalVer or otherwise). Run BumbVer with:

bumpver update --major
bumpver update --minor
bumpver update --patch

Build

poetry build

Publish

poetry publish

Automated PyPI Publishing

This project uses GitHub Actions to automatically publish to PyPI when a new version tag is pushed.

Setup (One-time configuration)

  1. Register a Trusted Publisher on PyPI:
    • Go to https://pypi.org/manage/account/publishing/
    • Click "Add a new pending publisher"
    • Fill in the following details:
      • PyPI Project Name: plugo
      • Owner: RyanJulyan (your GitHub username)
      • Repository name: plugo
      • Workflow name: publish.yml
      • Environment name: pypi
    • Click "Add pending publisher"

How it works

When you use bumpver to update the version:

bumpver update --patch  # or --minor, --major

This will:

  1. Update the version in pyproject.toml, src/plugo/__init__.py, and README.md
  2. Create a git commit with the version bump
  3. Create a git tag (e.g., 4.0.1)
  4. Push the tag to GitHub

GitHub Actions will automatically detect the new tag and:

  1. Build the distribution packages (wheel and source)
  2. Publish to PyPI using the trusted publisher authentication

Security

This approach uses OpenID Connect (OIDC) Trusted Publishers, which is more secure than API tokens because:

  • ✅ No credentials are stored in GitHub secrets
  • ✅ Only this specific workflow can publish
  • ✅ Only from this specific repository
  • ✅ PyPI automatically verifies the request is legitimate

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

licensekit-0.0.1.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

licensekit-0.0.1-py3-none-any.whl (23.5 kB view details)

Uploaded Python 3

File details

Details for the file licensekit-0.0.1.tar.gz.

File metadata

  • Download URL: licensekit-0.0.1.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.12.2 Windows/11

File hashes

Hashes for licensekit-0.0.1.tar.gz
Algorithm Hash digest
SHA256 d207dc0c30e4935ed906277cb82a8aa32269b574f9b1a53988383d89271faa5c
MD5 a523a8cb95066186fc0ed571440176c3
BLAKE2b-256 cbfcfabf37692ff53291165c09d2e3b7ea06e9655a29b769ec1802d5cca77973

See more details on using hashes here.

File details

Details for the file licensekit-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: licensekit-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 23.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.12.2 Windows/11

File hashes

Hashes for licensekit-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 932c9bc822cf57a2c80b0e85c63d10af52083c9d19169fa4d56307b77993ef02
MD5 5a42fa8913db50e9947f2b8d8ade0d06
BLAKE2b-256 447795539b85c7e14de14e928387a8f342cdcb1bada48f8f936516af7a2fb830

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page