Pular para o conteúdo

Catálogo de demonstração. Produtos, criadores, preços e avaliações são fictícios. Recursos gratuitos podem ser adquiridos de verdade; itens pagos não são cobrados nem simulados. Ver o que já funciona

Protocolo de autorização v1

O formato exato do documento assinado, para quem precisa verificar sem usar o SDK oficial.

Formato

LSA1.<payload base64url>.<assinatura base64url>
  • LSA1 — prefixo fixo da versão 1.
  • payload — JSON canônico, em base64url sem padding.
  • assinatura — Ed25519 sobre os bytes UTF-8 de LSA1.<payload>, em base64url sem padding.

O prefixo entra na assinatura

Não é só um rótulo: ele faz parte dos bytes assinados. Um documento da versão 1 nunca pode ser reapresentado como se fosse de uma versão futura com outras regras.

Payload

json
{
  "v": 1,
  "kid": "lsk_a1b2c3d4e5f6a7b8",
  "licenseId": "clx1a2b3c4d5e6f7g8h9",
  "productId": "lsp_a1b2c3d4e5f6a7b8c9d0e1f2",
  "installationId": "srv-Yk3mPq8RtLvNxZbW2cHdAe",
  "sessionId": "clx9z8y7x6w5v4u3t2s1",
  "status": "AUTHORIZED",
  "issuedAt": 1762905600,
  "expiresAt": 1762909200,
  "graceUntil": 1763168400,
  "requestId": "aG0wZ2tsMnB4dnJ0czk4N2FiY2Rl",
  "nonce": "n7Kp2QxR4vTz",
  "features": ["premium", "multiworld"],
  "test": false
}
CampoTipoO que é
vinteiroVersão do protocolo. Documento de versão desconhecida é recusado.
kidstringIdentificador da chave que assinou. Escolhe a pública correta.
licenseIdstringLicença.
productIdstringPrecisa ser igual ao do seu plugin.
installationIdstringPrecisa ser igual ao da sua instalação.
sessionIdstringSessão, usada no heartbeat.
statusstringAUTHORIZED. A decisão real é a assinatura, não este campo.
issuedAtinteiroEpoch em SEGUNDOS — cabe em int no Java.
expiresAtinteiroAté quando vale sem renovar.
graceUntilinteiroAté quando vale offline depois de expiresAt.
requestIdstringIdentificador da solicitação. Confira contra o que você enviou.
noncestringEco do nonce que você enviou.
featuresarray de stringRecursos autorizados pela política.
testbooleanoLicença de Playground. Recuse em produção.

JSON canônico

A assinatura cobre um texto específico. Sem forma canônica, o servidor assina {"a":1,"b":2} e o cliente reconstrói {"b":2,"a":1} — mesma informação, bytes diferentes, assinatura inválida.

Regras:

  • Chaves ordenadas lexicograficamente.
  • Sem espaço em branco.
  • UTF-8.
  • Inteiro sem casa decimal — 1762905600, nunca 1762905600.0.
  • Apenas string, inteiro, booleano e array de string. Qualquer campo novo entra como um desses, por regra do protocolo.
Não é RFC 8785 completo, e não precisa ser: o documento só carrega esses quatro tipos, e o formato está congelado pela versão do protocolo. A simplicidade é o que permite um verificador de ~40 linhas em qualquer linguagem.

Ordem de verificação

O SDK faz exatamente estas checagens, nesta ordem. Falhar qualquer uma é motivo de recusa; nenhuma é opcional.

  1. Assinatura contra a chave pública de kid. É a única coisa que o servidor do cliente não consegue forjar.
  2. Versão do protocolo. Desconhecida é recusada, nunca “interpretada do jeito que der”.
  3. Produto esperado. Um documento legítimo de outro plugin não autoriza este.
  4. Instalação esperada. O documento de outra máquina não vale aqui, mesmo sendo autêntico.
  5. requestId e nonce. Precisam ser os que este processo acabou de enviar. É o que impede reapresentar uma resposta gravada antes.
  6. Prazo. expiresAt e graceUntil vêm de dentro da assinatura.
  7. test. Recuse em produção.

Obter as chaves públicas

http
GET /api/v1/licensing/.well-known/keys
json
{
  "protocol": 1,
  "algorithm": "Ed25519",
  "format": "spki-der-base64url",
  "keys": [
    { "kid": "lsk_a1b2c3d4e5f6a7b8", "alg": "Ed25519", "publicKey": "MCowBQYDK2VwAyEA...", "status": "ACTIVE",  "createdAt": "2026-11-11T22:00:00.000Z" },
    { "kid": "lsk_0f1e2d3c4b5a6978", "alg": "Ed25519", "publicKey": "MCowBQYDK2VwAyEA...", "status": "RETIRED", "createdAt": "2026-05-02T10:30:00.000Z" }
  ]
}

publicKey é SPKI DER em base64url — o formato que X509EncodedKeySpec consome direto em Java, e que crypto.createPublicKey aceita no Node.

Rebusque as chaves quando encontrar um kid desconhecido. É assim que uma rotação da plataforma é absorvida sem configuração: o primeiro documento com a chave nova dispara uma única rebusca.

Verificador em outras linguagens

Java 17+

java
byte[] der = Base64.getUrlDecoder().decode(publicKeyBase64Url);
PublicKey key = KeyFactory.getInstance("Ed25519")
        .generatePublic(new X509EncodedKeySpec(der));

int last = token.lastIndexOf('.');
byte[] assinado = token.substring(0, last).getBytes(StandardCharsets.UTF_8);
byte[] assinatura = Base64.getUrlDecoder().decode(token.substring(last + 1));

Signature sig = Signature.getInstance("Ed25519");
sig.initVerify(key);
sig.update(assinado);
boolean autentica = sig.verify(assinatura);

Node.js

javascript
import { createPublicKey, verify } from 'node:crypto'

const key = createPublicKey({
  key: Buffer.from(publicKeyBase64Url, 'base64url'),
  format: 'der',
  type: 'spki',
})

const last = token.lastIndexOf('.')
const assinado = Buffer.from(token.slice(0, last), 'utf8')
const assinatura = Buffer.from(token.slice(last + 1), 'base64url')

// null = PureEdDSA. Ed25519 faz o hash internamente.
const autentica = verify(null, assinado, key, assinatura)

PureEdDSA, não Ed25519ph

Passar um digest (como sha256) produziria Ed25519ph, que não verifica contra o que a plataforma assina. O erro resultante é “assinatura inválida”, sem pista nenhuma da causa — por isso o aviso.

Resposta completa da API

json
{
  "ok": true,
  "data": {
    "status": "AUTHORIZED",
    "protocol": 1,
    "licenseId": "clx1a2b3c4d5e6f7g8h9",
    "sessionId": "clx9z8y7x6w5v4u3t2s1",
    "requestId": "aG0wZ2tsMnB4dnJ0czk4N2FiY2Rl",
    "authorization": "LSA1.eyJleHBpcmVz...xyz.MEUCIQDa...",
    "issuedAt": "2026-11-11T22:00:00.000Z",
    "expiresAt": "2026-11-11T23:00:00.000Z",
    "graceUntil": "2026-11-14T23:00:00.000Z",
    "heartbeatIntervalMinutes": 30,
    "features": ["premium"],
    "test": false
  }
}

Os campos fora de authorization são conveniência

Eles existem para log e para a interface. Não decida por eles: só o que está dentro do documento assinado é confiável. Um proxy malicioso reescreve o JSON à vontade; a assinatura, não.