Overview
The DateValidator is a .NET component that compares a given date against one retrieved from a Time Server. I recently needed to produce a time limited version of an application. In this particular case, the expiry date of the application did not need to be flexible so it was hard coded into the application. Checking against the system's date was not enough as it can easily be changed, so the obvious path was to get the 'real' date from another source. This article is the quick solution I came up with.
This is not a comprehensive solution to securing your time limited applications or to NTP/SNTP, but could act as a good starting point.
SNTP
There are many time servers around the world, and there are several different protocols in use. This component uses a small part of the Simple Network Time Protocol (SNTP). SNTP works by sending/receiving packets of 48 (or more) bytes over a UDP socket on port 123. In the case of this component (due to the low precision required), we are only interested in 5 of those bytes (0, and 40-43). The client connects to the server and sends a request packet, then receives a response packet which contains the data. The actual Date/Time is not contained in the data! Instead, a Timestamp containing the number of seconds since 1st Jan 1900 00:00:00 UTC is embedded in bytes 40-43 (and fractions of seconds in bytes 44-47). From this, we can calculate the current UTC and compare against ours to see if it's valid.
For full time synchronization, many other bytes/Timestamps are used to get high precision and to compensate for delays in the sending/receiving. This is not needed for this component as we only need accuracy to 1 day; so it is not covered here, but is very easy to implement should you need to.
The Component
Note: Any exceptions that are thrown during the validation process are caught and passed to the event args rather than being re-thrown. You can examine the args to determine which exceptions to treat as validation failure.
The component is very simple. It has one method (+3 overloads) ValidateAsync
and one event Validated
. The method takes a DateTime
that is the expiry date of the application, and optionally a TimeServer
and a TimeOut
value. This method starts a BackgroundWorker
, where the work is actually done (on a separate thread). When it's completed, the event is raised along with a ValidatedEventArgs
instance that gives us all the information we need.
This is the code that does the validation (details below):
ValidatedEventArgs validationArgs =
new ValidatedEventArgs(_TimeServer, _MaxDate);
UdpClient client = null;
try
{
client = new UdpClient();
IPEndPoint ipEndPoint = _TimeServer.GetIPEndPoint();
client.Client.SendTimeout = _TimeOut * 1000;
client.Client.ReceiveTimeout = _TimeOut * 1000;
client.Connect(ipEndPoint);
client.Send(Request, byteCount);
byte[] received = client.Receive(ref ipEndPoint);
if (received.Length >= byteCount &&
((received[0] & modeMask) == modeServer))
{
ulong transmitTimeStamp = 0;
for (int i = 40; i <= 43; i++)
transmitTimeStamp = (transmitTimeStamp << 8) | received[i];
DateTime result = rootDateTime;
result += TimeSpan.FromSeconds(transmitTimeStamp);
validationArgs.TimeServerDate = result.Date;
if (validationArgs.TimeServerDate <= _MaxDate)
{
validationArgs.ValidationResult = ValidationResult.OK;
}
else
{
validationArgs.ValidationResult = ValidationResult.Fail;
validationArgs.ErrorText = "This software has expired.";
}
}
else
{
validationArgs.ValidationResult = ValidationResult.Invalid;
validationArgs.ErrorText = "Invalid result from TimeServer.";
}
}
catch (Exception ex)
{
validationArgs.ValidationResult = ValidationResult.Exception;
validationArgs.ErrorText = ex.Message;
validationArgs.Exception = ex;
}
finally
{
if(client!=null)
client.Close();
e.Result = validationArgs;
}
First, we create, initialize, and connect the UDP socket. Then, we send the request packet. This is just an array of 48 bytes. Bits 0, 1, and 2 of byte[0] indicate the mode. We are a client, which is indicated by the value 3 (011). Bits 3, 4, and 5 of byte[0] indicate the version of SNTP we're using. This is using version 4, so we left shift '4' (100) three bits and OR it with the mode to give us byte[0] (00100011). We could have just set byte[0] to equal 35, but we are following the logic in the protocol properly, and this will make refactoring the code easier in case of a change in the protocol.
private const int byteCount = 48;
private const byte sntpVersion = 4;
private const int versionBitOffset = 3;
private const byte modeMask = 7;
private const byte modeClient = 3;
private const byte modeServer = 4;
private static byte[] Request
{
get
{
byte[] result = new byte[byteCount];
result[0] = modeClient | (sntpVersion << versionBitOffset);
return result;
}
}
Next, we wait to receive a response from the server. Once we have the received byte array, we check the length and the mode bits to make sure the data is valid and it came from a server. Then, we iterate over the integer part of the timestamp (bytes 40-43) to get the seconds, and convert to a DateTime
(UTC). If there are any errors or exceptions along the way, these are placed accordingly in an instance of ValidatedEventArgs
. Finally, we close the socket.
At this point, the BackgroundWorker
's RunWorkerCompleted
method is called, where we retrieve the event args and raise the event; now, we're back on the original thread.
backgroundWorker = null;
ValidatedEventArgs result = e.Result as ValidatedEventArgs;
OnValidated(result);
Other Classes
TimeServer
This is a simple class that holds the host name of the server and the port. I have put several static TimeServer
s in there (some grouped in arrays) for convenience.
ValidatedEventArgs
A simple class derived from System.EventArgs
that holds data about the validation.
Useful References
History
- 8th July 2009: Initial version.