diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/aws-cloudfront-signed-cookies.cabal b/aws-cloudfront-signed-cookies.cabal
new file mode 100644
--- /dev/null
+++ b/aws-cloudfront-signed-cookies.cabal
@@ -0,0 +1,67 @@
+name: aws-cloudfront-signed-cookies
+version: 0.1.0.0
+
+synopsis: Generate signed cookies for AWS CloudFront
+category: Network, AWS, Cloud
+
+description: One way to [serve private content through AWS CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html) is to use [signed cookies](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html). This package helps you generate signed cookies [using a custom IAM policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html) which may include a range of time for which the cookie is valid and an IP address restriction.
+
+homepage:    https://github.com/typeclasses/aws-cloudfront-signed-cookies
+bug-reports: https://github.com/typeclasses/aws-cloudfront-signed-cookies/issues
+
+author:     Chris Martin
+maintainer: Chris Martin, Julie Moronuki
+
+copyright: 2018 Typeclass Consulting, LLC
+license: MIT
+license-file: license.txt
+
+build-type: Simple
+cabal-version: >= 1.10
+
+extra-source-files:
+    readme.md
+
+source-repository head
+  type: git
+  location: https://github.com/typeclasses/aws-cloudfront-signed-cookies
+
+library
+  default-language: Haskell2010
+  hs-source-dirs: library
+
+  exposed-modules:
+      Network.AWS.CloudFront.SignedCookies
+    , Network.AWS.CloudFront.SignedCookies.CLI
+    , 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
+
+  build-depends:
+      aeson
+    , asn1-encoding
+    , asn1-types
+    , base >=4.7 && <5
+    , base64-bytestring
+    , bytestring
+    , cookie
+    , cryptonite
+    , optparse-applicative
+    , pem
+    , text
+    , time
+    , unordered-containers
+    , vector
+
+executable aws-cloudfront-signed-cookies
+  default-language: Haskell2010
+  hs-source-dirs: executables
+  main-is: aws-cloudfront-signed-cookies.hs
+
+  build-depends:
+      aws-cloudfront-signed-cookies
+    , base >=4.7 && <5
diff --git a/executables/aws-cloudfront-signed-cookies.hs b/executables/aws-cloudfront-signed-cookies.hs
new file mode 100644
--- /dev/null
+++ b/executables/aws-cloudfront-signed-cookies.hs
@@ -0,0 +1,3 @@
+module Main (main) where
+
+import Network.AWS.CloudFront.SignedCookies.CLI (main)
diff --git a/library/Network/AWS/CloudFront/SignedCookies.hs b/library/Network/AWS/CloudFront/SignedCookies.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies.hs
@@ -0,0 +1,124 @@
+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, TypeApplications #-}
+
+-- |
+-- Example usage:
+--
+-- > {-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
+-- >
+-- > import Network.AWS.CloudFront.SignedCookies
+-- >
+-- > import qualified Data.Text.IO
+-- >
+-- > main :: IO ()
+-- > main = do
+-- >
+-- >   -- Construct an IAM policy that expires three days from now
+-- >   policy :: Policy <- simplePolicy
+-- >     (Resource "https://example.com/secrets/*")
+-- >     (Lifespan (3 * nominalDay))
+-- >
+-- >   -- Parse the .pem file to get the private key
+-- >   key :: PrivateKey <- readPrivateKeyPemFile
+-- >     (PemFilePath "./pk-APKAIATXN3RCIOVT5WRQ.pem")
+-- >
+-- >   -- Construct signed cookies
+-- >   cookies :: CookiesText <- createSignedCookies
+-- >     (KeyPairId "APKAIATXN3RCIOVT5WRQ") key policy
+-- >
+-- >   Data.Text.IO.putStrLn (renderCookiesText cookies)
+--
+-- Output:
+--
+-- > Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc29...
+-- > Cookie: CloudFront-Signature=wMN6V3Okxk7sdSPZeebMh-wo...
+-- > Cookie: CloudFront-Key-Pair-Id=APKAIATXN3RCIOVT5WRQ
+--
+-- You can see a very similar example in action in the
+-- "Network.AWS.CloudFront.SignedCookies.CLI" module.
+
+module Network.AWS.CloudFront.SignedCookies
+
+  (
+  -- * Creating signed cookies
+    createSignedCookies
+
+  -- ** Defining a CloudFront policy
+  , simplePolicy
+  , Policy (..)
+  , Resource (..)
+  , Lifespan (..)
+  , StartTime (..)
+  , EndTime (..)
+  , IpAddress (..)
+
+  -- ** Getting your private key
+  , readPrivateKeyPemFile
+  , PemFilePath (..)
+  , KeyPairId (..)
+  , PrivateKey
+
+  -- * Micellaneous
+
+  -- ** Cookies
+  , CookiesText
+  , renderCookiesText
+
+  -- ** Time
+  , NominalDiffTime
+  , POSIXTime
+  , nominalDay
+  , getPOSIXTime
+
+  -- ** Text
+  , Text
+
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies.Crypto
+import Network.AWS.CloudFront.SignedCookies.Encoding
+import Network.AWS.CloudFront.SignedCookies.Policy
+import Network.AWS.CloudFront.SignedCookies.Types
+
+-- base
+import Data.Coerce (coerce)
+import Data.Semigroup ((<>))
+
+-- text
+import qualified Data.Text as Text
+
+-- time
+import Data.Time.Clock.POSIX (getPOSIXTime)
+import Data.Time.Clock (nominalDay)
+
+createSignedCookies
+  :: KeyPairId
+    -- ^ A CloudFront key pair ID, which must be associated with a
+    --   trusted signer in the CloudFront distribution that you
+    --   specify in the 'policyResource'.
+  -> PrivateKey
+    -- ^ The private key associated with the 'KeyPairId'. See
+    --   'readPrivateKeyPemFile' for how to read this key from a
+    --   @.pem@ file you downloaded from AWS.
+  -> Policy
+    -- ^ The policy specifies what resource is being granted, for what
+    --   time period, and to what IP addresses. Construct a policy
+    --   using the 'Policy' constructor or with the 'simplePolicy'
+    --   function.
+  -> IO CookiesText
+
+createSignedCookies kpid key policy = do
+
+  let
+    policyBS :: ByteString = policyJSON policy
+
+  sigBS <- sign key policyBS
+
+  pure
+    [ ( "CloudFront-Policy"      , base64Encode policyBS        )
+    , ( "CloudFront-Signature"   , base64Encode sigBS           )
+    , ( "CloudFront-Key-Pair-Id" , coerce @KeyPairId @Text kpid )
+    ]
+
+renderCookiesText :: CookiesText -> Text
+renderCookiesText =
+  Text.unlines . map (\(k, v) -> "Cookie: " <> k <> "=" <> v)
diff --git a/library/Network/AWS/CloudFront/SignedCookies/CLI.hs b/library/Network/AWS/CloudFront/SignedCookies/CLI.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/CLI.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE FlexibleContexts, OverloadedStrings, RecordWildCards #-}
+
+-- | Command-line interface for generating AWS CloudFront signed cookies.
+
+module Network.AWS.CloudFront.SignedCookies.CLI
+  ( main, Opts (..), optsParser
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies
+
+-- 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)
+
+-- | The entry point for the AWS CloudFront cookie-signing
+-- 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
+
+    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/Crypto.hs b/library/Network/AWS/CloudFront/SignedCookies/Crypto.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/Crypto.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE ScopedTypeVariables, TypeApplications #-}
+
+module Network.AWS.CloudFront.SignedCookies.Crypto
+  (
+  -- * Reading the private key
+    readPrivateKeyPemFile
+
+  -- * Generating signatures
+  , sign
+
+  -- * Types
+  , PrivateKey
+  , ByteString
+
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies.Crypto.Internal
+import Network.AWS.CloudFront.SignedCookies.Types
+
+-- asn1-encoding
+import Data.ASN1.BinaryEncoding (DER (DER))
+import Data.ASN1.Encoding (decodeASN1')
+import Data.ASN1.Error (ASN1Error)
+
+-- asn1-types
+import Data.ASN1.Types (ASN1)
+
+-- bytestring
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LBS
+
+-- cryptonite
+import Crypto.PubKey.RSA (PrivateKey)
+import qualified Crypto.PubKey.RSA.PKCS15 as RSA
+import Crypto.Hash.Algorithms (SHA1 (SHA1))
+
+-- pem
+import qualified Data.PEM as PEM
+
+-- text
+import qualified Data.Text as Text
+
+-- | Construct the signature that will go into the
+--   @CloudFront-Signature@ cookie.
+
+sign
+  :: PrivateKey     -- ^ The RSA private key that you read from the @.pem@ file
+  -> ByteString     -- ^ The JSON representation of the 'Policy'
+                    --   (see "Network.AWS.CloudFront.SignedCookies.Policy")
+  -> IO ByteString
+
+sign key bs =
+  RSA.signSafer (Just SHA1) key bs >>= either (fail . show) pure
+
+-- | Read an RSA private key from a @.pem@ file you downloaded from AWS.
+
+readPrivateKeyPemFile
+  :: PemFilePath    -- ^ The filesystem path of the @.pem@ file
+  -> IO PrivateKey
+
+readPrivateKeyPemFile (PemFilePath path) = do
+
+  lbs <- BS.readFile (Text.unpack path)
+
+  pemSections <- either fail pure (PEM.pemParseBS lbs)
+
+  pemBs <- PEM.pemContent <$> case pemSections of
+    [x] -> pure x
+    xs ->
+      let msg = "Expected exactly 1 PEM section but found " ++
+                show @Int (length xs)
+      in  fail msg
+
+  asn1s :: [ASN1] <- either (fail . show) pure (decodeASN1' DER pemBs)
+
+  either fail pure (rsaPrivateKeyFromASN1 asn1s)
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Crypto/Internal.hs b/library/Network/AWS/CloudFront/SignedCookies/Crypto/Internal.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/Crypto/Internal.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE LambdaCase, RecordWildCards, TypeApplications #-}
+
+{-
+
+This module is based on a part of snaplet-saml. If my hs-certificate PR gets merged, then we probably ought to remove this and use hs-certificate instead.
+
+https://github.com/duairc/snaplet-saml/blob/dd89116f8f0b3d755d2feea73f2c40beb78c409c/src/Data/X509/IO.hs#L87-L102
+
+https://github.com/vincenthz/hs-certificate/pull/98
+
+-}
+
+module Network.AWS.CloudFront.SignedCookies.Crypto.Internal
+  ( rsaPrivateKeyFromASN1
+  ) where
+
+import Data.Bifunctor (first)
+import Data.Semigroup ((<>))
+
+import Data.ASN1.Types
+import Data.ASN1.Encoding
+import Data.ASN1.BinaryEncoding
+import Data.ASN1.BitArray
+
+import qualified Crypto.PubKey.RSA as RSA
+
+rsaPrivateKeyFromASN1 :: [ASN1] -> Either String RSA.PrivateKey
+
+rsaPrivateKeyFromASN1 =
+  \case
+    ( Start Sequence
+        : IntVal version
+        : IntVal n
+        : IntVal e
+        : IntVal d
+        : IntVal p
+        : IntVal q
+        : IntVal dP
+        : IntVal dQ
+        : IntVal qinv
+        : End Sequence
+        : [] ) ->
+
+      case version of
+        0 -> Right (buildKey Params{..})
+        v -> Left $ "rsaPrivateKeyFromASN1: unexpected version " <>
+                    show @Integer version
+
+    _ -> Left "rsaPrivateKeyFromASN1: unexpected format"
+
+-- https://tls.mbed.org/kb/cryptography/asn1-key-structures-in-der-and-pem
+data Params =
+  Params
+    { n    :: Integer -- modulus
+    , e    :: Integer -- publicExponent
+    , d    :: Integer -- privateExponent
+    , p    :: Integer -- prime1
+    , q    :: Integer -- prime2
+    , dP   :: Integer -- exponent1 = d mod (p-1)
+    , dQ   :: Integer -- exponent2 = d mod (q-1)
+    , qinv :: Integer -- coefficient = (inverse of q) mod p
+    }
+
+buildKey :: Params -> RSA.PrivateKey
+buildKey Params{..} =
+  let
+    size = head [i | i <- [1..], 2 ^ (i * 8) > n]
+
+    pub = RSA.PublicKey
+      { public_size = size
+      , public_n = n
+      , public_e = e
+      }
+
+  in
+    RSA.PrivateKey
+      { RSA.private_pub  = pub
+      , RSA.private_d    = d
+      , RSA.private_p    = p
+      , RSA.private_q    = q
+      , RSA.private_dP   = dP
+      , RSA.private_dQ   = dQ
+      , RSA.private_qinv = qinv
+      }
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs b/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/Encoding.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE LambdaCase #-}
+
+module Network.AWS.CloudFront.SignedCookies.Encoding
+  ( base64Encode
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies.Types
+
+-- base64-bytestring
+import qualified Data.ByteString.Base64 as Base64
+
+-- text
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+
+{- |
+
+The base 64 encoding scheme used for AWS CloudFront signed cookies.
+We use this to encode both the policy and the signature.
+
+Excerpts from [Setting Signed Cookies Using a Custom Policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html):
+
+1. Base64-encode the string using MIME base64 encoding. For more information, see
+   [Section 6.8, Base64 Content-Transfer-Encoding](https://tools.ietf.org/html/rfc2045#section-6.8)
+   in RFC 2045, MIME (Multipurpose Internet Mail Extensions) Part One:
+   Format of Internet Message Bodies.
+
+2. Replace characters that are invalid in a URL query string with
+   characters that are valid. The following table lists invalid and
+   valid characters.
+
+    > Replace these          With these
+    > invalid characters     valid characters
+    > ------------------     ----------------
+    > +                      - (hyphen)
+    > =                      _ (underscore)
+    > /                      ~ (tilde)
+
+-}
+
+base64Encode :: ByteString -> Text
+base64Encode =
+  Text.map charSubstitution . Text.decodeUtf8 . Base64.encode
+
+charSubstitution :: Char -> Char
+charSubstitution =
+  \case
+    '+' -> '-'
+    '=' -> '_'
+    '/' -> '~'
+    x   -> x
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Policy.hs b/library/Network/AWS/CloudFront/SignedCookies/Policy.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/Policy.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
+
+module Network.AWS.CloudFront.SignedCookies.Policy
+  (
+  -- * Defining a policy
+    Policy (..)
+  , simplePolicy
+
+  -- * Components of a policy
+  , Resource (..)
+  , StartTime (..)
+  , EndTime (..)
+  , Lifespan (..)
+  , IpAddress (..)
+
+  -- * JSON representation
+  , policyJSON
+
+  ) where
+
+import Network.AWS.CloudFront.SignedCookies.Types
+
+-- aeson
+import qualified Data.Aeson as A
+
+-- base
+import Data.Semigroup ((<>))
+
+-- bytestring
+import qualified Data.ByteString.Lazy as LBS
+
+-- time
+import Data.Time.Clock.POSIX (getPOSIXTime)
+
+-- unordered-containers
+import qualified Data.HashMap.Strict as Map
+
+-- vector
+import qualified Data.Vector as Vec
+
+{- |
+
+Encode a 'Policy' as JSON, with no whitespace, as AWS requires.
+
+Excerpt from [Setting Signed Cookies Using a Custom Policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html):
+
+* "Remove all whitespace (including tabs and newline characters) from the policy statement."
+
+-}
+
+policyJSON :: Policy -> ByteString
+policyJSON =
+  LBS.toStrict . A.encode . policyValue
+
+policyValue :: Policy -> A.Value
+policyValue policy =
+  A.Object $ Map.singleton "Statement" $
+    A.Array $ Vec.singleton $
+      A.Object $ "Resource" .= resourceValue policy <>
+                 "Condition" .= conditionValue policy
+
+resourceValue :: Policy -> A.Value
+resourceValue (Policy (Resource x) _ _ _) = A.String x
+
+conditionValue :: Policy -> A.Value
+conditionValue (Policy _ start end ip) =
+  A.Object $ startCondition <> endCondition <> ipCondition
+  where
+
+    startCondition :: A.Object =
+      case start of
+        StartImmediately -> mempty
+        StartTime x -> "DateGreaterThan" .= posixTimeValue x
+
+    endCondition :: A.Object =
+      case end of
+        EndTime x -> "DateLessThan" .= posixTimeValue x
+
+    ipCondition :: A.Object =
+      case ip of
+        AnyIp -> mempty
+        IpAddress x -> "IpAddress" .= sourceIpValue x
+
+posixTimeValue :: POSIXTime -> A.Value
+posixTimeValue =
+  A.Object . ("AWS:EpochTime" .=) . A.Number . fromInteger . round
+
+sourceIpValue :: Text -> A.Value
+sourceIpValue =
+  A.Object . ("AWS:SourceIp" .=) . A.String
+
+(.=) :: Text -> A.Value -> A.Object
+(.=) = Map.singleton
+
+{- |
+
+This function provides one convenient way to construct a simple 'Policy'.
+
+For the full set of policy options, use the 'Policy' constructor directly.
+
+-}
+
+simplePolicy
+  :: Resource   -- ^ URL that the policy will grant access to,
+                --   optionally containing asterisks for wildcards
+  -> Lifespan   -- ^ How long from now the credentials expire
+  -> IO Policy
+
+simplePolicy res life = do
+
+  now :: POSIXTime <- getPOSIXTime
+
+  let end = case life of Lifespan x -> EndTime (now + x)
+
+  pure
+    Policy
+      { policyResource  = res
+      , policyEnd       = end
+      , policyStart     = StartImmediately
+      , policyIpAddress = AnyIp
+      }
diff --git a/library/Network/AWS/CloudFront/SignedCookies/Types.hs b/library/Network/AWS/CloudFront/SignedCookies/Types.hs
new file mode 100644
--- /dev/null
+++ b/library/Network/AWS/CloudFront/SignedCookies/Types.hs
@@ -0,0 +1,125 @@
+module Network.AWS.CloudFront.SignedCookies.Types
+
+  (
+  -- * Policy
+    Policy (..), Resource (..)
+
+  -- * Crypto
+  , PemFilePath (..), KeyPairId (..)
+
+  -- * Cookies
+  , CookiesText, SetCookie, CookieDomain (..), CookiePath (..)
+
+  -- * Time
+  , NominalDiffTime, POSIXTime, Lifespan (..), StartTime (..), EndTime (..)
+
+  -- * IP address
+  , IpAddress (..)
+
+  -- * Strings
+  , Text, ByteString
+
+  -- * Crypto
+  , PrivateKey
+
+  ) where
+
+-- bytestring
+import Data.ByteString (ByteString)
+
+-- cookie
+import Web.Cookie (CookiesText, SetCookie)
+
+-- cryptonite
+import Crypto.PubKey.RSA (PrivateKey (..), PublicKey (..))
+
+-- text
+import Data.Text (Text)
+
+-- time
+import Data.Time.Clock (NominalDiffTime)
+import Data.Time.Clock.POSIX (POSIXTime)
+
+{- |
+
+Location in the filesystem where a .pem file containing an
+RSA secret key can be found.
+
+The filename downloaded from AWS looks like this:
+
+* @"pk-APKAIATX@@N3RCIOVT5WRQ.pem"@
+
+-}
+newtype PemFilePath = PemFilePath Text
+
+{- |
+
+CloudFront key pair ID for the key pair that you are using to
+generate signature.
+
+The key pair ID can be found in the name of key files that you
+download, and looks like this:
+
+* @APKAIATXN3@@RCIOVT5WRQ@
+
+-}
+newtype KeyPairId = KeyPairId Text
+
+{- |
+
+Examples:
+
+* @"d123example.cl@@oudfront.net"@
+* @"cloudfrontalia@@s.example.com"@
+
+-}
+newtype CookieDomain = CookieDomain Text
+
+-- | Usually @"/"@
+newtype CookiePath = CookiePath Text
+
+{- |
+
+URL that a policy will grant access to, optionally containing
+asterisks for wildcards.
+
+Examples:
+
+* @"https:\/@@\/d123example.cloudfront.net/index.html"@
+* @"https:\/@@\/d123example.cloudfront.net/*.jpeg"@
+
+-}
+newtype Resource = Resource Text
+
+-- | How long from now the credentials expire
+newtype Lifespan = Lifespan NominalDiffTime
+
+-- | The time at which credentials begin to take effect
+data StartTime = StartImmediately | StartTime POSIXTime
+
+-- | The time at which credentials expire
+newtype EndTime = EndTime POSIXTime
+
+-- | The IP address or address range of clients allowed to make requests
+data IpAddress = AnyIp | IpAddress Text
+
+{- |
+
+A policy specifies what resource is being granted, for what time period,
+and to what IP addresses.
+
+For AWS's documentation on what going into a CloudFront policy statement, see [Values That You Specify in the Policy Statement for a Custom Policy for Signed Cookies](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-cookies-values).
+
+-}
+data Policy =
+  Policy
+    { policyResource  :: Resource
+        -- ^ URL that the policy will grant access to,
+        --   optionally containing asterisks for wildcards
+    , policyStart     :: StartTime
+        -- ^ The time at which credentials begin to take effect
+    , policyEnd       :: EndTime
+        -- ^ The time at which credentials expire
+    , policyIpAddress :: IpAddress
+        -- ^ The IP address or address range of clients allowed to make requests
+    }
diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,18 @@
+Copyright 2018 Typeclass Consulting, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,77 @@
+# aws-cloudfront-signed-cookies
+
+Generate signed cookies for AWS CloudFront
+
+One way to [serve private content through AWS CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html) is to use [signed cookies](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html). This package helps you generate signed cookies [using a custom IAM policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html) which may include a range of time for which the cookie is valid and an IP address restriction.
+
+## The library
+
+Example usage:
+
+```haskell
+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
+
+import Network.AWS.CloudFront.SignedCookies
+
+import qualified Data.Text.IO
+
+main :: IO ()
+main = do
+
+  -- Construct an IAM policy that expires three days from now
+  policy :: Policy <- simplePolicy
+    (Resource "https://example.com/secrets/*")
+    (Lifespan (3 * nominalDay))
+
+  -- Parse the .pem file to get the private key
+  key :: PrivateKey <- readPrivateKeyPemFile
+    (PemFilePath "./pk-APKAIATXN3RCIOVT5WRQ.pem")
+
+  -- Construct signed cookies
+  cookies :: CookiesText <- createSignedCookies
+    (KeyPairId "APKAIATXN3RCIOVT5WRQ") key policy
+
+  Data.Text.IO.putStrLn (renderCookiesText cookies)
+```
+
+The output should look something like this:
+
+```haskell
+Cookie: CloudFront-Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc29...
+Cookie: CloudFront-Signature=wMN6V3Okxk7sdSPZeebMh-wo...
+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.
+
+## The executable
+
+You can also generate cookies using the command-line interface.
+
+```
+$ aws-cloudfront-signed-cookies --help
+Generator of signed cookies for AWS CloudFront
+
+Usage: aws-cloudfront-signed-cookies --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
+                           using to generate signature
+  --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
+```
+
+Example usage:
+
+```
+$ aws-cloudfront-signed-cookies                \
+    --pem-file pk-APKAIATXN3RCIOVT5WRQ.pem     \
+    --key-pair-id APKAIATXN3RCIOVT5WRQ         \
+    --resource "https://example.com/secrets/*" \
+    --days 2
+```
