Licences de site : lier une clé Keyright au domaine du client plutôt qu'à une machine, et ce que ce lien peut et ne peut pas prouver
Une entreprise qui achète quatre cents postes ne veut pas quatre cents activations, et vous ne voulez pas que la clé qui les déverrouille tous parte sur le portable d'un prestataire. La réponse classique est une licence de site — une clé pour toute l'organisation — et la question que personne n'écrit est à quoi la clé est liée, si ce n'est à une machine. Ce billet construit une licence de site sur Keyright sous forme d'un droit signé qui nomme le domaine DNS du client, éventuellement épinglé par le SID de domaine Windows : d'où viennent ces deux identités (IPGlobalProperties.DomainName, pas Environment.UserDomainName, et SecurityIdentifier.AccountDomainSid), pourquoi la correspondance par suffixe est la bonne comparaison pour une forêt avec des domaines enfants, comment la porte échoue fermée sur un portable non joint, comment le client vous transmet les valeurs sans aller-retour avec l'informatique, et le modèle de menace honnête : un nom de domaine est une barrière de politique qu'un attaquant déterminé peut contourner en renommant une VM, un SID de domaine relève la barre jusqu'au clonage d'un contrôleur de domaine, et aucun des deux ne remplace un décompte de postes quand vous en avez vraiment besoin.
Un client avec quatre cents postes de travail ne veut pas activer quatre cents machines, et son équipe informatique vous le dira avant que le bon de commande soit signé. La réponse à laquelle tout éditeur arrive est une licence de site : une clé pour toute l’organisation, sans cérémonie par machine. La question qui est rarement écrite est à quoi une telle clé est liée. Une clé verrouillée par nœud est liée à une empreinte de machine. Une clé flottante est liée à un décompte de postes sur un serveur. Une clé de site sans aucun lien est un fichier qui déverrouille votre produit pour quiconque le possède — et les fichiers voyagent.
Ce billet construit une licence de site sur Keyright liée à quelque chose que toute l’organisation partage et qu’un inconnu n’a pas : le domaine d’annuaire. C’est un droit signé, pas un nouveau type de licence ; la porte fait vingt lignes ; et le modèle de menace est énoncé honnêtement, parce qu’un nom de domaine est une barrière, pas un verrou.
Ce que dit la clé
Une licence Keyright verrouillée par nœud porte un machineId et le validateur refuse de s’exécuter sur toute machine dont l’empreinte ne correspond pas. Une licence de site laisse machineId vide et nomme à la place le site dans l’ensemble de droits signé :
{
"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": "…"
}
Deux choses comptent ici. Les entrées site: sont des droits ordinaires — des valeurs de chaîne dans la charge canonique que la signature couvre — donc un client ne peut pas plus changer corp.contoso.example en home.example qu’il ne peut repousser l’expiration d’un an : un octet modifié et ce que voit l’application est LicenseStatus.SignatureInvalid. Et le préfixe est une convention, pas une fonctionnalité Keyright : il garde où cette clé peut s’exécuter visiblement séparé de ce que cette clé déverrouille, pour que personne ne conditionne plus tard une fonctionnalité à une revendication de site ou inversement.
site:domain-sid est optionnel. Le domaine DNS est le lien portable et multiplateforme ; le SID est le durcissement réservé à Windows que vous ajoutez quand le client ou la valeur du contrat le justifie. Nous reviendrons sur ce que chacun prouve réellement.
D’où vient l’identité de la machine
L’application doit répondre « sur quel domaine suis-je ? » depuis la machine elle-même, hors ligne, au démarrage. Il y a deux API tentantes et l’une d’elles est la mauvaise.
Environment.UserDomainName renvoie le domaine NetBIOS de l’utilisateur qui exécute le processus — CONTOSO — et sur une machine qui n’est jointe à aucun domaine, il renvoie à la place le nom de la machine. Cela le rend inutilisable pour une vérification qui échoue fermée (vous ne pouvez pas distinguer « non jointe » de « jointe à un domaine qui porte par hasard le nom de la machine ») et il est plat, donc il ne peut pas exprimer que emea.corp.contoso.example fait partie de corp.contoso.example.
IPGlobalProperties.GetIPGlobalProperties().DomainName renvoie le suffixe DNS principal de l’hôte, que l’appartenance au domaine définit : emea.corp.contoso.example sur une station jointe, une chaîne vide sur un portable qui n’a jamais été joint. Il est hiérarchique, il est multiplateforme (sous Linux, il lit le nom de domaine configuré de l’hôte), et le cas de la chaîne vide est exactement le signal que vous voulez.
Pour le SID, WindowsIdentity.GetCurrent().User.AccountDomainSid renvoie le SID du domaine auquel appartient le compte courant — S-1-5-21-… avec l’identifiant relatif final retiré. Pour un utilisateur de domaine sur une machine jointe, c’est le SID de domaine de la forêt. Pour un compte local, c’est le SID de la machine locale, qui ne correspondra pas et ne devrait pas.
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; }
}
}
}
Chaque chemin d’échec renvoie null, et null signifie « non jointe ». C’est délibéré : la porte ci-dessous traite une identité inconnue sur une clé de site comme un refus, pas comme un passage.
La porte
La porte s’exécute après Validate() et ne fait quelque chose que lorsque la clé porte une revendication de site. La correspondance du domaine DNS est une correspondance par suffixe sur les frontières d’étiquette : une clé émise pour corp.contoso.example doit accepter une station dans le domaine enfant emea.corp.contoso.example, mais ne doit pas accepter notcorp.contoso.example — d’où le point initial dans la comparaison.
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;
}
}
Câblez-la au démarrage, après les vérifications de signature et d’expiration que le SDK effectue déjà :
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);
Notez ce que le SDK n’a pas fait : avec machineId absent, Validate() n’a jamais calculé d’empreinte de machine, donc il n’y a pas d’état par machine, rien à « activer », et rien qui dérive quand l’informatique réinstalle un poste. Quatre cents machines, un fichier, une vérification.
Un ordre délibéré : la vérification de site s’exécute après IsValid. Une clé avec une mauvaise signature n’atteint jamais la porte, donc les droits qu’elle porte — y compris un site:domain qu’un attaquant aurait pu saisir — ne sont jamais lus. Les droits ne sont dignes de confiance qu’une fois que la signature qui les couvre a été vérifiée, et Keyright l’impose en renvoyant un ensemble de droits vide pour tout ce qui n’a pas validé.
Obtenir les valeurs du client
Le point faible de toute licence liée à une identité est la transmission : le client doit vous dire quel est son domaine, et s’il vous dit quelque chose de subtilement différent de ce que la machine rapporte (contoso.example au lieu de corp.contoso.example, un nom NetBIOS au lieu d’un nom DNS), le premier lancement après l’achat échoue avec « mauvais domaine » tandis que toutes les personnes impliquées jurent qu’elles sont sur le bon domaine.
Comblez cet écart en faisant rapporter au produit sa propre réponse, depuis exactement le chemin de code que la porte utilise :
// yourapp --site-info
var id = new RealSiteIdentity();
Console.WriteLine($"DNS domain : {id.DnsDomain ?? "(not domain-joined)"}");
Console.WriteLine($"Domain SID : {id.DomainSid ?? "(n/a)"}");
Demandez cette sortie sur le bon de commande. Aucune des deux valeurs n’est confidentielle — le suffixe DNS figure dans chaque e-mail envoyé par l’entreprise, et le SID de domaine est lisible par tout utilisateur authentifié du domaine — donc ce n’est pas plus sensible que de demander un nom d’entreprise. Le côté émission écrit les deux chaînes dans les droits et signe ; ce que le client a collé est, par construction, ce que la vérification verra. (Émettez pour le domaine parent que le client nomme, pas pour l’enfant dans lequel le diagnostic s’est trouvé exécuté : une clé pour corp.contoso.example couvre EMEA et APAC ; une clé pour emea.corp.contoso.example non.)
Ce que le lien prouve — honnêtement
Voici la partie qui appartient à votre propre modèle de menace, pas seulement à la page marketing.
Un nom de domaine DNS est une barrière de politique. Ce n’est ni un secret ni une identité cryptographique. Quiconque détient une clé de site divulguée peut monter une forêt jetable nommée corp.contoso.example, y joindre une VM, et la vérification de nom passe. Ce que le nom arrête bel et bien, c’est la défaillance de loin la plus courante : l’employé qui copie license.json sur un PC personnel, le prestataire qui le garde après la mission, le fichier qui finit sur un lecteur partagé. Ces machines ne sont pas jointes au domaine du client, la porte échoue fermée, et le produit leur dit précisément pourquoi. Pour la plupart des licences de site — où le client est une organisation connue avec un contrat — c’est tout le travail.
Un SID de domaine relève considérablement la barre. Le SID d’une forêt est généré aléatoirement à la création de la forêt ; aucun outil pris en charge ne permet de le choisir, donc reproduire S-1-5-21-3623811015-3361044348-30300820 signifie cloner le véritable contrôleur de domaine du client, pas taper un nom dans un assistant. Un attaquant extérieur à l’organisation ne peut pas le faire ; un initié avec des droits d’administrateur de domaine le pourrait, mais un initié avec des droits d’administrateur de domaine est aussi la personne que le contrat du client couvre déjà. Épinglez le SID quand l’affaire est assez importante pour qu’une forêt renommée soit une attaque réaliste, et acceptez qu’il soit réservé à Windows — sur une station Linux, DomainSid est null, et la porte ci-dessus ne saute la comparaison de SID que lorsque la clé n’en porte pas, donc émettez les sites Linux uniquement sans lui.
Aucun des deux n’est un décompte de postes. Une clé de site ne dit rien sur combien de machines l’exécutent, parce qu’elle a été conçue pour ne pas le faire. Si un contrat a besoin d’un plafond strict — « jusqu’à 400 installations simultanées » — l’instrument est l’activation en ligne avec des postes par machine et un serveur qui les compte, ce qui est une fonctionnalité Keyright différente avec un coût opérationnel différent (verrouillage par nœud et gestion des postes le couvre). Essayer de tirer un plafond d’une revendication de domaine est la façon dont les éditeurs se retrouvent avec une licence de site qui n’est ni pratique ni appliquée.
Tests : un faux site, trois machines
La porte est pure étant donné un ISiteIdentity, donc les trois machines de la figure sont trois tests :
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")));
Le troisième test est celui que les gens oublient. Sans le point initial dans la comparaison par suffixe, notcorp.contoso.example se termine par corp.contoso.example et la porte s’ouvre pour un domaine que le client n’a jamais possédé.
Le déployer
Traitez la revendication de site comme toute nouvelle application de licence : émettez les premières clés de site avec la revendication présente mais la porte en mode audit — journalisez SiteStatus sans agir dessus — pendant une version, lisez les journaux, puis basculez en mode appliqué. Le schéma de déploiement en mode audit s’applique sans changement, et ce qu’il attrapera est la seule chose que ce billet ne peut pas prédire pour vous : le client dont « l’unique domaine » se révèle être trois forêts issues de deux acquisitions.
Une licence de site est une promesse sur où le logiciel peut s’exécuter, faite à une organisation plutôt qu’à une machine. Keyright vous permet de signer cette promesse dans la clé ; le domaine donne à l’application quelque chose de réel pour la vérifier ; et le SID, quand vous en avez besoin, rend la vérification coûteuse à tromper. Ce qu’elle ne fait pas, c’est compter — et le savoir est ce qui empêche la commodité de devenir discrètement une fuite.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.