Licencias de sitio: vincular una clave Keyright al dominio del cliente en lugar de a una máquina, y qué puede y qué no puede demostrar ese vínculo
Una empresa que compra cuatrocientos puestos no quiere cuatrocientas activaciones, y tú no quieres que la clave que los desbloquea todos salga por la puerta en el portátil de un contratista. La respuesta clásica es una licencia de sitio —una clave para toda la organización— y la pregunta que nadie escribe es a qué está vinculada la clave, si no es a una máquina. Esta entrada construye una licencia de sitio sobre Keyright como una capacidad firmada que nombra el dominio DNS del cliente, opcionalmente fijada por el SID del dominio de Windows: de dónde salen esas dos identidades (IPGlobalProperties.DomainName, no Environment.UserDomainName, y SecurityIdentifier.AccountDomainSid), por qué la coincidencia por sufijo es la comparación correcta para un bosque con dominios hijos, cómo la puerta falla cerrada en un portátil no unido al dominio, cómo el cliente te entrega los valores sin un viaje de ida y vuelta con TI, y el modelo de amenazas honesto: un nombre de dominio es una valla de política que un atacante decidido puede rodear renombrando una VM, un SID de dominio eleva el listón hasta clonar un controlador de dominio, y ninguno de los dos sustituye a un recuento de puestos cuando realmente lo necesitas.
Un cliente con cuatrocientos escritorios no quiere activar cuatrocientas máquinas, y su equipo de TI te lo dirá antes de firmar la orden de compra. La respuesta a la que llega todo proveedor es una licencia de sitio: una clave para toda la organización, sin ceremonia por máquina. La pregunta que rara vez se escribe es a qué está vinculada esa clave. Una clave bloqueada por nodo está vinculada a una huella de máquina. Una clave flotante está vinculada a un recuento de puestos en un servidor. Una clave de sitio sin vínculo alguno es un archivo que desbloquea tu producto para cualquiera que lo tenga — y los archivos viajan.
Esta entrada construye una licencia de sitio sobre Keyright vinculada a algo que toda la organización comparte y un desconocido no: el dominio del directorio. Es una capacidad firmada, no un nuevo tipo de licencia; la puerta son veinte líneas; y el modelo de amenazas se enuncia con honestidad, porque un nombre de dominio es una valla, no una cerradura.
Qué dice la clave
Una licencia Keyright bloqueada por nodo lleva un machineId y el validador se niega a ejecutarse en cualquier máquina cuya huella no coincida. Una licencia de sitio deja machineId vacío y en su lugar nombra el sitio en el conjunto de capacidades firmado:
{
"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": "…"
}
Dos cosas importan aquí. Las entradas site: son capacidades ordinarias — valores de cadena dentro de la carga canónica que cubre la firma — así que un cliente no puede cambiar corp.contoso.example por home.example más de lo que puede retrasar la caducidad un año: un byte cambiado y lo que ve la app es LicenseStatus.SignatureInvalid. Y el prefijo es una convención, no una característica de Keyright: mantiene dónde puede ejecutarse esta clave visiblemente separado de qué desbloquea esta clave, para que nadie condicione luego una característica a una reclamación de sitio o viceversa.
site:domain-sid es opcional. El dominio DNS es el vínculo portable y multiplataforma; el SID es el endurecimiento solo para Windows que añades cuando el cliente o el valor del contrato lo justifican. Volveremos sobre lo que cada uno demuestra realmente.
De dónde sale la identidad de la máquina
La app tiene que responder «¿en qué dominio estoy?» desde la propia máquina, sin conexión, al arrancar. Hay dos API tentadoras y una de ellas es la equivocada.
Environment.UserDomainName devuelve el dominio NetBIOS del usuario que ejecuta el proceso — CONTOSO — y en una máquina que no está unida a ningún dominio devuelve en su lugar el nombre de la máquina. Eso lo hace inútil para una comprobación que falle cerrada (no puedes distinguir «no unida» de «unida a un dominio que casualmente comparte el nombre de la máquina») y es plano, así que no puede expresar que emea.corp.contoso.example forma parte de corp.contoso.example.
IPGlobalProperties.GetIPGlobalProperties().DomainName devuelve el sufijo DNS primario del host, que establece la pertenencia al dominio: emea.corp.contoso.example en una estación unida, una cadena vacía en un portátil que nunca se unió. Es jerárquico, es multiplataforma (en Linux lee el nombre de dominio configurado del host), y el caso de la cadena vacía es exactamente la señal que quieres.
Para el SID, WindowsIdentity.GetCurrent().User.AccountDomainSid devuelve el SID del dominio al que pertenece la cuenta actual — S-1-5-21-… con el identificador relativo final eliminado. Para un usuario de dominio en una máquina unida, ese es el SID del dominio del bosque. Para una cuenta local es el SID de la máquina local, que no coincidirá y no debería.
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; }
}
}
}
Toda ruta de fallo devuelve null, y null significa «no unida». Es deliberado: la puerta de abajo trata una identidad desconocida sobre una clave de sitio como un rechazo, no como un pase.
La puerta
La puerta se ejecuta después de Validate() y solo hace algo cuando la clave lleva una reclamación de sitio. La coincidencia del dominio DNS es una coincidencia por sufijo en límites de etiqueta: una clave emitida para corp.contoso.example debe aceptar una estación en el dominio hijo emea.corp.contoso.example, pero no debe aceptar notcorp.contoso.example — de ahí el punto inicial en la comparación.
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;
}
}
Cabléala al arrancar, después de las comprobaciones de firma y caducidad que el SDK ya realiza:
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);
Fíjate en lo que el SDK no hizo: con machineId ausente, Validate() nunca calculó una huella de máquina, así que no hay estado por máquina, nada que «activar», y nada que derive cuando TI reinstala un escritorio. Cuatrocientas máquinas, un archivo, una comprobación.
Un orden deliberado: la comprobación de sitio se ejecuta después de IsValid. Una clave con una firma incorrecta nunca llega a la puerta, así que las capacidades que lleva — incluido un site:domain que un atacante podría haber escrito — nunca se leen. Las capacidades solo son fiables una vez que la firma sobre ellas ha verificado, y Keyright lo impone devolviendo un conjunto de capacidades vacío para cualquier cosa que no validó.
Obtener los valores del cliente
El punto débil de cualquier licencia vinculada a una identidad es el traspaso: el cliente tiene que decirte cuál es su dominio, y si te dice algo sutilmente distinto de lo que informa la máquina (contoso.example frente a corp.contoso.example, un nombre NetBIOS en lugar de uno DNS), el primer arranque tras la compra falla con «dominio incorrecto» mientras todos los implicados insisten en que están en el dominio correcto.
Cierra esa brecha haciendo que el producto informe su propia respuesta, desde exactamente el código que usa la puerta:
// yourapp --site-info
var id = new RealSiteIdentity();
Console.WriteLine($"DNS domain : {id.DnsDomain ?? "(not domain-joined)"}");
Console.WriteLine($"Domain SID : {id.DomainSid ?? "(n/a)"}");
Pide esa salida en el formulario de pedido. Ningún valor es confidencial — el sufijo DNS va en cada correo que envía la empresa, y el SID de dominio puede leerlo cualquier usuario autenticado del dominio — así que esto no es más sensible que pedir el nombre de la empresa. El lado emisor escribe las dos cadenas en las capacidades y firma; lo que el cliente pegó es, por construcción, lo que verá la comprobación. (Emite para el dominio padre que nombra el cliente, no para el hijo en el que casualmente se ejecutó el diagnóstico: una clave para corp.contoso.example cubre EMEA y APAC; una clave para emea.corp.contoso.example no.)
Qué demuestra el vínculo — con honestidad
Esta es la parte que pertenece a tu propio modelo de amenazas, no solo a la página de marketing.
Un nombre de dominio DNS es una valla de política. No es un secreto ni una identidad criptográfica. Cualquiera que tenga una clave de sitio filtrada puede levantar un bosque desechable llamado corp.contoso.example, unir una VM a él, y la comprobación de nombre pasa. Lo que el nombre sí detiene es el fallo abrumadoramente común: el empleado que copia license.json a un PC de casa, el contratista que lo conserva tras el encargo, el archivo que acaba en una unidad compartida. Esas máquinas no están unidas al dominio del cliente, la puerta falla cerrada, y el producto les dice exactamente por qué. Para la mayoría de las licencias de sitio — donde el cliente es una organización conocida con un contrato — ese es todo el trabajo.
Un SID de dominio eleva el listón considerablemente. El SID de un bosque se genera aleatoriamente al crear el bosque; no hay herramienta soportada para elegirlo, así que reproducir S-1-5-21-3623811015-3361044348-30300820 significa clonar el controlador de dominio real del cliente, no escribir un nombre en un asistente. Un atacante externo a la organización no puede hacerlo; un interno con derechos de administrador de dominio podría, pero un interno con derechos de administrador de dominio es también la persona que el contrato del cliente ya cubre. Fija el SID cuando el acuerdo sea lo bastante grande como para que un bosque renombrado sea un ataque realista, y acepta que es solo para Windows — en una estación Linux DomainSid es null, y la puerta de arriba omite la comparación de SID solo cuando la clave no lleva uno, así que emite los sitios solo-Linux sin él.
Ninguno es un recuento de puestos. Una clave de sitio no dice nada sobre cuántas máquinas la ejecutan, porque se diseñó para no hacerlo. Si un contrato necesita un techo duro — «hasta 400 instalaciones concurrentes» — el instrumento es la activación online con puestos por máquina y un servidor que los cuente, que es una característica distinta de Keyright con un coste operativo distinto (bloqueo por nodo y gestión de puestos lo cubre). Intentar sacar un techo de una reclamación de dominio es como los proveedores acaban con una licencia de sitio que no es ni cómoda ni aplicada.
Pruebas: un sitio falso, tres máquinas
La puerta es pura dado un ISiteIdentity, así que las tres máquinas de la figura son tres pruebas:
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")));
La tercera prueba es la que la gente olvida. Sin el punto inicial en la comparación por sufijo, notcorp.contoso.example termina en corp.contoso.example y la puerta se abre para un dominio que el cliente nunca tuvo.
Desplegarlo
Trata la reclamación de sitio como cualquier nueva aplicación de licencia: emite las primeras claves de sitio con la reclamación presente pero la puerta en modo auditoría — registra SiteStatus sin actuar sobre él — durante una versión, lee los registros, y luego pasa a modo aplicado. El patrón de despliegue en modo auditoría se aplica sin cambios, y lo que detectará es lo único que esta entrada no puede predecir por ti: el cliente cuyo «un dominio» resulta ser tres bosques de dos adquisiciones.
Una licencia de sitio es una promesa sobre dónde puede ejecutarse el software, hecha a una organización en lugar de a una máquina. Keyright te permite firmar esa promesa dentro de la clave; el dominio le da a la app algo real contra lo que comprobarla; y el SID, cuando lo necesitas, hace que mentirle a la comprobación salga caro. Lo que no hace es contar — y saberlo es lo que impide que la comodidad se convierta silenciosamente en una fuga.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.