Système de licence¶
PerfShop est publié sous licence duale : AGPL-3.0-or-later pour l'usage open source, et licence commerciale pour les usages qui ne peuvent pas satisfaire les obligations de l'AGPL (voir Licence). L'accès aux interfaces sensibles de la plateforme est conditionné à une clé de licence commerciale validée cryptographiquement. Cette page décrit le fonctionnement technique du système.
Sources
backend/src/main/java/com/perfshop/service/LicenseService.java, controller/LicenseController.java, chaos/LicenseInterceptor.java, backend/src/main/resources/db/migration-fr/V1__schema.sql (table perfshop_license)
Format de la clé¶
Une clé de licence PerfShop suit le format :
Le préfixe PFSH- est un marqueur explicite qui permet une validation de forme immédiate (avant même d'appeler l'algorithme cryptographique). Le corps est constitué de deux segments séparés par un point :
payload— un objet JSON encodé en base64url contenant les informations de licencesignature— la signature RSA-PSS du payload, elle aussi encodée en base64url
Payload JSON¶
{
"licenseId": "uuid-v4",
"holder": "Nom du détenteur ou organisation",
"plan": "functional | performance | enterprise",
"issuedAt": "2026-01-15",
"expiresAt": "2027-01-15"
}
Le champ expiresAt peut être null pour une licence illimitée. Les dates sont au format ISO YYYY-MM-DD.
Algorithme cryptographique¶
La signature utilise RSA-PSS avec SHA-256, paramétré comme suit :
- Clé publique : RSA 2048 bits (format SPKI / X.509 DER, embarquée dans
LicenseService.javasous forme PEM) - Algorithme :
RSASSA-PSS - Hash :
SHA-256 - MGF :
MGF1avecSHA-256 - Salt length :
32octets - Trailer field :
1
Signature sig = Signature.getInstance("RSASSA-PSS");
PSSParameterSpec pssSpec = new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
sig.setParameter(pssSpec);
sig.initVerify(publicKey);
sig.update(payloadB64.getBytes(StandardCharsets.UTF_8));
boolean valid = sig.verify(fromBase64url(sigB64));
La clé privée correspondante reste sur perfshop.io et ne quitte jamais le serveur de génération. Elle seule peut forger des licences valides. Une tentative de forger une clé sans accès à cette clé privée est calculatoirement infaisable (sécurité standard RSA 2048).
Protection d'intégrité de la clé publique¶
LicenseService.java embarque la clé publique en dur et stocke également le SHA-256 attendu de cette clé dans la constante EXPECTED_PUBLIC_KEY_HASH. Au démarrage (@PostConstruct init()), le service calcule le hash de la clé embarquée et le compare à la valeur attendue :
private void verifyPublicKeyIntegrity() {
byte[] hashBytes = MessageDigest.getInstance("SHA-256")
.digest(PUBLIC_KEY_PEM.getBytes(StandardCharsets.UTF_8));
String actualHash = bytesToHex(hashBytes);
if (!actualHash.equals(EXPECTED_PUBLIC_KEY_HASH)) {
throw new IllegalStateException("PerfShop license public key integrity check failed");
}
}
Si le hash ne correspond pas, la JVM refuse de démarrer. C'est un filet de sécurité contre une substitution de clé publique : un attaquant qui recompilerait PerfShop pour injecter sa propre clé publique (afin de forger ses propres licences) devrait aussi modifier EXPECTED_PUBLIC_KEY_HASH. Cette modification est détectable par diff et matérialise la dérivation du code source, ce qui déclenche les obligations de publication de l'AGPL.
Ce n'est pas une protection cryptographique — un attaquant déterminé peut toujours contourner ce check. C'est un garde-fou explicite qui rend toute altération visible et force la transparence.
Priorité de chargement¶
Au démarrage, LicenseService.loadLicense() tente de charger une licence dans cet ordre :
flowchart TD
Start([Démarrage Spring Boot]) --> CheckEnv{PERFSHOP_LICENSE_KEY<br/>dans .env ?}
CheckEnv -- oui --> ParseEnv[Parser + vérifier signature]
ParseEnv -- valide --> CacheEnv[Cache mémoire<br/>licence active]
ParseEnv -- invalide --> CheckDB
CheckEnv -- non --> CheckDB{Ligne dans<br/>perfshop_license<br/>id='current' ?}
CheckDB -- oui --> ParseDB[Parser + vérifier signature]
ParseDB -- valide --> CacheDB[Cache mémoire<br/>licence active]
ParseDB -- invalide --> NoLicense
CheckDB -- non --> NoLicense[Aucune licence<br/>interfaces protégées bloquées]
Priorité 1 — variable d'environnement PERFSHOP_LICENSE_KEY. C'est le mode recommandé en salle de classe ou en déploiement automatisé : la clé est injectée au moment du docker compose up et l'instance démarre avec sa licence déjà active. Exemple :
# Unix / macOS
echo "PERFSHOP_LICENSE_KEY=PFSH-xxx.yyy" >> .env
# Windows PowerShell
Add-Content .env "PERFSHOP_LICENSE_KEY=PFSH-xxx.yyy"
Priorité 2 — table perfshop_license en base. Utilisée quand la licence est activée a posteriori via l'UI (panneau formateur → gestion licence → coller la clé → Activer) ou via POST /api/license/activate. Un seul enregistrement est conservé, toujours avec id = 'current'.
Si aucune des deux sources ne fournit une licence valide et non expirée, le cache reste null et le LicenseInterceptor bloque les interfaces protégées.
Les trois plans¶
| Plan | Débloque | Cible |
|---|---|---|
functional |
chaos-admin, admin, monitoring, scripts-ui | Équipes de test fonctionnel, chaos engineering hors charge |
performance |
Tout du plan functional + jmeter-ui |
Équipes de performance engineering |
enterprise |
Accès complet (toutes features actuelles et futures) | Grandes organisations, usage multi-équipes |
Les tarifs et conditions commerciales ne sont pas documentés ici — contactez contact@perfshop.io pour obtenir une licence.
La méthode hasFeature(String feature) de LicenseService implémente cette logique :
return switch (info.plan) {
case "enterprise" -> true;
case "performance" -> !feature.equals("unavailable");
case "functional" -> List.of("chaos-admin", "admin", "monitoring", "scripts-ui").contains(feature);
default -> false;
};
Interception HTTP — LicenseInterceptor¶
LicenseInterceptor est un HandlerInterceptor Spring MVC qui intercepte toutes les requêtes HTTP et décide de les laisser passer ou de répondre 402 Payment Required.
Chemins toujours autorisés¶
Ces chemins ne sont jamais bloqués, quelle que soit la licence :
/api/license/*— activation, statut, révocation (sinon deadlock : impossible d'activer sans être authentifié)/api/chaos/student/*— la page étudiant freemium doit rester accessible/api/chaos/public/*— monitoring pédagogique lecture seule/api/products,/api/auth/,/api/cart/,/api/checkout/,/api/orders,/api/countries— le shop e-commerce reste libre/actuator/*— Prometheus doit pouvoir scraper/images/*— assets produits
Chemins protégés par plan¶
Pour les autres chemins, l'intercepteur identifie la feature requise :
| Préfixe d'URL | Feature requise |
|---|---|
/api/jmeter/* |
jmeter-ui |
/api/admin/* |
admin |
/api/chaos/* (sauf /student/ et /public/) |
chaos-admin |
/api/scripts/* |
scripts-ui |
/api/monitoring/* |
monitoring |
Si la licence ne débloque pas la feature demandée, la réponse est :
HTTP/1.1 402 Payment Required
Content-Type: application/json;charset=UTF-8
X-License-Required: true
{
"error": "LICENSE_REQUIRED",
"message": "Licence requise pour accéder à chaos-admin",
"path": "/api/chaos/backend",
"activateUrl": "/api/license/activate",
"statusUrl": "/api/license/status",
"portalUrl": "https://perfshop.io"
}
Le header X-License-Required: true permet aux clients HTTP de détecter rapidement ce cas.
Endpoints publics de gestion¶
LicenseController expose trois endpoints publics (sans authentification) :
GET /api/license/status¶
Retourne le statut actuel de la licence. Toujours disponible. Utilisé par toutes les UI (chaos-admin, scripts-ui, jmeter-ui) pour afficher l'état avant même d'authentifier l'utilisateur.
{
"valid": true,
"plan": "performance",
"planLabel": "🎯 Performance Testing",
"holder": "École XYZ",
"issuedAt": "2026-01-15",
"expiresAt": "2027-01-15",
"unlimited": false,
"daysRemaining": 180,
"features": ["chaos-admin", "admin", "monitoring", "scripts-ui", "jmeter-ui"]
}
Si aucune licence n'est active, la réponse est :
{
"valid": false,
"plan": "none",
"planLabel": "Aucune licence",
"message": "Aucune licence active"
}
POST /api/license/activate¶
Active une clé de licence. Appelable sans authentification — c'est nécessaire pour le premier déploiement, sinon deadlock (on ne peut pas se connecter sans licence). La sécurité est assurée par la vérification RSA-PSS de la clé.
POST /api/license/activate
Content-Type: application/json
{ "licenseKey": "PFSH-eyJsaWNlbnNlSWQi...xyz" }
Réponses possibles :
200 OK+{ success, message, status }— succès400 Bad Request— clé vide ou format invalide (ne commence pas parPFSH-)422 Unprocessable Entity— signature invalide, clé expirée, ou plan inconnu
POST /api/license/revoke¶
Révoque la licence active. Marque revoked = 1 en base et vide le cache mémoire. Également public — sans cela, impossible de révoquer une licence expirée sans accéder à la base directement. Retourne 409 Conflict si aucune licence n'est active.
Diagramme d'activation¶
sequenceDiagram
autonumber
participant U as Utilisateur
participant C as chaos-admin UI
participant B as LicenseController
participant S as LicenseService
participant DB as MySQL
U->>C: Colle clé PFSH-xxx.yyy
C->>B: POST /api/license/activate
B->>S: activateLicense(key)
S->>S: parseLicense → vérif RSA-PSS
alt Signature valide
S->>DB: UPDATE perfshop_license SET licence_key=...
S->>S: cachedLicense.set(info)
S-->>B: ActivationResult.success
B-->>C: 200 { success, status }
C-->>U: ✅ Licence activée
else Signature invalide
S-->>B: ActivationResult.error
B-->>C: 422 { error }
C-->>U: ❌ Clé invalide
end
Schéma de la table perfshop_license¶
La table est créée par V1__schema.sql (voir Schéma de la base de données).
CREATE TABLE perfshop_license (
id VARCHAR(36) NOT NULL DEFAULT 'current' PRIMARY KEY,
license_key TEXT NOT NULL,
plan VARCHAR(20) NOT NULL,
holder VARCHAR(255) NOT NULL,
issued_at DATE NOT NULL,
expires_at DATE NULL,
activated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
revoked TINYINT(1) NOT NULL DEFAULT 0,
CONSTRAINT chk_plan CHECK (plan IN ('functional', 'performance', 'enterprise'))
);
Un seul enregistrement à la fois (contrainte id = 'current'). L'upsert de activateLicense() fait un UPDATE d'abord, puis un INSERT si aucune ligne n'existait.
Obtenir une licence¶
Pour obtenir une licence PerfShop commerciale, contactez contact@perfshop.io. Les licences sont délivrées pour des durées d'un an ou pour une durée illimitée selon le plan négocié. Les licences incluent l'accès à la bibliothèque de TP vendue séparément et hébergée sur le portail perfshop.io.