Skip to content
← Todas las publicaciones
· Delta1 Labs Licenciamiento.NETGuía

Licenciamiento por usuario nombrado: vincular derechos a una persona, no a una máquina

La vinculación a máquina responde a "¿qué ordenador es este?", pero muchos productos se venden por *asiento*, no por caja. Un desarrollador con un portátil, un equipo de escritorio y un runner de CI es un usuario de pago, no tres activaciones. El licenciamiento por usuario nombrado cambia el ancla: la licencia pertenece a una identidad, y los dispositivos cuelgan de ella como hojas bajo un límite que tú controlas. Aquí tienes cómo modelar un derecho de identidad firmado en Keyright, canjear un inicio de sesión por un contrato (lease) de corta duración acotado al dispositivo, imponer un límite de dispositivos sin atar al usuario a una sola caja, y permitir que un usuario retire un dispositivo de un asiento lleno, con el C# real para emitirlo, vincularlo y verificarlo.

La mayoría de las guías de licenciamiento parten de la máquina: toma la huella del dispositivo, vincula la licencia a ella, y ahora una activación equivale a una caja. Ese modelo es limpio y es el valor por defecto correcto para mucho software de escritorio. Pero asume en silencio algo que a menudo es falso: que un cliente es una máquina. Vende una herramienta de desarrollo y tu cliente es una persona que tiene un portátil, un equipo de escritorio en la oficina y un agente de CI que ejecuta la misma herramienta en un contenedor. Con node-locking, ese único desarrollador de pago quema tres activaciones y abre un ticket de soporte la primera semana. La unidad que realmente vendiste era un asiento, y un asiento pertenece a una persona.

El licenciamiento por usuario nombrado lo hace explícito. La licencia está anclada a una identidad —un correo, un id de sujeto estable— y los dispositivos cuelgan de la identidad como hijos acotados y revocables. El desarrollador inicia sesión en tres máquinas; el servidor entrega a cada una un contrato de corta duración acotado al dispositivo; el asiento se mantiene en uno. Este artículo recorre cómo modelar eso en Keyright: qué aspecto tiene el derecho de identidad firmado, cómo un inicio de sesión se convierte en un contrato de dispositivo, cómo se impone el límite de dispositivos sin atar a nadie a una sola caja, y cómo un usuario retira un dispositivo obsoleto de un asiento lleno.

Dos anclas, no una

El cambio mental es dejar de pensar en “la licencia” como un único blob firmado vinculado al hardware, y empezar a pensar en dos artefactos firmados con vidas distintas. El derecho está vinculado a la identidad del usuario y vive mientras dure la suscripción. El contrato de dispositivo está vinculado a un dispositivo bajo esa identidad, es de corta duración, y es lo que la aplicación en ejecución verifica realmente en cada arranque.

derecho (usuario)sub=alex@acme.dev · cap=3vive durante la suscripcióncontrato de dispositivodev=laptop-9f3 · 12hfirmado, id en los claimscontrato de dispositivodev=desk-21a · 12hfirmado, id en los claimscontrato de dispositivodev=ci-7b2 · 12hfirmado, id en los claims4.º dispositivo → rechazadolímite lleno hasta liberar ranura

El derecho es lo que vendes y renuevas. El contrato de dispositivo es una credencial derivada y desechable. Como el contrato es de corta duración y está firmado con el id del dispositivo dentro, obtienes la revocación casi gratis: deja de emitir contratos para un dispositivo y dejará de funcionar en la siguiente renovación, sin tener que meter mano en el cliente en ejecución.

El derecho de identidad firmado

Keyright firma una carga canónica; para el licenciamiento por usuario nombrado el ancla de la carga es el sujeto (sub) en lugar de una huella de máquina. Una carga de derecho mínima tiene este aspecto:

public sealed record Entitlement
{
    public required string Sub { get; init; }        // stable user id, e.g. "usr_7Yq..." (not the raw email)
    public required string Product { get; init; }     // "acme-tool"
    public required string Tier { get; init; }         // "Pro"
    public required int DeviceCap { get; init; }        // seats-per-user: 3
    public required DateTimeOffset NotAfter { get; init; } // subscription end
    public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
}

Dos decisiones deliberadas. Primera, Sub es un id opaco y estable, no el correo en bruto. Los correos cambian; un id de sujeto no, y mantener el correo fuera del claim firmado significa que no reemites derechos cada vez que alguien cambia de dirección. Segunda, DeviceCap vive en el derecho firmado, así que el límite viaja con la licencia y no se puede editar en el cliente. El servidor lee el límite del derecho verificado cuando decide si emitir otro contrato de dispositivo.

El contrato de dispositivo es una segunda carga, firmada por separado, que referencia el derecho:

public sealed record DeviceLease
{
    public required string Sub { get; init; }        // same subject as the entitlement
    public required string Device { get; init; }      // device id derived from the fingerprint
    public required string Product { get; init; }
    public required string Tier { get; init; }
    public required DateTimeOffset NotAfter { get; init; } // SHORT: hours/days
    public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
}

La aplicación en ejecución solo verifica un DeviceLease. Nunca ve el plazo de la suscripción directamente; ve una caducidad corta que la obliga a volver al servidor periódicamente, donde se reverifica el derecho de larga duración.

Convertir un inicio de sesión en un contrato de dispositivo

El dispositivo calcula una huella (el mismo material de identidad de máquina que usarías para node-locking), la convierte por hash en un id de dispositivo compacto y pide un contrato al servidor. La petición lleva la identidad autenticada del usuario y el id del dispositivo; la respuesta es un DeviceLease firmado o un rechazo.

public async Task<LeaseResult> RequestLeaseAsync(
    AuthenticatedUser user, CancellationToken ct)
{
    // Device id is a stable, non-reversible hash of the fingerprint inputs.
    string deviceId = DeviceId.Compute(MachineFingerprint.Collect());

    var resp = await _client.PostAsJsonAsync("v1/lease", new LeaseRequest
    {
        Product = "acme-tool",
        Device  = deviceId
    }, ct); // user identity travels on the authenticated channel, not in the body

    if (resp.StatusCode == HttpStatusCode.Conflict)
    {
        // Cap is full: the server tells us which devices hold the slots
        // so the user can choose one to release.
        var full = await resp.Content.ReadFromJsonAsync<SeatFullResponse>(ct);
        return LeaseResult.SeatFull(full!.Devices);
    }

    resp.EnsureSuccessStatusCode();
    var lease = await resp.Content.ReadFromJsonAsync<SignedLease>(ct);
    return LeaseResult.Issued(lease!);
}

En el lado del servidor, el endpoint del contrato es donde se encuentran las dos anclas. Resuelve el derecho para el sujeto autenticado, comprueba que la suscripción está activa, y luego impone el límite de dispositivos contando dispositivos registrados distintos, reemitiendo para un dispositivo que ya tiene una ranura en lugar de consumir una nueva:

public async Task<IResult> IssueLease(LeaseRequest req, ClaimsPrincipal caller)
{
    string sub = caller.RequireSubject();
    Entitlement ent = await _entitlements.GetActiveAsync(sub, req.Product)
        ?? return Results.Forbid(); // no live subscription → no lease

    // A device that already holds a slot just gets a fresh lease (idempotent).
    DeviceRegistration? existing = await _devices.FindAsync(sub, req.Product, req.Device);
    if (existing is null)
    {
        int inUse = await _devices.CountAsync(sub, req.Product);
        if (inUse >= ent.DeviceCap)
            return Results.Conflict(await _devices.DescribeAsync(sub, req.Product));

        await _devices.RegisterAsync(sub, req.Product, req.Device, DateTimeOffset.UtcNow);
    }
    else
    {
        await _devices.TouchAsync(existing.Id, DateTimeOffset.UtcNow); // last-seen
    }

    DeviceLease lease = new()
    {
        Sub      = sub,
        Device   = req.Device,
        Product  = ent.Product,
        Tier     = ent.Tier,
        Features = ent.Features,
        NotAfter = DateTimeOffset.UtcNow.AddHours(12)   // short, independent clock
    };

    return Results.Ok(_signer.Sign(lease)); // Keyright canonical-payload signing
}

De esto se desprenden dos propiedades. El endpoint es idempotente por dispositivo: un dispositivo que vuelve reemite en lugar de consumir una ranura, así que renovar un contrato nunca cuesta un asiento. Y el límite se impone contra el DeviceCap firmado, leído del derecho que el servidor resolvió: el cliente nunca tiene voto sobre cuántos dispositivos puede registrar.

Qué verifica el cliente

La aplicación verifica el contrato de dispositivo igual que verificaría cualquier licencia de Keyright: recupera la clave pública del firmante, verifica la firma sobre los bytes canónicos y solo entonces lee los claims. El único paso específico de usuario nombrado es que comprueba el id de dispositivo del contrato contra este dispositivo, de modo que un contrato emitido para una máquina no se pueda reproducir en otra ni siquiera bajo la misma cuenta:

public LicenseState Evaluate(SignedLease signed)
{
    if (!_verifier.TryVerify(signed, out DeviceLease lease))
        return LicenseState.Invalid("signature");

    // The lease is bound to a device; confirm it is *this* device.
    string thisDevice = DeviceId.Compute(MachineFingerprint.Collect());
    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(lease.Device),
            Encoding.UTF8.GetBytes(thisDevice)))
        return LicenseState.Invalid("device-mismatch");

    if (lease.NotAfter <= DateTimeOffset.UtcNow)
        return LicenseState.Expired; // refresh against the server

    return LicenseState.Active(lease.Tier, lease.Features);
}

La caducidad aquí es la del contrato de dispositivo —el reloj corto—. Cuando caduca, el cliente se renueva; la renovación vuelve a ejecutar la comprobación de derecho del servidor, así que una suscripción caducada o un dispositivo eliminado produce un rechazo y la aplicación cae a su estado sin licencia en el siguiente límite. Nunca tienes que empujar una señal de apagado a un cliente en ejecución; el contrato corto es el interruptor de apagado, y el silencio del servidor es lo que lo dispara.

Retirar un dispositivo de un asiento lleno

Un límite de dispositivos solo es humano si el usuario puede liberar una ranura sin un ticket de soporte. Como cada contrato actualiza una marca de tiempo de última vez visto, la respuesta de asiento lleno puede listar los dispositivos del usuario con suficiente contexto para elegir cuál descartar:

public async Task<IResult> ReleaseDevice(string device, ClaimsPrincipal caller)
{
    string sub = caller.RequireSubject();

    // Cooldown stops a shared account from churning devices to fake extra seats.
    if (await _devices.RemovedRecentlyAsync(sub, TimeSpan.FromMinutes(30)))
        return Results.StatusCode(StatusCodes.Status429TooManyRequests);

    await _devices.RemoveAsync(sub, "acme-tool", device);
    return Results.NoContent();
}

El periodo de enfriamiento es el detalle que mantiene significativo el límite. Sin él, una cuenta compartida podría eliminar y volver a añadir dispositivos cada pocos segundos y ejecutar máquinas ilimitadas de hecho; con un mínimo de 30 minutos en las eliminaciones, el límite se convierte en un techo real y compartir cuenta perjudica a quien comparte —su propia siguiente máquina queda bloqueada— mucho más que a ti. El contrato actual del dispositivo liberado sigue funcionando hasta su corta caducidad, y luego no se puede renovar porque su ranura desapareció. Sin corte brusco, sin asiento atascado.

Qué llevarse

El node-locking pregunta “¿qué máquina es esta?” y esa es la pregunta correcta para software vendido por caja. El licenciamiento por usuario nombrado pregunta “¿de quién es este asiento?”, la pregunta correcta cuando vendes a personas que legítimamente trabajan en varias máquinas. Ancla el derecho a un id de sujeto estable con el límite de dispositivos dentro de los claims firmados, deriva contratos de dispositivo de corta duración que lleven el id del dispositivo para que no se puedan reproducir, e impón el límite en el servidor contando dispositivos registrados distintos con reemisión idempotente para que una renovación nunca cueste un asiento. Da a los usuarios una retirada de dispositivos de autoservicio con un periodo de enfriamiento, y deja que el contrato corto sea tu mecanismo de revocación para no tener que meter mano en un cliente en ejecución. Haz eso y un desarrollador con un portátil, un escritorio y un agente de compilación es exactamente lo que pagó por ser: un asiento. Para la mitad de identidad de dispositivo de esto —cómo se construye la huella en sí— consulta huella de máquina y vinculación de dispositivo, y para la mecánica de carga firmada bajo ambas anclas, la anatomía de una clave de licencia firmada.

Prueba Nebula.NET

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