diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
 # Changelog
 
+- 0.2.1 (2026-01-11)
+  * Adds a basic quickcheck test suite for asserting memory safety of
+    library functions on random inputs.
+
 - 0.2.0 (2026-01-10)
   * The HMAC functions now produce a value of type MAC, which is a
     newtype over a ByteString. The 'Eq' instance for MAC compares values
diff --git a/ppad-sha512.cabal b/ppad-sha512.cabal
--- a/ppad-sha512.cabal
+++ b/ppad-sha512.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-sha512
-version:            0.2.0
+version:            0.2.1
 synopsis:           The SHA-512 and HMAC-SHA512 algorithms
 license:            MIT
 license-file:       LICENSE
@@ -49,6 +49,7 @@
   hs-source-dirs:      test
   main-is:             Main.hs
   other-modules:
+    Property
     Wycheproof
 
   ghc-options:
@@ -60,8 +61,10 @@
     , bytestring
     , ppad-base16
     , ppad-sha512
+    , quickcheck-instances
     , tasty
     , tasty-hunit
+    , tasty-quickcheck
     , text
 
 benchmark sha512-bench
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -14,6 +14,7 @@
 import qualified Data.Text.IO as TIO
 import Test.Tasty
 import Test.Tasty.HUnit
+import qualified Property
 import qualified Wycheproof as W
 
 main :: IO ()
@@ -24,6 +25,7 @@
     Just w  -> defaultMain $ testGroup "ppad-sha512" [
         unit_tests
       , wycheproof_tests w
+      , Property.properties
       ]
 
 wycheproof_tests :: W.Wycheproof -> TestTree
diff --git a/test/Property.hs b/test/Property.hs
new file mode 100644
--- /dev/null
+++ b/test/Property.hs
@@ -0,0 +1,65 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Property (
+    properties
+  ) where
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import Crypto.Hash.SHA512
+import Test.Tasty
+import qualified Test.Tasty.QuickCheck as Q
+import Test.QuickCheck.Instances.ByteString ()
+
+-- strict/lazy hash equivalence
+hash_equiv :: BS.ByteString -> Bool
+hash_equiv bs = hash bs == hash_lazy (BL.fromStrict bs)
+
+-- strict/lazy hmac equivalence
+hmac_equiv :: BS.ByteString -> BS.ByteString -> Bool
+hmac_equiv k m = hmac k m == hmac_lazy k (BL.fromStrict m)
+
+-- hash output is always 64 bytes
+hash_length :: BS.ByteString -> Bool
+hash_length bs = BS.length (hash bs) == 64
+
+-- hmac output is always 64 bytes
+hmac_length :: BS.ByteString -> BS.ByteString -> Bool
+hmac_length k m = let MAC h = hmac k m in BS.length h == 64
+
+-- hash_lazy produces same result regardless of chunking
+hash_chunking :: BS.ByteString -> [Q.Positive Int] -> Bool
+hash_chunking bs chunks =
+  let lazy_single = BL.fromStrict bs
+      lazy_chunked = BL.fromChunks (chunk_by (map Q.getPositive chunks) bs)
+  in  hash_lazy lazy_single == hash_lazy lazy_chunked
+
+-- hmac_lazy produces same result regardless of chunking
+hmac_chunking :: BS.ByteString -> BS.ByteString -> [Q.Positive Int] -> Bool
+hmac_chunking k m chunks =
+  let lazy_single = BL.fromStrict m
+      lazy_chunked = BL.fromChunks (chunk_by (map Q.getPositive chunks) m)
+  in  hmac_lazy k lazy_single == hmac_lazy k lazy_chunked
+
+chunk_by :: [Int] -> BS.ByteString -> [BS.ByteString]
+chunk_by _ bs | BS.null bs = []
+chunk_by [] bs = [bs]
+chunk_by (n:ns) bs =
+  let (h, t) = BS.splitAt n bs
+  in  h : chunk_by ns t
+
+properties :: TestTree
+properties = testGroup "properties" [
+    Q.testProperty "hash == hash_lazy" $
+      Q.withMaxSuccess 1000 hash_equiv
+  , Q.testProperty "hmac == hmac_lazy" $
+      Q.withMaxSuccess 1000 hmac_equiv
+  , Q.testProperty "hash output is 64 bytes" $
+      Q.withMaxSuccess 1000 hash_length
+  , Q.testProperty "hmac output is 64 bytes" $
+      Q.withMaxSuccess 1000 hmac_length
+  , Q.testProperty "hash_lazy chunking-invariant" $
+      Q.withMaxSuccess 1000 hash_chunking
+  , Q.testProperty "hmac_lazy chunking-invariant" $
+      Q.withMaxSuccess 1000 hmac_chunking
+  ]
