Skip to content
← Tous les articles
· Delta1 Labs KeyrightLicences.NETAnalyse approfondie

Décalage d'horloge dans la vérification de licence : pourquoi une clé toute neuve échoue sur un portable en retard, et la tolérance dont votre vérification a besoin

Un client active une licence à 9 h 00, et votre application la rejette comme « pas encore valide ». Rien n'est falsifié, rien n'est expiré — l'horloge de son portable a quatre minutes de retard sur la machine qui a émis la clé, si bien que la revendication NotBefore se situe dans son futur. Cet article traite du problème de l'horloge honnête (et non de l'attaquant qui recule l'horloge, qui fait l'objet d'un autre article) : les quelques minutes de désaccord entre l'horloge de l'émetteur et celle du vérificateur qui transforment une licence correcte en échec injustifié au pire moment possible — juste après l'achat. Il lit les trois revendications temporelles d'une licence Keyright (IssuedAt, NotBefore, Expiry), montre la comparaison naïve qui casse, et construit la vérification tolérante au décalage : une petite tolérance symétrique sur NotBefore et Expiry, pourquoi cette tolérance est asymétrique dans son intention, et pourquoi le temps écoulé au sein d'une session doit provenir d'une horloge monotone, et non de l'horloge murale à laquelle vous venez de décider de ne pas entièrement vous fier.

Un client achète votre logiciel, active la licence à 9 h 00 pile, et votre application lui annonce que la licence n’est « pas encore valide ». La clé n’a rien d’anormal. Elle est signée, elle lui appartient, elle n’expire pas avant un an. L’horloge de son portable a simplement quatre minutes de retard sur la machine qui a émis la clé, si bien que l’instant NotBefore de la licence se situe quatre minutes dans son futur. Une comparaison temporelle stricte la rejette, et vous venez de transformer une vente réussie en ticket de support dans les dix premières secondes.

C’est le problème de l’horloge honnête, et il est distinct de celui d’un attaquant qui recule l’horloge pour contourner une expiration — c’est une autre défense. Ici, personne ne triche ; deux horloges sont simplement en désaccord. Cet article construit la vérification Keyright qui absorbe ce désaccord sans ouvrir de brèche.

Trois revendications temporelles, et les deux comparaisons qui comptent

Une licence Keyright porte trois instants UTC, qui répondent à trois questions différentes :

public sealed class LicenseInfo
{
    public DateTime IssuedAtUtc  { get; }   // when the issuer signed it
    public DateTime? NotBeforeUtc { get; }  // not valid before this (usually == IssuedAt)
    public DateTime? ExpiryUtc    { get; }  // not valid after this (null = perpetual)
    // ...
}

IssuedAtUtc est informatif — il vous dit l’âge de la clé. Les deux revendications qui conditionnent la validité sont NotBeforeUtc et ExpiryUtc, et la vérification se résume, au fond, à deux comparaisons avec maintenant :

// The naive gate. Correct on paper, fragile in the field.
var now = DateTime.UtcNow;
if (info.NotBeforeUtc is { } nbf && now < nbf) return Invalid("not yet valid");
if (info.ExpiryUtc    is { } exp && now >= exp) return Invalid("expired");

Le défaut, c’est que now est l’idée que cette machine se fait de maintenant, et cette machine n’est pas celle qui a estampillé la licence. Les deux comparaisons sont exactes à la milliseconde, de sorte que le moindre désaccord entre les deux horloges tombe précisément sur la frontière et fait basculer le résultat.

Pourquoi l’horloge en retard perd, juste après l’achat

Les machines grand public dérivent bien plus souvent en retard qu’en avance — un portable resté en veille, un poste de travail dont la synchronisation NTP est en souffrance, une VM reprise depuis un instantané. Imaginez l’émetteur à 9:00:00 et l’horloge du client à 8:56:00, quatre minutes derrière. La licence est estampillée NotBefore = 09:00:00. Le DateTime.UtcNow du client affiche 08:56:00, ce qui est inférieur à 09:00:00 ; now < nbf est donc vrai et la barrière répond pas encore valide. La licence ne commencera même pas à fonctionner avant que l’horloge du client n’ait péniblement franchi 9 h 00 — quatre minutes pendant lesquelles un client payant contemple un refus.

Horloge du vérificateur en retard de 4 min ; NotBefore = 09:0008:5609:00NotBefore« now » du vérificateurtolérance (5 min)strict : now < NotBefore → REJET (pas encore valide)tolérance : now ≥ NotBefore − 5 min → ACCEPTÉzone valide

Le remède : un seul réglage de tolérance, appliqué aux deux extrémités

Le remède est une petite marge — la tolérance (leeway) — soustraite de la comparaison NotBefore et ajoutée à la comparaison Expiry. Keyright l’expose sous la forme d’une option unique, que vous fixez une fois pour toutes au lieu de parsemer votre barrière de nombres magiques :

var result = KeyrightClient.Validate(key, new ValidationOptions
{
    PublicKey = AppKeys.LicensePublicKey,
    ClockSkew = TimeSpan.FromMinutes(5),   // default; tune per deployment
});

En interne, les deux comparaisons deviennent :

var now  = DateTime.UtcNow;
var skew = options.ClockSkew;              // e.g. 5 minutes

// Accept a license that looks slightly early because our clock may be slow.
if (info.NotBeforeUtc is { } nbf && now < nbf - skew)
    return Invalid("not yet valid");

// Allow a hair of grace so a license does not blink off exactly on the tick.
if (info.ExpiryUtc is { } exp && now >= exp + skew)
    return Invalid("expired");

Avec un ClockSkew de cinq minutes, le client dont l’horloge a quatre minutes de retard satisfait désormais now < nbf - skew → 08:56 < 08:55 → faux, et la licence est acceptée. L’attaquant qui devrait reculer une horloge de plus de cinq minutes pour maintenir en vie une licence expirée ne gagne que ces cinq minutes, un bruit de fond face à un abonnement annuel.

La tolérance est symétrique en valeur, asymétrique en intention

La même valeur skew s’applique aux deux extrémités, mais elle y remplit deux rôles différents, et il est utile de les nommer. Du côté NotBefore, la tolérance pardonne à un vérificateur dont l’horloge est en retard sur celle de l’émetteur — le cas courant du « portable en retard » ci-dessus. Du côté Expiry, la tolérance est une fenêtre de grâce de courtoisie, pour qu’une licence ne s’éteigne pas à la milliseconde exacte de l’expiration sur un vérificateur dont l’horloge avance légèrement ; sans elle, un client dont l’horloge est en avance perd l’accès quelques minutes avant l’expiration que vous lui avez réellement vendue.

Gardez les deux petites. Une grande tolérance sur Expiry n’est rien d’autre qu’une prolongation gratuite que vous n’aviez pas l’intention d’accorder, et une grande tolérance sur NotBefore élargit la fenêtre dans laquelle une horloge reculée pourrait se loger. Des minutes, pas des heures. La valeur par défaut de cinq minutes est le juste milieu : confortablement supérieure à la dérive ordinaire, bien inférieure à tout ce qui vaudrait la peine de tricher.

Mesurer des durées : prenez l’horloge monotone

Il existe un second piège, plus subtil, dès lors que vous admettez que l’horloge murale n’est pas entièrement digne de confiance. Partout où vous mesurez vous-même du temps écoulé — une période de grâce après que le serveur de licences a été injoignable, un TTL de cache sur un résultat vérifié, « combien de temps depuis la dernière validation réussie » — ne soustrayez pas deux lectures de DateTime.UtcNow. L’horloge murale peut sauter : NTP la corrige, la machine sort de veille, un utilisateur la modifie. Soustraire deux lectures de part et d’autre d’un tel saut peut vous livrer un intervalle négatif ou démesurément gonflé.

// Wrong: wall-clock subtraction can go backwards or jump.
var started = DateTime.UtcNow;
// ... later ...
var elapsed = DateTime.UtcNow - started;   // may be negative after an NTP step

// Right: a monotonic clock only moves forward, immune to clock edits.
var sw = Stopwatch.StartNew();
// ... later ...
var elapsed = sw.Elapsed;                    // always sane

Stopwatch s’appuie sur le minuteur monotone du système d’exploitation ; il ne fait qu’avancer, à un rythme constant, quoi que fasse l’horloge murale. La répartition des rôles est donc nette : utilisez l’horloge murale uniquement pour comparer aux instants absolus NotBefore et Expiry de la licence — avec tolérance — et utilisez Stopwatch pour chaque durée que vous calculez au sein d’une session en cours. L’horloge à laquelle vous ne pouvez pas entièrement vous fier pour « quelle heure est-il » est précisément celle qu’il ne faut pas utiliser pour « combien de temps s’est écoulé ».

À retenir

Le décalage d’horloge n’est pas une attaque, c’est l’état normal du monde : deux machines qui divergent de quelques minutes sans la moindre mauvaise intention. Une vérification qui compare des instants à la milliseconde rejettera des licences honnêtes, fraîchement achetées, dès que l’horloge d’un client prendra du retard — le faux négatif le plus coûteux qui soit. Accordez à la comparaison quelques minutes de tolérance aux deux extrémités, NotBefore et Expiry, via un seul réglage ClockSkew ; limitez cette tolérance à des minutes pour qu’elle ne devienne jamais une prolongation gratuite ni une brèche de falsification ; et mesurez chaque durée écoulée sur un Stopwatch monotone plutôt que sur l’horloge murale dont vous avez décidé de vous méfier. C’est la différence entre une barrière de licence qui fonctionne en laboratoire et une qui fonctionne sur un portable qui vient de se réveiller.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.