API de licenciamento de software: chaves assinadas digitalmente (Ed25519) com validação offline no cliente e ativação online por máquina no servidor.
Stack: Python 3.12 · FastAPI · SQLAlchemy 2 (async) · PostgreSQL · Alembic · Docker Compose · uv.
Existem dois mecanismos que se complementam:
Cada licença emitida é uma string assinada com Ed25519 (criptografia assimétrica):
LIC1.<base64url(payload JSON)>.<base64url(assinatura)>
O payload contém os dados da licença:
| Campo | Significado |
|---|---|
iss |
emissor (identifica sua API) |
sub |
nome do cliente |
lic |
UUID da licença no banco |
iat |
momento da emissão |
exp |
expiração (epoch); determinada pelo pacote de 1, 2, 3 ou 5 anos |
tier |
tipo de licença (standard/premium/...) |
max |
limite de ativações |
A assinatura usa a chave privada que só existe no servidor. O software do cliente embute apenas a chave pública e verifica a assinatura localmente — funciona sem internet. Se alguém forjar uma chave, a assinatura não confere. É a mesma ideia usada em licenças de software comercial.
O cliente envia a chave + um identificador de máquina (machine_id) para POST /api/v1/activate. O servidor:
- verifica a assinatura da chave;
- confere se a licença existe no banco, não está revogada e não expirou;
- conta as ativações ativas e registra a máquina (respeitando
max_activations); - responde com a ativação criada.
Isso dá revogação e controle de máquinas — coisas que uma chave offline sozinha não tem.
Não existe licença inquebrável. O objetivo é elevar o custo da pirataria (assinatura) e dar controle (revogação, limite de máquinas). Quem consegue o binário sempre pode tentar extrair a chave pública — a robustez final depende de ofuscação no cliente, o que foge ao escopo desta API.
| Camada | Técnica |
|---|---|
| Assinatura das licenças | Ed25519 (PyNaCl) |
| Senha dos admins | Argon2 (argon2-cffi) |
| Sessão dos admins | JWT HS256 com expiração (pyjwt) |
| Rotas públicas | Rate limiting por IP (slowapi) |
| Transporte | sempre HTTPS em produção (reverse proxy) |
| Segredos | fora do código, via variáveis de ambiente / .env |
licensing-api/
├── app/
│ ├── main.py # FastAPI, CORS, rate limit, rotas
│ ├── core/
│ │ ├── config.py # Settings (lê .env)
│ │ ├── database.py # engine async + sessão
│ │ ├── security.py # Argon2 + JWT
│ │ ├── signing.py # assina/valida chaves Ed25519
│ │ └── ratelimit.py # wrapper do slowapi
│ ├── models/ # SQLAlchemy: Admin, License, Activation
│ ├── schemas/ # Pydantic: entrada/saída das rotas
│ ├── services/ # lógica de negócio (emissão/ativação)
│ └── api/
│ ├── deps.py # depende do admin autenticado
│ └── routes/
│ ├── auth.py # login + me
│ ├── licenses.py # CRUD de licenças (admin)
│ └── activations.py # ativar/validar (público)
├── alembic/ # migrações de banco
├── cli/main.py # painel CLI (typer + rich)
├── scripts/
│ ├── generate_keys.py # gera o par Ed25519
│ └── create_admin.py # cria usuário admin
├── tests/ # pytest (assinatura + API)
├── Dockerfile
├── docker-compose.yml
└── pyproject.toml # dependências (uv)
| Campo | Descrição |
|---|---|
customer_name |
licenciado (empresa ou pessoa titular da licença) |
email |
e-mail do licenciado |
contact_name |
contato para renovação (geralmente uma pessoa) |
contact_email |
e-mail do contato |
contact_phone |
telefone do contato |
tier |
tipo de licença (standard/premium/enterprise) |
validity_years |
pacote comprado: 1, 2, 3 ou 5 anos (define expires_at na emissão) |
expires_at |
data de expiração (calculada do pacote; renovação soma ao vencimento atual) |
max_activations |
limite de máquinas ativas |
revoked |
se a licença está revogada |
Os campos de contato servem para você localizar e negociar a renovação e não entram na chave assinada — alterá-los não invalida a chave que os clientes já têm.
docker compose up -d --builddb→ PostgreSQL 17 (porta 5432)migrate→ rodaalembic upgrade head(cria as tabelas) e encerraapi→ FastAPI na porta 8000
docker compose exec -T api uv run scripts/create_admin.py --username admin --password 'Senha@Fort3'# saúde
curl http://localhost:8000/healthz
# documentação interativa (Swagger)
# http://localhost:8000/docs| Método | Rota | Descrição |
|---|---|---|
| POST | /api/v1/auth/login |
login do admin → retorna JWT |
| POST | /api/v1/activate |
ativa uma máquina com a chave (rate: 10/min) |
| POST | /api/v1/validate |
valida a chave no servidor (rate: 30/min) |
| Método | Rota | Descrição |
|---|---|---|
| POST | /api/v1/admin/admins |
cria usuário administrador |
| GET | /api/v1/admin/admins |
lista administradores |
| GET | /api/v1/admin/admins/{id} |
detalhe de um admin |
| PATCH | /api/v1/admin/admins/{id} |
edita usuário/senha de um admin |
| DELETE | /api/v1/admin/admins/{id} |
exclui admin (não permite excluir a si mesmo) |
| POST | /api/v1/admin/licenses |
emite licença (pacote de 1/2/3/5 anos) → retorna a chave assinada |
| GET | /api/v1/admin/licenses |
lista licenças |
| GET | /api/v1/admin/licenses/{id} |
detalhe de uma licença |
| GET | /api/v1/admin/licenses/{id}/key |
recupera a chave assinada |
| PATCH | /api/v1/admin/licenses/{id} |
edita (re-assina a chave só se cliente/tier/max/expiração mudarem) |
| POST | /api/v1/admin/licenses/{id}/renew |
renova somando 1/2/3/5 anos ao vencimento atual (re-assina a chave) |
| DELETE | /api/v1/admin/licenses/{id} |
exclui |
| GET | /api/v1/admin/licenses/{id}/activations |
lista ativações da licença |
| DELETE | /api/v1/admin/licenses/{id}/activations/{aid} |
revoga uma ativação (libera a máquina) |
| GET | /api/v1/auth/me |
quem é o admin logado |
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"Senha@Fort3"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
# emitir licença (guarde a chave retornada no campo "key")
curl -s -X POST http://localhost:8000/api/v1/admin/licenses \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"customer_name":"Empresa XPTO","email":"contato@xpto.com","tier":"enterprise","max_activations":2}'
# cliente ativa uma máquina
curl -s -X POST http://localhost:8000/api/v1/activate \
-H 'Content-Type: application/json' \
-d '{"license_key":"LIC1.eyJpc3Mi...","machine_id":"maq-a"}'
# cliente valida
curl -s -X POST http://localhost:8000/api/v1/validate \
-H 'Content-Type: application/json' \
-d '{"license_key":"LIC1.eyJpc3Mi..."}'Acompanha um painel de terminal que consome a API (usando o token guardado localmente).
uv sync # instala o comando licensing-cli
uv run licensing-cli --help # lista os comandoslicensing-cli login --base-url http://localhost:8000 --username admin
licensing-cli whoami
licensing-cli logoutlicensing-cli admins list
licensing-cli admins create --username gestor
licensing-cli admins password <id> # troca senha (pergunta, escondida)
licensing-cli admins delete <id>licensing-cli licenses list
licensing-cli licenses show <id>
licensing-cli licenses key <id> # recupera a chave assinada (saída crua)
licensing-cli licenses create --customer "Empresa XPTO" --email contato@xpto.com --tier enterprise --max-activations 2 --anos 2
licensing-cli licenses create --customer "Empresa Acme LTDA" --contact-name "Maria Silva" --contact-email maria@acme.com --contact-phone "+55 11 99999-0000" --anos 5
licensing-cli licenses renew <id> --anos 2 # estende do vencimento atual
licensing-cli licenses revoke <id> # revoga (a chave NÃO muda)
licensing-cli licenses revoke <id> --no-revoke # desrevoga
licensing-cli licenses activations <id> # máquinas ativas
licensing-cli licenses revoke-activation <id> <activation-id> # libera vaga
licensing-cli licenses delete <id>licensing-cli activate --key "LIC1...." --machine "maq-a"
licensing-cli validate --key "LIC1...."
licensing-cli statsO token é salvo em ~/.config/licensing-cli/config.json. Em sessão expirada, o CLI pede para você rodar login novamente.
O cliente deve:
- validar offline a assinatura com a chave pública (funciona sem internet);
- ativar online uma vez por máquina;
- revalidar periodicamente com
POST /validate(detecta revogação).
Exemplo mínimo de validação offline em Python:
import base64, json
import nacl.signing
PUBLIC_KEY_B64 = "vONy5XOTy2ABVWyIb5v7kncmKkhnP3HHdbeA6kqRkO0=" # embutida no binário
def b64u_decode(s):
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def verificar_licenca(chave):
prefix, body, sig = chave.split(".")
if prefix != "LIC1":
raise ValueError("chave invalida")
vk = nacl.signing.VerifyKey(base64.b64decode(PUBLIC_KEY_B64))
vk.verify(body.encode(), b64u_decode(sig)) # levanta se for falsa
return json.loads(b64u_decode(body))A chave pública pode ser publicada sem risco; só a privada é segredo. Em produção troque pelo par gerado por você e não use a deste repositório.
Os testes usam SQLite em memória (sem precisar do Postgres):
uv sync
uv run pytest -qCobrem: assinatura/verificação, chave adulterada/expirada, login, emissão, limite de ativações, revogação, liberação de vaga, validação de chave forjada e CRUD de administradores (criar, editar senha, excluir, sem exclusão do próprio usuário).
- Gerar novo par de chaves:
uv run scripts/generate_keys.pye trocar no.env. - Trocar
JWT_SECRETpor um valor aleatório longo (python3 -c "import secrets; print(secrets.token_urlsafe(48))"). - Trocar senha do Postgres no
docker-compose.yml(e emDATABASE_URL). - HTTPS via reverse proxy (nginx/Caddy/Traefik) na frente da API.
- Restringir
CORS_ORIGINSaos seus domínios. - Backup do Postgres (volume
pgdata). - Guardar a chave privada com segurança; só a pública vai para os clientes.
docker compose down # derruba tudo
docker compose up -d --build # reconstrói e sobe
docker compose logs -f api # logs da API
uv run scripts/create_admin.py --username admin --password 'Senha@Fort3' # novo admin (local)
uv run alembic upgrade head # aplica migrações (local)