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

Registrar una comprobación de licencia: eventos estructurados que un operador puede leer, y nunca la clave en sí

La comprobación de licencia es el único evento que decide si tu software tiene permiso para ejecutarse, y casi siempre se registra o bien como nada en absoluto o bien como un desastre: un log.LogInformation("license: {License}", info) que serializa el objeto completo e imprime la clave firmada, la firma y el correo del licenciatario en un fichero que se envía a un agregador de logs y se comparte en un ticket de soporte. Esta entrada construye el camino intermedio: un único evento estructurado por verificación, emitido a través de un LoggerMessage generado en compilación para que la regla de redacción viva exactamente en un sitio, con la clave reducida a una huella corta que permite a un operador correlacionar sin transportar nunca el secreto. Mapea el LicenseStatus de Keyright sobre niveles de log de modo que una licencia inválida sea un Error sobre el que puedas alertar y una sana sea una Information silenciosa, asigna un rango de EventId estable para que un panel pueda contar fallos de licencia sin comparar cadenas, envuelve la verificación en un scope de logging para que cada línea posterior lleve la huella, mantiene el evento fuera de la ruta caliente por petición, y termina con la prueba —una aserción con FakeLogger— que demuestra que la clave en bruto nunca llega al sink.

Una comprobación de licencia es el único evento que decide si tu software tiene permiso para ejecutarse esta noche, y casi siempre se registra de una de dos formas incorrectas. O bien se registra como nada —la verificación ocurre, tiene éxito o falla en silencio, y la primera noticia que alguien tiene de una clave vencida es una queja de un usuario—, o bien se registra como un desastre: log.LogInformation("license: {@License}", info), que serializa el objeto completo y escribe la clave firmada, la firma en bruto y el correo del licenciatario en un fichero que será enviado a un agregador y pegado en un ticket de soporte antes de la hora de comer.

Esta entrada construye la versión intermedia: un evento estructurado y consultable por verificación, emitido de modo que un operador pueda leerlo, alertar sobre él y correlacionarlo, y de modo que la clave en sí nunca salga del proceso. Trata de registrar una verificación de Keyright como registrarías un evento de autenticación, porque eso es lo que es.

Lo que estás verificando

El Validate() de Keyright devuelve un LicenseInfo con un Status, un Licensee, una caducidad y los derivados DaysRemaining/IsExpiringSoon en los que ya se apoyan el trabajo de renovación y el health check:

LicenseInfo info = client.Validate(licenseKey);
// info.Status        -> LicenseStatus (Valid, Expired, Revoked, SignatureInvalid, ...)
// info.Licensee      -> "Acme GmbH"
// info.ExpiryUtc     -> DateTime?
// info.DaysRemaining -> int
// info.IsExpiringSoon-> bool

El licenseKey de esa llamada es lo que nunca debe llegar al log. Todo lo demás en info es legítimo de registrar; la clave y la firma no. Así que la primera decisión de diseño es calcular una huella de la clave una sola vez, y pasar eso al logger, nunca la clave.

using System.Security.Cryptography;
using System.Text;

static string Fingerprint(string licenseKey)
{
    // A stable, non-reversible handle: first 8 hex of SHA-256 over the key.
    // Enough to say "same key as the one in the ticket?" — not enough to be the key.
    Span<byte> hash = stackalloc byte[32];
    SHA256.HashData(Encoding.UTF8.GetBytes(licenseKey), hash);
    return Convert.ToHexString(hash)[..8].ToLowerInvariant();   // e.g. "9f3a1c20"
}

Si tu payload lleva un claim key-id, prefiérelo: ya es un identificador no secreto. La huella es el recurso para las claves que no lo llevan.

Un evento, declarado una vez

La regla de redacción solo es tan buena como el número de sitios en los que tiene que cumplirse. Reparte el logging entre una docena de puntos de llamada log.LogInformation("...") y el que pasa licenseKey en lugar de fingerprint es la fuga. Así que declara el evento una vez, como un LoggerMessage generado en compilación, cuya firma hace imposible pasar la clave en bruto:

using Microsoft.Extensions.Logging;

public static partial class LicenseLog
{
    // Note the parameters: fingerprint, status, licensee, days — never the key.
    [LoggerMessage(
        EventId = 4000, Level = LogLevel.Information,
        Message = "License verified: {Status} for {Licensee} (key {Fingerprint}), {DaysRemaining} day(s) remaining")]
    public static partial void Verified(
        this ILogger logger,
        string status, string licensee, string fingerprint, int daysRemaining);

    [LoggerMessage(
        EventId = 4001, Level = LogLevel.Warning,
        Message = "License expiring: {Status} for {Licensee} (key {Fingerprint}) expires in {DaysRemaining} day(s)")]
    public static partial void Expiring(
        this ILogger logger,
        string status, string licensee, string fingerprint, int daysRemaining);

    [LoggerMessage(
        EventId = 4002, Level = LogLevel.Error,
        Message = "License invalid: {Status} for {Licensee} (key {Fingerprint})")]
    public static partial void Invalid(
        this ILogger logger,
        string status, string licensee, string fingerprint);
}

El generador de código convierte cada uno de estos en un método sin asignaciones que comprueba el nivel, escribe los campos con nombre y formatea el mensaje solo si un sink lo quiere. No hay ningún parámetro licenseKey en ninguno de ellos, así que un llamador no puede registrar la clave a través de este tipo ni por accidente: el compilador se lo impide. Esa es toda la razón para preferir el generador frente a una plantilla libre: la regla es ahora una firma de método, no una convención de revisión de código.

Mapear el estado al nivel

El despacho de LicenseStatus a uno de esos tres eventos es la tabla de traducción, y encaja con cómo funcionan ya las alertas: Information es silencioso, Warning se nota, Error se cuenta y se avisa:

public static void LogLicense(this ILogger logger, LicenseInfo info, string fingerprint)
{
    string status   = info.Status.ToString();
    string licensee  = info.Licensee ?? "(none)";

    switch (info.Status)
    {
        case LicenseStatus.Valid when info.IsExpiringSoon:
            logger.Expiring(status, licensee, fingerprint, info.DaysRemaining);
            break;
        case LicenseStatus.Valid:
            logger.Verified(status, licensee, fingerprint, info.DaysRemaining);
            break;
        default:   // Expired, Revoked, SignatureInvalid, MachineMismatch, ClockTampered, NoLicense
            logger.Invalid(status, licensee, fingerprint);
            break;
    }
}
LicenseInfo → redactar → evento estructuradoLicenseInfoStatusLicenseeDaysRemainingclave (secreto portador)bytes de la firmafrontera de redacciónSHA-256→ 9f3a1c204000 · Information · verificadaStatus Valid → línea base silenciosa4001 · Warning · a punto de caducarValid + IsExpiringSoon4002 · Error · inválidaExpired · Revoked · SignatureInvalidMachineMismatch · ClockTamperedLa clave en bruto nunca cruza la frontera: solo se emite la huella de 8 caracteres.

Un sink de consola JSON renderiza entonces el caso sano así; fíjate en que la clave no está en ninguna parte, solo su huella:

{
  "Timestamp": "2026-10-16T09:00:01.42Z",
  "LogLevel": "Information",
  "EventId": 4000,
  "Message": "License verified: Valid for Acme GmbH (key 9f3a1c20), 48 day(s) remaining",
  "State": { "Status": "Valid", "Licensee": "Acme GmbH", "Fingerprint": "9f3a1c20", "DaysRemaining": 48 }
}

Como Status, Fingerprint y DaysRemaining son campos estructurados con nombre y no simple texto incrustado en el mensaje, un backend de logs puede graficar «huellas distintas vistas esta semana» o alertar sobre «recuento de EventId 4002 > 0» sin parsear la cadena del mensaje. Ese es el dividendo de la forma estructurada: el evento es consultable, no solo legible.

Un scope para que el resto de los logs se correlacionen

La verificación es una línea, pero el trabajo que la sigue —una renovación de lease en segundo plano, una comprobación de entitlement, una puerta de funcionalidad— produce sus propios logs, y quien revisa un incidente quiere vincularlos a la licencia que estaba en vigor. Abre un scope de logging indexado por la huella alrededor de ese trabajo, y cada línea dentro de él lleva el campo automáticamente:

using (logger.BeginScope(new Dictionary<string, object> { ["LicenseKey"] = fingerprint }))
{
    logger.LogLicense(info, fingerprint);
    // ... anything logged in here also carries LicenseKey=9f3a1c20 ...
}

Ahora un SignatureInvalid a las 03:00 y una comprobación de entitlement fallida a las 03:01 comparten un campo LicenseKey, y el revisor puede extraer toda la historia con un único filtro. El scope lleva la huella, nunca la clave: la misma regla, aplicada al identificador de correlación.

Demostrar que la clave nunca escapa

La única aserción que merece la pena escribir es la negativa: que la clave en bruto no aparece en nada de lo que el logger emite. Microsoft.Extensions.Diagnostics.Testing incluye un FakeLogger que captura los registros en memoria, así que la prueba es directa:

[Fact]
public void Verification_never_logs_the_raw_key()
{
    var fake = new FakeLogger<LicenseService>();
    const string key = "KR1-REALKEY-abcdef ...";          // the actual signed key
    var info = new LicenseInfo { Status = LicenseStatus.Valid, Licensee = "Acme", DaysRemaining = 48 };

    fake.LogLicense(info, Fingerprint(key));

    foreach (FakeLogRecord rec in fake.Collector.GetSnapshot())
    {
        Assert.DoesNotContain(key, rec.Message);
        foreach (var kv in rec.StructuredState ?? [])
            Assert.DoesNotContain(key, kv.Value ?? "");    // check the structured fields too
    }
}

Haz la aserción tanto sobre el mensaje renderizado como sobre el estado estructurado, porque una fuga puede esconderse en cualquiera de los dos: un {Key} extraviado en una plantilla aparece en el mensaje, un BeginScope que capturó la clave aparece en el estado. Esta prueba es lo que mantiene la regla cierta a medida que el código crece: el día en que alguien añada un cuarto evento que pase la clave, se pone en rojo.

Conclusión

Una comprobación de licencia merece registrarse como el evento de autenticación que es: un registro estructurado, no silencio y no un volcado de datos. Declara el evento una vez como un LoggerMessage generado en compilación cuyos parámetros son una huella y los campos no secretos, de modo que la clave en bruto sea imposible de pasar por construcción. Mapea LicenseStatus sobre niveles que tus alertas existentes ya entienden —Information para válida, Warning para a punto de caducar, Error para inválida— y da a cada uno un EventId estable para que un panel cuente los fallos sin comparar cadenas. Emítelo en la verificación y en las transiciones, no en cada petición. Envuelve el trabajo en un scope indexado por la huella para que el resto de los logs se correlacionen. Después escribe la única prueba que demuestra que la clave nunca llega al sink, y deja que custodie la regla para siempre. El resultado es un log que un operador puede leer, que un backend puede consultar y que un ticket de soporte puede transportar con seguridad, porque la única cosa que no debe viajar nunca pasó de la primera línea.

Prueba Nebula.NET

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