From 73e06d81645c0851ffc31737e5620cd7a72ce174 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:30:13 +0200 Subject: [PATCH] test(web): cover constant-time compare path in HMAC verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing rejectsTamperedSignature uses len+1 sig — short-circuits in MessageDigest.isEqual on length mismatch. Same-length tamper test forces the byte-by-byte compare so the constant-time branch is exercised. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/web/ArtifactDownloadTokenSignerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/web/ArtifactDownloadTokenSignerTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/web/ArtifactDownloadTokenSignerTest.java index 7c76f048..7b7cfb80 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/web/ArtifactDownloadTokenSignerTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/web/ArtifactDownloadTokenSignerTest.java @@ -47,4 +47,18 @@ class ArtifactDownloadTokenSignerTest { String sig = signer.signRaw(id, pastExp); assertThat(signer.verify(id, pastExp, sig)).isFalse(); } + + /** Forces the constant-time-compare branch by flipping one char at the same length — + * exercises {@code MessageDigest.isEqual}'s byte-by-byte path, not the length short-circuit. */ + @Test + void rejectsSameLengthTamperedSignature() { + var signer = new ArtifactDownloadTokenSigner(secret, clock); + UUID id = UUID.randomUUID(); + var token = signer.sign(id, Duration.ofMinutes(5)); + char last = token.sig().charAt(token.sig().length() - 1); + char swapped = last == 'A' ? 'B' : 'A'; + String tampered = token.sig().substring(0, token.sig().length() - 1) + swapped; + assertThat(tampered).hasSameSizeAs(token.sig()); + assertThat(signer.verify(id, token.exp(), tampered)).isFalse(); + } }