Skip to content
← All posts
· Delta1 Labs KeyrightLicensing.NETDeep Dive

Clock skew in license verification: why a brand-new key fails on a slow laptop, and the leeway your verify needs

A customer activates a license at 9:00, and your app rejects it as "not yet valid." Nothing is forged, nothing is expired — their laptop clock is four minutes behind the machine that issued the key, so the NotBefore claim sits in their future. This post is about the honest-clock problem (not the attacker rolling the clock back, which is a different post): the few minutes of disagreement between the issuer's clock and the verifier's clock that turn a correct license into a spurious failure at the worst possible moment — right after purchase. It reads the three time claims on a Keyright license (IssuedAt, NotBefore, Expiry), shows the naive comparison that breaks, and builds the leeway-aware verify: a small symmetric tolerance on NotBefore and Expiry, why the tolerance is asymmetric in intent, and why elapsed time inside a session should come off a monotonic clock, not the wall clock you just decided you cannot fully trust.

A customer buys your software, activates the license at 9:00 sharp, and your app tells them the license is “not valid yet.” Nothing is wrong with the key. It is signed, it is theirs, it does not expire for a year. Their laptop clock is simply four minutes behind the machine that issued the key, so the license’s NotBefore instant sits four minutes in their future. A strict time comparison rejects it, and you have turned a successful sale into a support ticket in the first ten seconds.

This is the honest-clock problem, and it is distinct from an attacker rolling the clock back to cheat an expiry — that is a different defence. Here nobody is cheating; two clocks just disagree. This post builds the Keyright verify that absorbs that disagreement without opening a hole.

Three time claims, and the two comparisons that matter

A Keyright license carries three UTC instants, and they answer three different questions:

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 is informational — it tells you how old the key is. The two claims that gate validity are NotBeforeUtc and ExpiryUtc, and verification is, at heart, two comparisons against now:

// 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");

The flaw is that now is this machine’s idea of now, and this machine is not the one that stamped the license. Both comparisons are exact to the millisecond, so any disagreement between the two clocks lands exactly on the boundary and flips the result.

Why the slow clock loses, right after purchase

Consumer machines drift slow far more often than fast — a laptop that has been asleep, a desktop whose NTP sync is overdue, a VM resumed from a snapshot. Picture the issuer at 9:00:00 and the customer’s clock at 8:56:00, four minutes behind. The license is stamped NotBefore = 09:00:00. The customer’s DateTime.UtcNow reads 08:56:00, which is less than 09:00:00, so now < nbf is true and the gate says not yet valid. The license will not even start working until the customer’s clock crawls past 9:00 — four minutes of a paying customer staring at a rejection.

Verifier clock is 4 min slow; NotBefore = 09:0008:5609:00NotBeforeverifier "now"leeway (5 min)strict: now < NotBefore → REJECT (not yet valid)with leeway: now ≥ NotBefore − 5 min → ACCEPTvalid region

The fix: one leeway knob, applied at both ends

The cure is a small tolerance — leeway — subtracted from the NotBefore comparison and added to the Expiry comparison. Keyright exposes it as a single option so you set it once instead of sprinkling magic numbers through your gate:

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

Internally the two comparisons become:

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");

With a five-minute ClockSkew, the customer whose clock is four minutes slow now satisfies now < nbf - skew → 08:56 < 08:55 → false, so the license is accepted. The attacker who would need to roll a clock back by more than five minutes to keep an expired license alive gains only those five minutes, which is noise against a yearly subscription.

The leeway is symmetric in value, asymmetric in intent

The same skew value lands at both ends, but it is doing two different jobs, and it helps to name them. At the NotBefore end, leeway forgives a verifier whose clock is behind the issuer’s — the common “slow laptop” case above. At the Expiry end, leeway is a courtesy grace window so a license does not wink out at the exact millisecond of expiry on a verifier whose clock runs slightly fast; without it, a customer on a fast clock loses access a few minutes before the expiry you actually sold them.

Keep both small. A large Expiry leeway is simply a free extension you did not mean to grant, and a large NotBefore leeway widens the window a rolled-back clock could sit inside. Minutes, not hours. The default of five minutes is the sweet spot: comfortably larger than ordinary drift, far smaller than anything worth cheating for.

Measuring durations: reach for the monotonic clock

There is a second, subtler trap once you accept that the wall clock is not fully trustworthy. Anywhere you measure elapsed time yourself — a grace period after the license server was unreachable, a cache TTL on a verified result, “how long since the last successful validation” — do not subtract two DateTime.UtcNow readings. The wall clock can jump: NTP corrects it, the machine resumes from sleep, a user edits it. Subtracting two readings across such a jump can hand you a negative interval or a wildly inflated one.

// 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 is backed by the OS monotonic timer; it only ever advances, at a steady rate, regardless of what the wall clock does. So the division of labour is clean: use the wall clock only to compare against the absolute NotBefore and Expiry instants on the license — with leeway — and use Stopwatch for every duration you compute inside a running session. The clock you cannot fully trust for “what time is it” is exactly the clock you must not use for “how much time has passed.”

The takeaway

Clock skew is not an attack, it is the normal state of the world: two machines that disagree by a few minutes with no ill intent. A verify that compares instants to the millisecond will reject honest, freshly purchased licenses the moment a customer’s clock runs slow — the costliest possible false negative. Give the comparison a few minutes of leeway at both the NotBefore and Expiry ends through one ClockSkew knob, keep that leeway to minutes so it never becomes a free extension or a tampering hole, and measure every elapsed duration off a monotonic Stopwatch rather than the wall clock you have decided to distrust. That is the difference between a license gate that works in a lab and one that works on a laptop that just woke up.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.