Apertura próxima — los autores se quedan el 100 % de su precio Hazte autor Iniciar sesión Crear cuenta
Hola, inicia sesión Cuenta y listas TuÁrea de autor

Licencias, documentación para desarrolladores

Cómo comprobar una clave de licencia dentro de tu propio producto: la biblioteca, la API, integraciones listas para once entornos y las reglas que mantienen en marcha las tiendas de tus compradores.

1.Qué hace esto, y qué no hace

Una clave de licencia vincula tu producto a un dominio, o a tantos como el comprador haya pagado. Permite que un cliente honesto se mantenga dentro de lo que compró, te da el recuento de tus instalaciones y hace que una clave filtrada se pueda matar: su propietario la regenera y la filtrada muere. No es una protección contra la copia. Tu comprador tiene tu código fuente y puede quitar la comprobación en cinco minutos. Quien te diga lo contrario te está vendiendo algo.

2.Tres pasos

  1. Abre tu producto en tu área de autor, ve a Licencia y descarga la biblioteca. Ya lleva nuestra clave pública y el identificador de tu producto: no hay nada que configurar.
  2. Pide su clave al comprador en la primera instalación y guárdala donde tu producto guarde sus ajustes. Nunca la escribas en el código.
  3. Llama a valide() donde importe. Una vez por petición es suficiente: la biblioteca usa caché y no saldrá a la red en cada página.

3.Tu biblioteca no es la de nadie más

El archivo que descargas se genera para un producto. La clase, el nombre del archivo y el archivo de caché llevan todos un nombre derivado de ese producto, y la dirección a la que llama se ensambla en tiempo de ejecución en vez de escribirse de una pieza. Dos de tus propios productos no comparten ni un solo identificador.

Seamos claros sobre lo que eso compra. Quien abra tus archivos y los lea encuentra la comprobación en dos minutos: eso no ha cambiado y no cambiará nunca. Lo que sí detiene es lo otro: un script que busca un nombre de clase conocido en miles de productos y lo quita solo. Así se fabrican las copias nulled en serie, y aquí ya no funciona. El coste de piratear tu producto pasa de nada a una revisión manual, producto por producto.

Los ejemplos de esta página llaman a la clase Licence. En tu descarga lleva su nombre real, mostrado junto al botón de descarga en tu área de autor.

4.La integración, entorno por entorno

Abre el que corresponda a tu producto. Cada bloque está completo y listo para pegar; renombra monmodule y adapta el almacenamiento de la clave a tus propios ajustes. Los entornos PHP usan la biblioteca descargable; los demás muestran el cliente completo, con caché y verificación de firma incluidas.

PHP PHP sin framework SDK PHP 5.6 → 8.4

La forma más corta. Pon la comprobación en la entrada de aquello que proteges: un controlador frontal, una página de administración, un script programado.

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 y 8 SDK 1.7 · 8 · 9

Mantén una sola instancia para todo el módulo. Comprueba en getContent() para que el comerciante vea por qué la pantalla de configuración se niega, y también en tus hooks de visualización para que un módulo a medio configurar no se muestre en la tienda.

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();
    }
}

Comprobado en 1.7, 8 y 9. Las llamadas usadas — Module, Configuration, Tools, _PS_CACHE_DIR_ — han atravesado las eliminaciones de la 9.0 sin tocarse. Las deprecaciones anunciadas para la 10.0 afectan a los controladores de administración, que este ejemplo no usa.

WP WordPress SDK 5.0 → 7.1

Una instancia estática en una función, un aviso en la administración y el cuerpo del plugin detrás de la comprobación. No hagas la comprobación en el front de cada página: la caché la hace barata, pero un aviso pertenece a la administración.

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();
    }
});

Las cinco llamadas usadas — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — no han cambiado desde WordPress 5.0 y siguen funcionando en la rama 7.1. Nada de esto depende del editor de bloques ni de una ruta REST.

En WooCommerce, condiciona lo que añades a la tienda en lugar del plugin entero: una pasarela de pago que desaparece es más clara para el comerciante que un plugin que muere en silencio.

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

Envuelve la biblioteca en un modelo e inyéctalo donde lo necesites. El directorio de caché debe ser el de Magento, o un despliegue borrará tu veredicto en cada publicación.

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();
    }
}

La biblioteca no tiene espacio de nombres: decláralas en el composer.json de tu módulo en lugar de llamar a require_once a mano.

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

Un middleware es el sitio adecuado: regístralo en el grupo de rutas que pertenece a tu producto, nunca de forma global, para que un problema de licencia nunca tumbe el resto de la aplicación.

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

Un suscriptor de eventos en kernel.request, que ignora las subpeticiones. Pasa la clave y el directorio de caché como argumentos en tu definición de servicio en lugar de leer la configuración dentro de la clase.

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() llegó en Symfony 5.3. En una versión anterior, llama a isMasterRequest() en su lugar: todo lo demás es idéntico.

JS Node.js API Node 18+

Ninguna dependencia más allá de la biblioteca estándar. Este es el cliente completo: respuesta firmada, caché vinculada a su host, reutilización de 24 horas y prórroga de 30 días, las mismas reglas que la biblioteca PHP.

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 o posterior: fetch pasó a ser global en la 18, y AbortSignal.timeout en la 17.3. En un runtime anterior, sustituye esos dos por https.request y tu propio temporizador.

Después llámalo una vez por petición, y nunca bloquees por un fallo de red.

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

No hay forma honesta de comprobar una licencia en un navegador. Lo que escribas se ejecuta en la máquina del comprador, en código que puede leer, y quitarlo cuesta una línea en las herramientas de desarrollo. No ofrecemos una biblioteca de front porque sería venderte un placebo.

Si tu producto tiene servidor, comprueba allí y expón el veredicto como un dato. La interfaz podrá entonces decir algo útil sin que la comprobación esté en el navegador.

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>;
}

Si tu producto es una plantilla de front sin ningún servidor, no hay nada que comprobar en tiempo de ejecución y no conviene fingir lo contrario. La licencia se aplica donde puede: en la descarga y mediante los términos que tu comprador aceptó.

SH Shopify API

Una app de Shopify tiene su propio servidor: usa allí el cliente Node o PHP y envía el dominio de la tienda, la dirección myshopify.com o el dominio propio si es en él en el que te apoyas. Un tema de Shopify no tiene servidor, así que se aplica la sección de front de arriba.

PY Python API Python 3.7+

Biblioteca estándar más cryptography para la firma. Las mismas reglas que en todas partes: reutilizar 24 horas, conservar el último veredicto 30 días si callamos, y bloquear solo ante un rechazo firmado.

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'}
{ } HTTP en crudo API

Para todo lo demás: Go, Ruby, .NET, Java, un script de shell. Un POST, un cuerpo JSON. Los campos están en la sección de la API más abajo, y lo que debes verificar antes de fiarte de la respuesta, en la sección de la firma.

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.Decirlo en vez de romperlo

Para un tema o un script vendido a alguien que no es desarrollador, tumbar el sitio ante una copia sin licencia es el gesto equivocado: quien lo sufre suele ser el comprador que copió mal su clave, y lo paga tu bandeja de soporte. banniere() devuelve en su lugar un pequeño aviso fijo: nada más cambia en la página.

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');
});

El comprador lo quita poniendo su clave en un archivo, junto a la biblioteca. Ese archivo es lo único que tiene que tocar.

licence.php
<?php

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

Devuelve una cadena vacía cuando la licencia es válida, y también cuando la red ha fallado: un visitante nunca ve un aviso porque nuestro servidor pasara una mala noche. Con una clave de desarrollo lo dice, con discreción, que es justo lo que quieres mientras construyes.

6.Los productos sin servidor: temas HTML, plantillas, gráficos

Un tema HTML, un archivo de Figma, un juego de iconos, una tipografía: nada de eso se ejecuta en un servidor al que puedas preguntar. Todo lo que entregas lo lee, lo modifica y lo redistribuye quien tenga los archivos. No hay comprobación que añadir, y no existe ninguna versión de esta página donde eso cambie.

No finjas. Un script ofuscado que llama a casa desde una página estática, una huella oculta en un canvas, una compilación que se rompe a los treinta días: todo eso lo quita en una tarde la única persona que iba a piratearte, y todo eso acaba en un ticket de soporte de los cincuenta que pagaron. Estarías gastando tu credibilidad para no proteger nada.

Lo que existe en su lugar actúa en la entrega, no en la ejecución. Cada descarga se reempaqueta para el comprador que la pidió, y lleva tres cosas:

  • Un archivo de licencia en la raíz del ZIP, que nombra el pedido, el titular y el alcance de la licencia.
  • Una huella, en ese archivo y en el comentario del ZIP: veinte caracteres derivados de la licencia y del producto, que identifican esa descarga y ninguna otra.
  • Una frase, en dos idiomas, que dice lo que cuesta redistribuirlo: los derechos de uso y las actualizaciones, sin reembolso.

Eso es trazabilidad, no prevención, y no te la vamos a vender como otra cosa: un comprador que borre el archivo y vacíe el comentario se va con una copia limpia. Lo que sí te da es un nombre cuando una copia aparece donde no debería, un motivo para que ese comprador se lo piense dos veces, y las dos cosas que una copia pirata nunca tiene: las actualizaciones y tu soporte.

Aquí importa una distinción. Si tu tema tiene algo de PHP — un formulario de contacto, un archivo de configuración, un include — entonces se ejecuta en un servidor, la comprobación de arriba es real y el aviso funciona. Solo en el caso totalmente estático, HTML y CSS y nada más, no hay comprobación posible en ejecución.

7.La biblioteca

Un solo archivo PHP, sin dependencias, compatible hasta PHP 5.6 para que funcione en las instalaciones antiguas que aún tienen tus compradores. Bastan cuatro métodos.

new Licence($cle, $dossierCache) Constrúyela con la clave del comprador y un directorio con permiso de escritura para la caché.
$licence->valide() Verdadero cuando el producto puede funcionar en este dominio.
$licence->essai() Verdadero con una clave de desarrollo o un dominio de pruebas. Muestra un aviso discreto: nunca publiques una versión que lo oculte.
$licence->message() Una frase para mostrar al comprador, ya traducida, que nombra los dominios y fecha el suceso.

8.La regla que más importa

Nunca dejes que un problema de red tumbe la tienda de tu comprador. La biblioteca ya se ocupa de ello y no debes esquivarla: un veredicto reciente se reutiliza 24 horas; cuando nuestro servidor no responde, el último veredicto conocido se conserva 30 días y el producto sigue funcionando; solo un rechazo firmado lo detiene, de inmediato. Si nuestro servidor pasa una mala noche, ninguna tienda se apaga. Si una clave se transfiere, el sitio antiguo se detiene en menos de 24 horas.

9.Los dominios

Una licencia se vincula al dominio registrable. Una clave en ejemplo.es cubre www.ejemplo.es, tienda.ejemplo.es y cualquier otro subdominio: tu comprador no te escribirá por una redirección que falta. Las direcciones de prueba nunca gastan licencia: localhost, .local, .test y los prefijos habituales dev., staging., preprod. funcionan sin consumir nada.

10.La API, si prefieres llamarla tú mismo

La biblioteca no es más que una envoltura fina sobre un único punto de entrada. Llámalo directamente si trabajas fuera de PHP, pero entonces tendrás que implementar tú mismo las reglas de caché anteriores y verificar la firma.

Punto de entrada

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

Petición

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"
}

Lo que envías

cle string La clave del comprador. Solo letras y dígitos; quitamos lo demás, así que los espacios y guiones pegados no molestan.
url string El host en el que funciona tu producto. Una URL completa también sirve: solo nos quedamos con el host. Quita tú mismo el puerto y el www. inicial si quieres que la respuesta coincida con lo que enviaste.

Respuesta

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…"
}

Campos de la respuesta

ok bool Si el producto puede funcionar aquí.
motif string Por qué no, cuando ok es falso. Mira la tabla de abajo.
domaine string El dominio al que está vinculada la licencia.
essai bool Clave de desarrollo o dominio de pruebas.
quand string Cuándo se reemplazó o se revocó la clave.
emis int Cuándo respondimos. El comienzo de la prórroga de 30 días.
revalider int Marca de tiempo antes de la cual no debes volver a preguntar.
sursis int Segundos durante los que puedes conservar este veredicto si dejamos de responder.
hote string El host sobre el que hemos respondido. Rechaza la respuesta si no es el tuyo.

Códigos HTTP

200 Un veredicto firmado. Ojo: un rechazo también es un 200; lee ok, no el código de estado.
400 Falta la clave o el host.
405 Cualquier cosa que no sea POST u OPTIONS.
429 Límite de llamadas alcanzado. Conserva tu veredicto en caché y reinténtalo más tarde; nunca lo trates como un rechazo.

Treinta llamadas por minuto y por dirección IP. Si se respetan las reglas de caché, una tienda hace una llamada al día, así que solo alcanzarás este límite desde una prueba de carga o un bucle que olvidó su caché.

Envía un User-Agent que nombre tu producto. Algunos agentes por defecto de bibliotecas HTTP, como el de urllib en Python, se filtran antes de llegarnos y reciben un 403. Cualquier cadena personalizada lo evita.

11.Por qué la respuesta va firmada

Sin firma, una línea en el archivo hosts de un servidor basta para responder «licencia válida» en nuestro lugar. Cada respuesta lleva una firma Ed25519 sobre la forma canónica de los datos: claves ordenadas, JSON compacto. Verifícala antes de fiarte de nada. La biblioteca lo hace por ti, incluso sobre la copia en caché, que a su vez está vinculada a su host para que no pueda copiarse de un sitio a otro.

12.Verificar la firma tú mismo

Solo hace falta si llamas a la API directamente. Reconstruye la forma canónica a partir del objeto analizado, nunca de los bytes en crudo recibidos, que pueden diferir, y después verifica la firma Ed25519 separada con nuestra clave pública.

Nuestra clave pública

Ed25519, 32 bytes en crudo en base64. Es la misma para todos los autores y todos los productos.

/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=

La forma canónica

La firma cubre el objeto donnees serializado con estas cuatro reglas, y nada más. Falla una y todas las firmas parecerán inválidas.

  1. Claves ordenadas de forma ascendente.
  2. Sin espacios en blanco: ninguno tras los dos puntos ni tras las comas.
  3. Las barras no se escapan.
  4. Los caracteres no ASCII se quedan en UTF-8, sin escape barra invertida-u.

Para la respuesta de arriba, los bytes firmados exactamente:

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.Motivos de rechazo

autre_domaine La clave es válida pero pertenece a otro dominio. Dile al comprador que la transfiera desde su cuenta.
transfert La clave se ha movido a otro sitio y se ha reemplazado. El comprador tiene una nueva.
regeneration El comprador pidió una clave nueva. La antigua está muerta para siempre.
revocation Hemos revocado la clave: abuso, fraude o una disputa resuelta.
expiree Una clave de prueba con plazo que ha caducado.
inconnue Esa clave no existe. Normalmente una errata.
reseau No se nos ha podido contactar y no queda ningún veredicto en caché. No bloquees por esto.

14.Probar antes de vender

Tu área de autor te da una clave de desarrollo para cada uno de tus productos. Funciona en cualquier dominio, solo abre tus propios productos y siempre informa essai: así, una instalación que la use muestra un aviso de desarrollo y nunca puede confundirse con una copia vendida.

Para ver cómo se comporta tu producto cuando se le rechaza, envía una clave que no exista. Obtienes un rechazo firmado real, idéntico al de una clave revocada, así que puedes comprobar tu pantalla de error sin revocar nada.

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.Lo que debes decirle a tu comprador

Tu producto enviará a nuestro servidor el dominio en el que funciona. Eso es un tratamiento de datos personales según el derecho europeo, y tu comprador debe saberlo. Dilo en la descripción de tu producto y en tu propia documentación. Conservamos el dominio, y la IP durante doce meses para resolver disputas; nada más, y no vendemos nada de ello.

¿Algo no queda claro?

Escríbenos desde tu área de autor. Una pregunta que hubo que hacer suele significar que a esta página le falta un párrafo.

Área de autor