Skip to content
← Alle Beiträge
· Delta1 Labs KeyrightLizenzierung.NETTiefenanalyse

Standortlizenzen: einen Keyright-Schlüssel an die Domäne des Kunden statt an eine Maschine binden – und was diese Bindung beweisen kann und was nicht

Ein Unternehmen, das vierhundert Arbeitsplätze kauft, will keine vierhundert Aktivierungen, und Sie wollen nicht, dass der Schlüssel, der sie alle freischaltet, auf dem Laptop eines Auftragnehmers das Haus verlässt. Die klassische Antwort ist eine Standortlizenz — ein Schlüssel für die ganze Organisation — und die Frage, die niemand aufschreibt, lautet: Woran ist der Schlüssel gebunden, wenn nicht an eine Maschine? Dieser Beitrag baut eine Standortlizenz auf Keyright als signierte Berechtigung, die die DNS-Domäne des Kunden nennt, optional durch die Windows-Domänen-SID fixiert: woher diese beiden Identitäten stammen (IPGlobalProperties.DomainName, nicht Environment.UserDomainName, und SecurityIdentifier.AccountDomainSid), warum der Suffix-Vergleich der richtige Vergleich für eine Gesamtstruktur mit untergeordneten Domänen ist, wie das Gate auf einem nicht beigetretenen Laptop geschlossen fehlschlägt, wie der Kunde Ihnen die Werte ohne Umweg über die IT liefert, und das ehrliche Bedrohungsmodell: ein Domänenname ist ein Richtlinienzaun, den ein entschlossener Angreifer durch Umbenennen einer VM umgehen kann, eine Domänen-SID hebt die Hürde bis zum Klonen eines Domänencontrollers, und keines von beiden ersetzt eine Platzzählung, wenn Sie wirklich eine brauchen.

Ein Kunde mit vierhundert Desktops will keine vierhundert Maschinen aktivieren, und sein IT-Team wird Ihnen das sagen, bevor die Bestellung unterschrieben ist. Die Antwort, bei der jeder Anbieter landet, ist eine Standortlizenz: ein Schlüssel für die ganze Organisation, keine Zeremonie pro Maschine. Die Frage, die selten aufgeschrieben wird, ist, woran ein solcher Schlüssel gebunden ist. Ein knotengebundener Schlüssel ist an einen Maschinen-Fingerabdruck gebunden. Ein Floating-Schlüssel ist an eine Platzzählung auf einem Server gebunden. Ein Standortschlüssel ohne jede Bindung ist eine Datei, die Ihr Produkt für jeden freischaltet, der sie hat — und Dateien reisen.

Dieser Beitrag baut eine Standortlizenz auf Keyright, die an etwas gebunden ist, das die ganze Organisation teilt und ein Fremder nicht hat: die Verzeichnisdomäne. Es ist eine signierte Berechtigung, kein neuer Lizenztyp; das Gate hat zwanzig Zeilen; und das Bedrohungsmodell wird ehrlich benannt, weil ein Domänenname ein Zaun ist, kein Schloss.

Was der Schlüssel sagt

Eine knotengebundene Keyright-Lizenz trägt eine machineId, und der Validator verweigert die Ausführung auf jeder Maschine, deren Fingerabdruck nicht passt. Eine Standortlizenz lässt machineId leer und nennt stattdessen den Standort in der signierten Berechtigungsmenge:

{
  "product":  "acme-studio",
  "licensee": "Contoso Ltd",
  "expiryUtc": "2027-10-17T00:00:00Z",
  "tier":     "pro",
  "entitlements": {
    "site:domain":     "corp.contoso.example",
    "site:domain-sid": "S-1-5-21-3623811015-3361044348-30300820",
    "export":          "true",
    "max-projects":    "unlimited"
  },
  "signature": "…"
}

Zwei Dinge zählen hier. Die site:-Einträge sind gewöhnliche Berechtigungen — String-Werte innerhalb der kanonischen Nutzlast, die die Signatur abdeckt — ein Kunde kann corp.contoso.example also genauso wenig in home.example ändern, wie er das Ablaufdatum um ein Jahr verschieben kann: ein geändertes Byte, und die App sieht LicenseStatus.SignatureInvalid. Und das Präfix ist eine Konvention, kein Keyright-Feature: es hält wo dieser Schlüssel laufen darf sichtbar getrennt von was dieser Schlüssel freischaltet, damit später niemand ein Feature an einen Standort-Claim knüpft oder umgekehrt.

site:domain-sid ist optional. Die DNS-Domäne ist die portable, plattformübergreifende Bindung; die SID ist die Windows-spezifische Härtung, die Sie hinzufügen, wenn der Kunde oder der Vertragswert es rechtfertigt. Was jede davon tatsächlich beweist, kommt weiter unten.

Woher die Identität der Maschine stammt

Die App muss „in welcher Domäne bin ich?” von der Maschine selbst beantworten, offline, beim Start. Es gibt zwei verlockende APIs, und eine davon ist die falsche.

Environment.UserDomainName liefert die NetBIOS-Domäne des Benutzers, der den Prozess ausführt — CONTOSO — und auf einer Maschine, die keiner Domäne beigetreten ist, liefert es stattdessen den Maschinennamen. Das macht es für eine geschlossen fehlschlagende Prüfung unbrauchbar (man kann „nicht beigetreten” nicht von „einer Domäne beigetreten, die zufällig den Maschinennamen trägt” unterscheiden), und es ist flach, kann also nicht ausdrücken, dass emea.corp.contoso.example Teil von corp.contoso.example ist.

IPGlobalProperties.GetIPGlobalProperties().DomainName liefert das primäre DNS-Suffix des Hosts, das die Domänenmitgliedschaft setzt: emea.corp.contoso.example auf einer beigetretenen Workstation, ein leerer String auf einem Laptop, der nie beigetreten ist. Es ist hierarchisch, es ist plattformübergreifend (unter Linux liest es den konfigurierten Domänennamen des Hosts), und der Fall des leeren Strings ist genau das Signal, das Sie wollen.

Für die SID liefert WindowsIdentity.GetCurrent().User.AccountDomainSid die SID der Domäne, zu der das aktuelle Konto gehört — S-1-5-21-… mit abgeschnittenem letzten relativen Bezeichner. Für einen Domänenbenutzer auf einer beigetretenen Maschine ist das die Domänen-SID der Gesamtstruktur. Für ein lokales Konto ist es die SID der lokalen Maschine, die nicht passen wird und nicht passen soll.

using System.Net.NetworkInformation;
using System.Security.Principal;

public interface ISiteIdentity
{
    string? DnsDomain { get; }   // "emea.corp.contoso.example"; null when not joined
    string? DomainSid { get; }   // "S-1-5-21-…"; null off Windows or for a local account
}

public sealed class RealSiteIdentity : ISiteIdentity
{
    public string? DnsDomain
    {
        get
        {
            try
            {
                var d = IPGlobalProperties.GetIPGlobalProperties().DomainName;
                return string.IsNullOrWhiteSpace(d) ? null : d.TrimEnd('.').ToLowerInvariant();
            }
            catch { return null; }                       // no network stack info → treat as unjoined
        }
    }

    public string? DomainSid
    {
        get
        {
            if (!OperatingSystem.IsWindows()) return null;
            try
            {
                using var id = WindowsIdentity.GetCurrent();
                return id.User?.AccountDomainSid?.Value;  // null for well-known/non-domain SIDs
            }
            catch { return null; }
        }
    }
}

Jeder Fehlerpfad liefert null, und null bedeutet „nicht beigetreten”. Das ist Absicht: das Gate unten behandelt eine unbekannte Identität bei einem Standortschlüssel als Ablehnung, nicht als Freigabe.

Ein signierter Schlüssel, drei MaschinenStandortschlüssel (signiert)machineId: (none)site:domain = corp.contoso.examplesite:domain-sid = S-1-5-21-…beigetretene Workstationdns: emea.corp.contoso.examplesid: S-1-5-21-… (passt)Auftragnehmer-Laptop zu Hausedns: (leer) → nicht beigetretensid: Maschinen-SIDVM in gefälschter Gesamtstrukturdns: corp.contoso.example ✓sid: S-1-5-21-(anders) ✗läuftblockiert:nicht beigetr.blockiert:falsche SIDDer Name hält Dateien vom Reisen ab; die SID stoppt umbenannte Gesamtstrukturen. Keines zählt Plätze — dafür Aktivierung nutzen.

Das Gate

Das Gate läuft nach Validate() und tut nur etwas, wenn der Schlüssel einen Standort-Claim trägt. Der Abgleich der DNS-Domäne ist ein Suffix-Vergleich an Label-Grenzen: ein für corp.contoso.example ausgestellter Schlüssel muss eine Workstation in der untergeordneten Domäne emea.corp.contoso.example akzeptieren, darf aber notcorp.contoso.example nicht akzeptieren — daher der führende Punkt im Vergleich.

public enum SiteStatus { Ok, NotSiteLicensed, NotJoined, WrongDomain, WrongDomainSid }

public static class SiteGate
{
    public static SiteStatus Check(LicenseInfo info, ISiteIdentity site)
    {
        var claim = info.Entitlements.GetRaw("site:domain");
        if (string.IsNullOrWhiteSpace(claim))
            return SiteStatus.NotSiteLicensed;            // ordinary key: nothing to check here

        var here = site.DnsDomain;
        if (here is null)
            return SiteStatus.NotJoined;                  // fail closed: unknown ≠ allowed

        claim = claim.Trim().TrimEnd('.').ToLowerInvariant();
        bool inDomain = here == claim
                     || here.EndsWith("." + claim, StringComparison.Ordinal);
        if (!inDomain)
            return SiteStatus.WrongDomain;

        var sidClaim = info.Entitlements.GetRaw("site:domain-sid");
        if (!string.IsNullOrWhiteSpace(sidClaim)
            && !string.Equals(site.DomainSid, sidClaim.Trim(), StringComparison.OrdinalIgnoreCase))
            return SiteStatus.WrongDomainSid;

        return SiteStatus.Ok;
    }
}

Verdrahten Sie es beim Start, nach den Signatur- und Ablaufprüfungen, die das SDK bereits durchführt:

static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
    Product         = "acme-studio",
    PublicKeyBase64 = AcmeKeys.LicensePublicKey,        // public key only; safe to ship
});

LicenseInfo info = License.Validate();                  // signature, product, expiry, (no) node-lock
if (!info.IsValid) { RunEvaluation(info); return; }

var site = SiteGate.Check(info, new RealSiteIdentity());
if (site is not (SiteStatus.Ok or SiteStatus.NotSiteLicensed))
{
    RunEvaluation(info, reason: site);                  // "this key is for corp.contoso.example"
    return;
}

RunLicensed(info);

Beachten Sie, was das SDK nicht getan hat: ohne machineId hat Validate() nie einen Maschinen-Fingerabdruck berechnet, es gibt also keinen Zustand pro Maschine, nichts zu „aktivieren” und nichts, das driftet, wenn die IT einen Desktop neu aufsetzt. Vierhundert Maschinen, eine Datei, eine Prüfung.

Eine bewusste Reihenfolge: die Standortprüfung läuft nach IsValid. Ein Schlüssel mit ungültiger Signatur erreicht das Gate nie, sodass die Berechtigungen, die er trägt — einschließlich eines site:domain, das ein Angreifer eingetippt haben könnte — nie gelesen werden. Berechtigungen sind erst vertrauenswürdig, wenn die Signatur über ihnen verifiziert ist, und Keyright erzwingt das, indem es für alles, was nicht validiert hat, eine leere Berechtigungsmenge zurückgibt.

Die Werte vom Kunden bekommen

Die Schwachstelle jeder identitätsgebundenen Lizenz ist die Übergabe: der Kunde muss Ihnen sagen, was seine Domäne ist, und wenn er Ihnen etwas subtil anderes sagt als das, was die Maschine meldet (contoso.example statt corp.contoso.example, ein NetBIOS-Name statt eines DNS-Namens), scheitert der erste Start nach dem Kauf mit „falsche Domäne”, während alle Beteiligten beteuern, in der richtigen Domäne zu sein.

Schließen Sie diese Lücke, indem das Produkt seine eigene Antwort meldet, aus exakt dem Codepfad, den das Gate nutzt:

// yourapp --site-info
var id = new RealSiteIdentity();
Console.WriteLine($"DNS domain : {id.DnsDomain ?? "(not domain-joined)"}");
Console.WriteLine($"Domain SID : {id.DomainSid ?? "(n/a)"}");

Fragen Sie diese Ausgabe im Bestellformular ab. Keiner der Werte ist vertraulich — das DNS-Suffix steht in jeder E-Mail, die das Unternehmen verschickt, und die Domänen-SID kann jeder authentifizierte Benutzer der Domäne lesen — es ist also nicht sensibler, als nach einem Firmennamen zu fragen. Die Ausstellungsseite schreibt die beiden Strings in die Berechtigungen und signiert; was der Kunde eingefügt hat, ist per Konstruktion das, was die Prüfung sehen wird. (Stellen Sie für die übergeordnete Domäne aus, die der Kunde nennt, nicht für die untergeordnete, in der die Diagnose zufällig lief: ein Schlüssel für corp.contoso.example deckt EMEA und APAC ab; ein Schlüssel für emea.corp.contoso.example nicht.)

Was die Bindung beweist — ehrlich

Hier kommt der Teil, der in Ihr eigenes Bedrohungsmodell gehört, nicht nur auf die Marketingseite.

Ein DNS-Domänenname ist ein Richtlinienzaun. Er ist kein Geheimnis und keine kryptografische Identität. Jeder, der einen geleakten Standortschlüssel besitzt, kann eine Wegwerf-Gesamtstruktur namens corp.contoso.example aufsetzen, eine VM hinzufügen, und die Namensprüfung besteht. Was der Name sehr wohl stoppt, ist der mit Abstand häufigste Fehlerfall: der Mitarbeiter, der license.json auf einen Heim-PC kopiert, der Auftragnehmer, der sie nach dem Auftrag behält, die Datei, die auf einem freigegebenen Laufwerk landet. Diese Maschinen sind der Domäne des Kunden nicht beigetreten, das Gate schlägt geschlossen fehl, und das Produkt sagt ihnen genau, warum. Für die meisten Standortlizenzen — bei denen der Kunde eine bekannte Organisation mit Vertrag ist — ist das die ganze Aufgabe.

Eine Domänen-SID hebt die Hürde erheblich. Die SID einer Gesamtstruktur wird beim Anlegen der Gesamtstruktur zufällig erzeugt; es gibt kein unterstütztes Werkzeug, sie zu wählen, sodass das Reproduzieren von S-1-5-21-3623811015-3361044348-30300820 bedeutet, den tatsächlichen Domänencontroller des Kunden zu klonen, nicht einen Namen in einen Assistenten zu tippen. Ein Angreifer außerhalb der Organisation kann das nicht; ein Insider mit Domänenadministratorrechten könnte es, aber ein Insider mit Domänenadministratorrechten ist auch die Person, die der Vertrag des Kunden bereits abdeckt. Fixieren Sie die SID, wenn das Geschäft groß genug ist, dass eine umbenannte Gesamtstruktur ein realistischer Angriff ist, und akzeptieren Sie, dass sie Windows-spezifisch ist — auf einer Linux-Workstation ist DomainSid null, und das Gate oben überspringt den SID-Vergleich nur, wenn der Schlüssel keine trägt, also stellen Sie reine Linux-Standorte ohne sie aus.

Keines von beiden ist eine Platzzählung. Ein Standortschlüssel sagt nichts darüber, wie viele Maschinen ihn ausführen, weil er dafür entworfen wurde, das nicht zu tun. Wenn ein Vertrag eine harte Obergrenze braucht — „bis zu 400 gleichzeitige Installationen” — ist das Instrument die Online-Aktivierung mit Plätzen pro Maschine und einem Server, der sie zählt, was ein anderes Keyright-Feature mit anderen Betriebskosten ist (Node-Locking und Platzverwaltung behandelt es). Zu versuchen, aus einem Domänen-Claim eine Obergrenze herauszuholen, ist der Weg, auf dem Anbieter bei einer Standortlizenz landen, die weder bequem noch durchgesetzt ist.

Tests: ein gefälschter Standort, drei Maschinen

Das Gate ist bei gegebenem ISiteIdentity rein, sodass die drei Maschinen in der Abbildung drei Tests sind:

sealed record FakeSite(string? DnsDomain, string? DomainSid) : ISiteIdentity;

static LicenseInfo SiteKey(string domain, string? sid = null) =>
    TestLicenses.Valid(entitlements: new()
    {
        ["site:domain"] = domain,
        ["site:domain-sid"] = sid!,          // null entry is dropped by the test helper
    });

[Fact]
public void Child_domain_workstation_passes_by_suffix()
    => Assert.Equal(SiteStatus.Ok, SiteGate.Check(
        SiteKey("corp.contoso.example", "S-1-5-21-1-2-3"),
        new FakeSite("emea.corp.contoso.example", "S-1-5-21-1-2-3")));

[Fact]
public void Unjoined_laptop_fails_closed()
    => Assert.Equal(SiteStatus.NotJoined, SiteGate.Check(
        SiteKey("corp.contoso.example"),
        new FakeSite(DnsDomain: null, DomainSid: null)));

[Fact]
public void Lookalike_domain_does_not_suffix_match()
    => Assert.Equal(SiteStatus.WrongDomain, SiteGate.Check(
        SiteKey("corp.contoso.example"),
        new FakeSite("notcorp.contoso.example", null)));

[Fact]
public void Renamed_forest_fails_the_sid_check()
    => Assert.Equal(SiteStatus.WrongDomainSid, SiteGate.Check(
        SiteKey("corp.contoso.example", "S-1-5-21-1-2-3"),
        new FakeSite("corp.contoso.example", "S-1-5-21-9-9-9")));

Der dritte Test ist der, den man vergisst. Ohne den führenden Punkt im Suffix-Vergleich endet notcorp.contoso.example auf corp.contoso.example, und das Gate öffnet sich für eine Domäne, die der Kunde nie besessen hat.

Ausrollen

Behandeln Sie den Standort-Claim wie jede neue Durchsetzung: stellen Sie die ersten Standortschlüssel mit vorhandenem Claim aus, aber das Gate im Audit-Modus — SiteStatus protokollieren, ohne darauf zu reagieren — für ein Release, lesen Sie die Protokolle, dann schalten Sie auf Durchsetzung um. Das Muster des Audit-Modus-Rollouts gilt unverändert, und was es fangen wird, ist das Eine, das dieser Beitrag für Sie nicht vorhersagen kann: der Kunde, dessen „eine Domäne” sich als drei Gesamtstrukturen aus zwei Übernahmen herausstellt.

Eine Standortlizenz ist ein Versprechen darüber, wo Software laufen darf, gegeben an eine Organisation statt an eine Maschine. Keyright lässt Sie dieses Versprechen in den Schlüssel signieren; die Domäne gibt der App etwas Reales, wogegen sie es prüfen kann; und die SID macht, wenn Sie sie brauchen, das Belügen der Prüfung teuer. Was sie nicht tut, ist zählen — und das zu wissen, ist es, was die Bequemlichkeit davor bewahrt, stillschweigend zum Leck zu werden.

Nebula.NET testen

Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.