Eröffnung in Kürze — Autoren behalten 100 % ihres Preises Autor werden Anmelden Konto erstellen
Hallo, anmelden Konto & Listen IhrAutorenbereich

Lizenzen, Entwicklerdokumentation

So prüfen Sie einen Lizenzschlüssel in Ihrem eigenen Produkt: die Bibliothek, die API, fertige Integrationen für elf Umgebungen und die Regeln, die die Shops Ihrer Käufer am Laufen halten.

1.Was das leistet, und was nicht

Ein Lizenzschlüssel bindet Ihr Produkt an eine Domain, oder an so viele, wie der Käufer bezahlt hat. Er hilft einem ehrlichen Kunden, in dem zu bleiben, was er gekauft hat, er gibt Ihnen die Zahl Ihrer Installationen, und er macht einen durchgesickerten Schlüssel abschaltbar — der Besitzer erzeugt ihn neu, und der durchgesickerte stirbt. Es ist kein Kopierschutz. Ihr Käufer hat Ihren Quelltext und kann die Prüfung in fünf Minuten entfernen. Wer Ihnen etwas anderes erzählt, will Ihnen etwas verkaufen.

2.Drei Schritte

  1. Öffnen Sie Ihr Produkt im Autorenbereich, gehen Sie zu Lizenz und laden Sie die Bibliothek herunter. Sie enthält bereits unseren öffentlichen Schlüssel und Ihre Produktkennung — nichts einzurichten.
  2. Fragen Sie den Käufer bei der ersten Installation nach seinem Schlüssel und legen Sie ihn dort ab, wo Ihr Produkt seine Einstellungen speichert. Schreiben Sie ihn nie fest in den Code.
  3. Rufen Sie valide() dort auf, wo es zählt. Einmal pro Anfrage genügt — die Bibliothek speichert zwischen und geht nicht bei jeder Seite ins Netz.

3.Ihre Bibliothek ist die von niemandem sonst

Die Datei, die Sie herunterladen, wird für ein Produkt erzeugt. Klasse, Dateiname und Cache-Datei tragen alle einen aus diesem Produkt abgeleiteten Namen, und die aufgerufene Adresse wird zur Laufzeit zusammengesetzt statt als eine Zeichenkette geschrieben. Zwei Ihrer eigenen Produkte teilen keinen einzigen Bezeichner.

Seien wir klar, was das bringt. Wer Ihre Dateien öffnet und liest, findet die Prüfung in zwei Minuten — das hat sich nicht geändert und wird sich nie ändern. Was es stoppt, ist das andere: ein Skript, das in Tausenden Produkten nach einem bekannten Klassennamen sucht und ihn automatisch entfernt. So entstehen genullte Kopien in Serie, und das funktioniert hier nicht mehr. Die Kosten, Ihr Produkt zu raubkopieren, steigen von null auf eine manuelle Durchsicht, Produkt für Produkt.

Die Beispiele auf dieser Seite nennen die Klasse Licence. In Ihrem Download trägt sie ihren echten Namen, angezeigt neben dem Download-Button in Ihrem Autorenbereich.

4.Die Integration, Umgebung für Umgebung

Öffnen Sie den Block, der zu Ihrem Produkt passt. Jeder ist vollständig und zum Einfügen bereit; benennen Sie monmodule um und passen Sie die Ablage des Schlüssels an Ihre eigenen Einstellungen an. Die PHP-Umgebungen nutzen die herunterladbare Bibliothek; die anderen zeigen den kompletten Client, samt Cache und Signaturprüfung.

PHP PHP ohne Framework SDK PHP 5.6 → 8.4

Die kürzeste Form. Setzen Sie die Prüfung an den Eingang dessen, was Sie schützen — einen Front-Controller, eine Verwaltungsseite, ein geplantes Skript.

monproduit.php
require_once __DIR__ . '/Licence.php';

$licence = new Licence(
    $config['licence'],
    __DIR__ . '/cache'
);

if (!$licence->valide()) {
    http_response_code(403);
    exit($licence->message());
}
PS PrestaShop 1.7 und 8 SDK 1.7 · 8 · 9

Halten Sie eine einzige Instanz für das ganze Modul. Prüfen Sie in getContent(), damit der Händler sieht, warum der Konfigurationsbildschirm ablehnt, und ebenso in Ihren Anzeige-Hooks, damit ein halb eingerichtetes Modul im Shop nichts ausgibt.

monmodule.php
require_once dirname(__FILE__) . '/Licence.php';

class MonModule extends Module
{
    private $licence = null;

    private function licence()
    {
        if ($this->licence === null) {
            $this->licence = new Licence(
                Configuration::get('MONMODULE_LICENCE'),
                _PS_CACHE_DIR_ . 'monmodule'
            );
        }
        return $this->licence;
    }

    public function getContent()
    {
        if (Tools::isSubmit('submitLicence')) {
            Configuration::updateValue('MONMODULE_LICENCE', Tools::getValue('licence'));
        }

        $l = $this->licence();

        if (!$l->valide()) {
            return $this->displayError($l->message()) . $this->formulaireLicence();
        }
        if ($l->essai()) {
            return $this->displayWarning($this->l('Development licence')) . $this->contenu();
        }

        return $this->contenu();
    }

    public function hookDisplayHeader()
    {
        if (!$this->licence()->valide()) {
            return '';
        }
        return $this->contenuFront();
    }
}

Geprüft gegen 1.7, 8 und 9. Die verwendeten Aufrufe — Module, Configuration, Tools, _PS_CACHE_DIR_ — haben die Entfernungen in 9.0 unberührt überstanden. Die für 10.0 angekündigten Deprecations betreffen die Admin-Controller, die dieses Beispiel nicht benutzt.

WP WordPress SDK 5.0 → 7.1

Eine statische Instanz in einer Funktion, ein Hinweis in der Verwaltung und der Rumpf des Plugins hinter der Prüfung. Führen Sie die Prüfung nicht im Frontend jeder Seite aus — der Cache macht sie billig, aber ein Hinweis gehört in die Verwaltung.

monplug.php
require_once __DIR__ . '/Licence.php';

function monplug_licence() {
    static $l = null;
    if ($l === null) {
        $l = new Licence(
            get_option('monplug_licence'),
            WP_CONTENT_DIR . '/cache/monplug'
        );
    }
    return $l;
}

add_action('admin_notices', function () {
    $l = monplug_licence();
    if (!$l->valide()) {
        printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($l->message()));
    } elseif ($l->essai()) {
        echo '<div class="notice notice-warning"><p>'
           . esc_html__('Development licence', 'monplug') . '</p></div>';
    }
});

add_action('init', function () {
    if (monplug_licence()->valide()) {
        monplug_demarrer();
    }
});

Die fünf verwendeten Aufrufe — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — haben sich seit WordPress 5.0 nicht geändert und funktionieren weiterhin auf dem 7.1-Zweig. Nichts hier hängt vom Block-Editor oder von einer REST-Route ab.

Bei WooCommerce sollten Sie das absichern, was Sie dem Shop hinzufügen, nicht das Plugin als Ganzes: ein Zahlungsweg, der verschwindet, ist für den Händler klarer als ein Plugin, das still stirbt.

woocommerce.php
add_filter('woocommerce_payment_gateways', function ($passerelles) {
    if (monplug_licence()->valide()) {
        $passerelles[] = 'WC_Gateway_MonPlug';
    }
    return $passerelles;
});

add_action('woocommerce_admin_field_monplug_licence', function () {
    $l = monplug_licence();
    if (!$l->valide()) {
        echo '<div class="error inline"><p>' . esc_html($l->message()) . '</p></div>';
    }
});
M2 Magento 2 SDK 2.4

Verpacken Sie die Bibliothek in ein Model und injizieren Sie es, wo Sie es brauchen. Das Cache-Verzeichnis muss das von Magento sein, sonst löscht ein Deployment Ihr Urteil bei jeder Veröffentlichung.

Model/Licence.php
namespace Editeur\MonModule\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Filesystem\DirectoryList;

class Licence
{
    private $licence;

    public function __construct(ScopeConfigInterface $config, DirectoryList $dossiers)
    {
        $this->licence = new \Licence(
            (string) $config->getValue('monmodule/general/licence'),
            $dossiers->getPath(DirectoryList::CACHE) . '/monmodule'
        );
    }

    public function valide(): bool
    {
        return $this->licence->valide();
    }

    public function message(): string
    {
        return $this->licence->message();
    }
}

Die Bibliothek hat keinen Namensraum: Deklarieren Sie sie in der composer.json Ihres Moduls, statt require_once von Hand aufzurufen.

composer.json
{
  "autoload": {
    "files": [
      "Licence.php"
    ]
  }
}
LV Laravel SDK 9 → 12

Eine Middleware ist der richtige Ort: Registrieren Sie sie auf der Routengruppe, die zu Ihrem Produkt gehört, nie global, damit ein Lizenzproblem nie den Rest der Anwendung mitreißt.

app/Licence/VerifieLicence.php
namespace App\Licence;

use Closure;
use Illuminate\Http\Request;

class VerifieLicence
{
    private $licence;

    public function __construct()
    {
        $this->licence = new \Licence(
            config('monmodule.licence'),
            storage_path('app/monmodule')
        );
    }

    public function handle(Request $requete, Closure $suivant)
    {
        if (!$this->licence->valide()) {
            abort(403, $this->licence->message());
        }

        return $suivant($requete);
    }
}
SF Symfony SDK 5.3 → 7

Ein Event-Subscriber auf kernel.request, der Sub-Requests überspringt. Übergeben Sie Schlüssel und Cache-Verzeichnis als Argumente in Ihrer Service-Definition, statt die Konfiguration in der Klasse zu lesen.

src/Licence/LicenceSubscriber.php
namespace App\Licence;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;

class LicenceSubscriber implements EventSubscriberInterface
{
    private $licence;

    public function __construct(string $cle, string $dossierCache)
    {
        $this->licence = new \Licence($cle, $dossierCache);
    }

    public function onKernelRequest(RequestEvent $evenement): void
    {
        if (!$evenement->isMainRequest()) {
            return;
        }

        if (!$this->licence->valide()) {
            throw new AccessDeniedHttpException($this->licence->message());
        }
    }

    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::REQUEST => ['onKernelRequest', 16]];
    }
}

isMainRequest() kam mit Symfony 5.3. Rufen Sie auf einer älteren Version stattdessen isMasterRequest() auf — alles andere ist identisch.

JS Node.js API Node 18+

Keine Abhängigkeit über die Standardbibliothek hinaus. Das ist der vollständige Client: signierte Antwort, an seinen Host gebundener Cache, 24 Stunden Wiederverwendung und 30 Tage Schonfrist — dieselben Regeln wie in der PHP-Bibliothek.

licence.mjs
import { createPublicKey, verify } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';

const API = 'https://addonsmarket.com/api/licence';
const PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=';
const SURSIS = 2592000;

const publique = createPublicKey({
  key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
                      Buffer.from(PUBLIQUE, 'base64')]),
  format: 'der',
  type: 'spki',
});

const canonique = (d) =>
  JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));

const signee = (rep, hote) =>
  rep && rep.donnees && rep.signature
  && rep.donnees.hote === hote
  && verify(null, Buffer.from(canonique(rep.donnees), 'utf8'), publique,
            Buffer.from(rep.signature, 'base64'));

export async function etat(cle, hote, fichierCache) {
  const maintenant = Math.floor(Date.now() / 1000);
  let cache = null;

  try {
    const lu = JSON.parse(await readFile(fichierCache, 'utf8'));
    if (signee(lu, hote)) cache = lu;
  } catch {}

  if (cache && cache.donnees.revalider > maintenant) return cache.donnees;

  try {
    const r = await fetch(API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'User-Agent': 'MonProduit/1.0' },
      body: JSON.stringify({ cle, url: hote }),
      signal: AbortSignal.timeout(6000),
    });
    if (!r.ok) throw new Error(String(r.status));

    const rep = await r.json();
    if (!signee(rep, hote)) throw new Error('signature');

    await mkdir(dirname(fichierCache), { recursive: true });
    await writeFile(fichierCache, JSON.stringify(rep));
    return rep.donnees;
  } catch {
    if (cache && cache.donnees.emis + SURSIS > maintenant) return cache.donnees;
    return { ok: false, motif: 'reseau' };
  }
}

Node 18 oder neuer: fetch wurde in 18 global, AbortSignal.timeout in 17.3. Ersetzen Sie die beiden auf einer älteren Laufzeit durch https.request und einen eigenen Timer.

Rufen Sie ihn dann einmal pro Anfrage auf, und blockieren Sie nie bei einem Netzwerkfehler.

serveur.mjs
import { etat } from './licence.mjs';

const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');

if (!d.ok && d.motif !== 'reseau') {
  return res.status(403).json({ erreur: d.motif });
}
UI React, Vue, Angular API

Es gibt keinen ehrlichen Weg, eine Lizenz im Browser zu prüfen. Was Sie schreiben, läuft auf dem Rechner des Käufers, in Code, den er lesen kann, und es zu entfernen kostet eine Zeile in den Entwicklerwerkzeugen. Wir liefern keine Frontend-Bibliothek, weil wir Ihnen damit ein Placebo verkaufen würden.

Hat Ihr Produkt ein Backend, prüfen Sie dort und geben Sie das Urteil als Datum aus. Die Oberfläche kann dann etwas Nützliches sagen, ohne dass die Prüfung selbst im Browser liegt.

serveur.mjs
app.get('/api/etat', async (req, res) => {
  const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
  res.json({ actif: d.ok, essai: d.essai });
});
Licence.jsx
const [actif, setActif] = useState(null);

useEffect(() => {
  fetch('/api/etat')
    .then((r) => r.json())
    .then((d) => setActif(d.actif));
}, []);

if (actif === false) {
  return <Bandeau>Licence inactive</Bandeau>;
}

Ist Ihr Produkt eine reine Frontend-Vorlage ganz ohne Backend, gibt es zur Laufzeit nichts zu prüfen, und Sie sollten nicht so tun als ob. Die Lizenz greift dort, wo sie kann: beim Herunterladen und über die Bedingungen, die Ihr Käufer angenommen hat.

SH Shopify API

Eine Shopify-App hat ein eigenes Backend: Nutzen Sie dort den Node- oder PHP-Client und senden Sie die Shop-Domain — die myshopify.com-Adresse oder die eigene Domain, wenn Sie darauf abstellen. Ein Shopify-Theme hat kein Backend, dafür gilt der Frontend-Abschnitt oben.

PY Python API Python 3.7+

Standardbibliothek plus cryptography für die Signatur. Dieselben Regeln wie überall: 24 Stunden wiederverwenden, das letzte Urteil 30 Tage behalten, wenn wir schweigen, und nur bei einer signierten Ablehnung blockieren.

licence.py
import base64, json, time, urllib.request
from pathlib import Path
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API = 'https://addonsmarket.com/api/licence'
PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='
SURSIS = 2592000

_publique = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIQUE))


def _canonique(d):
    return json.dumps(d, sort_keys=True, separators=(',', ':'), ensure_ascii=False)


def _signee(rep, hote):
    try:
        if rep['donnees']['hote'] != hote:
            return False
        _publique.verify(base64.b64decode(rep['signature']),
                         _canonique(rep['donnees']).encode('utf-8'))
        return True
    except (KeyError, TypeError, ValueError, InvalidSignature):
        return False


def etat(cle, hote, fichier_cache):
    maintenant = int(time.time())
    chemin = Path(fichier_cache)
    cache = None

    try:
        lu = json.loads(chemin.read_text(encoding='utf-8'))
        if _signee(lu, hote):
            cache = lu
    except (OSError, ValueError):
        pass

    if cache and cache['donnees']['revalider'] > maintenant:
        return cache['donnees']

    try:
        corps = json.dumps({'cle': cle, 'url': hote}).encode('utf-8')
        requete = urllib.request.Request(API, data=corps, headers={
            'Content-Type': 'application/json',
            'User-Agent': 'MonProduit/1.0',
        })
        with urllib.request.urlopen(requete, timeout=6) as r:
            rep = json.loads(r.read().decode('utf-8'))

        if not _signee(rep, hote):
            raise ValueError('signature')

        chemin.parent.mkdir(parents=True, exist_ok=True)
        chemin.write_text(json.dumps(rep), encoding='utf-8')
        return rep['donnees']

    except Exception:
        if cache and cache['donnees']['emis'] + SURSIS > maintenant:
            return cache['donnees']
        return {'ok': False, 'motif': 'reseau'}
{ } Rohes HTTP API

Für alles andere — Go, Ruby, .NET, Java, ein Shell-Skript. Ein POST, ein JSON-Rumpf. Die Felder stehen im API-Abschnitt weiter unten, und was Sie prüfen müssen, bevor Sie der Antwort glauben, im Signatur-Abschnitt.

curl
curl -X POST https://addonsmarket.com/api/licence \
     -H 'Content-Type: application/json' \
     -H 'User-Agent: MonProduit/1.0' \
     -d '{"cle":"VOTRE_CLE","url":"boutique.exemple.fr"}'

5.Es sagen, statt es kaputtzumachen

Bei einem Theme oder Skript, das an jemanden verkauft wird, der kein Entwickler ist, ist es der falsche Zug, bei einer unlizenzierten Kopie die Seite abzuschalten: Es trifft meist den Käufer, der seinen Schlüssel falsch abgetippt hat, und Ihr Support-Postfach zahlt dafür. banniere() liefert stattdessen einen kleinen festen Hinweis — sonst ändert sich auf der Seite nichts.

index.php
require_once __DIR__ . '/Licence.php';

$reglages = @include __DIR__ . '/licence.php';

$licence = new Licence(
    is_array($reglages) ? $reglages['cle'] : '',
    __DIR__ . '/cache'
);

register_shutdown_function(function () use ($licence) {
    echo $licence->banniere('My theme');
});

Der Käufer wird ihn los, indem er seinen Schlüssel in eine Datei neben der Bibliothek einträgt. Diese Datei ist das Einzige, was er anfassen muss.

licence.php
<?php

return array(
    'cle' => '',
);

Sie gibt eine leere Zeichenkette zurück, wenn die Lizenz gültig ist, und ebenso, wenn das Netz ausgefallen ist — ein Besucher sieht nie einen Hinweis, weil unser Server eine schlechte Nacht hatte. Bei einem Entwicklungsschlüssel sagt sie es dezent, und genau das wollen Sie beim Bauen.

6.Produkte ohne Server: HTML-Themes, Vorlagen, Grafiken

Ein HTML-Theme, eine Figma-Datei, ein Icon-Set, eine Schrift: nichts davon läuft auf einem Server, den Sie abfragen können. Alles, was Sie ausliefern, wird von dem gelesen, geändert und weitergegeben, der die Dateien hat. Es gibt keine Prüfung zum Einbauen, und keine Fassung dieser Seite, in der sich das ändert.

Tun Sie nicht so als ob. Ein verschleiertes Skript, das aus einer statischen Seite heraus nach Hause telefoniert, ein verstecktes Canvas-Fingerprinting, ein Build, der nach dreißig Tagen kaputtgeht: all das entfernt die eine Person, die Sie ohnehin raubkopiert hätte, an einem Nachmittag, und all das landet als Support-Ticket bei den fünfzig, die bezahlt haben. Sie würden Ihre Glaubwürdigkeit ausgeben, um nichts zu schützen.

Was es stattdessen gibt, wirkt bei der Auslieferung, nicht zur Laufzeit. Jeder Download wird für den Käufer neu verpackt, der ihn angefordert hat, und trägt drei Dinge:

  • Eine Lizenzdatei im Wurzelverzeichnis des Archivs, die Bestellung, Inhaber und Umfang der Lizenz nennt.
  • Einen Fingerabdruck, in dieser Datei und im Archivkommentar — zwanzig Zeichen, abgeleitet aus Lizenz und Produkt, die genau diesen einen Download kennzeichnen und keinen anderen.
  • Einen Satz in zwei Sprachen, der sagt, was die Weitergabe kostet: die Nutzungsrechte und die Updates, ohne Erstattung.

Das ist Rückverfolgbarkeit, keine Verhinderung, und wir verkaufen es Ihnen nicht als etwas anderes: Ein Käufer, der die Datei löscht und den Kommentar leert, geht mit einer sauberen Kopie. Was es Ihnen gibt, ist ein Name, wenn eine Kopie dort auftaucht, wo sie nicht sein sollte, ein Grund für diesen Käufer, es sich zweimal zu überlegen, und die zwei Dinge, die eine Raubkopie nie bekommt — die Updates und Ihren Support.

Eine Unterscheidung zählt hier. Enthält Ihr Theme überhaupt PHP — ein Kontaktformular, eine Konfigurationsdatei, ein Include —, dann läuft es auf einem Server, die obige Prüfung ist echt und der Hinweis funktioniert. Nur im vollständig statischen Fall, HTML und CSS und sonst nichts, gibt es zur Laufzeit keine Prüfung.

7.Die Bibliothek

Eine einzige PHP-Datei, keine Abhängigkeit, kompatibel bis hinunter zu PHP 5.6, damit sie auf den alten Installationen läuft, die Ihre Käufer noch haben. Vier Methoden reichen.

new Licence($cle, $dossierCache) Erzeugen Sie sie mit dem Schlüssel des Käufers und einem beschreibbaren Verzeichnis für den Cache.
$licence->valide() Wahr, wenn das Produkt auf dieser Domain laufen darf.
$licence->essai() Wahr bei einem Entwicklungsschlüssel oder einer Testdomain. Zeigen Sie einen dezenten Hinweis — liefern Sie nie eine Version aus, die ihn verbirgt.
$licence->message() Ein Satz für den Käufer, bereits übersetzt, der die Domains nennt und das Ereignis datiert.

8.Die wichtigste Regel

Lassen Sie nie ein Netzwerkproblem den Shop Ihres Käufers abschalten. Die Bibliothek regelt das bereits, und Sie sollten es nicht umgehen: ein frisches Urteil wird 24 Stunden wiederverwendet; antwortet unser Server nicht, bleibt das letzte bekannte Urteil 30 Tage gültig und das Produkt läuft weiter; nur eine signierte Ablehnung stoppt es, und zwar sofort. Hat unser Server eine schlechte Nacht, geht kein Shop aus. Wird ein Schlüssel übertragen, hört die alte Seite binnen 24 Stunden auf.

9.Domains

Eine Lizenz bindet an die registrierbare Domain. Ein Schlüssel auf beispiel.de deckt www.beispiel.de, shop.beispiel.de und jede andere Subdomain ab — Ihr Käufer schreibt Ihnen nicht wegen einer fehlenden Weiterleitung. Test- und Staging-Adressen verbrauchen nie eine Lizenz: localhost, .local, .test sowie die üblichen Präfixe dev., staging., preprod. funktionieren, ohne etwas zu verbrauchen.

10.Die API, wenn Sie lieber selbst aufrufen

Die Bibliothek ist nur eine dünne Hülle um einen einzigen Endpunkt. Rufen Sie ihn direkt auf, wenn Sie außerhalb von PHP arbeiten — dann müssen Sie die obigen Cache-Regeln allerdings selbst umsetzen und die Signatur prüfen.

Endpunkt

POST https://addonsmarket.com/api/licence

Anfrage

HTTP
POST /api/licence HTTP/1.1
Host: addonsmarket.com
Content-Type: application/json
User-Agent: MonProduit/1.0

{
  "cle": "VOTRE_CLE",
  "url": "boutique.exemple.fr"
}

Was Sie senden

cle string Der Schlüssel des Käufers. Nur Buchstaben und Ziffern; alles andere entfernen wir, eingefügte Leerzeichen und Bindestriche schaden also nicht.
url string Der Host, auf dem Ihr Produkt läuft. Eine vollständige URL geht auch — wir behalten nur den Host. Entfernen Sie Port und führendes www. selbst, wenn die Antwort dem entsprechen soll, was Sie gesendet haben.

Antwort

JSON
{
  "donnees": {
    "ok": true,
    "motif": "",
    "domaine": "exemple.fr",
    "essai": false,
    "quand": "",
    "emis": 1790687453,
    "revalider": 1790773853,
    "sursis": 2592000,
    "hote": "boutique.exemple.fr"
  },
  "signature": "4p9JXJRfcB5R/LNEQwr/cwnVv1M5R1OdCNcU…"
}

Felder der Antwort

ok bool Ob das Produkt hier laufen darf.
motif string Warum nicht, wenn ok falsch ist. Siehe die Tabelle unten.
domaine string Die Domain, an die die Lizenz gebunden ist.
essai bool Entwicklungsschlüssel oder Testdomain.
quand string Wann der Schlüssel ersetzt oder widerrufen wurde.
emis int Wann wir geantwortet haben. Der Beginn der 30-tägigen Schonfrist.
revalider int Zeitstempel, vor dem Sie nicht erneut fragen dürfen.
sursis int Sekunden, die Sie dieses Urteil behalten dürfen, falls wir nicht mehr antworten.
hote string Der Host, zu dem wir geantwortet haben. Weisen Sie die Antwort zurück, wenn es nicht Ihrer ist.

HTTP-Codes

200 Ein signiertes Urteil. Beachten Sie: auch eine Ablehnung ist ein 200 — lesen Sie ok, nicht den Statuscode.
400 Schlüssel oder Host fehlt.
405 Alles außer POST oder OPTIONS.
429 Aufruflimit erreicht. Behalten Sie Ihr Urteil im Cache und versuchen Sie es später erneut — werten Sie das nie als Ablehnung.

Dreißig Aufrufe pro Minute und IP-Adresse. Werden die Cache-Regeln eingehalten, ruft ein Shop einmal am Tag auf; dieses Limit erreichen Sie also nur aus einem Lasttest oder einer Schleife, die ihren Cache vergessen hat.

Senden Sie einen User-Agent, der Ihr Produkt nennt. Einige Standard-Agenten von HTTP-Bibliotheken — der von urllib in Python etwa — werden gefiltert, bevor sie uns erreichen, und bekommen ein 403. Jede eigene Zeichenkette vermeidet das.

11.Warum die Antwort signiert ist

Ohne Signatur genügt eine Zeile in der hosts-Datei eines Servers, um an unserer Stelle „Lizenz gültig“ zu antworten. Jede Antwort trägt eine Ed25519-Signatur über die kanonische Form der Daten — Schlüssel sortiert, kompaktes JSON. Prüfen Sie sie, bevor Sie irgendetwas glauben. Die Bibliothek tut das für Sie, auch bei der Kopie im Cache, die ihrerseits an ihren Host gebunden ist und sich deshalb nicht von einer Seite auf eine andere kopieren lässt.

12.Die Signatur selbst prüfen

Nur nötig, wenn Sie die API direkt aufrufen. Bauen Sie die kanonische Form aus dem geparsten Objekt neu auf — nie aus den empfangenen Rohbytes, die abweichen können — und prüfen Sie dann die abgetrennte Ed25519-Signatur gegen unseren öffentlichen Schlüssel.

Unser öffentlicher Schlüssel

Ed25519, 32 rohe Bytes in base64. Er ist für jeden Autor und jedes Produkt derselbe.

/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=

Die kanonische Form

Die Signatur deckt das Objekt donnees ab, serialisiert nach diesen vier Regeln, und nichts sonst. Eine davon falsch, und jede Signatur wirkt ungültig.

  1. Schlüssel aufsteigend sortiert.
  2. Keine Leerzeichen: weder nach den Doppelpunkten noch nach den Kommas.
  3. Schrägstriche werden nicht maskiert.
  4. Nicht-ASCII-Zeichen bleiben UTF-8, ohne Backslash-u-Maskierung.

Für die obige Antwort die genau signierten Bytes:

utf-8
{"domaine":"exemple.fr","emis":1790687453,"essai":false,"hote":"boutique.exemple.fr","motif":"","ok":true,"quand":"","revalider":1790773853,"sursis":2592000}
PHP PHP
sodium
function signature_valide(array $donnees, $signature, $publique)
{
    ksort($donnees);
    $corps = json_encode($donnees, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

    return sodium_crypto_sign_verify_detached(
        base64_decode($signature, true),
        $corps,
        base64_decode($publique, true)
    );
}
JS Node.js
node:crypto
import { createPublicKey, verify } from 'node:crypto';

const publique = createPublicKey({
  key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
                      Buffer.from('/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=', 'base64')]),
  format: 'der',
  type: 'spki',
});

const canonique = (d) =>
  JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));

const valide = verify(null, Buffer.from(canonique(rep.donnees), 'utf8'),
                      publique, Buffer.from(rep.signature, 'base64'));
PY Python
cryptography
import base64, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

publique = Ed25519PublicKey.from_public_bytes(base64.b64decode('/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='))

canonique = json.dumps(rep['donnees'], sort_keys=True,
                       separators=(',', ':'), ensure_ascii=False)

publique.verify(base64.b64decode(rep['signature']), canonique.encode('utf-8'))

13.Gründe für eine Ablehnung

autre_domaine Der Schlüssel ist gültig, gehört aber zu einer anderen Domain. Sagen Sie dem Käufer, er soll ihn in seinem Konto übertragen.
transfert Der Schlüssel wurde auf eine andere Seite verschoben und ersetzt. Der Käufer hat einen neuen.
regeneration Der Käufer hat einen neuen Schlüssel angefordert. Der alte ist endgültig tot.
revocation Wir haben den Schlüssel widerrufen: Missbrauch, Betrug oder ein entschiedener Streitfall.
expiree Ein befristeter Testschlüssel, der abgelaufen ist.
inconnue Diesen Schlüssel gibt es nicht. Meist ein Tippfehler.
reseau Wir waren nicht erreichbar und es ist kein Urteil mehr im Cache. Blockieren Sie deswegen nicht.

14.Testen, bevor Sie verkaufen

Ihr Autorenbereich gibt Ihnen für jedes Ihrer Produkte einen Entwicklungsschlüssel. Er funktioniert auf jeder Domain, öffnet nur Ihre eigenen Produkte und meldet immer essai — eine damit laufende Installation zeigt also einen Entwicklungshinweis und kann nie für eine verkaufte Kopie gehalten werden.

Um zu sehen, wie sich Ihr Produkt bei einer Ablehnung verhält, senden Sie einen Schlüssel, den es nicht gibt. Sie bekommen eine echte signierte Ablehnung, genau wie bei einem widerrufenen Schlüssel — so prüfen Sie Ihren Fehlerbildschirm, ohne irgendetwas zu widerrufen.

curl
curl -X POST https://addonsmarket.com/api/licence \
     -H 'Content-Type: application/json' \
     -H 'User-Agent: MonProduit/1.0' \
     -d '{"cle":"cettecleNexistePas000000","url":"boutique.exemple.fr"}'

{"donnees":{"ok":false,"motif":"inconnue", …},"signature":"…"}

15.Was Sie Ihrem Käufer sagen müssen

Ihr Produkt sendet die Domain, auf der es läuft, an unseren Server. Das ist eine Verarbeitung personenbezogener Daten nach europäischem Recht, und Ihr Käufer muss davon wissen. Sagen Sie es in Ihrer Produktbeschreibung und in Ihrer eigenen Dokumentation. Wir bewahren die Domain auf, und die IP zwölf Monate lang, um Streitfälle zu klären; sonst nichts, und wir verkaufen nichts davon.

Etwas unklar?

Schreiben Sie uns aus Ihrem Autorenbereich. Eine Frage, die gestellt werden musste, heißt meistens, dass dieser Seite ein Absatz fehlt.

Autorenbereich