Langages
Intégration Python
SDK Python 3.9+ : signatures vérifiées, retries, heartbeat, identifiant d’appareil.
SDK officiel
Paquet keyline-sdk (dossier sdks/python du dépôt), dépendance unique : cryptography. Client thread-safe, résultats typés, aucune exception pour un refus de licence.
bash
pip install ./sdks/pythonpython
from keyline import LicenseClient
client = LicenseClient(
application_id="<application_id>",
api_url="https://keyline.example.com/api/v1",
public_keys={"<kid>": PUBLIC_KEY_PEM},
app_version="1.4.2",
)
result = client.activate(license_key="LIC-XXXX-XXXX-XXXX-XXXX") # identifiant d'appareil par défaut
if result.success:
print("Licence active jusqu'au", result.license.expires_at or "à vie")
stop = client.start_heartbeat(on_failure=lambda r: lock_premium_features(r.error.code))
else:
print("Refus :", result.error.code, result.error.message)Sans device_id, le SDK dérive un identifiant stable de l’identifiant machine, haché localement. result.verified indique si une erreur provient d’une réponse signée.
Sans SDK : le protocole en 60 lignes
Référence pour un environnement où le SDK ne convient pas (même contrat, mêmes vérifications).
python
# keyline_client.py — Python 3.10+ : pip install requests cryptography
import base64
import secrets
import time
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
API = "https://keyline.example.com/api/v1"
APPLICATION_ID = "<application_id>"
PUBLIC_KEYS = { # dashboard → Applications → Clés de signature
"<kid>": serialization.load_pem_public_key(b"""-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----"""),
}
class LicenseError(Exception):
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def _b64url(s: str) -> bytes:
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def _verify(resp: requests.Response) -> None:
header = resp.headers.get("Keyline-Signature")
if not header:
raise LicenseError("UNSIGNED", "Réponse non signée")
parts = dict(p.split("=", 1) for p in header.split(","))
key = PUBLIC_KEYS.get(parts["kid"])
if key is None:
raise LicenseError("UNKNOWN_KEY", "Clé de signature inconnue")
raw = _b64url(parts["sig"]) # format IEEE P1363 : r || s (2 × 32 octets)
der = encode_dss_signature(int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big"))
# Lève cryptography.exceptions.InvalidSignature si la réponse a été altérée.
key.verify(der, parts["ts"].encode() + b"." + resp.content, ec.ECDSA(hashes.SHA256()))
def _call(endpoint: str, **payload) -> dict:
nonce = secrets.token_urlsafe(16)
# Les champs facultatifs absents ne sont pas envoyés (null est refusé par l'API).
body = {"application_id": APPLICATION_ID, "nonce": nonce, **{k: v for k, v in payload.items() if v is not None}}
for attempt in range(3):
try:
resp = requests.post(f"{API}/licenses/{endpoint}", json=body, timeout=10) # TLS vérifié par défaut
except requests.RequestException:
if attempt == 2:
raise
time.sleep(0.5 * 2**attempt)
continue
if resp.status_code == 429 and attempt < 2:
time.sleep(int(resp.headers.get("Retry-After", "1")))
continue
_verify(resp)
data = resp.json()
if data.get("nonce") != nonce:
raise LicenseError("REPLAY", "Nonce inattendu")
if not data["success"]:
raise LicenseError(data["error"]["code"], data["error"]["message"])
return data
raise LicenseError("RATE_LIMITED", "Trop de requêtes")
def activate(license_key: str, device_id: str, app_version: str | None = None) -> dict:
return _call("activate", license_key=license_key, device_id=device_id, app_version=app_version)
def heartbeat(session_token: str) -> dict:
return _call("heartbeat", session_token=session_token)
def deactivate(session_token: str) -> dict:
return _call("deactivate", session_token=session_token)python
import hashlib
import platform
import subprocess
import uuid
def device_id(salt: str = "mon-logiciel") -> str:
"""Identifiant stable, haché localement : la valeur brute ne quitte jamais la machine."""
raw = None
system = platform.system()
try:
if system == "Windows":
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as k:
raw = winreg.QueryValueEx(k, "MachineGuid")[0]
elif system == "Linux":
raw = open("/etc/machine-id").read().strip()
elif system == "Darwin":
out = subprocess.check_output(["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], text=True)
raw = out.split('"IOPlatformUUID" = "')[1].split('"')[0]
except Exception:
raw = None
raw = raw or str(uuid.getnode())
return hashlib.sha256(f"{salt}:{raw}".encode()).hexdigest()python
from keyline_client import LicenseError, activate, heartbeat
try:
result = activate("LIC-8F3A-29BC-74DE-6A12", device_id(), app_version="1.4.2")
print("Licence active jusqu'au", result["license"]["expires_at"] or "jamais")
token = result["session"]["token"]
except LicenseError as e:
if e.code == "DEVICE_LIMIT_REACHED":
print("Nombre maximal d'appareils atteint : libérez un poste depuis votre espace client.")
else:
print(f"Licence refusée : {e.code}")