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:91 → Arrays.equals()
- CT alternative:
java_src/.../subtle/Bytes.java → MessageDigest.isEqual()
- CWE-208: Observable Timing Discrepancy
Body
Description
ChunkedHmacVerification.javaline 52 compares the expected MAC against theattacker-supplied tag using
util.Bytes.equals():util.Bytes.equals()callsjava.util.Arrays.equals(), which exits the loopon 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()callsMessageDigest.isEqual(), which is guaranteedconstant-time by the JCA spec. It is already used correctly in
PrfMac.java.The two classes share nearly identical names (
util.Bytesvssubtle.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 permatching 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:
Note: JDK 11+ JIT may use SIMD for
Arrays.equals()locally, but theunderlying 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.javasubtle.Bytes.equal()is already present in the codebase and used correctlyin
PrfMac.java— this fix requires changing one line.References
java_src/.../mac/internal/ChunkedHmacVerification.java:52java_src/.../util/Bytes.java:91→Arrays.equals()java_src/.../subtle/Bytes.java→MessageDigest.isEqual()