Skip to content
← Todas las publicaciones
· Delta1 Labs KeyrightLicenciamiento.NETAnálisis a fondo

Una sola clave de licencia, tres productos: la claim de producto, los paquetes de suite como entitlements y el gate que permite que una clave Lite desbloquee Pro

Distribuyes un Designer, un Renderer y una CLI, y tu cliente quiere una sola clave para todos ellos, no tres. El atajo evidente —dar a cada aplicación el mismo slug de producto y dejar que la firma haga el resto— es el atajo que, sin hacer ruido, vende tu aplicación más cara al precio de la más barata, porque una clave que verifica para la línea de producto verifica en todas las aplicaciones de la línea. Esta entrada lee qué comprueba realmente la claim de producto de una licencia Keyright (es una comprobación de audiencia, no de características), expone las tres formas de licenciar una suite, muestra el agujero del diseño de slug compartido con el gate exacto que lo abre y construye la versión que aguanta: un slug para la línea, la pertenencia por aplicación como entitlements con espacio de nombres y un gate en cada aplicación que pregunta «¿estoy en este paquete?» antes de preguntar cualquier otra cosa. Termina con la ruta de actualización de una clave de una sola aplicación a una clave de suite, y el respaldo de dos clientes para equipos que ya distribuyeron un slug por aplicación.

Distribuyes tres cosas: un Designer, un Renderer y una CLI. Se venden por separado como niveles Lite y juntas como una Suite, y lo primero que pregunta un cliente de Suite es por qué tiene tres claves de licencia que pegar en tres aplicaciones. Pregunta razonable. Una clave por cliente es la respuesta correcta, y la forma en que la mayoría de los equipos llegan a ella —dar a cada aplicación el mismo slug de producto y dejar que la firma se encargue del resto— es la forma que, sin hacer ruido, vende tu Renderer al precio de tu Designer.

Esta entrada trata del gate que hace que una clave por cliente aguante: qué comprueba realmente la claim de producto de una licencia Keyright, por dónde se filtra el atajo del slug compartido y cómo la pertenencia por aplicación como entitlements lo cierra.

La claim de producto es una comprobación de audiencia

Toda licencia Keyright lleva un slug de producto, firmado junto con el resto del payload. Todo cliente Keyright lleva el slug que espera. Ambos se encuentran en la validación:

static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "acme-renderer",                     // the audience THIS app accepts
    PublicKeyBase64 = AcmeKeys.LicensePublicKey,           // public key only; safe to ship
    ServiceUrl      = "https://licensing.acme.example",
});

LicenseInfo info = License.Validate();   // fails closed if the key's product != "acme-renderer"

Si un cliente pega una clave de Designer en el Renderer, la firma está bien, la caducidad está bien, y aun así la validación falla: info.IsValid es false e info.Message indica que la clave se emitió para otro producto. Ese es exactamente el papel de una claim aud en un JWT: ¿es este token para mí? Se comprueba antes de leer cualquier otra claim.

Lo que la claim de producto no hace es decir qué pagó el titular de la licencia. Es identidad, no entitlement. Retén esa distinción, porque el agujero de más abajo nace de pedirle a la claim de producto un trabajo para el que nunca se diseñó.

Tres formas de licenciar una suite

Solo hay tres formas, y dos de ellas resultan tentadoras por malas razones.

Un slug por aplicación, tres claves. acme-designer, acme-renderer, acme-cli, emitidos cada uno por separado. Es seguro —una clave de Designer no puede tocar el Renderer— y es lo que obtienes por defecto cuando añades licenciamiento a las aplicaciones de una en una. El coste es el ticket de soporte con el que empezamos: tres claves que entregar, tres que pegar, tres que renovar.

Un slug para la línea, nada más. acme-suite en todas partes; el cliente Lite recibe una clave acme-suite con menos entitlements de características, el cliente Suite recibe una clave acme-suite con todos ellos. Una clave por cliente. Esta es la que se filtra, y la siguiente sección muestra exactamente cómo.

Un slug para la línea, más entitlements de pertenencia por aplicación. acme-suite en todas partes, y la clave dice qué aplicaciones cubre. Este es el diseño que sobrevive, y cuesta una llamada a IsEnabled por aplicación.

El agujero, en concreto

Toma la segunda forma. Esta es una clave Lite —solo Designer— emitida bajo el slug de la línea. No tiene nada malo:

{
  "product":  "acme-suite",
  "licensee": "acct_8f21",
  "expiry":   "2027-10-16T00:00:00Z",
  "entitlements": {
    "export":   true,
    "max-dpi":  300
  }
}

Y este es el gate del Renderer, escrito como se escriben normalmente los gates: validar y luego ramificar según IsValid:

// Renderer: initialised with Product = "acme-suite"
LicenseInfo info = License.Validate();

if (info.IsValid)             // signature ok, product ok, not expired → true
    RunLicensed();            // ...and the Lite customer is now running the Renderer
else
    RunEvaluation();

Repásalo paso a paso. El producto de la clave es acme-suite; el Renderer espera acme-suite; la firma verifica; la caducidad está a un año vista. Todas las comprobaciones pasan. IsValid es true, y el Renderer se ejecuta con licencia completa a partir de una clave que se vendió como «solo Designer». Sin falsificación, sin manipulación: el cliente compró el SKU más barato y la claim de producto, haciendo exactamente su trabajo, le dijo al Renderer que la clave era para él. Y lo era. Lo que la clave nunca dijo fue qué aplicaciones cubría, y nadie lo preguntó.

Una clave, tres apps: la claim de producto pasa en todasClave Lite (firmada)product: acme-suiteapp:designer = trueapp:cli = true(sin app:renderer)Designer · Product=acme-suiteIsValid ✓ IsEnabled("app:designer") ✓CLI · Product=acme-suiteIsValid ✓ IsEnabled("app:cli") ✓Renderer · Product=acme-suiteIsValid ✓ (la claim de producto pasa)gate solo con IsValid → DESBLOQUEAgate con app:renderer → se niegacon licenciacon licenciafugacorrectoDos preguntasclaim de producto:«¿es esta clavede mi línea?»entitlement depertenencia:«¿está ESTA appen el paquete?»ambos firmados;ninguno editablea mano

El gate que aguanta: la pertenencia como entitlement

La solución no es un slug por aplicación —eso tira por la borda el objetivo de una sola clave—. Es poner el paquete en la clave, como entitlements, y hacer que cada aplicación compruebe el suyo. La clave Suite:

{
  "product":  "acme-suite",
  "licensee": "acct_8f21",
  "expiry":   "2027-10-16T00:00:00Z",
  "entitlements": {
    "app:designer": true,
    "app:renderer": true,
    "app:cli":      true,
    "export":       true,
    "max-dpi":      1200
  }
}

La clave Lite tiene la misma forma con solo app:designer activado. El prefijo app: es una convención, no una característica de Keyright: mantiene la pertenencia visiblemente separada de las características como export y max-dpi, para que nadie confunda más tarde «qué aplicaciones» con «qué capacidades».

Cada aplicación hace ahora dos preguntas, en orden, y la segunda es el gate de verdad:

// Renderer. Same client, same slug — plus the question that was missing.
LicenseInfo info = License.Validate();

bool licensed = info.IsValid                       // 1. is this key for my product line?
             && License.IsEnabled("app:renderer"); // 2. is THIS app in the bundle?

if (!licensed) { RunEvaluation(info); return; }

// Feature gates come after membership, never instead of it.
if (License.IsEnabled("export"))             EnableExport();
int maxDpi = (int)License.GetLimit("max-dpi", fallback: 150);

Pasa la clave Lite por este Renderer. La claim de producto pasa, la firma pasa, IsEnabled("app:renderer") es false, licensed es false. El Designer, al preguntar IsEnabled("app:designer"), se desbloquea. Una sola clave; cada aplicación ve solo la porción que se le vendió. Y como los entitlements están dentro del payload firmado, un cliente no puede añadir "app:renderer": true a una clave Lite igual que tampoco puede alargar la caducidad: un byte cambiado y la firma falla.

Mantén la comprobación por aplicación en exactamente una línea, en exactamente un sitio, y ejecútala antes de cualquier gate de características. Si alguna vez un gate de características se ejecuta sin que haya pasado el gate de pertenencia, has reconstruido el agujero con pasos de más.

Emisión: un SKU, un conjunto de entitlements

En el lado de la emisión, un SKU no es más que un conjunto de entitlements con nombre, así que los tres niveles son tres plantillas, no tres productos:

{
  "product": "acme-suite",
  "skus": {
    "designer-lite": { "app:designer": true, "export": false, "max-dpi": 300  },
    "renderer-lite": { "app:renderer": true, "export": true,  "max-dpi": 600  },
    "suite":         { "app:designer": true, "app:renderer": true, "app:cli": true,
                       "export": true, "max-dpi": 1200 }
  }
}

Actualizar a un cliente de Designer Lite a Suite es una reemisión bajo el mismo slug con el conjunto suite. Las aplicaciones no cambian, nada se vuelve a desplegar, y el cliente pega una única clave nueva en cualquiera de las tres aplicaciones —la licencia almacenada se comparte— y las tres se encienden.

Si ya distribuiste un slug por aplicación

Muchos equipos añadieron licenciamiento de una aplicación en una y tienen claves acme-designer, acme-renderer y acme-cli en circulación. Aún puedes llegar a una clave por cliente sin una migración de golpe. Cada aplicación sigue aceptando su slug heredado y, además, acepta el slug de suite, pero solo con su propio entitlement de pertenencia:

static readonly KeyrightClient Suite  = KeyrightClient.Initialize(new KeyrightOptions
    { Product = "acme-suite",    PublicKeyBase64 = AcmeKeys.LicensePublicKey, ServiceUrl = Url });
static readonly KeyrightClient Legacy = KeyrightClient.Initialize(new KeyrightOptions
    { Product = "acme-renderer", PublicKeyBase64 = AcmeKeys.LicensePublicKey, ServiceUrl = Url });

static (LicenseInfo info, KeyrightClient via) Gate()
{
    var s = Suite.Validate();
    if (s.IsValid && Suite.IsEnabled("app:renderer")) return (s, Suite);   // suite key, in bundle

    var l = Legacy.Validate();
    if (l.IsValid) return (l, Legacy);                                      // old per-app key

    return (s, Suite);                                                      // fail closed; report the suite diagnosis
}

Los clientes nuevos reciben claves de suite. Los clientes existentes siguen funcionando. Cuando un cliente heredado se actualiza, le emites una clave de suite y la ruta heredada simplemente deja de ser la que responde primero. Lleva la cuenta de cuántas claves heredadas quedan a partir de tus registros de emisión, y cuando llegue a cero, elimina Legacy. Lo único que no hay que hacer es «simplificar» una aplicación heredada cambiando su único cliente al slug de suite y dejando el gate en IsValid: eso es de nuevo la segunda forma, con el agujero.

Prueba la fuga, no solo el camino feliz

La prueba que importa es la que habría cazado el diseño de slug compartido. Emite una clave Lite con una clave de firma de prueba y verifica que el gate del Renderer la rechaza:

[Fact]
public void Renderer_refuses_a_designer_only_key()
{
    string lite = TestIssuer.Issue(product: "acme-suite",
        entitlements: new() { ["app:designer"] = true, ["export"] = true });
    string path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.lic");
    File.WriteAllText(path, lite);

    var renderer = KeyrightClient.Initialize(new KeyrightOptions
        { Product = "acme-suite", PublicKeyBase64 = TestIssuer.PublicKeyBase64, LicenseFilePath = path });

    var info = renderer.Validate();
    Assert.True(info.IsValid);                           // product claim passes — by design
    Assert.False(renderer.IsEnabled("app:renderer"));   // membership does not — the real gate
}

Fíjate en lo que documenta la primera aserción: IsValid sí es true, y eso es correcto. La prueba existe para asegurarse de que nadie llegue nunca a distribuir una compilación en la que eso sea lo único que comprueba el Renderer. Combínala con la inversa —una clave de suite habilita las tres— y con un servicio de emisión falso si quieres cubrir también el viaje de ida y vuelta de la activación.

La conclusión

La claim de producto de una licencia es una comprobación de audiencia —«¿es esta clave para mi línea de producto?»— y se comprueba primero, antes que cualquier otra claim, que es precisamente lo que la convierte en la herramienta equivocada para decidir qué aplicaciones de esa línea compró un cliente. Dale a toda la línea un único slug para que el cliente tenga una sola clave, pon el paquete en la clave como entitlements de pertenencia por aplicación, y haz que cada aplicación decida según su propia pertenencia antes de que se ejecute cualquier gate de características. Eso convierte las suites, los niveles Lite y las actualizaciones en conjuntos de entitlements en el lado de la emisión, mantiene la comprobación de una línea dentro de cada aplicación y cierra el atajo que, de otro modo, vendería tu Renderer al precio de tu Designer.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.