diff --git a/README.rst b/README.rst
index 3606057ff706..e9dd4207ae55 100644
--- a/README.rst
+++ b/README.rst
@@ -61,7 +61,7 @@ for the libcamera core: [required]
         libyaml-dev python3-yaml python3-ply python3-jinja2
 
 for IPA module signing: [recommended]
-        libgnutls28-dev openssl
+        Either libgnutls28-dev or libssl-dev, openssl
 
         Without IPA module signing, all IPA modules will be isolated in a
         separate process. This adds an unnecessary extra overhead at runtime.
diff --git a/include/libcamera/internal/pub_key.h b/include/libcamera/internal/pub_key.h
index a22ba037cff6..ea7d9af84515 100644
--- a/include/libcamera/internal/pub_key.h
+++ b/include/libcamera/internal/pub_key.h
@@ -11,7 +11,9 @@
 
 #include <libcamera/base/span.h>
 
-#if HAVE_GNUTLS
+#if HAVE_CRYPTO
+struct rsa_st;
+#elif HAVE_GNUTLS
 struct gnutls_pubkey_st;
 #endif
 
@@ -28,7 +30,9 @@ public:
 
 private:
 	bool valid_;
-#if HAVE_GNUTLS
+#if HAVE_CRYPTO
+	struct rsa_st *pubkey_;
+#elif HAVE_GNUTLS
 	struct gnutls_pubkey_st *pubkey_;
 #endif
 };
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index e144d4f9ae70..ce1f0f2f3ef6 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -65,14 +65,22 @@ subdir('pipeline')
 subdir('proxy')
 
 libdl = cc.find_library('dl')
-libgnutls = dependency('gnutls', required : false)
 libudev = dependency('libudev', required : false)
 libyaml = dependency('yaml-0.1', required : false)
 
-if libgnutls.found()
+# Use one of gnutls or libcrypto (provided by OpenSSL), trying gnutls first.
+libcrypto = dependency('gnutls', required : false)
+if libcrypto.found()
     config_h.set('HAVE_GNUTLS', 1)
 else
-    warning('gnutls not found, all IPA modules will be isolated')
+    libcrypto = dependency('libcrypto', required : false)
+    if libcrypto.found()
+        config_h.set('HAVE_CRYPTO', 1)
+    endif
+endif
+
+if not libcrypto.found()
+    warning('Neither gnutls nor libcrypto found, all IPA modules will be isolated')
 endif
 
 if liblttng.found()
@@ -137,8 +145,8 @@ libcamera_deps = [
     libatomic,
     libcamera_base,
     libcamera_base_private,
+    libcrypto,
     libdl,
-    libgnutls,
     liblttng,
     libudev,
     libyaml,
diff --git a/src/libcamera/pub_key.cpp b/src/libcamera/pub_key.cpp
index b2045a103bc0..723f311b91a2 100644
--- a/src/libcamera/pub_key.cpp
+++ b/src/libcamera/pub_key.cpp
@@ -7,7 +7,12 @@
 
 #include "libcamera/internal/pub_key.h"
 
-#if HAVE_GNUTLS
+#if HAVE_CRYPTO
+#include <openssl/bio.h>
+#include <openssl/rsa.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+#elif HAVE_GNUTLS
 #include <gnutls/abstract.h>
 #endif
 
@@ -33,7 +38,14 @@ namespace libcamera {
 PubKey::PubKey([[maybe_unused]] Span<const uint8_t> key)
 	: valid_(false)
 {
-#if HAVE_GNUTLS
+#if HAVE_CRYPTO
+	const uint8_t *data = key.data();
+	pubkey_ = d2i_RSA_PUBKEY(nullptr, &data, key.size());
+	if (!pubkey_)
+		return;
+
+	valid_ = true;
+#elif HAVE_GNUTLS
 	int ret = gnutls_pubkey_init(&pubkey_);
 	if (ret < 0)
 		return;
@@ -52,7 +64,9 @@ PubKey::PubKey([[maybe_unused]] Span<const uint8_t> key)
 
 PubKey::~PubKey()
 {
-#if HAVE_GNUTLS
+#if HAVE_CRYPTO
+	RSA_free(pubkey_);
+#elif HAVE_GNUTLS
 	gnutls_pubkey_deinit(pubkey_);
 #endif
 }
@@ -79,7 +93,20 @@ bool PubKey::verify([[maybe_unused]] Span<const uint8_t> data,
 	if (!valid_)
 		return false;
 
-#if HAVE_GNUTLS
+#if HAVE_CRYPTO
+	/* Calculate the SHA256 digest of the data. */
+	SHA256_CTX ctx;
+	SHA256_Init(&ctx);
+	SHA256_Update(&ctx, data.data(), data.size());
+
+	uint8_t digest[SHA256_DIGEST_LENGTH];
+	SHA256_Final(digest, &ctx);
+
+	/* Decrypt the signature and verify it matches the digest. */
+	int ret = RSA_verify(NID_sha256, digest, SHA256_DIGEST_LENGTH,
+			     sig.data(), sig.size(), pubkey_);
+	return ret == 1;
+#elif HAVE_GNUTLS
 	const gnutls_datum_t gnuTlsData{
 		const_cast<unsigned char *>(data.data()),
 		static_cast<unsigned int>(data.size())
