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 deLSA1.<payload>, em base64url sem padding.
O prefixo entra na assinatura
Payload
{
"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
}| Campo | Tipo | O que é |
|---|---|---|
| v | inteiro | Versão do protocolo. Documento de versão desconhecida é recusado. |
| kid | string | Identificador da chave que assinou. Escolhe a pública correta. |
| licenseId | string | Licença. |
| productId | string | Precisa ser igual ao do seu plugin. |
| installationId | string | Precisa ser igual ao da sua instalação. |
| sessionId | string | Sessão, usada no heartbeat. |
| status | string | AUTHORIZED. A decisão real é a assinatura, não este campo. |
| issuedAt | inteiro | Epoch em SEGUNDOS — cabe em int no Java. |
| expiresAt | inteiro | Até quando vale sem renovar. |
| graceUntil | inteiro | Até quando vale offline depois de expiresAt. |
| requestId | string | Identificador da solicitação. Confira contra o que você enviou. |
| nonce | string | Eco do nonce que você enviou. |
| features | array de string | Recursos autorizados pela política. |
| test | booleano | Licenç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, nunca1762905600.0. - Apenas string, inteiro, booleano e array de string. Qualquer campo novo entra como um desses, por regra do protocolo.
Ordem de verificação
O SDK faz exatamente estas checagens, nesta ordem. Falhar qualquer uma é motivo de recusa; nenhuma é opcional.
- Assinatura contra a chave pública de
kid. É a única coisa que o servidor do cliente não consegue forjar. - Versão do protocolo. Desconhecida é recusada, nunca “interpretada do jeito que der”.
- Produto esperado. Um documento legítimo de outro plugin não autoriza este.
- Instalação esperada. O documento de outra máquina não vale aqui, mesmo sendo autêntico.
requestIdenonce. Precisam ser os que este processo acabou de enviar. É o que impede reapresentar uma resposta gravada antes.- Prazo.
expiresAtegraceUntilvêm de dentro da assinatura. test. Recuse em produção.
Obter as chaves públicas
GET /api/v1/licensing/.well-known/keys{
"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+
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
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
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
{
"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