Skip to content

Integración del SDK

Usa la API HTTP directamente (cualquier lenguaje)

¿No hay SDK para tu lenguaje? Llama a la API de runtime de Keyright por HTTP: activa una máquina, verifica tú mismo el lease firmado sin conexión (payload canónico + RSA/SHA-256), desactiva y gestiona máquinas aisladas (air-gapped) — con ejemplos copiables que puedes portar a Go, Rust, C++, PHP y más.

Keyright ofrece SDK oficiales para .NET, Node.js, Python y Java — si estás en uno de esos, úsalo. Cada SDK implementa todo lo de esta página (activación, verificación de lease sin conexión, caché, bloqueo por equipo, revocación) y es la ruta soportada. Consulta la guía Integración del SDK de .NET para la forma que todos siguen.

Esta página es para todos los demás — Go, Rust, C++, PHP, Ruby, Swift o cualquier cosa sin un SDK de Keyright. La API de runtime es JSON plano sobre HTTP, y los formatos de licencia/lease son especificaciones versionadas e independientes del lenguaje. Puedes comunicarte con ella directamente. Esta página es a la vez un cómo-hacerlo y la referencia exacta a nivel de bytes que necesitas para verificar un lease correctamente en tu lenguaje.

Todo el modelo en dos movimientos

Todo lo que hace un cliente se reduce a dos cosas:

  1. Activar — haz POST de una clave de licencia más un id de máquina estable, y recibe de vuelta un lease firmado con RSA, de vida corta, vinculado a esa máquina. Ese es el paso en línea: consume un puesto y prueba que la clave es válida ahora mismo.
  2. Verificar el lease sin conexión — comprueba tú mismo la firma del lease contra la clave pública del proveedor, luego comprueba producto/máquina/caducidad. Una vez que puedes hacer esto, tu aplicación sigue funcionando sin red hasta que termina la ventana de gracia del lease. Si solo llamas siempre en línea, puedes saltarte la verificación y confiar en el campo status — pero entonces una caída de red deja fuera a tus usuarios, así que la mayoría de los clientes verifican.

Ambos endpoints de runtime (/v1/activate, /v1/validate) y /v1/free-seat son sin auth — sin token, llamados directamente por tu aplicación. Están limitados por tasa por IP de cliente (una ventana por minuto generosa; un 429 lleva Retry-After: 60).

$BASE, abajo, es la URL del servicio emisor de tu proveedor, p. ej. https://keyright.delta1labs.com.

Activar

Haz POST de JSON a /v1/activate. Los únicos campos obligatorios son key, machineId y product; el resto son metadatos opcionales que el proveedor ve en su panel.

curl -s -X POST "$BASE/v1/activate" \
  -H "content-type: application/json" \
  -d '{
        "key": "LIC-9F3A...",
        "machineId": "b1946ac92492d2347c6235b4d2611184",
        "product": "acme-app",
        "os": "linux",
        "hostname": "build-07",
        "appVersion": "2.4.0"
      }'

Una respuesta correcta:

{
  "status": "ok",
  "seats": 3,
  "used": 1,
  "license": {
    "licensee": "Acme GmbH",
    "product": "acme-app",
    "tier": "pro",
    "expiryUtc": "2027-01-01T00:00:00.0000000Z",
    "trial": false,
    "revoked": false
  },
  "lease": "{\"key\":\"LIC-9F3A...\",\"machineId\":\"b1946ac9...\",\"product\":\"acme-app\",\"licensee\":\"Acme GmbH\",\"tier\":\"pro\",\"licenseExpiryUtc\":\"2027-01-01T00:00:00.0000000Z\",\"leaseExpiresUtc\":\"2026-10-11T09:14:22.1030000Z\",\"entitlements\":{\"export\":\"true\",\"max-projects\":\"10\"},\"signature\":\"Base64Sig==\"}"
}

Dos cosas a tener en cuenta:

  • lease es una cadena JSON — todo el documento de lease firmado, serializado, como un campo dentro de la respuesta. Almacenas esta cadena literalmente (es tu prueba sin conexión), y la analizas como su propio objeto JSON para verificarla. No la reformatees antes de almacenarla.
  • El bloque license es un resumen de conveniencia. No confíes en él para restringir el acceso — no está firmado individualmente. El lease firmado es la fuente de verdad; el resumen es solo para mostrar.

Gestionar status

status es el campo sobre el que ramificas. Valores:

statusSignificadoQué hacer
okActivado; lease está presenteVerifica y almacena en caché el lease; desbloquea características
not_foundClave desconocida, o producto equivocadoMuestra “key not recognized”; permanece en modo libre
revokedLa clave fue revocadaCae a modo libre
expiredLa licencia (o la prueba) ha caducadoCae a modo libre; invita a renovar
seat_limitTodos los puestos de esta clave están en usoDile al usuario que libere un puesto en otra máquina

Solo ok devuelve un lease. Falla cerrado: trata cualquier estado distinto de ok, cualquier código HTTP no-2xx, un 429, un cuerpo mal formado o un lease que falle la verificación como sin licencia. Nunca caigas a un estado desbloqueado ante un error.

Puestos e idempotencia

Los puestos se aplican del lado del servidor. El recuento seats de la clave es el techo; used te dice cuántas máquinas tienen actualmente un puesto. Reactivar una máquina que ya está vinculada a la clave no quema otro puesto — el servidor indexa por (key, machineId), así que llamar a /v1/activate de nuevo para la misma máquina es idempotente y solo refresca el lease. Por eso el id de máquina debe ser estable (siguiente sección). Si cambia, la máquina parece nueva y consume un puesto nuevo, y el usuario topa con seat_limit.

El id de máquina

machineId es tu identificador para el dispositivo — tú lo eliges. Los SDK oficiales calculan una huella digital de bloqueo por equipo con sal (su esquema interno KRM1) a partir de señales de hardware y SO, pero el servidor trata machineId como una cadena opaca. Un cliente sin SDK no necesita coincidir con ese esquema. Solo necesitas un id que sea:

  • Estable entre reinicios, actualizaciones y reinicios del sistema. Si cambia, la máquina parece nueva, quema un puesto y acaba disparando seat_limit. Esta es la propiedad más importante.
  • Por máquina, para que el conteo de puestos tenga sentido.
  • No trivialmente falsificable por un usuario final que quiera clonar una activación en muchas máquinas (un hash, no un valor que pueda teclear).

Una buena receta: reúne uno o dos identificadores estables de SO/hardware (un GUID de máquina, una MAC principal, un número de serie de disco o un machine-id de systemd), concaténalos con una cadena de sal constante propia, haz hash con SHA-256 y codifica el digest en hex o base64. Calcúlalo una vez, y reutiliza exactamente la misma cadena cada vez que actives, valides, verifiques o liberes un puesto. Almacénalo junto al lease en caché para poder compararlo luego.

machineId = hex( sha256( "acme-app-salt|" + stableHardwareId ) )

Mantenlo consistente para siempre para una instalación dada. Eso es todo lo que el servidor necesita.

Verifica el lease sin conexión

Esta es la parte sin atajos. El lease está firmado con la clave RSA privada del proveedor, que nunca sale de su servidor. Lo verificas con la clave pública correspondiente, que el proveedor te da (pestaña Integration del panel, o GET $BASE/admin/public-key con un token de administrador) como una cadena base64 SubjectPublicKeyInfo (SPKI). Esa clave no es un secreto — solo puede verificar, nunca firmar — así que la embebes en tu binario.

La verificación es: reconstruir los bytes exactos que se firmaron, luego verificar con RSA la firma sobre ellos. Los bytes firmados no son el JSON del lease — son una cadena canónica ensamblada a partir de campos específicos en un orden específico.

El payload canónico — consíguelo exacto byte a byte

La cadena firmada es (versión de especificación krlease1):

krlease1|<key>|<machineId>|<product>|<licensee>|<tier>|<licenseExpiryUtc-or-empty>|<leaseExpiresUtc>

Luego, condicionalmente y en este orden exacto, se añaden dos sufijos:

  1. |ent=<entitlements> — añadido solo si el lease tiene un objeto entitlements no vacío. Los derechos se serializan de forma determinista: claves ordenadas por orden ordinal (byte/codepoint), cada una representada como key=value, unidas por comas. P. ej. {"max-projects":"10","export":"true"} → export=true,max-projects=10.
  2. |trial — añadido solo si el campo trial del lease es true. No se añade nada cuando es falso.

Toda la cadena se codifica en UTF-8, y la firma es RSA PKCS#1 v1.5 sobre SHA-256, codificada en base64 en el campo signature del lease.

Este fallo de ordenación es real — ten cuidado. |ent= va antes de |trial. Si equivocas el orden, omites un sufijo condicional que debería estar presente, añades uno que no debería, o reformateas una fecha, los bytes reconstruidos difieren en un carácter — y la firma falla silenciosamente al verificar. No hay coincidencia parcial. Este fallo de ordenación exacto mordió a los SDK durante el desarrollo; por eso el orden se detalla con tanta precisión. Cuando un lease que sabes que es bueno no verifica, la cadena canónica es el primer sitio donde mirar.

Detalles críticos que hacen tropezar a la gente:

  • Usa las cadenas de fecha tal cual del JSON. licenseExpiryUtc y leaseExpiresUtc son cadenas ISO-8601 de ida y vuelta como 2026-10-11T09:14:22.1030000Z (7 dígitos fraccionarios). No las analices y reserialices para la cadena canónica — copia los caracteres exactos del lease. (Las analizas por separado, luego, solo para comparar con “ahora”.)
  • licenseExpiryUtc puede estar ausente/nulo para una licencia perpetua. En ese caso pon la cadena vacía en esa posición — los dos separadores | a su alrededor siguen apareciendo: ...|<tier>||<leaseExpiresUtc>.
  • Derechos vacíos = ningún |ent= en absoluto. Un lease sin derechos firma sobre exactamente los mismos bytes que uno emitido antes de que existieran los derechos. Añade |ent= solo cuando el objeto está presente y no vacío.

Ejemplo concreto (Go — pórtalo a tu lenguaje)

La biblioteca estándar de Go cubre todo. La lógica se porta directamente a Rust (rsa + sha2), PHP (openssl_verify con OPENSSL_ALGO_SHA256), Ruby (OpenSSL::PKey::RSA#verify), C++ (OpenSSL EVP_DigestVerify), etc. — solo cambian las llamadas de crypto.

package main

import (
	"crypto"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"errors"
	"sort"
	"strings"
	"time"
)

type Lease struct {
	Key              string            `json:"key"`
	MachineID        string            `json:"machineId"`
	Product          string            `json:"product"`
	Licensee         string            `json:"licensee"`
	Tier             string            `json:"tier"`
	LicenseExpiryUtc *string           `json:"licenseExpiryUtc"` // pointer: may be null
	LeaseExpiresUtc  string            `json:"leaseExpiresUtc"`
	Entitlements     map[string]string `json:"entitlements"`
	Trial            bool              `json:"trial"`
	Signature        string            `json:"signature"`
}

// Rebuild the EXACT bytes that were signed. Order is load-bearing.
func canonical(l *Lease) []byte {
	var b strings.Builder
	b.WriteString("krlease1|")
	b.WriteString(l.Key + "|")
	b.WriteString(l.MachineID + "|")
	b.WriteString(l.Product + "|")
	b.WriteString(l.Licensee + "|")
	b.WriteString(l.Tier + "|")
	if l.LicenseExpiryUtc != nil { // empty string when perpetual (null)
		b.WriteString(*l.LicenseExpiryUtc)
	}
	b.WriteString("|")
	b.WriteString(l.LeaseExpiresUtc)

	// |ent=  FIRST, only when non-empty. Keys sorted ordinal, k=v, comma-joined.
	if len(l.Entitlements) > 0 {
		keys := make([]string, 0, len(l.Entitlements))
		for k := range l.Entitlements {
			keys = append(keys, k)
		}
		sort.Strings(keys) // Go's sort.Strings is ordinal (byte-wise) — matches the spec
		parts := make([]string, len(keys))
		for i, k := range keys {
			parts[i] = k + "=" + l.Entitlements[k]
		}
		b.WriteString("|ent=" + strings.Join(parts, ","))
	}

	// |trial  LAST, only when true.
	if l.Trial {
		b.WriteString("|trial")
	}
	return []byte(b.String())
}

// publicKeyB64 is the vendor's base64 SubjectPublicKeyInfo (SPKI).
func VerifyLease(leaseJSON, publicKeyB64, expectedProduct, ourMachineID string, now time.Time) (*Lease, error) {
	var l Lease
	if err := json.Unmarshal([]byte(leaseJSON), &l); err != nil {
		return nil, err // fail closed
	}

	// 1. Load the public key (SPKI).
	der, err := base64.StdEncoding.DecodeString(publicKeyB64)
	if err != nil {
		return nil, err
	}
	pubAny, err := x509.ParsePKIXPublicKey(der)
	if err != nil {
		return nil, err
	}
	pub, ok := pubAny.(*rsa.PublicKey)
	if !ok {
		return nil, errors.New("not an RSA key")
	}

	// 2. Verify the signature over the canonical bytes (PKCS#1 v1.5, SHA-256).
	sig, err := base64.StdEncoding.DecodeString(l.Signature)
	if err != nil {
		return nil, err
	}
	digest := sha256.Sum256(canonical(&l))
	if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, digest[:], sig); err != nil {
		return nil, errors.New("bad signature") // fail closed
	}

	// 3. Semantic checks — all must pass, or fail closed.
	if l.Product != expectedProduct {
		return nil, errors.New("wrong product")
	}
	if l.MachineID != ourMachineID {
		return nil, errors.New("lease bound to a different machine")
	}
	leaseExp, err := time.Parse(time.RFC3339Nano, l.LeaseExpiresUtc)
	if err != nil || !now.Before(leaseExp) {
		return nil, errors.New("lease grace window elapsed") // time to re-activate online
	}
	if l.LicenseExpiryUtc != nil {
		licExp, err := time.Parse(time.RFC3339Nano, *l.LicenseExpiryUtc)
		if err != nil || !now.Before(licExp) {
			return nil, errors.New("license expired")
		}
	}
	return &l, nil // licensed — read l.Tier / l.Entitlements to gate features
}

La lista de verificación de verificación (cualquier lenguaje)

  1. Analiza el JSON del lease.
  2. Reconstruye la cadena canónica exactamente — ten cuidado con |ent= antes de |trial, la posición de cadena vacía para un licenseExpiryUtc nulo, las fechas literales y las claves de derechos ordenadas por orden ordinal.
  3. Decodifica en base64 la signature, decodifica en base64 la clave pública SPKI y verifica con RSA (PKCS#1 v1.5 / SHA-256).
  4. Comprueba que product es igual a tu producto.
  5. Comprueba que machineId es igual al id de máquina que almacenaste en la activación.
  6. Comprueba que leaseExpiresUtc está en el futuro (la ventana de gracia sin conexión — cuando pasa, reactiva en línea).
  7. Comprueba que licenseExpiryUtc, si está presente, está en el futuro.
  8. Falla cerrado ante cualquier cosa — un error de análisis, una firma errónea, una discrepancia, una caducidad, un reloj en el que no puedes confiar. Solo un lease que supera cada paso está licenciado.

Una vez que supera, lee tier para la edición y entitlements para las restricciones por característica.

Desactiva una máquina

Para liberar el puesto que esta máquina tiene (p. ej. antes de desinstalar, o al migrar a nuevo hardware), haz POST a /v1/free-seat:

curl -s -X POST "$BASE/v1/free-seat" \
  -H "content-type: application/json" \
  -d '{"key":"LIC-9F3A...","machineId":"b1946ac9...","product":"acme-app"}'

Respuesta: {"status":"ok"}. El puesto se libera y used baja, de modo que otra máquina puede activar. Borra tu lease en caché local al mismo tiempo.

Revalidar y refrescar

  • POST /v1/validate vuelve a comprobar una clave + máquina contra el estado actual del servidor — detecta la revocación y la caducidad que ocurrieron tras la activación — sin consumir un puesto nuevo. Toma el mismo cuerpo de petición que /v1/activate y devuelve la misma forma de respuesta (incluido un lease fresco cuando sigue siendo ok). Úsalo como comprobación de salud periódica.
  • Reactivar (/v1/activate) en una máquina ya vinculada es idempotente y refresca el lease con un nuevo leaseExpiresUtc. En la práctica esto es lo que la mayoría de los clientes hacen de forma programada.

Los leases son de vida corta — la ventana de gracia por defecto es de 14 días (el proveedor puede configurarla). Así que un cliente en línea debería reactivar (o validar) periódicamente — cómodamente antes del leaseExpiresUtc del lease actual — para recoger revocaciones y mantener un lease fresco en caché. Entre refrescos, verifica el lease en caché sin conexión en cada arranque y restringe según eso.

Máquinas sin conexión / aisladas (air-gapped)

Para una máquina que nunca puede alcanzar el servicio, el proveedor acuña un lease de larga duración por ti fuera de banda. Un operador llama (token de administrador requerido, así que este es el paso del proveedor, no de tu cliente):

curl -s -X POST "$BASE/admin/licenses/{licenseId}/offline-lease" \
  -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"machineId":"b1946ac9...","days":365}'

que devuelve { "machineId": "...", "ttlDays": 365, "lease": "{...signed lease JSON...}" }. El campo days por defecto es 365 — un TTL mucho más largo que la ventana de gracia de un lease en línea, ya que la máquina nunca refrescará.

Obtienes el id de máquina de la máquina destino primero (calcúlalo exactamente como lo hace tu cliente, y entrégalo al proveedor), entregas la cadena lease devuelta a la máquina por archivo o USB, y la verificas con exactamente el mismo código que un lease en línea — mismo payload canónico, misma comprobación de firma, mismas comprobaciones de producto/máquina/caducidad. Un lease sin conexión es un lease ordinario con un leaseExpiresUtc largo; nada sobre la verificación cambia. (Los leases sin conexión acuñados de esta forma no se marcan como trial, así que no aplica ningún sufijo |trial.)

Una nota sobre los archivos de licencia sin conexión

La activación y los leases son la ruta en-línea-luego-en-caché. Keyright también admite archivos de licencia totalmente sin conexión — un documento JSON firmado (license.json) sin ninguna ida y vuelta de activación. Su payload canónico firmado es una especificación diferente y más simple (versión kr1):

<licensee>|<expiryUtc-or-empty>|<tier>[|trial][|kr1:<keyright-fields>]

donde el bloque opcional kr1: lleva campos product=, id=, machine=, td= y ent= unidos por punto y coma (la misma codificación de derechos ordenada por orden ordinal y unida por comas). Mismo crypto — RSA PKCS#1 v1.5 / SHA-256, verificado contra la misma clave pública SPKI. Si tu proveedor te envía archivos de licencia en lugar de usar la activación, verifícalos de la misma forma: reconstruye esa cadena canónica y verifica con RSA. La mayoría de las integraciones sin SDK solo necesitan la ruta de lease de arriba; recurre a la verificación de archivos de licencia solo si tu proveedor distribuye archivos directamente.

Véase también