There is no single way to mitigate time spoofing attacks. However, getting time information from several sources and ensuring none of the sources too strongly disagree with each other is a start.
The Network Time Protocol, with the common version 3 specified in RFC 1305, is the most widely used and most well known method of getting time. It is an unauthenticated, unencrypted protocol which uses UDP, making spoofing and tampering quite easy. Typically, a user will poll the time from a pool of stratum 3 servers. Stratum refers to the number of servers the time has to go through after leaving the source, usually a machine which measures the state transitions of an atom. A single second is defined precisely as 9,192,631,770 transitions of the hyperfine ground state of a single cesium-133 atom at near absolute zero, and the radiation emissions that correspond to these state changes are what gives the reference clock its accuracy. There are four commonly used strata:
- Stratum 3 (pool) servers are used by end-users and are accurate to within a fraction of a second.
- Stratum 2 (secondary) servers are used only by applications that require higher accuracy.
- Stratum 1 (primary) servers attach directly to the source, and are accurate to the microsecond.
- Stratum 0 (reference) servers are the atomic clocks themselves, and are perfectly accurate.
Each stratum connects to lower strata, in order to avoid overloading the services closer to the source. Stratum 0 is not a regular networked server, and is connected directly to stratum 1 (e.g. through RS-232). Only select services that require extreme accuracy (for scientific experiments, for example) are allowed to connect to stratum 1. Stratum 2 servers may have specific policies for who can connect, while sometimes they may just politely ask you to not use them if not necessary. Stratum 3 servers can be used by anyone. There can be up to 15 strata, with the 16th meaning unsynchronized, but in practice, stratum 3 is the last. Every stratum other than stratum 0 is capable of synchronizing itself with the other strata of the same level to improve accuracy and reduce the issue of clock skew.
That's a lot of servers to trust. While a server that majorly disagrees with the others will be kicked out, that only happens if it occurs often enough. A targeted attack against a single user or a group of users can easily go unnoticed. Your NTP client may average out the result of the time given by all members of the pool you are using, or it may ignore the ones which seem insane. For this case, a single malicious NTP server is not a huge issue. However, as NTP is unauthenticated and unencrypted, it is not difficult for a single entity to give you a false value from all the servers you are using.
There is no way to avoid this issue completely. Using multiple NTP servers helps against naïve attacks and accidental misconfiguration. Routing NTP through a VPS or VPN may also raise the bar slightly, especially if you suspect that your adversary is not willing to hijack NTP for everyone and is unable to figure out which server is connecting to NTP on your behalf. There is a type of authenticated NTP defined in RFC 5906 for NTPv4, but very few NTP servers support it, and it is not simple to configure.
The way GPS works is by transmitting a periodic, perfectly synchronized signal (each GPS satellite has three stratum 1 sources). When one signal is received earlier or later than another, you will know that you are closer or farther from that satellite than from the other (because the speed of light is finite). By taking into account these relative time differences from multiple sources, it becomes possible to tell your position in relation to the satellites in orbit, and thus in relation to the Earth.
A GPS clock works by combining the time given by multiple orbital satellites. This time signal can be used to synchronize a clock, but unless you know your precise location, it can take some time of averaging the signals to get accurate time. A high-end, dedicated GPS-based clock can, when calibrated properly, be accurate to within a microsecond.
GPS spoofing is not particularly difficult, but is easy to detect. The GPS signal is, like NTP, unauthenticated and unencrypted. Three is no simple way to prevent this.
A Radio-Controlled Clock, or RCC, transmits a time code from a radio transmitter which is connected directly to a highly accurate clock source, such as a stratum 1 server. The frequencies used can be either longwave or shortwave, and vary between 25 KHz and 25 MHz. Devices marketed as having "atomic clocks" typically use RCC, as it is simple and requires little power to receive. They are highly accurate, providing precision within several microseconds.
Like GPS, they are highly vulnerable to spoofing.
TLS is Transport Layer Security, used for encrypting and authenticating communications over TCP.
This is an interesting way to keep time. The TLS 1.2 specification provides a field in the ClientHello requests which contains the current time, in seconds since the epoch. This is not always reliable, as many servers either have inaccurate or purely random values for the timestamp. TLS 1.3 plans to do away with this timestamp entirely, so this is not a long-term solution. From section 126.96.36.199 of the RFC:
Structure of this message:
The ClientHello message includes a random structure, which is used
later in the protocol.
The current time and date in standard UNIX 32-bit format
(seconds since the midnight starting Jan 1, 1970, UTC, ignoring
leap seconds) according to the sender's internal clock. Clocks
are not required to be set correctly by the basic TLS protocol;
higher-level or application protocols may define additional
requirements. Note that, for historical reasons, the data
element is named using GMT, the predecessor of the current
worldwide time base, UTC.
28 bytes generated by a secure random number generator.
Because the first four bytes of the "random" field sent by the server contain the current time, a TLS handshake can be (ab)used for obtaining time from an authenticated source. This is what is done by the tlsdate program. In order to obtain accurate time, one can connect to multiple TLS servers and average the time. This is used by Tails. Tails has hardcoded three "pools" of TLS servers:
- Pal, which contains websites that promote privacy like the EFF.
- Neutral, which contains websites that are neither extra nice nor malicious, like Stack Exchange.
- Foe, which contains websites which tend to be seen as privacy-invading, like Microsoft.
Tails queries one server from each pool and averages the results out, bailing if any one of them disagrees too strongly. The idea is that the pools have different enough affiliations that they are unlikely to collaborate in providing maliciously incorrect time values in their TLS timestamps. The specific reasoning for this setup is unknown (and seems a bit like homebrew security), but the idea itself of extracting time from the TLS handshake is fairly solid.
The Real-Time Clock, RTC, is a component in any computer as part of the CMOS chip. It usually uses a quartz oscillator at 32.768 kHz for timekeeping and is typically powered by a button cell battery. Unlike a cesium atomic clock whose frequency is determined by fundamental constants in nature, the quartz oscillator is intentionally tuned to that frequency, resulting in slight imperfections. I actually didn't know the reason it was tuned to this specific frequency (it seemed too perfect, being exactly the maximum value of a signed 16-bit integer), but after looking it up when writing this answer, it turned out this value was chosen because it was always divisible by two, making it perfect for conversion into the 1 Hz signal required to control a digital watch. The frequency just stuck.
This is actually the biggest thing you have going for you. Your RTC is not perfectly accurate, but it cannot be directly controlled by an attacker (though there are indirect ways to influence it, such as strong electromagnetic fields or ambient heat). As long as it's set correctly at least once, you can always be sure to detect significant clock skew attacks, as your RTC will never drift too far.
Putting it all together
These are quite a lot of sources, each with their own benefits and drawbacks. It turns out there is an easy way to combine them all. The OpenNTPD project, designed by the same folks behind OpenBSD, is capable of combining all of these time sources at once. It is primarily an NTP server, obtaining the time from the NTP pool given to it and using that to set the system time.
OpenNTPD also supports Authenticated TLS constraints, where the timestamp obtained from a TLS handshake is used to ensure the NTP responses are not too inaccurate. It is not designed to be used as the primary time source. This is similar to tlsdate, but is not affected by the potential inaccuracy of timestamps in TLS handshakes. Hardware time sources can also be used, such as GPS or RCC. This, of course, requires the actual hardware, but it can provide much greater accuracy.
Unlike many other NTP implementations, OpenNTPD does not set the time immediately when it changes. Rather, if the time jumps forward or back, it will gradually change your system time, skewing it until it matches its view of the current time. For example, rather than instantly setting the time 5 seconds into the future or past, it will speed up or slow down the clock frequency, and push it forward or back in very small increments. While this is designed to avoid confusing applications that rely on relative timestamps, it is also useful to slow down attackers who want to immediately set your clock to the point where an expired and compromised certificate is still valid.
There have been criticisms (and responses to said criticism) of OpenNTPD. The simplicity of the project means it lacks several sanity checks and algorithms that improve time accuracy, leading to at best 200ms accuracy. This is not an issue for security, but it may not be ideal for certain applications. For the average computer user, it is more than enough.