Modeva

2.0.2 Not latest — view latest

Integrated tool for model development and validation.

Published
July 13, 2026
8d ago
Package Registry
README badge Customize →
License Sources
SourceLicenseClass
Licensie (detected)
Pending-
PyPI (reported)
proprietaryUnknown

License detection is still in progress for this version.

Loading dependencies…
License File
# MoDeVa Licensing Design

*Prepared for team review — 2026-06-18. Branch: `feat/nuitka-enterprise-build`.*

Design for the license system shipped inside the compiled (Nuitka) MoDeVa
wheel. Models the approach used by the sibling Knowlytix GMS product.

---

## Decisions (settled)

| Decision | Choice | Rationale |
|---|---|---|
| License transport | **Pure offline signed JWT (RS256)** | Tamper-proof entitlement, verified offline against an embedded public key. No runtime server. |
| Machine binding | **None (honor-system)** | Removes UUID-ping fragility and VM/CI/airgap support burden. Anti-sharing rests on expiry + compliance. |
| Tiers | **demo (keyless), developer (180d), enterprise (365d)** | Classroom dropped. Demo = keyless eval; developer = 6-month trial; enterprise = commercial. |
| Demo (no key) | **Keyless eval, built-in datasets only** | `pip install` works out of the box at demo caps; loading *custom* data needs a key. Not time-boxed (keyless). |
| Capability gating | **Scale-only numeric caps** (+ demo's built-in-only gate) | `max_dataset_rows`, `max_registered_models`, `max_workers`; demo additionally blocks custom-data loading. |
| Feature availability | **All tools open to all paid tiers** | Demo is the limited exception (built-in data only). |
| Commercial use | **Enterprise only; EULA + enforced** | Demo/developer are non-commercial (EULA). Enterprise is commercial AND issuable only by the internal key (see §1b). |
| Developer issuance | **Internal now; self-serve later** | `provision.py --template developer` today; the self-serve Worker (`provisioning/issuer-worker/`, NOT deployed) automates it later. |
| Online server (Supabase) | **Removed from the client** | Pure-offline → no runtime server. Eliminates the exposed-credential problem. |

### Consequences for the current security debt
- **(A) default-on `MODEVA_DEV_MODE` bypass → fixed** (auth.py rewritten; no key now → restricted demo, not full access).
- **(B) forgeable XOR offline licenses → fixed** by the signed JWT.
- **(C) exposed Supabase creds → fixed** by removing the client-side Supabase code.

---

## 1. License key format

A signed JWT, prefixed for human recognition: `modeva_live_<jwt>`.

```json
{
  "customer_id": "user@org.com",
  "tier": "developer | enterprise",   // demo is keyless (no token)
  "expires": "2026-12-16",          // ISO date = issue date + duration_days
  "key_version": 1,
  "caps": {
    "max_dataset_rows": 50000,
    "max_registered_models": 10,
    "max_workers": 1
  }
}
```

- **Signature:** RS256. Private key held only by the provisioner; the wheel
  embeds the **public** key(s) (safe to ship — cannot forge).
- **`kid`** (JWT header): names the public key the client should verify against.
- **`expires`** is a custom ISO-date claim (not the numeric JWT `exp`); "expires"
  means the last valid day inclusive.
- **No `machine_id`, no `commercial_use`** claim (honor-system; commercial scope
  lives in the EULA).

---

## 1a. Key management & rotation (multi-key)

Only **public** keys ever ship. The verifier trusts a *set* of public keys:
`modeva/modeva-public.pem` (primary, `kid="modeva-public"`) plus every
`modeva/_license_keys/<id>.pem` (rotation keys, `kid="<id>"`). A license verifies
if its signature matches **any one** trusted key (JWKS-style), so multiple
keypairs can coexist.

**Where each half lives:**
- Private keys → KMS / secret store only (and the GitHub Actions secret
  `MODEVA_PRIVATE_KEY` for CI smoke tests). Never committed (`.gitignore`).
- Public keys → committed and bundled in the wheel.

**Rotating the signing key without downtime:**
1. Generate a new keypair; keep the new private key in the KMS.
2. Commit the new **public** key to `modeva/_license_keys/<new-id>.pem` and cut a
   release. Wheels now trust both old and new keys.
3. Issue new/renewed licenses with the new private key:
   `provision.py --kid <new-id> --private-key <new-private.pem> ...`. Old licenses
   keep verifying against the old key **until they expire**.
4. After all old-key licenses have expired, remove the old public key in a later
   release. The system then runs on the new key alone.

> Caveat: pure-offline licensing has no real revocation. Rotating because of a
> *leaked* private key only protects customers who take the new wheel — it cannot
> invalidate already-issued (or attacker-minted) licenses on copies that don't
> upgrade. Multi-key is what lets a rotation *start* without breaking everyone.

---

## 1b. Commercial = internal-only (tier→key binding)

Two signing keys, both trusted by the wheel:
- **internal** (`kid="modeva-public"`) — the internal team's `provision.py`; may
  issue **any** tier (developer or enterprise).
- **self-serve** (`kid="selfserve"`) — the (future) public Worker endpoint; in
  practice issues developer.

`verify_license` records *which trusted key actually verified the signature* and
enforces `_TIER_KID_RESTRICTIONS = {"enterprise": {"modeva-public"}}`: a
`tier=enterprise` token is rejected unless it was verified by the **internal**
key. So a compromised public endpoint (self-serve key) can mint developer keys
but **never a valid commercial license**. Spoofing the `kid` header doesn't help —
the check is on the verifying key, not the header. `developer` is unrestricted so
it can be issued internally now *and* by the endpoint later.

---

## 2. Tier templates  *(strawman caps — tune before launch)*

| | demo (keyless) | developer (180d) | enterprise (365d) |
|---|---|---|---|
| `max_dataset_rows` | 50,000 | 50,000 | unlimited |
| `max_registered_models` | 3 | 10 | unlimited |
| `max_workers` (n_jobs) | 1 | 1 | unlimited |
| custom data (own CSV/DataFrame) | ❌ built-in only | ✅ | ✅ |
| commercial use | ❌ | ❌ | ✅ |
| issued by | — (keyless) | internal now / self-serve later | internal only |

`"unlimited"` is a sentinel parsed to a typed `Unlimited` singleton so call sites
branch on `isinstance(cap, Unlimited)` rather than magic numbers.

---

## 3. Runtime verification flow

Called once at import (replacing today's `auth.run()` checkpoints):

1. **Read key** — precedence: `MODEVA_LICENSE_KEY` env var → `~/.modeva/license.key`.
2. **Verify RS256** signature against the embedded public key. Any failure →
   generic `LicenseError("signature verification failed.")` (**fail-closed**).
3. **Check expiry** — `expires < today` → expired error with renewal URL.
4. **Validate `caps`** against the schema; missing/malformed caps on a known
   `key_version` = tampering → fail-closed.
5. **Publish caps** via `set_caps()` for enforcement call sites.
6. **Startup banner** to stderr: `modeva vX licensed to customer=… tier=… expires=…`.

`get_caps()` raises if called before successful verification — a tamper that
strips the verify call fails closed (no caps → error), never open.

---

## 4. Enforcement (numeric caps at existing chokepoints)

`auth.run()` is already invoked in `DataSet`, `ModelZoo`, `TestSuite`, and every
model constructor — caps slot into those same points:

| Cap | Where | Behaviour |
|---|---|---|
| `max_dataset_rows` | `DataSet.load` / `_set_raw_data` | **Reject** with actionable error ("developer tier supports up to 50k rows; sample your data or upgrade") — clearer than silently truncating data |
| `max_registered_models` | `ModelZoo.register` | **Reject** beyond cap |
| `max_workers` | `n_jobs` plumbing | **Clamp** downward (clamp, don't crash) |

---

## 5. Provisioning

A `provisioning/provision.py` CLI (kept **out** of the shipped wheel):

```
provision.py --template {developer|enterprise} \
             --customer user@org.com \
             [--expires YYYY-MM-DD]        # else issue-date + duration_days \
             [--kid <signing-key-id>]      # default "modeva-public" (internal) \
             [--overrides customer.json]   # enterprise only, tighten-only
```

- Loads `provisioning/templates/<tier>.json` (duration + caps).
- Computes `expires`, signs with the private key, prints `modeva_live_<jwt>`.
- **Enterprise overrides** may only *tighten* caps (int ≤ template; bool
  true→false) and duration; developer is a fixed shape.
- Records the signing `kid`, resolved caps, and customer in the **tracked**
  `provisioning/issued.jsonl` log, and emits a ready-to-paste email snippet.
- **Private key** lives in a KMS / secret store — never in the repo or a wheel.
- The (future) self-serve Worker at `provisioning/issuer-worker/` automates
  developer issuance online; **not deployed** — see its README. Key-retirement
  timing: `provisioning/key_status.py`.

---

## 6. Packaging notes

- Embed `modeva/modeva-public.pem`; load via **`importlib.resources`** (not
  `__file__` math) — this sidesteps the Nuitka `..`/normpath resource gotcha
  entirely.
- Add the PEM to `package_data` in **both** `setup.py` and `setup_nuitka.py`.
- `provisioning/` and `templates/` are **excluded** from the wheel (private-key
  tooling must never ship).

---

## 7. Security prerequisites & threat model

**Done:** the old default-on `MODEVA_DEV_MODE` full-access bypass is gone. No key
now drops to a **restricted demo** (built-in datasets only), not full access.

**Ceiling (honest):** all checks run client-side in the wheel. A determined
attacker can patch the compiled binary. The goal is to deter casual bypass and
make the granted scope **auditable** (the signature is tamper-proof evidence) —
not to defeat reverse engineering. Compilation (and optionally Nuitka Commercial
constant-encryption) raises that bar; see `COMPILATION_COMPARISON.md`.

---

## 8. File layout

```
modeva/_license.py                      # verify(), key sources, multi-key load, tier->kid binding
modeva/_caps.py                         # schema, Unlimited, apply_ceiling, get/set_caps, tier defaults (+ DEMO_CAPS)
modeva/auth.py                          # verify once + publish caps; require_licensed_feature() (demo gate)
modeva/modeva-public.pem                # embedded INTERNAL public key (shipped)
modeva/_license_keys/*.pem              # additional trusted public keys (rotation + selfserve), shipped
provisioning/provision.py               # internal issuance CLI (NOT shipped)
provisioning/key_status.py              # when is a signing key safe to retire (NOT shipped)
provisioning/templates/{developer,enterprise}.json   # (NOT shipped)
provisioning/issued.jsonl               # TRACKED internal issuance log (NOT shipped in wheel)
provisioning/issuer-worker/             # self-serve trial Worker (future infra, NOT deployed)
```

---

## 9. Open items
- Confirm the strawman cap numbers (§2), incl. demo caps.
- `EULA.md`: legal entity name (§3); counsel review. Non-commercial clause done.
- Production keys: generate internal + self-serve keypairs in a KMS; commit their
  public halves; private keys to KMS + (for the Worker) a Worker secret.
- If/when the self-serve Worker is deployed: generate `selfserve.pem`, set the
  Worker secrets + Turnstile, and consider D1 for a queryable trial user list.
- Migration: existing PyPI `Modeva` 1.0.x users upgrading to 1.1.0 now hit
  licensing (demo if no key) — decide comms / version story (see RELEASE.md).
Versions
4 versions
VersionLicensePublishedStatus
2.1.0 Latest proprietary (Unverified)Jul 16, 2026 Pending
2.0.2 Viewingproprietary (Unverified)Jul 13, 2026 Pending
2.0.1 proprietary (Unverified)Jul 9, 2026 Pending
2.0.0 proprietary (Unverified)Jul 8, 2026 Pending