Eine Lizenzprüfung loggen: strukturierte Events, die ein Operator lesen kann – und nie der Schlüssel selbst
Die Lizenzprüfung ist das eine Ereignis, das entscheidet, ob Ihre Software laufen darf, und sie wird fast immer entweder gar nicht geloggt oder als Desaster – ein log.LogInformation("license: {License}", info), das das ganze Objekt serialisiert und den signierten Schlüssel, die Signatur und die E-Mail-Adresse des Lizenznehmers in eine Datei schreibt, die an einen Log-Aggregator verschickt und in einem Support-Ticket geteilt wird. Dieser Beitrag baut den Mittelweg: ein einziges strukturiertes Event pro Verifikation, ausgegeben über eine Source-generierte LoggerMessage, damit die Redaktionsregel an genau einer Stelle lebt, mit dem Schlüssel reduziert auf einen kurzen Fingerprint, der einem Operator das Korrelieren erlaubt, ohne je das Geheimnis mitzuführen. Er bildet den LicenseStatus von Keyright auf Log-Level ab, sodass eine ungültige Lizenz ein Error ist, auf den Sie alarmieren können, und eine gesunde Lizenz stille Information bleibt, vergibt einen stabilen EventId-Bereich, damit ein Dashboard Lizenzfehler ohne String-Matching zählen kann, legt einen Logging-Scope um die Verifikation, damit jede nachgelagerte Zeile den Fingerprint trägt, hält das Event vom Hot Path pro Request fern und endet mit dem Test – einer FakeLogger-Assertion –, der beweist, dass der rohe Schlüssel nie die Senke erreicht.
Eine Lizenzprüfung ist das eine Ereignis, das entscheidet, ob Ihre Software heute Nacht laufen darf, und sie wird fast immer auf eine von zwei falschen Arten geloggt. Entweder wird sie als nichts geloggt – die Verifikation findet statt, gelingt oder scheitert stumm, und das Erste, was irgendjemand von einem verfallenen Schlüssel hört, ist eine Nutzerbeschwerde – oder sie wird als Desaster geloggt: log.LogInformation("license: {@License}", info), das das ganze Objekt serialisiert und den signierten Schlüssel, die rohe Signatur und die E-Mail-Adresse des Lizenznehmers in eine Datei schreibt, die an einen Aggregator verschickt und bis zur Mittagspause in ein Support-Ticket eingefügt wird.
Dieser Beitrag baut die Version dazwischen: ein strukturiertes, abfragbares Event pro Verifikation, so ausgegeben, dass ein Operator es lesen, darauf alarmieren und es korrelieren kann – und dass der Schlüssel selbst nie den Prozess verlässt. Es geht darum, eine Keyright-Verifikation so zu loggen, wie Sie ein Auth-Event loggen würden, denn genau das ist sie.
Das, was Sie verifizieren
Keyrights Validate() liefert eine LicenseInfo mit einem Status, einem Licensee, einem Ablaufdatum und den abgeleiteten Werten DaysRemaining/IsExpiringSoon, auf die sich die Verlängerungs- und Health-Check-Arbeit bereits stützt:
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
Der licenseKey in diesem Aufruf ist das, was das Log niemals erreichen darf. Alles andere auf info darf aufgezeichnet werden; der Schlüssel und die Signatur nicht. Die erste Designentscheidung ist also, einmal einen Fingerprint des Schlüssels zu berechnen und den an den Logger zu übergeben – nie den Schlüssel.
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"
}
Wenn Ihre Payload einen Key-ID-Claim trägt, bevorzugen Sie den – er ist bereits ein nicht-geheimes Handle. Der Fingerprint ist der Rückfall für Schlüssel, die keinen haben.
Ein Event, einmal deklariert
Die Redaktionsregel ist nur so gut wie die Zahl der Stellen, an denen sie gelten muss. Verteilen Sie das Logging über ein Dutzend log.LogInformation("...")-Aufrufstellen, und die eine, die licenseKey statt fingerprint übergibt, ist das Leck. Deklarieren Sie das Event also einmal, als Source-generierte LoggerMessage, deren Signatur den rohen Schlüssel unübergebbar macht:
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);
}
Der Source-Generator macht aus jeder dieser Deklarationen eine allokationsfreie Methode, die das Level prüft, die benannten Felder schreibt und die Nachricht nur formatiert, wenn eine Senke sie haben will. Keine davon hat einen licenseKey-Parameter, also kann ein Aufrufer den Schlüssel über diesen Typ nicht einmal versehentlich loggen – der Compiler hält ihn auf. Das ist der ganze Grund, den Generator einem Freiform-Template vorzuziehen: Die Regel ist jetzt eine Methodensignatur, keine Code-Review-Konvention.
Status auf Level abbilden
Die Verteilung von LicenseStatus auf eines dieser drei Events ist die Übersetzungstabelle, und sie deckt sich mit der Art, wie Alerting ohnehin funktioniert – Information ist still, Warning wird bemerkt, Error wird gezählt und gepagt:
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;
}
}
Eine JSON-Konsolensenke rendert den gesunden Fall dann so – beachten Sie, dass der Schlüssel nirgends darin vorkommt, nur sein Fingerprint:
{
"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 }
}
Weil Status, Fingerprint und DaysRemaining benannte strukturierte Felder sind und nicht bloß Text, der in die Nachricht eingebacken ist, kann ein Log-Backend „diese Woche gesehene unterschiedliche Fingerprints“ grafisch darstellen oder auf „EventId 4002 count > 0“ alarmieren, ohne den Nachrichten-String zu parsen. Das ist die Dividende der strukturierten Form: Das Event ist abfragbar, nicht nur lesbar.
Ein Scope, damit der Rest der Logs korreliert
Die Verifikation ist eine Zeile, aber die Arbeit, die ihr folgt – eine Lease-Verlängerung im Hintergrund, eine Entitlement-Prüfung, ein Feature-Gate – produziert ihre eigenen Logs, und ein Incident-Reviewer will sie mit der Lizenz verknüpfen, die gerade in Kraft war. Öffnen Sie um diese Arbeit einen Logging-Scope, der mit dem Fingerprint verschlüsselt ist, und jede Zeile darin trägt das Feld automatisch:
using (logger.BeginScope(new Dictionary<string, object> { ["LicenseKey"] = fingerprint }))
{
logger.LogLicense(info, fingerprint);
// ... anything logged in here also carries LicenseKey=9f3a1c20 ...
}
Jetzt teilen sich ein SignatureInvalid um 03:00 und eine fehlgeschlagene Entitlement-Prüfung um 03:01 ein LicenseKey-Feld, und der Reviewer kann die ganze Geschichte mit einem einzigen Filter herausziehen. Der Scope trägt den Fingerprint, nie den Schlüssel – dieselbe Regel, angewandt auf das Korrelations-Handle.
Beweisen, dass der Schlüssel nie entkommt
Die eine Assertion, die es wert ist, geschrieben zu werden, ist die negative: dass der rohe Schlüssel in nichts auftaucht, was der Logger ausgibt. Microsoft.Extensions.Diagnostics.Testing liefert einen FakeLogger, der Einträge im Speicher erfasst, also ist der Test direkt:
[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
}
}
Prüfen Sie sowohl die gerenderte Nachricht als auch den strukturierten State, denn ein Leck kann sich in beidem verstecken – ein verirrtes {Key} in einem Template taucht in der Nachricht auf, ein BeginScope, das den Schlüssel erfasst hat, taucht im State auf. Dieser Test ist das, was die Regel wahr hält, während der Code wächst: An dem Tag, an dem jemand ein viertes Event hinzufügt, das den Schlüssel übergibt, wird er rot.
Das Fazit
Eine Lizenzprüfung verdient es, wie das Auth-Event geloggt zu werden, das sie ist: ein strukturierter Eintrag, nicht Schweigen und kein Daten-Dump. Deklarieren Sie das Event einmal als Source-generierte LoggerMessage, deren Parameter ein Fingerprint und die nicht-geheimen Felder sind, sodass der rohe Schlüssel per Konstruktion unübergebbar ist. Bilden Sie LicenseStatus auf Level ab, die Ihr bestehendes Alerting bereits versteht – Information für gültig, Warning für bald ablaufend, Error für ungültig – und geben Sie jedem eine stabile EventId, damit ein Dashboard Fehler ohne String-Matching zählt. Geben Sie es bei der Verifikation und bei Übergängen aus, nicht bei jedem Request. Legen Sie um die Arbeit einen Scope mit dem Fingerprint als Schlüssel, damit der Rest der Logs korreliert. Schreiben Sie dann den einen Test, der beweist, dass der Schlüssel nie die Senke erreicht, und lassen Sie ihn die Regel für immer bewachen. Das Ergebnis ist ein Log, das ein Operator lesen, ein Backend abfragen und ein Support-Ticket gefahrlos tragen kann – weil das eine, das nicht reisen darf, es nie über die erste Zeile hinaus geschafft hat.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.