Pulsars
0 %
Log inSign up

When the implementation betrays the secret

Timing attacks

The simplest of the side channels to understand is time. If the duration of a computation depends on the value of the secret, then timing that computation already means spying on the secret.

The idea: time speaks

A computer does not always take the same time to perform an operation. A loop that stops earlier, an if branch that skips a step, a memory access already present in the cache: all of this shortens or lengthens the execution. If these variations are correlated with the secret, the attacker exploits them.

The classic example: the naive comparison

Imagine a system that verifies a password (or a signature) by comparing byte by byte, and that stops at the first wrong byte:

comparer(saisie, secret):
    pour i de 0 à longueur - 1:
        si saisie[i] != secret[i]:
            retourner FAUX        <- sortie anticipée !
    retourner VRAI

The flaw: the more correct leading bytes the input has, the longer the function runs before returning FALSE. By measuring this time, the attacker guesses the bytes one by one:

secret = "K7z..."

essai "A???"  -> échec au 1er octet   (rapide)
essai "K???"  -> échec au 2e octet    (un peu plus long)  => 'K' est bon !
essai "K7??"  -> échec au 3e octet    (encore plus long)  => '7' est bon !

We no longer test 256^n combinations, but 256 × n: the secret falls byte by byte. An exponential attack becomes linear.

The same flaw in RSA

The modular exponentiation m = c^d mod n of RSA decryption walks through the bits of the secret exponent d. A naive implementation performs an extra multiplication only when the bit is 1:

bit = 1  ->  élévation au carré + multiplication   (lent)
bit = 0  ->  élévation au carré seulement           (rapide)

The total time then reveals the number — or even the position — of the 1-bits of the private key. Real attacks have recovered TLS keys this way.

The countermeasure: constant time

The countermeasure has a precise name: constant-time code (constant-time). Its rule is strict:

The execution time must depend neither on branches nor on memory accesses that depend on the secret.

Concretely, a constant-time comparison examines all the bytes, without ever stopping along the way:

comparer_ct(saisie, secret):
    diff = 0
    pour i de 0 à longueur - 1:
        diff = diff | (saisie[i] XOR secret[i])   # accumule, ne sort jamais
    retourner (diff == 0)

The result is identical, but the duration is the same whatever the secret: the stopwatch no longer learns anything. Serious libraries provide dedicated functions (for example so-called constant-time comparisons) precisely for this.

In summary

If the computation time depends on the secret — comparison with early exit, branching exponentiation — the stopwatch reveals the secret bit by bit, turning an exponential search into a linear one. The countermeasure is constant-time code: no branch or memory access dependent on the secret, an always identical duration.