diff --git a/ChangeLog b/ChangeLog
index 136fc78f2c2604071a55063bd7159bf8ac28dccc..5cc379018947ea013feba536d1282f7a0fd7ce5a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -49,6 +49,28 @@
 	* tools/sexp-conv-test: Likewise.
 	* tools/pkcs1-conv-test: Likewise.
 
+2023-08-05  Niels Möller  <nisse@lysator.liu.se>
+
+	* testsuite/testutils.c (mark_bytes_undefined)
+	(mark_bytes_defined): New functions. Update side-channel related
+	tests to use them.
+	(main): Check environment variable NETTLE_TEST_SIDE_CHANNEL.
+	(test_side_channel): New global variable.
+
+	* testsuite/sc-valgrind.sh (with_valgrind): New file, new shell
+	utility function.
+
+	* testsuite/sc-pkcs1-sec-decrypt-test: New test, for side channel
+	silence.
+	* testsuite/sc-memeql-test: Likewise.
+	* testsuite/sc-gcm-test: Likewise.
+	* testsuite/sc-cnd-memcpy-test: Likewise.
+	* testsuite/rsa-sec-decrypt-test: Likewise.
+
+	* rsa-sec-decrypt.c (_rsa_sec_decrypt): New internal function,
+	without input range checks.
+	(rsa_sec_decrypt): Use it.
+
 2023-08-02  Niels Möller  <nisse@lysator.liu.se>
 
 	* configure.ac: Replace obsoleted macros, require autoconf-2.69,
diff --git a/rsa-internal.h b/rsa-internal.h
index f66a7df049b675ac471d345d56000a2180fc31f6..ed4ebe887397e0588b772bd982884fb61b810369 100644
--- a/rsa-internal.h
+++ b/rsa-internal.h
@@ -44,6 +44,7 @@
 #define _rsa_sec_compute_root_itch _nettle_rsa_sec_compute_root_itch
 #define _rsa_sec_compute_root _nettle_rsa_sec_compute_root
 #define _rsa_sec_compute_root_tr _nettle_rsa_sec_compute_root_tr
+#define _rsa_sec_decrypt _nettle_rsa_sec_decrypt
 
 /* Internal functions. */
 int
@@ -85,4 +86,13 @@ _rsa_sec_compute_root_tr(const struct rsa_public_key *pub,
 			 void *random_ctx, nettle_random_func *random,
 			 mp_limb_t *x, const mp_limb_t *m);
 
+/* Variant without range check of the input, to ease testing for
+   side-channel silence. */
+int
+_rsa_sec_decrypt (const struct rsa_public_key *pub,
+		  const struct rsa_private_key *key,
+		  void *random_ctx, nettle_random_func *random,
+		  size_t length, uint8_t *message,
+		  const mpz_t gibberish);
+
 #endif /* NETTLE_RSA_INTERNAL_H_INCLUDED */
diff --git a/rsa-sec-decrypt.c b/rsa-sec-decrypt.c
index 4c98958dd52863dfc2682dfa60532bd4980f44d8..e2f953e280f5ff3178876b142e073c4b43a309ea 100644
--- a/rsa-sec-decrypt.c
+++ b/rsa-sec-decrypt.c
@@ -44,21 +44,19 @@
 
 #include "gmp-glue.h"
 
+/* Variant without range check of the input, to ease testing for
+   side-channel silence. */
 int
-rsa_sec_decrypt(const struct rsa_public_key *pub,
-	        const struct rsa_private_key *key,
-	        void *random_ctx, nettle_random_func *random,
-	        size_t length, uint8_t *message,
-	        const mpz_t gibberish)
+_rsa_sec_decrypt (const struct rsa_public_key *pub,
+		  const struct rsa_private_key *key,
+		  void *random_ctx, nettle_random_func *random,
+		  size_t length, uint8_t *message,
+		  const mpz_t gibberish)
 {
   TMP_GMP_DECL (m, mp_limb_t);
   TMP_GMP_DECL (em, uint8_t);
   int res;
 
-  /* First check that input is in range. */
-  if (mpz_sgn (gibberish) < 0 || mpz_cmp (gibberish, pub->n) >= 0)
-    return 0;
-
   TMP_GMP_ALLOC (m, mpz_size(pub->n));
   TMP_GMP_ALLOC (em, key->size);
 
@@ -78,3 +76,16 @@ rsa_sec_decrypt(const struct rsa_public_key *pub,
   return res;
 }
 
+int
+rsa_sec_decrypt (const struct rsa_public_key *pub,
+		 const struct rsa_private_key *key,
+		 void *random_ctx, nettle_random_func *random,
+		 size_t length, uint8_t *message,
+		 const mpz_t gibberish)
+{
+  /* First check that input is in range. */
+  if (mpz_sgn (gibberish) < 0 || mpz_cmp (gibberish, pub->n) >= 0)
+    return 0;
+
+  return _rsa_sec_decrypt (pub, key, random_ctx, random, length, message, gibberish);
+}
diff --git a/testsuite/Makefile.in b/testsuite/Makefile.in
index 2aa1dd810d918649be28929b05048e65f02ea93a..ff491c4a34972207c709c3dc486564a5022bf1ba 100644
--- a/testsuite/Makefile.in
+++ b/testsuite/Makefile.in
@@ -66,7 +66,9 @@ TS_HOGWEED = $(TS_HOGWEED_SOURCES:.c=$(EXEEXT))
 TS_C = $(TS_NETTLE) @IF_HOGWEED@ $(TS_HOGWEED)
 TS_CXX = @IF_CXX@ $(CXX_SOURCES:.cxx=$(EXEEXT))
 TARGETS = $(TS_C) $(TS_CXX)
-TS_SH = symbols-test
+TS_SC = sc-cnd-memcpy-test sc-gcm-test sc-memeql-test \
+	@IF_HOGWEED@ sc-pkcs1-sec-decrypt-test sc-rsa-sec-decrypt-test
+TS_SH = $(TS_SC) symbols-test
 TS_ALL = $(TARGETS) $(TS_SH) @IF_DLOPEN_TEST@ dlopen-test$(EXEEXT)
 
 TS_FAT = $(patsubst %, %$(EXEEXT), aes-test cbc-test \
@@ -127,7 +129,7 @@ $(TARGETS) $(EXTRA_TARGETS): testutils.$(OBJEXT) ../nettle-internal.$(OBJEXT) \
 # data.
 VALGRIND = valgrind --error-exitcode=1 --leak-check=full --show-reachable=yes @IF_ASM@ --partial-loads-ok=yes
 
-check: $(TS_ALL)
+check: $(TS_ALL) $(TS_ALL:sc-%=%)
 	TEST_SHLIB_DIR="$(TEST_SHLIB_DIR)" \
 	  srcdir="$(srcdir)" \
 	  EMULATOR="$(EMULATOR)" NM="$(NM)" EXEEXT="$(EXEEXT)" \
diff --git a/testsuite/cnd-memcpy-test.c b/testsuite/cnd-memcpy-test.c
index 6e5db3413dfbe05127db79ce2748e6a6aa232563..466608d3b539c7294d1aed8f1879e5d4859da474 100644
--- a/testsuite/cnd-memcpy-test.c
+++ b/testsuite/cnd-memcpy-test.c
@@ -2,24 +2,19 @@
 #include "knuth-lfib.h"
 #include "memops.h"
 
-#if HAVE_VALGRIND_MEMCHECK_H
-# include <valgrind/memcheck.h>
 static void
 cnd_memcpy_for_test(int cnd, void *dst, const void *src, size_t n)
 {
   /* Makes valgrind trigger on any branches depending on the input
      data. */
-  VALGRIND_MAKE_MEM_UNDEFINED (dst, n);
-  VALGRIND_MAKE_MEM_UNDEFINED (src, n);
-  VALGRIND_MAKE_MEM_UNDEFINED (&cnd, sizeof(cnd));
+  mark_bytes_undefined (n, dst);
+  mark_bytes_undefined (n, src);
+  mark_bytes_undefined (sizeof(cnd), &cnd);
 
   cnd_memcpy (cnd, dst, src, n);
-  VALGRIND_MAKE_MEM_DEFINED (src, n);
-  VALGRIND_MAKE_MEM_DEFINED (dst, n);
+  mark_bytes_defined (n, src);
+  mark_bytes_defined (n, dst);
 }
-#else
-#define cnd_memcpy_for_test cnd_memcpy
-#endif
 
 #define MAX_SIZE 50
 void
diff --git a/testsuite/gcm-test.c b/testsuite/gcm-test.c
index bc555d60819bff5ae078df9743e891e76115e9a3..023ff6f62f9ce589e6f06fb8ea32c55db53a99eb 100644
--- a/testsuite/gcm-test.c
+++ b/testsuite/gcm-test.c
@@ -6,13 +6,6 @@
 #include "gcm.h"
 #include "ghash-internal.h"
 
-#if HAVE_VALGRIND_MEMCHECK_H
-# include <valgrind/memcheck.h>
-#else
-# define VALGRIND_MAKE_MEM_UNDEFINED(p, n)
-# define VALGRIND_MAKE_MEM_DEFINED(p, n)
-#endif
-
 static void
 test_gcm_hash (const struct tstring *msg, const struct tstring *ref)
 {
@@ -49,19 +42,19 @@ test_ghash_internal (const struct tstring *key,
   struct gcm_key gcm_key;
   union nettle_block16 state;
 
-  /* Use VALGRIND_MAKE_MEM_DEFINED to mark inputs as "undefined", to
-     get valgrind to warn about any branches or memory accesses
-     depending on secret data. */
+  /* Mark inputs as "undefined" to valgrind, to get warnings about any
+     branches or memory accesses depending on secret data. */
   memcpy (state.b, key->data, GCM_BLOCK_SIZE);
-  VALGRIND_MAKE_MEM_UNDEFINED (&state, sizeof(state));
+  mark_bytes_undefined (sizeof(state), &state);
   _ghash_set_key (&gcm_key, &state);
 
   memcpy (state.b, iv->data, GCM_BLOCK_SIZE);
-  VALGRIND_MAKE_MEM_UNDEFINED (&state, sizeof(state));
-  VALGRIND_MAKE_MEM_UNDEFINED (message->data, message->length);
+  mark_bytes_undefined (sizeof(state), &state);
+  mark_bytes_undefined (message->length, message->data);
   _ghash_update (&gcm_key, &state, message->length / GCM_BLOCK_SIZE, message->data);
-  VALGRIND_MAKE_MEM_DEFINED (&state, sizeof(state));
-  VALGRIND_MAKE_MEM_DEFINED (message->data, message->length);
+  mark_bytes_defined (sizeof(state), &state);
+  mark_bytes_defined (message->length, message->data);
+
   if (!MEMEQ(GCM_BLOCK_SIZE, state.b, digest->data))
     {
       fprintf (stderr, "gcm_hash (internal) failed\n");
diff --git a/testsuite/memeql-test.c b/testsuite/memeql-test.c
index 356671d6018bb61ab135749d1d0ad45aefd3abd5..98cd8a0cd12a9c5c5e6550ff31b96835c4e1e8ae 100644
--- a/testsuite/memeql-test.c
+++ b/testsuite/memeql-test.c
@@ -2,8 +2,6 @@
 #include "knuth-lfib.h"
 #include "memops.h"
 
-#if HAVE_VALGRIND_MEMCHECK_H
-# include <valgrind/memcheck.h>
 static int
 memeql_sec_for_test(const void *a, const void *b, size_t n)
 {
@@ -11,16 +9,13 @@ memeql_sec_for_test(const void *a, const void *b, size_t n)
 
   /* Makes valgrind trigger on any branches depending on the input
      data. */
-  VALGRIND_MAKE_MEM_UNDEFINED (a, n);
-  VALGRIND_MAKE_MEM_UNDEFINED (b, n);
+  mark_bytes_undefined (n, a);
+  mark_bytes_undefined (n, b);
 
   res = memeql_sec (a, b, n);
-  VALGRIND_MAKE_MEM_DEFINED (&res, sizeof(res));
+  mark_bytes_defined (sizeof(res), &res);
   return res;
 }
-#else
-#define memeql_sec_for_test memeql_sec
-#endif
 
 #define MAX_SIZE 50
 void
diff --git a/testsuite/pkcs1-sec-decrypt-test.c b/testsuite/pkcs1-sec-decrypt-test.c
index c7fcdcb602c3fc5e0dc927b26f97f47fcf6624b8..28189382a4ef12b40f79cd1bd244c058ee990388 100644
--- a/testsuite/pkcs1-sec-decrypt-test.c
+++ b/testsuite/pkcs1-sec-decrypt-test.c
@@ -2,28 +2,23 @@
 
 #include "pkcs1-internal.h"
 
-#if HAVE_VALGRIND_MEMCHECK_H
-# include <valgrind/memcheck.h>
 static int
 pkcs1_decrypt_for_test(size_t msg_len, uint8_t *msg,
                        size_t pad_len, uint8_t *pad)
 {
   int ret;
 
-  VALGRIND_MAKE_MEM_UNDEFINED (msg, msg_len);
-  VALGRIND_MAKE_MEM_UNDEFINED (pad, pad_len);
+  mark_bytes_undefined (msg_len, msg);
+  mark_bytes_undefined (pad_len, pad);
 
   ret = _pkcs1_sec_decrypt (msg_len, msg, pad_len, pad);
 
-  VALGRIND_MAKE_MEM_DEFINED (msg, msg_len);
-  VALGRIND_MAKE_MEM_DEFINED (pad, pad_len);
-  VALGRIND_MAKE_MEM_DEFINED (&ret, sizeof (ret));
+  mark_bytes_defined (msg_len, msg);
+  mark_bytes_defined (pad_len, pad);
+  mark_bytes_defined (sizeof (ret), &ret);
 
   return ret;
 }
-#else
-#define pkcs1_decrypt_for_test _pkcs1_sec_decrypt
-#endif
 
 void
 test_main(void)
diff --git a/testsuite/rsa-sec-decrypt-test.c b/testsuite/rsa-sec-decrypt-test.c
index be7ab5fb57e49bbad6cb5a12b13bc12e33af2121..f257723bb5dfef31d1a6a6e83fd2068a2b8e31a4 100644
--- a/testsuite/rsa-sec-decrypt-test.c
+++ b/testsuite/rsa-sec-decrypt-test.c
@@ -1,17 +1,15 @@
 #include "testutils.h"
 
 #include "rsa.h"
+#include "rsa-internal.h"
 #include "knuth-lfib.h"
 
-#if HAVE_VALGRIND_MEMCHECK_H
-# include <valgrind/memcheck.h>
+#define MARK_MPZ_LIMBS_UNDEFINED(x) \
+  mark_bytes_undefined (mpz_size (x) * sizeof (mp_limb_t), mpz_limbs_read (x))
+
+#define MARK_MPZ_LIMBS_DEFINED(x) \
+  mark_bytes_defined (mpz_size (x) * sizeof (mp_limb_t), mpz_limbs_read (x))
 
-#define MARK_MPZ_LIMBS_UNDEFINED(parm) \
-  VALGRIND_MAKE_MEM_UNDEFINED (mpz_limbs_read (parm), \
-                               mpz_size (parm) * sizeof (mp_limb_t))
-#define MARK_MPZ_LIMBS_DEFINED(parm) \
-  VALGRIND_MAKE_MEM_DEFINED (mpz_limbs_read (parm), \
-                               mpz_size (parm) * sizeof (mp_limb_t))
 static int
 rsa_decrypt_for_test(const struct rsa_public_key *pub,
                      const struct rsa_private_key *key,
@@ -20,6 +18,9 @@ rsa_decrypt_for_test(const struct rsa_public_key *pub,
                      const mpz_t gibberish)
 {
   int ret;
+  if (!test_side_channel)
+    return rsa_sec_decrypt (pub, key, random_ctx, random, length, message, gibberish);
+
   /* Makes valgrind trigger on any branches depending on the input
      data. Except that (i) we have to allow rsa_sec_compute_root_tr to
      check that p and q are odd, (ii) mpn_sec_div_r may leak
@@ -27,20 +28,21 @@ rsa_decrypt_for_test(const struct rsa_public_key *pub,
      normalization check and table lookup in invert_limb, and (iii)
      mpn_sec_powm may leak information about the least significant
      bits of p and q, due to table lookup in binvert_limb. */
-  VALGRIND_MAKE_MEM_UNDEFINED (message, length);
+  mark_bytes_undefined (length, message);
   MARK_MPZ_LIMBS_UNDEFINED(gibberish);
   MARK_MPZ_LIMBS_UNDEFINED(key->a);
   MARK_MPZ_LIMBS_UNDEFINED(key->b);
   MARK_MPZ_LIMBS_UNDEFINED(key->c);
-  VALGRIND_MAKE_MEM_UNDEFINED(mpz_limbs_read (key->p) + 1,
-			      (mpz_size (key->p) - 3) * sizeof(mp_limb_t));
-  VALGRIND_MAKE_MEM_UNDEFINED(mpz_limbs_read (key->q) + 1,
-			      (mpz_size (key->q) - 3) * sizeof(mp_limb_t));
+  mark_bytes_undefined ((mpz_size (key->p) - 3) * sizeof(mp_limb_t),
+			mpz_limbs_read (key->p) + 1);
+  mark_bytes_undefined((mpz_size (key->q) - 3) * sizeof(mp_limb_t), 
+		       mpz_limbs_read (key->q) + 1);
 
-  ret = rsa_sec_decrypt (pub, key, random_ctx, random, length, message, gibberish);
+  /* Call variant not checking that 0 <= gibberish < n. */
+  ret = _rsa_sec_decrypt (pub, key, random_ctx, random, length, message, gibberish);
 
-  VALGRIND_MAKE_MEM_DEFINED (message, length);
-  VALGRIND_MAKE_MEM_DEFINED (&ret, sizeof(ret));
+  mark_bytes_defined (length, message);
+  mark_bytes_defined (sizeof(ret), &ret);
   MARK_MPZ_LIMBS_DEFINED(gibberish);
   MARK_MPZ_LIMBS_DEFINED(key->a);
   MARK_MPZ_LIMBS_DEFINED(key->b);
@@ -50,9 +52,6 @@ rsa_decrypt_for_test(const struct rsa_public_key *pub,
 
   return ret;
 }
-#else
-#define rsa_decrypt_for_test rsa_sec_decrypt
-#endif
 
 #define PAYLOAD_SIZE 50
 #define DECRYPTED_SIZE 256
diff --git a/testsuite/sc-cnd-memcpy-test b/testsuite/sc-cnd-memcpy-test
new file mode 100755
index 0000000000000000000000000000000000000000..056bbdaab060392d2a5a168ec09981f62e1d6e2a
--- /dev/null
+++ b/testsuite/sc-cnd-memcpy-test
@@ -0,0 +1,6 @@
+#! /bin/sh
+
+srcdir=`dirname $0`
+. "${srcdir}/sc-valgrind.sh"
+
+with_valgrind ./cnd-memcpy-test
diff --git a/testsuite/sc-gcm-test b/testsuite/sc-gcm-test
new file mode 100755
index 0000000000000000000000000000000000000000..57e39511feadecf50e5570b6f91d63c00436452a
--- /dev/null
+++ b/testsuite/sc-gcm-test
@@ -0,0 +1,6 @@
+#! /bin/sh
+
+srcdir=`dirname $0`
+. "${srcdir}/sc-valgrind.sh"
+
+with_valgrind ./gcm-test
diff --git a/testsuite/sc-memeql-test b/testsuite/sc-memeql-test
new file mode 100755
index 0000000000000000000000000000000000000000..a6dfcbefa18fb5eef30a7ebdccfe7d77d74a0de7
--- /dev/null
+++ b/testsuite/sc-memeql-test
@@ -0,0 +1,6 @@
+#! /bin/sh
+
+srcdir=`dirname $0`
+. "${srcdir}/sc-valgrind.sh"
+
+with_valgrind ./memeql-test
diff --git a/testsuite/sc-pkcs1-sec-decrypt-test b/testsuite/sc-pkcs1-sec-decrypt-test
new file mode 100755
index 0000000000000000000000000000000000000000..2e0ddc34ca1af4bdf069c70a2bd20a0c0d9523d0
--- /dev/null
+++ b/testsuite/sc-pkcs1-sec-decrypt-test
@@ -0,0 +1,6 @@
+#! /bin/sh
+
+srcdir=`dirname $0`
+. "${srcdir}/sc-valgrind.sh"
+
+with_valgrind ./pkcs1-sec-decrypt-test
diff --git a/testsuite/sc-rsa-sec-decrypt-test b/testsuite/sc-rsa-sec-decrypt-test
new file mode 100755
index 0000000000000000000000000000000000000000..0453ce2525c44f619fe9aac6db74b50edc742d06
--- /dev/null
+++ b/testsuite/sc-rsa-sec-decrypt-test
@@ -0,0 +1,6 @@
+#! /bin/sh
+
+srcdir=`dirname $0`
+. "${srcdir}/sc-valgrind.sh"
+
+with_valgrind ./rsa-sec-decrypt-test
diff --git a/testsuite/sc-valgrind.sh b/testsuite/sc-valgrind.sh
new file mode 100644
index 0000000000000000000000000000000000000000..39e2e941797232f0145e2ec6016a1d9612d7dbeb
--- /dev/null
+++ b/testsuite/sc-valgrind.sh
@@ -0,0 +1,7 @@
+# To setup a test to check for branches or memory accesses depending on secret data,
+# using valgrind.
+
+with_valgrind () {
+    type valgrind >/dev/null || exit 77
+    NETTLE_TEST_SIDE_CHANNEL=1 valgrind -q --error-exitcode=1 "$@"
+}
diff --git a/testsuite/testutils.c b/testsuite/testutils.c
index 3420ae9d20ae97131977201f8857a6f68c48b856..1a8d10a9f9f56e65c613c85d0da2fac0eefbaf70 100644
--- a/testsuite/testutils.c
+++ b/testsuite/testutils.c
@@ -15,6 +15,11 @@
 #include <ctype.h>
 #include <sys/time.h>
 
+#if HAVE_VALGRIND_MEMCHECK_H
+# include <valgrind/memcheck.h>
+# include <valgrind/valgrind.h>
+#endif
+
 void
 die(const char *format, ...)
 {
@@ -119,6 +124,28 @@ print_hex(size_t length, const uint8_t *data)
 
 int verbose = 0;
 
+#if HAVE_VALGRIND_MEMCHECK_H
+int test_side_channel = 0;
+
+void
+mark_bytes_undefined (size_t size, const void *p)
+{
+  if (test_side_channel)
+    VALGRIND_MAKE_MEM_UNDEFINED(p, size);
+}
+void
+mark_bytes_defined (size_t size, const void *p)
+{
+  if (test_side_channel)
+    VALGRIND_MAKE_MEM_DEFINED(p, size);
+}
+#else
+void
+mark_bytes_undefined (size_t size, const void *p) {}
+void
+mark_bytes_defined (size_t size, const void *p) {}
+#endif
+
 int
 main(int argc, char **argv)
 {
@@ -134,6 +161,15 @@ main(int argc, char **argv)
 	}
     }
 
+  if (getenv("NETTLE_TEST_SIDE_CHANNEL"))
+    {
+#if HAVE_VALGRIND_MEMCHECK_H
+      if (RUNNING_ON_VALGRIND)
+	test_side_channel = 1;
+      else
+#endif
+	SKIP();
+    }
   test_main();
 
   tstring_clear();
diff --git a/testsuite/testutils.h b/testsuite/testutils.h
index 687bcd7311b446130a837bbfd1f7ea5137633b96..97710fc93817bfebdbdd8c235dfb3a5c7778e0a8 100644
--- a/testsuite/testutils.h
+++ b/testsuite/testutils.h
@@ -73,11 +73,20 @@ tstring_print_hex(const struct tstring *s);
 void
 print_hex(size_t length, const uint8_t *data);
 
+/* If side-channel tests are requested, attach valgrind annotations on
+   given memory area. */
+void
+mark_bytes_undefined (size_t size, const void *p);
+
+void
+mark_bytes_defined (size_t size, const void *p);
+
 /* The main program */
 void
 test_main(void);
 
 extern int verbose;
+extern int test_side_channel;
 
 typedef void
 nettle_encrypt_message_func(void *ctx,