Files
keysat-client-python/src/keysat_licensing_client/verify.py
T
2026-05-07 10:39:37 -05:00

83 lines
2.6 KiB
Python

"""Offline signature verification — the bulk of the value of the SDK."""
from __future__ import annotations
import uuid
from dataclasses import dataclass
from cryptography.exceptions import InvalidSignature
from .errors import LicensingError
from .key import LicensePayload, parse_license_key
from .pubkey import PublicKey
@dataclass(frozen=True)
class VerifyOk:
"""Result of a successful offline `Verifier.verify()` call."""
license_id: uuid.UUID
product_id: uuid.UUID
issued_at: int
expires_at: int # 0 = perpetual
is_trial: bool
is_fingerprint_bound: bool
fingerprint_hash: bytes # 32 bytes; all-zero if unbound
entitlements: list[str]
payload: LicensePayload # the full parsed payload
class Verifier:
"""Offline license-key verifier.
Given the issuer's PEM-encoded public key, verifies the cryptographic
integrity of a license key string with no network access. Suitable
for boot-time license checks where the licensing server may be
unreachable.
Usage::
verifier = Verifier(PublicKey.from_pem(ISSUER_PUBKEY_PEM))
ok = verifier.verify(key_from_user)
# raises LicensingError on bad signature / bad format
For revocation and fingerprint binding, layer the online
`Client.validate(...)` on top of this — but only AFTER offline
verification has passed.
"""
def __init__(self, public_key: PublicKey):
self._pubkey = public_key
def verify(self, key: str) -> VerifyOk:
"""Verify a `LIC1-...-...` key.
On success, returns a `VerifyOk` with all parsed fields. On
failure, raises `LicensingError`:
- `kind="bad_format"`: the key string is malformed.
- `kind="bad_signature"`: signature didn't verify against
the issuer's public key (key was edited, fabricated, or
issued by a different server).
"""
parsed = parse_license_key(key)
try:
self._pubkey.underlying.verify(parsed.signature, parsed.payload_bytes)
except InvalidSignature as e:
raise LicensingError(
"bad_signature",
"signature did not verify against the issuer's public key",
) from e
p = parsed.payload
return VerifyOk(
license_id=p.license_id,
product_id=p.product_id,
issued_at=p.issued_at,
expires_at=p.expires_at,
is_trial=p.is_trial,
is_fingerprint_bound=p.is_fingerprint_bound,
fingerprint_hash=p.fingerprint_hash,
entitlements=list(p.entitlements),
payload=p,
)