diff --git a/jose.cabal b/jose.cabal
--- a/jose.cabal
+++ b/jose.cabal
@@ -1,9 +1,8 @@
 cabal-version:       2.2
 name:                jose
-version:             0.8.5.1
+version:             0.9
 synopsis:
-  Javascript Object Signing and Encryption (JOSE)
-  and JSON Web Token (JWT) library
+  JSON Object Signing and Encryption (JOSE) and JSON Web Token (JWT) library
 description:
   .
   An implementation of the Javascript Object Signing and Encryption
@@ -29,11 +28,11 @@
   test/data/fido.jwt
 author:              Fraser Tweedale
 maintainer:          frase@frase.id.au
-copyright:           Copyright (C) 2013-2018  Fraser Tweedale
+copyright:           Copyright (C) 2013-2021  Fraser Tweedale
 category:            Cryptography
 build-type:          Simple
 tested-with:
-  GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.4, GHC==9.0.1
+  GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.4, GHC==9.0.1
 
 flag demos
   description: Build demonstration programs
@@ -44,15 +43,12 @@
   ghc-options:    -Wall
 
   build-depends:
-    base >= 4.8 && < 5
-    , aeson >= 0.11.1.0 && < 2.0
-    , bytestring == 0.10.*
+    base >= 4.9 && < 5
+    , aeson >= 2.0.1.0 && < 3
+    , bytestring >= 0.10 && < 0.12
     , lens >= 4.16
     , mtl >= 2
     , text >= 1.1
-  if impl(ghc < 8.0)
-    build-depends:
-      semigroups >= 0.15
 
 library
   import: common
@@ -80,26 +76,19 @@
     Crypto.JOSE.Types.Orphans
 
   build-depends:
-    attoparsec
     , base64-bytestring >= 1.2.1.0 && < 1.3
     , concise >= 0.1
     , containers >= 0.5
     , cryptonite >= 0.7
     , memory >= 0.7
-    , monad-time >= 0.1
-    , template-haskell >= 2.4
-    , safe >= 0.3
-    , unordered-containers == 0.2.*
+    , monad-time >= 0.3
+    , template-haskell >= 2.11
     , time >= 1.5
     , network-uri >= 2.6
     , QuickCheck >= 2.9
     , quickcheck-instances
     , x509 >= 1.4
-    , vector
 
-  if impl(ghc<8)
-    build-depends: fail
-
   hs-source-dirs: src
 
 source-repository head
@@ -121,18 +110,11 @@
     Types
 
   build-depends:
-    attoparsec
     , base64-bytestring
     , containers
     , cryptonite
-    , memory
-    , monad-time
-    , template-haskell
-    , safe
-    , unordered-containers
     , time
     , network-uri
-    , vector
     , x509
     , pem
 
diff --git a/src/Crypto/JOSE.hs b/src/Crypto/JOSE.hs
--- a/src/Crypto/JOSE.hs
+++ b/src/Crypto/JOSE.hs
@@ -19,7 +19,8 @@
 -}
 module Crypto.JOSE
   (
-    module Crypto.JOSE.Compact
+    vulnerableToHashFlood
+  , module Crypto.JOSE.Compact
   , module Crypto.JOSE.Error
   , module Crypto.JOSE.JWK
   , module Crypto.JOSE.JWK.Store
@@ -31,6 +32,22 @@
 import Crypto.JOSE.JWK
 import Crypto.JOSE.JWK.Store
 import Crypto.JOSE.JWS
-import Crypto.JOSE.Types (base64url)
 
+import qualified Data.Aeson.KeyMap as KeyMap
+
 {-# ANN module ("HLint: ignore Use import/export shortcut" :: String) #-}
+
+-- | /aeson/ supports multiple map implementations.  The
+-- implementation using @Data.HashMap@ from *unordered-containers*
+-- is vulnerable to hash-flooding DoS attacks.  If your program
+-- processes JOSE objects from untrusted sources, you can check this
+-- value to find out if the *aeson* build uses a secure map
+-- implementation, or not.
+--
+vulnerableToHashFlood :: Bool
+vulnerableToHashFlood = case KeyMap.coercionToMap of
+  -- Don't check that the map implementation is NOT Data.HashMap, in
+  -- case some other insecure implementation emerges.  Instead,
+  -- check that the implementation IS known to be secure.
+  Just _  -> False
+  Nothing -> True
diff --git a/src/Crypto/JOSE/Header.hs b/src/Crypto/JOSE/Header.hs
--- a/src/Crypto/JOSE/Header.hs
+++ b/src/Crypto/JOSE/Header.hs
@@ -63,6 +63,7 @@
 
 
 import qualified Control.Monad.Fail as Fail
+import Data.Kind (Type)
 import Data.List.NonEmpty (NonEmpty)
 import Data.Monoid ((<>))
 import Data.Proxy (Proxy(..))
@@ -70,8 +71,9 @@
 import Control.Lens (Lens', Getter, review, to)
 import Data.Aeson (FromJSON(..), Object, Value, encode, object)
 import Data.Aeson.Types (Pair, Parser)
+import qualified Data.Aeson.Key as Key
+import qualified Data.Aeson.KeyMap as M
 import qualified Data.ByteString.Lazy as L
-import qualified Data.HashMap.Strict as M
 import qualified Data.Text as T
 
 import qualified Crypto.JOSE.JWA.JWS as JWA.JWS
@@ -83,7 +85,7 @@
 
 -- | A thing with parameters.
 --
-class HasParams (a :: * -> *) where
+class HasParams (a :: Type -> Type) where
   -- | Return a list of parameters,
   -- each paired with whether it is protected or not.
   params :: ProtectionIndicator p => a p -> [(Bool, Pair)]
@@ -228,14 +230,16 @@
   -> Maybe Object
   -> Maybe Object
   -> Parser (Maybe (HeaderParam p a))
-headerOptional k hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
-  (Just _, Just _)    -> fail $ "duplicate header " ++ show k
+headerOptional kText hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
+  (Just _, Just _)    -> fail $ "duplicate header " ++ show kText
   (Just v, Nothing)   -> Just . HeaderParam getProtected <$> parseJSON v
   (Nothing, Just v)   -> maybe
     (fail "unprotected header not supported")
     (\p -> Just . HeaderParam p <$> parseJSON v)
     getUnprotected
   (Nothing, Nothing)  -> pure Nothing
+  where
+    k = Key.fromText kText
 
 -- | Parse an optional parameter that, if present, MUST be carried
 -- in the protected header.
@@ -246,11 +250,13 @@
   -> Maybe Object
   -> Maybe Object
   -> Parser (Maybe a)
-headerOptionalProtected k hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
-  (Just _, Just _)    -> fail $ "duplicate header " ++ show k
-  (_, Just _) -> fail $ "header must be protected: " ++ show k
+headerOptionalProtected kText hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
+  (Just _, Just _)    -> fail $ "duplicate header " ++ show kText
+  (_, Just _) -> fail $ "header must be protected: " ++ show kText
   (Just v, _) -> Just <$> parseJSON v
   _           -> pure Nothing
+  where
+    k = Key.fromText kText
 
 -- | Parse a required parameter that may be carried in either
 -- the protected or the unprotected header.
@@ -261,14 +267,16 @@
   -> Maybe Object
   -> Maybe Object
   -> Parser (HeaderParam p a)
-headerRequired k hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
-  (Just _, Just _)    -> fail $ "duplicate header " ++ show k
+headerRequired kText hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
+  (Just _, Just _)    -> fail $ "duplicate header " ++ show kText
   (Just v, Nothing)   -> HeaderParam getProtected <$> parseJSON v
   (Nothing, Just v)   -> maybe
     (fail "unprotected header not supported")
     (\p -> HeaderParam p <$> parseJSON v)
     getUnprotected
   (Nothing, Nothing)  -> fail $ "missing required header " ++ show k
+  where
+    k = Key.fromText kText
 
 -- | Parse a required parameter that MUST be carried
 -- in the protected header.
@@ -279,11 +287,13 @@
   -> Maybe Object
   -> Maybe Object
   -> Parser a
-headerRequiredProtected k hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
-  (Just _, Just _)    -> fail $ "duplicate header " ++ show k
-  (_, Just _) -> fail $ "header must be protected: " <> show k
+headerRequiredProtected kText hp hu = case (hp >>= M.lookup k, hu >>= M.lookup k) of
+  (Just _, Just _)    -> fail $ "duplicate header " ++ show kText
+  (_, Just _) -> fail $ "header must be protected: " <> show kText
   (Just v, _) -> parseJSON v
-  _           -> fail $ "missing required protected header: " <> show k
+  _           -> fail $ "missing required protected header: " <> show kText
+  where
+    k = Key.fromText kText
 
 
 critObjectParser
@@ -292,7 +302,7 @@
 critObjectParser reserved exts o s
   | s `elem` reserved         = Fail.fail "crit key is reserved"
   | s `notElem` exts          = Fail.fail "crit key is not understood"
-  | not (s `M.member` o)      = Fail.fail "crit key is not present in headers"
+  | not (Key.fromText s `M.member` o) = Fail.fail "crit key is not present in headers"
   | otherwise                 = pure s
 
 -- | Parse a "crit" header param
diff --git a/src/Crypto/JOSE/JWA/JWE.hs b/src/Crypto/JOSE/JWA/JWE.hs
--- a/src/Crypto/JOSE/JWA/JWE.hs
+++ b/src/Crypto/JOSE/JWA/JWE.hs
@@ -24,14 +24,13 @@
 
 import Data.Maybe (catMaybes)
 
-import qualified Data.HashMap.Strict as M
-
 import Crypto.JOSE.JWK
 import Crypto.JOSE.TH
 import Crypto.JOSE.Types
-import Crypto.JOSE.Types.Internal (objectPairs)
+import Crypto.JOSE.Types.Internal (insertToObject)
 
 import Data.Aeson
+import qualified Data.Aeson.KeyMap as M
 
 
 -- | RFC 7518 §4.  Cryptographic Algorithms for Key Management
@@ -84,7 +83,7 @@
 algObject s = object [("alg", s)]
 
 algWithParamsObject :: ToJSON a => a -> Value -> Value
-algWithParamsObject a s = object $ ("alg", s) : objectPairs (toJSON a)
+algWithParamsObject a s = insertToObject "alg" s (toJSON a)
 
 instance ToJSON AlgWithParams where
   toJSON RSA1_5       = algObject "RSA1_5"
diff --git a/src/Crypto/JOSE/JWA/JWK.hs b/src/Crypto/JOSE/JWA/JWK.hs
--- a/src/Crypto/JOSE/JWA/JWK.hs
+++ b/src/Crypto/JOSE/JWA/JWK.hs
@@ -96,9 +96,9 @@
 import qualified Crypto.PubKey.Curve25519 as Curve25519
 import Crypto.Random
 import Data.Aeson
+import qualified Data.Aeson.KeyMap as M
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
-import qualified Data.HashMap.Strict as M
 import Data.List.NonEmpty (NonEmpty)
 import qualified Data.Text as T
 import Data.X509 as X509
@@ -200,8 +200,9 @@
       else pure Nothing)
 
 instance ToJSON RSAPrivateKeyParameters where
-  toJSON RSAPrivateKeyParameters {..} = object $
-    ("d" .= rsaD) : maybe [] (Types.objectPairs . toJSON) rsaOptionalParameters
+  toJSON RSAPrivateKeyParameters {..} =
+    Types.insertToObject "d" rsaD
+      $ maybe (Object mempty) toJSON rsaOptionalParameters
 
 instance Arbitrary RSAPrivateKeyParameters where
   arbitrary = RSAPrivateKeyParameters <$> arbitrary <*> arbitrary
@@ -361,11 +362,13 @@
         else pure Nothing
 
 instance ToJSON RSAKeyParameters where
-  toJSON RSAKeyParameters {..} = object $
-      ("kty" .= ("RSA" :: T.Text))
-    : ("n" .= _rsaN)
-    : ("e" .= _rsaE)
-    : maybe [] (Types.objectPairs . toJSON) _rsaPrivateKeyParameters
+  toJSON RSAKeyParameters {..} =
+    Types.insertManyToObject
+      [ "kty" .= ("RSA" :: T.Text)
+      , "n" .= _rsaN
+      , "e" .= _rsaE
+      ]
+      $ maybe (Object mempty) toJSON _rsaPrivateKeyParameters
 
 instance Arbitrary RSAKeyParameters where
   arbitrary = RSAKeyParameters
@@ -602,10 +605,10 @@
       Just s      -> fail $ "unsupported \"kty\": " <> show s
 
 instance ToJSON KeyMaterial where
-  toJSON (ECKeyMaterial p)  = object $ Types.objectPairs (toJSON p)
-  toJSON (RSAKeyMaterial p) = object $ Types.objectPairs (toJSON p)
-  toJSON (OctKeyMaterial p) = object $ Types.objectPairs (toJSON p)
-  toJSON (OKPKeyMaterial p) = object $ Types.objectPairs (toJSON p)
+  toJSON (ECKeyMaterial p)  = toJSON p
+  toJSON (RSAKeyMaterial p) = toJSON p
+  toJSON (OctKeyMaterial p) = toJSON p
+  toJSON (OKPKeyMaterial p) = toJSON p
 
 -- | Keygen parameters.
 --
diff --git a/src/Crypto/JOSE/JWK.hs b/src/Crypto/JOSE/JWK.hs
--- a/src/Crypto/JOSE/JWK.hs
+++ b/src/Crypto/JOSE/JWK.hs
@@ -209,17 +209,18 @@
       | otherwise = pure k
 
 instance ToJSON JWK where
-  toJSON JWK{..} = object $ catMaybes
-    [ fmap ("alg" .=) _jwkAlg
-    , fmap ("use" .=) _jwkUse
-    , fmap ("key_ops" .=) _jwkKeyOps
-    , fmap ("kid" .=) _jwkKid
-    , fmap ("x5u" .=) _jwkX5u
-    , fmap (("x5c" .=) . fmap Types.Base64X509) _jwkX5cRaw
-    , fmap ("x5t" .=) _jwkX5t
-    , fmap ("x5t#S256" .=) _jwkX5tS256
-    ]
-    ++ Types.objectPairs (toJSON _jwkMaterial)
+  toJSON JWK{..} = Types.insertManyToObject kvs (toJSON _jwkMaterial)
+    where
+      kvs = catMaybes
+        [ fmap ("alg" .=) _jwkAlg
+        , fmap ("use" .=) _jwkUse
+        , fmap ("key_ops" .=) _jwkKeyOps
+        , fmap ("kid" .=) _jwkKid
+        , fmap ("x5u" .=) _jwkX5u
+        , fmap (("x5c" .=) . fmap Types.Base64X509) _jwkX5cRaw
+        , fmap ("x5t" .=) _jwkX5t
+        , fmap ("x5t#S256" .=) _jwkX5tS256
+        ]
 
 -- | Generate a JWK.  Apart from key parameters, no other parameters are set.
 --
diff --git a/src/Crypto/JOSE/JWK/Store.hs b/src/Crypto/JOSE/JWK/Store.hs
--- a/src/Crypto/JOSE/JWK/Store.hs
+++ b/src/Crypto/JOSE/JWK/Store.hs
@@ -31,21 +31,44 @@
 -- | A KeyDB is just a filesystem directory
 newtype KeyDB = KeyDB FilePath
 
-instance (MonadIO m, HasKid h)
-    => VerificationKeyStore m (h p) ClaimsSet KeyDB where
-  getVerificationKeys h claims (KeyDB dir) = liftIO $
+instance (MonadIO m, t'Crypto.JOSE.Header.HasKid' h)
+    => VerificationKeyStore m (h p) t'Crypto.JWT.ClaimsSet' KeyDB where
+  'getVerificationKeys' h claims (KeyDB dir) = liftIO $
     fmap catMaybes . traverse findKey $ catMaybes
-      [ preview (kid . _Just . param) h
-      , preview (claimIss . _Just . string) claims]
+      [ preview ('Crypto.JOSE.Header.kid' . _Just . 'Crypto.JOSE.Header.param') h
+      , preview ('Crypto.JWT.claimIss' . _Just . 'Crypto.JWT.string') claims]
     where
     findKey :: T.Text -> IO (Maybe JWK)
     findKey s =
       let path = dir <> "/" <> T.unpack s <> ".jwk"
       in handle
-        (\(_ :: IOException) -> pure Nothing)
-        (decode <$> L.readFile path)
+        (\\(_ :: IOException) -> pure Nothing)
+        (decode \<$> L.readFile path)
 @
 
+The next example shows how to retrieve public keys from a JWK Set
+(@\/.well-known\/jwks.json@) resource.  For production use, it would
+be a good idea to cache the HTTP response.  Thanks to Steve Mao for
+this example.
+
+@
+-- | URI of JWK Set
+newtype JWKsURI = JWKsURI String
+
+instance (MonadIO m, t'Crypto.JOSE.Header.HasKid' h)
+    => 'VerificationKeyStore' m (h p) t'Crypto.JWT.ClaimsSet' JWKsURI where
+  'getVerificationKeys' h claims (JWKsURI url) = liftIO $
+    maybe [] (:[]) . join
+      \<$> traverse findKey (preview ('Crypto.JOSE.Header.kid' . _Just . 'Crypto.JOSE.Header.param') h)
+    where
+    findKey :: T.Text -> IO (Maybe JWK)
+    findKey kid' =
+      handle (\\(_ :: SomeException) -> pure Nothing) $ do
+        request \<- setRequestCheckStatus \<$> parseRequest url
+        response \<- getResponseBody \<$> httpJSON request
+        keys \<- getVerificationKeys h claims response
+        pure $ find (\\j -> view 'Crypto.JOSE.JWK.jwkKid' j == Just kid') keys
+@
 -}
 module Crypto.JOSE.JWK.Store
   (
diff --git a/src/Crypto/JOSE/JWS.hs b/src/Crypto/JOSE/JWS.hs
--- a/src/Crypto/JOSE/JWS.hs
+++ b/src/Crypto/JOSE/JWS.hs
@@ -90,7 +90,6 @@
 import Data.Maybe (catMaybes, fromMaybe)
 import Data.Monoid ((<>))
 import Data.List.NonEmpty (NonEmpty)
-import Data.Traversable (traverse)
 import Data.Word (Word8)
 
 import Control.Lens hiding ((.=))
@@ -98,8 +97,8 @@
 import Control.Monad.Error.Lens (throwing, throwing_)
 import Control.Monad.Except (MonadError, unless)
 import Data.Aeson
+import qualified Data.Aeson.KeyMap as M
 import qualified Data.ByteString as B
-import qualified Data.HashMap.Strict as M
 import qualified Data.Set as S
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as T
@@ -423,11 +422,11 @@
       else (\p s -> JWS p (pure s)) <$> o .: "payload" <*> parseJSON (Object o)
 
 instance (HasParams a, ProtectionIndicator p) => ToJSON (JWS [] p a) where
-  toJSON (JWS p [s]) = object $ "payload" .= p : Types.objectPairs (toJSON s)
+  toJSON (JWS p [s]) = Types.insertToObject "payload" p (toJSON s)
   toJSON (JWS p ss) = object ["payload" .= p, "signatures" .= ss]
 
 instance (HasParams a, ProtectionIndicator p) => ToJSON (JWS Identity p a) where
-  toJSON (JWS p (Identity s)) = object $ "payload" .= p : Types.objectPairs (toJSON s)
+  toJSON (JWS p (Identity s)) = Types.insertToObject "payload" p (toJSON s)
 
 
 signingInput
diff --git a/src/Crypto/JOSE/TH.hs b/src/Crypto/JOSE/TH.hs
--- a/src/Crypto/JOSE/TH.hs
+++ b/src/Crypto/JOSE/TH.hs
@@ -100,9 +100,7 @@
   let
     derive = map mkName ["Eq", "Ord", "Show"]
   in
-#if ! MIN_VERSION_template_haskell(2,11,0)
-    dataD (cxt []) (mkName s) [] (map conQ vs) derive
-#elif ! MIN_VERSION_template_haskell(2,12,0)
+#if ! MIN_VERSION_template_haskell(2,12,0)
     dataD (cxt []) (mkName s) [] Nothing (map conQ vs) (mapM conT derive)
 #else
     dataD (cxt []) (mkName s) [] Nothing (map conQ vs) [return (DerivClause Nothing (map ConT derive))]
diff --git a/src/Crypto/JOSE/Types/Internal.hs b/src/Crypto/JOSE/Types/Internal.hs
--- a/src/Crypto/JOSE/Types/Internal.hs
+++ b/src/Crypto/JOSE/Types/Internal.hs
@@ -23,7 +23,8 @@
 -}
 module Crypto.JOSE.Types.Internal
   (
-    objectPairs
+    insertToObject
+  , insertManyToObject
   , encodeB64
   , parseB64
   , encodeB64Url
@@ -36,7 +37,6 @@
   ) where
 
 import Data.Bifunctor (first)
-import Data.Monoid ((<>))
 import Data.Tuple (swap)
 import Data.Word (Word8)
 
@@ -44,19 +44,28 @@
 import Control.Lens.Cons.Extras
 import Crypto.Number.Basic (log2)
 import Data.Aeson.Types
+import qualified Data.Aeson.KeyMap as M
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Base64 as B64
 import qualified Data.ByteString.Base64.URL as B64U
-import qualified Data.HashMap.Strict as M
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as E
 
--- | Convert a JSON object into a list of pairs or the empty list
--- if the JSON value is not an object.
+-- | Insert the given key and value into the given @Value@, which
+-- is expected to be an @Object@.  If the value is not an @Object@,
+-- this is a no-op.
 --
-objectPairs :: Value -> [Pair]
-objectPairs (Object o) = M.toList o
-objectPairs _ = []
+insertToObject :: ToJSON v => Key -> v -> Value -> Value
+insertToObject k v (Object o) = Object $ M.insert k (toJSON v) o
+insertToObject _ _ v          = v
+
+-- | Insert several key/value pairs to the given @Value@, which
+-- is expected to be an @Object@.  If the value is not an @Object@,
+-- this is a no-op.
+--
+insertManyToObject :: [Pair] -> Value -> Value
+insertManyToObject kvs (Object o) = Object $ foldr (uncurry M.insert) o kvs
+insertManyToObject _ v            = v
 
 -- | Produce a parser of base64 encoded text from a bytestring parser.
 --
diff --git a/src/Crypto/JWT.hs b/src/Crypto/JWT.hs
--- a/src/Crypto/JWT.hs
+++ b/src/Crypto/JWT.hs
@@ -115,13 +115,11 @@
 import Control.Applicative
 import Control.Monad
 import Control.Monad.Time (MonadTime(..))
-#if ! MIN_VERSION_monad_time(0,2,0)
-import Control.Monad.Time.Instances ()
-#endif
 import Data.Foldable (traverse_)
 import Data.Functor.Identity
 import Data.Maybe
 import qualified Data.String
+import Data.Semigroup ((<>))
 
 import Control.Lens (
   makeClassy, makeClassyPrisms, makePrisms,
@@ -132,7 +130,10 @@
 import Control.Monad.Except (MonadError)
 import Control.Monad.Reader (ReaderT, asks, runReaderT)
 import Data.Aeson
-import qualified Data.HashMap.Strict as M
+import qualified Data.Aeson.Key as Key
+import qualified Data.Aeson.KeyMap as KeyMap
+import qualified Data.Map as M
+import qualified Data.Set as S
 import qualified Data.Text as T
 import Data.Time (NominalDiffTime, UTCTime, addUTCTime)
 import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
@@ -251,7 +252,7 @@
   , _claimNbf :: Maybe NumericDate
   , _claimIat :: Maybe NumericDate
   , _claimJti :: Maybe T.Text
-  , _unregisteredClaims :: M.HashMap T.Text Value
+  , _unregisteredClaims :: M.Map T.Text Value
   }
   deriving (Eq, Show)
 
@@ -320,7 +321,7 @@
   fmap (\a' -> h { _claimJti = a' }) (f a)
 
 -- | Claim Names can be defined at will by those using JWTs.
-unregisteredClaims :: Lens' ClaimsSet (M.HashMap T.Text Value)
+unregisteredClaims :: Lens' ClaimsSet (M.Map T.Text Value)
 unregisteredClaims f h@ClaimsSet{ _unregisteredClaims = a} =
   fmap (\a' -> h { _unregisteredClaims = a' }) (f a)
 
@@ -333,10 +334,31 @@
 addClaim :: T.Text -> Value -> ClaimsSet -> ClaimsSet
 addClaim k v = over unregisteredClaims (M.insert k v)
 
-filterUnregistered :: M.HashMap T.Text Value -> M.HashMap T.Text Value
-filterUnregistered = M.filterWithKey (\k _ -> k `notElem` registered) where
-  registered = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"]
+registeredClaims :: S.Set T.Text
+registeredClaims = S.fromDistinctAscList
+  [ "aud"
+  , "exp"
+  , "iat"
+  , "iss"
+  , "jti"
+  , "nbf"
+  , "sub"
+  ]
 
+filterUnregistered :: M.Map T.Text Value -> M.Map T.Text Value
+filterUnregistered m =
+#if MIN_VERSION_containers(0,5,8)
+  m `M.withoutKeys` registeredClaims
+#else
+  m `M.difference` M.fromSet (const ()) registeredClaims
+#endif
+
+toKeyMap :: M.Map T.Text Value -> KeyMap.KeyMap Value
+toKeyMap = KeyMap.fromMap . M.mapKeysMonotonic Key.fromText
+
+fromKeyMap :: KeyMap.KeyMap Value -> M.Map T.Text Value
+fromKeyMap = M.mapKeysMonotonic Key.toText . KeyMap.toMap
+
 instance FromJSON ClaimsSet where
   parseJSON = withObject "JWT Claims Set" (\o -> ClaimsSet
     <$> o .:? "iss"
@@ -346,18 +368,22 @@
     <*> o .:? "nbf"
     <*> o .:? "iat"
     <*> o .:? "jti"
-    <*> pure (filterUnregistered o))
+    <*> pure (filterUnregistered . fromKeyMap $ o)
+    )
 
 instance ToJSON ClaimsSet where
-  toJSON (ClaimsSet iss sub aud exp' nbf iat jti o) = object $ catMaybes [
-    fmap ("iss" .=) iss
-    , fmap ("sub" .=) sub
-    , fmap ("aud" .=) aud
-    , fmap ("exp" .=) exp'
-    , fmap ("nbf" .=) nbf
-    , fmap ("iat" .=) iat
-    , fmap ("jti" .=) jti
-    ] ++ M.toList (filterUnregistered o)
+  toJSON (ClaimsSet iss sub aud exp' nbf iat jti o) = Object $
+    ( KeyMap.fromMap . M.fromDistinctAscList $ catMaybes
+      [ fmap ("aud" .=) aud
+      , fmap ("exp" .=) exp'
+      , fmap ("iat" .=) iat
+      , fmap ("iss" .=) iss
+      , fmap ("jti" .=) jti
+      , fmap ("nbf" .=) nbf
+      , fmap ("sub" .=) sub
+      ]
+    )
+    <> toKeyMap (filterUnregistered o)
 
 
 data JWTValidationSettings = JWTValidationSettings
diff --git a/test/AESKW.hs b/test/AESKW.hs
--- a/test/AESKW.hs
+++ b/test/AESKW.hs
@@ -14,8 +14,6 @@
 
 module AESKW where
 
-import Control.Applicative ((<$>), pure)
-
 import qualified Data.ByteString as B
 import Crypto.Cipher.AES
 import Crypto.Cipher.Types
@@ -28,6 +26,7 @@
 import Crypto.JOSE.AESKW
 
 
+aeskwProperties :: TestTree
 aeskwProperties = testGroup "AESKW"
   [ testProperty "AESKW round-trip" prop_roundTrip
   ]
@@ -52,3 +51,4 @@
     16 -> assert $ check (cipherInit kek :: CryptoFailable AES128)
     24 -> assert $ check (cipherInit kek :: CryptoFailable AES192)
     32 -> assert $ check (cipherInit kek :: CryptoFailable AES256)
+    _  -> assert False  -- can't happen
diff --git a/test/JWK.hs b/test/JWK.hs
--- a/test/JWK.hs
+++ b/test/JWK.hs
@@ -21,7 +21,6 @@
 import Control.Lens (_Left, _Right, review, view)
 import Control.Lens.Extras (is)
 import Data.Aeson
-import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as L
 import Test.Hspec
@@ -298,8 +297,9 @@
       pk <- _A2_result
       pure $ view asPublicKey sk == Just pk
 
-rfc8037_A1_jwkJson = ""
-  <> "{\"kty\":\"OKP\",\"crv\":\"Ed25519\","
+rfc8037_A1_jwkJson :: L.ByteString
+rfc8037_A1_jwkJson =
+     "{\"kty\":\"OKP\",\"crv\":\"Ed25519\","
   <> "\"d\":\"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A\","
   <> "\"x\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\"}"
 
diff --git a/test/JWS.hs b/test/JWS.hs
--- a/test/JWS.hs
+++ b/test/JWS.hs
@@ -25,12 +25,10 @@
 import Control.Monad.Except (runExceptT)
 import Data.Aeson
 import qualified Data.ByteString as BS
-import qualified Data.ByteString.Lazy as L
 import qualified Data.ByteString.Base64.URL as B64U
 import Test.Hspec
 
 import Crypto.JOSE.Compact
-import Crypto.JOSE.Error (Error)
 import Crypto.JOSE.JWA.JWK
 import Crypto.JOSE.JWK
 import Crypto.JOSE.JWS
@@ -224,11 +222,11 @@
 
   it "computes the HMAC correctly" $
     fst (withDRG drg $
-      runExceptT (sign alg (jwk ^. jwkMaterial) (signingInput' ^. recons)))
+      runExceptT (sign alg_ (k ^. jwkMaterial) (signingInput' ^. recons)))
       `shouldBe` (Right mac :: Either Error BS.ByteString)
 
   it "validates the JWS correctly" $
-    (jws >>= verifyJWS defaultValidationSettings jwk)
+    (jws >>= verifyJWS defaultValidationSettings k)
     `shouldBe` Right examplePayloadBytes
 
   where
@@ -239,14 +237,14 @@
       \cGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
     compactJWS = signingInput' <> ".dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
     jws = decodeCompact compactJWS :: Either Error (CompactJWS JWSHeader)
-    alg = JWA.JWS.HS256
-    h = newJWSHeader ((), alg)
+    alg_ = JWA.JWS.HS256
+    h = newJWSHeader ((), alg_)
         & typ .~ Just (HeaderParam () "JWT")
     mac = view recons
       [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173,
       187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83,
       132, 141, 121]
-    jwk = fromOctets
+    k = fromOctets
       [3,35,53,75,43,15,165,188,131,126,6,101,119,123,166,143,90,179,40,
        230,240,84,201,40,169,15,132,178,210,80,46,191,211,251,90,146,
        210,6,71,239,150,138,180,195,119,98,61,34,61,46,33,114,5,46,79,8,
@@ -277,11 +275,11 @@
 appendixA2Spec :: Spec
 appendixA2Spec = describe "RFC 7515 A.2. Example JWS using RSASSA-PKCS-v1_5 SHA-256" $ do
   it "computes the signature correctly" $
-    fst (withDRG drg $ runExceptT (sign JWA.JWS.RS256 (jwk ^. jwkMaterial) signingInput'))
+    fst (withDRG drg $ runExceptT (sign JWA.JWS.RS256 (k ^. jwkMaterial) signingInput'))
       `shouldBe` (Right sig :: Either Error BS.ByteString)
 
   it "validates the signature correctly" $
-    verify JWA.JWS.RS256 (jwk ^. jwkMaterial) signingInput' sig
+    verify JWA.JWS.RS256 (k ^. jwkMaterial) signingInput' sig
       `shouldBe` (Right True :: Either Error Bool)
 
   it "prohibits signing with 1024-bit key" $
@@ -295,7 +293,7 @@
       \.\
       \eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt\
       \cGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
-    jwk = fromJust $ decode "\
+    k = fromJust $ decode "\
       \{\"kty\":\"RSA\",\
       \ \"n\":\"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx\
             \HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs\
@@ -336,7 +334,7 @@
 appendixA3Spec :: Spec
 appendixA3Spec = describe "RFC 7515 A.3.  Example JWS using ECDSA P-256 SHA-256" $
   it "validates the signature correctly" $
-    verify JWA.JWS.ES256 (jwk ^. jwkMaterial) signingInput' sig
+    verify JWA.JWS.ES256 (k ^. jwkMaterial) signingInput' sig
     `shouldBe` (Right True :: Either Error Bool)
   where
     signingInput' = "\
@@ -344,7 +342,7 @@
       \.\
       \eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt\
       \cGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
-    jwk = fromJust $ decode "\
+    k = fromJust $ decode "\
       \{\"kty\":\"EC\",\
       \ \"crv\":\"P-256\",\
       \ \"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\",\
@@ -504,7 +502,7 @@
 cfrgSpec :: Spec
 cfrgSpec = describe "RFC 8037 signature/validation test vectors" $ do
   let
-    jwk = fromJust $ decode "\
+    k = fromJust $ decode "\
       \{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\
       \\"d\":\"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A\",\
       \\"x\":\"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\"}"
@@ -516,10 +514,10 @@
     sig = BS.pack sigOctets
     signingInput = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc"
   it "computes the correct signature" $
-    fst (withDRG drg $ runExceptT (sign JWA.JWS.EdDSA (view jwkMaterial jwk) signingInput))
+    fst (withDRG drg $ runExceptT (sign JWA.JWS.EdDSA (view jwkMaterial k) signingInput))
       `shouldBe` (Right sig :: Either Error BS.ByteString)
   it "validates signatures correctly" $
-    verify JWA.JWS.EdDSA (view jwkMaterial jwk) signingInput sig
+    verify JWA.JWS.EdDSA (view jwkMaterial k) signingInput sig
       `shouldBe` (Right True :: Either Error Bool)
 
 base64urlSpec :: Spec
diff --git a/test/JWT.hs b/test/JWT.hs
--- a/test/JWT.hs
+++ b/test/JWT.hs
@@ -12,7 +12,6 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE OverloadedStrings #-}
 
@@ -25,16 +24,11 @@
 import Control.Lens.Extras (is)
 import Control.Monad.Except (runExceptT)
 import Control.Monad.Trans (liftIO)
-import Control.Monad.Reader (MonadReader(..), ReaderT, runReaderT)
-import Control.Monad.State (execState)
-import Control.Monad.Time (MonadTime(..))
+import Control.Monad.Reader (runReaderT)
 import Data.Aeson hiding ((.=))
-import Data.Functor.Identity (runIdentity)
-import Data.HashMap.Strict (insert)
 import qualified Data.Set as S
 import Data.Time
 import Network.URI (parseURI)
-import Safe (headMay)
 import Test.Hspec
 
 import Crypto.JWT
@@ -50,18 +44,14 @@
 exampleClaimsSet = emptyClaimsSet
   & claimIss .~ preview stringOrUri ("joe" :: String)
   & claimExp .~ intDate "2011-03-22 18:43:00"
-  & over unregisteredClaims (insert "http://example.com/is_root" (Bool True))
   & addClaim "http://example.com/is_root" (Bool True)
 
-#if ! MIN_VERSION_monad_time(0,3,0)
-instance Monad m => MonadTime (ReaderT UTCTime m) where
-  currentTime = ask
-#endif
-
 spec :: Spec
 spec = do
   let conf = set algorithms (S.singleton None)
               (defaultJWTValidationSettings (const False))
+      headMay []    = Nothing
+      headMay (h:_) = Just h
 
   describe "JWT Claims Set" $ do
     it "parses from JSON correctly" $
@@ -74,9 +64,9 @@
         decode claimsJSON `shouldBe` Just exampleClaimsSet
 
     it "JWT compact round-trip" $ do
-      jwk <- genJWK $ RSAGenParam 256
+      k <- genJWK $ RSAGenParam 256
       res <- runExceptT $ do
-        token <- signClaims jwk (newJWSHeader ((), RS512)) emptyClaimsSet
+        token <- signClaims k (newJWSHeader ((), RS512)) emptyClaimsSet
         token' <- decodeCompact . encodeCompact $ token
         liftIO $ token' `shouldBe` token
       either (error . show) return (res :: Either JWTError ()) :: IO ()
diff --git a/test/Properties.hs b/test/Properties.hs
--- a/test/Properties.hs
+++ b/test/Properties.hs
@@ -16,7 +16,6 @@
 
 module Properties where
 
-import Control.Applicative
 import Control.Monad.Except (runExceptT)
 
 import Data.Aeson
@@ -27,11 +26,11 @@
 import Test.QuickCheck.Monadic
 import Test.QuickCheck.Instances ()
 
-import Crypto.JOSE.Error (Error(..))
 import Crypto.JOSE.Types
 import Crypto.JOSE.JWK
 import Crypto.JOSE.JWS
 
+properties :: TestTree
 properties = testGroup "Properties"
   [ testProperty "SizedBase64Integer round-trip"
     (prop_roundTrip :: SizedBase64Integer -> Property)
@@ -58,9 +57,9 @@
 prop_rsaSignAndVerify msg = monadicIO $ do
   keylen <- pick $ elements ((`div` 8) <$> [2048, 3072, 4096])
   k :: JWK <- run $ genJWK (RSAGenParam keylen)
-  alg <- pick $ elements [RS256, RS384, RS512, PS256, PS384, PS512]
-  monitor (collect alg)
-  wp (runExceptT (signJWS msg [(newJWSHeader (Protected, alg), k)]
+  alg_ <- pick $ elements [RS256, RS384, RS512, PS256, PS384, PS512]
+  monitor (collect alg_)
+  wp (runExceptT (signJWS msg [(newJWSHeader (Protected, alg_), k)]
     >>= verifyJWS defaultValidationSettings k)) (checkSignVerifyResult msg)
 
 prop_bestJWSAlg :: B.ByteString -> Property
@@ -70,11 +69,11 @@
   case bestJWSAlg k of
     Left (KeyMismatch _) -> pre False  -- skip non-signing keys
     Left _ -> assert False
-    Right alg -> do
-      monitor (collect alg)
+    Right alg_ -> do
+      monitor (collect alg_)
       let
         go = do
-          jws <- signJWS msg [(newJWSHeader (Protected, alg), k)]
+          jws <- signJWS msg [(newJWSHeader (Protected, alg_), k)]
           verifyJWS defaultValidationSettings k jws
       wp (runExceptT go) (checkSignVerifyResult msg)
 
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -12,10 +12,12 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 
+import Test.Hspec
 import Test.Tasty
 import Test.Tasty.Hspec
-import Test.Tasty.QuickCheck
 
+import Crypto.JOSE (vulnerableToHashFlood)
+
 import AESKW
 import Examples
 import JWK
@@ -32,9 +34,18 @@
 
 unitTestsIO :: IO TestTree
 unitTestsIO = do
-  types <- testSpec "Types" Types.spec
-  jwk <- testSpec "JWK" JWK.spec
-  jws <- testSpec "JWS" JWS.spec
-  jwt <- testSpec "JWT" JWT.spec
-  examples <- testSpec "Examples" Examples.spec
-  return $ testGroup "Unit tests" [types, jwk, jws, jwt, examples]
+  testGroup "Unit tests" <$> sequenceA specs
+  where
+    specs =
+      [ testSpec "Types" Types.spec
+      , testSpec "JWK" JWK.spec
+      , testSpec "JWS" JWS.spec
+      , testSpec "JWT" JWT.spec
+      , testSpec "Examples" Examples.spec
+      , testSpec "Security" securitySpec
+      ]
+
+securitySpec :: Spec
+securitySpec = describe "security characteristics" $
+  it "not vulnerable to hash-flood" $
+    vulnerableToHashFlood `shouldBe` False
