diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 authentication to the web, logging in using a FIDO U2F Security Key, finger
 print scanner and some other authenticator.
 
-This project was originally developed in 2020 as a Zurihac project by
+This project was originally developed in 2020 as a [Zurihac project](https://wire.engineering/haskell/events/2020/06/18/wire-goes-zurihac.html) by
 [@arianvp](https://github.com/arianvp/) and [@duijf](https://github.com/duijf).
 Starting September 2021 a team at [Tweag](https://www.tweag.io/) was sponsored
 by [Mercury](https://mercury.com/) to create a production-ready open-source
@@ -34,6 +34,12 @@
 multiple hours.
 
 All further instructions in this README assume that you are in a Nix shell.
+
+## Getting Started
+To get started with the library it is best to read the documentation of the
+`Crypto.WebAuthn` module. An example of how to use this library to implement a
+relying party can be found in the `server` directory, particularly the
+`server/src/Main.hs` module.
 
 ## Developing the Library
 The [Haskell Language Server (hls)][hls] and [Ormolu][ormolu] are highly
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,12 @@
+### 0.2.0.0
+
+* [#115](https://github.com/tweag/webauthn/pull/115) Increase the upper bound
+  of the supported Aeson versions, allowing the library to be built with Aeson
+  2.0. Drop the deriving-aeson dependency.
+* [#117](https://github.com/tweag/webauthn/pull/117) Rename and expand
+  documentation for attestation statement format errors. Some unused errors
+  were removed.
+
 ### 0.1.1.0
 
 * [#111](https://github.com/tweag/webauthn/pull/111) Support the
diff --git a/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidKey.hs b/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidKey.hs
--- a/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidKey.hs
+++ b/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidKey.hs
@@ -175,20 +175,48 @@
 data VerificationError
   = -- | The public key in the certificate is different from the on in the
     -- attested credential data
-    VerificationErrorCredentialKeyMismatch
+    PublicKeyMismatch
+      { -- | The public key part of the credential data
+        credentialDataPublicKey :: Cose.PublicKey,
+        -- | The public key extracted from the signed certificate
+        certificatePublicKey :: Cose.PublicKey
+      }
   | -- | The challenge field of the certificate extension does not match the
     -- clientDataHash
-    VerificationErrorClientDataHashMismatch
+    -- (first: challenge from certificate extension, second: clientDataHash)
+    HashMismatch
+      { -- | The challenge part of the
+        -- [@attestation-extension@](https://source.android.com/security/keystore/attestation#attestation-extension)
+        certificateChallenge :: Digest SHA256,
+        -- | The client data hash
+        clientDataHash :: Digest SHA256
+      }
   | -- | The "attestation" extension is scoped to all applications instead of just the RpId
-    VerificationErrorAndroidKeyAllApplicationsFieldFound
-  | -- | The origin field(s) were not equal to KM_ORIGIN_GENERATED
-    VerificationErrorAndroidKeyOriginFieldInvalid
+    AndroidKeyAllApplicationsFieldFound
+  | -- | The origin field(s) were not equal to KM_ORIGIN_GENERATED (0)
+    -- (first: tee-enforced origin, second: software-enforced origin (if allowed by the specified Format))
+    AndroidKeyOriginFieldInvalid
+      { -- | The origin enforced by the trusted execution environment
+        teeEnforcedOrigin :: Maybe Integer,
+        -- | The origin enforced by software. NOTE: This field is explicitly
+        -- set to `Nothing` if the `Format` specified `TeeEnforced` as the
+        -- `requiredTrustLevel`.
+        softwareEnforcedOrigin :: Maybe Integer
+      }
   | -- | The purpose field(s) were not equal to the singleton set containing
-    -- KM_PURPOSE_SIGN
-    VerificationErrorAndroidKeyPurposeFieldInvalid
+    -- KM_PURPOSE_SIGN (2)
+    -- (first: tee-enforced purpose, second: software-enforced purpose (if allowed by the specified Format))
+    AndroidKeyPurposeFieldInvalid
+      { -- | The purpose enforced by the trusted execution environment
+        teeEnforcedPurpose :: Maybe (Set Integer),
+        -- | The purpose enforced by software. NOTE: This field is explicitly
+        -- set to `Nothing` if the `Format` specified `TeeEnforced` as the
+        -- `requiredTrustLevel`.
+        softwareEnforcedPurpose :: Maybe (Set Integer)
+      }
   | -- | The Public key cannot verify the signature over the authenticatorData
     -- and the clientDataHash.
-    VerificationErrorVerificationFailure Text
+    VerificationFailure Text
   deriving (Show, Exception)
 
 -- | [(spec)](https://android.googlesource.com/platform/hardware/libhardware/+/master/include/hardware/keymaster_defs.h)
@@ -204,7 +232,7 @@
 
   asfIdentifier _ = "android-key"
 
-  asfDecode _ xs = do
+  asfDecode _ xs =
     case (xs !? "alg", xs !? "sig", xs !? "x5c") of
       (Just (CBOR.TInt algId), Just (CBOR.TBytes sig), Just (CBOR.TList (NE.nonEmpty -> Just x5cRaw))) -> do
         alg <- Cose.toCoseSignAlg algId
@@ -247,16 +275,19 @@
     let signedData = rawData <> convert (M.unClientDataHash clientDataHash)
     case Cose.verify alg pubKey signedData sig of
       Right () -> pure ()
-      Left err -> failure $ VerificationErrorVerificationFailure err
+      Left err -> failure $ VerificationFailure err
 
     -- 3. Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the
     -- attestedCredentialData in authenticatorData.
-    unless (Cose.fromCose (M.acdCredentialPublicKey adAttestedCredentialData) == pubKey) $ failure VerificationErrorCredentialKeyMismatch
+    let credentialPublicKey = Cose.fromCose (M.acdCredentialPublicKey adAttestedCredentialData)
+    unless (credentialPublicKey == pubKey) . failure $ PublicKeyMismatch credentialPublicKey pubKey
 
     -- 4. Verify that the attestationChallenge field in the attestation certificate extension data is identical to
     -- clientDataHash.
     -- See https://source.android.com/security/keystore/attestation for the ASN1 description
-    unless (attestationChallenge attExt == M.unClientDataHash clientDataHash) $ failure VerificationErrorClientDataHashMismatch
+    let attChallenge = attestationChallenge attExt
+    let clientDataHashDigest = M.unClientDataHash clientDataHash
+    unless (attChallenge == clientDataHashDigest) . failure $ HashMismatch attChallenge clientDataHashDigest
 
     -- 5. Verify the following using the appropriate authorization list from the attestation certificate extension data:
 
@@ -264,8 +295,8 @@
     -- authorization list (softwareEnforced nor teeEnforced), since
     -- PublicKeyCredential MUST be scoped to the RP ID.
     let software = softwareEnforced attExt
-        tee = teeEnforced attExt
-    when (isJust (allApplications software) || isJust (allApplications tee)) $ failure VerificationErrorAndroidKeyAllApplicationsFieldFound
+    let tee = teeEnforced attExt
+    when (isJust (allApplications software) || isJust (allApplications tee)) $ failure AndroidKeyAllApplicationsFieldFound
 
     -- 5.b For the following, use only the teeEnforced authorization list if the
     -- RP wants to accept only keys from a trusted execution environment,
@@ -273,15 +304,15 @@
     -- 5.b.1 The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED.
     -- 5.b.2 The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN.
     -- NOTE: This statement is ambiguous as the purpose field is a set. Existing libraries take the same approach, checking if KM_PURPOSE_SIGN is the only member.
-    let targetSet = Just $ Set.singleton kmPurposeSign
+    let targetSet = Set.singleton kmPurposeSign
     case requiredTrustLevel of
       SoftwareEnforced -> do
-        unless (origin software == Just kmOriginGenerated || origin tee == Just kmOriginGenerated) $ failure VerificationErrorAndroidKeyOriginFieldInvalid
-        unless (targetSet == purpose software || targetSet == purpose tee) $ failure VerificationErrorAndroidKeyPurposeFieldInvalid
+        unless (origin software == Just kmOriginGenerated || origin tee == Just kmOriginGenerated) . failure $ AndroidKeyOriginFieldInvalid (origin tee) (origin software)
+        unless (Just targetSet == purpose software || Just targetSet == purpose tee) . failure $ AndroidKeyPurposeFieldInvalid (purpose tee) (purpose software)
         pure ()
       TeeEnforced -> do
-        unless (origin tee == Just kmOriginGenerated) $ failure VerificationErrorAndroidKeyOriginFieldInvalid
-        unless (targetSet == purpose tee) $ failure VerificationErrorAndroidKeyPurposeFieldInvalid
+        unless (origin tee == Just kmOriginGenerated) . failure $ AndroidKeyOriginFieldInvalid (origin tee) Nothing
+        unless (Just targetSet == purpose tee) . failure $ AndroidKeyPurposeFieldInvalid (purpose tee) Nothing
         pure ()
 
     -- 6. If successful, return implementation-specific values representing attestation type Basic and attestation trust
diff --git a/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidSafetyNet.hs b/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidSafetyNet.hs
--- a/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidSafetyNet.hs
+++ b/src/Crypto/WebAuthn/AttestationStatementFormat/AndroidSafetyNet.hs
@@ -121,16 +121,25 @@
 data VerificationError
   = -- | The receiced nonce was not set to the concatenation of the
     -- authenticator data and client data hash
-    VerificationErrorInvalidNonce
-  | -- | The response was created to far in the past
-    -- (first: now, second: generated time)
-    VerificationErrorResponseTooOld HG.DateTime HG.DateTime
-  | -- | The response was created to far in the future
-    -- (first: now, second: generated time)
-    VerificationErrorResponseInFuture HG.DateTime HG.DateTime
+    NonceMismatch
+      { -- | Nonce from the AndroidSafetyNet response
+        responseNonce :: Text,
+        -- | Base64 encoding of the SHA-256 hash of the concatenation of
+        -- authenticatorData and clientDataHash
+        calculatedNonce :: Text
+      }
+  | -- | The response was created to far in the past or future
+    ResponseTimeInvalid
+      { -- | The UTC time minus the allowed drift specified in the `Format`.
+        lowerBound :: HG.DateTime,
+        -- | The UTC time plus the allowed drift specified in the `Format`.
+        upperBound :: HG.DateTime,
+        -- | The UTC time when the Android SafetyNet response was generated
+        generatedtime :: HG.DateTime
+      }
   | -- | The integrity check failed based on the required integrity from the
     -- format
-    VerificationErrorFailedIntegrityCheck Integrity
+    IntegrityCheckFailed Integrity
   deriving (Show, Exception)
 
 androidHostName :: VerificationHostName
@@ -215,7 +224,8 @@
     let signedData = rawData <> BA.convert (M.unClientDataHash clientDataHash)
     let hashedData = Hash.hashWith Hash.SHA256 signedData
     let encodedData = decodeUtf8 . Base64.encode $ BA.convert hashedData
-    unless (nonce response == encodedData) . failure $ VerificationErrorInvalidNonce
+    let responseNonce = nonce response
+    unless (responseNonce == encodedData) . failure $ NonceMismatch responseNonce encodedData
 
     -- 4. Verify that the SafetyNet response actually came from the SafetyNet service by following the steps in the
     -- SafetyNet online documentation.
@@ -236,15 +246,17 @@
     -- NOTE: For WebAuthn, we need not care about the package name or the app's signing certificate. The Nonce as
     -- has already been dealt with.
     let generatedTime = HG.timeConvert $ timestampMs response
-    when ((generatedTime `HG.timeAdd` driftBackwardsTolerance) < now) $ failure $ VerificationErrorResponseTooOld now generatedTime
-    when (generatedTime > (now `HG.timeAdd` driftForwardsTolerance)) $ failure $ VerificationErrorResponseInFuture now generatedTime
+    let lowerBound = now `HG.timeAdd` negate (HG.toSeconds driftBackwardsTolerance)
+    let upperBound = now `HG.timeAdd` driftForwardsTolerance
+    when (generatedTime < lowerBound) $ failure $ ResponseTimeInvalid lowerBound upperBound generatedTime
+    when (generatedTime > upperBound) $ failure $ ResponseTimeInvalid lowerBound upperBound generatedTime
 
     let integrity = case (basicIntegrity response, ctsProfileMatch response) of
           (_, True) -> CTSProfileIntegrity
           (True, False) -> BasicIntegrity
           (False, False) -> NoIntegrity
     unless (integrity >= requiredIntegrity) $
-      failure $ VerificationErrorFailedIntegrityCheck integrity
+      failure $ IntegrityCheckFailed integrity
 
     -- 5. If successful, return implementation-specific values representing attestation type Basic and attestation trust
     -- path x5c.
diff --git a/src/Crypto/WebAuthn/AttestationStatementFormat/Apple.hs b/src/Crypto/WebAuthn/AttestationStatementFormat/Apple.hs
--- a/src/Crypto/WebAuthn/AttestationStatementFormat/Apple.hs
+++ b/src/Crypto/WebAuthn/AttestationStatementFormat/Apple.hs
@@ -48,12 +48,22 @@
 data VerificationError
   = -- | The nonce found in the certificate extension does not match the
     -- expected nonce
-    -- (first: expected, second: received)
-    NonceMismatch (Digest SHA256) (Digest SHA256)
+    NonceMismatch
+      { -- | The SHA256 hash of the concatenation of the @authenticatorData@
+        -- and @clientDataHash@
+        calculatedNonce :: Digest SHA256,
+        -- | The nonce from the Apple nonce certificate extension
+        -- (1.2.840.113635.100.8.2)
+        receivedNonce :: Digest SHA256
+      }
   | -- | The public Key found in the certificate does not match the
     -- credential's public key.
-    -- (first: credential, second: certificate)
-    PublickeyMismatch Cose.PublicKey Cose.PublicKey
+    PublicKeyMismatch
+      { -- | The public key part of the credential data
+        credentialDataPublicKey :: Cose.PublicKey,
+        -- | The public key extracted from the signed certificate
+        certificatePublicKey :: Cose.PublicKey
+      }
   deriving (Show, Exception)
 
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation)
@@ -152,7 +162,7 @@
       -- 5. Verify that the credential public key equals the Subject Public Key
       -- of credCert.
       let credentialPublicKey = Cose.fromCose $ M.acdCredentialPublicKey credData
-      unless (credentialPublicKey == pubKey) . failure $ PublickeyMismatch credentialPublicKey pubKey
+      unless (credentialPublicKey == pubKey) . failure $ PublicKeyMismatch credentialPublicKey pubKey
 
       -- 6. If successful, return implementation-specific values representing
       -- attestation type Anonymization CA and attestation trust path x5c.
diff --git a/src/Crypto/WebAuthn/AttestationStatementFormat/FidoU2F.hs b/src/Crypto/WebAuthn/AttestationStatementFormat/FidoU2F.hs
--- a/src/Crypto/WebAuthn/AttestationStatementFormat/FidoU2F.hs
+++ b/src/Crypto/WebAuthn/AttestationStatementFormat/FidoU2F.hs
@@ -36,28 +36,21 @@
 instance Show Format where
   show = Text.unpack . M.asfIdentifier
 
--- | Decoding errors specific to Fido U2F attestation
-data DecodingError
-  = -- | No Signature field was present
-    NoSig
-  | -- | No x5c certificate was present
-    NoX5C
-  | -- | Multiple x5c certificates were found where only one was expected
-    MultipleX5C
-  | -- | There was an error decoding the x5c certificate, string is the error resulted by the `Data.X509.decodeSignedCertificate` function
-    DecodingErrorX5C String
-  deriving (Show, Exception)
-
 -- | Verification errors specific to Fido U2F attestation
 data VerificationError
   = -- | The public key in the certificate was not an EC Key or the curve was not the p256 curve
-    InvalidCertificatePublicKey X509.PubKey
+    CertificatePublicKeyInvalid X509.PubKey
   | -- | The credential public key is not an ECDSA key
-    NonECDSACredentialPublicKey Cose.PublicKey
+    CredentialPublicKeyNotECDSA Cose.PublicKey
   | -- | The x and/or y coordinates of the credential public key don't have a length of 32 bytes
-    WrongCoordinateSize Int Int
+    CoordinateSizeInvalid
+      { -- | Actual length in bytes of the x coordinate
+        xLength :: Int,
+        -- | Actual length in bytes of the y coordinate
+        yLength :: Int
+      }
   | -- | The provided public key cannot validate the signature over the verification data
-    InvalidSignature X509.SignatureFailure
+    SignatureInvalid X509.SignatureFailure
   deriving (Show, Exception)
 
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation)
@@ -116,8 +109,8 @@
         X509.PubKeyEC pk ->
           case X509.ecPubKeyCurveName pk of
             Just SEC_p256r1 -> pure ()
-            _ -> failure $ InvalidCertificatePublicKey certPubKey
-        _ -> failure $ InvalidCertificatePublicKey certPubKey
+            _ -> failure $ CertificatePublicKeyInvalid certPubKey
+        _ -> failure $ CertificatePublicKeyInvalid certPubKey
 
       -- 3. Extract the claimed rpIdHash from authenticatorData, and the claimed
       -- credentialId and credentialPublicKey from authenticatorData.attestedCredentialData.
@@ -140,7 +133,7 @@
         Cose.PublicKeyECDSA {ecdsaX = xb, ecdsaY = yb} -> do
           let xlen = BS.length xb
               ylen = BS.length yb
-          unless (xlen == 32 && ylen == 32) $ failure $ WrongCoordinateSize xlen ylen
+          unless (xlen == 32 && ylen == 32) $ failure $ CoordinateSizeInvalid xlen ylen
 
           -- 4.c Let publicKeyU2F be the concatenation 0x04 || x || y.
           let publicKeyU2F = BS.singleton 0x04 <> xb <> yb
@@ -149,15 +142,20 @@
           -- clientDataHash || credentialId || publicKeyU2F) (see Section 4.3 of
           -- [FIDO-U2F-Message-Formats]).
           let credId = M.unCredentialId acdCredentialId
-              verificationData = BS.singleton 0x00 <> BA.convert (M.unRpIdHash adRpIdHash) <> BA.convert (M.unClientDataHash clientDataHash) <> credId <> publicKeyU2F
+              verificationData =
+                BS.singleton 0x00
+                  <> BA.convert (M.unRpIdHash adRpIdHash)
+                  <> BA.convert (M.unClientDataHash clientDataHash)
+                  <> credId
+                  <> publicKeyU2F
 
           -- 6. Verify the sig using verificationData and the certificate public key per
           -- section 4.1.4 of [SEC1] with SHA-256 as the hash function used in step two.
           case X509.verifySignature (X509.SignatureALG X509.HashSHA256 X509.PubKeyALG_EC) certPubKey verificationData sig of
             X509.SignaturePass -> pure ()
-            X509.SignatureFailed e -> failure $ InvalidSignature e
+            X509.SignatureFailed e -> failure $ SignatureInvalid e
           pure ()
-        key -> failure $ NonECDSACredentialPublicKey key
+        key -> failure $ CredentialPublicKeyNotECDSA key
 
       -- 7. Optionally, inspect x5c and consult externally provided knowledge to
       -- determine whether attStmt conveys a Basic or AttCA attestation.
diff --git a/src/Crypto/WebAuthn/AttestationStatementFormat/Packed.hs b/src/Crypto/WebAuthn/AttestationStatementFormat/Packed.hs
--- a/src/Crypto/WebAuthn/AttestationStatementFormat/Packed.hs
+++ b/src/Crypto/WebAuthn/AttestationStatementFormat/Packed.hs
@@ -19,8 +19,8 @@
 import qualified Crypto.WebAuthn.Cose.Internal.Verify as Cose
 import qualified Crypto.WebAuthn.Cose.Key as Cose
 import Crypto.WebAuthn.Internal.Utils (IdFidoGenCeAAGUID (IdFidoGenCeAAGUID), failure)
+import Crypto.WebAuthn.Model (AAGUID)
 import qualified Crypto.WebAuthn.Model.Types as M
-import Data.ASN1.Error (ASN1Error)
 import qualified Data.ASN1.OID as OID
 import Data.Aeson (ToJSON, object, toJSON, (.=))
 import Data.Bifunctor (first)
@@ -63,25 +63,31 @@
 data VerificationError
   = -- | The Algorithm from the attestation format does not match the algorithm
     -- of the key in the credential data
-    VerificationErrorAlgorithmMismatch
+    AlgorithmMismatch
+      { -- | The algorithm received in the attestation statement
+        statementAlg :: Cose.CoseSignAlg,
+        -- | The algorithm of the credentialPublicKey in authenticatorData
+        credentialAlg :: Cose.CoseSignAlg
+      }
   | -- | The statement key cannot verify the signature over the attested
     -- credential data and client data for self attestation
-    VerificationErrorInvalidSignature Text
+    InvalidSignature Text
   | -- | The statement certificate cannot verify the signature over the attested
     -- credential data and client data for nonself attestation
-    VerificationErrorVerificationFailure X509.SignatureFailure
+    VerificationFailure X509.SignatureFailure
   | -- | The certificate does not meet the requirements layed out in the
     -- webauthn specification
     -- https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation-cert-requirements
-    VerificationErrorCertificateRequirementsUnmet
-  | -- | The (supposedly) ASN1 encoded certificate extension could not be
-    -- decoded
-    VerificationErrorASN1Error ASN1Error
-  | -- | The certificate extension does not contain a AAGUID
-    VerificationErrorCredentialAAGUIDMissing
+    CertificateRequirementsUnmet
   | -- | The AAGUID in the certificate extension does not match the AAGUID in
     -- the authenticator data
-    VerificationErrorCertificateAAGUIDMismatch
+    CertificateAAGUIDMismatch
+      { -- | AAGUID from the id-fido-gen-ce-aaguid certificate extension
+        certificateExtensionAAGUID :: AAGUID,
+        -- | A AGUID from the attested credential data in the authenticator
+        -- data
+        attestedCredentialDataAAGUID :: AAGUID
+      }
   deriving (Show, Exception)
 
 instance M.AttestationStatementFormat Format where
@@ -136,25 +142,25 @@
           -- Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
           let key = M.acdCredentialPublicKey credData
               signAlg = Cose.keySignAlg key
-          when (stmtAlg /= signAlg) $ failure VerificationErrorAlgorithmMismatch
+          when (stmtAlg /= signAlg) . failure $ AlgorithmMismatch stmtAlg signAlg
 
           -- Verify that sig is a valid signature over the concatenation of
           -- authenticatorData and clientDataHash using the credential public key with alg.
           case Cose.verify signAlg (Cose.fromCose key) signedData stmtSig of
             Right () -> pure ()
-            Left err -> failure $ VerificationErrorInvalidSignature err
+            Left err -> failure $ InvalidSignature err
 
           pure $ M.SomeAttestationType M.AttestationTypeSelf
 
         -- Basic, AttCA
-        Just (x5c@(certCred :| _), IdFidoGenCeAAGUID credAAGUID) -> do
+        Just (x5c@(certCred :| _), IdFidoGenCeAAGUID certAAGUID) -> do
           let cert = X509.getCertificate certCred
               pubKey = X509.certPubKey cert
           -- Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using
           -- the attestation public key in attestnCert with the algorithm specified in alg.
           case X509.verifySignature (X509.SignatureALG X509.HashSHA256 X509.PubKeyALG_EC) pubKey signedData stmtSig of
             X509.SignaturePass -> pure ()
-            X509.SignatureFailed err -> failure $ VerificationErrorVerificationFailure err
+            X509.SignatureFailed err -> failure $ VerificationFailure err
 
           -- Verify that attestnCert meets the requirements in § 8.2.1 Packed Attestation Statement Certificate
           -- Requirements.
@@ -165,12 +171,12 @@
                 && hasDnElement X509.DnCommonName dnElements
                 && findDnElement X509.DnOrganizationUnit dnElements == Just "Authenticator Attestation"
             )
-            $ failure VerificationErrorCertificateRequirementsUnmet
+            $ failure CertificateRequirementsUnmet
 
           -- If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that
           -- the value of this extension matches the aaguid in authenticatorData.
           let aaguid = M.acdAaguid credData
-          unless (aaguid == credAAGUID) $ failure VerificationErrorCertificateAAGUIDMismatch
+          unless (certAAGUID == aaguid) . failure $ CertificateAAGUIDMismatch certAAGUID aaguid
 
           pure $
             M.SomeAttestationType $
diff --git a/src/Crypto/WebAuthn/AttestationStatementFormat/TPM.hs b/src/Crypto/WebAuthn/AttestationStatementFormat/TPM.hs
--- a/src/Crypto/WebAuthn/AttestationStatementFormat/TPM.hs
+++ b/src/Crypto/WebAuthn/AttestationStatementFormat/TPM.hs
@@ -11,6 +11,8 @@
   ( format,
     Format (..),
     VerificationError (..),
+    -- Exported because it's part of an error constructor
+    TPMAlgId (..),
   )
 where
 
@@ -24,6 +26,7 @@
 import qualified Crypto.WebAuthn.Cose.Internal.Verify as Cose
 import qualified Crypto.WebAuthn.Cose.Key as Cose
 import Crypto.WebAuthn.Internal.Utils (IdFidoGenCeAAGUID (IdFidoGenCeAAGUID), failure)
+import Crypto.WebAuthn.Model.Identifier (AAGUID)
 import qualified Crypto.WebAuthn.Model.Types as M
 import Data.ASN1.Error (ASN1Error)
 import Data.ASN1.OID (OID)
@@ -233,48 +236,69 @@
 data VerificationError
   = -- | The public key in the certificate is different from the on in the
     -- attested credential data
-    VerificationErrorCredentialKeyMismatch
-  | -- | The magic number in certInfo was not set to TPM_GENERATED_VALUE
-    VerificationErrorInvalidMagicNumber Word32
-  | -- | The type in certInfo was not set to TPM_ST_ATTEST_CERTIFY
-    VerificationErrorInvalidType Word16
+    PublicKeyMismatch
+      { -- | The public key extracted from the certificate
+        certificatePublicKey :: Cose.PublicKey,
+        -- | The public key part of the credential data
+        credentialDataPublicKey :: Cose.PublicKey
+      }
+  | -- | The magic number in certInfo was not set to TPM_GENERATED_VALUE (0xff544347)
+    MagicNumberInvalid Word32
+  | -- | The type in certInfo was not set to TPM_ST_ATTEST_CERTIFY (0x8017)
+    TypeInvalid Word16
   | -- | The algorithm specified in the nameAlg field is unsupported or is not
     -- a valid name algorithm
-    VerificationErrorInvalidNameAlgorithm
+    NameAlgorithmInvalid TPMAlgId
   | -- | The calulated name does not match the provided name.
-    -- (first: expected, second: received)
-    VerificationErrorInvalidName BS.ByteString BS.ByteString
+    NameMismatch
+      { -- | The name calculated from the TPMT_PUBLIC structure with the name
+        -- algorithm.
+        pubAreaName :: BS.ByteString,
+        -- | The expected name from TPMS_CERTIFY_INFO of the TPMS_ATTEST
+        -- structure
+        certifyInfoName :: BS.ByteString
+      }
   | -- | The public key in the certificate was invalid, either because the it
     -- had an unexpected algorithm, or because it was otherwise malformed
-    VerificationErrorInvalidPublicKey Text
-  | -- | The certificate didn't have the expected version-value
-    -- (first: expected, second: received)
-    VerificationErrorCertificateVersion Int Int
+    PublicKeyInvalid Text
+  | -- | The certificate didn't have the expected version-value (2)
+    CertificateVersionInvalid Int
   | -- | The Public key cannot verify the signature over the authenticatorData
     -- and the clientDataHash.
-    VerificationErrorVerificationFailure Text
+    VerificationFailure Text
   | -- | The subject field was not empty
-    VerificationErrorNonEmptySubjectField
+    SubjectFieldNotEmpty [(OID, X509.ASN1CharacterString)]
   | -- | The vendor was unknown
-    VerificationErrorUnknownVendor
+    VendorUnknown Text
   | -- | The Extended Key Usage did not contain the 2.23.133.8.3 OID
-    VerificationErrorExtKeyOIDMissing
+    ExtKeyOIDMissing
   | -- | The CA component of the basic constraints extension was set to True
-    VerificationErrorBasicConstraintsTrue
-  | -- | The AAGUID in the certificate extension does not match the AAGUID in
-    -- the authenticator data
-    VerificationErrorCertificateAAGUIDMismatch
+    BasicConstraintsTrue
+  | -- | The AAGUID in the attested credential data does not match the AAGUID
+    -- in the fido certificate extension
+    CertificateAAGUIDMismatch
+      { -- | AAGUID from the id-fido-gen-ce-aaguid certificate extension
+        certificateExtensionAAGUID :: AAGUID,
+        -- | AAGUID from the attested credential data
+        attestedCredentialDataAAGUID :: AAGUID
+      }
   | -- | The (supposedly) ASN1 encoded certificate extension could not be
     -- decoded
-    VerificationErrorASN1Error ASN1Error
+    ASN1Error ASN1Error
   | -- | The certificate extension does not contain a AAGUID
-    VerificationErrorCredentialAAGUIDMissing
+    CredentialAAGUIDMissing
   | -- | The desired algorithm does not have a known associated hash function
-    VerificationErrorUnknownHashFunction
+    HashFunctionUnknown
   | -- | The calculated hash over the attToBeSigned does not match the received
     -- hash
-    -- (first: calculated, second: received)
-    VerificationErrorHashMismatch BS.ByteString BS.ByteString
+    HashMismatch
+      { -- | The hash of the concatenation of the @authenticatorData@ and
+        -- @clientDataHash@ (@attToBeSigned@) calculated by the @alg@ specified in
+        -- the @Statement@.
+        calculatedHash :: BS.ByteString,
+        -- | The extra data from the TPMS_ATTEST structure.
+        extraData :: BS.ByteString
+      }
   deriving (Show, Exception)
 
 -- [(spec)](https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf)
@@ -499,7 +523,7 @@
       -- fields of pubArea is identical to the credentialPublicKey in the
       -- attestedCredentialData in authenticatorData.
       let pubKey = Cose.fromCose $ M.acdCredentialPublicKey adAttestedCredentialData
-      unless (pubKey == pubAreaKey) $ failure VerificationErrorCredentialKeyMismatch
+      unless (pubAreaKey == pubKey) . failure $ PublicKeyMismatch pubAreaKey pubKey
 
       -- 3. Concatenate authenticatorData and clientDataHash to form attToBeSigned.
       let attToBeSigned = adRawData <> BA.convert (M.unClientDataHash clientDataHash)
@@ -507,20 +531,20 @@
       -- 4. Validate that certInfo is valid:
       -- 4.1 Verify that magic is set to TPM_GENERATED_VALUE.
       let magic = tpmsaMagic certInfo
-      unless (magic == tpmGeneratedValue) . failure $ VerificationErrorInvalidMagicNumber magic
+      unless (magic == tpmGeneratedValue) . failure $ MagicNumberInvalid magic
 
       -- 4.2 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
       let typ = tpmsaType certInfo
-      unless (typ == tpmStAttestCertify) . failure $ VerificationErrorInvalidType typ
+      unless (typ == tpmStAttestCertify) . failure $ TypeInvalid typ
 
       -- 4.3 Verify that extraData is set to the hash of attToBeSigned using
       -- the hash algorithm employed in "alg".
       case hashWithCorrectAlgorithm alg attToBeSigned of
         Just attHash -> do
           let extraData = tpmsaExtraData certInfo
-          unless (attHash == extraData) . failure $ VerificationErrorHashMismatch attHash extraData
+          unless (attHash == extraData) . failure $ HashMismatch attHash extraData
           pure ()
-        Nothing -> failure VerificationErrorUnknownHashFunction
+        Nothing -> failure HashFunctionUnknown
 
       -- 4.5 Verify that attested contains a TPMS_CERTIFY_INFO structure as
       -- specified in [TPMv2-Part2] section 10.12.3, whose name field contains
@@ -528,22 +552,22 @@
       -- nameAlg field of pubArea using the procedure specified in
       -- [TPMv2-Part1] section 16.
       let mPubAreaHash = case tpmtpNameAlg pubArea of
-            TPMAlgSHA1 -> Just $ BA.convert $ hashWith SHA1 pubAreaRaw
-            TPMAlgSHA256 -> Just $ BA.convert $ hashWith SHA256 pubAreaRaw
-            TPMAlgECC -> Nothing
-            TPMAlgRSA -> Nothing
+            TPMAlgSHA1 -> Right $ BA.convert $ hashWith SHA1 pubAreaRaw
+            TPMAlgSHA256 -> Right $ BA.convert $ hashWith SHA256 pubAreaRaw
+            TPMAlgECC -> Left TPMAlgECC
+            TPMAlgRSA -> Left TPMAlgRSA
 
       case mPubAreaHash of
-        Just pubAreaHash -> do
+        Right pubAreaHash -> do
           let pubName = LBS.toStrict $
                 Put.runPut $ do
                   Put.putWord16be (tpmtpNameAlgRaw pubArea)
                   Put.putByteString pubAreaHash
 
           let name = tpmsciName (tpmsaAttested certInfo)
-          unless (name == pubName) . failure $ VerificationErrorInvalidName pubName name
+          unless (name == pubName) . failure $ NameMismatch pubName name
           pure ()
-        Nothing -> failure VerificationErrorInvalidNameAlgorithm
+        Left alg -> failure $ NameAlgorithmInvalid alg
 
       -- 4.6 Verify that x5c is present
       -- NOTE: Done in decoding
@@ -560,8 +584,8 @@
       case Cose.fromX509 $ X509.certPubKey unsignedAikCert of
         Right certPubKey -> case Cose.verify alg certPubKey certInfoRaw sig of
           Right () -> pure ()
-          Left err -> failure $ VerificationErrorVerificationFailure err
-        Left err -> failure $ VerificationErrorInvalidPublicKey err
+          Left err -> failure $ VerificationFailure err
+        Left err -> failure $ PublicKeyInvalid err
 
       -- 4.9 Verify that aikCert meets the requirements in § 8.3.1 TPM Attestation
       -- Statement Certificate Requirements.
@@ -569,23 +593,25 @@
       -- 4.9.1 Version MUST be set to 3.
       -- Version ::= INTEGER { v1(0), v2(1), v3(2) }, see https://datatracker.ietf.org/doc/html/rfc5280.html#section-4.1
       let version = X509.certVersion unsignedAikCert
-      unless (version == 2) . failure $ VerificationErrorCertificateVersion 2 version
+      unless (version == 2) . failure $ CertificateVersionInvalid version
       -- 4.9.2. Subject field MUST be set to empty.
-      unless (null . X509.getDistinguishedElements $ X509.certSubjectDN unsignedAikCert) $ failure VerificationErrorNonEmptySubjectField
+      let subject = X509.getDistinguishedElements $ X509.certSubjectDN unsignedAikCert
+      unless (null subject) . failure $ SubjectFieldNotEmpty subject
       -- 4.9.3 The Subject Alternative Name extension MUST be set as defined in
       -- [TPMv2-EK-Profile] section 3.2.9.
       -- 4.9.3.1 The TPM manufacturer identifies the manufacturer of the TPM. This value MUST be the
       -- vendor ID defined in the TCG Vendor ID Registry[3]
-      unless (Set.member (tpmManufacturer subjectAlternativeName) tpmManufacturers) $ failure VerificationErrorUnknownVendor
+      let vendor = tpmManufacturer subjectAlternativeName
+      unless (Set.member vendor tpmManufacturers) . failure $ VendorUnknown vendor
 
       -- 4.9.4 The Extended Key Usage extension MUST contain the OID
       -- 2.23.133.8.3 ("joint-iso-itu-t(2) internationalorganizations(23) 133
       -- tcg-kp(8) tcg-kp-AIKCertificate(3)").
-      unless (X509.KeyUsagePurpose_Unknown [2, 23, 133, 8, 3] `elem` extendedKeyUsage) $ failure VerificationErrorExtKeyOIDMissing
+      unless (X509.KeyUsagePurpose_Unknown [2, 23, 133, 8, 3] `elem` extendedKeyUsage) $ failure ExtKeyOIDMissing
 
       -- 4.9.5 The Basic Constraints extension MUST have the CA component set
       -- to false.
-      when basicConstraintsCA $ failure VerificationErrorBasicConstraintsTrue
+      when basicConstraintsCA $ failure BasicConstraintsTrue
 
       -- 4.9.6 An Authority Information Access (AIA) extension with entry
       -- id-ad-ocsp and a CRL Distribution Point extension [RFC5280] are both
@@ -598,8 +624,10 @@
       -- If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4
       -- (id-fido-gen-ce-aaguid) verify that the value of this extension
       -- matches the aaguid in authenticatorData.
+      let credentialAAGUID = M.acdAaguid adAttestedCredentialData
       case aaguidExt of
-        Just (IdFidoGenCeAAGUID aaguid) -> unless (M.acdAaguid adAttestedCredentialData == aaguid) $ failure VerificationErrorCertificateAAGUIDMismatch
+        Just (IdFidoGenCeAAGUID aaguid) -> do
+          unless (aaguid == credentialAAGUID) . failure $ CertificateAAGUIDMismatch aaguid credentialAAGUID
         Nothing -> pure ()
 
       pure $
diff --git a/src/Crypto/WebAuthn/Internal/Utils.hs b/src/Crypto/WebAuthn/Internal/Utils.hs
--- a/src/Crypto/WebAuthn/Internal/Utils.hs
+++ b/src/Crypto/WebAuthn/Internal/Utils.hs
@@ -5,10 +5,8 @@
 --
 -- Internal utilities
 module Crypto.WebAuthn.Internal.Utils
-  ( JSONEncoding,
-    EnumJSONEncoding,
-    Aeson.CustomJSON (..),
-    Lowercase,
+  ( jsonEncodingOptions,
+    enumJSONEncodingOptions,
     failure,
     certificateSubjectKeyIdentifier,
     IdFidoGenCeAAGUID (..),
@@ -23,32 +21,37 @@
 import qualified Data.ASN1.Parse as ASN1
 import Data.ASN1.Prim (ASN1 (OctetString))
 import qualified Data.ASN1.Types as ASN1
+import qualified Data.Aeson as Aeson
 import Data.Bifunctor (first)
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as LBS
 import Data.Char (toLower)
+import Data.List (stripPrefix)
 import Data.List.NonEmpty (NonEmpty)
+import Data.Maybe (fromMaybe)
 import qualified Data.UUID as UUID
 import Data.Validation (Validation (Failure))
 import Data.X509 (Extension)
 import qualified Data.X509 as X509
-import qualified Deriving.Aeson as Aeson
-import GHC.TypeLits (Symbol)
 
--- | Custom JSONEncoding for use in the library. We add a "lit" prefix to every
+-- | Custom Aeson Options for use in the library. We add a "lit" prefix to every
 -- field that would otherwise be a Haskell keyword.
-type JSONEncoding = Aeson.CustomJSON '[Aeson.OmitNothingFields, Aeson.FieldLabelModifier (Aeson.StripPrefix "lit")]
-
--- | Type for 'Aeson.StringModifier' that makes all characters lowercase
-data Lowercase
-
--- | Deriving.Aeson instance turning a string into lowercase.
-instance Aeson.StringModifier Lowercase where
-  getStringModifier = map toLower
+jsonEncodingOptions :: Aeson.Options
+jsonEncodingOptions =
+  Aeson.defaultOptions
+    { Aeson.omitNothingFields = True,
+      Aeson.fieldLabelModifier = \l -> fromMaybe l $ stripPrefix "lit" l
+    }
 
 -- | Custom JSON Encoding for enumerations, strips the given prefix and maps
 -- all constructors to lowercase.
-type EnumJSONEncoding (prefix :: Symbol) = Aeson.CustomJSON '[Aeson.ConstructorTagModifier '[Aeson.StripPrefix prefix, Lowercase]]
+enumJSONEncodingOptions :: String -> Aeson.Options
+enumJSONEncodingOptions prefix =
+  Aeson.defaultOptions
+    { Aeson.omitNothingFields = True,
+      Aeson.fieldLabelModifier = \l -> fromMaybe l $ stripPrefix prefix l,
+      Aeson.constructorTagModifier = \l -> map toLower . fromMaybe l $ stripPrefix prefix l
+    }
 
 -- | A convenience function for creating a 'Validation' failure of a single
 -- 'NonEmpty' value
diff --git a/src/Crypto/WebAuthn/Metadata/FidoRegistry.hs b/src/Crypto/WebAuthn/Metadata/FidoRegistry.hs
--- a/src/Crypto/WebAuthn/Metadata/FidoRegistry.hs
+++ b/src/Crypto/WebAuthn/Metadata/FidoRegistry.hs
@@ -15,9 +15,8 @@
   )
 where
 
-import Crypto.WebAuthn.Internal.Utils (EnumJSONEncoding)
+import Crypto.WebAuthn.Internal.Utils (enumJSONEncodingOptions)
 import qualified Data.Aeson as Aeson
-import Deriving.Aeson (CustomJSON (CustomJSON))
 import GHC.Generics (Generic)
 
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#user-verification-methods)
@@ -36,8 +35,13 @@
   | USER_VERIFY_NONE
   | USER_VERIFY_ALL
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "USER_VERIFY_" UserVerificationMethod
 
+instance Aeson.FromJSON UserVerificationMethod where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "USER_VERIFY_"
+
+instance Aeson.ToJSON UserVerificationMethod where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "USER_VERIFY_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#key-protection-types)
 data KeyProtectionType
   = KEY_PROTECTION_SOFTWARE
@@ -46,16 +50,26 @@
   | KEY_PROTECTION_SECURE_ELEMENT
   | KEY_PROTECTION_REMOTE_HANDLE
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "KEY_PROTECTION_" KeyProtectionType
 
+instance Aeson.FromJSON KeyProtectionType where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "KEY_PROTECTION_"
+
+instance Aeson.ToJSON KeyProtectionType where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "KEY_PROTECTION_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#matcher-protection-types)
 data MatcherProtectionType
   = MATCHER_PROTECTION_SOFTWARE
   | MATCHER_PROTECTION_TEE
   | MATCHER_PROTECTION_ON_CHIP
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "MATCHER_PROTECTION_" MatcherProtectionType
 
+instance Aeson.FromJSON MatcherProtectionType where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "MATCHER_PROTECTION_"
+
+instance Aeson.ToJSON MatcherProtectionType where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "MATCHER_PROTECTION_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authenticator-attachment-hints)
 data AuthenticatorAttachmentHint
   = ATTACHMENT_HINT_INTERNAL
@@ -68,8 +82,13 @@
   | ATTACHMENT_HINT_READY
   | ATTACHMENT_HINT_WIFI_DIRECT
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "ATTACHMENT_HINT_" AuthenticatorAttachmentHint
 
+instance Aeson.FromJSON AuthenticatorAttachmentHint where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "ATTACHMENT_HINT_"
+
+instance Aeson.ToJSON AuthenticatorAttachmentHint where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "ATTACHMENT_HINT_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#transaction-confirmation-display-types)
 data TransactionConfirmationDisplayType
   = TRANSACTION_CONFIRMATION_DISPLAY_ANY
@@ -78,8 +97,13 @@
   | TRANSACTION_CONFIRMATION_DISPLAY_HARDWARE
   | TRANSACTION_CONFIRMATION_DISPLAY_REMOTE
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "TRANSACTION_CONFIRMATION_DISPLAY_" TransactionConfirmationDisplayType
 
+instance Aeson.FromJSON TransactionConfirmationDisplayType where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "TRANSACTION_CONFIRMATION_DISPLAY_"
+
+instance Aeson.ToJSON TransactionConfirmationDisplayType where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "TRANSACTION_CONFIRMATION_DISPLAY_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authentication-algorithms)
 data AuthenticationAlgorithm
   = ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW
@@ -101,8 +125,13 @@
   | ALG_SIGN_SECP512R1_ECDSA_SHA512_RAW
   | ALG_SIGN_ED25519_EDDSA_SHA512_RAW
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "ALG_SIGN_" AuthenticationAlgorithm
 
+instance Aeson.FromJSON AuthenticationAlgorithm where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "ALG_SIGN_"
+
+instance Aeson.ToJSON AuthenticationAlgorithm where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "ALG_SIGN_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#public-key-representation-formats)
 data PublicKeyRepresentationFormat
   = ALG_KEY_ECC_X962_RAW
@@ -111,8 +140,13 @@
   | ALG_KEY_RSA_2048_DER
   | ALG_KEY_COSE
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "ALG_KEY_" PublicKeyRepresentationFormat
 
+instance Aeson.FromJSON PublicKeyRepresentationFormat where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "ALG_KEY_"
+
+instance Aeson.ToJSON PublicKeyRepresentationFormat where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "ALG_KEY_"
+
 -- | [(spec)](https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#authenticator-attestation-types)
 data AuthenticatorAttestationType
   = ATTESTATION_BASIC_FULL
@@ -120,4 +154,9 @@
   | ATTESTATION_ECDAA
   | ATTESTATION_ATTCA
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "ATTESTATION_" AuthenticatorAttestationType
+
+instance Aeson.FromJSON AuthenticatorAttestationType where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "ATTESTATION_"
+
+instance Aeson.ToJSON AuthenticatorAttestationType where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "ATTESTATION_"
diff --git a/src/Crypto/WebAuthn/Metadata/Service/Processing.hs b/src/Crypto/WebAuthn/Metadata/Service/Processing.hs
--- a/src/Crypto/WebAuthn/Metadata/Service/Processing.hs
+++ b/src/Crypto/WebAuthn/Metadata/Service/Processing.hs
@@ -45,6 +45,7 @@
 import Crypto.WebAuthn.Internal.DateOrphans ()
 import Crypto.WebAuthn.Metadata.Service.Decode (decodeMetadataPayload)
 import qualified Crypto.WebAuthn.Metadata.Service.Types as Service
+import qualified Crypto.WebAuthn.Metadata.Service.WebIDL as ServiceIDL
 import qualified Crypto.WebAuthn.Model as M
 import Crypto.WebAuthn.Model.Identifier
   ( AAGUID,
@@ -56,22 +57,23 @@
       ),
     SubjectKeyIdentifier,
   )
-import Data.Aeson (Value (Object))
-import qualified Data.Aeson as Aeson
+import Data.Aeson (Value)
+import qualified Data.Aeson.Types as Aeson
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as LBS
+import Data.Either (partitionEithers)
 import Data.FileEmbed (embedFile)
-import Data.HashMap.Strict (HashMap)
+import Data.HashMap.Strict (HashMap, (!?))
 import qualified Data.HashMap.Strict as HashMap
 import Data.Hourglass (DateTime)
 import qualified Data.List.NonEmpty as NE
-import Data.Maybe (mapMaybe)
-import Data.Singletons (SingI, sing)
+import Data.Singletons (singByProxy)
 import Data.Text (Text)
 import qualified Data.Text as Text
 import qualified Data.X509 as X509
 import qualified Data.X509.CertificateStore as X509
 import qualified Data.X509.Validation as X509
+import GHC.Exts (fromList, toList)
 
 -- | A root certificate along with the host it should be verified against
 data RootCertificate = RootCertificate
@@ -179,7 +181,7 @@
 jwtToJson blob rootCert now = runExcept $ do
   jwt <- decodeCompact $ LBS.fromStrict blob
   claims <- runReaderT (verifyClaims (defaultJWTValidationSettings (const True)) rootCert jwt) now
-  return $ claims ^. unregisteredClaims
+  return . fromList . toList $ claims ^. unregisteredClaims
 
 -- | Decodes a FIDO Metadata payload JSON value to a 'Service.MetadataPayload',
 -- returning an error when the JSON is invalid, and ignoring any entries not
@@ -188,12 +190,23 @@
 -- and `Crypto.WebAuthn.Metadata.Service.Types.mpEntries` fields are most
 -- important.
 jsonToPayload :: HashMap Text Value -> Either Text Service.MetadataPayload
-jsonToPayload value = case Aeson.fromJSON $ Object value of
-  Aeson.Error err -> Left $ Text.pack err
-  Aeson.Success payload -> case decodeMetadataPayload payload of
+jsonToPayload value = case Aeson.parseEither metadataPayloadParser value of
+  Left err -> Left $ Text.pack err
+  Right payload -> case decodeMetadataPayload payload of
     Left err -> Left err
     Right result -> pure result
 
+metadataPayloadParser :: HashMap Text Aeson.Value -> Aeson.Parser ServiceIDL.MetadataBLOBPayload
+metadataPayloadParser hm = case (hm !? "legalHeader", hm !? "no", hm !? "nextUpdate", hm !? "entries") of
+  (Just legalHeader, Just no, Just nextUpdate, Just entries) -> do
+    legalHeader <- Aeson.parseJSON legalHeader
+    no <- Aeson.parseJSON no
+    nextUpdate <- Aeson.parseJSON nextUpdate
+    entries <- Aeson.parseJSON entries
+    pure $
+      ServiceIDL.MetadataBLOBPayload {..}
+  _ -> fail "Could not decode MetadataBLOB: missing fields"
+
 -- | Creates a 'Service.MetadataServiceRegistry' from a list of
 -- 'Service.SomeMetadataEntry', which can either be obtained from a
 -- 'Service.MetadataPayload's 'Service.mpEntries' field, or be constructed
@@ -204,33 +217,14 @@
 createMetadataRegistry :: [Service.SomeMetadataEntry] -> Service.MetadataServiceRegistry
 createMetadataRegistry entries = Service.MetadataServiceRegistry {..}
   where
-    fido2Entries = HashMap.fromList $ mapMaybe getFido2Pairs entries
-    fidoU2FEntries = HashMap.fromList $ mapMaybe getFidoU2FPairs entries
-
-    getFido2Pairs (Service.SomeMetadataEntry ident entry) = getFido2Pairs' ident entry
-    getFidoU2FPairs (Service.SomeMetadataEntry ident entry) = getFidoU2FPairs' ident entry
-
-    getFido2Pairs' ::
-      forall p.
-      SingI p =>
-      AuthenticatorIdentifier p ->
-      Service.MetadataEntry p ->
-      Maybe (AAGUID, Service.MetadataEntry 'M.Fido2)
-    getFido2Pairs' ident entry = case sing @p of
-      M.SFido2 ->
-        Just (idAaguid ident, entry)
-      _ -> Nothing
+    fido2Entries = HashMap.fromList fido2Pairs
+    fidoU2FEntries = HashMap.fromList fidoU2FPairs
+    (fido2Pairs, fidoU2FPairs) = partitionEithers $ map fromSomeMetadataEntry entries
 
-    getFidoU2FPairs' ::
-      forall p.
-      SingI p =>
-      AuthenticatorIdentifier p ->
-      Service.MetadataEntry p ->
-      Maybe (SubjectKeyIdentifier, Service.MetadataEntry 'M.FidoU2F)
-    getFidoU2FPairs' ident entry = case sing @p of
-      M.SFidoU2F ->
-        Just (idSubjectKeyIdentifier ident, entry)
-      _ -> Nothing
+    fromSomeMetadataEntry :: Service.SomeMetadataEntry -> Either (AAGUID, Service.MetadataEntry 'M.Fido2) (SubjectKeyIdentifier, Service.MetadataEntry 'M.FidoU2F)
+    fromSomeMetadataEntry (Service.SomeMetadataEntry ident entry) = case singByProxy ident of
+      M.SFido2 -> Left (idAaguid ident, entry)
+      M.SFidoU2F -> Right (idSubjectKeyIdentifier ident, entry)
 
 -- | Query a 'Service.MetadataEntry' for an 'M.AuthenticatorIdentifier'
 queryMetadata ::
diff --git a/src/Crypto/WebAuthn/Metadata/Service/WebIDL.hs b/src/Crypto/WebAuthn/Metadata/Service/WebIDL.hs
--- a/src/Crypto/WebAuthn/Metadata/Service/WebIDL.hs
+++ b/src/Crypto/WebAuthn/Metadata/Service/WebIDL.hs
@@ -17,7 +17,7 @@
 -- Unless otherwise specified, if a WebIDL dictionary member is DOMString, it MUST NOT be empty.
 -- Unless otherwise specified, if a WebIDL dictionary member is a List, it MUST NOT be an empty list.
 
-import Crypto.WebAuthn.Internal.Utils (CustomJSON (CustomJSON), JSONEncoding)
+import Crypto.WebAuthn.Internal.Utils (jsonEncodingOptions)
 import Crypto.WebAuthn.Metadata.Statement.WebIDL (AAGUID, MetadataStatement)
 import qualified Crypto.WebAuthn.Metadata.UAF as UAF
 import qualified Crypto.WebAuthn.WebIDL as IDL
@@ -46,8 +46,13 @@
     -- entryRogueListHash :: IDL.DOMString
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding MetadataBLOBPayloadEntry
 
+instance Aeson.FromJSON MetadataBLOBPayloadEntry where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON MetadataBLOBPayloadEntry where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#biometricstatusreport-dictionary)
 data BiometricStatusReport = BiometricStatusReport
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-biometricstatusreport-certlevel)
@@ -66,8 +71,13 @@
     certificationRequirementsVersion :: Maybe IDL.DOMString
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding BiometricStatusReport
 
+instance Aeson.FromJSON BiometricStatusReport where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON BiometricStatusReport where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#statusreport-dictionary)
 data StatusReport = StatusReport
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-statusreport-status)
@@ -90,8 +100,13 @@
     certificationRequirementsVersion :: Maybe IDL.DOMString
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding StatusReport
 
+instance Aeson.FromJSON StatusReport where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON StatusReport where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#authenticatorstatus-enum)
 data AuthenticatorStatus
   = NOT_FIDO_CERTIFIED
@@ -110,8 +125,13 @@
   | FIDO_CERTIFIED_L3
   | FIDO_CERTIFIED_L3plus
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding AuthenticatorStatus
 
+instance Aeson.FromJSON AuthenticatorStatus where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON AuthenticatorStatus where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-dictionary)
 data MetadataBLOBPayload = MetadataBLOBPayload
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#dom-metadatablobpayload-legalheader)
@@ -124,4 +144,9 @@
     entries :: [MetadataBLOBPayloadEntry]
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding MetadataBLOBPayload
+
+instance Aeson.FromJSON MetadataBLOBPayload where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON MetadataBLOBPayload where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
diff --git a/src/Crypto/WebAuthn/Metadata/Statement/WebIDL.hs b/src/Crypto/WebAuthn/Metadata/Statement/WebIDL.hs
--- a/src/Crypto/WebAuthn/Metadata/Statement/WebIDL.hs
+++ b/src/Crypto/WebAuthn/Metadata/Statement/WebIDL.hs
@@ -26,7 +26,7 @@
   )
 where
 
-import Crypto.WebAuthn.Internal.Utils (CustomJSON (CustomJSON), EnumJSONEncoding, JSONEncoding)
+import Crypto.WebAuthn.Internal.Utils (enumJSONEncodingOptions, jsonEncodingOptions)
 import qualified Crypto.WebAuthn.Metadata.FidoRegistry as Registry
 import qualified Crypto.WebAuthn.Metadata.UAF as UAF
 import qualified Crypto.WebAuthn.WebIDL as IDL
@@ -53,8 +53,13 @@
     blockSlowdown :: Maybe IDL.UnsignedShort
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding CodeAccuracyDescriptor
 
+instance Aeson.FromJSON CodeAccuracyDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON CodeAccuracyDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#biometricaccuracydescriptor-dictionary)
 data BiometricAccuracyDescriptor = BiometricAccuracyDescriptor
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-biometricaccuracydescriptor-selfattestedfrr)
@@ -69,8 +74,13 @@
     blockSlowdown :: Maybe IDL.UnsignedShort
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding BiometricAccuracyDescriptor
 
+instance Aeson.FromJSON BiometricAccuracyDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON BiometricAccuracyDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#patternaccuracydescriptor-dictionary)
 data PatternAccuracyDescriptor = PatternAccuracyDescriptor
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-patternaccuracydescriptor-mincomplexity)
@@ -84,8 +94,13 @@
     blockSlowdown :: Maybe IDL.UnsignedShort
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PatternAccuracyDescriptor
 
+instance Aeson.FromJSON PatternAccuracyDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PatternAccuracyDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#verificationmethoddescriptor-dictionary)
 data VerificationMethodDescriptor = VerificationMethodDescriptor
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-verificationmethoddescriptor-userverificationmethod)
@@ -98,8 +113,13 @@
     paDesc :: Maybe PatternAccuracyDescriptor
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding VerificationMethodDescriptor
 
+instance Aeson.FromJSON VerificationMethodDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON VerificationMethodDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#verificationmethodandcombinations-typedef)
 newtype VerificationMethodANDCombinations = VerificationMethodANDCombinations (NonEmpty VerificationMethodDescriptor)
   deriving (Show, Eq)
@@ -115,8 +135,13 @@
     b :: IDL.UnsignedShort
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding RgbPaletteEntry
 
+instance Aeson.FromJSON RgbPaletteEntry where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON RgbPaletteEntry where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#displaypngcharacteristicsdescriptor-dictionary)
 data DisplayPNGCharacteristicsDescriptor = DisplayPNGCharacteristicsDescriptor
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-displaypngcharacteristicsdescriptor-width)
@@ -137,8 +162,13 @@
     plte :: Maybe (NonEmpty RgbPaletteEntry)
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding DisplayPNGCharacteristicsDescriptor
 
+instance Aeson.FromJSON DisplayPNGCharacteristicsDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON DisplayPNGCharacteristicsDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#ecdaatrustanchor-dictionary)
 data EcdaaTrustAnchor = EcdaaTrustAnchor
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-ecdaatrustanchor-x)
@@ -155,8 +185,13 @@
     litG1Curve :: IDL.DOMString
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding EcdaaTrustAnchor
 
+instance Aeson.FromJSON EcdaaTrustAnchor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON EcdaaTrustAnchor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#extensiondescriptor-dictionary)
 data ExtensionDescriptor = ExtensionDescriptor
   { -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#dom-extensiondescriptor-id)
@@ -169,8 +204,13 @@
     fail_if_unknown :: IDL.Boolean
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding ExtensionDescriptor
 
+instance Aeson.FromJSON ExtensionDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON ExtensionDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#alternativedescriptions-dictionary)
 -- TODO: Replace Text with
 -- <https://hackage.haskell.org/package/aeson-2.0.2.0/docs/Data-Aeson-Key.html#t:Key>
@@ -253,12 +293,22 @@
     authenticatorGetInfo :: Maybe AuthenticatorGetInfo
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding MetadataStatement
 
+instance Aeson.FromJSON MetadataStatement where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON MetadataStatement where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | Possible FIDO protocol families for 'protocolFamily'
 data ProtocolFamily
   = ProtocolFamilyUAF
   | ProtocolFamilyU2F
   | ProtocolFamilyFIDO2
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via EnumJSONEncoding "ProtocolFamily" ProtocolFamily
+
+instance Aeson.FromJSON ProtocolFamily where
+  parseJSON = Aeson.genericParseJSON $ enumJSONEncodingOptions "ProtocolFamily"
+
+instance Aeson.ToJSON ProtocolFamily where
+  toJSON = Aeson.genericToJSON $ enumJSONEncodingOptions "ProtocolFamily"
diff --git a/src/Crypto/WebAuthn/Metadata/UAF.hs b/src/Crypto/WebAuthn/Metadata/UAF.hs
--- a/src/Crypto/WebAuthn/Metadata/UAF.hs
+++ b/src/Crypto/WebAuthn/Metadata/UAF.hs
@@ -8,11 +8,10 @@
   )
 where
 
-import Crypto.WebAuthn.Internal.Utils (JSONEncoding)
+import Crypto.WebAuthn.Internal.Utils (jsonEncodingOptions)
 import qualified Crypto.WebAuthn.WebIDL as IDL
 import qualified Data.Aeson as Aeson
 import Data.Text (Text)
-import qualified Deriving.Aeson as Aeson
 import GHC.Generics (Generic)
 
 -- | [(spec)](https://fidoalliance.org/specs/fido-uaf-v1.2-ps-20201020/fido-uaf-protocol-v1.2-ps-20201020.html#authenticator-attestation-id-aaid-typedef)
@@ -35,4 +34,9 @@
     minor :: IDL.UnsignedShort
   }
   deriving (Show, Eq, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding Version
+
+instance Aeson.FromJSON Version where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON Version where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
diff --git a/src/Crypto/WebAuthn/Model/Types.hs b/src/Crypto/WebAuthn/Model/Types.hs
--- a/src/Crypto/WebAuthn/Model/Types.hs
+++ b/src/Crypto/WebAuthn/Model/Types.hs
@@ -760,6 +760,33 @@
 -- and
 -- [requesting](https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options).
 -- The CeremonyKind araument specifies which.
+--
+-- Values of this type are send to the client to
+-- [create](https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create)
+-- and
+-- [get](https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-get)
+-- a credential. After they have been sent, they have to be stored awaiting the
+-- response from the client for further validation. At least the following
+-- fields have to be stored, the others are not currently used.
+--
+--  For `Crypto.WebAuthn.Operation.Registration.verifyRegistrationResponse`:
+--
+-- - `corChallenge`
+-- - `ascUserVerification` of `corAuthenticatorSelection`
+-- - `corPubKeyCredParams`
+-- - `corUser` (of which `cueId` is used in the
+--   `Crypto.WebAuthn.Operation.Registration.verifyRegistrationResponse`, and it
+--   and the other fields are need to be stored permanently by the relying party
+--   as the user entity).
+--
+--  For `Crypto.WebAuthn.Operation.Authentication.verifyAuthenticationResponse`:
+--
+-- - `coaChallenge`
+-- - `coaUserVerification`
+-- - `coaAllowCredentials`
+--
+-- Depending on implementation choices by the RP, some of these fields might
+-- additionally be constants, and could thus also be omitted when storing.
 data CredentialOptions (c :: CeremonyKind) where
   -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions)
   CredentialOptionsRegistration ::
@@ -846,6 +873,7 @@
       -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialrequestoptions-extensions)
       -- This OPTIONAL member contains additional parameters requesting additional processing by the client and authenticator.
       -- For example, if transaction confirmation is sought from the user, then the prompt string might be included as an extension.
+      -- TODO: Extensions are not implemented by this library, see "Crypto.WebAuthn.Model#extensions".
       coaExtensions :: Maybe AuthenticationExtensionsClientInputs
     } ->
     CredentialOptions 'Authentication
@@ -910,7 +938,7 @@
   }
   deriving (Eq, Show)
 
-instance SingI c => ToJSON (CollectedClientData c raw) where
+instance SingI c => ToJSON (CollectedClientData (c :: CeremonyKind) raw) where
   toJSON CollectedClientData {..} =
     object
       [ "webauthnKind" .= sing @c,
@@ -1114,7 +1142,14 @@
 
 -- | Attempt to find the desired attestation statement format in a map of
 -- supported formats. Can then be used to perform attestation.
-lookupAttestationStatementFormat :: Text -> SupportedAttestationStatementFormats -> Maybe SomeAttestationStatementFormat
+lookupAttestationStatementFormat ::
+  -- | The desired format, e.g. "android-safetynet" or "none"
+  Text ->
+  -- | The [attestation statement formats](https://www.w3.org/TR/webauthn-2/#sctn-attestation-formats)
+  -- that should be supported. The value of 'Crypto.WebAuthn.allSupportedFormats'
+  -- can be passed here, but additional or custom formats may also be used if needed.
+  SupportedAttestationStatementFormats ->
+  Maybe SomeAttestationStatementFormat
 lookupAttestationStatementFormat id (SupportedAttestationStatementFormats sasf) = sasf !? id
 
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#attestation-object)
diff --git a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Decoding.hs b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Decoding.hs
--- a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Decoding.hs
+++ b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Decoding.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE ScopedTypeVariables #-}
@@ -23,7 +24,7 @@
 import Control.Monad.Except (MonadError (throwError))
 import Control.Monad.State (MonadState (get, put), StateT (runStateT))
 import qualified Crypto.Hash as Hash
-import Crypto.WebAuthn.Internal.Utils (CustomJSON (CustomJSON), JSONEncoding)
+import Crypto.WebAuthn.Internal.Utils (jsonEncodingOptions)
 import Crypto.WebAuthn.Model.Identifier (AAGUID (AAGUID))
 import qualified Crypto.WebAuthn.Model.Kinds as K
 import qualified Crypto.WebAuthn.Model.Types as M
@@ -216,14 +217,16 @@
     -- tokenBinding :: Maybe TokenBinding
   }
   deriving (Generic)
-  -- Note: Encoding should NOT be derived via aeson. See the Encoding module instead
-  deriving (Aeson.FromJSON) via JSONEncoding ClientDataJSON
 
+-- Note: Encoding should NOT be derived via aeson. See the Encoding module instead
+instance Aeson.FromJSON ClientDataJSON where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
 -- | Decodes a 'M.CollectedClientData' from a 'BS.ByteString'. This is needed
 -- to parse the [clientDataJSON](https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson)
 -- field in the [AuthenticatorResponse](https://www.w3.org/TR/webauthn-2/#iface-authenticatorresponse)
 -- structure, which is used for both attestation and assertion
-decodeCollectedClientData :: forall c. SingI c => BS.ByteString -> Either Text (M.CollectedClientData c 'True)
+decodeCollectedClientData :: forall (c :: K.CeremonyKind). SingI c => BS.ByteString -> Either Text (M.CollectedClientData c 'True)
 decodeCollectedClientData bytes = do
   -- https://www.w3.org/TR/webauthn-2/#collectedclientdata-json-compatible-serialization-of-client-data
   ClientDataJSON {..} <-
diff --git a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Encoding.hs b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Encoding.hs
--- a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Encoding.hs
+++ b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Binary/Encoding.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 
@@ -42,7 +43,7 @@
 -- | Encodes all raw fields of a 'M.Credential'. This function is
 -- mainly useful for testing that the encoding/decoding functions are correct.
 -- The counterpart to this function is 'Crypto.WebAuthn.Model.Binary.Decoding.stripRawCredential'
-encodeRawCredential :: forall c raw. SingI c => M.Credential c raw -> M.Credential c 'True
+encodeRawCredential :: forall (c :: K.CeremonyKind) raw. SingI c => M.Credential c raw -> M.Credential c 'True
 encodeRawCredential M.Credential {..} =
   M.Credential
     { cResponse = case sing @c of
@@ -132,7 +133,7 @@
         credentialLength :: Word16
         credentialLength = fromIntegral $ BS.length $ M.unCredentialId acdCredentialId
 
-    encodeRawAttestedCredentialData :: forall c raw. SingI c => M.AttestedCredentialData c raw -> M.AttestedCredentialData c 'True
+    encodeRawAttestedCredentialData :: forall (c :: K.CeremonyKind) raw. SingI c => M.AttestedCredentialData c raw -> M.AttestedCredentialData c 'True
     encodeRawAttestedCredentialData = case sing @c of
       K.SRegistration -> \M.AttestedCredentialData {..} ->
         M.AttestedCredentialData
@@ -195,5 +196,5 @@
 
 -- | Encodes an 'M.CollectedClientData' as a 'BS.ByteString'. This is needed by
 -- the client side to generate a valid JSON response
-encodeCollectedClientData :: forall c. SingI c => M.CollectedClientData c 'True -> BS.ByteString
+encodeCollectedClientData :: forall (c :: K.CeremonyKind). SingI c => M.CollectedClientData c 'True -> BS.ByteString
 encodeCollectedClientData M.CollectedClientData {..} = M.unRaw ccdRawData
diff --git a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Decoding.hs b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Decoding.hs
--- a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Decoding.hs
+++ b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Decoding.hs
@@ -2,6 +2,7 @@
 {-# LANGUAGE DefaultSignatures #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE RecordWildCards #-}
 
 -- | Stability: internal
@@ -56,7 +57,7 @@
   -- module documentation of `Crypto.WebAuthn.Model` for more information.
   decode _ = pure M.AuthenticationExtensionsClientOutputs {}
 
-instance SingI c => Decode (M.CollectedClientData c 'True) where
+instance SingI c => Decode (M.CollectedClientData (c :: K.CeremonyKind) 'True) where
   decode (IDL.URLEncodedBase64 bytes) = B.decodeCollectedClientData bytes
 
 instance Decode (M.AuthenticatorData 'K.Authentication 'True) where
diff --git a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Encoding.hs b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Encoding.hs
--- a/src/Crypto/WebAuthn/Model/WebIDL/Internal/Encoding.hs
+++ b/src/Crypto/WebAuthn/Model/WebIDL/Internal/Encoding.hs
@@ -185,7 +185,7 @@
       }
 
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson)
-instance SingI c => Encode (M.CollectedClientData c 'True) where
+instance SingI c => Encode (M.CollectedClientData (c :: K.CeremonyKind) 'True) where
   encode ccd = IDL.URLEncodedBase64 $ B.encodeCollectedClientData ccd
 
 instance Encode (M.AuthenticatorResponse 'K.Authentication 'True) where
diff --git a/src/Crypto/WebAuthn/Model/WebIDL/Types.hs b/src/Crypto/WebAuthn/Model/WebIDL/Types.hs
--- a/src/Crypto/WebAuthn/Model/WebIDL/Types.hs
+++ b/src/Crypto/WebAuthn/Model/WebIDL/Types.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE StandaloneDeriving #-}
 
 -- | Stability: experimental
 -- This module models direct representations of JavaScript objects interacting with the
@@ -42,7 +41,7 @@
   )
 where
 
-import Crypto.WebAuthn.Internal.Utils (CustomJSON (CustomJSON), JSONEncoding)
+import Crypto.WebAuthn.Internal.Utils (jsonEncodingOptions)
 import qualified Crypto.WebAuthn.WebIDL as IDL
 import qualified Data.Aeson as Aeson
 import Data.Map (Map)
@@ -71,8 +70,13 @@
     extensions :: Maybe (Map Text Aeson.Value)
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PublicKeyCredentialCreationOptions
 
+instance Aeson.FromJSON PublicKeyCredentialCreationOptions where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PublicKeyCredentialCreationOptions where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dictionary-rp-credential-params)
 data PublicKeyCredentialRpEntity = PublicKeyCredentialRpEntity
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialrpentity-id)
@@ -81,8 +85,13 @@
     name :: IDL.DOMString
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PublicKeyCredentialRpEntity
 
+instance Aeson.FromJSON PublicKeyCredentialRpEntity where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PublicKeyCredentialRpEntity where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dictionary-user-credential-params)
 data PublicKeyCredentialUserEntity = PublicKeyCredentialUserEntity
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialuserentity-id)
@@ -93,8 +102,13 @@
     name :: IDL.DOMString
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PublicKeyCredentialUserEntity
 
+instance Aeson.FromJSON PublicKeyCredentialUserEntity where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PublicKeyCredentialUserEntity where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dictionary-credential-params)
 data PublicKeyCredentialParameters = PublicKeyCredentialParameters
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialparameters-type)
@@ -103,8 +117,13 @@
     alg :: COSEAlgorithmIdentifier
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PublicKeyCredentialParameters
 
+instance Aeson.FromJSON PublicKeyCredentialParameters where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PublicKeyCredentialParameters where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier)
 type COSEAlgorithmIdentifier = IDL.Long
 
@@ -118,8 +137,13 @@
     transports :: Maybe [IDL.DOMString]
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PublicKeyCredentialDescriptor
 
+instance Aeson.FromJSON PublicKeyCredentialDescriptor where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PublicKeyCredentialDescriptor where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dictdef-authenticatorselectioncriteria)
 data AuthenticatorSelectionCriteria = AuthenticatorSelectionCriteria
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-authenticatorselectioncriteria-authenticatorattachment)
@@ -132,8 +156,13 @@
     userVerification :: Maybe IDL.DOMString
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding AuthenticatorSelectionCriteria
 
+instance Aeson.FromJSON AuthenticatorSelectionCriteria where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON AuthenticatorSelectionCriteria where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options)
 data PublicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialrequestoptions-challenge)
@@ -150,8 +179,13 @@
     extensions :: Maybe (Map Text Aeson.Value)
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding PublicKeyCredentialRequestOptions
 
+instance Aeson.FromJSON PublicKeyCredentialRequestOptions where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON PublicKeyCredentialRequestOptions where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#iface-pkcredential)
 data PublicKeyCredential response = PublicKeyCredential
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-publickeycredential-identifier-slot)
@@ -163,17 +197,11 @@
   }
   deriving (Eq, Show, Generic)
 
-deriving via
-  JSONEncoding (PublicKeyCredential response)
-  instance
-    Aeson.FromJSON response =>
-    Aeson.FromJSON (PublicKeyCredential response)
+instance Aeson.FromJSON response => Aeson.FromJSON (PublicKeyCredential response) where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
 
-deriving via
-  JSONEncoding (PublicKeyCredential response)
-  instance
-    Aeson.ToJSON response =>
-    Aeson.ToJSON (PublicKeyCredential response)
+instance Aeson.ToJSON response => Aeson.ToJSON (PublicKeyCredential response) where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
 
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#iface-authenticatorattestationresponse)
 data AuthenticatorAttestationResponse = AuthenticatorAttestationResponse
@@ -187,8 +215,13 @@
     transports :: Maybe [IDL.DOMString]
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding AuthenticatorAttestationResponse
 
+instance Aeson.FromJSON AuthenticatorAttestationResponse where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON AuthenticatorAttestationResponse where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
+
 -- | [(spec)](https://www.w3.org/TR/webauthn-2/#iface-authenticatorassertionresponse)
 data AuthenticatorAssertionResponse = AuthenticatorAssertionResponse
   { -- | [(spec)](https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson)
@@ -201,4 +234,9 @@
     userHandle :: Maybe IDL.ArrayBuffer
   }
   deriving (Eq, Show, Generic)
-  deriving (Aeson.FromJSON, Aeson.ToJSON) via JSONEncoding AuthenticatorAssertionResponse
+
+instance Aeson.FromJSON AuthenticatorAssertionResponse where
+  parseJSON = Aeson.genericParseJSON jsonEncodingOptions
+
+instance Aeson.ToJSON AuthenticatorAssertionResponse where
+  toJSON = Aeson.genericToJSON jsonEncodingOptions
diff --git a/tests/MetadataSpec.hs b/tests/MetadataSpec.hs
--- a/tests/MetadataSpec.hs
+++ b/tests/MetadataSpec.hs
@@ -4,18 +4,19 @@
 module MetadataSpec (spec) where
 
 import Crypto.WebAuthn.Metadata.Service.Processing (RootCertificate (RootCertificate), fidoAllianceRootCertificate, jsonToPayload, jwtToJson)
-import Crypto.WebAuthn.Metadata.Service.WebIDL (MetadataBLOBPayload)
-import Data.Aeson (Result (Success), ToJSON (toJSON), Value (Object), decodeFileStrict, fromJSON)
+import Crypto.WebAuthn.Metadata.Service.WebIDL (MetadataBLOBPayload, entries, legalHeader, nextUpdate, no)
+import Data.Aeson (Result (Success), ToJSON (toJSON), decodeFileStrict, fromJSON)
 import Data.Aeson.Types (Result (Error))
 import qualified Data.ByteString as BS
 import Data.Either (isRight)
+import Data.HashMap.Strict ((!), (!?))
 import qualified Data.PEM as PEM
 import qualified Data.Text as Text
 import Data.Text.Encoding (decodeUtf8)
 import qualified Data.X509 as X509
 import qualified Data.X509.CertificateStore as X509
 import System.Hourglass (dateCurrent)
-import Test.Hspec (SpecWith, describe, it, shouldSatisfy)
+import Test.Hspec (SpecWith, describe, it, shouldBe, shouldSatisfy)
 import Test.Hspec.Expectations.Json (shouldBeUnorderedJson)
 
 golden :: FilePath -> SpecWith ()
@@ -34,7 +35,10 @@
 
     Just expectedPayload <- decodeFileStrict $ "tests/golden-metadata/" <> subdir <> "/payload.json"
 
-    Object result `shouldBeUnorderedJson` expectedPayload
+    (result !? "legalHeader") `shouldBe` toJSON <$> legalHeader expectedPayload
+    (result !? "no") `shouldBe` Just (toJSON (no expectedPayload))
+    (result !? "nextUpdate") `shouldBe` Just (toJSON (nextUpdate expectedPayload))
+    (result ! "entries") `shouldBeUnorderedJson` toJSON (entries expectedPayload)
 
   it "can decode and reencode the payload to the partially parsed JSON" $ do
     Just payload <- decodeFileStrict $ "tests/golden-metadata/" <> subdir <> "/payload.json"
diff --git a/webauthn.cabal b/webauthn.cabal
--- a/webauthn.cabal
+++ b/webauthn.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.4
 name: webauthn
-version: 0.1.1.0
+version: 0.2.0.0
 license: Apache-2.0
 license-file: LICENSE
 copyright:
@@ -17,11 +17,19 @@
 bug-reports: https://github.com/tweag/webauthn/issues
 synopsis: Relying party (server) implementation of the WebAuthn 2 specification
 description:
-  Implements the [Relying Party conformance class](https://www.w3.org/TR/webauthn-2/#sctn-conforming-relying-parties)
+  == About
+  This library implements the [Relying Party conformance class](https://www.w3.org/TR/webauthn-2/#sctn-conforming-relying-parties)
   of the [Web Authentication Level 2](https://www.w3.org/TR/webauthn-2/) specification.
   This allows web applications to create strong, attested, scoped, public key-based
   credentials for the purpose of strongly authenticating users.
   .
+  == Getting started
+  The "Crypto.WebAuthn" module and its documentation is the best place to get
+  started with the library.
+  The example server: [Main.hs](https://github.com/tweag/webauthn/blob/master/server/src/Main.hs)
+  shows how this module may be used to implement a relying party.
+  .
+  == Stability
   While the general design of the library won't change, it's still in an alpha
   state, so smaller breaking changes should be expected for now. We will
   however follow the [PVP](https://pvp.haskell.org/) and properly label changes
@@ -66,9 +74,7 @@
   hs-source-dirs: src
   build-depends:
     base                  >= 4.14.3 && < 4.15,
-    -- deriving-aeson has compilation performance problems with aeson >= 2.0,
-    -- see https://github.com/fumieval/deriving-aeson/issues/16
-    aeson                 >= 1.4.7.1 && < 2.0,
+    aeson                 >= 1.4.7.1 && < 2.1,
     asn1-encoding         >= 0.9.6 && < 0.10,
     asn1-parse            >= 0.9.5 && < 0.10,
     asn1-types            >= 0.3.4 && < 0.4,
@@ -79,11 +85,10 @@
     cborg                 >= 0.2.6 && < 0.3,
     containers            >= 0.6.5 && < 0.7,
     cryptonite            >= 0.29 && < 0.30,
-    deriving-aeson        >= 0.2.8 && < 0.3,
     file-embed            >= 0.0.15 && < 0.1,
-    hashable              >= 1.3.0 && < 1.4,
+    hashable              >= 1.3.0 && < 1.5,
     hourglass             >= 0.2.12 && < 0.3,
-    jose                  >= 0.8.5 && < 0.9,
+    jose                  >= 0.8.5 && < 0.10,
     lens                  >= 4.19.2 && < 4.20,
     memory                >= 0.15.0 && < 0.16,
     monad-time            >= 0.3.1 && < 0.4,
@@ -181,6 +186,7 @@
     serialise,
     singletons,
     text,
+    unordered-containers,
     uuid,
     validation,
     webauthn,
