diff --git a/include/libcamera/internal/ipa_manager.h b/include/libcamera/internal/ipa_manager.h
index 7f36e58e8bfa..bf823563c91c 100644
--- a/include/libcamera/internal/ipa_manager.h
+++ b/include/libcamera/internal/ipa_manager.h
@@ -47,6 +47,13 @@ public:
 		return proxy;
 	}
 
+#if HAVE_IPA_PUBKEY
+	static const PubKey &pubKey()
+	{
+		return pubKey_;
+	}
+#endif
+
 private:
 	static IPAManager *self_;
 
diff --git a/src/apps/ipa-verify/main.cpp b/src/apps/ipa-verify/main.cpp
new file mode 100644
index 000000000000..76ba5073d25a
--- /dev/null
+++ b/src/apps/ipa-verify/main.cpp
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2023, Ideas on Board Oy
+ *
+ * ipa_verify.cpp - Verify signature on an IPA module
+ */
+
+#include <iostream>
+#include <libgen.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/span.h>
+
+#include "libcamera/internal/ipa_manager.h"
+#include "libcamera/internal/ipa_module.h"
+
+using namespace libcamera;
+
+namespace {
+
+bool isSignatureValid(IPAModule *ipa)
+{
+	File file{ ipa->path() };
+	if (!file.open(File::OpenModeFlag::ReadOnly))
+		return false;
+
+	Span<uint8_t> data = file.map();
+	if (data.empty())
+		return false;
+
+	return IPAManager::pubKey().verify(data, ipa->signature());
+}
+
+void usage(char *argv0)
+{
+	std::cout << "Usage: " << basename(argv0) << " ipa_name.so" << std::endl;
+	std::cout << std::endl;
+	std::cout << "Verify the signature of an IPA module. The signature file ipa_name.so.sign is" << std::endl;
+	std::cout << "expected to be in the same directory as the IPA module." << std::endl;
+}
+
+} /* namespace */
+
+int main(int argc, char **argv)
+{
+	if (argc != 2) {
+		usage(argv[0]);
+		return EXIT_FAILURE;
+	}
+
+	IPAModule module{ argv[1] };
+	if (!module.isValid()) {
+		std::cout << "Invalid IPA module " << argv[1] << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	if (!isSignatureValid(&module)) {
+		std::cout << "IPA module signature is invalid" << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	std::cout << "IPA module signature is valid" << std::endl;
+	return 0;
+}
diff --git a/src/apps/ipa-verify/meson.build b/src/apps/ipa-verify/meson.build
new file mode 100644
index 000000000000..7fdda3b9af4b
--- /dev/null
+++ b/src/apps/ipa-verify/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: CC0-1.0
+
+if not ipa_sign_module
+    subdir_done()
+endif
+
+ipa_verify_sources = files([
+    'main.cpp',
+])
+
+ipa_verify  = executable('ipa_verify', ipa_verify_sources,
+                         dependencies : [
+                             libcamera_private,
+                         ],
+                         install : false)
diff --git a/src/apps/meson.build b/src/apps/meson.build
index 099876356bd1..af632b9a7b0b 100644
--- a/src/apps/meson.build
+++ b/src/apps/meson.build
@@ -18,3 +18,5 @@ subdir('lc-compliance')
 
 subdir('cam')
 subdir('qcam')
+
+subdir('ipa-verify')
diff --git a/src/libcamera/ipa_manager.cpp b/src/libcamera/ipa_manager.cpp
index ac5397003b50..7a4515d90d7b 100644
--- a/src/libcamera/ipa_manager.cpp
+++ b/src/libcamera/ipa_manager.cpp
@@ -279,6 +279,19 @@ IPAModule *IPAManager::module(PipelineHandler *pipe, uint32_t minVersion,
  * found or if the IPA proxy fails to initialize
  */
 
+#if HAVE_IPA_PUBKEY
+/**
+ * \fn IPAManager::pubKey()
+ * \brief Retrieve the IPA module signing public key
+ *
+ * IPA module signature verification is normally handled internally by the
+ * IPAManager class. This function is meant to be used by utilities that need to
+ * verify signatures externally.
+ *
+ * \return The IPA module signing public key
+ */
+#endif
+
 bool IPAManager::isSignatureValid([[maybe_unused]] IPAModule *ipa) const
 {
 #if HAVE_IPA_PUBKEY
