Skip to content

ChunkedHmacVerification: non-constant-time MAC comparison at line 52 enables timing side-channel attack #72

@JACKURUVI99

Description

@JACKURUVI99
ChunkedHmacVerification: non-constant-time MAC comparison at line 52 enables timing side-channel attack

Body

Description

ChunkedHmacVerification.java line 52 compares the expected MAC against the
attacker-supplied tag using util.Bytes.equals():

if (!tag.equals(Bytes.copyFrom(other)))
    throw new GeneralSecurityException("invalid MAC");

util.Bytes.equals() calls java.util.Arrays.equals(), which exits the loop
on the first mismatching byte. This leaks timing information proportional
to the number of correct prefix bytes — a textbook timing side-channel (CWE-208).

The correct constant-time API already exists in the same codebase:
subtle.Bytes.equal() calls MessageDigest.isEqual(), which is guaranteed
constant-time by the JCA spec. It is already used correctly in PrfMac.java.
The two classes share nearly identical names (util.Bytes vs subtle.Bytes),
which is likely how the wrong one was used here.


Proof of Concept

Tested on Tink 1.15.0, Temurin JDK 8u452, Debian 13.

On JDK 8, Arrays.equals() is a pure Java loop (~4 ns timing difference per
matching byte). Using 200,000 samples with a minimum-statistic filter, I
recovered a full 16-byte HMAC-SHA256 tag byte-by-byte in under 3 minutes with
no knowledge of the secret key:

[byte 01/16] timing scan...  recovered=0x38  elapsed=5s
[byte 02/16] timing scan...  recovered=0xe8  elapsed=12s
[byte 03/16] timing scan...  recovered=0x15  elapsed=19s
[byte 04/16] timing scan...  recovered=0x30  elapsed=27s
[byte 05/16] timing scan...  recovered=0xda  elapsed=36s
[byte 06/16] timing scan...  recovered=0xe0  elapsed=45s
[byte 07/16] timing scan...  recovered=0xd0  elapsed=56s
[byte 08/16] timing scan...  recovered=0x10  elapsed=67s
[byte 09/16] timing scan...  recovered=0x5b  elapsed=79s
[byte 10/16] timing scan...  recovered=0xcf  elapsed=91s
[byte 11/16] timing scan...  recovered=0xaf  elapsed=105s
[byte 12/16] timing scan...  recovered=0x02  elapsed=119s
[byte 13/16] timing scan...  recovered=0x98  elapsed=133s
[byte 14/16] timing scan...  recovered=0x8f  elapsed=149s
[byte 15/16] timing scan...  recovered=0xe6  elapsed=165s
[byte 16/16] oracle brute-force... recovered=0x17  (24 attempts)

Recovered tag : 38e81530dae0d0105bcfaf02988fe617
Real tag      : 38e81530dae0d0105bcfaf02988fe617
Bytes correct : 16 / 16

EXPLOIT SUCCESS — full MAC recovered in 165s (2 min 45 sec)

Note: JDK 11+ JIT may use SIMD for Arrays.equals() locally, but the
underlying code is still incorrect and exploitable over low-latency networks
with more samples.


Fix

File: java_src/src/main/java/com/google/crypto/tink/mac/internal/ChunkedHmacVerification.java

// BEFORE (line 52) — non-constant-time
if (!tag.equals(Bytes.copyFrom(other)))
    throw new GeneralSecurityException("invalid MAC");

// AFTER — constant-time
if (!com.google.crypto.tink.subtle.Bytes.equal(tag.toByteArray(), other))
    throw new GeneralSecurityException("invalid MAC");

subtle.Bytes.equal() is already present in the codebase and used correctly
in PrfMac.java — this fix requires changing one line.


References

  • Vulnerable: java_src/.../mac/internal/ChunkedHmacVerification.java:52
  • Non-CT comparison: java_src/.../util/Bytes.java:91Arrays.equals()
  • CT alternative: java_src/.../subtle/Bytes.javaMessageDigest.isEqual()
  • CWE-208: Observable Timing Discrepancy

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions