Skip to content
← Todas las publicaciones
· Delta1 Labs LicenciamientoSeguridadGuía

Confiar en el reloj: cómo el licenciamiento sobrevive al retroceso de fecha

Las pruebas y las suscripciones caducan en una fecha — pero el reloj del dispositivo está bajo control del atacante, y retrasarlo es el truco más antiguo para revivir una licencia caducada. Aquí tienes por qué no puedes confiar en la hora local, y las técnicas de marca de agua y de hora del servidor que defienden la caducidad sin castigar a los usuarios honestos.

Casi toda licencia por tiempo se reduce a una comparación: ¿es ahora posterior a la fecha de caducidad? El problema es la palabra ahora. En el dispositivo del usuario, «ahora» es lo que diga el reloj del sistema, y el reloj del sistema está totalmente bajo el control del usuario. Retrasarlo al mes pasado es la forma más antigua y fiable de revivir una prueba caducada o una suscripción vencida — y una comprobación de licencia que confía en DateTime.UtcNow cae directamente en ello. Defender la caducidad significa tratar la hora local como una pista, no como un hecho.

Por qué falla la comprobación ingenua

La implementación tentadora es también la rota:

// DON'T: trusts an attacker-controlled clock as the only source of time.
if (DateTime.UtcNow > license.ExpiryUtc)
    Deactivate();

Nada aquí está mal como código; el fallo es el modelo de confianza. DateTime.UtcNow lee el reloj del sistema operativo, y el usuario puede poner el reloj del SO en lo que quiera. En cuanto termina la prueba, ponen la fecha de vuelta al día de instalación y la comparación cambia a «válida» para siempre. Peor aún, falla en silencio — no hay error, ni señal de manipulación, solo una licencia que calladamente nunca caduca. Cualquier lógica de caducidad que tenga solo el reloj local como entrada ya ha perdido; necesitas una fuente de tiempo que el usuario no pueda rebobinar.

La marca de agua: el tiempo solo avanza

No puedes impedir que un usuario cambie su reloj, pero sí puedes notar cuándo el tiempo parece correr hacia atrás. Lleva una marca de agua persistida — el instante más reciente que la app haya observado — y avánzala, nunca la retrocedas. En cada comprobación, si el reloj en vivo es anterior a la marca de agua, el reloj ha sido retrasado, y usas la marca de agua como el «ahora» efectivo:

DateTime HighWaterMark = LoadPersistedHighWater();   // last time we ever saw

DateTime EffectiveNow()
{
    var clock = DateTime.UtcNow;
    if (clock < HighWaterMark)
        return HighWaterMark;        // clock went backwards → don't trust it

    HighWaterMark = clock;           // advance and persist
    PersistHighWater(HighWaterMark);
    return clock;
}

// expiry check now uses EffectiveNow(), not the raw clock
if (EffectiveNow() > license.ExpiryUtc)
    Deactivate();

Ahora retrasar el reloj no sirve de nada: el tiempo efectivo nunca cae por debajo del punto más lejano que la app ya ha alcanzado. Si una prueba ha corrido 25 de 30 días, la marca de agua está en el día 25, y poner el reloj en el día 1 deja el tiempo efectivo en el día 25. La caducidad llega igualmente a tiempo.

Unos pocos detalles hacen esto robusto. Persiste la marca de agua en un sitio no obvio y no atado a un único archivo que el usuario pueda borrar para reiniciarla — el mismo almacén firmado y a prueba de manipulaciones que guarda el lease de la licencia es un buen hogar. Avánzala desde todas las fuentes de tiempo fiables que veas, no solo el arranque de la app: cada check-in en línea es una oportunidad de empujarla hacia adelante con hora autoritativa. Y permite una pequeña tolerancia (unos minutos) para que la deriva normal del reloj y las correcciones NTP no se registren como ataques.

instalacióndía 25 (marca de agua)caducidadla marca de agua avanza →reloj retrasado al día 1…el ahora efectivo sigue en el día 25

La hora del servidor es la autoridad

La marca de agua defiende sin conexión, pero es un trinquete, no un reloj — sabe que el tiempo avanzó, no la fecha verdadera. La respuesta autoritativa viene del servidor en el check-in. Cuando la app contacta el servicio de licencias para validar o renovar, la respuesta lleva la hora del servidor, y ese es el «ahora» real: no puede alterarse en el dispositivo, y a la vez avanza la marca de agua y ancla la siguiente ventana sin conexión.

Por eso un lease firmado y acotado en el tiempo se combina tan bien con el trinquete. El servidor emite un lease válido por una ventana limitada y sellado con la hora del servidor; entre check-ins la app corre sin conexión, usando la marca de agua para rechazar retrocesos; en el siguiente check-in el servidor reancla la hora real y reemite el lease. Un usuario honesto que está sin conexión una semana sigue trabajando toda la semana — su reloj está bien, la marca de agua sube con normalidad, el lease sigue dentro de su ventana. Solo un reloj retrasado dispara la comprobación. (Para el lado sin conexión de este contrato, consulta validación de licencias sin conexión; para la clave que lleva la ventana, activación en entornos aislados.)

No castigues al usuario honesto

El modo de fallo a evitar es tratar toda anomalía del reloj como fraude. Los relojes reales son caóticos: los portátiles despiertan del suspenso con un reloj obsoleto, las VM se restauran desde instantáneas, las baterías mueren y reinician el RTC a 1970, y los viajeros cruzan zonas horarias (aunque UTC lo vuelve irrelevante si comparas en UTC). Si bloqueas en seco al primer paso hacia atrás, dejarás fuera a clientes que pagan cuyo reloj tuvo un hipo.

Diseña la respuesta para que sea proporcional. Un reloj anterior a la marca de agua no es prueba de malicia — así que no desactives; simplemente niégate a confiar en el reloj y recurre a la marca de agua, que ya es el comportamiento seguro. Reserva la acción dura para el caso que de verdad importa: la ventana del lease ha transcurrido genuinamente y la app no puede alcanzar el servidor para renovar. Incluso entonces, prefiere un recordatorio de gracia y una vía para revalidar antes que un bloqueo abrupto. El atacante que retrasó el reloj no ve beneficio alguno; el usuario honesto cuyo reloj falló no ve nada en absoluto.

La versión corta

La hora local es entrada de una fuente no confiable, así que nunca dejes que DateTime.UtcNow por sí solo decida la caducidad. Lleva una marca de agua de solo avance para que un reloj retrasado no pueda des-caducar nada sin conexión, toma la hora autoritativa del servidor en cada check-in, y haz tu respuesta a la manipulación proporcional para que el ruido legítimo del reloj nunca le cueste el acceso a un usuario real. La caducidad deja de ser una fecha que el usuario puede editar y pasa a ser un hecho que tu sistema controla de verdad.

Prueba Nebula.NET

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