+
+
+
+A constant-time algorithm should be used for checking a MAC or a digital signature.
+In other words, the comparison time should not depend on the content of the input.
+Otherwise, an attacker may be able to forge a valid signature for an arbitrary message
+by running a timing attack if they can send to the validation procedure
+both the message and the signature. A successful attack can result in authentication bypass.
+
+
+
+
+
+Use MessageDigest.isEqual() method to check MACs and signatures.
+If this method is used, then the calculation time depends only on the length of input byte arrays,
+and does not depend on the contents of the arrays.
+
+
+
+
+
+The following example uses Arrays.equals() method for validating a MAC over a message.
+This method implements a non-constant-time algorithm.
+Both the message and the signature come from an untrusted HTTP request:
+
+
+
+
+The next example uses a safe constant-time algorithm for validating a MAC:
+
+
+
+
+
+
+ Wikipedia:
+ Timing attack.
+
+
+ Coursera:
+ Timing attacks on MAC verification
+
+
+ NCC Group:
+ Time Trial: Racing Towards Practical Remote Timing Attacks
+
+
+ Java API Specification:
+ MessageDigest.isEqual() method
+
+
+
+
diff --git a/java/ql/src/experimental/Security/CWE/CWE-208/TimingAttackAgainstSignature.ql b/java/ql/src/experimental/Security/CWE/CWE-208/TimingAttackAgainstSignature.ql
new file mode 100644
index 00000000000..488b49684b2
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-208/TimingAttackAgainstSignature.ql
@@ -0,0 +1,28 @@
+/**
+ * @name Timing attack against signature validation
+ * @description When checking a signature over a message, a constant-time algorithm should be used.
+ * Otherwise, an attacker may be able to forge a valid signature for an arbitrary message
+ * by running a timing attack if they can send to the validation procedure
+ * both the message and the signature.
+ * A successful attack can result in authentication bypass.
+ * @kind path-problem
+ * @problem.severity error
+ * @precision high
+ * @id java/timing-attack-against-signature
+ * @tags security
+ * external/cwe/cwe-208
+ */
+
+import java
+import NonConstantTimeCheckOnSignatureQuery
+import DataFlow::PathGraph
+
+from DataFlow::PathNode source, DataFlow::PathNode sink, NonConstantTimeCryptoComparisonConfig conf
+where
+ conf.hasFlowPath(source, sink) and
+ (
+ source.getNode().(CryptoOperationSource).includesUserInput() and
+ sink.getNode().(NonConstantTimeComparisonSink).includesUserInput()
+ )
+select sink.getNode(), source, sink, "Timing attack against $@ validation.", source,
+ source.getNode().(CryptoOperationSource).getCall().getResultType()
diff --git a/java/ql/src/experimental/Security/CWE/CWE-208/UnsafeMacComparison.java b/java/ql/src/experimental/Security/CWE/CWE-208/UnsafeMacComparison.java
new file mode 100644
index 00000000000..1785ff2e7c6
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-208/UnsafeMacComparison.java
@@ -0,0 +1,9 @@
+public boolean validate(HttpRequest request, SecretKey key) throws Exception {
+ byte[] message = getMessageFrom(request);
+ byte[] signature = getSignatureFrom(request);
+
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(key.getEncoded(), "HmacSHA256"));
+ byte[] actual = mac.doFinal(message);
+ return Arrays.equals(signature, actual);
+}
\ No newline at end of file
diff --git a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll
index 3b49289d885..aae71ce6d9a 100644
--- a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll
+++ b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll
@@ -268,11 +268,15 @@ private predicate summaryModelCsv(string row) {
// qualifier to arg
"java.io;InputStream;true;read;(byte[]);;Argument[-1];Argument[0];taint",
"java.io;InputStream;true;read;(byte[],int,int);;Argument[-1];Argument[0];taint",
+ "java.io;InputStream;true;readNBytes;(byte[],int,int);;Argument[-1];Argument[0];taint",
+ "java.io;InputStream;true;transferTo;(OutputStream);;Argument[-1];Argument[0];taint",
"java.io;ByteArrayOutputStream;false;writeTo;;;Argument[-1];Argument[0];taint",
"java.io;Reader;true;read;;;Argument[-1];Argument[0];taint",
// qualifier to return
"java.io;ByteArrayOutputStream;false;toByteArray;;;Argument[-1];ReturnValue;taint",
"java.io;ByteArrayOutputStream;false;toString;;;Argument[-1];ReturnValue;taint",
+ "java.io;InputStream;true;readAllBytes;;;Argument[-1];ReturnValue;taint",
+ "java.io;InputStream;true;readNBytes;(int);;Argument[-1];ReturnValue;taint",
"java.util;StringTokenizer;false;nextElement;();;Argument[-1];ReturnValue;taint",
"java.util;StringTokenizer;false;nextToken;;;Argument[-1];ReturnValue;taint",
"javax.xml.transform.sax;SAXSource;false;getInputSource;;;Argument[-1];ReturnValue;taint",
@@ -283,10 +287,12 @@ private predicate summaryModelCsv(string row) {
"java.net;URI;false;toAsciiString;;;Argument[-1];ReturnValue;taint",
"java.io;File;false;toURI;;;Argument[-1];ReturnValue;taint",
"java.io;File;false;toPath;;;Argument[-1];ReturnValue;taint",
+ "java.nio;ByteBuffer;false;array;();;Argument[-1];ReturnValue;taint",
"java.nio.file;Path;false;toFile;;;Argument[-1];ReturnValue;taint",
"java.io;BufferedReader;true;readLine;;;Argument[-1];ReturnValue;taint",
"java.io;Reader;true;read;();;Argument[-1];ReturnValue;taint",
// arg to return
+ "java.nio;ByteBuffer;false;wrap;(byte[]);;Argument[0];ReturnValue;taint",
"java.util;Base64$Encoder;false;encode;(byte[]);;Argument[0];ReturnValue;taint",
"java.util;Base64$Encoder;false;encode;(ByteBuffer);;Argument[0];ReturnValue;taint",
"java.util;Base64$Encoder;false;encodeToString;(byte[]);;Argument[0];ReturnValue;taint",
diff --git a/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.expected b/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.expected
new file mode 100644
index 00000000000..b6d3211a64e
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.expected
@@ -0,0 +1,15 @@
+edges
+| Test.java:14:28:14:44 | doFinal(...) : byte[] | Test.java:15:43:15:51 | actualMac |
+| Test.java:30:28:30:40 | sign(...) : byte[] | Test.java:31:40:31:48 | signature |
+| Test.java:47:22:47:46 | doFinal(...) : byte[] | Test.java:48:40:48:42 | tag |
+nodes
+| Test.java:14:28:14:44 | doFinal(...) : byte[] | semmle.label | doFinal(...) : byte[] |
+| Test.java:15:43:15:51 | actualMac | semmle.label | actualMac |
+| Test.java:30:28:30:40 | sign(...) : byte[] | semmle.label | sign(...) : byte[] |
+| Test.java:31:40:31:48 | signature | semmle.label | signature |
+| Test.java:47:22:47:46 | doFinal(...) : byte[] | semmle.label | doFinal(...) : byte[] |
+| Test.java:48:40:48:42 | tag | semmle.label | tag |
+#select
+| Test.java:15:43:15:51 | actualMac | Test.java:14:28:14:44 | doFinal(...) : byte[] | Test.java:15:43:15:51 | actualMac | Possible timing attack against $@ validation. | Test.java:14:28:14:44 | doFinal(...) : byte[] | MAC |
+| Test.java:31:40:31:48 | signature | Test.java:30:28:30:40 | sign(...) : byte[] | Test.java:31:40:31:48 | signature | Possible timing attack against $@ validation. | Test.java:30:28:30:40 | sign(...) : byte[] | signature |
+| Test.java:48:40:48:42 | tag | Test.java:47:22:47:46 | doFinal(...) : byte[] | Test.java:48:40:48:42 | tag | Possible timing attack against $@ validation. | Test.java:47:22:47:46 | doFinal(...) : byte[] | ciphertext |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.java b/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.java
new file mode 100644
index 00000000000..7a4433e485d
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.java
@@ -0,0 +1,59 @@
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Arrays;
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+
+public class Test {
+
+ // BAD: compare MACs using a not-constant time method
+ public boolean unsafeMacCheck(byte[] expectedMac, byte[] data) throws Exception {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ byte[] actualMac = mac.doFinal(data);
+ return Arrays.equals(expectedMac, actualMac);
+ }
+
+ // GOOD: compare MACs using a constant time method
+ public boolean saferMacCheck(byte[] expectedMac, byte[] data) throws Exception {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ byte[] actualMac = mac.doFinal(data);
+ return MessageDigest.isEqual(expectedMac, actualMac);
+ }
+
+ // BAD: compare signatures using a not-constant time method
+ public boolean unsafeCheckSignatures(byte[] expected, byte[] data, PrivateKey key) throws Exception {
+ Signature engine = Signature.getInstance("SHA256withRSA");
+ engine.initSign(key);
+ engine.update(data);
+ byte[] signature = engine.sign();
+ return Arrays.equals(expected, signature);
+ }
+
+ // GOOD: compare signatures using a constant time method
+ public boolean saferCheckSignatures(byte[] expected, byte[] data, PrivateKey key) throws Exception {
+ Signature engine = Signature.getInstance("SHA256withRSA");
+ engine.initSign(key);
+ engine.update(data);
+ byte[] signature = engine.sign();
+ return MessageDigest.isEqual(expected, signature);
+ }
+
+ // BAD: compare ciphertexts using a not-constant time method
+ public boolean unsafeCheckCustomMac(byte[] expected, byte[] plaintext, Key key) throws Exception {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] tag = cipher.doFinal(plaintext);
+ return Arrays.equals(expected, tag);
+ }
+
+ // GOOD: compare ciphertexts using a constant time method
+ public boolean saferCheckCustomMac(byte[] expected, byte[] plaintext, Key key) throws Exception {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] tag = cipher.doFinal(plaintext);
+ return MessageDigest.isEqual(expected, tag);
+ }
+
+}
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.qlref b/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.qlref
new file mode 100644
index 00000000000..3a113b0fd71
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-208/NotConstantTimeCheckOnSignature/Test.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE/CWE-208/PossibleTimingAttackAgainstSignature.ql
diff --git a/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.expected b/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.expected
new file mode 100644
index 00000000000..805be02abf3
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.expected
@@ -0,0 +1,44 @@
+edges
+| Test.java:21:32:21:48 | doFinal(...) : byte[] | Test.java:23:47:23:55 | actualMac |
+| Test.java:34:25:34:33 | actualMac : byte[] | Test.java:36:47:36:55 | actualMac |
+| Test.java:59:32:59:44 | sign(...) : byte[] | Test.java:61:44:61:52 | signature |
+| Test.java:73:25:73:33 | signature : byte[] | Test.java:75:44:75:52 | signature |
+| Test.java:99:26:99:45 | doFinal(...) : byte[] | Test.java:101:49:101:51 | tag |
+| Test.java:116:28:116:30 | tag : byte[] | Test.java:118:44:118:46 | tag |
+| Test.java:134:56:134:58 | tag : ByteBuffer | Test.java:136:44:136:46 | tag : ByteBuffer |
+| Test.java:136:44:136:46 | tag : ByteBuffer | Test.java:136:44:136:54 | array(...) |
+| Test.java:148:56:148:58 | tag : ByteBuffer | Test.java:150:53:150:55 | tag |
+| Test.java:174:26:174:50 | doFinal(...) : byte[] | Test.java:176:44:176:46 | tag |
+| Test.java:201:34:201:50 | doFinal(...) : byte[] | Test.java:204:26:204:36 | computedTag |
+nodes
+| Test.java:21:32:21:48 | doFinal(...) : byte[] | semmle.label | doFinal(...) : byte[] |
+| Test.java:23:47:23:55 | actualMac | semmle.label | actualMac |
+| Test.java:34:25:34:33 | actualMac : byte[] | semmle.label | actualMac : byte[] |
+| Test.java:36:47:36:55 | actualMac | semmle.label | actualMac |
+| Test.java:59:32:59:44 | sign(...) : byte[] | semmle.label | sign(...) : byte[] |
+| Test.java:61:44:61:52 | signature | semmle.label | signature |
+| Test.java:73:25:73:33 | signature : byte[] | semmle.label | signature : byte[] |
+| Test.java:75:44:75:52 | signature | semmle.label | signature |
+| Test.java:99:26:99:45 | doFinal(...) : byte[] | semmle.label | doFinal(...) : byte[] |
+| Test.java:101:49:101:51 | tag | semmle.label | tag |
+| Test.java:116:28:116:30 | tag : byte[] | semmle.label | tag : byte[] |
+| Test.java:118:44:118:46 | tag | semmle.label | tag |
+| Test.java:134:56:134:58 | tag : ByteBuffer | semmle.label | tag : ByteBuffer |
+| Test.java:136:44:136:46 | tag : ByteBuffer | semmle.label | tag : ByteBuffer |
+| Test.java:136:44:136:54 | array(...) | semmle.label | array(...) |
+| Test.java:148:56:148:58 | tag : ByteBuffer | semmle.label | tag : ByteBuffer |
+| Test.java:150:53:150:55 | tag | semmle.label | tag |
+| Test.java:174:26:174:50 | doFinal(...) : byte[] | semmle.label | doFinal(...) : byte[] |
+| Test.java:176:44:176:46 | tag | semmle.label | tag |
+| Test.java:201:34:201:50 | doFinal(...) : byte[] | semmle.label | doFinal(...) : byte[] |
+| Test.java:204:26:204:36 | computedTag | semmle.label | computedTag |
+#select
+| Test.java:23:47:23:55 | actualMac | Test.java:21:32:21:48 | doFinal(...) : byte[] | Test.java:23:47:23:55 | actualMac | Timing attack against $@ validation. | Test.java:21:32:21:48 | doFinal(...) : byte[] | MAC |
+| Test.java:36:47:36:55 | actualMac | Test.java:34:25:34:33 | actualMac : byte[] | Test.java:36:47:36:55 | actualMac | Timing attack against $@ validation. | Test.java:34:25:34:33 | actualMac : byte[] | MAC |
+| Test.java:61:44:61:52 | signature | Test.java:59:32:59:44 | sign(...) : byte[] | Test.java:61:44:61:52 | signature | Timing attack against $@ validation. | Test.java:59:32:59:44 | sign(...) : byte[] | signature |
+| Test.java:75:44:75:52 | signature | Test.java:73:25:73:33 | signature : byte[] | Test.java:75:44:75:52 | signature | Timing attack against $@ validation. | Test.java:73:25:73:33 | signature : byte[] | signature |
+| Test.java:101:49:101:51 | tag | Test.java:99:26:99:45 | doFinal(...) : byte[] | Test.java:101:49:101:51 | tag | Timing attack against $@ validation. | Test.java:99:26:99:45 | doFinal(...) : byte[] | ciphertext |
+| Test.java:118:44:118:46 | tag | Test.java:116:28:116:30 | tag : byte[] | Test.java:118:44:118:46 | tag | Timing attack against $@ validation. | Test.java:116:28:116:30 | tag : byte[] | ciphertext |
+| Test.java:136:44:136:54 | array(...) | Test.java:134:56:134:58 | tag : ByteBuffer | Test.java:136:44:136:54 | array(...) | Timing attack against $@ validation. | Test.java:134:56:134:58 | tag : ByteBuffer | ciphertext |
+| Test.java:150:53:150:55 | tag | Test.java:148:56:148:58 | tag : ByteBuffer | Test.java:150:53:150:55 | tag | Timing attack against $@ validation. | Test.java:148:56:148:58 | tag : ByteBuffer | ciphertext |
+| Test.java:176:44:176:46 | tag | Test.java:174:26:174:50 | doFinal(...) : byte[] | Test.java:176:44:176:46 | tag | Timing attack against $@ validation. | Test.java:174:26:174:50 | doFinal(...) : byte[] | ciphertext |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.java b/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.java
new file mode 100644
index 00000000000..0755f1fe668
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.java
@@ -0,0 +1,236 @@
+import java.io.InputStream;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Arrays;
+import java.util.Objects;
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+
+public class Test {
+
+ // BAD: compare MACs using a non-constant-time method
+ public boolean unsafeMacCheckWithArrayEquals(Socket socket) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ byte[] data = new byte[1024];
+ is.read(data);
+ byte[] actualMac = mac.doFinal(data);
+ byte[] expectedMac = is.readNBytes(32);
+ return Arrays.equals(expectedMac, actualMac);
+ }
+ }
+
+ // BAD: compare MACs using a non-constant-time method
+ public boolean unsafeMacCheckWithDoFinalWithOutputArray(Socket socket) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] data = is.readNBytes(100);
+ Mac mac = Mac.getInstance("HmacSHA256");
+ byte[] actualMac = new byte[256];
+ mac.update(data);
+ mac.doFinal(actualMac, 0);
+ byte[] expectedMac = socket.getInputStream().readNBytes(256);
+ return Arrays.equals(expectedMac, actualMac);
+ }
+ }
+
+ // GOOD: compare MACs using a constant-time method
+ public boolean saferMacCheck(Socket socket) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ byte[] data = new byte[1024];
+ is.read(data);
+ byte[] actualMac = mac.doFinal(data);
+ byte[] expectedMac = is.readNBytes(32);
+ return MessageDigest.isEqual(expectedMac, actualMac);
+ }
+ }
+
+ // BAD: compare signatures using a non-constant-time method
+ public boolean unsafeCheckSignatures(Socket socket, PrivateKey key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Signature engine = Signature.getInstance("SHA256withRSA");
+ engine.initSign(key);
+ byte[] data = socket.getInputStream().readAllBytes();
+ engine.update(data);
+ byte[] signature = engine.sign();
+ byte[] expected = is.readNBytes(256);
+ return Arrays.equals(expected, signature);
+ }
+ }
+
+ // BAD: compare signatures using a non-constant-time method
+ public boolean unsafeCheckSignaturesWithOutputArray(Socket socket, PrivateKey key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Signature engine = Signature.getInstance("SHA256withRSA");
+ engine.initSign(key);
+ byte[] data = socket.getInputStream().readAllBytes();
+ engine.update(data);
+ byte[] signature = new byte[1024];
+ engine.sign(signature, 0, 1024);
+ byte[] expected = is.readNBytes(256);
+ return Arrays.equals(expected, signature);
+ }
+ }
+
+ // GOOD: compare signatures using a constant-time method
+ public boolean saferCheckSignatures(Socket socket, PrivateKey key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Signature engine = Signature.getInstance("SHA256withRSA");
+ engine.initSign(key);
+ byte[] data = socket.getInputStream().readAllBytes();
+ engine.update(data);
+ byte[] signature = engine.sign();
+ byte[] expected = is.readNBytes(256);
+ return MessageDigest.isEqual(expected, signature);
+ }
+ }
+
+ // BAD: compare ciphertexts (custom MAC) using a non-constant-time method
+ public boolean unsafeCheckCiphertext(Socket socket, Key key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] plaintext = is.readNBytes(100);
+ byte[] hash = MessageDigest.getInstance("SHA-256").digest(plaintext);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] tag = cipher.doFinal(hash);
+ byte[] expected = socket.getInputStream().readAllBytes();
+ return Objects.deepEquals(expected, tag);
+ }
+ }
+
+ // BAD: compare ciphertexts (custom MAC) using a non-constant-time method
+ public boolean unsafeCheckCiphertextWithOutputArray(Socket socket, Key key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] plaintext = socket.getInputStream().readAllBytes();
+ MessageDigest md = MessageDigest.getInstance("SHA-512");
+ md.update(plaintext);
+ byte[] hash = md.digest();
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ cipher.update(hash);
+ byte[] tag = new byte[1024];
+ cipher.doFinal(tag, 0);
+ byte[] expected = is.readNBytes(32);
+ return Arrays.equals(expected, tag);
+ }
+ }
+
+ // BAD: compare ciphertexts (custom MAC) using a non-constant-time method
+ public boolean unsafeCheckCiphertextWithByteBuffer(Socket socket, Key key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] plaintext = is.readNBytes(300);
+ MessageDigest md = MessageDigest.getInstance("SHA-512");
+ md.update(plaintext);
+ byte[] hash = new byte[1024];
+ md.digest(hash, 0, hash.length);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ cipher.update(hash);
+ ByteBuffer tag = ByteBuffer.wrap(new byte[1024]);
+ cipher.doFinal(ByteBuffer.wrap(plaintext), tag);
+ byte[] expected = socket.getInputStream().readNBytes(1024);
+ return Arrays.equals(expected, tag.array());
+ }
+ }
+
+ // BAD: compare ciphertexts (custom MAC) using a non-constant-time method
+ public boolean unsafeCheckCiphertextWithByteBufferEquals(Socket socket, Key key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] plaintext = socket.getInputStream().readAllBytes();
+ cipher.update(plaintext);
+ ByteBuffer tag = ByteBuffer.wrap(new byte[1024]);
+ cipher.doFinal(ByteBuffer.wrap(plaintext), tag);
+ byte[] expected = is.readNBytes(32);
+ return ByteBuffer.wrap(expected).equals(tag);
+ }
+ }
+
+ // GOOD: compare ciphertexts (custom MAC) using a constant-time method
+ public boolean saferCheckCiphertext(Socket socket, Key key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] plaintext = is.readNBytes(200);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] hash = MessageDigest.getInstance("SHA-256").digest(plaintext);
+ byte[] tag = cipher.doFinal(hash);
+ byte[] expected = socket.getInputStream().readAllBytes();
+ return MessageDigest.isEqual(expected, tag);
+ }
+ }
+
+ // GOOD: compare ciphertexts using a constant-time method, but no user input
+ // but NonConstantTimeCheckOnSignature.ql still detects it
+ public boolean noUserInputWhenCheckingCiphertext(Socket socket, Key key) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] plaintext = is.readNBytes(100);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] tag = cipher.doFinal(plaintext);
+ byte[] expected = is.readNBytes(32);
+ return Arrays.equals(expected, tag);
+ }
+ }
+
+ // GOOD: compare MAC with constant using a constant-time method
+ public boolean compareMacWithConstant(Socket socket) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ byte[] data = new byte[1024];
+ socket.getInputStream().read(data);
+ byte[] actualMac = mac.doFinal(data);
+ return "constant".equals(new String(actualMac));
+ }
+ }
+
+ // BAD: compare MAC using a non-constant-time loop
+ public boolean unsafeMacCheckWithLoop(Socket socket) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] data = new byte[256];
+ byte[] tag = new byte[32];
+
+ is.read(data);
+ is.read(tag);
+
+ Mac mac = Mac.getInstance("Hmac256");
+ byte[] computedTag = mac.doFinal(data);
+
+ for (int i = 0; i < computedTag.length; i++) {
+ byte a = computedTag[i];
+ byte b = tag[i];
+ if (a != b) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ // GOOD: compare MAC using a constant-time loop
+ public boolean safeMacCheckWithLoop(Socket socket) throws Exception {
+ try (InputStream is = socket.getInputStream()) {
+ byte[] data = new byte[256];
+ byte[] tag = new byte[32];
+
+ is.read(data);
+ is.read(tag);
+
+ Mac mac = Mac.getInstance("Hmac256");
+ byte[] computedTag = mac.doFinal(data);
+
+ int result = 0;
+ for (int i = 0; i < computedTag.length; i++) {
+ result |= computedTag[i] ^ tag[i];
+ }
+
+ return result == 0;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.qlref b/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.qlref
new file mode 100644
index 00000000000..0ed112d4d1f
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-208/TimingAttackAgainstSignagure/Test.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE/CWE-208/TimingAttackAgainstSignature.ql
\ No newline at end of file