Structure générale du .lic

Un fichier de licence IronLock v2 est un document JSON signé ECDSA P-384. La signature est intégrée dans le JSON lui-même, ce qui simplifie la distribution : un seul fichier contient à la fois les données de licence et leur preuve d'authenticité.

# Structure complète d'un .lic IronLock v2
{
  // ── Champs obligatoires ──
  "format_version": "2.0",
  "licence_id":     "LIC-2026-A3F8C2E1",
  "product":        "MonApp",
  "product_version":"1.1.0",
  "hardware_id":    "a3f8c2e1d4b7...",
  "issued_at":      "2026-01-15T10:00:00Z",
  "expires_at":     "2027-01-15T10:00:00Z",

  // ── Champs optionnels ──
  "licensee":       "Entreprise SAS",
  "max_activations":1,
  "grace_days":     7,
  "features":       ["module_analytics", "export_pdf"],
  "custom":         {"client_ref": "CLI-0042"},

  // ── Signature ECDSA P-384 ──
  "signature":      "base64_ecdsa_p384_signature..."
}

Champs obligatoires

  • format_version — version du format de licence. Permet au loader de savoir comment parser le fichier. Valeur actuelle : "2.0".
  • licence_id — identifiant unique généré par l'éditeur. Format libre, recommandé : LIC-YYYY-HASH8. Permet de tracer et révoquer une licence spécifique.
  • product et product_version — le loader vérifie que la licence correspond bien au produit qu'il protège. Une licence MonApp ne peut pas être utilisée pour MonAutreApp.
  • hardware_id — hash SHA-256 du fingerprint hardware de la machine cliente. C'est le champ central qui lie la licence à une machine précise.
  • issued_at et expires_at — horodatages ISO 8601 UTC. Le loader vérifie l'expiration à chaque démarrage.

Champs optionnels

  • licensee — nom du client. Affiché dans l'interface "À propos" si disponible.
  • max_activations — nombre maximum d'activations autorisées (utile pour les licences multi-postes).
  • grace_days — période de grâce après expiration avant blocage total. Utile pour éviter les coupures brutales si le renouvellement est en cours.
  • features — liste des fonctionnalités activées. Le code peut vérifier licence.features.contains("module_analytics") pour activer des modules premium.

Extensions custom

Le champ custom accepte n'importe quel objet JSON. Il est inclus dans le payload signé, donc toute modification invalide la signature :

"custom": {
  "client_ref":    "CLI-0042",
  "contract_type": "enterprise",
  "seats":        10,
  "reseller":     "Partenaire SARL",
  "notes":        "Renouvellement annuel — contact: jean@client.fr"
}

Ces données custom sont accessibles dans le code protégé via context.licence.custom["seats"]. Elles permettent d'adapter le comportement de l'application selon le contrat sans recompiler.

Signature ECDSA intégrée

La signature est calculée sur le payload JSON canonicalisé (clés triées alphabétiquement, sans le champ signature lui-même) :

import json, base64
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes

def sign_licence(data: dict, private_key) -> dict:
    payload = {k: v for k, v in data.items() if k != 'signature'}
    canonical = json.dumps(payload, sort_keys=True, separators=(',',':')).encode()
    sig = private_key.sign(canonical, ec.ECDSA(hashes.SHA384()))
    data['signature'] = base64.b64encode(sig).decode()
    return data

Parsing sécurisé Python côté client

def load_licence(path: str, public_key) -> dict:
    with open(path, encoding='utf-8') as f:
        data = json.load(f)

    # 1. Vérifier la version du format
    if data.get('format_version') != '2.0':
        raise ValueError("Format de licence non supporté")

    # 2. Vérifier la signature ECDSA
    sig_b64 = data.pop('signature')
    canonical = json.dumps(data, sort_keys=True, separators=(',',':')).encode()
    try:
        public_key.verify(base64.b64decode(sig_b64), canonical, ec.ECDSA(hashes.SHA384()))
    except:
        raise SecurityError("Signature de licence invalide")

    data['signature'] = sig_b64  # restaurer
    return data

Validation complète

from datetime import datetime, timezone
import sys

def validate_licence(lic: dict, current_hw_id: str, product: str):
    now = datetime.now(timezone.utc)

    # Produit
    if lic['product'] != product:
        sys.exit("Licence invalide pour ce produit")

    # Machine
    if lic['hardware_id'] != current_hw_id:
        sys.exit("Licence non autorisée sur cette machine")

    # Expiration (avec grâce)
    expires = datetime.fromisoformat(lic['expires_at'].replace('Z','+00:00'))
    grace = lic.get('grace_days', 0)
    from datetime import timedelta
    if now > expires + timedelta(days=grace):
        sys.exit("Licence expirée")

Conclusion

Le format .lic IronLock v2 est minimal mais extensible : les champs obligatoires garantissent la sécurité de base, le champ features permet la gestion des éditions, et le champ custom libre permet d'embarquer n'importe quelle donnée métier dans la licence — sans recompiler le loader. La signature ECDSA P-384 rend toute falsification détectable immédiatement.

🔐
PRODUIT LIÉ
IronLock v2.0
← Retour au blog Article suivant →