diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@
 src/*.css
 .idea
 h-gpgme.iml
+dist-newstyle/
+dist-docs.*/
diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
--- a/CHANGELOG.markdown
+++ b/CHANGELOG.markdown
@@ -1,5 +1,15 @@
 # Changelog
 
+## 0.6.1.0
+
+### New Features
+
+- feat: performance improvement / copy more than one byte from gpgme_data: https://github.com/rethab/h-gpgme/pull/61
+
+### Maintenance
+
+- chore(ci): Run tests without docker-compose in CI & cross-test with various GHC versions: https://github.com/rethab/h-gpgme/pull/60
+
 ## 0.6.0.0
 
 ### New Features
diff --git a/h-gpgme.cabal b/h-gpgme.cabal
--- a/h-gpgme.cabal
+++ b/h-gpgme.cabal
@@ -1,5 +1,5 @@
 Name:                h-gpgme
-Version:             0.6.0.0
+Version:             0.6.1.0
 Description:         High Level Binding for GnuPG Made Easy (gpgme): A Haskell API for the gpgme C library.
 Synopsis:            High Level Binding for GnuPG Made Easy (gpgme)
 License:             MIT
@@ -9,7 +9,9 @@
 Copyright:           (c) Reto 2022
 Homepage:            https://github.com/rethab/h-gpgme
 Bug-reports:         https://github.com/rethab/h-gpgme/issues
-Tested-With:         GHC==7.10.3
+Tested-With:           GHC==8.8
+                     , GHC==9.0
+                     , GHC==9.2
 Category:            Cryptography
 Build-Type:          Simple
 Cabal-Version:       >=1.10
diff --git a/src/Crypto/Gpgme/Internal.hs b/src/Crypto/Gpgme/Internal.hs
--- a/src/Crypto/Gpgme/Internal.hs
+++ b/src/Crypto/Gpgme/Internal.hs
@@ -3,7 +3,10 @@
 import Bindings.Gpgme
 import Control.Monad (unless)
 import qualified Data.ByteString as BS
-import Foreign (allocaBytes, castPtr, nullPtr, peek, Ptr, malloc)
+import qualified Data.ByteString.Internal as BS (createAndTrim)
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.ByteString.Lazy.Internal as LBS (defaultChunkSize)
+import Foreign (castPtr, nullPtr, peek, Ptr, malloc)
 import Foreign.C.String (peekCString)
 import Foreign.C.Types (CUInt, CInt)
 import System.IO.Unsafe (unsafePerformIO)
@@ -19,20 +22,35 @@
             rest <- go (c'_gpgme_invalid_key'next invalid)
             return ((fpr, reason) : rest)
 
+-- | Read the buffer into a ByteString.
+--
+-- Chunks of Data.ByteString.Lazy.Internal.defaultChunkSize are allocated and
+-- copied, until the gpgme_data_readbuffer read returns less than this.
+-- Then the list of chunks are copied into a strict ByteString by way of a lazy
+-- ByteString.
 collectResult :: C'gpgme_data_t -> BS.ByteString
 collectResult dat' = unsafePerformIO $ do
     -- make sure we start at the beginning
     _ <- c'gpgme_data_seek dat' 0 seekSet
-    go dat'
-  where go :: C'gpgme_data_t -> IO BS.ByteString
-        go dat = allocaBytes 1 $ \buf ->
-                    do read_bytes <- c'gpgme_data_read dat buf 1
-                       if read_bytes == 1
-                          then do byte <- peek (castPtr buf)
-                                  rest <- go dat
-                                  return (byte `BS.cons` rest)
-                          else return BS.empty
+    chunks <- go dat'
+    pure $ LBS.toStrict (LBS.fromChunks chunks)
+  where makeChunk :: C'gpgme_data_t -> IO BS.ByteString
+        makeChunk dat = BS.createAndTrim chunkSize $ \buf -> do
+          -- createAndTrim gives a Ptr Word8 but the gpgme functions wants a Ptr ()
+          read_bytes <- c'gpgme_data_read dat (castPtr buf) (fromIntegral chunkSize)
+          pure $ fromIntegral read_bytes
+
+        go :: C'gpgme_data_t -> IO [BS.ByteString]
+        go dat = do
+          bs <- makeChunk dat
+          if BS.length bs < chunkSize
+          then pure [bs]
+          else do
+            bss <- go dat
+            pure (bs : bss)
+
         seekSet = 0
+        chunkSize = LBS.defaultChunkSize
 
 -- ^ Unsafe IO version of `collectSignatures`. Try to use `collectSignatures'` instead.
 collectSignatures :: C'gpgme_ctx_t -> VerificationResult
