Metadata-Version: 2.4
Name: lsyods_license_sdk
Version: 0.1.0
Summary: Shared machine identity and offline license SDK for LSY ODS products
Author: 成都领数云科技有限公司
License-Expression: MIT
Project-URL: Homepage, https://github.com/9kl/lsyods_license_sdk
Project-URL: Repository, https://github.com/9kl/lsyods_license_sdk.git
Project-URL: Issues, https://github.com/9kl/lsyods_license_sdk/issues
Keywords: license,offline-license,ed25519,machine-fingerprint,lsyods
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography<49.0.0,>=41.0.0
Dynamic: license-file

# LSY ODS License SDK

[![Python](https://img.shields.io/pypi/pyversions/lsyods-license-sdk.svg)](https://pypi.org/project/lsyods-license-sdk/)
[![PyPI](https://img.shields.io/pypi/v/lsyods-license-sdk.svg)](https://pypi.org/project/lsyods-license-sdk/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/9kl/lsyods_license_sdk/blob/main/LICENSE)

`lsyods-license-sdk` is the shared Python SDK for LSY ODS machine identity and
offline product licensing. It provides stable machine fingerprints,
installation IDs, Ed25519 license issuance, signature verification, and
license status evaluation.

- Author: 成都领数云科技有限公司
- Repository: <https://github.com/9kl/lsyods_license_sdk>
- License: MIT

## Features

- Calculate a versioned, one-way machine fingerprint on Windows and Linux.
- Generate product-specific installation IDs.
- Issue Ed25519-signed offline license documents.
- Verify signatures against an explicit trusted-public-key set.
- Bind a license to a product, software version, product edition,
  installation ID, and machine fingerprint.
- Evaluate subscription, perpetual, expired, and not-yet-valid states.
- Keep renewal history through `supersedes_license_id`.

The product edition represents a commercial level such as `basic` or
`advanced`. Individual feature/module decisions remain in the licensed
product. A perpetual license has no expiry date, but it still applies only to
the software version declared in its signed claims.

## Installation

Install from PyPI:

```bash
pip install lsyods-license-sdk
```

Or with uv:

```bash
uv add lsyods-license-sdk
```

Python 3.10 or newer is required.

## Generate Signing Keys

The SDK includes the `lsyods-license-keygen` command. By default it prompts for
a password and encrypts the generated Ed25519 private key:

```powershell
$env:LSYODS_LICENSE_KEY_PASSWORD = Read-Host "Key password" -MaskInput

lsyods-license-keygen `
  --private-key D:\secure\license-private.pem `
  --public-key D:\secure\license-public.pem
```

The command prints the public `key_id` and `public_key_base64` used by licensed
products. Only the controlled license issuer may access the private key. Never
ship the private key or its password with a client or licensed product.

`--no-password` is available for protected automated environments, but the key
file must then be secured with operating-system access controls.

## Machine Fingerprint

```python
from lsyods_license_sdk import calculate_machine_fingerprint

machine_fingerprint = calculate_machine_fingerprint()
print(machine_fingerprint)  # sha256:<64 lowercase hexadecimal characters>
```

The SDK hashes a stable operating-system machine identifier with a versioned
domain separator. It does not return the raw Windows MachineGuid or Linux
machine-id.

## Issue a License

License issuance belongs in a controlled server environment:

```python
from datetime import datetime, timedelta, timezone
from pathlib import Path

from lsyods_license_sdk import (
    generate_installation_id,
    issue_license,
    load_private_key,
)

now = datetime.now(timezone.utc).replace(microsecond=0)
private_key = load_private_key(
    Path("D:/secure/license-private.pem"),
    password="private-key-password",
)

claims = {
    "license_id": "L260819000001",
    "issuer": "LSY ODS",
    "customer": "Example Customer",
    "product": {
        "code": "example-product",
        "version": "V1.0",
        "edition": "advanced",
    },
    "installation": {
        "installation_id": generate_installation_id(),
        "machine_fingerprint": "sha256:" + "a" * 64,
    },
    "license_type": "subscription",
    "issued_at": now.isoformat(),
    "not_before": now.isoformat(),
    "expires_at": (now + timedelta(days=90)).isoformat(),
}

license_document = issue_license(claims, private_key)
```

For a perpetual license, use `license_type="perpetual"` and
`expires_at=None`.

## Verify a License

Licensed products contain only trusted public keys and verify the local
license against the running product and installation:

```python
from lsyods_license_sdk import calculate_machine_fingerprint, verify_license

trusted_keys = {
    "<key_id>": "<public_key_base64>",
}

result = verify_license(
    license_document,
    trusted_keys,
    expected_product="example-product",
    expected_installation_id=installation_id,
    expected_machine_fingerprint=calculate_machine_fingerprint(),
)

if not result["valid"]:
    raise RuntimeError(f"License is {result['status']}")

licensed_version = result["claims"]["product"]["version"]
licensed_edition = result["claims"]["product"]["edition"]
```

The product is responsible for comparing `licensed_version` and
`licensed_edition` with its compatibility and feature rules.

## Renewal

Renewal creates a new signed document while retaining the existing
installation ID and machine fingerprint. The previous document remains
unchanged and verifiable. The new claims may include:

```json
{
  "supersedes_license_id": "L260819000001"
}
```

## Security Model

- Security depends on protecting the Ed25519 private key, not hiding this SDK.
- Products must trust only explicitly configured public keys.
- License documents must be treated as immutable after signing.
- A machine fingerprint is an identifier, not an authentication secret.
- Offline licensing cannot prevent a customer from patching an application
  they fully control; products should enforce licensing at appropriate trust
  boundaries.
- Key rotation should add the new public key before issuing licenses with it.

## Development

The project uses uv and the Aliyun PyPI mirror configured in `pyproject.toml`:

```bash
uv sync --locked
uv run pytest
uv run pyflakes src tests
uv build --no-sources
```

## License

Copyright (c) 2026 成都领数云科技有限公司.

Released under the [MIT License](https://github.com/9kl/lsyods_license_sdk/blob/main/LICENSE).
