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.
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.
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.
Ö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.
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.
require_once __DIR__ . '/Licence.php';
$licence = new Licence(
$config['licence'],
__DIR__ . '/cache'
);
if (!$licence->valide()) {
http_response_code(403);
exit($licence->message());
}
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.
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.
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.
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.
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>';
}
});
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.
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.
{
"autoload": {
"files": [
"Licence.php"
]
}
}
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.
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);
}
}
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.
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.
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.
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.
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 });
}
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.
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 });
});
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.
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.
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.
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'}
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 -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"}'
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.
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.
<?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.
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:
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.
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. |
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.
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.
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.
POST https://addonsmarket.com/api/licence
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"
}
| 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. |
{
"donnees": {
"ok": true,
"motif": "",
"domaine": "exemple.fr",
"essai": false,
"quand": "",
"emis": 1790687453,
"revalider": 1790773853,
"sursis": 2592000,
"hote": "boutique.exemple.fr"
},
"signature": "4p9JXJRfcB5R/LNEQwr/cwnVv1M5R1OdCNcU…"
}
| 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. |
| 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.
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.
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.
Ed25519, 32 rohe Bytes in base64. Er ist für jeden Autor und jedes Produkt derselbe.
/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=
Die Signatur deckt das Objekt donnees ab, serialisiert nach diesen vier Regeln, und nichts sonst. Eine davon falsch, und jede Signatur wirkt ungültig.
Für die obige Antwort die genau signierten Bytes:
{"domaine":"exemple.fr","emis":1790687453,"essai":false,"hote":"boutique.exemple.fr","motif":"","ok":true,"quand":"","revalider":1790773853,"sursis":2592000}
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)
);
}
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'));
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'))
| 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. |
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 -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":"…"}
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.
Schreiben Sie uns aus Ihrem Autorenbereich. Eine Frage, die gestellt werden musste, heißt meistens, dass dieser Seite ein Absatz fehlt.