diff --git a/aws-cloudfront-signed-cookies.cabal b/aws-cloudfront-signed-cookies.cabal
--- a/aws-cloudfront-signed-cookies.cabal
+++ b/aws-cloudfront-signed-cookies.cabal
@@ -1,5 +1,5 @@
 name: aws-cloudfront-signed-cookies
-version: 0.1.0.1
+version: 0.2.0.0
 
 synopsis: Generate signed cookies for AWS CloudFront
 category: Network, AWS, Cloud
@@ -33,16 +33,20 @@
   exposed-modules:
       Network.AWS.CloudFront.SignedCookies
     , Network.AWS.CloudFront.SignedCookies.CLI
+    , Network.AWS.CloudFront.SignedCookies.CLI.Decode
+    , Network.AWS.CloudFront.SignedCookies.CLI.Sign
     , Network.AWS.CloudFront.SignedCookies.Crypto
     , Network.AWS.CloudFront.SignedCookies.Encoding
     , Network.AWS.CloudFront.SignedCookies.Policy
     , Network.AWS.CloudFront.SignedCookies.Types
 
   other-modules:
-      Network.AWS.CloudFront.SignedCookies.Crypto.Internal
+      Network.AWS.CloudFront.SignedCookies.CLI.Internal
+    , Network.AWS.CloudFront.SignedCookies.Crypto.Internal
 
   build-depends:
       aeson
+    , aeson-pretty
     , asn1-encoding
     , asn1-types
     , base >=4.7 && <5
@@ -50,6 +54,8 @@
     , bytestring
     , cookie
     , cryptonite
+    , lens
+    , lens-aeson
     , optparse-applicative
     , pem
     , text
@@ -65,3 +71,15 @@
   build-depends:
       aws-cloudfront-signed-cookies
     , base >=4.7 && <5
+
+test-suite hedgehog
+  default-language: Haskell2010
+  hs-source-dirs: test
+  type: exitcode-stdio-1.0
+  main-is: hedgehog.hs
+  ghc-options: -threaded
+  build-depends:
+      aws-cloudfront-signed-cookies
+    , base >=4.7 && <5
+    , hedgehog
+    , neat-interpolation
diff --git a/library/Network/AWS/CloudFront/SignedCookies.hs b/library/Network/AWS/CloudFront/SignedCookies.hs
--- a/library/Network/AWS/CloudFront/SignedCookies.hs
+++ b/library/Network/AWS/CloudFront/SignedCookies.hs
@@ -57,11 +57,21 @@
   , KeyPairId (..)
   , PrivateKey
 
+  -- * Policy JSON
+  , policyJSON
+  , jsonTextPolicy
+  , jsonValPolicy
+
+  -- * Reading cookies
+  , cookiePolicy
+
   -- * Miscellaneous
 
   -- ** Cookies
   , CookiesText
   , renderCookiesText
+  , PolicyCookie (..)
+  , SignatureCookie (..)
 
   -- ** Time
   , NominalDiffTime
@@ -79,10 +89,17 @@
 import Network.AWS.CloudFront.SignedCookies.Policy
 import Network.AWS.CloudFront.SignedCookies.Types
 
+-- aeson
+import qualified Data.Aeson as A
+
 -- base
+import Control.Monad ((>=>))
 import Data.Coerce (coerce)
 import Data.Semigroup ((<>))
 
+-- bytestring
+import qualified Data.ByteString.Lazy as LBS
+
 -- text
 import qualified Data.Text as Text
 
@@ -123,3 +140,10 @@
 renderCookiesText :: CookiesText -> Text
 renderCookiesText =
   Text.unlines . map (\(k, v) -> "Cookie: " <> k <> "=" <> v)
+
+-- | Parse the text value of a @CloudFront-Policy@ cookie into a 'Policy'
+-- value, producing 'Left' with an error message if parsing fails.
+cookiePolicy :: PolicyCookie -> Either String Policy
+cookiePolicy =
+  (base64Decode . coerce @PolicyCookie @Text) >=>
+  (A.eitherDecode' . LBS.fromStrict) >=> jsonValPolicy
diff --git a/library/Network/AWS/CloudFront/SignedCookies/CLI.hs b/library/Network/AWS/CloudFront/SignedCookies/CLI.hs
--- a/library/Network/AWS/CloudFront/SignedCookies/CLI.hs
+++ b/library/Network/AWS/CloudFront/SignedCookies/CLI.hs
@@ -1,94 +1,41 @@
 {-# LANGUAGE FlexibleContexts, OverloadedStrings, RecordWildCards #-}
 
 -- | Command-line interface for generating AWS CloudFront signed cookies.
+--
+-- This program is the aggregation of these subcommands:
+--
+-- * "Network.AWS.CloudFront.SignedCookies.CLI.Decode"
+-- * "Network.AWS.CloudFront.SignedCookies.CLI.Sign"
 
 module Network.AWS.CloudFront.SignedCookies.CLI
-  ( main, Opts (..), optsParser
+  ( main
   ) where
 
-import Network.AWS.CloudFront.SignedCookies
+import Network.AWS.CloudFront.SignedCookies.CLI.Internal
 
+import qualified Network.AWS.CloudFront.SignedCookies.CLI.Decode as Decode
+import qualified Network.AWS.CloudFront.SignedCookies.CLI.Sign   as Sign
+
 -- base
-import Data.Coerce (Coercible, coerce)
-import Data.Foldable (for_)
+import Control.Monad (join)
 import Data.Semigroup ((<>))
 
 -- optparse-applicative
 import qualified Options.Applicative as Opt
 
--- text
-import qualified Data.Text as Text
-import qualified Data.Text.IO as Text
-
--- time
-import Data.Time.Clock (nominalDay)
-
--- | The entry point for the AWS CloudFront cookie-signing
--- command-line interface.
+-- | Entry point for the AWS CloudFront signed cookie command-line interface.
 
 main :: IO ()
-main = do
-
-  -- Parse the command-line arguments
-  Opts{..} <- Opt.execParser $
-    Opt.info (Opt.helper <*> optsParser) $
-      Opt.header "Generator of signed cookies for AWS CloudFront"
-
-  -- Construct an IAM policy
-  policy <- simplePolicy opt_resource opt_lifespan
-
-  -- Parse the .pem file to get the private key
-  key <- readPrivateKeyPemFile opt_pemFilePath
-
-  -- Construct signed cookies
-  cookies <- createSignedCookies opt_keyPairId key policy
-
-  -- Print the cookies to stdout
-  (Text.putStr . renderCookiesText) cookies
-
--- | The parse result for the command-line arguments.
-
-data Opts =
-  Opts
-    { opt_pemFilePath :: PemFilePath
-        -- ^ Location in the filesystem where a .pem file
-        --   containing an RSA secret key can be found
-    , opt_keyPairId   :: KeyPairId
-        -- ^ CloudFront key pair ID for the key pair that
-        --   you are using to generate signature
-    , opt_resource    :: Resource
-        -- ^ URL that the policy will grant access to,
-        --   optionally containing asterisks for wildcards
-    , opt_lifespan    :: Lifespan
-        -- ^ Amount of time until the policy expires
-    }
-
--- | Parser for all of the command-line arguments.
---
--- See "Options.Applicative", 'Opt.info', and 'Opt.execParser'
--- to learn how to use a 'Opt.Parser'.
-
-optsParser :: Opt.Parser Opts
-optsParser =
-  Opts
-    <$> text "pem-file"    "Location in the filesystem where a .pem file \
-                           \containing an RSA secret key can be found"
-
-    <*> text "key-pair-id" "CloudFront key pair ID for the key pair that \
-                           \you are using to generate signature"
-
-    <*> text "resource"    "URL that the policy will grant access to, \
-                           \optionally containing asterisks for wildcards"
-
-    <*> days "days"        "Integer number of days until the policy expires"
-
-  where
-    text :: Coercible Text a => String -> String -> Opt.Parser a
-    text long help = fmap (coerce . Text.pack) $ Opt.strOption $ mod long help
+main =
+  join $ Opt.execParser mainParserInfo
 
-    days :: Coercible NominalDiffTime a => String -> String -> Opt.Parser a
-    days long help = fmap (coerce . (* nominalDay) . fromInteger) $
-      Opt.option Opt.auto $ mod long help
+mainParserInfo :: Opt.ParserInfo (IO ())
+mainParserInfo =
+  Opt.info (Opt.helper <*> mainParser) $
+    Opt.header "AWS CloudFront signed cookie utility"
 
-    mod :: Opt.HasName f => String -> String -> Opt.Mod f a
-    mod long help = Opt.long long <> Opt.help help
+mainParser :: Opt.Parser (IO ())
+mainParser =
+  Opt.hsubparser $
+    Opt.command "decode" Decode.mainParserInfo <>
+    Opt.command "sign"   Sign.mainParserInfo
diff --git a/library/Network/AWS/CloudFront/SignedCookies/CLI/Decode.hs b/library/Network/AWS/CloudFront/SignedCookies/CLI/Decode.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/CLI/Decode.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards, ScopedTypeVariables, TypeApplications #-}
+
+-- | Command-line interface for decoding AWS CloudFront policy cookies.
+
+module Network.AWS.CloudFront.SignedCookies.CLI.Decode
+  ( main, mainOpts, Opts (..), optsParser, mainParserInfo
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies
+import Network.AWS.CloudFront.SignedCookies.CLI.Internal
+import Network.AWS.CloudFront.SignedCookies.Encoding
+
+-- aeson
+import qualified Data.Aeson as A
+
+-- aeson-pretty
+import Data.Aeson.Encode.Pretty (encodePrettyToTextBuilder)
+
+-- base
+import Data.Coerce (Coercible, coerce)
+import Data.Foldable (for_)
+import Data.Semigroup ((<>))
+
+-- bytestring
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
+
+-- optparse-applicative
+import qualified Options.Applicative as Opt
+
+-- text
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Text.Lazy as LText
+import qualified Data.Text.Lazy.IO as LText
+import Data.Text.Lazy.Builder as Text
+
+-- time
+import Data.Time.Clock (nominalDay)
+
+-- | Entry point for the AWS CloudFront cookie decoding command-line interface.
+
+main :: IO ()
+main = do
+
+  opts <- Opt.execParser $ Opt.info (Opt.helper <*> optsParser) infoMod
+
+  mainOpts opts
+
+mainOpts :: Opts -> IO ()
+mainOpts Opts{..} = do
+
+  bs :: BS.ByteString <-
+    case base64Decode $ coerce @PolicyCookie @Text opt_policyCookie of
+      Left e  -> fail e
+      Right x -> pure x
+
+  value <-
+    case A.eitherDecode' (LBS.fromStrict bs) of
+      Left e  -> fail e
+      Right x -> pure x
+
+  let
+    result :: LText.Text =
+      Text.toLazyText $ encodePrettyToTextBuilder @A.Value value
+
+  -- Print the JSON to stdout
+  LText.putStr result
+
+-- | Parse result for the command-line arguments for cookie decoding.
+
+data Opts =
+  Opts
+    { opt_policyCookie :: PolicyCookie
+        -- ^ The value of a @CloudFront-Policy@ cookie.
+    }
+
+-- | Parser for all of the command-line arguments for cookie decoding.
+--
+-- See "Options.Applicative", 'Opt.info', and 'Opt.execParser'
+-- to learn how to use a 'Opt.Parser'.
+
+optsParser :: Opt.Parser Opts
+optsParser =
+  Opts
+    <$> text "policy-cookie" "The value of a CloudFront-Policy cookie"
+
+mainParserInfo :: Opt.ParserInfo (IO ())
+mainParserInfo =
+  mainOpts <$> Opt.info optsParser infoMod
+
+infoMod :: Opt.InfoMod a
+infoMod =
+  Opt.header "Decode signed AWS CloudFront policy cookies"
diff --git a/library/Network/AWS/CloudFront/SignedCookies/CLI/Internal.hs b/library/Network/AWS/CloudFront/SignedCookies/CLI/Internal.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/CLI/Internal.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+module Network.AWS.CloudFront.SignedCookies.CLI.Internal
+  ( text, days
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies
+import Network.AWS.CloudFront.SignedCookies.Types
+
+-- base
+import Data.Coerce (Coercible, coerce)
+import Data.Semigroup ((<>))
+import Prelude hiding (mod)
+
+-- optparse-applicative
+import qualified Options.Applicative as Opt
+
+-- text
+import qualified Data.Text as Text
+
+
+text :: Coercible Text a => String -> String -> Opt.Parser a
+text long help = fmap (coerce . Text.pack) $ Opt.strOption $ mod long help
+
+days :: Coercible NominalDiffTime a => String -> String -> Opt.Parser a
+days long help = fmap (coerce . (* nominalDay) . fromInteger) $
+  Opt.option Opt.auto $ mod long help
+
+mod :: Opt.HasName f => String -> String -> Opt.Mod f a
+mod long help = Opt.long long <> Opt.help help
diff --git a/library/Network/AWS/CloudFront/SignedCookies/CLI/Sign.hs b/library/Network/AWS/CloudFront/SignedCookies/CLI/Sign.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/CLI/Sign.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
+
+-- | Command-line interface for generating AWS CloudFront signed cookies.
+
+module Network.AWS.CloudFront.SignedCookies.CLI.Sign
+  ( main, mainOpts, Opts (..), optsParser, mainParserInfo
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies
+import Network.AWS.CloudFront.SignedCookies.CLI.Internal
+
+-- base
+import Data.Coerce (Coercible, coerce)
+import Data.Foldable (for_)
+import Data.Semigroup ((<>))
+
+-- optparse-applicative
+import qualified Options.Applicative as Opt
+
+-- text
+import qualified Data.Text as Text
+import qualified Data.Text.IO as Text
+
+-- time
+import Data.Time.Clock (nominalDay)
+
+-- | Entry point for the AWS CloudFront cookie-signing command-line interface.
+
+main :: IO ()
+main = do
+
+  opts <- Opt.execParser $ Opt.info (Opt.helper <*> optsParser) infoMod
+
+  mainOpts opts
+
+mainOpts :: Opts -> IO ()
+mainOpts Opts{..} = do
+
+  -- Construct an IAM policy
+  policy <- simplePolicy opt_resource opt_lifespan
+
+  -- Parse the .pem file to get the private key
+  key <- readPrivateKeyPemFile opt_pemFilePath
+
+  -- Construct signed cookies
+  cookies <- createSignedCookies opt_keyPairId key policy
+
+  -- Print the cookies to stdout
+  (Text.putStr . renderCookiesText) cookies
+
+-- | Parse result for the command-line arguments for signed cookie generation.
+
+data Opts =
+  Opts
+    { opt_pemFilePath :: PemFilePath
+        -- ^ Location in the filesystem where a .pem file
+        --   containing an RSA secret key can be found
+    , opt_keyPairId   :: KeyPairId
+        -- ^ CloudFront key pair ID for the key pair that
+        --   you are using to generate signature
+    , opt_resource    :: Resource
+        -- ^ URL that the policy will grant access to,
+        --   optionally containing asterisks for wildcards
+    , opt_lifespan    :: Lifespan
+        -- ^ Amount of time until the policy expires
+    }
+
+-- | Parser for all the command-line arguments for signed cookie generation.
+--
+-- See "Options.Applicative", 'Opt.info', and 'Opt.execParser'
+-- to learn how to use a 'Opt.Parser'.
+
+optsParser :: Opt.Parser Opts
+optsParser =
+  Opts
+    <$> text "pem-file"    "Location in the filesystem where a .pem file \
+                           \containing an RSA secret key can be found"
+
+    <*> text "key-pair-id" "CloudFront key pair ID for the key pair that \
+                           \you are using to generate signature"
+
+    <*> text "resource"    "URL that the policy will grant access to, \
+                           \optionally containing asterisks for wildcards"
+
+    <*> days "days"        "Integer number of days until the policy expires"
+
+mainParserInfo :: Opt.ParserInfo (IO ())
+mainParserInfo =
+  mainOpts <$> Opt.info optsParser infoMod
+
+infoMod :: Opt.InfoMod a
+infoMod =
+  Opt.header "Generate signed cookies for AWS CloudFront"
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs b/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs
--- a/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs
+++ b/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs
@@ -2,6 +2,7 @@
 
 module Network.AWS.CloudFront.SignedCookies.Encoding
   ( base64Encode
+  , base64Decode
   ) where
 
 import Network.AWS.CloudFront.SignedCookies.Types
@@ -40,12 +41,29 @@
 
 base64Encode :: ByteString -> Text
 base64Encode =
-  Text.map charSubstitution . Text.decodeUtf8 . Base64.encode
+  Text.map charEncode . Text.decodeUtf8 . Base64.encode
+  where
+    charEncode =
+      \case
+        '+' -> '-'
+        '=' -> '_'
+        '/' -> '~'
+        x   -> x
 
-charSubstitution :: Char -> Char
-charSubstitution =
-  \case
-    '+' -> '-'
-    '=' -> '_'
-    '/' -> '~'
-    x   -> x
+{- |
+
+The inverse of 'base64Encode'. Produces a 'Left' value with
+an error message if decoding fails.
+
+-}
+
+base64Decode :: Text -> Either String ByteString
+base64Decode =
+  Base64.decode . Text.encodeUtf8 . Text.map charDecode
+  where
+    charDecode =
+      \case
+        '-' -> '+'
+        '_' -> '='
+        '~' -> '/'
+        x   -> x
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Policy.hs b/library/Network/AWS/CloudFront/SignedCookies/Policy.hs
--- a/library/Network/AWS/CloudFront/SignedCookies/Policy.hs
+++ b/library/Network/AWS/CloudFront/SignedCookies/Policy.hs
@@ -15,6 +15,8 @@
 
   -- * JSON representation
   , policyJSON
+  , jsonTextPolicy
+  , jsonValPolicy
 
   ) where
 
@@ -24,11 +26,21 @@
 import qualified Data.Aeson as A
 
 -- base
+import Control.Monad ((>=>))
 import Data.Semigroup ((<>))
 
 -- bytestring
 import qualified Data.ByteString.Lazy as LBS
 
+-- lens
+import Control.Lens ((&), (^.), (^?))
+
+-- lens-aeson
+import Data.Aeson.Lens (AsNumber (..), AsPrimitive (..), key, nth, _Array, _Object)
+
+-- text
+import qualified Data.Text.Encoding as Text
+
 -- time
 import Data.Time.Clock.POSIX (getPOSIXTime)
 
@@ -91,6 +103,61 @@
 
 (.=) :: Text -> A.Value -> A.Object
 (.=) = Map.singleton
+
+jsonTextPolicy :: Text -> Either String Policy
+jsonTextPolicy =
+  (A.eitherDecode' . LBS.fromStrict . Text.encodeUtf8) >=>
+  jsonValPolicy
+
+jsonValPolicy :: A.Value -> Either String Policy
+jsonValPolicy val =
+  Policy
+    <$> jsonResource  val
+    <*> jsonStart     val
+    <*> jsonEnd       val
+    <*> jsonIpAddress val
+
+jsonResource :: A.Value -> Either String Resource
+jsonResource val =
+  maybe (Left "Missing \"Resource\"") Right $
+    fmap Resource
+      (val ^? key "Statement"
+            . nth 0
+            . key "Resource"
+            . _String)
+
+jsonStart :: A.Value -> Either String StartTime
+jsonStart val =
+  Right $
+    maybe StartImmediately (StartTime . fromInteger)
+      (val ^? key "Statement"
+            . nth 0
+            . key "Condition"
+            . key "DateGreaterThan"
+            . key "AWS:EpochTime"
+            . _Integer)
+
+jsonEnd :: A.Value -> Either String EndTime
+jsonEnd val =
+  maybe (Left "Missing \"DateLessThan\"") Right $
+    fmap (EndTime . fromInteger)
+      (val ^? key "Statement"
+            . nth 0
+            . key "Condition"
+            . key "DateLessThan"
+            . key "AWS:EpochTime"
+            . _Integer)
+
+jsonIpAddress :: A.Value -> Either String IpAddress
+jsonIpAddress val =
+  Right $
+    maybe AnyIp IpAddress
+      (val ^? key "Statement"
+            . nth 0
+            . key "Condition"
+            . key "IpAddress"
+            . key "AWS:SourceIp"
+            . _String)
 
 {- |
 
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Types.hs b/library/Network/AWS/CloudFront/SignedCookies/Types.hs
--- a/library/Network/AWS/CloudFront/SignedCookies/Types.hs
+++ b/library/Network/AWS/CloudFront/SignedCookies/Types.hs
@@ -9,6 +9,7 @@
 
   -- * Cookies
   , CookiesText, SetCookie, CookieDomain (..), CookiePath (..)
+  , PolicyCookie (..), SignatureCookie (..)
 
   -- * Time
   , NominalDiffTime, POSIXTime, Lifespan (..), StartTime (..), EndTime (..)
@@ -51,6 +52,7 @@
 
 -}
 newtype PemFilePath = PemFilePath Text
+  deriving (Eq, Show)
 
 {- |
 
@@ -64,6 +66,7 @@
 
 -}
 newtype KeyPairId = KeyPairId Text
+  deriving (Eq, Show)
 
 {- |
 
@@ -74,10 +77,20 @@
 
 -}
 newtype CookieDomain = CookieDomain Text
+  deriving (Eq, Show)
 
 -- | Usually @"/"@
 newtype CookiePath = CookiePath Text
+  deriving (Eq, Show)
 
+-- | The value of a @CloudFront-Policy@ cookie.
+newtype PolicyCookie = PolicyCookie Text
+  deriving (Eq, Show)
+
+-- | The value of a @CloudFront-Signature@ cookie.
+newtype SignatureCookie = SignatureCookie Text
+  deriving (Eq, Show)
+
 {- |
 
 URL that a policy will grant access to, optionally containing
@@ -90,18 +103,23 @@
 
 -}
 newtype Resource = Resource Text
+  deriving (Eq, Show)
 
 -- | How long from now the credentials expire
 newtype Lifespan = Lifespan NominalDiffTime
+  deriving (Eq, Show)
 
 -- | The time at which credentials begin to take effect
 data StartTime = StartImmediately | StartTime POSIXTime
+  deriving (Eq, Show)
 
 -- | The time at which credentials expire
 newtype EndTime = EndTime POSIXTime
+  deriving (Eq, Show)
 
 -- | The IP address or address range of clients allowed to make requests
 data IpAddress = AnyIp | IpAddress Text
+  deriving (Eq, Show)
 
 {- |
 
@@ -123,3 +141,4 @@
     , policyIpAddress :: IpAddress
         -- ^ The IP address or address range of clients allowed to make requests
     }
+    deriving (Eq, Show)
diff --git a/readme.md b/readme.md
--- a/readme.md
+++ b/readme.md
@@ -6,6 +6,8 @@
 
 ## The library
 
+### Creating signed cookies
+
 Example usage:
 
 ```haskell
@@ -37,26 +39,41 @@
 The output should look something like this:
 
 ```haskell
-Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc29...
-Cookie: CloudFront-Signature=wMN6V3Okxk7sdSPZeebMh-wo...
+Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__
+Cookie: CloudFront-Signature=R17isgiJD36sb4kh4pNxqq5eFCx5Q~vC~QbQKMhi9BqyhLboLQfb3pCdPp-WSVYOn4wlOobqhgcwoX2vjCd3y8eiUtelX1~ddaDlPBkkdZA~5Imd2z-W1o3r0coKB1SE2SPFT7M1XgNvwOPanQoft1VLchfVPGaitFYMum4KS~GXffqQZzaVybKJ64KfFLLBPSobg8MmBhHvpO9DBiwKijmGhDip~6L3W7OcqGT8HdqAmWxjnTHInXpx7bbVotla6a~J6WB6xmtJrmQzMLdTUxAY6IbJ2PzMtTXKgdNzbByGHvImg9k3Q3fNBTim8l7Tds1zpwX9GsuPcFkPe9HZ1Q__
 Cookie: CloudFront-Key-Pair-Id=APKAIATXN3RCIOVT5WRQ
 ```
 
-You can see a very similar example in action in the `Network.AWS.CloudFront.SignedCookies.CLI` module which defines the command-line interface.
+You can see a very similar example in action in the `Network.AWS.CloudFront.SignedCookies.CLI.Sign` module which defines the command-line interface for creating signed cookies.
 
+### Decoding policy cookies
+
+After you generate a policy cookie and send it to a client, you may later want to parse it back into a `Policy` value -- for example, to determine whether you need to send a new set of cookies to replace an expired policy.
+
+Example in GHCi:
+
+```
+λ> cookiePolicy (PolicyCookie "eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__")
+Right (Policy {policyResource = Resource "https://example.com/secrets/*", policyStart = StartImmediately, policyEnd = EndTime 1523664125s, policyIpAddress = AnyIp})
+```
+
 ## The executable
 
-You can also generate cookies using the command-line interface.
+You can also generate cookies using the command-line interface. It provides two commands:
 
+* `sign` - Given a private key and policy options, produces signed cookies and prints them as HTTP request headers.
+* `decode` - Decodes a `CloudFront-Policy` cookie and prints it in human-readable JSON format.
+
+### `sign`
+
 ```
-$ aws-cloudfront-signed-cookies --help
-Generator of signed cookies for AWS CloudFront
+$ aws-cloudfront-signed-cookies sign --help
+Generate signed cookies for AWS CloudFront
 
-Usage: aws-cloudfront-signed-cookies --pem-file ARG --key-pair-id ARG
-                                     --resource ARG --days ARG
+Usage: aws-cloudfront-signed-cookies sign --pem-file ARG --key-pair-id ARG
+                                          --resource ARG --days ARG
 
 Available options:
-  -h,--help                Show this help text
   --pem-file ARG           Location in the filesystem where a .pem file
                            containing an RSA secret key can be found
   --key-pair-id ARG        CloudFront key pair ID for the key pair that you are
@@ -64,14 +81,55 @@
   --resource ARG           URL that the policy will grant access to, optionally
                            containing asterisks for wildcards
   --days ARG               Integer number of days until the policy expires
+  -h,--help                Show this help text
 ```
 
 Example usage:
 
 ```
-$ aws-cloudfront-signed-cookies                \
+$ aws-cloudfront-signed-cookies sign           \
     --pem-file pk-APKAIATXN3RCIOVT5WRQ.pem     \
     --key-pair-id APKAIATXN3RCIOVT5WRQ         \
     --resource "https://example.com/secrets/*" \
     --days 2
+```
+
+Output:
+
+```
+Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__
+Cookie: CloudFront-Signature=R17isgiJD36sb4kh4pNxqq5eFCx5Q~vC~QbQKMhi9BqyhLboLQfb3pCdPp-WSVYOn4wlOobqhgcwoX2vjCd3y8eiUtelX1~ddaDlPBkkdZA~5Imd2z-W1o3r0coKB1SE2SPFT7M1XgNvwOPanQoft1VLchfVPGaitFYMum4KS~GXffqQZzaVybKJ64KfFLLBPSobg8MmBhHvpO9DBiwKijmGhDip~6L3W7OcqGT8HdqAmWxjnTHInXpx7bbVotla6a~J6WB6xmtJrmQzMLdTUxAY6IbJ2PzMtTXKgdNzbByGHvImg9k3Q3fNBTim8l7Tds1zpwX9GsuPcFkPe9HZ1Q__
+Cookie: CloudFront-Key-Pair-Id=APKAIATXN3RCIOVT5WRQ
+```
+
+### `decode`
+
+```
+$ aws-cloudfront-signed-cookies decode --help
+Decode signed AWS CloudFront policy cookies
+
+Usage: aws-cloudfront-signed-cookies decode --policy-cookie ARG
+
+Available options:
+  --policy-cookie ARG      The value of a CloudFront-Policy cookie
+  -h,--help                Show this help text
+
+```
+
+Example usage:
+
+```
+$ aws-cloudfront-signed-cookies decode --policy-cookie eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zZWNyZXRzLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE1MjM2NjQxMjV9fX1dfQ__
+{
+    "Statement": [
+        {
+            "Resource": "https://example.com/secrets/*",
+            "Condition": {
+                "DateLessThan": {
+                    "AWS:EpochTime": 1523664125
+                }
+            }
+        }
+    ]
+}
 ```
diff --git a/test/hedgehog.hs b/test/hedgehog.hs
new file mode 100644
--- /dev/null
+++ b/test/hedgehog.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell #-}
+
+module Main (main) where
+
+import Network.AWS.CloudFront.SignedCookies
+
+-- base
+import Control.Monad (unless)
+import Data.Foldable (for_)
+import qualified System.Exit as Exit
+import qualified System.IO   as IO
+
+-- hedgehog
+import           Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+-- neat-interpolation
+import NeatInterpolation
+
+main :: IO ()
+main = do
+  for_ [IO.stdout, IO.stderr] $ \h -> do
+    IO.hSetEncoding h IO.utf8
+    IO.hSetBuffering h IO.LineBuffering
+  success <- Hedgehog.checkParallel $$(Hedgehog.discover)
+  unless success Exit.exitFailure
+
+prop_jsonPolicy_1 :: Property
+prop_jsonPolicy_1 = withTests 1 $ property $
+  let
+    x = [text|
+        {
+          "Statement": [
+              {
+                 "Resource": "http://d111111abcdef8.cloudfront.net/game_download.zip",
+                 "Condition": {
+                    "IpAddress": {"AWS:SourceIp": "192.0.2.0/24"},
+                    "DateLessThan": {"AWS:EpochTime": 1357034400}
+                 }
+              }
+           ]
+        }
+      |]
+
+    p = Policy
+      (Resource "http://d111111abcdef8.cloudfront.net/game_download.zip")
+      StartImmediately
+      (EndTime 1357034400)
+      (IpAddress "192.0.2.0/24")
+  in
+    jsonTextPolicy x === Right p
+
+prop_jsonPolicy_2 :: Property
+prop_jsonPolicy_2 = withTests 1 $ property $
+  let
+    x = [text|
+        {
+           "Statement": [
+              {
+                 "Resource": "http://*",
+                 "Condition": {
+                    "IpAddress": {"AWS:SourceIp": "192.0.2.10/32"},
+                    "DateGreaterThan": {"AWS:EpochTime": 1357034400},
+                    "DateLessThan": {"AWS:EpochTime": 1357120800}
+                 }
+              }
+           ]
+        }
+      |]
+
+    p = Policy
+      (Resource "http://*")
+      (StartTime 1357034400)
+      (EndTime 1357120800)
+      (IpAddress "192.0.2.10/32")
+  in
+    jsonTextPolicy x === Right p
+
+prop_jsonPolicy_3 :: Property
+prop_jsonPolicy_3 = withTests 1 $ property $
+  let
+    x = [text|
+        {
+           "Statement": [
+              {
+                 "Resource": "http://*",
+                 "Condition": {
+                    "IpAddress": {"AWS:SourceIp": "192.0.2.10/32"},
+                    "DateGreaterThan":
+      |]
+
+  in
+    jsonTextPolicy x === Left "Error in $: not enough input"
