Introduction

Every function you call produces two things: an output and a duration. Most programmers only worry about the output. Attackers watch both.

A timing side channel is the gap between what a program is supposed to reveal and what its execution time accidentally reveals. The gap can be tiny — a few nanoseconds — but when you can measure it millions of times and average the noise away, even single-bit differences become recoverable.

The classic example is a password or token comparison. A naive byte-by-byte check returns early the moment it finds a mismatch: comparing "secret" against "sadder" stops at the second character and returns in roughly half the time it takes to compare two strings that differ only at the last byte. An attacker who can send many guesses and measure response times can recover the secret one byte at a time — no brute force needed.

This is not a theoretical concern. Real attacks have broken web-application tokens, MAC verifiers, and even some cryptographic key-comparison routines in production systems.

Try It: Recover the Secret

The demo below simulates a server that compares your guess against a hidden secret. The vulnerable version exits as soon as a byte mismatches — so a correct prefix takes longer than a wrong one. Use that difference to discover the secret one byte at a time.

<!-- {{c_html_comment}} -->
<div class="controls">
  <label class="mode-label">
    <input type="checkbox" id="ctMode"> {{label_ct_mode}}
  </label>
  <button id="resetBtn" type="button" class="ghost">{{btn_reset}}</button>
</div>
<div class="secret-row">
  <span class="secret-label">{{label_secret}}</span>
  <span id="secretDisplay" class="secret-display">?????</span>
</div>
<div class="guess-row">
  <span class="guess-label">{{label_guess}}</span>
  <div id="byteBoxes" class="byte-boxes"></div>
</div>
<div class="timing-row">
  <span class="timing-label">{{label_timing}}</span>
  <div id="timingBar" class="timing-bar-wrap">
    <div id="timingFill" class="timing-fill"></div>
    <span id="timingVal" class="timing-val">—</span>
  </div>
</div>
<div id="status" class="status"></div>
<div class="action-row">
  <button id="guessBtn" type="button">{{btn_guess}}</button>
  <button id="autoBtn" type="button">{{btn_auto}}</button>
</div>
<div class="log-wrap">
  <div class="log-header">{{label_log}}</div>
  <div id="log" class="log"></div>
</div>
/* {{c_css_comment}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .92rem; }
.controls { display: flex; align-items: center; gap: 1rem; margin-bottom: .6rem; }
.mode-label { display: flex; align-items: center; gap: .4rem; font-weight: 600; cursor: pointer; }
.secret-row, .guess-row, .timing-row { display: flex; align-items: center; gap: .5rem; margin: .35rem 0; }
.secret-label, .guess-label, .timing-label { width: 5.5rem; font-weight: 600; color: #444; flex-shrink: 0; }
.secret-display { font: 700 1.1rem ui-monospace, monospace; letter-spacing: .18em; color: #1d3557; }
.byte-boxes { display: flex; gap: 4px; }
.byte-box { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
            font: 700 .9rem ui-monospace, monospace; border-radius: 6px; border: 1px solid #b0b8c4;
            background: #e8eef3; color: #1d3557; transition: background .15s; }
.byte-box.match { background: #b7e8c4; border-color: #5db87a; }
.byte-box.guess-cur { background: #ffefc0; border-color: #e6a817; }
.timing-bar-wrap { flex: 1; display: flex; align-items: center; gap: .5rem; height: 18px; }
.timing-fill { height: 12px; border-radius: 6px; background: #e63946; transition: width .2s; min-width: 0; }
.timing-val { font: 600 .85rem ui-monospace, monospace; color: #555; white-space: nowrap; }
.status { min-height: 1.4em; font-weight: 700; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.action-row { display: flex; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: not-allowed; }
.log-wrap { border: 1px solid #dde2e8; border-radius: 8px; overflow: hidden; }
.log-header { background: #f0f4f8; padding: .3rem .6rem; font-weight: 600; font-size: .82rem; color: #444; }
.log { max-height: 130px; overflow-y: auto; font: .8rem ui-monospace, monospace; color: #333; }
.log-entry { padding: .18rem .6rem; border-bottom: 1px solid #edf0f3; }
.log-entry.match { background: #f0faf4; }
.log-entry.found { background: #e8f4ea; color: #0a7d33; font-weight: 700; }
// Code not found

Notice how Vulnerable mode leaks information: the response time grows as your guess matches more correct bytes. Switch to Constant-time mode and the signal disappears — every comparison takes the same time regardless of how many bytes match. The secret is now safe from timing observation.

The Real Complexity

Timing attacks expose a gap between two views of computation:

  • The abstract view (what theory assumes): a function either returns the correct answer or it does not. Time is not part of the contract.
  • The physical view (what hardware delivers): every branch, every memory access, every cache miss leaves a measurable trace in wall-clock time.

Writing constant-time code — code whose duration does not depend on secret values — sounds simple but is surprisingly subtle:

  • No early exit on secret data. A loop that stops as soon as it finds a mismatch leaks the position of the first wrong byte.
  • No secret-dependent branches. An if that takes the fast path for one secret value and the slow path for another leaks the value through the branch-predictor and timing.
  • No secret-dependent memory access. Table lookups indexed by secret bytes leak the index through CPU cache timing (the basis of the AES cache-timing attacks in the mid-2000s).
  • Compilers can break your fix. An optimising compiler may silently reintroduce early exits because it cannot tell that the variable is secret. You must use special idioms — cmov, memory barriers, or dedicated library functions — to prevent it.

There is no single algorithmic complexity class that captures "constant-time." It is an implementation property, not a language property, and verifying it requires specialised static analysis tools (ct-verif, ctgrind, Binsec/Haunted) rather than standard correctness proofs.

Where It Matters

Timing leaks are not academic curiosities. They have broken deployed systems and driven widespread API changes:

  • Web authentication tokens: a 2009 paper by Crosby, Wallach, and Müller showed that HMAC comparison in many web frameworks was vulnerable to timing attacks over a local network, recovering tokens byte by byte.
  • TLS Lucky Thirteen (2013): Al Fardan and Paterson exploited a 1–2 instruction timing difference in CBC-mode MAC verification in OpenSSL and GnuTLS, decrypting TLS records without the key.
  • RSA blinding and branch-free modular exponentiation: every serious cryptographic library now uses Montgomery multiplication and blinding factors precisely to remove secret-dependent timing from RSA and DSA.
  • crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), crypto_verify_* (libsodium): these constant-time comparison functions exist because the naive === / == comparison leaks timing on every platform.
  • Remote attacks over the internet: while network jitter blurs timing, researchers have demonstrated successful attacks over WAN links by averaging thousands of measurements — latency is noise, not protection.

The lesson generalises: any code path that touches secret data and has variable-length execution is a potential channel. Good security engineering treats time as an output, just like a return value.

Conclusion

A correct program and a secure program are not the same thing. A comparison function that always returns the right answer can still leak which bytes matched — through nothing but the time it took.

Timing side channels remind us that the physical world is part of the attack surface. The formal model says functions map inputs to outputs; the attacker measures a third quantity the model ignores. Bridging that gap requires constant-time idioms, careful compiler management, and tools that reason about execution time as a first-class security property.

The next time you write a comparison involving a secret — a token, a hash, a key — reach for the constant-time primitive your language provides. The few microseconds you spend calling it are nothing compared to the seconds an attacker needs to recover your secret through the timing oracle you might otherwise hand them.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/timing-side-channels/Content licensed under CC BY-NC 4.0.