diff --git a/Codec/Encryption/OpenPGP/Arbitrary.hs b/Codec/Encryption/OpenPGP/Arbitrary.hs
--- a/Codec/Encryption/OpenPGP/Arbitrary.hs
+++ b/Codec/Encryption/OpenPGP/Arbitrary.hs
@@ -266,7 +266,7 @@
 
 --
 instance Arbitrary PubKeyAlgorithm where
-    arbitrary = elements [RSA, DSA, ECDH, ECDSA, DH, EdDSA]
+    arbitrary = elements [RSA, DSA, ECDH, ECDSA, DH, EdDSALegacy]
 
 instance Arbitrary EightOctetKeyId where
     arbitrary = fmap (EightOctetKeyId . BL.pack) (vector 8)
diff --git a/Codec/Encryption/OpenPGP/CFB.hs b/Codec/Encryption/OpenPGP/CFB.hs
--- a/Codec/Encryption/OpenPGP/CFB.hs
+++ b/Codec/Encryption/OpenPGP/CFB.hs
@@ -3,213 +3,181 @@
 -- Copyright © 2013  Daniel Kahn Gillmor
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
-
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE KindSignatures #-}
 
 module Codec.Encryption.OpenPGP.CFB
-  ( decrypt
-  , decryptPreservingNonce
-  , decryptNoNonce
-  , decryptOpenPGPCfb
-  , decryptOpenPGPCfbWithNonce
-  , OpenPGPCFBMode(..)
-  , OpenPGPCFBModeW(..)
-  , encryptNoNonce
-  , encryptOpenPGPCfbRaw
-  , mdcTrailerForSEIPDv1
-  , seipdv1NonceFromIV
-  , validateSEIPD1MDC
-  , calculateMDC
-  ) where
+    ( decrypt
+    , decryptPreservingNonce
+    , decryptNoNonce
+    , decryptOpenPGPCfb
+    , decryptOpenPGPCfbWithNonce
+    , OpenPGPCFBMode (..)
+    , OpenPGPCFBModeW (..)
+    , encryptNoNonce
+    , encryptOpenPGPCfbRaw
+    ) where
 
-import Codec.Encryption.OpenPGP.BlockCipher (CipherError(..), withSymmetricCipher)
+import qualified Data.ByteString as B
+
+import Codec.Encryption.OpenPGP.BlockCipher
+    ( CipherError (..)
+    , withSymmetricCipher
+    )
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
 import Codec.Encryption.OpenPGP.Types
-import qualified Crypto.Hash as CH
-import qualified Crypto.Hash.Algorithms as CHA
-import qualified Data.ByteArray as BA
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Lazy as BL
-import Control.Monad (when)
 
 data OpenPGPCFBMode
-  = OpenPGPCFBResync
-  | OpenPGPCFBNoResync
-  deriving (Eq, Show)
+    = OpenPGPCFBResync
+    | OpenPGPCFBNoResync
+    deriving (Eq, Show)
 
 data OpenPGPCFBModeW (mode :: OpenPGPCFBMode) where
-  OpenPGPCFBResyncW :: OpenPGPCFBModeW 'OpenPGPCFBResync
-  OpenPGPCFBNoResyncW :: OpenPGPCFBModeW 'OpenPGPCFBNoResync
+    OpenPGPCFBResyncW :: OpenPGPCFBModeW 'OpenPGPCFBResync
+    OpenPGPCFBNoResyncW :: OpenPGPCFBModeW 'OpenPGPCFBNoResync
 
-decryptOpenPGPCfb ::
-     SymmetricAlgorithm
-  -> B.ByteString
-  -> B.ByteString
-  -> Either CipherError B.ByteString
+decryptOpenPGPCfb
+    :: SymmetricAlgorithm
+    -> B.ByteString
+    -> B.ByteString
+    -> Either CipherError B.ByteString
 decryptOpenPGPCfb sa ciphertext keydata =
-  snd <$> decryptOpenPGPCfbWithNonce sa ciphertext keydata
+    snd <$> decryptOpenPGPCfbWithNonce sa ciphertext keydata
 
-decryptOpenPGPCfbWithNonce ::
-     SymmetricAlgorithm
-  -> B.ByteString
-  -> B.ByteString
-  -> Either CipherError (B.ByteString, B.ByteString)
+decryptOpenPGPCfbWithNonce
+    :: SymmetricAlgorithm
+    -> B.ByteString
+    -> B.ByteString
+    -> Either CipherError (B.ByteString, B.ByteString)
 decryptOpenPGPCfbWithNonce Plaintext ciphertext _ = return (mempty, ciphertext)
 decryptOpenPGPCfbWithNonce sa ciphertext keydata =
-  withSymmetricCipher sa keydata $ \bc -> do
-    nonce <- decrypt1 ciphertext bc
-    cleartext <- decrypt2 ciphertext bc
-    if nonceCheck bc nonce
-      then return (nonce, cleartext)
-      else Left "Session key quickcheck failed"
+    withSymmetricCipher sa keydata $ \bc -> do
+        nonce <- decrypt1 ciphertext bc
+        cleartext <- decrypt2 ciphertext bc
+        if nonceCheck bc nonce
+            then return (nonce, cleartext)
+            else Left "Session key quickcheck failed"
   where
-    decrypt1 ::
-         HOBlockCipher cipher
-      => B.ByteString
-      -> cipher
-      -> Either String B.ByteString
+    decrypt1
+        :: HOBlockCipher cipher
+        => B.ByteString
+        -> cipher
+        -> Either String B.ByteString
     decrypt1 ct cipher =
-      paddedCfbDecrypt
-        cipher
-        (B.replicate (blockSize cipher) 0)
-        (B.take (blockSize cipher + 2) ct)
-    decrypt2 ::
-         HOBlockCipher cipher
-      => B.ByteString
-      -> cipher
-      -> Either String B.ByteString
+        paddedCfbDecrypt
+            cipher
+            (B.replicate (blockSize cipher) 0)
+            (B.take (blockSize cipher + 2) ct)
+    decrypt2
+        :: HOBlockCipher cipher
+        => B.ByteString
+        -> cipher
+        -> Either String B.ByteString
     decrypt2 ct cipher =
-      let i = B.take (blockSize cipher) (B.drop 2 ct)
-       in paddedCfbDecrypt cipher i (B.drop (blockSize cipher + 2) ct)
+        let i = B.take (blockSize cipher) (B.drop 2 ct)
+         in paddedCfbDecrypt cipher i (B.drop (blockSize cipher + 2) ct)
 
 -- should deprecate this?
-decrypt ::
-     SymmetricAlgorithm
-  -> B.ByteString
-  -> B.ByteString
-  -> Either CipherError B.ByteString
+decrypt
+    :: SymmetricAlgorithm
+    -> B.ByteString
+    -> B.ByteString
+    -> Either CipherError B.ByteString
 decrypt x y z = snd <$> (decryptPreservingNonce x y z)
 
-decryptPreservingNonce ::
-     SymmetricAlgorithm
-  -> B.ByteString
-  -> B.ByteString
-  -> Either CipherError (B.ByteString, B.ByteString)
+decryptPreservingNonce
+    :: SymmetricAlgorithm
+    -> B.ByteString
+    -> B.ByteString
+    -> Either CipherError (B.ByteString, B.ByteString)
 decryptPreservingNonce Plaintext ciphertext _ = return (mempty, ciphertext)
 decryptPreservingNonce sa ciphertext keydata =
-  withSymmetricCipher sa keydata $ \bc -> do
-    let bs = blockSize bc
-    decrypted <- paddedCfbDecrypt bc (B.replicate bs 0) ciphertext
-    let (nonce, cleartext) = B.splitAt (bs + 2) decrypted
-    if nonceCheck bc nonce
-      then return (nonce, cleartext)
-      else Left "Session key quickcheck failed"
+    withSymmetricCipher sa keydata $ \bc -> do
+        let bs = blockSize bc
+        decrypted <- paddedCfbDecrypt bc (B.replicate bs 0) ciphertext
+        let (nonce, cleartext) = B.splitAt (bs + 2) decrypted
+        if nonceCheck bc nonce
+            then return (nonce, cleartext)
+            else Left "Session key quickcheck failed"
 
-decryptNoNonce ::
-     SymmetricAlgorithm
-  -> IV
-  -> B.ByteString
-  -> B.ByteString
-  -> Either CipherError B.ByteString
+decryptNoNonce
+    :: SymmetricAlgorithm
+    -> IV
+    -> B.ByteString
+    -> B.ByteString
+    -> Either CipherError B.ByteString
 decryptNoNonce Plaintext _ ciphertext _ = return ciphertext
 decryptNoNonce sa iv ciphertext keydata =
-  withSymmetricCipher sa keydata (decrypt' ciphertext)
+    withSymmetricCipher sa keydata (decrypt' ciphertext)
   where
-    decrypt' ::
-         HOBlockCipher cipher
-      => B.ByteString
-      -> cipher
-      -> Either String B.ByteString
+    decrypt'
+        :: HOBlockCipher cipher
+        => B.ByteString
+        -> cipher
+        -> Either String B.ByteString
     decrypt' ct cipher = paddedCfbDecrypt cipher (unIV iv) ct
 
-nonceCheck :: HOBlockCipher cipher => cipher -> B.ByteString -> Bool
+nonceCheck
+    :: HOBlockCipher cipher => cipher -> B.ByteString -> Bool
 nonceCheck bc =
-  (==) <$> B.take 2 . B.drop (blockSize bc - 2) <*> B.drop (blockSize bc)
+    (==)
+        <$> B.take 2 . B.drop (blockSize bc - 2)
+        <*> B.drop (blockSize bc)
 
-encryptNoNonce ::
-     SymmetricAlgorithm
-  -> S2K
-  -> IV
-  -> B.ByteString
-  -> B.ByteString
-  -> Either CipherError B.ByteString
+encryptNoNonce
+    :: SymmetricAlgorithm
+    -> S2K
+    -> IV
+    -> B.ByteString
+    -> B.ByteString
+    -> Either CipherError B.ByteString
 encryptNoNonce Plaintext _ _ payload _ = return payload
-encryptNoNonce sa s2k iv payload keydata =
-  withSymmetricCipher sa keydata (encrypt' payload)
+encryptNoNonce sa _s2k iv payload keydata =
+    withSymmetricCipher sa keydata (encrypt' payload)
   where
-    encrypt' ::
-         HOBlockCipher cipher
-      => B.ByteString
-      -> cipher
-      -> Either String B.ByteString
+    encrypt'
+        :: HOBlockCipher cipher
+        => B.ByteString
+        -> cipher
+        -> Either String B.ByteString
     encrypt' ct cipher = paddedCfbEncrypt cipher (unIV iv) ct
 
-encryptOpenPGPCfbRaw ::
-    OpenPGPCFBModeW mode
-  -> SymmetricAlgorithm
-  -> IV
-  -> B.ByteString  -- ^ plaintext (with MDC trailer already appended)
-  -> B.ByteString  -- ^ raw session key bytes
-  -> Either CipherError B.ByteString
+encryptOpenPGPCfbRaw
+    :: OpenPGPCFBModeW mode
+    -> SymmetricAlgorithm
+    -> IV
+    -> B.ByteString
+    -- ^ plaintext (with MDC trailer already appended)
+    -> B.ByteString
+    -- ^ raw session key bytes
+    -> Either CipherError B.ByteString
 encryptOpenPGPCfbRaw _ Plaintext _ cleartext _ = Right cleartext
 encryptOpenPGPCfbRaw mode sa iv cleartext keydata =
-  withSymmetricCipher sa keydata $ \cipher -> do
-    let initialVector = unIV iv
-        bs            = blockSize cipher
-    if B.length initialVector /= bs
-      then Left
-             ("IV length mismatch for " ++
-              show sa ++
-              ": expected " ++ show bs ++ ", got " ++ show (B.length initialVector))
-      else do
-        let prefix = initialVector <> B.drop (bs - 2) initialVector
-        case mode of
-          OpenPGPCFBResyncW -> do
-            nonceAndCheck <- paddedCfbEncrypt cipher (B.replicate bs 0) prefix
-            encryptedPayload <-
-              paddedCfbEncrypt cipher (B.take bs (B.drop 2 nonceAndCheck)) cleartext
-            return (nonceAndCheck <> encryptedPayload)
-          OpenPGPCFBNoResyncW ->
-            paddedCfbEncrypt cipher (B.replicate bs 0) (prefix <> cleartext)
-
--- | Compute the MDC trailer appended to SEIPDv1 plaintext before encryption.
--- The trailer is: @0xd3 0x14 SHA1(nonce || plaintext || 0xd3 0x14)@.
-mdcTrailerForSEIPDv1 :: IV -> B.ByteString -> B.ByteString
-mdcTrailerForSEIPDv1 iv plaintext = mdcHeader <> digest
-  where
-    mdcHeader = B.pack [0xd3, 0x14]
-    nonce     = seipdv1NonceFromIV iv
-    digest    = BA.convert (CH.hash (nonce <> plaintext <> mdcHeader) :: CH.Digest CHA.SHA1)
-
--- | The SEIPDv1 nonce: the IV bytes followed by its last two bytes (resync prefix).
-seipdv1NonceFromIV :: IV -> B.ByteString
-seipdv1NonceFromIV (IV ivBytes) = ivBytes <> B.drop (B.length ivBytes - 2) ivBytes
-
-calculateMDC :: B.ByteString -> B.ByteString -> Maybe BL.ByteString
-calculateMDC nonce garbage
-  | B.length garbage < 23 = Nothing
-  | otherwise =
-    let digest = CH.hash (nonce <> B.take (B.length garbage - 22) garbage <> B.pack [211, 20]) :: CH.Digest CHA.SHA1
-     in Just (BL.fromStrict (BA.convert digest :: B.ByteString))
-
--- | Verify the MDC trailer of a decrypted SEIPDv1 payload.
--- Takes the CFB nonce (blockSize+2 prefix bytes retained from decryption)
--- and the full decrypted bytes (payload + MDC packet), and returns the
--- payload without the MDC trailer on success.
-validateSEIPD1MDC :: B.ByteString -> B.ByteString -> Either String B.ByteString
-validateSEIPD1MDC nonce decrypted = do
-  when (B.length decrypted < 22) $
-    Left "SEIPD1 cleartext too short to contain MDC trailer"
-  let (payload, trailer) = B.splitAt (B.length decrypted - 22) decrypted
-  when (B.take 2 trailer /= B.pack [211, 20]) $
-    Left "SEIPD1 cleartext missing MDC packet trailer (tag 19)"
-  expectedMdc <-
-    case calculateMDC nonce decrypted of
-     Nothing -> Left "SEIPD1 cleartext too short for MDC calculation"
-     Just x -> Right x
-  let actualMdc = BL.fromStrict (B.drop 2 trailer)
-  when (expectedMdc /= actualMdc) $
-    Left "MDC indicates tampering"
-  Right payload
+    withSymmetricCipher sa keydata $ \cipher -> do
+        let initialVector = unIV iv
+            bs = blockSize cipher
+        if B.length initialVector /= bs
+            then
+                Left
+                    ( "IV length mismatch for "
+                        ++ show sa
+                        ++ ": expected "
+                        ++ show bs
+                        ++ ", got "
+                        ++ show (B.length initialVector)
+                    )
+            else do
+                let prefix = initialVector <> B.drop (bs - 2) initialVector
+                case mode of
+                    OpenPGPCFBResyncW -> do
+                        nonceAndCheck <-
+                            paddedCfbEncrypt cipher (B.replicate bs 0) prefix
+                        encryptedPayload <-
+                            paddedCfbEncrypt
+                                cipher
+                                (B.take bs (B.drop 2 nonceAndCheck))
+                                cleartext
+                        return (nonceAndCheck <> encryptedPayload)
+                    OpenPGPCFBNoResyncW ->
+                        paddedCfbEncrypt cipher (B.replicate bs 0) (prefix <> cleartext)
diff --git a/Codec/Encryption/OpenPGP/Encrypt.hs b/Codec/Encryption/OpenPGP/Encrypt.hs
--- a/Codec/Encryption/OpenPGP/Encrypt.hs
+++ b/Codec/Encryption/OpenPGP/Encrypt.hs
@@ -13,6 +13,7 @@
 
 module Codec.Encryption.OpenPGP.Encrypt
     ( PKESKEncryptError (..)
+    , renderPKESKEncryptError
     , RecipientCapabilityNegotiationMode (..)
     , RecipientCapabilityError (..)
     , renderRecipientCapabilityError
@@ -23,13 +24,9 @@
     , RecipientEncryptionTargetRejected (..)
     , RecipientEncryptionTargetsReport (..)
     , recipientEncryptionTargetsReportFromTKAtTimestamp
-    , recipientEncryptionTargetsReportFromTK
-    , recipientEncryptionTargetFromTKAtTimestamp
     , recipientEncryptionTargetFromTKAtTimestampWithPolicy
     , recipientEncryptionTargetsFromTKAtTimestamp
-    , recipientEncryptionTargetFromTK
     , recipientEncryptionTargetFromTKWithPolicy
-    , recipientEncryptionTargetsFromTK
     , RecipientTargetSelectionPolicy (..)
     , PassphraseSKESKVersionPolicy (..)
     , PassphraseEncryptRequest (..)
@@ -45,10 +42,8 @@
     , SomeEncryptCompatibilityProfileW (..)
     , RecipientEncryptionTarget (..)
     , recipientEncryptionTarget
-    , recipientEncryptionTargetWithStrategy
     , recipientEncryptionTargetWithCapabilities
     , recipientEncryptionTargetWithStrategyTyped
-    , recipientVersionStrategyForProfile
     , recipientVersionStrategyForProfileTyped
     , RecipientPayloadShape (..)
     , defaultRecipientPayloadShape
@@ -77,7 +72,6 @@
     , buildPKESKv3PktForRecipient
     , buildPKESKPayloadForRecipient
     , buildPKESKPktForRecipient
-    , buildPKESKPktsForRecipientTargetsWithSelector
     , buildPKESKPktsForRecipientTargetsWithSelectorTyped
     , encryptSEIPDv2Payload
     , encryptSEIPDv1Payload
@@ -85,15 +79,25 @@
     , encryptSEIPDv2WithSKESKBlock
     , encryptSEIPDv2LiteralDataWithSKESK
     , composeMessageWithSEIPDv2
+    , buildOnePassSignature
+    , OPSBuildError (..)
+    , renderOPSBuildError
+    , NestedFlag
+    , aesKeyWrapRFC3394
+    , deriveX25519Kek
+    , deriveX448Kek
     ) where
 
 import Control.Applicative ((<|>))
+import Control.Error.Util (note)
 import Control.Lens (ix, (.~))
 import Control.Monad (when)
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
 import qualified Crypto.Error as CE
-import qualified Crypto.Hash.Algorithms as CHAlg
+import qualified Crypto.Hash.Algorithms as CHA
 import Crypto.KDF.HKDF (expand, extract)
-import Crypto.Number.Serialize (i2osp, os2ip)
+import Crypto.Number.Serialize (os2ip)
 import qualified Crypto.PubKey.Curve25519 as C25519
 import qualified Crypto.PubKey.Curve448 as C448
 import qualified Crypto.PubKey.ECC.DH as ECCDH
@@ -103,25 +107,25 @@
 import Crypto.Random.Types (MonadRandom, getRandomBytes)
 import Data.Bifunctor (first)
 import Data.Binary (put)
-import Data.Binary.Put (putWord64be, runPut)
-import Data.Bits (shiftL, shiftR, xor, (.&.))
+import Data.Binary.Put (runPut)
+import Data.Bits (shiftL)
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as BL
 import Data.Containers.ListUtils (nubOrd)
 import Data.Int (Int64)
-import Data.List (find, foldl', maximumBy)
+import Data.List (find, maximumBy)
 import Data.List.NonEmpty (NonEmpty (..))
-import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
+import Data.Maybe (fromMaybe, listToMaybe)
 import Data.Ord (comparing)
 import qualified Data.Set as Set
 import Data.Time.Clock (UTCTime)
 import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
-import Data.Word (Word16, Word64, Word8)
+import Data.Word (Word64, Word8)
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
 import Codec.Encryption.OpenPGP.BlockCipher
-    ( CipherError
+    ( CipherError (..)
     , keySize
     , renderCipherError
     , withSymmetricCipher
@@ -129,7 +133,6 @@
 import Codec.Encryption.OpenPGP.CFB
     ( OpenPGPCFBModeW (..)
     , encryptOpenPGPCfbRaw
-    , mdcTrailerForSEIPDv1
     )
 import Codec.Encryption.OpenPGP.Expirations
     ( effectiveKeyPreferencesAtTimestamp
@@ -142,7 +145,15 @@
     ( eightOctetKeyID
     , fingerprint
     )
-import Codec.Encryption.OpenPGP.Internal (leftPadTo, point2MBS)
+import Codec.Encryption.OpenPGP.Internal
+    ( checksum16Bytes
+    , chunksOf8
+    , edPointBytes
+    , encodeWord64be
+    , leftPadTo
+    , point2MBS
+    , xorBS
+    )
 import Codec.Encryption.OpenPGP.Internal.CryptoAES
     ( withAESCipher
     )
@@ -151,12 +162,6 @@
     , deriveECDHKek
     , normalizeMontgomeryPublic
     )
-import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2
-    ( aeadModeAndNonceSizeForSEIPDv2
-    , deriveSKESK6KEK
-    , encryptSKESK6SessionKey
-    , seipdv2SymmetricKeySize
-    )
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
     ( HOBlockCipher (..)
     )
@@ -171,7 +176,6 @@
     ( MessageEncryptionPolicy
     , OpenPGPRFC (..)
     , PKESKVersionPolicy (..)
-    , defaultPKESKVersionPolicy
     , messageDefaultAEADAlgorithm
     , messageDefaultChunkSize
     , messageDefaultSymmetricAlgorithm
@@ -180,7 +184,18 @@
     , policyForRFC
     , policyMessageEncryption
     )
-import Codec.Encryption.OpenPGP.S2K (renderS2KError, string2Key)
+import Codec.Encryption.OpenPGP.S2K
+    ( string2Key
+    )
+import Codec.Encryption.OpenPGP.SEIPDv1 (mdcTrailerForSEIPDv1)
+import Codec.Encryption.OpenPGP.SEIPDv2
+    ( SEIPDv2Failure (..)
+    , aeadModeAndNonceSizeForSEIPDv2
+    , deriveSKESK6KEK
+    , encryptSKESK6SessionKey
+    , renderSEIPDv2Failure
+    , seipdv2SymmetricKeySize
+    )
 import Codec.Encryption.OpenPGP.Serialize ()
 import Codec.Encryption.OpenPGP.SignatureQualities
     ( sigCT
@@ -218,8 +233,10 @@
     | InvalidRecipientKeyMaterial PubKeyAlgorithm String
     | RecipientKdfFailure PubKeyAlgorithm String
     | RecipientKeyWrapFailure PubKeyAlgorithm String
+    | RecipientKeyWrapFailureCipher PubKeyAlgorithm CipherError
     | RecipientCapabilitySelectionFailure RecipientCapabilityError
     | PayloadBuildFailure String
+    | PayloadBuildFailureCipher CipherError
     | NoRecipientsProvided
     deriving (Eq, Show)
 
@@ -255,10 +272,17 @@
         ++ show algo
         ++ ": "
         ++ reason
+renderPKESKEncryptError (RecipientKeyWrapFailureCipher algo err) =
+    "key wrap failure for recipient algorithm "
+        ++ show algo
+        ++ ": "
+        ++ renderCipherError err
 renderPKESKEncryptError (RecipientCapabilitySelectionFailure err) =
     renderRecipientCapabilityError err
 renderPKESKEncryptError (PayloadBuildFailure reason) =
     "payload build failure: " ++ reason
+renderPKESKEncryptError (PayloadBuildFailureCipher err) =
+    "payload build failure: " ++ renderCipherError err
 renderPKESKEncryptError NoRecipientsProvided =
     "no recipients provided"
 
@@ -433,20 +457,6 @@
     recipientCapabilityAdvertisesSEIPDv1Support caps
         && Set.member FeatureSEIPDv2 (recipientCapabilityFeatures caps)
 
-{-# DEPRECATED
-    recipientEncryptionTargetFromTKAtTimestamp
-    "Use recipientEncryptionTargetFromTKAtTimestampWithPolicy instead"
-    #-}
-recipientEncryptionTargetFromTKAtTimestamp
-    :: ThirtyTwoBitTimeStamp
-    -> TK 'PublicTK
-    -> Either RecipientCapabilityError RecipientEncryptionTarget
-recipientEncryptionTargetFromTKAtTimestamp timestamp tk =
-    recipientEncryptionTargetFromTKAtTimestampWithPolicy
-        RecipientTargetSelectionFirstValid
-        timestamp
-        tk
-
 data RecipientTargetSelectionPolicy
     = RecipientTargetSelectionFirstValid
     | RecipientTargetSelectionPreferPrimary
@@ -460,27 +470,14 @@
     -> TK 'PublicTK
     -> Either RecipientCapabilityError RecipientEncryptionTarget
 recipientEncryptionTargetFromTKAtTimestampWithPolicy policy timestamp tk =
-    case chooseRecipientTarget policy tk acceptedTargets of
-        Just target -> Right target
-        Nothing -> Left RecipientCapabilityNoEncryptableKeyMaterialInTK
+    note
+        RecipientCapabilityNoEncryptableKeyMaterialInTK
+        (chooseRecipientTarget policy tk acceptedTargets)
   where
     acceptedTargets =
         recipientEncryptionTargetsAccepted
             (recipientEncryptionTargetsReportFromTKAtTimestamp timestamp tk)
 
-{-# DEPRECATED
-    recipientEncryptionTargetFromTK
-    "Use recipientEncryptionTargetFromTKWithPolicy instead"
-    #-}
-recipientEncryptionTargetFromTK
-    :: TK 'PublicTK
-    -> Either RecipientCapabilityError RecipientEncryptionTarget
-recipientEncryptionTargetFromTK tk =
-    recipientEncryptionTargetFromTKAtTimestampWithPolicy
-        RecipientTargetSelectionFirstValid
-        (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))
-        tk
-
 recipientEncryptionTargetFromTKWithPolicy
     :: RecipientTargetSelectionPolicy
     -> TK 'PublicTK
@@ -569,24 +566,6 @@
                                                 : recipientEncryptionTargetsRejected report
                                         }
 
-{-# DEPRECATED
-    recipientEncryptionTargetsFromTK
-    "Use recipientEncryptionTargetsFromTKAtTimestamp instead"
-    #-}
-recipientEncryptionTargetsFromTK
-    :: TK 'PublicTK -> [RecipientEncryptionTarget]
-recipientEncryptionTargetsFromTK tk =
-    recipientEncryptionTargetsFromTKAtTimestamp
-        (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))
-        tk
-
-recipientEncryptionTargetsReportFromTK
-    :: TK 'PublicTK -> RecipientEncryptionTargetsReport
-recipientEncryptionTargetsReportFromTK tk =
-    recipientEncryptionTargetsReportFromTKAtTimestamp
-        (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))
-        tk
-
 subkeyBindingCapabilityPayloads
     :: ThirtyTwoBitTimeStamp
     -> TK 'PublicTK
@@ -892,13 +871,6 @@
     deriving (Eq, Show)
 
 type family
-    PayloadVersionForProfile (profile :: EncryptCompatibilityProfile)
-        :: SEIPDVersion
-    where
-    PayloadVersionForProfile 'EncryptStrictDefault = 'SEIPDv2
-    PayloadVersionForProfile 'EncryptInteropLegacy = 'SEIPDv1
-
-type family
     ProfileForPayloadVersion (version :: SEIPDVersion)
         :: EncryptCompatibilityProfile
     where
@@ -927,17 +899,6 @@
 recipientEncryptionTarget recipient =
     RecipientEncryptionTarget recipient Nothing Nothing
 
-{-# DEPRECATED
-    recipientEncryptionTargetWithStrategy
-    "Use recipientEncryptionTargetWithStrategyTyped instead"
-    #-}
-recipientEncryptionTargetWithStrategy
-    :: SomePKPayload
-    -> RecipientPKESKVersionStrategy
-    -> RecipientEncryptionTarget
-recipientEncryptionTargetWithStrategy recipient strategy =
-    RecipientEncryptionTarget recipient (Just strategy) Nothing
-
 recipientEncryptionTargetWithCapabilities
     :: SomePKPayload
     -> RecipientCapabilities
@@ -950,23 +911,10 @@
     -> RecipientPKESKVersionStrategyW strategy
     -> RecipientEncryptionTarget
 recipientEncryptionTargetWithStrategyTyped recipient strategyW =
-    recipientEncryptionTargetWithStrategy
+    RecipientEncryptionTarget
         recipient
-        (demoteRecipientStrategy strategyW)
-
-{-# DEPRECATED
-    recipientVersionStrategyForProfile
-    "Use recipientVersionStrategyForProfileTyped instead"
-    #-}
-recipientVersionStrategyForProfile
-    :: EncryptCompatibilityProfile
-    -> RecipientEncryptionTarget
-    -> Either PKESKEncryptError RecipientPKESKVersionStrategy
-recipientVersionStrategyForProfile profile target =
-    case promoteEncryptCompatibilityProfile profile of
-        SomeEncryptCompatibilityProfileW profileW ->
-            demoteSomeRecipientStrategy
-                <$> recipientVersionStrategyForProfileTyped profileW target
+        (Just (demoteRecipientStrategy strategyW))
+        Nothing
 
 recipientVersionStrategyForProfileTyped
     :: EncryptCompatibilityProfileW profile
@@ -1036,7 +984,7 @@
 encryptPassphraseWithPolicy
     :: MonadRandom m
     => PassphraseEncryptRequest
-    -> m (Either String [Pkt])
+    -> m (Either SEIPDv2Failure [Pkt])
 encryptPassphraseWithPolicy request =
     case passphraseEncryptVersionPolicy request of
         PassphraseSKESKForceV4Interop ->
@@ -1272,27 +1220,6 @@
         (fmap PKESKPkt)
         (buildPKESKv3PayloadForRecipient recipient material)
 
--- | Build PKESK packets for all recipients with a single shared session key.
-{-# DEPRECATED
-    buildPKESKPktsForRecipientTargetsWithSelector
-    "Use buildPKESKPktsForRecipientTargetsWithSelectorTyped instead"
-    #-}
-buildPKESKPktsForRecipientTargetsWithSelector
-    :: MonadRandom m
-    => ( RecipientEncryptionTarget
-         -> Either PKESKEncryptError RecipientPKESKVersionStrategy
-       )
-    -> [RecipientEncryptionTarget]
-    -> PKESKSessionMaterial
-    -> m (Either PKESKEncryptError [Pkt])
-buildPKESKPktsForRecipientTargetsWithSelector selector targets material =
-    buildPKESKPktsForRecipientTargetsWithSelectorTyped
-        ( \target ->
-            promoteRecipientStrategy <$> selector target
-        )
-        targets
-        material
-
 buildPKESKPktsForRecipientTargetsWithSelectorTyped
     :: MonadRandom m
     => ( RecipientEncryptionTarget
@@ -1363,9 +1290,13 @@
     -> RecipientPKESKRequestPayload strategy
     -> m (Either PKESKEncryptError Pkt)
 buildPKESKPktForRecipientWithPreparedPayload strategy recipient payload =
-    fmap fmapPKESKPkt payloadResult
+    fmap (fmap PKESKPkt) payloadResult
   where
-    fmapPKESKPkt = fmap PKESKPkt
+    packetizeV6
+        :: Functor m
+        => m (Either PKESKEncryptError PKESKPayloadV6)
+        -> m (Either PKESKEncryptError PKESKPayload)
+    packetizeV6 = fmap (fmap PKESKPayloadV6Packet)
     payloadResult =
         case (strategy, payload) of
             (RecipientForceV3InteropW, RecipientForceV3Payload v3Material) ->
@@ -1374,22 +1305,10 @@
                 , RecipientPreferV6Payload material v6RawMaterial
                 ) ->
                     case _pkalgo recipient of
-                        RSA ->
-                            fmap
-                                (fmap PKESKPayloadV6Packet)
-                                (buildRsaPKESKv6 recipient material)
-                        ECDH ->
-                            fmap
-                                (fmap PKESKPayloadV6Packet)
-                                (buildECDHPKESKv6 recipient material)
-                        X25519 ->
-                            fmap
-                                (fmap PKESKPayloadV6Packet)
-                                (buildX25519PKESKv6 recipient v6RawMaterial)
-                        X448 ->
-                            fmap
-                                (fmap PKESKPayloadV6Packet)
-                                (buildX448PKESKv6 recipient v6RawMaterial)
+                        RSA -> packetizeV6 (buildRsaPKESKv6 recipient material)
+                        ECDH -> packetizeV6 (buildECDHPKESKv6 recipient material)
+                        X25519 -> packetizeV6 (buildX25519PKESKv6 recipient v6RawMaterial)
+                        X448 -> packetizeV6 (buildX448PKESKv6 recipient v6RawMaterial)
                         pka ->
                             pure (Left (UnsupportedRecipientAlgorithm pka))
 
@@ -1433,98 +1352,27 @@
     -> m (Either PKESKEncryptError RecipientEncryptResult)
 encryptForRecipientsWithCapabilityNegotiation negotiationMode request
     | null targets = pure (Left NoRecipientsProvided)
-    | otherwise =
-        case selectSymmetricAlgorithm
-            negotiationMode
+    | otherwise = runExceptT $ do
+        symalgo <-
+            ExceptT . pure $
+                selectSymmetricAlgorithm
+                    negotiationMode
+                    request
+                    messagePolicy
+                    targets
+        sessionMaterial <- ExceptT $ generateSessionKeyMaterial symalgo
+        pkeskPkts <-
+            ExceptT $
+                buildPKESKPktsForRecipientTargetsWithSelectorTyped
+                    (recipientVersionStrategyForProfileTyped profileW)
+                    targets
+                    sessionMaterial
+        buildEncryptedPayload
             request
-            messagePolicy
-            targets of
-            Left err -> pure (Left err)
-            Right symalgo -> do
-                sessionMaterialResult <- generateSessionKeyMaterial symalgo
-                case sessionMaterialResult of
-                    Left err -> pure (Left err)
-                    Right sessionMaterial -> do
-                        pkeskResult <-
-                            buildPKESKPktsForRecipientTargetsWithSelectorTyped
-                                (recipientVersionStrategyForProfileTyped profileW)
-                                targets
-                                sessionMaterial
-                        case pkeskResult of
-                            Left err -> pure (Left err)
-                            Right pkeskPkts -> do
-                                payloadResult <- case recipientEncryptRequestOverrides request of
-                                    RecipientEncryptRequestSEIPDv2Overrides
-                                        { recipientEncryptRequestAEADOverride = aeadOverride
-                                        , recipientEncryptRequestChunkSizeOverride = chunkSizeOverride
-                                        , recipientEncryptRequestSaltOverride = saltOverride
-                                        } ->
-                                            case recipientsMissingSEIPDv2Support targets of
-                                                [] -> do
-                                                    case selectAEADAlgorithm
-                                                        negotiationMode
-                                                        messagePolicy
-                                                        targets
-                                                        aeadOverride of
-                                                        Left err -> pure (Left err)
-                                                        Right aead -> do
-                                                            salt <- maybe (Salt <$> getRandomBytes 32) pure saltOverride
-                                                            let chunkSize =
-                                                                    maybe
-                                                                        (messageDefaultChunkSize messagePolicy)
-                                                                        id
-                                                                        chunkSizeOverride
-                                                            pure $
-                                                                buildEncryptedPacketSequenceWithShape
-                                                                    symalgo
-                                                                    aead
-                                                                    chunkSize
-                                                                    (recipientEncryptRequestPayloadShape request)
-                                                                    salt
-                                                                    (pkeskSessionKey sessionMaterial)
-                                                                    pkeskPkts
-                                                                    (recipientEncryptRequestPayload request)
-                                                _missingSEIPDv2 ->
-                                                    case recipientsMissingSEIPDv1Support targets of
-                                                        [] ->
-                                                            buildSEIPDv1PayloadWithIV
-                                                                symalgo
-                                                                sessionMaterial
-                                                                pkeskPkts
-                                                                Nothing
-                                                        missingSEIPDv1 ->
-                                                            pure
-                                                                ( Left
-                                                                    ( RecipientCapabilitySelectionFailure
-                                                                        (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)
-                                                                    )
-                                                                )
-                                    RecipientEncryptRequestSEIPDv1Overrides
-                                        { recipientEncryptRequestIVOverride = ivOverride
-                                        } ->
-                                            case recipientsMissingSEIPDv1Support targets of
-                                                [] ->
-                                                    buildSEIPDv1PayloadWithIV
-                                                        symalgo
-                                                        sessionMaterial
-                                                        pkeskPkts
-                                                        ivOverride
-                                                missingSEIPDv1 ->
-                                                    pure
-                                                        ( Left
-                                                            ( RecipientCapabilitySelectionFailure
-                                                                (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)
-                                                            )
-                                                        )
-                                pure $
-                                    fmap
-                                        ( \pkts ->
-                                            RecipientEncryptResult
-                                                { recipientEncryptPackets = pkts
-                                                , recipientEncryptSessionMaterial = sessionMaterial
-                                                }
-                                        )
-                                        payloadResult
+            targets
+            symalgo
+            sessionMaterial
+            pkeskPkts
   where
     targets = recipientEncryptRequestTargets request
     profileW =
@@ -1536,6 +1384,99 @@
                 policyMessageEncryption (policyForRFC RFC9580)
             EncryptInteropLegacyW ->
                 policyMessageEncryption (policyForRFC RFC4880)
+    buildEncryptedPayload
+        :: MonadRandom m
+        => RecipientEncryptRequest v
+        -> [RecipientEncryptionTarget]
+        -> SymmetricAlgorithm
+        -> PKESKSessionMaterial
+        -> [Pkt]
+        -> ExceptT PKESKEncryptError m RecipientEncryptResult
+    buildEncryptedPayload request targets symalgo sessionMaterial pkeskPkts =
+        case recipientEncryptRequestOverrides request of
+            RecipientEncryptRequestSEIPDv2Overrides
+                { recipientEncryptRequestAEADOverride = aeadOverride
+                , recipientEncryptRequestChunkSizeOverride = chunkSizeOverride
+                , recipientEncryptRequestSaltOverride = saltOverride
+                } ->
+                    case recipientsMissingSEIPDv2Support targets of
+                        [] ->
+                            case selectAEADAlgorithm
+                                negotiationMode
+                                messagePolicy
+                                targets
+                                aeadOverride of
+                                Left err -> ExceptT . pure $ Left err
+                                Right aead -> do
+                                    salt <-
+                                        lift $ maybe (Salt <$> getRandomBytes 32) pure saltOverride
+                                    let chunkSize =
+                                            maybe
+                                                (messageDefaultChunkSize messagePolicy)
+                                                id
+                                                chunkSizeOverride
+                                    ExceptT . pure $
+                                        fmap
+                                            ( \pkts ->
+                                                RecipientEncryptResult
+                                                    { recipientEncryptPackets = pkts
+                                                    , recipientEncryptSessionMaterial = sessionMaterial
+                                                    }
+                                            )
+                                            ( buildEncryptedPacketSequenceWithShape
+                                                symalgo
+                                                aead
+                                                chunkSize
+                                                (recipientEncryptRequestPayloadShape request)
+                                                salt
+                                                (pkeskSessionKey sessionMaterial)
+                                                pkeskPkts
+                                                (recipientEncryptRequestPayload request)
+                                            )
+                        _missingSEIPDv2 ->
+                            case recipientsMissingSEIPDv1Support targets of
+                                [] -> do
+                                    pkts <-
+                                        ExceptT $
+                                            buildSEIPDv1PayloadWithIV
+                                                symalgo
+                                                sessionMaterial
+                                                pkeskPkts
+                                                Nothing
+                                    pure $
+                                        RecipientEncryptResult
+                                            { recipientEncryptPackets = pkts
+                                            , recipientEncryptSessionMaterial = sessionMaterial
+                                            }
+                                missingSEIPDv1 ->
+                                    ExceptT . pure $
+                                        Left
+                                            ( RecipientCapabilitySelectionFailure
+                                                (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)
+                                            )
+            RecipientEncryptRequestSEIPDv1Overrides
+                { recipientEncryptRequestIVOverride = ivOverride
+                } ->
+                    case recipientsMissingSEIPDv1Support targets of
+                        [] -> do
+                            pkts <-
+                                ExceptT $
+                                    buildSEIPDv1PayloadWithIV
+                                        symalgo
+                                        sessionMaterial
+                                        pkeskPkts
+                                        ivOverride
+                            pure $
+                                RecipientEncryptResult
+                                    { recipientEncryptPackets = pkts
+                                    , recipientEncryptSessionMaterial = sessionMaterial
+                                    }
+                        missingSEIPDv1 ->
+                            ExceptT . pure $
+                                Left
+                                    ( RecipientCapabilitySelectionFailure
+                                        (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)
+                                    )
 
     buildSEIPDv1PayloadWithIV
         :: MonadRandom m
@@ -1544,18 +1485,9 @@
         -> [Pkt]
         -> Maybe IV
         -> m (Either PKESKEncryptError [Pkt])
-    buildSEIPDv1PayloadWithIV symalgo sessionMaterial pkeskPkts ivOverride = do
-        ivResult <-
-            case ivOverride of
-                Just iv -> pure (Right iv)
-                Nothing ->
-                    let keyBytes = unSessionKey (pkeskSessionKey sessionMaterial)
-                     in case withSymmetricCipher symalgo keyBytes (\c -> Right (blockSize c)) of
-                            Left err -> pure (Left (PayloadBuildFailure (renderCipherError err)))
-                            Right n -> fmap (Right . IV) (getRandomBytes n)
-        case ivResult of
-            Left err -> pure (Left err)
-            Right iv ->
+    buildSEIPDv1PayloadWithIV symalgo sessionMaterial pkeskPkts ivOverride =
+        case ivOverride of
+            Just iv ->
                 pure $
                     buildEncryptedPacketSequenceWithShapeSEIPDv1
                         symalgo
@@ -1564,6 +1496,20 @@
                         (pkeskSessionKey sessionMaterial)
                         pkeskPkts
                         (recipientEncryptRequestPayload request)
+            Nothing ->
+                let keyBytes = unSessionKey (pkeskSessionKey sessionMaterial)
+                 in case withSymmetricCipher symalgo keyBytes (\c -> Right (blockSize c)) of
+                        Left err -> pure (Left (PayloadBuildFailure (renderCipherError err)))
+                        Right n ->
+                            getRandomBytes n >>= \bytes ->
+                                pure $
+                                    buildEncryptedPacketSequenceWithShapeSEIPDv1
+                                        symalgo
+                                        (IV bytes)
+                                        (recipientEncryptRequestPayloadShape request)
+                                        (pkeskSessionKey sessionMaterial)
+                                        pkeskPkts
+                                        (recipientEncryptRequestPayload request)
 
     recipientsMissingSEIPDv1Support
         :: [RecipientEncryptionTarget] -> [SomePKPayload]
@@ -1644,7 +1590,8 @@
         case recipientEncryptionTargetCapabilities target of
             Just caps ->
                 let preferred = recipientCapabilityPreferredSymmetricAlgorithms caps
-                    allowed = [alg | alg <- policyOrder, alg `elem` preferred]
+                    preferredSet = Set.fromList preferred
+                    allowed = [alg | alg <- policyOrder, alg `Set.member` preferredSet]
                  in if null allowed
                         then policyOrder
                         else allowed
@@ -1670,14 +1617,15 @@
         case recipientEncryptionTargetCapabilities target of
             Just caps ->
                 let preferred = recipientCapabilityPreferredAEADAlgorithms caps
-                    allowed = [alg | alg <- policyOrder, alg `elem` preferred]
+                    preferredSet = Set.fromList preferred
+                    allowed = [alg | alg <- policyOrder, alg `Set.member` preferredSet]
                  in if null allowed
                         then policyOrder
                         else allowed
             Nothing -> policyOrder
 
 chooseCommonAlgorithm
-    :: Eq a
+    :: (Eq a, Ord a)
     => [a]
     -> [[a]]
     -> RecipientCapabilityError
@@ -1687,12 +1635,13 @@
         [] -> Left (RecipientCapabilitySelectionFailure err)
         (firstChoices : restChoices) ->
             let common = foldl' intersectOrdered firstChoices restChoices
-                orderedCommon = [alg | alg <- policyOrder, alg `elem` common]
+                commonSet = Set.fromList common
+                orderedCommon = [alg | alg <- policyOrder, alg `Set.member` commonSet]
              in case orderedCommon of
                     (selected : _) -> Right selected
                     [] -> Left (RecipientCapabilitySelectionFailure err)
   where
-    intersectOrdered as bs = [a | a <- as, a `elem` bs]
+    intersectOrdered as bs = [a | a <- as, a `Set.member` Set.fromList bs]
 
 promoteRecipientStrategy
     :: RecipientPKESKVersionStrategy
@@ -1708,49 +1657,6 @@
 demoteRecipientStrategy RecipientPreferV6W = RecipientPreferV6
 demoteRecipientStrategy RecipientForceV3InteropW = RecipientForceV3Interop
 
-demoteSomeRecipientStrategy
-    :: SomeRecipientPKESKVersionStrategyW
-    -> RecipientPKESKVersionStrategy
-demoteSomeRecipientStrategy (SomeRecipientPKESKVersionStrategyW strategyW) =
-    demoteRecipientStrategy strategyW
-
-promoteEncryptCompatibilityProfile
-    :: EncryptCompatibilityProfile
-    -> SomeEncryptCompatibilityProfileW
-promoteEncryptCompatibilityProfile EncryptStrictDefault =
-    SomeEncryptCompatibilityProfileW EncryptStrictDefaultW
-promoteEncryptCompatibilityProfile EncryptInteropLegacy =
-    SomeEncryptCompatibilityProfileW EncryptInteropLegacyW
-
-{- | High-level encrypt-side helper for public-key recipient encryption.
-
-Returns a complete packet sequence:
-@[PKESK ..., SEIPD2 ...]@.
--}
-buildEncryptedPacketSequence
-    :: SymmetricAlgorithm
-    -> AEADAlgorithm
-    -> Word8
-    -> RecipientPayloadShape
-    -> Salt
-    -> SessionKey
-    -> [Pkt]
-    -> B.ByteString
-    -> Either String [Pkt]
-buildEncryptedPacketSequence symalgo aead chunkSize payloadShape salt sessionKey pkesks payload =
-    first
-        renderPKESKEncryptError
-        ( buildEncryptedPacketSequenceWithShape
-            symalgo
-            aead
-            chunkSize
-            payloadShape
-            salt
-            sessionKey
-            pkesks
-            payload
-        )
-
 buildEncryptedPacketSequenceWithShape
     :: SymmetricAlgorithm
     -> AEADAlgorithm
@@ -1779,7 +1685,7 @@
                     ++ map SignaturePkt signatures
                 )
     ciphertext <-
-        first PayloadBuildFailure $
+        first (PayloadBuildFailure . renderSEIPDv2Failure) $
             encryptSEIPDv2Payload
                 symalgo
                 aead
@@ -1801,18 +1707,15 @@
     -> SessionKey
     -> B.ByteString
     -- ^ inner packet block plaintext
-    -> Either String B.ByteString
+    -> Either CipherError B.ByteString
 encryptSEIPDv1Payload symalgo iv (SessionKey keyBytes) plaintext =
     let cleartextWithMDC = plaintext <> mdcTrailerForSEIPDv1 iv plaintext
-     in first
-            renderCipherError
-            ( encryptOpenPGPCfbRaw
-                OpenPGPCFBNoResyncW
-                symalgo
-                iv
-                cleartextWithMDC
-                keyBytes
-            )
+     in encryptOpenPGPCfbRaw
+            OpenPGPCFBNoResyncW
+            symalgo
+            iv
+            cleartextWithMDC
+            keyBytes
 
 -- | Build a complete RFC 4880-conformant packet sequence using SEIPDv1 (CFB + MDC).
 buildEncryptedPacketSequenceWithShapeSEIPDv1
@@ -1841,7 +1744,7 @@
                     ++ map SignaturePkt signatures
                 )
     ciphertext <-
-        first PayloadBuildFailure $
+        first PayloadBuildFailureCipher $
             encryptSEIPDv1Payload
                 symalgo
                 iv
@@ -2325,7 +2228,7 @@
                 sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
                 kek = deriveX25519Kek ephPublicBytes recipientPublic sharedSecret
             wrapped <-
-                first (RecipientKeyWrapFailure X25519)
+                first (RecipientKeyWrapFailureCipher X25519)
                     . aesKeyWrapRFC3394 AES128 kek
                     $ unPKESKV6RawSessionMaterial material
             esk <- encodeV6X25519Esk ephPublicBytes wrapped
@@ -2358,7 +2261,7 @@
                 sharedSecret = BA.convert (C448.dh recipientPub ephSecret) :: B.ByteString
                 kek = deriveX448Kek ephPublicBytes recipientPublic sharedSecret
             wrapped <-
-                first (RecipientKeyWrapFailure X448)
+                first (RecipientKeyWrapFailureCipher X448)
                     . aesKeyWrapRFC3394 AES256 kek
                     $ unPKESKV6RawSessionMaterial material
             esk <- encodeV6X448Esk ephPublicBytes wrapped
@@ -2387,7 +2290,7 @@
         first (RecipientKdfFailure pka) $
             deriveECDHKek kdfHA kdfSA sharedSecret kdfParam
     wrapped <-
-        first (RecipientKeyWrapFailure pka)
+        first (RecipientKeyWrapFailureCipher pka)
             . aesKeyWrapRFC3394 kdfSA kek
             $ padToMultipleOf8 (pkeskEncodedSessionMaterial material)
     esk <- encodeV6EcdhEsk ephemeralBytes wrapped
@@ -2417,7 +2320,7 @@
         first (RecipientKdfFailure pka) $
             deriveECDHKek kdfHA kdfSA sharedSecret kdfParam
     wrapped <-
-        first (RecipientKeyWrapFailure pka)
+        first (RecipientKeyWrapFailureCipher pka)
             . aesKeyWrapRFC3394 kdfSA kek
             $ padToMultipleOf8 (unPKESKV3SessionMaterial material)
     Right
@@ -2541,25 +2444,21 @@
             56
             "invalid X448 public key length/prefix: "
 
-edPointBytes :: EdPoint -> B.ByteString
-edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x
-edPointBytes (NativeEPoint (EPoint x)) = i2osp x
-
 deriveX25519Kek
     :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString
 deriveX25519Kek ephemeralPublic recipientPublic sharedSecret =
     let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
-        prk = extract @CHAlg.SHA256 B.empty ikm
+        prk = extract @CHA.SHA256 B.empty ikm
         info = "OpenPGP X25519" :: B.ByteString
-     in expand @CHAlg.SHA256 prk info 16
+     in expand @CHA.SHA256 prk info 16
 
 deriveX448Kek
     :: B.ByteString -> B.ByteString -> B.ByteString -> B.ByteString
 deriveX448Kek ephemeralPublic recipientPublic sharedSecret =
     let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
-        prk = extract @CHAlg.SHA512 B.empty ikm
+        prk = extract @CHA.SHA512 B.empty ikm
         info = "OpenPGP X448" :: B.ByteString
-     in expand @CHAlg.SHA512 prk info 32
+     in expand @CHA.SHA512 prk info 32
 
 padToMultipleOf8 :: B.ByteString -> B.ByteString
 padToMultipleOf8 bs
@@ -2569,47 +2468,37 @@
     rem8 = B.length bs `mod` 8
     padLen = if rem8 == 0 then 0 else 8 - rem8
 
-checksum16 :: B.ByteString -> Word16
-checksum16 =
-    fromIntegral
-        . B.foldl'
-            (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))
-            0
-
-checksum16Bytes :: B.ByteString -> B.ByteString
-checksum16Bytes bs =
-    B.pack
-        [ fromIntegral ((chk `shiftR` 8) .&. 0xff)
-        , fromIntegral (chk .&. 0xff)
-        ]
-  where
-    chk = checksum16 bs
-
 aesKeyWrapRFC3394
     :: SymmetricAlgorithm
     -> B.ByteString
     -> B.ByteString
-    -> Either String B.ByteString
+    -> Either CipherError B.ByteString
 aesKeyWrapRFC3394 sa kek plain =
     withAESCipher
-        "ECDH PKESK currently supports AES KEK algorithms only"
+        (\err -> CipherInitFailed sa (show err))
+        (UnsupportedAlgorithm sa)
         sa
         kek
         wrapWithCipher
   where
     wrapWithCipher
-        :: CCT.BlockCipher cipher => cipher -> Either String B.ByteString
+        :: CCT.BlockCipher cipher
+        => cipher -> Either CipherError B.ByteString
     wrapWithCipher cipher = do
         if B.length plain < 16 || B.length plain `mod` 8 /= 0
             then
                 Left
-                    "ECDH key wrap input must be at least 16 octets and a multiple of 8"
+                    ( CipherOperationFailed
+                        "ECDH key wrap input must be at least 16 octets and a multiple of 8"
+                    )
             else Right ()
         let rs = chunksOf8 plain
         if length rs < 2
             then
                 Left
-                    "ECDH key wrap input must contain at least two 64-bit blocks"
+                    ( CipherOperationFailed
+                        "ECDH key wrap input must contain at least two 64-bit blocks"
+                    )
             else Right ()
         (aFinal, rFinal) <- wrapRounds cipher (B.replicate 8 0xA6) rs
         Right (aFinal <> B.concat rFinal)
@@ -2618,7 +2507,7 @@
         => cipher
         -> B.ByteString
         -> [B.ByteString]
-        -> Either String (B.ByteString, [B.ByteString])
+        -> Either CipherError (B.ByteString, [B.ByteString])
     wrapRounds cipher aInit rsInit = goJ 0 aInit rsInit
       where
         n = length rsInit
@@ -2639,16 +2528,6 @@
                         rsNext = (ix (i - 1) .~ lsb) curRs
                     goI (i + 1) aNext rsNext
 
-chunksOf8 :: B.ByteString -> [B.ByteString]
-chunksOf8 bs
-    | B.null bs = []
-    | otherwise =
-        let (h, t) = B.splitAt 8 bs
-         in h : chunksOf8 t
-
-xorBS :: B.ByteString -> B.ByteString -> B.ByteString
-xorBS a b = B.pack (B.zipWith xor a b)
-
 encryptSEIPDv1WithSKESK
     :: MonadRandom m
     => SymmetricAlgorithm
@@ -2656,16 +2535,16 @@
     -> Maybe IV
     -> BL.ByteString
     -> B.ByteString
-    -> m (Either String [Pkt])
+    -> m (Either SEIPDv2Failure [Pkt])
 encryptSEIPDv1WithSKESK symalgo s2k ivOverride passphrase literalPayload = do
     let eSessionKey = do
             keyLen <- symKeySize symalgo
-            first renderS2KError (string2Key s2k keyLen passphrase)
+            first SEIPDv2SessionKeyError (string2Key s2k keyLen passphrase)
     case eSessionKey of
         Left err -> pure (Left err)
         Right sessionKeyMaterial ->
             case first
-                renderCipherError
+                SEIPDv2CipherFailed
                 (withSymmetricCipher symalgo sessionKeyMaterial (pure . blockSize)) of
                 Left err -> pure (Left err)
                 Right ivLength -> do
@@ -2677,7 +2556,7 @@
                     let iv = IV ivBytes
                     let sessionKey = SessionKey sessionKeyMaterial
                     case encryptSEIPDv1Payload symalgo iv sessionKey literalPayload of
-                        Left err -> pure (Left err)
+                        Left err -> pure (Left (SEIPDv2CipherFailed err))
                         Right encrypted ->
                             pure
                                 ( Right
@@ -2698,17 +2577,14 @@
     -> S2K
     -> BL.ByteString
     -> B.ByteString
-    -> Either String [Pkt]
+    -> Either SEIPDv2Failure [Pkt]
 encryptSEIPDv2WithSKESK symalgo aead chunkSize salt s2k passphrase literalPayload = do
     keyLen <- symKeySize symalgo
     sessionKeyMaterial <-
-        first renderS2KError (string2Key s2k keyLen passphrase)
-    (_, nonceSize) <-
-        aeadModeAndNonceSizeForSEIPDv2
-            "unsupported AEAD algorithm for SKESK v6 encrypt"
-            aead
+        first SEIPDv2SessionKeyError (string2Key s2k keyLen passphrase)
+    (_, nonceSize) <- aeadModeAndNonceSize aead
     when (B.length (unSalt salt) < nonceSize) $
-        Left "SEIPD v2 salt is too short to derive the SKESK v6 IV"
+        Left SEIPDv2InvalidSaltLength
     let skeskIV = B.take nonceSize (unSalt salt)
     kek <- deriveSKESK6KEK symalgo aead sessionKeyMaterial
     (wrappedSessionKey, skeskTag) <-
@@ -2751,7 +2627,7 @@
     -> S2K
     -> BL.ByteString
     -> Block Pkt
-    -> Either String [Pkt]
+    -> Either SEIPDv2Failure [Pkt]
 encryptSEIPDv2WithSKESKBlock symalgo aead chunkSize salt s2k passphrase packetBlock =
     encryptSEIPDv2WithSKESK
         symalgo
@@ -2770,7 +2646,7 @@
     -> S2K
     -> BL.ByteString
     -> B.ByteString
-    -> Either String [Pkt]
+    -> Either SEIPDv2Failure [Pkt]
 encryptSEIPDv2LiteralDataWithSKESK symalgo aead chunkSize salt s2k passphrase payload =
     encryptSEIPDv2WithSKESKBlock
         symalgo
@@ -2795,32 +2671,34 @@
     -> Salt
     -> SessionKey
     -> B.ByteString
-    -> Either String B.ByteString
+    -> Either SEIPDv2Failure B.ByteString
 encryptSEIPDv2Payload symalgo aead chunkSize salt (SessionKey sessionKey) plaintext = do
     (mode, nonceSize) <- aeadModeAndNonceSize aead
     keyLen <- symKeySize symalgo
     let outputLen = keyLen + nonceSize - 8
         info = B.pack [0xd2, 2, fromFVal symalgo, fromFVal aead, chunkSize]
-        prk = extract @CHAlg.SHA256 (unSalt salt) sessionKey
-        okm = expand @CHAlg.SHA256 prk info outputLen :: B.ByteString
+        prk = extract @CHA.SHA256 (unSalt salt) sessionKey
+        okm = expand @CHA.SHA256 prk info outputLen :: B.ByteString
         messageKey = B.take keyLen okm
         noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)
     withAESCipher
-        "SEIPD v2 encrypt currently supports AES-128/192/256 only"
+        SEIPDv2CipherInitFailed
+        (SEIPDv2UnsupportedSymmetricAlgorithm symalgo)
         symalgo
         messageKey
-        (encryptChunks mode info chunkSize noncePrefix plaintext)
+        (encryptChunks aead mode info chunkSize noncePrefix plaintext)
 
 encryptChunks
     :: CCT.BlockCipher cipher
-    => CCT.AEADMode
+    => AEADAlgorithm
+    -> CCT.AEADMode
     -> B.ByteString
     -> Word8
     -> B.ByteString
     -> B.ByteString
     -> cipher
-    -> Either String B.ByteString
-encryptChunks mode info chunkSize noncePrefix plaintext cipher = go 0 plaintext [] 0
+    -> Either SEIPDv2Failure B.ByteString
+encryptChunks aead mode info chunkSize noncePrefix plaintext cipher = go 0 plaintext [] 0
   where
     chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)
     go idx remaining acc totalPlain
@@ -2828,11 +2706,12 @@
             (finalTag, finalCipher) <-
                 if mode == CCT.AEAD_OCB
                     then
-                        encryptWithOCBRFC7253
-                            cipher
-                            (noncePrefix <> encodeWord64be idx)
-                            (info <> encodeWord64be (fromIntegral totalPlain))
-                            B.empty
+                        first SEIPDv2CipherInitFailed $
+                            encryptWithOCBRFC7253
+                                cipher
+                                (noncePrefix <> encodeWord64be idx)
+                                (info <> encodeWord64be (fromIntegral totalPlain))
+                                B.empty
                     else do
                         aead <- initAEAD idx
                         let (tag, out) =
@@ -2844,17 +2723,24 @@
                         Right (tag, out)
             if B.null finalCipher
                 then return (B.concat (reverse acc) <> authTagToBS finalTag)
-                else Left "expected empty ciphertext for final SEIPD v2 tag"
+                else
+                    Left
+                        ( SEIPDv2CipherFailed
+                            ( CipherOperationFailed
+                                "expected empty ciphertext for final SEIPD v2 tag"
+                            )
+                        )
         | otherwise = do
             let (chunkPlain, rest) = B.splitAt chunkLen remaining
             (tag, chunkCipher) <-
                 if mode == CCT.AEAD_OCB
                     then
-                        encryptWithOCBRFC7253
-                            cipher
-                            (noncePrefix <> encodeWord64be idx)
-                            info
-                            chunkPlain
+                        first SEIPDv2CipherInitFailed $
+                            encryptWithOCBRFC7253
+                                cipher
+                                (noncePrefix <> encodeWord64be idx)
+                                info
+                                chunkPlain
                     else do
                         aead <- initAEAD idx
                         pure (CCT.aeadSimpleEncrypt aead info chunkPlain 16)
@@ -2866,26 +2752,20 @@
                 (totalPlain + B.length chunkPlain)
 
     initAEAD idx =
-        first show . CE.eitherCryptoError $
-            CCT.aeadInit mode cipher (noncePrefix <> encodeWord64be idx)
+        first SEIPDv2CipherInitFailed
+            . CE.eitherCryptoError
+            $ CCT.aeadInit mode cipher (noncePrefix <> encodeWord64be idx)
 
 aeadModeAndNonceSize
-    :: AEADAlgorithm -> Either String (CCT.AEADMode, Int)
-aeadModeAndNonceSize =
-    aeadModeAndNonceSizeForSEIPDv2
-        "unsupported AEAD algorithm for SEIPD v2 encrypt"
+    :: AEADAlgorithm -> Either SEIPDv2Failure (CCT.AEADMode, Int)
+aeadModeAndNonceSize = aeadModeAndNonceSizeForSEIPDv2
 
-symKeySize :: SymmetricAlgorithm -> Either String Int
-symKeySize =
-    seipdv2SymmetricKeySize
-        "unsupported symmetric algorithm for SEIPD v2 encrypt"
+symKeySize :: SymmetricAlgorithm -> Either SEIPDv2Failure Int
+symKeySize = seipdv2SymmetricKeySize
 
 authTagToBS :: CCT.AuthTag -> B.ByteString
 authTagToBS = BA.convert . CCT.unAuthTag
 
-encodeWord64be :: Word64 -> B.ByteString
-encodeWord64be = BL.toStrict . runPut . putWord64be
-
 {- | Compose a complete AEAD-encrypted message with optional literal data and signature.
 Returns a packet list (SKESK, SEIPD v2, optional signature) ready for serialization.
 
@@ -2904,7 +2784,7 @@
     -> BL.ByteString
     -> B.ByteString
     -> Maybe [Pkt]
-    -> Either String [Pkt]
+    -> Either SEIPDv2Failure [Pkt]
 composeMessageWithSEIPDv2 symalgo aead chunkSize salt s2k passphrase payload mSigs = do
     let packets = case mSigs of
             Nothing ->
diff --git a/Codec/Encryption/OpenPGP/Expirations.hs b/Codec/Encryption/OpenPGP/Expirations.hs
--- a/Codec/Encryption/OpenPGP/Expirations.hs
+++ b/Codec/Encryption/OpenPGP/Expirations.hs
@@ -25,7 +25,7 @@
     ) where
 
 import Control.Error.Util (hush)
-import Control.Lens ((&), (^.), _1)
+import Control.Lens ((&), (^.))
 import Data.List (maximumBy)
 import Data.Maybe (listToMaybe, mapMaybe)
 import Data.Ord (comparing)
@@ -45,9 +45,6 @@
     , signatureHashedSubpacketsKnown
     )
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Pkt
-    ( keyPktPKPayload
-    )
 
 data KeyState
     = KeyState
@@ -75,16 +72,20 @@
             (keyPktPKPayload (tk ^. tkPrimaryKey))
             relevantSelfSignatures
     relevantSelfSignatures =
-        filter (isDirectKeySelfSigFor primaryKey) (tk ^. tkRevs)
-            ++ filter
+        concat
+            [ filter (isDirectKeySelfSigFor primaryKey) (tk ^. tkRevs)
+            , filter
                 (isSelfCertificationFor primaryKey)
                 (concatMap snd (tk ^. tkUIDs))
-            ++ filter
+            , filter
                 (isSelfCertificationFor primaryKey)
                 (concatMap snd (tk ^. tkUAts))
+            ]
     selfCertificationGroups =
-        map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkUIDs)
-            ++ map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkUAts)
+        concat
+            [ map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkUIDs)
+            , map (filter (isSelfSignatureFor primaryKey) . snd) (tk ^. tkUAts)
+            ]
     hasAnySelfCertification = any (any isCertificationSig) selfCertificationGroups
     hasAnyActiveSelfCertification =
         any (selfCertificationGroupActiveAt ct) selfCertificationGroups
diff --git a/Codec/Encryption/OpenPGP/Internal.hs b/Codec/Encryption/OpenPGP/Internal.hs
--- a/Codec/Encryption/OpenPGP/Internal.hs
+++ b/Codec/Encryption/OpenPGP/Internal.hs
@@ -13,6 +13,11 @@
     , issuerFP
     , emptyPSC
     , leftPadTo
+    , checksum16
+    , checksum16Bytes
+    , edPointBytes
+    , encodeWord64be
+    , chunksOf8
     , pubkeyToMPIs
     , multiplicativeInverse
     , curveoidBSToCurve
@@ -22,6 +27,7 @@
     , edSigningCurveToCurveoidBS
     , curve2Curve
     , curveFromCurve
+    , xorBS
     ) where
 
 import Crypto.Number.Serialize (i2osp, os2ip)
@@ -29,16 +35,16 @@
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import qualified Crypto.PubKey.ECC.Types as ECCT
 import qualified Crypto.PubKey.RSA as RSA
-import Data.Bits (testBit)
+import Data.Binary.Put (putWord64be, runPut)
+import Data.Bits (shiftR, testBit, xor, (.&.))
 import qualified Data.ByteString as B
 import Data.ByteString.Lazy (ByteString)
 import qualified Data.ByteString.Lazy as BL
 import Data.List (find)
-import Data.Word (Word16, Word8)
+import Data.Word (Word16, Word64, Word8)
 
 import Codec.Encryption.OpenPGP.Ontology
     ( isIssuerSSP
-    , isSigCreationTime
     )
 import Codec.Encryption.OpenPGP.Types
 
@@ -79,6 +85,34 @@
     | B.length bs >= targetLen = bs
     | otherwise = B.replicate (targetLen - B.length bs) 0 <> bs
 
+checksum16 :: B.ByteString -> Word16
+checksum16 =
+    fromIntegral
+        . B.foldl'
+            (\acc octet -> (acc + fromIntegral octet) .&. (0xffff :: Int))
+            (0 :: Int)
+
+checksum16Bytes :: B.ByteString -> B.ByteString
+checksum16Bytes bs = B.cons hi (B.singleton lo)
+  where
+    chk = checksum16 bs
+    hi = fromIntegral (chk `shiftR` 8)
+    lo = fromIntegral chk
+
+edPointBytes :: EdPoint -> B.ByteString
+edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x
+edPointBytes (NativeEPoint (EPoint x)) = i2osp x
+
+encodeWord64be :: Word64 -> B.ByteString
+encodeWord64be = BL.toStrict . runPut . putWord64be
+
+chunksOf8 :: B.ByteString -> [B.ByteString]
+chunksOf8 bs
+    | B.null bs = []
+    | otherwise =
+        let (h, t) = B.splitAt 8 bs
+         in h : chunksOf8 t
+
 issuer :: Pkt -> Maybe EightOctetKeyId
 issuer pkt =
     case fromPktIssuerExtractionCase pkt of
@@ -256,3 +290,6 @@
     | c == ECCT.getCurveByName ECCT.SEC_p256r1 = NISTP256
     | c == ECCT.getCurveByName ECCT.SEC_p384r1 = NISTP384
     | c == ECCT.getCurveByName ECCT.SEC_p521r1 = NISTP521
+
+xorBS :: B.ByteString -> B.ByteString -> B.ByteString
+xorBS a b = B.pack (B.zipWith xor a b)
diff --git a/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs b/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs
--- a/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs
+++ b/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs
@@ -2,36 +2,49 @@
 -- Copyright © 2012-2026  Clint Adams
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
-
 {-# LANGUAGE PackageImports #-}
 {-# LANGUAGE RankNTypes #-}
 
 module Codec.Encryption.OpenPGP.Internal.CryptoAES
-  ( withAESCipher
-  ) where
+    ( withAESCipher
+    ) where
 
-import Codec.Encryption.OpenPGP.Types
-import qualified "crypton" Crypto.Cipher.AES as AES
-import qualified "crypton" Crypto.Cipher.Types as CCT
 import qualified Crypto.Error as CE
 import Data.Bifunctor (first)
 import qualified Data.ByteString as B
+import qualified "crypton" Crypto.Cipher.AES as AES
+import qualified "crypton" Crypto.Cipher.Types as CCT
 
-withAESCipher ::
-     String
-  -> SymmetricAlgorithm
-  -> B.ByteString
-  -> (forall cipher. CCT.BlockCipher cipher => cipher -> Either String a)
-  -> Either String a
-withAESCipher unsupportedSymmetricError symalgo keyBytes f =
-  case symalgo of
-    AES128 ->
-      first show (CE.eitherCryptoError (CCT.cipherInit keyBytes :: CE.CryptoFailable AES.AES128)) >>=
-      f
-    AES192 ->
-      first show (CE.eitherCryptoError (CCT.cipherInit keyBytes :: CE.CryptoFailable AES.AES192)) >>=
-      f
-    AES256 ->
-      first show (CE.eitherCryptoError (CCT.cipherInit keyBytes :: CE.CryptoFailable AES.AES256)) >>=
-      f
-    _ -> Left unsupportedSymmetricError
+import Codec.Encryption.OpenPGP.Types
+
+withAESCipher
+    :: (CE.CryptoError -> e)
+    -> e
+    -> SymmetricAlgorithm
+    -> B.ByteString
+    -> (forall cipher. CCT.BlockCipher cipher => cipher -> Either e a)
+    -> Either e a
+withAESCipher mkCryptoError unsupportedSymmetricError symalgo keyBytes f =
+    case symalgo of
+        AES128 ->
+            first
+                mkCryptoError
+                ( CE.eitherCryptoError
+                    (CCT.cipherInit keyBytes :: CE.CryptoFailable AES.AES128)
+                )
+                >>= f
+        AES192 ->
+            first
+                mkCryptoError
+                ( CE.eitherCryptoError
+                    (CCT.cipherInit keyBytes :: CE.CryptoFailable AES.AES192)
+                )
+                >>= f
+        AES256 ->
+            first
+                mkCryptoError
+                ( CE.eitherCryptoError
+                    (CCT.cipherInit keyBytes :: CE.CryptoFailable AES.AES256)
+                )
+                >>= f
+        _ -> Left unsupportedSymmetricError
diff --git a/Codec/Encryption/OpenPGP/Internal/CryptoSEIPDv2.hs b/Codec/Encryption/OpenPGP/Internal/CryptoSEIPDv2.hs
deleted file mode 100644
--- a/Codec/Encryption/OpenPGP/Internal/CryptoSEIPDv2.hs
+++ /dev/null
@@ -1,135 +0,0 @@
--- CryptoSEIPDv2.hs: OpenPGP (RFC9580) SEIPD-v2 and SKESK-v6 crypto helpers
--- Copyright © 2012-2026  Clint Adams
--- This software is released under the terms of the Expat license.
--- (See the LICENSE file).
-
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE PackageImports #-}
-{-# LANGUAGE TypeApplications #-}
-
-module Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2
-  ( aeadModeAndNonceSizeForSEIPDv2
-  , seipdv2SymmetricKeySize
-  , deriveSKESK6KEK
-  , encryptSKESK6SessionKey
-  , decryptSKESK6SessionKey
-  ) where
-
-import Codec.Encryption.OpenPGP.Internal.CryptoAES (withAESCipher)
-import Codec.Encryption.OpenPGP.Internal.RFC7253OCB
-  ( decryptWithOCBRFC7253With
-  , encryptWithOCBRFC7253
-  )
-import Codec.Encryption.OpenPGP.Types
-import qualified "crypton" Crypto.Cipher.Types as CCT
-import qualified Crypto.Error as CE
-import qualified Crypto.Hash.Algorithms as CHA
-import Crypto.KDF.HKDF (expand, extract)
-import Data.Bifunctor (first)
-import qualified Data.ByteArray as BA
-import qualified Data.ByteString as B
-
-aeadModeAndNonceSizeForSEIPDv2 ::
-     String -> AEADAlgorithm -> Either String (CCT.AEADMode, Int)
-aeadModeAndNonceSizeForSEIPDv2 otherAeadError EAX =
-  Left "EAX is currently unsupported by the crypton AEAD backend"
-aeadModeAndNonceSizeForSEIPDv2 otherAeadError OCB = Right (CCT.AEAD_OCB, 15)
-aeadModeAndNonceSizeForSEIPDv2 otherAeadError GCM = Right (CCT.AEAD_GCM, 12)
-aeadModeAndNonceSizeForSEIPDv2 otherAeadError (OtherAEADAlgo _) = Left otherAeadError
-
-seipdv2SymmetricKeySize :: String -> SymmetricAlgorithm -> Either String Int
-seipdv2SymmetricKeySize unsupportedSymmetricError symalgo =
-  case symalgo of
-    AES128 -> Right 16
-    AES192 -> Right 24
-    AES256 -> Right 32
-    _ -> Left unsupportedSymmetricError
-
-skeskV6Info :: SymmetricAlgorithm -> AEADAlgorithm -> B.ByteString
-skeskV6Info symalgo aead = B.pack [0xc3, 6, fromFVal symalgo, fromFVal aead]
-
-deriveSKESK6KEK ::
-     SymmetricAlgorithm
-  -> AEADAlgorithm
-  -> B.ByteString
-  -> Either String B.ByteString
-deriveSKESK6KEK symalgo aead ikm = do
-  keyLen <-
-    seipdv2SymmetricKeySize
-      "SKESK v6 currently supports AES-128/192/256 only"
-      symalgo
-  let prk = extract @CHA.SHA256 B.empty ikm
-  pure (expand @CHA.SHA256 prk (skeskV6Info symalgo aead) keyLen)
-
-encryptSKESK6SessionKey ::
-     SymmetricAlgorithm
-  -> AEADAlgorithm
-  -> B.ByteString
-  -> B.ByteString
-  -> B.ByteString
-  -> Either String (B.ByteString, B.ByteString)
-encryptSKESK6SessionKey symalgo aead kek iv sessionKey = do
-  (mode, nonceSize) <-
-    aeadModeAndNonceSizeForSEIPDv2
-      "unsupported AEAD algorithm for SKESK v6 encrypt"
-      aead
-  if B.length iv /= nonceSize
-    then Left "SKESK v6 IV length does not match AEAD algorithm"
-    else
-      withAESCipher
-        "SKESK v6 encrypt currently supports AES-128/192/256 only"
-        symalgo
-        kek
-        (\cipher ->
-           if mode == CCT.AEAD_OCB
-             then do
-               (tag, ciphertext) <-
-                 encryptWithOCBRFC7253 cipher iv (skeskV6Info symalgo aead) sessionKey
-               pure (ciphertext, authTagToBS tag)
-             else do
-               aeadCtx <- first show . CE.eitherCryptoError $ CCT.aeadInit mode cipher iv
-               let (tag, ciphertext) =
-                     CCT.aeadSimpleEncrypt aeadCtx (skeskV6Info symalgo aead) sessionKey 16
-               pure (ciphertext, authTagToBS tag))
-
-decryptSKESK6SessionKey ::
-     SymmetricAlgorithm
-  -> AEADAlgorithm
-  -> B.ByteString
-  -> B.ByteString
-  -> B.ByteString
-  -> B.ByteString
-  -> Either String B.ByteString
-decryptSKESK6SessionKey symalgo aead kek iv ciphertext tag = do
-  (mode, nonceSize) <-
-    aeadModeAndNonceSizeForSEIPDv2
-      "unsupported AEAD algorithm for SKESK v6 decrypt"
-      aead
-  if B.length iv /= nonceSize
-    then Left "SKESK v6 IV length does not match AEAD algorithm"
-    else
-      withAESCipher
-        "SKESK v6 decrypt currently supports AES-128/192/256 only"
-        symalgo
-        kek
-        (\cipher ->
-           if mode == CCT.AEAD_OCB
-             then
-               decryptWithOCBRFC7253With
-                 (\_ _ _ _ _ _ -> "SKESK v6 authentication failed")
-                 cipher
-                 iv
-                 (skeskV6Info symalgo aead)
-                 ciphertext
-                 (mkAuthTag tag)
-             else do
-               aeadCtx <- first show . CE.eitherCryptoError $ CCT.aeadInit mode cipher iv
-               case CCT.aeadSimpleDecrypt aeadCtx (skeskV6Info symalgo aead) ciphertext (mkAuthTag tag) of
-                 Nothing -> Left "SKESK v6 authentication failed"
-                 Just plain -> Right plain)
-
-authTagToBS :: CCT.AuthTag -> B.ByteString
-authTagToBS = BA.convert . CCT.unAuthTag
-
-mkAuthTag :: B.ByteString -> CCT.AuthTag
-mkAuthTag = CCT.AuthTag . BA.convert
diff --git a/Codec/Encryption/OpenPGP/Internal/RFC7253OCB.hs b/Codec/Encryption/OpenPGP/Internal/RFC7253OCB.hs
--- a/Codec/Encryption/OpenPGP/Internal/RFC7253OCB.hs
+++ b/Codec/Encryption/OpenPGP/Internal/RFC7253OCB.hs
@@ -23,20 +23,21 @@
     )
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
-import Data.List (foldl')
 import Data.Word (Word8)
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
+import Codec.Encryption.OpenPGP.Internal (xorBS)
+
 encryptWithOCBRFC7253
     :: CCT.BlockCipher c
     => c
     -> B.ByteString
     -> B.ByteString
     -> B.ByteString
-    -> Either String (CCT.AuthTag, B.ByteString)
+    -> Either e (CCT.AuthTag, B.ByteString)
 encryptWithOCBRFC7253 cipher nonce ad plaintext = do
     when (B.length nonce > 15 || B.null nonce) $
-        Left "invalid nonce size for OCB"
+        error "invalid nonce size for OCB"
     offset0 <- ocbOffset0 cipher nonce
     let zeroBlock = B.replicate 16 0
         lStar = CCT.ecbEncrypt cipher zeroBlock
@@ -93,19 +94,19 @@
          -> B.ByteString
          -> B.ByteString
          -> B.ByteString
-         -> String
+         -> e
        )
     -> c
     -> B.ByteString
     -> B.ByteString
     -> B.ByteString
     -> CCT.AuthTag
-    -> Either String B.ByteString
+    -> Either e B.ByteString
 decryptWithOCBRFC7253With onAuthFailure cipher nonce ad ciphertext authTag = do
     when (B.length nonce > 15 || B.null nonce) $
-        Left "invalid nonce size for OCB"
+        error "invalid nonce size for OCB"
     when (B.length tagBytes /= 16) $
-        Left "invalid auth tag size for OCB"
+        error "invalid auth tag size for OCB"
     offset0 <- ocbOffset0 cipher nonce
     let zeroBlock = B.replicate 16 0
         lStar = CCT.ecbEncrypt cipher zeroBlock
@@ -154,12 +155,12 @@
 
 ocbOffset0
     :: CCT.BlockCipher c
-    => c -> B.ByteString -> Either String B.ByteString
+    => c -> B.ByteString -> Either e B.ByteString
 ocbOffset0 cipher nonce = do
     let nonceLen = B.length nonce
         prefixLen = 16 - nonceLen
     when (prefixLen <= 0) $
-        Left "invalid nonce size for OCB"
+        error "invalid nonce size for OCB"
     let prefix = B.pack (replicate (prefixLen - 1) 0 <> [1 :: Word8])
         nonceBlock = prefix <> nonce
         bottom = fromIntegral (B.last nonceBlock .&. 0x3f) :: Int
@@ -216,9 +217,6 @@
             )
             ([], 0)
             (reverse (B.unpack bs))
-
-xorBS :: B.ByteString -> B.ByteString -> B.ByteString
-xorBS a b = B.pack (B.zipWith xor a b)
 
 ocbBitSlice128 :: B.ByteString -> Int -> B.ByteString
 ocbBitSlice128 stretch startBit =
diff --git a/Codec/Encryption/OpenPGP/Internal/Whitespace.hs b/Codec/Encryption/OpenPGP/Internal/Whitespace.hs
--- a/Codec/Encryption/OpenPGP/Internal/Whitespace.hs
+++ b/Codec/Encryption/OpenPGP/Internal/Whitespace.hs
@@ -17,23 +17,19 @@
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Builder as BB
 import qualified Data.ByteString.Lazy as BL
-import Data.Foldable (foldl')
+import Data.List (mapAccumL)
 import Data.Word (Word8)
 
 canonicalizeLineEndings :: BL.ByteString -> BL.ByteString
-canonicalizeLineEndings bs = BL.fromStrict (B.unfoldr step (Nothing, BL.toStrict bs))
+canonicalizeLineEndings bs =
+    BL.fromChunks $ chunks ++ [flush]
   where
-    step (Nothing, rest)
-        | B.null rest = Nothing
-        | otherwise =
-            case B.uncons rest of
-                Just (0x0d, tail') ->
-                    case B.uncons tail' of
-                        Just (0x0a, tail'') -> Just (0x0d, (Just 0x0a, tail''))
-                        _ -> Just (0x0d, (Just 0x0a, tail'))
-                Just (0x0a, tail') -> Just (0x0d, (Just 0x0a, tail'))
-                Just (w, tail') -> Just (w, (Nothing, tail'))
-    step (Just w, rest) = Just (w, (Nothing, rest))
+    (state, chunks) =
+        mapAccumL
+            canonicalizeLineEndingsChunk
+            (CRState False)
+            (BL.toChunks bs)
+    flush = canonicalizeLineEndingsFlush state
 
 -- | State carried between chunks when canonicalizing line endings.
 data CRState = CRState
@@ -43,7 +39,8 @@
 {- | Process one strict chunk and return updated state plus emitted bytes.
 
 The returned bytes may end with a standalone @\\r@ if the chunk boundary
-falls mid-pair; feed it to 'canonicalizeLineEndingsFlush' at the end.
+falls mid-pair; feed it to 'canonicalizeLineEndingsFlush' at the end to
+preserve it as-is.
 -}
 canonicalizeLineEndingsChunk
     :: CRState
@@ -52,28 +49,29 @@
 canonicalizeLineEndingsChunk (CRState prevWasCR) chunk
     | B.null chunk = (CRState False, B.empty)
     | otherwise =
-        ( CRState newPrevCR
+        ( CRState isPrevCR
         , BL.toStrict $ BB.toLazyByteString bldr
         )
   where
-    newPrevCR = B.last chunk == 0x0d
+    isPrevCR = B.last chunk == 0x0d
     bldr = snd $ B.foldl' step (prevWasCR, mempty) chunk
-    step (prevCR, b) w
-        | prevCR && w == 0x0a =
+    step (hadPrevCR, b) w
+        | hadPrevCR && w == 0x0a =
             (False, b <> BB.word8 0x0d <> BB.word8 0x0a)
-        | prevCR && w == 0x0d =
-            (True, b <> BB.word8 0x0d <> BB.word8 0x0a)
-        | prevCR = (False, b <> BB.word8 0x0d <> BB.word8 w)
+        | hadPrevCR && w == 0x0d =
+            (True, b <> BB.word8 0x0d)
+        | hadPrevCR =
+            (False, b <> BB.word8 0x0d <> BB.word8 w)
         | w == 0x0d = (True, b)
         | w == 0x0a = (False, b <> BB.word8 0x0d <> BB.word8 0x0a)
         | otherwise = (False, b <> BB.word8 w)
 
 {- | Emit any pending state as final bytes (a trailing standalone @\\r@
-becomes @\\r\\n@).
+is preserved as-is).
 -}
 canonicalizeLineEndingsFlush :: CRState -> B.ByteString
-canonicalizeLineEndingsFlush (CRState prevCR)
-    | prevCR = B.pack [0x0d, 0x0a]
+canonicalizeLineEndingsFlush (CRState hadPrevCR)
+    | hadPrevCR = B.pack [0x0d]
     | otherwise = B.empty
 
 {- | Strip trailing spaces (0x20) and tabs (0x09) from each line.
@@ -132,15 +130,15 @@
     crlf :: BB.Builder
     crlf = BB.word8 0x0d <> BB.word8 0x0a
 
-    stepByte (StripWSState prevCR line, bldr) w
-        | prevCR && w == 0x0a =
-            (StripWSState False B.empty, bldr <> trimmedLine <> crlf)
-        | prevCR =
+    stepByte (StripWSState pcr line, acc) w
+        | pcr && w == 0x0a =
+            (StripWSState False B.empty, acc <> trimmedLine <> crlf)
+        | pcr =
             ( StripWSState False (B.singleton w)
-            , bldr <> trimmedLine <> BB.word8 0x0d
+            , acc <> trimmedLine <> BB.word8 0x0d
             )
-        | w == 0x0d = (StripWSState True line, bldr)
-        | otherwise = (StripWSState False (line <> B.singleton w), bldr)
+        | w == 0x0d = (StripWSState True line, acc)
+        | otherwise = (StripWSState False (line <> B.singleton w), acc)
       where
         trimmedLine = BB.byteString $ B.dropWhileEnd isTrailingWhitespace line
 
@@ -149,8 +147,8 @@
 -}
 stripTrailingWhitespacePerLineFlush
     :: StripWSState -> B.ByteString
-stripTrailingWhitespacePerLineFlush (StripWSState prevCR line) =
-    if prevCR
+stripTrailingWhitespacePerLineFlush (StripWSState pcr line) =
+    if pcr
         then
             BL.toStrict $
                 BB.toLazyByteString $
diff --git a/Codec/Encryption/OpenPGP/KeyInfo.hs b/Codec/Encryption/OpenPGP/KeyInfo.hs
--- a/Codec/Encryption/OpenPGP/KeyInfo.hs
+++ b/Codec/Encryption/OpenPGP/KeyInfo.hs
@@ -59,7 +59,7 @@
 pkalgoAbbrev ECDSA = "ecd"
 pkalgoAbbrev ForbiddenElgamal = "-eg"
 pkalgoAbbrev DH = "dh"
-pkalgoAbbrev EdDSA = "edd"
+pkalgoAbbrev EdDSALegacy = "edd"
 pkalgoAbbrev BaseTypes.X25519 = "x25"
 pkalgoAbbrev BaseTypes.X448 = "x448"
 pkalgoAbbrev BaseTypes.Ed25519 = "e25"
diff --git a/Codec/Encryption/OpenPGP/KeyringParser.hs b/Codec/Encryption/OpenPGP/KeyringParser.hs
--- a/Codec/Encryption/OpenPGP/KeyringParser.hs
+++ b/Codec/Encryption/OpenPGP/KeyringParser.hs
@@ -44,7 +44,6 @@
     , brokenWithWireRep
 
       -- * Utilities
-    , parseUnknownTKs
     , parseTKsEither
     , parseTKs
     , parsePublicTKs
@@ -398,22 +397,18 @@
     isBroken [BrokenPacketPkt _ a _] = t == fromIntegral a
     isBroken _ = False
 
-{-# DEPRECATED parseUnknownTKs "Use parsePublicTKs or parseSecretTKs instead" #-}
-
--- | parse TKs from packets
-parseUnknownTKs :: Bool -> [Pkt] -> [TKUnknown]
-parseUnknownTKs intolerant ps =
-    catMaybes $
-        runIncrementalParser
-            (anyTK intolerant)
-            (map (: []) (filter notTrustPacket ps))
-  where
-    notTrustPacket = not . isTrustPkt
-
 parseTKsEither
     :: Bool -> [Pkt] -> [Either TKConversionError SomeTK]
-parseTKsEither intolerant =
-    map fromUnknownToTKEither . parseUnknownTKs intolerant
+parseTKsEither intolerant ps =
+    map
+        fromUnknownToTKEither
+        ( catMaybes $
+            runIncrementalParser
+                (anyTK intolerant)
+                (map (: []) (filter notTrustPacket ps))
+        )
+  where
+    notTrustPacket = not . isTrustPkt
 
 parseTKs :: Bool -> [Pkt] -> [SomeTK]
 parseTKs intolerant packets = rights (parseTKsEither intolerant packets)
diff --git a/Codec/Encryption/OpenPGP/Message.hs b/Codec/Encryption/OpenPGP/Message.hs
--- a/Codec/Encryption/OpenPGP/Message.hs
+++ b/Codec/Encryption/OpenPGP/Message.hs
@@ -34,8 +34,18 @@
     , mkEd448SignerV4
     , mkEd448SignerV6
     , MessageParseFailure (..)
+    , renderMessageParseFailure
+    , MDCFailure (..)
+    , AEADFailure (..)
+    , renderAEADFailure
+    , PayloadDecryptFailure (..)
+    , renderPayloadDecryptFailure
     , MessageDecryptFailure (..)
+    , renderMessageDecryptFailure
+    , MessageEncryptFailure (..)
+    , renderMessageEncryptFailure
     , MessageError (..)
+    , renderMessageError
     , SessionMaterialExposure (..)
     , EncryptMessageProfile
     , EncryptMessageOptions (..)
@@ -52,18 +62,17 @@
 
 import Control.Monad (foldM)
 import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
-import qualified Crypto.Hash as CH
+import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
 import qualified Crypto.PubKey.Ed25519 as Ed25519
 import qualified Crypto.PubKey.Ed448 as Ed448
 import qualified Crypto.PubKey.RSA.Types as RSATypes
 import Crypto.Random.Types (MonadRandom, getRandomBytes)
-import Data.Bifunctor (first)
+import Data.Bifunctor (bimap, first)
 import Data.Binary (put)
 import Data.Binary.Put (runPut)
-import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as BL
+import Data.Functor.Identity (Identity (..), runIdentity)
 import Data.Kind (Type)
 import Data.Word (Word8)
 
@@ -71,31 +80,22 @@
     ( CipherError
     , keySize
     , renderCipherError
-    , withSymmetricCipher
     )
 import Codec.Encryption.OpenPGP.CFB
     ( OpenPGPCFBModeW (..)
     , decryptOpenPGPCfb
     , decryptPreservingNonce
     , encryptOpenPGPCfbRaw
-    , mdcTrailerForSEIPDv1
-    , seipdv1NonceFromIV
-    , validateSEIPD1MDC
     )
 import Codec.Encryption.OpenPGP.Encrypt
-    ( encryptSEIPDv2WithSKESKBlock
+    ( buildOnePassSignature
+    , encryptSEIPDv2WithSKESKBlock
+    , renderOPSBuildError
     )
 import Codec.Encryption.OpenPGP.Fingerprint
     ( eightOctetKeyID
     , fingerprint
     )
-import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2
-    ( decryptSKESK6SessionKey
-    , deriveSKESK6KEK
-    )
-import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
-    ( HOBlockCipher (..)
-    )
 import Codec.Encryption.OpenPGP.Policy
     ( HashAlgorithmW (..)
     , OpenPGPPolicy
@@ -115,10 +115,23 @@
     , skesk2SessionKey
     , string2Key
     )
+import Codec.Encryption.OpenPGP.SEIPDv1
+    ( MDCFailure (..)
+    , mdcTrailerForSEIPDv1
+    , renderMDCFailure
+    , validateSEIPD1MDC
+    )
+import Codec.Encryption.OpenPGP.SEIPDv2
+    ( SEIPDv2Failure (..)
+    , decryptSKESK6SessionKey
+    , deriveSKESK6KEK
+    , renderSEIPDv2Failure
+    )
 import Codec.Encryption.OpenPGP.Serialize (parsePkts)
 import Codec.Encryption.OpenPGP.Signatures
     ( SignError (..)
     , VerificationError
+    , renderSignError
     , signDataWithEd25519Builder
     , signDataWithEd25519V6Builder
     , signDataWithEd448Builder
@@ -135,9 +148,6 @@
     , sigBuilderInitV6Typed
     )
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Base
-    ( Passphrase (..)
-    )
 import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA
 import Data.Conduit.OpenPGP.Decrypt (decryptSEIPDv2Payload)
 import qualified Data.Conduit.OpenPGP.Message as ConduitMessage
@@ -238,7 +248,7 @@
 mkEd448SignerV6 = Ed448Signer
 
 data MessageError
-    = MessageEncryptError String
+    = MessageEncryptFailureError MessageEncryptFailure
     | MessageDecryptError String
     | MessageSignError SignError
     | MessageParseError String
@@ -246,38 +256,37 @@
     | MessageDecryptFailureError MessageDecryptFailure
     deriving (Eq, Show)
 
-newtype MessageFlow a
-    = MessageFlow
-    { runMessageFlow :: Either MessageError a
-    }
-
-instance Functor MessageFlow where
-    fmap f (MessageFlow result) = MessageFlow (fmap f result)
-
-instance Applicative MessageFlow where
-    pure = MessageFlow . Right
-    MessageFlow ff <*> MessageFlow fa = MessageFlow (ff <*> fa)
-
-instance Monad MessageFlow where
-    MessageFlow result >>= f =
-        case result of
-            Left err -> MessageFlow (Left err)
-            Right x -> f x
+messageStep
+    :: Monad m => Either MessageError a -> ExceptT MessageError m a
+messageStep = ExceptT . pure
 
-messageStep :: Either MessageError a -> MessageFlow a
-messageStep = MessageFlow
+runMessageFlow
+    :: ExceptT MessageError Identity a -> Either MessageError a
+runMessageFlow = runIdentity . runExceptT
 
 type MessageFlowT m = ExceptT MessageError m
 
 runMessageFlowT :: MessageFlowT m a -> m (Either MessageError a)
 runMessageFlowT = runExceptT
 
-liftMessageFlowT :: Monad m => MessageFlow a -> MessageFlowT m a
-liftMessageFlowT (MessageFlow result) =
-    case result of
-        Left err -> throwE err
-        Right x -> pure x
+signStepT :: Monad m => Either SignError a -> MessageFlowT m a
+signStepT = ExceptT . pure . first MessageSignError
 
+parseStep
+    :: Monad m
+    => Either MessageParseFailure a -> ExceptT MessageError m a
+parseStep = messageStep . first MessageParseFailureError
+
+decryptStep
+    :: Monad m
+    => Either MessageDecryptFailure a -> ExceptT MessageError m a
+decryptStep = messageStep . first MessageDecryptFailureError
+
+encryptStep
+    :: Monad m
+    => Either MessageEncryptFailure a -> ExceptT MessageError m a
+encryptStep = messageStep . first MessageEncryptFailureError
+
 data MessageParseFailure
     = MissingEncryptedMessage
     | ExpectedSKESKThenEncryptedData
@@ -288,11 +297,33 @@
     | BrokenCriticalPacketType Word8 String
     deriving (Eq, Show)
 
+data AEADFailure
+    = AEADChunkAuthFailed AEADAlgorithm Int
+    | AEADFinalTagFailed AEADAlgorithm
+    | AEADInitFailed CipherError
+    deriving (Eq, Show)
+
+data PayloadDecryptFailure
+    = PayloadDecryptCipherFailed CipherError
+    | PayloadDecryptMDCFailed MDCFailure
+    | PayloadDecryptAEADFailed AEADFailure
+    | PayloadDecryptSEIPDv2Failed SEIPDv2Failure
+    | PayloadDecryptGeneric String
+    deriving (Eq, Show)
+
 data MessageDecryptFailure
     = SessionMaterialDerivationFailed S2KError
-    | PayloadDecryptFailed String
+    | PayloadDecryptFailed PayloadDecryptFailure
     deriving (Eq, Show)
 
+data MessageEncryptFailure
+    = MessageEncryptSEIPDv2Failed SEIPDv2Failure
+    | MessageEncryptCipherFailed CipherError
+    | MessageEncryptS2KFailed S2KError
+    | MessageEncryptDeprecatedS2KHash HashAlgorithm
+    | MessageEncryptUnsupportedSymmetricAlgorithm SymmetricAlgorithm
+    deriving (Eq, Show)
+
 data ParsedEncryptedPayloadKind
     = LegacySEDPayloadKind
     | LegacySEIPDv1PayloadKind
@@ -355,8 +386,45 @@
 
 renderMessageDecryptFailure :: MessageDecryptFailure -> String
 renderMessageDecryptFailure (SessionMaterialDerivationFailed err) = renderS2KError err
-renderMessageDecryptFailure (PayloadDecryptFailed err) = err
+renderMessageDecryptFailure (PayloadDecryptFailed err) = renderPayloadDecryptFailure err
 
+renderMessageEncryptFailure :: MessageEncryptFailure -> String
+renderMessageEncryptFailure (MessageEncryptSEIPDv2Failed err) = renderSEIPDv2Failure err
+renderMessageEncryptFailure (MessageEncryptCipherFailed err) = renderCipherError err
+renderMessageEncryptFailure (MessageEncryptS2KFailed err) = renderS2KError err
+renderMessageEncryptFailure (MessageEncryptDeprecatedS2KHash ha) =
+    "deprecated hash algorithm disallowed for modern message generation: "
+        ++ show ha
+renderMessageEncryptFailure (MessageEncryptUnsupportedSymmetricAlgorithm sa) =
+    "symmetric algorithm disallowed for RFC9580 message generation: "
+        ++ show sa
+
+renderAEADFailure :: AEADFailure -> String
+renderAEADFailure (AEADChunkAuthFailed algo chunk) =
+    "AEAD chunk authentication failed for "
+        ++ show algo
+        ++ " at chunk "
+        ++ show chunk
+renderAEADFailure (AEADFinalTagFailed algo) =
+    "AEAD final tag verification failed for " ++ show algo
+renderAEADFailure (AEADInitFailed err) =
+    "AEAD initialization failed: " ++ renderCipherError err
+
+renderPayloadDecryptFailure :: PayloadDecryptFailure -> String
+renderPayloadDecryptFailure (PayloadDecryptCipherFailed err) = renderCipherError err
+renderPayloadDecryptFailure (PayloadDecryptMDCFailed err) = renderMDCFailure err
+renderPayloadDecryptFailure (PayloadDecryptAEADFailed err) = renderAEADFailure err
+renderPayloadDecryptFailure (PayloadDecryptSEIPDv2Failed err) = renderSEIPDv2Failure err
+renderPayloadDecryptFailure (PayloadDecryptGeneric err) = err
+
+renderMessageError :: MessageError -> String
+renderMessageError (MessageEncryptFailureError err) = renderMessageEncryptFailure err
+renderMessageError (MessageDecryptError err) = err
+renderMessageError (MessageSignError err) = renderSignError err
+renderMessageError (MessageParseError err) = err
+renderMessageError (MessageParseFailureError err) = renderMessageParseFailure err
+renderMessageError (MessageDecryptFailureError err) = renderMessageDecryptFailure err
+
 mkEncryptedPayload :: BL.ByteString -> EncryptedPayload
 mkEncryptedPayload = EncryptedPayload
 
@@ -369,49 +437,31 @@
 encryptedPayloadBytes :: EncryptedPayload -> BL.ByteString
 encryptedPayloadBytes = unEncryptedPayload
 
-firstLeft :: (e -> e') -> Either e a -> Either e' a
-firstLeft f = either (Left . f) Right
-
--- | Lift a parse failure step into the unified MessageError channel
-parseStep :: Either MessageParseFailure a -> MessageFlow a
-parseStep = messageStep . firstLeft MessageParseFailureError
-
--- | Lift a decrypt failure step into the unified MessageError channel
-decryptStep :: Either MessageDecryptFailure a -> MessageFlow a
-decryptStep = messageStep . firstLeft MessageDecryptFailureError
-
--- | Lift a string encrypt error step into the unified MessageError channel
-encryptStep :: Either String a -> MessageFlow a
-encryptStep = messageStep . firstLeft MessageEncryptError
-
--- | Lift a sign error step into the unified MessageError channel
-signStep :: Either SignError a -> MessageFlow a
-signStep = messageStep . firstLeft MessageSignError
-
-signStepT :: Monad m => Either SignError a -> MessageFlowT m a
-signStepT = liftMessageFlowT . signStep
-
 signBackendStep :: Either String a -> Either SignError a
-signBackendStep = firstLeft SignBackendError
+signBackendStep = first SignBackendError
 
 decryptSessionStep
     :: Either S2KError a -> Either MessageDecryptFailure a
-decryptSessionStep = firstLeft SessionMaterialDerivationFailed
+decryptSessionStep = first SessionMaterialDerivationFailed
 
 decryptSessionKeySizeStep
     :: Either CipherError a -> Either MessageDecryptFailure a
 decryptSessionKeySizeStep =
-    firstLeft
+    first
         (SessionMaterialDerivationFailed . S2KUnsupportedAlgorithm)
 
 decryptCipherStep
     :: Either CipherError a -> Either MessageDecryptFailure a
-decryptCipherStep = firstLeft (PayloadDecryptFailed . renderCipherError)
+decryptCipherStep = first (PayloadDecryptFailed . PayloadDecryptCipherFailed)
 
-decryptPayloadStep
-    :: Either String a -> Either MessageDecryptFailure a
-decryptPayloadStep = firstLeft PayloadDecryptFailed
+decryptMDCStep
+    :: Either MDCFailure a -> Either MessageDecryptFailure a
+decryptMDCStep = first (PayloadDecryptFailed . PayloadDecryptMDCFailed)
 
+decryptSEIPDv2Step
+    :: Either SEIPDv2Failure a -> Either MessageDecryptFailure a
+decryptSEIPDv2Step = first (PayloadDecryptFailed . PayloadDecryptSEIPDv2Failed)
+
 encryptMessage
     :: EncryptMessageOptions p
     -> Passphrase
@@ -433,7 +483,7 @@
             encryptStep $ validateRFC9580MessageSymmetric defaultPolicy sa
             encryptStep $ validateModernMessageS2K defaultPolicy s2k
             encrypted <-
-                encryptStep $
+                encryptStep . first MessageEncryptSEIPDv2Failed $
                     encryptSEIPDv2WithSKESKBlock
                         sa
                         (messageDefaultAEADAlgorithm messagePolicy)
@@ -464,17 +514,18 @@
         | otherwise = ivBytes
 
 encryptMessageWithRFC4880Fallback
-    :: SymmetricAlgorithm
+    :: Monad m
+    => SymmetricAlgorithm
     -> S2K
     -> IV
     -> Passphrase
     -> ClearPayload
-    -> MessageFlow EncryptedPayload
+    -> ExceptT MessageError m EncryptedPayload
 encryptMessageWithRFC4880Fallback sa s2k iv passphrase payload = do
     keyLen <-
-        encryptStep . first renderCipherError $ keySize sa
+        encryptStep . first MessageEncryptCipherFailed $ keySize sa
     sessionMaterial <-
-        encryptStep . first renderS2KError $
+        encryptStep . first MessageEncryptS2KFailed $
             WrappedSessionMaterial
                 <$> string2Key s2k keyLen (unPassphrase passphrase)
     let literal =
@@ -482,7 +533,7 @@
         cleartext = BL.toStrict (runPut (put (Block [literal])))
         cleartextWithMDC = cleartext <> mdcTrailerForSEIPDv1 iv cleartext
     encrypted <-
-        encryptStep . first renderCipherError $
+        encryptStep . first MessageEncryptCipherFailed $
             encryptOpenPGPCfbRaw
                 OpenPGPCFBNoResyncW
                 sa
@@ -497,13 +548,15 @@
             ]
 
 deriveSessionMaterial
-    :: SymmetricAlgorithm
+    :: Monad m
+    => SymmetricAlgorithm
     -> S2K
     -> Passphrase
-    -> MessageFlow B.ByteString
+    -> ExceptT MessageError m B.ByteString
 deriveSessionMaterial sa s2k passphrase = do
-    keyLen <- encryptStep . first renderCipherError $ keySize sa
-    encryptStep . first renderS2KError $
+    keyLen <-
+        encryptStep . first MessageEncryptCipherFailed $ keySize sa
+    encryptStep . first MessageEncryptS2KFailed $
         string2Key s2k keyLen (unPassphrase passphrase)
 
 exposedSessionMaterial
@@ -532,7 +585,7 @@
     payload <-
         parseStep $ extractEncryptedPayload encryptedPackets
     cleartext <-
-        decryptStep $ decryptPayloadTyped passphrase payload
+        decryptStep $ decryptPayload passphrase payload
     clearPackets <-
         parseStep $
             rejectUnknownCriticalPacketsTyped
@@ -544,84 +597,108 @@
     => Signer alg v
     -> ClearPayload
     -> m (Either MessageError BL.ByteString)
-signMessageWith signer payload = runMessageFlowT $
-    case signer of
-        RSASigner signerPK signingKey ->
-            case signerPK of
-                VersionedPKPayloadV4 pk ->
-                    signV4Message
-                        pk
-                        ( \hashed unhashed clear ->
-                            let builder =
-                                    sigBuilderInitTyped @'PKA.RSA RFC9580W BinarySig SHA512W
-                                withHashed = addHashedSubs (listToHashedSubs hashed) builder
-                                withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed
-                             in signDataWithRSABuilder withUnhashed signingKey clear
-                        )
-                        payload
-                VersionedPKPayloadV6 pk ->
-                    signV6Message
-                        pk
-                        ( \salt hashed unhashed clear ->
-                            let builder =
-                                    sigBuilderInitV6Typed @'PKA.RSA RFC9580W BinarySig SHA512W salt
-                                withHashed = addHashedSubs (listToHashedSubs hashed) builder
-                                withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed
-                             in signDataWithRSAV6Builder withUnhashed signingKey clear
-                        )
-                        payload
-        Ed25519Signer signerPK signingKey ->
-            case signerPK of
-                VersionedPKPayloadV4 pk ->
-                    signV4Message
-                        pk
-                        ( \hashed unhashed clear ->
-                            let builder =
-                                    sigBuilderInitTyped @'PKA.Ed25519 RFC9580W BinarySig SHA512W
-                                withHashed = addHashedSubs (listToHashedSubs hashed) builder
-                                withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed
-                             in signDataWithEd25519Builder withUnhashed signingKey clear
-                        )
-                        payload
-                VersionedPKPayloadV6 pk ->
-                    signV6Message
-                        pk
-                        ( \salt hashed unhashed clear ->
-                            let builder =
-                                    sigBuilderInitV6Typed @'PKA.Ed25519
-                                        RFC9580W
-                                        BinarySig
-                                        SHA512W
-                                        salt
-                                withHashed = addHashedSubs (listToHashedSubs hashed) builder
-                                withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed
-                             in signDataWithEd25519V6Builder withUnhashed signingKey clear
-                        )
-                        payload
-        Ed448Signer signerPK signingKey ->
-            case signerPK of
-                VersionedPKPayloadV4 pk ->
-                    signV4Message
-                        pk
-                        ( \hashed unhashed clear ->
-                            let builder =
-                                    sigBuilderInitTyped @'PKA.Ed448 RFC9580W BinarySig SHA512W
-                                withHashed = addHashedSubs (listToHashedSubs hashed) builder
-                                withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed
-                             in signDataWithEd448Builder withUnhashed signingKey clear
-                        )
-                        payload
-                VersionedPKPayloadV6 pk ->
-                    signV6Message
-                        pk
-                        ( \salt hashed unhashed clear ->
-                            let builder =
-                                    sigBuilderInitV6Typed @'PKA.Ed448 RFC9580W BinarySig SHA512W salt
-                                withHashed = addHashedSubs (listToHashedSubs hashed) builder
-                                withUnhashed = addUnhashedSubs (listToUnhashedSubs unhashed) withHashed
-                             in signDataWithEd448V6Builder withUnhashed signingKey clear
-                        )
-                        payload
+signMessageWith signer payload =
+    runMessageFlowT $
+        let applySubs builder hashed unhashed =
+                addUnhashedSubs
+                    (listToUnhashedSubs unhashed)
+                    (addHashedSubs (listToHashedSubs hashed) builder)
+         in case signer of
+                RSASigner signerPK signingKey ->
+                    case signerPK of
+                        VersionedPKPayloadV4 pk ->
+                            signV4Message
+                                pk
+                                ( \hashed unhashed clear ->
+                                    signDataWithRSABuilder
+                                        ( applySubs
+                                            (sigBuilderInitTyped @'PKA.RSA RFC9580W BinarySig SHA512W)
+                                            hashed
+                                            unhashed
+                                        )
+                                        signingKey
+                                        clear
+                                )
+                                payload
+                        VersionedPKPayloadV6 pk ->
+                            signV6Message
+                                pk
+                                ( \salt hashed unhashed clear ->
+                                    signDataWithRSAV6Builder
+                                        ( applySubs
+                                            (sigBuilderInitV6Typed @'PKA.RSA RFC9580W BinarySig SHA512W salt)
+                                            hashed
+                                            unhashed
+                                        )
+                                        signingKey
+                                        clear
+                                )
+                                payload
+                Ed25519Signer signerPK signingKey ->
+                    case signerPK of
+                        VersionedPKPayloadV4 pk ->
+                            signV4Message
+                                pk
+                                ( \hashed unhashed clear ->
+                                    signDataWithEd25519Builder
+                                        ( applySubs
+                                            (sigBuilderInitTyped @'PKA.Ed25519 RFC9580W BinarySig SHA512W)
+                                            hashed
+                                            unhashed
+                                        )
+                                        signingKey
+                                        clear
+                                )
+                                payload
+                        VersionedPKPayloadV6 pk ->
+                            signV6Message
+                                pk
+                                ( \salt hashed unhashed clear ->
+                                    signDataWithEd25519V6Builder
+                                        ( applySubs
+                                            ( sigBuilderInitV6Typed @'PKA.Ed25519
+                                                RFC9580W
+                                                BinarySig
+                                                SHA512W
+                                                salt
+                                            )
+                                            hashed
+                                            unhashed
+                                        )
+                                        signingKey
+                                        clear
+                                )
+                                payload
+                Ed448Signer signerPK signingKey ->
+                    case signerPK of
+                        VersionedPKPayloadV4 pk ->
+                            signV4Message
+                                pk
+                                ( \hashed unhashed clear ->
+                                    signDataWithEd448Builder
+                                        ( applySubs
+                                            (sigBuilderInitTyped @'PKA.Ed448 RFC9580W BinarySig SHA512W)
+                                            hashed
+                                            unhashed
+                                        )
+                                        signingKey
+                                        clear
+                                )
+                                payload
+                        VersionedPKPayloadV6 pk ->
+                            signV6Message
+                                pk
+                                ( \salt hashed unhashed clear ->
+                                    signDataWithEd448V6Builder
+                                        ( applySubs
+                                            (sigBuilderInitV6Typed @'PKA.Ed448 RFC9580W BinarySig SHA512W salt)
+                                            hashed
+                                            unhashed
+                                        )
+                                        signingKey
+                                        clear
+                                )
+                                payload
 
 signMessage
     :: (MonadRandom m, SigningCapability alg v)
@@ -630,10 +707,6 @@
     -> m (Either MessageError BL.ByteString)
 signMessage signer = signMessageWith signer . mkClearPayload
 
-versionedPKPayload :: VersionedPKPayload v -> PKPayload v
-versionedPKPayload (VersionedPKPayloadV4 pk) = pk
-versionedPKPayload (VersionedPKPayloadV6 pk) = pk
-
 verifySignedMessage
     :: ConduitMessage.VerificationOptions
     -> PublicKeyring
@@ -732,16 +805,13 @@
     let clear = unClearPayload payload
         literal = LiteralDataPkt BinaryData BL.empty 0 clear
     signature <- signingFn hashed unhashed clear
-    return . runPut . put $ Block [literal, SignaturePkt signature]
-
-encryptOpenPGPCfb
-    :: SymmetricAlgorithm
-    -> IV
-    -> B.ByteString
-    -> WrappedSessionMaterial
-    -> Either CipherError B.ByteString
-encryptOpenPGPCfb sa iv cleartext (WrappedSessionMaterial keydata) =
-    encryptOpenPGPCfbRaw OpenPGPCFBResyncW sa iv cleartext keydata
+    let sigPkt = SignaturePkt signature
+    bimap
+        (SignBackendError . renderOPSBuildError)
+        ( \ops ->
+            runPut . put $ Block [OnePassSignaturePkt ops, literal, sigPkt]
+        )
+        (buildOnePassSignature False signature)
 
 extractEncryptedPayload
     :: [Pkt] -> Either MessageParseFailure SomeParsedEncryptedPayload
@@ -840,7 +910,7 @@
                     (BL.toStrict payload)
                 )
             )
-toSEIPDv2Prelude (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag)) payloadSA aead chunkSize salt payload
+toSEIPDv2Prelude (SKESKPayloadV6Packet (SKESKPayloadV6 sa aa s2k iv esk tag)) payloadSA _aead chunkSize salt payload
     | sa /= payloadSA = Left SKESKSEIPDAlgorithmMismatch
     | otherwise =
         Right
@@ -926,16 +996,8 @@
 decryptPayload
     :: Passphrase
     -> SomeParsedEncryptedPayload
-    -> Either String ClearPayload
-decryptPayload passphrase =
-    first renderMessageDecryptFailure
-        . decryptPayloadTyped passphrase
-
-decryptPayloadTyped
-    :: Passphrase
-    -> SomeParsedEncryptedPayload
     -> Either MessageDecryptFailure ClearPayload
-decryptPayloadTyped passphrase (SomeParsedEncryptedPayload payload) =
+decryptPayload passphrase (SomeParsedEncryptedPayload payload) =
     case payload of
         LegacySEDPayload skesk encryptedPayload ->
             decryptLegacySEDPayloadTyped passphrase skesk encryptedPayload
@@ -983,7 +1045,7 @@
         decryptCipherStep $
             decryptPreservingNonce sessionAlgorithm payload sessionKeyBytes
     cleartext <-
-        decryptPayloadStep $ validateSEIPD1MDC nonce decrypted
+        decryptMDCStep $ validateSEIPD1MDC nonce decrypted
     Right (ClearPayload (BL.fromStrict cleartext))
 
 decryptSEIPDv2PayloadTyped
@@ -998,7 +1060,7 @@
 decryptSEIPDv2PayloadTyped passphrase sa aead chunkSize salt skeskInfo payload = do
     sessionKey <-
         SessionKey <$> deriveSEIPDv2SessionKeyBytes passphrase skeskInfo
-    decryptPayloadStep $
+    decryptSEIPDv2Step $
         ClearPayload . BL.fromStrict
             <$> decryptSEIPDv2Payload sa aead chunkSize salt payload sessionKey
 
@@ -1010,8 +1072,8 @@
     deriveSessionKeyBytes passphrase sa s2k
 deriveSEIPDv2SessionKeyBytes passphrase (SEIPDv2SKESK6 sa aead s2k iv esk tag) = do
     ikm <- deriveSessionKeyBytes passphrase sa s2k
-    kek <- decryptPayloadStep $ deriveSKESK6KEK sa aead ikm
-    decryptPayloadStep $
+    kek <- decryptSEIPDv2Step $ deriveSKESK6KEK sa aead ikm
+    decryptSEIPDv2Step $
         decryptSKESK6SessionKey
             sa
             aead
@@ -1049,27 +1111,24 @@
             _ -> Right (pkt : acc)
 
 validateModernMessageS2K
-    :: OpenPGPPolicy -> S2K -> Either String ()
+    :: OpenPGPPolicy -> S2K -> Either MessageEncryptFailure ()
 validateModernMessageS2K policy s2k =
     case s2kHashAlgorithm s2k of
         Just ha
             | ha
                 `elem` deprecatedHashAlgorithms (policyGenerationDeprecations policy) ->
-                Left
-                    ( "deprecated hash algorithm disallowed for modern message generation: "
-                        ++ show ha
-                    )
+                Left (MessageEncryptDeprecatedS2KHash ha)
         _ -> Right ()
 
 validateRFC9580MessageSymmetric
-    :: OpenPGPPolicy -> SymmetricAlgorithm -> Either String ()
+    :: OpenPGPPolicy
+    -> SymmetricAlgorithm
+    -> Either MessageEncryptFailure ()
 validateRFC9580MessageSymmetric policy sa
     | supportsSEIPDv2Symmetric policy sa = Right ()
     | otherwise =
         Left
-            ( "symmetric algorithm disallowed for RFC9580 message generation: "
-                ++ show sa
-            )
+            (MessageEncryptUnsupportedSymmetricAlgorithm sa)
 
 s2kHashAlgorithm :: S2K -> Maybe HashAlgorithm
 s2kHashAlgorithm (Simple ha) = Just ha
diff --git a/Codec/Encryption/OpenPGP/Policy.hs b/Codec/Encryption/OpenPGP/Policy.hs
--- a/Codec/Encryption/OpenPGP/Policy.hs
+++ b/Codec/Encryption/OpenPGP/Policy.hs
@@ -64,13 +64,12 @@
     ) where
 
 import qualified Crypto.Hash as CH
-import qualified Crypto.Hash.Algorithms as CHAlg
+import qualified Crypto.Hash.Algorithms as CHA
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import qualified Crypto.PubKey.ECC.Types as ECCT
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import Data.Kind (Constraint)
-import Data.List (elem)
 import Data.Word (Word8)
 import GHC.TypeLits (ErrorMessage (..), TypeError)
 
@@ -535,12 +534,12 @@
 ecdhKdfHashDigest
     :: HashAlgorithm -> B.ByteString -> Either String B.ByteString
 ecdhKdfHashDigest SHA1 _ = Left "ECDH KDF hash algorithm SHA1 is disallowed by policy"
-ecdhKdfHashDigest SHA224 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA224))
-ecdhKdfHashDigest SHA256 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA256))
-ecdhKdfHashDigest SHA384 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA384))
-ecdhKdfHashDigest SHA512 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA512))
-ecdhKdfHashDigest SHA3_256 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA3_256))
-ecdhKdfHashDigest SHA3_512 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHAlg.SHA3_512))
+ecdhKdfHashDigest SHA224 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHA.SHA224))
+ecdhKdfHashDigest SHA256 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHA.SHA256))
+ecdhKdfHashDigest SHA384 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHA.SHA384))
+ecdhKdfHashDigest SHA512 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHA.SHA512))
+ecdhKdfHashDigest SHA3_256 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHA.SHA3_256))
+ecdhKdfHashDigest SHA3_512 bs = Right (BA.convert (CH.hash bs :: CH.Digest CHA.SHA3_512))
 ecdhKdfHashDigest _ _ = Left "ECDH KDF hash algorithm is unsupported"
 
 validateTable30PolicyForRecipient
diff --git a/Codec/Encryption/OpenPGP/SEIPDv1.hs b/Codec/Encryption/OpenPGP/SEIPDv1.hs
new file mode 100644
--- /dev/null
+++ b/Codec/Encryption/OpenPGP/SEIPDv1.hs
@@ -0,0 +1,87 @@
+-- SEIPDv1.hs: OpenPGP (RFC9580) legacy MDC/SEIPDv1
+-- Copyright © 2026  Clint Adams
+-- This software is released under the terms of the Expat license.
+-- (See the LICENSE file).
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
+
+module Codec.Encryption.OpenPGP.SEIPDv1
+    ( MDCFailure (..)
+    , mdcTrailerForSEIPDv1
+    , renderMDCFailure
+    , seipdv1NonceFromIV
+    , validateSEIPD1MDC
+    , calculateMDC
+    ) where
+
+import Control.Error.Util (note)
+import Control.Monad (when)
+import qualified Crypto.Hash as CH
+import qualified Crypto.Hash.Algorithms as CHA
+import qualified Data.ByteArray as BA
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as BL
+
+import Codec.Encryption.OpenPGP.Types
+
+{- | Compute the MDC trailer appended to SEIPDv1 plaintext before encryption.
+The trailer is: @0xd3 0x14 SHA1(nonce || plaintext || 0xd3 0x14)@.
+-}
+mdcTrailerForSEIPDv1 :: IV -> B.ByteString -> B.ByteString
+mdcTrailerForSEIPDv1 iv plaintext = mdcHeader <> digest
+  where
+    mdcHeader = B.pack [0xd3, 0x14]
+    nonce = seipdv1NonceFromIV iv
+    digest =
+        BA.convert
+            (CH.hash (nonce <> plaintext <> mdcHeader) :: CH.Digest CHA.SHA1)
+
+-- | The SEIPDv1 nonce: the IV bytes followed by its last two bytes (resync prefix).
+seipdv1NonceFromIV :: IV -> B.ByteString
+seipdv1NonceFromIV (IV ivBytes) = ivBytes <> B.drop (B.length ivBytes - 2) ivBytes
+
+calculateMDC
+    :: B.ByteString -> B.ByteString -> Maybe BL.ByteString
+calculateMDC nonce garbage
+    | B.length garbage < 23 = Nothing
+    | otherwise =
+        let digest =
+                CH.hash
+                    ( nonce
+                        <> B.take (B.length garbage - 22) garbage
+                        <> B.pack [211, 20]
+                    )
+                    :: CH.Digest CHA.SHA1
+         in Just (BL.fromStrict (BA.convert digest :: B.ByteString))
+
+data MDCFailure
+    = MDCTrailerMissing
+    | MDCTrailerCorrupted
+    | MDCDigestMismatch
+    deriving (Eq, Show)
+
+renderMDCFailure :: MDCFailure -> String
+renderMDCFailure MDCTrailerMissing = "MDC trailer missing"
+renderMDCFailure MDCTrailerCorrupted = "MDC trailer corrupted"
+renderMDCFailure MDCDigestMismatch = "MDC digest mismatch"
+
+{- | Verify the MDC trailer of a decrypted SEIPDv1 payload.
+Takes the CFB nonce (blockSize+2 prefix bytes retained from decryption)
+and the full decrypted bytes (payload + MDC packet), and returns the
+payload without the MDC trailer on success.
+-}
+validateSEIPD1MDC
+    :: B.ByteString -> B.ByteString -> Either MDCFailure B.ByteString
+validateSEIPD1MDC nonce decrypted = do
+    when (B.length decrypted < 22) $
+        Left MDCTrailerMissing
+    let (payload, trailer) = B.splitAt (B.length decrypted - 22) decrypted
+    when (B.take 2 trailer /= B.pack [211, 20]) $
+        Left MDCTrailerCorrupted
+    expectedMdc <-
+        note MDCTrailerMissing (calculateMDC nonce decrypted)
+    let actualMdc = BL.fromStrict (B.drop 2 trailer)
+    when (expectedMdc /= actualMdc) $
+        Left MDCDigestMismatch
+    Right payload
diff --git a/Codec/Encryption/OpenPGP/SEIPDv2.hs b/Codec/Encryption/OpenPGP/SEIPDv2.hs
new file mode 100644
--- /dev/null
+++ b/Codec/Encryption/OpenPGP/SEIPDv2.hs
@@ -0,0 +1,212 @@
+-- SEIPDv2.hs: OpenPGP (RFC9580) SEIPDv2 and SKESK v6 crypto helpers
+-- Copyright © 2012-2026  Clint Adams
+-- This software is released under the terms of the Expat license.
+-- (See the LICENSE file).
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PackageImports #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Codec.Encryption.OpenPGP.SEIPDv2
+    ( SEIPDv2Failure (..)
+    , aeadModeAndNonceSizeForSEIPDv2
+    , seipdv2SymmetricKeySize
+    , deriveSKESK6KEK
+    , encryptSKESK6SessionKey
+    , decryptSKESK6SessionKey
+    , renderSEIPDv2Failure
+    ) where
+
+import Control.Error.Util (note)
+import qualified Crypto.Error as CE
+import qualified Crypto.Hash.Algorithms as CHA
+import Crypto.KDF.HKDF (expand, extract)
+import Data.Bifunctor (first)
+import qualified Data.ByteArray as BA
+import qualified Data.ByteString as B
+import qualified "crypton" Crypto.Cipher.Types as CCT
+
+import Codec.Encryption.OpenPGP.BlockCipher
+    ( CipherError (..)
+    , renderCipherError
+    )
+import Codec.Encryption.OpenPGP.Internal.CryptoAES
+    ( withAESCipher
+    )
+import Codec.Encryption.OpenPGP.Internal.RFC7253OCB
+    ( decryptWithOCBRFC7253With
+    , encryptWithOCBRFC7253
+    )
+import Codec.Encryption.OpenPGP.S2K (S2KError, renderS2KError)
+import Codec.Encryption.OpenPGP.Types
+
+data SEIPDv2Failure
+    = SEIPDv2UnsupportedAEADAlgorithm AEADAlgorithm
+    | SEIPDv2UnsupportedSymmetricAlgorithm SymmetricAlgorithm
+    | SEIPDv2InvalidSaltLength
+    | SEIPDv2InvalidIVLength
+    | SEIPDv2InvalidChunkSize
+    | SEIPDv2CiphertextTooShort
+    | SEIPDv2MalformedChunkLengths
+    | SEIPDv2MissingFinalTag
+    | SEIPDv2ChunkAuthFailed AEADAlgorithm Int
+    | SEIPDv2FinalTagFailed AEADAlgorithm
+    | SEIPDv2AuthFailed
+    | SEIPDv2CipherInitFailed CE.CryptoError
+    | SEIPDv2CipherFailed CipherError
+    | SEIPDv2SessionKeyError S2KError
+    deriving (Eq, Show)
+
+renderSEIPDv2Failure :: SEIPDv2Failure -> String
+renderSEIPDv2Failure (SEIPDv2UnsupportedAEADAlgorithm EAX) =
+    "EAX is currently unsupported by the crypton AEAD backend"
+renderSEIPDv2Failure (SEIPDv2UnsupportedAEADAlgorithm alg) =
+    "unsupported AEAD algorithm: " ++ show alg
+renderSEIPDv2Failure (SEIPDv2UnsupportedSymmetricAlgorithm _) =
+    "SEIPD v2 encrypt currently supports AES-128/192/256 only"
+renderSEIPDv2Failure SEIPDv2InvalidSaltLength =
+    "SEIPD v2 salt must be exactly 32 octets"
+renderSEIPDv2Failure SEIPDv2InvalidIVLength =
+    "SKESK v6 IV length does not match AEAD algorithm"
+renderSEIPDv2Failure SEIPDv2InvalidChunkSize =
+    "SEIPD v2 chunk size octet must be between 0 and 16"
+renderSEIPDv2Failure SEIPDv2CiphertextTooShort =
+    "SEIPD v2 ciphertext must include at least one chunk tag and a final tag"
+renderSEIPDv2Failure SEIPDv2MalformedChunkLengths =
+    "SEIPD v2 malformed chunk lengths"
+renderSEIPDv2Failure SEIPDv2MissingFinalTag =
+    "SEIPD v2 missing final authentication tag"
+renderSEIPDv2Failure (SEIPDv2ChunkAuthFailed algo chunk) =
+    "AEAD chunk authentication failed for "
+        ++ show algo
+        ++ " at chunk "
+        ++ show chunk
+renderSEIPDv2Failure (SEIPDv2FinalTagFailed algo) =
+    "AEAD final tag verification failed for " ++ show algo
+renderSEIPDv2Failure SEIPDv2AuthFailed =
+    "SKESK v6 authentication failed"
+renderSEIPDv2Failure (SEIPDv2CipherInitFailed err) =
+    "AEAD initialization failed: " ++ show err
+renderSEIPDv2Failure (SEIPDv2CipherFailed err) =
+    "AEAD/cipher operation failed: " ++ renderCipherError err
+renderSEIPDv2Failure (SEIPDv2SessionKeyError err) =
+    renderS2KError err
+
+aeadModeAndNonceSizeForSEIPDv2
+    :: AEADAlgorithm -> Either SEIPDv2Failure (CCT.AEADMode, Int)
+aeadModeAndNonceSizeForSEIPDv2 EAX =
+    Left $ SEIPDv2UnsupportedAEADAlgorithm EAX
+aeadModeAndNonceSizeForSEIPDv2 OCB = Right (CCT.AEAD_OCB, 15)
+aeadModeAndNonceSizeForSEIPDv2 GCM = Right (CCT.AEAD_GCM, 12)
+aeadModeAndNonceSizeForSEIPDv2 (OtherAEADAlgo _) =
+    Left . SEIPDv2UnsupportedAEADAlgorithm $ OtherAEADAlgo 0
+
+seipdv2SymmetricKeySize
+    :: SymmetricAlgorithm -> Either SEIPDv2Failure Int
+seipdv2SymmetricKeySize symalgo =
+    case symalgo of
+        AES128 -> Right 16
+        AES192 -> Right 24
+        AES256 -> Right 32
+        _ -> Left $ SEIPDv2UnsupportedSymmetricAlgorithm symalgo
+
+skeskV6Info
+    :: SymmetricAlgorithm -> AEADAlgorithm -> B.ByteString
+skeskV6Info symalgo aead = B.pack [0xc3, 6, fromFVal symalgo, fromFVal aead]
+
+deriveSKESK6KEK
+    :: SymmetricAlgorithm
+    -> AEADAlgorithm
+    -> B.ByteString
+    -> Either SEIPDv2Failure B.ByteString
+deriveSKESK6KEK symalgo aead ikm = do
+    keyLen <- seipdv2SymmetricKeySize symalgo
+    let prk = extract @CHA.SHA256 B.empty ikm
+    pure (expand @CHA.SHA256 prk (skeskV6Info symalgo aead) keyLen)
+
+encryptSKESK6SessionKey
+    :: SymmetricAlgorithm
+    -> AEADAlgorithm
+    -> B.ByteString
+    -> B.ByteString
+    -> B.ByteString
+    -> Either SEIPDv2Failure (B.ByteString, B.ByteString)
+encryptSKESK6SessionKey symalgo aead kek iv sessionKey = do
+    (mode, nonceSize) <- aeadModeAndNonceSizeForSEIPDv2 aead
+    if B.length iv /= nonceSize
+        then Left SEIPDv2InvalidIVLength
+        else
+            withAESCipher
+                SEIPDv2CipherInitFailed
+                (SEIPDv2UnsupportedSymmetricAlgorithm symalgo)
+                symalgo
+                kek
+                ( \cipher ->
+                    if mode == CCT.AEAD_OCB
+                        then do
+                            (tag, ciphertext) <-
+                                encryptWithOCBRFC7253
+                                    cipher
+                                    iv
+                                    (skeskV6Info symalgo aead)
+                                    sessionKey
+                            pure (ciphertext, authTagToBS tag)
+                        else do
+                            aeadCtx <-
+                                first SEIPDv2CipherInitFailed . CE.eitherCryptoError $
+                                    CCT.aeadInit mode cipher iv
+                            let (tag, ciphertext) =
+                                    CCT.aeadSimpleEncrypt
+                                        aeadCtx
+                                        (skeskV6Info symalgo aead)
+                                        sessionKey
+                                        16
+                            pure (ciphertext, authTagToBS tag)
+                )
+
+decryptSKESK6SessionKey
+    :: SymmetricAlgorithm
+    -> AEADAlgorithm
+    -> B.ByteString
+    -> B.ByteString
+    -> B.ByteString
+    -> B.ByteString
+    -> Either SEIPDv2Failure B.ByteString
+decryptSKESK6SessionKey symalgo aead kek iv ciphertext tag = do
+    (mode, nonceSize) <- aeadModeAndNonceSizeForSEIPDv2 aead
+    if B.length iv /= nonceSize
+        then Left SEIPDv2InvalidIVLength
+        else
+            withAESCipher
+                SEIPDv2CipherInitFailed
+                (SEIPDv2UnsupportedSymmetricAlgorithm symalgo)
+                symalgo
+                kek
+                ( \cipher ->
+                    if mode == CCT.AEAD_OCB
+                        then
+                            decryptWithOCBRFC7253With
+                                (\_ _ _ _ _ _ -> SEIPDv2AuthFailed)
+                                cipher
+                                iv
+                                (skeskV6Info symalgo aead)
+                                ciphertext
+                                (mkAuthTag tag)
+                        else do
+                            aeadCtx <-
+                                first SEIPDv2CipherInitFailed . CE.eitherCryptoError $
+                                    CCT.aeadInit mode cipher iv
+                            note
+                                SEIPDv2AuthFailed
+                                ( CCT.aeadSimpleDecrypt
+                                    aeadCtx
+                                    (skeskV6Info symalgo aead)
+                                    ciphertext
+                                    (mkAuthTag tag)
+                                )
+                )
+
+authTagToBS :: CCT.AuthTag -> B.ByteString
+authTagToBS = BA.convert . CCT.unAuthTag
+
+mkAuthTag :: B.ByteString -> CCT.AuthTag
+mkAuthTag = CCT.AuthTag . BA.convert
diff --git a/Codec/Encryption/OpenPGP/SecretKey.hs b/Codec/Encryption/OpenPGP/SecretKey.hs
--- a/Codec/Encryption/OpenPGP/SecretKey.hs
+++ b/Codec/Encryption/OpenPGP/SecretKey.hs
@@ -9,7 +9,6 @@
 
 module Codec.Encryption.OpenPGP.SecretKey
     ( decryptPrivateKey
-    , reinterpretUnknownSKeyForPKPayload
     , mkUnencryptedSKAddendum
     , encryptPrivateKeyWithPolicyAndSaltAndIV
     , encryptPrivateKey
@@ -18,6 +17,7 @@
     , reencryptSecretKeyRandomEither
     , reencryptPrivateKeyTyped
     , SecretKeyError (..)
+    , renderSecretKeyError
     , SecretKeyEncryptOptions (..)
     , decryptSecretKey
     , decryptSecretKeyAddendum
@@ -28,11 +28,11 @@
     , changeSecretKeyPassphrase
     ) where
 
+import Control.Error.Util (note)
 import Control.Monad (when)
 import Control.Monad.Trans.Class (lift)
 import Control.Monad.Trans.Except
-    ( ExceptT
-    , except
+    ( except
     , runExceptT
     , throwE
     )
@@ -54,8 +54,7 @@
     , runGetOrFail
     )
 import Data.Binary.Put
-    ( Put
-    , putByteString
+    ( putByteString
     , putLazyByteString
     , putWord16be
     , runPut
@@ -65,7 +64,7 @@
 import qualified Data.ByteString.Base16 as B16
 import qualified Data.ByteString.Char8 as BC
 import qualified Data.ByteString.Lazy as BL
-import Data.List (nub)
+import Data.Containers.ListUtils (nubOrd)
 import Data.Word (Word16, Word8)
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
@@ -107,9 +106,6 @@
     , putSKeyForPKPayload
     )
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Base
-    ( Passphrase (..)
-    )
 
 data SecretKeyError
     = SecretKeyDecryptError String
@@ -118,6 +114,13 @@
     | SecretKeyUnsupportedLegacyProtection
     deriving (Eq, Show)
 
+renderSecretKeyError :: SecretKeyError -> String
+renderSecretKeyError (SecretKeyDecryptError err) = err
+renderSecretKeyError (SecretKeyEncryptError err) = err
+renderSecretKeyError (SecretKeyPolicyError err) = err
+renderSecretKeyError SecretKeyUnsupportedLegacyProtection =
+    "unsupported legacy secret key protection"
+
 data SecretKeyEncryptOptions = SecretKeyEncryptOptions
     { skeoPolicy :: OpenPGPPolicy
     , skeoGenerateSaltAndIV :: Bool
@@ -141,8 +144,9 @@
     decryptSecretKeyAddendum
         (_secretKeyPKPayload sk)
         (_secretKeySKAddendum sk)
-        pp >>= \(skey, _) ->
-        Right skey
+        pp
+        >>= \(skey, _) ->
+            Right skey
 
 decryptSecretKeyAddendum
     :: SomePKPayload
@@ -274,18 +278,19 @@
 reencryptWithPolicyAndSaltAndIV pkp originalSka salt iv skey pp policy =
     first
         SecretKeyEncryptError
-        (fromSKAddendumForPKPayload pkp originalSka) >>= \case
-        SomeSKAddendumV skaV ->
-            first SecretKeyEncryptError $
-                toSKAddendum
-                    <$> reencryptPrivateKeyTypedWithPolicy
-                        policy
-                        pkp
-                        skaV
-                        salt
-                        iv
-                        skey
-                        pp
+        (fromSKAddendumForPKPayload pkp originalSka)
+        >>= \case
+            SomeSKAddendumV skaV ->
+                first SecretKeyEncryptError $
+                    toSKAddendum
+                        <$> reencryptPrivateKeyTypedWithPolicy
+                            policy
+                            pkp
+                            skaV
+                            salt
+                            iv
+                            skey
+                            pp
 
 reencryptSecretKeyRandom
     :: MonadRandom m
@@ -399,26 +404,6 @@
 decryptPrivateKeyTyped _ ska@(SKAUnencryptedLegacy {}) _ = Right ska
 decryptPrivateKeyTyped _ ska@(SKAUnencryptedV6 {}) _ = Right ska
 
-reinterpretUnknownSKeyForPKPayload
-    :: SomePKPayload -> SKey -> Either String SKey
-reinterpretUnknownSKeyForPKPayload _ sk@RSAPrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@DSAPrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@ElGamalPrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@ECDHPrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@ECDSAPrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@EdDSAPrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@X25519PrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload _ sk@X448PrivateKey {} = Right sk
-reinterpretUnknownSKeyForPKPayload pkp (UnknownSKey payload) =
-    case runGetOrFail
-        ((,) <$> getSecretKey pkp <*> getRemainingLazyByteString)
-        payload of
-        Left (_, _, err) -> Left err
-        Right (_, _, (skey, trailing))
-            | BL.null trailing -> Right skey
-            | otherwise ->
-                Left "decoded secret key material has trailing bytes"
-
 mkUnencryptedSKAddendum
     :: SomePKPayload -> SKey -> Either String SKAddendum
 mkUnencryptedSKAddendum pkp skey = do
@@ -509,14 +494,14 @@
     let keyCandidates = [keyMaterial]
         tagCandidates = [0xC5, 0xC7, 0x94, 0x95, 0x96, 0x97, 0x9C, 0x9D, 0x9E, 0x9F]
         infoCandidates =
-            nub
+            nubOrd
                 [ B.pack
                     [tag, keyVersionByte (_keyVersion pkp), fromFVal sa, fromFVal aa]
                 | tag <- tagCandidates
                 ]
         pkpBytes = BL.toStrict (runPut (put pkp))
         adCandidates =
-            nub
+            nubOrd
                 [B.cons tagByte pkpBytes | tagByte <- tagCandidates]
         aaCandidates = [aa]
         nonce = unIV iv
@@ -529,7 +514,7 @@
                 authTag = CCT.AuthTag (BA.convert tagBytes)
                 prk = extract @CHA.SHA256 B.empty candidateKeyMaterial
                 kekCandidates =
-                    nub
+                    nubOrd
                         [ B.take keyLen candidateKeyMaterial
                         , (expand @CHA.SHA256 prk info keyLen :: B.ByteString)
                         , (expand @CHA.SHA256 prk B.empty keyLen :: B.ByteString)
@@ -601,6 +586,7 @@
     case aa of
         OCB ->
             withAESCipher
+                show
                 unsupportedSecretKeyAEADError
                 sa
                 kek
@@ -618,14 +604,14 @@
             expectedNonceLen <- aeadNonceSize aa
             when (B.length nonce /= expectedNonceLen) $
                 Left "invalid nonce size for v6 AEAD secret key payload"
-            withAESCipher unsupportedSecretKeyAEADError sa kek $ \cipher ->
+            withAESCipher show unsupportedSecretKeyAEADError sa kek $ \cipher ->
                 first
                     show
-                    (CE.eitherCryptoError (CCT.aeadInit mode cipher nonce)) >>= \aead ->
-                    maybe
-                        (Left "failed to authenticate v6 AEAD secret key payload")
-                        Right
-                        (CCT.aeadSimpleDecrypt aead ad ciphertext authTag)
+                    (CE.eitherCryptoError (CCT.aeadInit mode cipher nonce))
+                    >>= \aead ->
+                        note
+                            "failed to authenticate v6 AEAD secret key payload"
+                            (CCT.aeadSimpleDecrypt aead ad ciphertext authTag)
 
 aeadMode :: AEADAlgorithm -> Either String CCT.AEADMode
 aeadMode EAX = Right CCT.AEAD_EAX
@@ -775,6 +761,10 @@
             Right (runPut (put (MPI d)))
         EdDSAPrivateKey _ bs ->
             Right (runPut (put (MPI (os2ip bs))))
+        Ed25519PrivateKey bs ->
+            Right (runPut (putByteString bs))
+        Ed448PrivateKey bs ->
+            Right (runPut (putByteString bs))
         X25519PrivateKey bs ->
             Right (runPut (putByteString bs))
         X448PrivateKey bs ->
@@ -883,17 +873,19 @@
     case aa of
         OCB ->
             withAESCipher
+                show
                 unsupportedSecretKeyAEADError
                 sa
                 kek
                 (\cipher -> encryptWithOCBRFC7253 cipher nonce ad plaintext)
         _ -> do
             mode <- aeadMode aa
-            withAESCipher unsupportedSecretKeyAEADError sa kek $ \cipher ->
+            withAESCipher show unsupportedSecretKeyAEADError sa kek $ \cipher ->
                 first
                     show
-                    (CE.eitherCryptoError (CCT.aeadInit mode cipher nonce)) >>= \aead ->
-                    pure (CCT.aeadSimpleEncrypt aead ad plaintext 16)
+                    (CE.eitherCryptoError (CCT.aeadInit mode cipher nonce))
+                    >>= \aead ->
+                        pure (CCT.aeadSimpleEncrypt aead ad plaintext 16)
 
 {-# DEPRECATED
     reencryptSecretKeyRandomEither
@@ -1014,11 +1006,9 @@
     -> BL.ByteString
     -> Either String SKAddendum
 reencryptPrivateKeyWithSaltAndIV pkp originalSka salt iv skey pp =
-    case fromSKAddendumForPKPayload pkp originalSka of
-        Left err -> Left err
-        Right (SomeSKAddendumV skaV) ->
-            toSKAddendum
-                <$> reencryptPrivateKeyTyped pkp skaV salt iv skey pp
+    fromSKAddendumForPKPayload pkp originalSka >>= \(SomeSKAddendumV skaV) ->
+        toSKAddendum
+            <$> reencryptPrivateKeyTyped pkp skaV salt iv skey pp
 
 reencryptS2KProtectedSecretKey
     :: SomePKPayload
@@ -1043,70 +1033,6 @@
         first renderS2KError (string2Key retargetedS2K keyLen pp)
     cleartext <- legacySecretKeyPayload pkp skey
     encryptFn sa retargetedS2K iv cleartext keyMaterial
-
-encryptLegacyCFBSecretKey
-    :: SomePKPayload
-    -> SymmetricAlgorithm
-    -> IV
-    -> SKey
-    -> BL.ByteString
-    -> Either String SKAddendum
-encryptLegacyCFBSecretKey pkp sa iv skey pp = do
-    keyLen <- first renderCipherError (keySize sa)
-    keyMaterial <-
-        first
-            renderS2KError
-            (string2Key (Simple DeprecatedMD5) keyLen pp)
-    cleartext <- legacySecretKeyPayload pkp skey
-    let clearWithChecksum =
-            BL.toStrict
-                ( cleartext
-                    <> runPut (putWord16be (checksum16 (BL.toStrict cleartext)))
-                )
-    (\encrypted -> SUSym sa iv (BL.fromStrict encrypted))
-        <$> first
-            renderCipherError
-            ( encryptNoNonce
-                sa
-                (Simple DeprecatedMD5)
-                iv
-                clearWithChecksum
-                keyMaterial
-            )
-
-encrypt16BitProtectedSecretKey
-    :: SymmetricAlgorithm
-    -> S2K
-    -> IV
-    -> BL.ByteString
-    -> B.ByteString
-    -> Either String SKAddendum
-encrypt16BitProtectedSecretKey sa s2k iv cleartext keyMaterial =
-    encryptProtectedSecretKey
-        sa
-        s2k
-        iv
-        cleartext
-        keyMaterial
-        checksum16Trailer
-        (\payload -> SUS16bit sa s2k iv payload)
-
-encryptSHA1ProtectedSecretKey
-    :: SymmetricAlgorithm
-    -> S2K
-    -> IV
-    -> BL.ByteString
-    -> B.ByteString
-    -> Either String SKAddendum
-encryptSHA1ProtectedSecretKey sa s2k iv cleartext keyMaterial =
-    encryptProtectedSecretKey
-        sa
-        s2k
-        iv
-        cleartext
-        keyMaterial
-        sha1Trailer
-        (\payload -> SUSSHA1 sa s2k iv payload)
 
 encryptProtectedSecretKey
     :: SymmetricAlgorithm
diff --git a/Codec/Encryption/OpenPGP/Serialize.hs b/Codec/Encryption/OpenPGP/Serialize.hs
--- a/Codec/Encryption/OpenPGP/Serialize.hs
+++ b/Codec/Encryption/OpenPGP/Serialize.hs
@@ -1885,9 +1885,7 @@
 Validates constraints before calling putPkt to ensure errors are caught early.
 -}
 putPktEither :: Pkt -> Either String Put
-putPktEither pkt = case validatePkt pkt of
-    Left err -> Left err
-    Right () -> Right (putPkt pkt)
+putPktEither pkt = (putPkt pkt) <$ validatePkt pkt
 
 putLengthThenPayload :: ByteString -> Put
 putLengthThenPayload bs = do
@@ -1999,7 +1997,7 @@
     kdfHA <- get
     kdfSA <- get
     return $ ECDHPubKey ed kdfHA kdfSA
-getPubkey EdDSA = do
+getPubkey EdDSALegacy = do
     curvelength <- getWord8
     when (curvelength == 0 || curvelength == 0xff) $
         fail "invalid EdDSA curve OID length octet (reserved value)"
@@ -2027,7 +2025,7 @@
                 . os2ip
                 . BL.toStrict
             )
-            (getPubkey EdDSA)
+            (getPubkey EdDSALegacy)
 getPubkey pka
     | pka == BTypes.Ed448 =
         parseFixedLengthOrLegacyPubkey
@@ -2038,7 +2036,7 @@
                 . os2ip
                 . BL.toStrict
             )
-            (getPubkey EdDSA)
+            (getPubkey EdDSALegacy)
 getPubkey X25519 =
     parseFixedLengthOrLegacyPubkey
         32
@@ -2315,94 +2313,142 @@
 parseOPSNestedFlag other =
     fail ("invalid OPS nested flag octet: " ++ show other)
 
-getSecretKey :: SomePKPayload -> Get SKey
-getSecretKey pkp
-    | _pkalgo pkp
-        `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly] = do
-        MPI d <- get
-        MPI p <- get
-        MPI q <- get
-        MPI _ <- get -- u
-        case inverse q p of
-            Nothing -> fail "invalid RSA secret key: q has no inverse modulo p"
-            Just qinv -> do
-                let dP = d `mod` (p - 1)
-                    dQ = d `mod` (q - 1)
-                    pub = (\(RSAPubKey (RSA_PublicKey x)) -> x) (_pubkey pkp)
+getRSAPrivateKey :: SomePKPayload -> Get SKey
+getRSAPrivateKey pkp = do
+    MPI d <- get
+    MPI p <- get
+    MPI q <- get
+    MPI _ <- get -- u
+    case inverse q p of
+        Nothing -> fail "invalid RSA secret key: q has no inverse modulo p"
+        Just qinv -> do
+            let dP = d `mod` (p - 1)
+                dQ = d `mod` (q - 1)
+                pub = (\(RSAPubKey (RSA_PublicKey x)) -> x) (_pubkey pkp)
+            return $
+                RSAPrivateKey
+                    (RSA_PrivateKey (R.PrivateKey pub d p q dP dQ qinv))
+
+getDSAPrivateKey :: SomePKPayload -> Get SKey
+getDSAPrivateKey pkp = do
+    MPI x <- get
+    return $
+        DSAPrivateKey (DSA_PrivateKey (D.PrivateKey (D.Params 0 0 0) x))
+
+getElGamalPrivateKey :: SomePKPayload -> Get SKey
+getElGamalPrivateKey pkp = do
+    MPI x <- get
+    return $ ElGamalPrivateKey x
+
+getECDSAPrivateKey :: SomePKPayload -> Get SKey
+getECDSAPrivateKey pkp = do
+    let pubcurve =
+            (\(ECDSAPubKey (ECDSA_PublicKey p)) -> ECDSA.public_curve p)
+                (_pubkey pkp)
+    getECDSAScalarPrivateKey pubcurve
+
+getECDHPrivateKey :: SomePKPayload -> Get SKey
+getECDHPrivateKey pkp = do
+    pubcurve <- ecdhPrivateCurveFromPKPayload pkp
+    getECDHScalarPrivateKey pubcurve
+
+getX25519PrivateKey :: SomePKPayload -> Get SKey
+getX25519PrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        sk <- getByteString 32
+        return $ X25519PrivateKey sk
+    | otherwise = do
+        pubcurve <- ecdhPrivateCurveFromPKPayload pkp
+        getECDHScalarPrivateKey pubcurve
+
+getX448PrivateKey :: SomePKPayload -> Get SKey
+getX448PrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        sk <- getByteString 56
+        return $ X448PrivateKey sk
+    | otherwise = UnknownSKey <$> getRemainingLazyByteString
+
+getEdDSALegacyPrivateKey :: SomePKPayload -> Get SKey
+getEdDSALegacyPrivateKey pkp
+    | _keyVersion pkp == V6 =
+        UnknownSKey <$> getRemainingLazyByteString
+    | otherwise = do
+        MPI x <- get
+        case _pubkey pkp of
+            EdDSAPubKey P.EdSigningCurve25519 _ ->
                 return $
-                    RSAPrivateKey
-                        (RSA_PrivateKey (R.PrivateKey pub d p q dP dQ qinv))
-    | _pkalgo pkp == DSA = do
+                    EdDSAPrivateKey P.EdSigningCurve25519 (leftPadTo 32 (i2osp x))
+            EdDSAPubKey P.EdSigningCurve448 _ ->
+                return $
+                    EdDSAPrivateKey P.EdSigningCurve448 (leftPadTo 57 (i2osp x))
+            _ -> return $ UnknownSKey (BL.fromStrict (i2osp x))
+
+getMLKEMPrivateKey :: SomePKPayload -> Get SKey
+getMLKEMPrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        len <- getWord32be
+        bs <- getByteString (fromIntegral len)
+        return $ MLKEMPrivateKey bs
+    | otherwise = UnknownSKey <$> getRemainingLazyByteString
+
+getMLDSAPrivateKey :: SomePKPayload -> Get SKey
+getMLDSAPrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        len <- getWord32be
+        bs <- getByteString (fromIntegral len)
+        return $ MLDSAPrivateKey bs
+    | otherwise = UnknownSKey <$> getRemainingLazyByteString
+
+getSLHDSAPrivateKey :: SomePKPayload -> Get SKey
+getSLHDSAPrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        len <- getWord32be
+        bs <- getByteString (fromIntegral len)
+        return $ SLHDSAPrivateKey bs
+    | otherwise = UnknownSKey <$> getRemainingLazyByteString
+
+getEd25519PrivateKey :: SomePKPayload -> Get SKey
+getEd25519PrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        Ed25519PrivateKey <$> getByteString 32
+    | otherwise = do
         MPI x <- get
         return $
-            DSAPrivateKey (DSA_PrivateKey (D.PrivateKey (D.Params 0 0 0) x))
-    | _pkalgo pkp `elem` [ElgamalEncryptOnly, ForbiddenElgamal] = do
-        MPI x <- get
-        return $ ElGamalPrivateKey x
-    | _pkalgo pkp == ECDSA = do
-        let pubcurve =
-                (\(ECDSAPubKey (ECDSA_PublicKey p)) -> ECDSA.public_curve p)
-                    (_pubkey pkp)
-        getECDSAScalarPrivateKey pubcurve
-    | _pkalgo pkp == ECDH =
-        do
-            pubcurve <- ecdhPrivateCurveFromPKPayload pkp
-            getECDHScalarPrivateKey pubcurve
-    | _pkalgo pkp == X25519 = do
-        if _keyVersion pkp == V6
-            then do
-                sk <- getByteString 32
-                return $ X25519PrivateKey sk
-            else do
-                pubcurve <- ecdhPrivateCurveFromPKPayload pkp
-                getECDHScalarPrivateKey pubcurve
-    | _pkalgo pkp == X448 = do
-        if _keyVersion pkp == V6
-            then do
-                sk <- getByteString 56
-                return $ X448PrivateKey sk
-            else UnknownSKey <$> getRemainingLazyByteString
-    | _pkalgo pkp == EdDSA = do
-        if _keyVersion pkp == V6
-            then do
-                case _pubkey pkp of
-                    EdDSAPubKey P.EdSigningCurve25519 _ -> EdDSAPrivateKey P.EdSigningCurve25519 <$> getByteString 32
-                    EdDSAPubKey P.EdSigningCurve448 _ -> EdDSAPrivateKey P.EdSigningCurve448 <$> getByteString 57
-                    _ -> UnknownSKey <$> getRemainingLazyByteString
-            else do
-                MPI x <- get
-                case _pubkey pkp of
-                    EdDSAPubKey P.EdSigningCurve25519 _ ->
-                        return $
-                            EdDSAPrivateKey P.EdSigningCurve25519 (leftPadTo 32 (i2osp x))
-                    EdDSAPubKey P.EdSigningCurve448 _ ->
-                        return $
-                            EdDSAPrivateKey P.EdSigningCurve448 (leftPadTo 57 (i2osp x))
-                    _ -> return $ UnknownSKey (BL.fromStrict (i2osp x))
-    | _pkalgo pkp `elem` [MLKEM768X25519, MLKEM1024X448] = do
-        if _keyVersion pkp == V6
-            then do
-                len <- getWord32be
-                bs <- getByteString (fromIntegral len)
-                return $ MLKEMPrivateKey bs
-            else UnknownSKey <$> getRemainingLazyByteString
-    | _pkalgo pkp `elem` [MLDSA65Ed25519, MLDSA87Ed448] = do
-        if _keyVersion pkp == V6
-            then do
-                len <- getWord32be
-                bs <- getByteString (fromIntegral len)
-                return $ MLDSAPrivateKey bs
-            else UnknownSKey <$> getRemainingLazyByteString
-    | _pkalgo pkp
-        `elem` [SLHDSASHAKE128s, SLHDSASHAKE128f, SLHDSASHAKE256s] = do
-        if _keyVersion pkp == V6
-            then do
-                len <- getWord32be
-                bs <- getByteString (fromIntegral len)
-                return $ SLHDSAPrivateKey bs
-            else UnknownSKey <$> getRemainingLazyByteString
+            EdDSAPrivateKey P.EdSigningCurve25519 (leftPadTo 32 (i2osp x))
+
+getEd448PrivateKey :: SomePKPayload -> Get SKey
+getEd448PrivateKey pkp
+    | _keyVersion pkp == V6 = do
+        Ed448PrivateKey <$> getByteString 57
     | otherwise = UnknownSKey <$> getRemainingLazyByteString
 
+getSecretKey :: SomePKPayload -> Get SKey
+getSecretKey pkp = case _pkalgo pkp of
+    pka
+        | pka `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly] ->
+            getRSAPrivateKey pkp
+    DSA -> getDSAPrivateKey pkp
+    pka
+        | pka `elem` [ElgamalEncryptOnly, ForbiddenElgamal] ->
+            getElGamalPrivateKey pkp
+    ECDSA -> getECDSAPrivateKey pkp
+    ECDH -> getECDHPrivateKey pkp
+    X25519 -> getX25519PrivateKey pkp
+    X448 -> getX448PrivateKey pkp
+    EdDSALegacy -> getEdDSALegacyPrivateKey pkp
+    Ed25519 -> getEd25519PrivateKey pkp
+    Ed448 -> getEd448PrivateKey pkp
+    pka
+        | pka `elem` [MLKEM768X25519, MLKEM1024X448] ->
+            getMLKEMPrivateKey pkp
+    pka
+        | pka `elem` [MLDSA65Ed25519, MLDSA87Ed448] ->
+            getMLDSAPrivateKey pkp
+    pka
+        | pka `elem` [SLHDSASHAKE128s, SLHDSASHAKE128f, SLHDSASHAKE256s] ->
+            getSLHDSAPrivateKey pkp
+    _ -> UnknownSKey <$> getRemainingLazyByteString
+
 getECDSAScalarPrivateKey :: ECCT.Curve -> Get SKey
 getECDSAScalarPrivateKey curve = do
     MPI pn <- get
@@ -2446,8 +2492,9 @@
     Right (put (MPI d))
 putSKey (ECDSAPrivateKey (ECDSA_PrivateKey (ECDSA.PrivateKey _ d))) =
     Right (put (MPI d))
-putSKey (EdDSAPrivateKey P.EdSigningCurve25519 sk) = Right (putByteString sk)
-putSKey (EdDSAPrivateKey P.EdSigningCurve448 sk) = Right (putByteString sk)
+putSKey (EdDSAPrivateKey _ bs) = Right (put (MPI (os2ip bs)))
+putSKey (Ed25519PrivateKey sk) = Right (putByteString sk)
+putSKey (Ed448PrivateKey sk) = Right (putByteString sk)
 putSKey (X25519PrivateKey sk) = Right (putByteString sk)
 putSKey (X448PrivateKey sk) = Right (putByteString sk)
 putSKey (MLKEMPrivateKey sk) = Right (putLazyByteString (BL.fromStrict sk))
@@ -2456,9 +2503,9 @@
 putSKey (UnknownSKey bs) = Right (putLazyByteString bs)
 
 putSKeyForPKPayload :: SomePKPayload -> SKey -> Either String Put
-putSKeyForPKPayload pkp sk@(EdDSAPrivateKey _ bs)
-    | _keyVersion pkp == V6 = putSKey sk
-    | otherwise = Right (put (MPI (os2ip bs)))
+putSKeyForPKPayload _ sk@(EdDSAPrivateKey {}) = putSKey sk
+putSKeyForPKPayload _ sk@(Ed25519PrivateKey {}) = putSKey sk
+putSKeyForPKPayload _ sk@(Ed448PrivateKey {}) = putSKey sk
 putSKeyForPKPayload _ sk = putSKey sk
 
 putMPI :: MPI -> Put
@@ -3610,10 +3657,7 @@
 
 decodeSingleArmorPayload
     :: ByteString -> Either String ByteString
-decodeSingleArmorPayload bs =
-    case AA.decodeLazy bs of
-        Left err -> Left err
-        Right armors -> singleArmorPayload armors
+decodeSingleArmorPayload bs = singleArmorPayload =<< AA.decodeLazy bs
 
 decodeSingleArmorPayloadLenient
     :: ByteString -> Either String ByteString
diff --git a/Codec/Encryption/OpenPGP/SerializeForSigs.hs b/Codec/Encryption/OpenPGP/SerializeForSigs.hs
--- a/Codec/Encryption/OpenPGP/SerializeForSigs.hs
+++ b/Codec/Encryption/OpenPGP/SerializeForSigs.hs
@@ -34,7 +34,6 @@
 import Data.ByteString.Lazy (ByteString)
 import qualified Data.ByteString.Lazy as BL
 import Data.Text.Encoding (encodeUtf8)
-import Data.Word (Word8)
 
 import Codec.Encryption.OpenPGP.Internal
     ( PktStreamContext (..)
diff --git a/Codec/Encryption/OpenPGP/Signatures.hs b/Codec/Encryption/OpenPGP/Signatures.hs
--- a/Codec/Encryption/OpenPGP/Signatures.hs
+++ b/Codec/Encryption/OpenPGP/Signatures.hs
@@ -27,7 +27,6 @@
     , verifyAgainstPKPs
     , verifyAgainstPKPsWithPolicy
     , verifyTKWith
-    , verifyUnknownTKWith
     , signCertificationWithRSA
     , signDirectKeyWithRSA
     , signKeyRevocationWithRSA
@@ -58,8 +57,8 @@
 
 import Control.Applicative ((<|>))
 import Control.Error.Util (hush)
-import Control.Lens ((&), (^.), _1)
-import Control.Monad (liftM2, when)
+import Control.Lens ((^.))
+import Control.Monad (liftM2)
 import Crypto.Error (eitherCryptoError)
 import Crypto.Hash (hashWith)
 import qualified Crypto.Hash.Algorithms as CHA
@@ -76,15 +75,14 @@
 import qualified Data.ByteString as B
 import Data.ByteString.Lazy (ByteString)
 import qualified Data.ByteString.Lazy as BL
+import Data.Containers.ListUtils (nubOrd)
 import Data.Either (isRight, lefts, rights)
-import Data.Function (on)
 import Data.IxSet.Typed ((@=))
 import qualified Data.IxSet.Typed as IxSet
-import Data.List (find, intercalate, nub)
+import Data.List (find, intercalate)
 import Data.List.NonEmpty (NonEmpty (..))
 import qualified Data.List.NonEmpty as NE
-import qualified Data.Map.Strict as Map
-import Data.Maybe (isJust, mapMaybe)
+import Data.Maybe (isJust)
 import qualified Data.Set as Set
 import Data.Text (Text)
 import Data.Time.Clock (UTCTime (..), addUTCTime, diffUTCTime)
@@ -120,11 +118,8 @@
     )
 import Codec.Encryption.OpenPGP.Policy
     ( VerificationPolicy (..)
-    , VerificationPolicyAction (..)
     , applyVerificationPolicy
     , defaultVerificationPolicy
-    , isVerificationError
-    , isVerificationWarning
     , signatureV6SaltSizeForHashAlgorithm
     )
 import Codec.Encryption.OpenPGP.SerializeForSigs
@@ -156,9 +151,6 @@
 import qualified Codec.Encryption.OpenPGP.Subpackets as SP
 import Codec.Encryption.OpenPGP.Types
 import qualified Codec.Encryption.OpenPGP.Types.Internal.Base as PKA
-import Codec.Encryption.OpenPGP.Types.Internal.Pkt
-    ( VerificationWarning (..)
-    )
 import Data.Conduit.OpenPGP.Keyring.Instances ()
 
 data VerificationError
@@ -288,7 +280,7 @@
     "verification failed: signature expired"
 renderVerificationError (CandidateKeyFailures errs) =
     "verification failed: no candidate key validated the signature ("
-        ++ intercalate "; " (nub (map renderVerificationError errs))
+        ++ intercalate "; " (nubOrd $ map renderVerificationError errs)
         ++ ")"
 renderVerificationError (InvalidSubkeyBackSignature err) =
     "verification failed: embedded primary-key back-signature verification failed: "
@@ -310,10 +302,6 @@
     :: VerificationError -> Either VerificationError a
 verificationError = Left
 
-renderVerificationResult
-    :: Either VerificationError a -> Either String a
-renderVerificationResult = first renderVerificationError
-
 data SignError
     = SignBackendError String
     | SignUnsupportedCertificationType SigType
@@ -367,13 +355,6 @@
 isVerifiableSignaturePayload :: SignaturePayload -> Bool
 isVerifiableSignaturePayload = isJust . fromSignaturePayloadVerifiableSignatureV
 
-toSignaturePayloadFromVerifiable
-    :: VerifiableSignatureV -> SignaturePayload
-toSignaturePayloadFromVerifiable (VerifiableSignatureV4 payload) =
-    toSignaturePayload payload
-toSignaturePayloadFromVerifiable (VerifiableSignatureV6 payload) =
-    toSignaturePayload payload
-
 fromPktEitherVerifiableSignatureV
     :: Pkt -> Either VerificationError VerifiableSignatureV
 fromPktEitherVerifiableSignatureV (SignaturePkt sigPayload) =
@@ -656,20 +637,7 @@
     -> Maybe UTCTime
     -> TK k
     -> Either VerificationError (TK k)
-verifyTKWith vsf mt tk =
-    verifyUnknownTKWith vsf mt tk
-
-{-# DEPRECATED verifyUnknownTKWith "Use verifyTKWith instead" #-}
-verifyUnknownTKWith
-    :: ( Pkt
-         -> PktStreamContext
-         -> Maybe UTCTime
-         -> Either VerificationError Verification
-       )
-    -> Maybe UTCTime
-    -> TK k
-    -> Either VerificationError (TK k)
-verifyUnknownTKWith vsf mt tk = do
+verifyTKWith vsf mt tk = do
     revokers <- checkRevokers tk
     revs <- checkKeyRevocations revokers tk
     let uids = filter (not . null . snd) . checkUidSigs $ tk ^. tkUIDs
@@ -1013,7 +981,7 @@
     ([], filter matchesP (candidatePKPs tk))
 resolveCandidateSignerPKPs allKeys _ (Just validationTime) matchesP tk =
     let rawMatches = filter matchesP (candidatePKPs tk)
-     in case verifyUnknownTKWith
+     in case verifyTKWith
             ( verifySigWith
                 defaultVerificationPolicy
                 (verifyAgainstKeys allKeys)
@@ -1025,12 +993,11 @@
                 let verifiedMatches =
                         map
                             ( \pkp ->
-                                case historicallyValidSigner
-                                    validationTime
-                                    (timelineValidationTK pkp verifiedTK)
-                                    pkp of
-                                    Right () -> Right pkp
-                                    Left err -> Left err
+                                pkp
+                                    <$ historicallyValidSigner
+                                        validationTime
+                                        (timelineValidationTK pkp verifiedTK)
+                                        pkp
                             )
                             rawMatches
                  in (lefts verifiedMatches, rights verifiedMatches)
@@ -1209,20 +1176,6 @@
 subkeyPKPFromPkt (SecretSubkeyPkt p _) = Just p
 subkeyPKPFromPkt _ = Nothing
 
-verifyAgainstKey'
-    :: SomePKPayload
-    -> Pkt
-    -> Maybe UTCTime
-    -> ByteString
-    -> Either VerificationError Verification
-verifyAgainstKey' pkp sig mt payload =
-    verifyAgainstKeyWithPolicy
-        defaultVerificationPolicy
-        pkp
-        sig
-        mt
-        payload
-
 {- | Verify a signature against a key with a custom verification policy.
 This allows callers to control whether certain signature features
 (deprecated hash algorithms, PKA mismatches, etc.) are treated as
@@ -1309,12 +1262,12 @@
             `elem` [RSA, DeprecatedRSAEncryptOnly, DeprecatedRSASignOnly]
     pkaCompatible DeprecatedRSASignOnly keyPka =
         keyPka `elem` [RSA, DeprecatedRSASignOnly]
-    pkaCompatible PKA.EdDSA keyPka =
-        keyPka `elem` [PKA.EdDSA, PKA.Ed25519, PKA.Ed448]
+    pkaCompatible PKA.EdDSALegacy keyPka =
+        keyPka `elem` [PKA.EdDSALegacy, PKA.Ed25519, PKA.Ed448]
     pkaCompatible PKA.Ed25519 keyPka =
-        keyPka `elem` [PKA.EdDSA, PKA.Ed25519]
+        keyPka `elem` [PKA.EdDSALegacy, PKA.Ed25519]
     pkaCompatible PKA.Ed448 keyPka =
-        keyPka `elem` [PKA.EdDSA, PKA.Ed448]
+        keyPka `elem` [PKA.EdDSALegacy, PKA.Ed448]
     pkaCompatible sigPka keyPka = sigPka == keyPka
     verify' details pub@(PKPayload V4 _ _ _ pkey) ha pl =
         verifyByHash details pub pkey ha pl
@@ -1355,10 +1308,10 @@
     verifyNoRSA _ (pka, _) _ _ _ _ = verificationError (UnsupportedKeyType pka)
     edVerify sigPka pub mpis hd key bs = case key of
         EdDSAPubKey EdSigningCurve25519 pkey
-            | sigPka `elem` [EdDSA, PKA.Ed25519] ->
+            | sigPka `elem` [EdDSALegacy, PKA.Ed25519] ->
                 ed25519Verify sigPka pub mpis hd pkey bs
         EdDSAPubKey EdSigningCurve448 pkey
-            | sigPka `elem` [EdDSA, PKA.Ed448] ->
+            | sigPka `elem` [EdDSALegacy, PKA.Ed448] ->
                 ed448Verify sigPka pub mpis hd pkey bs
         _ -> verificationError (UnsupportedKeyType sigPka)
     dsaVerify pub (r :| [s]) hd pkey bs =
@@ -2020,7 +1973,7 @@
     , ed448Params
         :: (String, Int, Int, PubKeyAlgorithm)
 ed25519Params = ("Ed25519", 64, 32, PKA.Ed25519)
-ed25519LegacyParams = ("Ed25519Legacy", 64, 32, PKA.EdDSA)
+ed25519LegacyParams = ("Ed25519Legacy", 64, 32, PKA.EdDSALegacy)
 ed448Params = ("Ed448", 114, 57, PKA.Ed448)
 
 signDataWithEdDSAV4Generic
diff --git a/Codec/Encryption/OpenPGP/Subpackets.hs b/Codec/Encryption/OpenPGP/Subpackets.hs
--- a/Codec/Encryption/OpenPGP/Subpackets.hs
+++ b/Codec/Encryption/OpenPGP/Subpackets.hs
@@ -2,7 +2,6 @@
 -- Copyright © 2012-2026  Clint Adams
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
-
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE KindSignatures #-}
@@ -13,105 +12,104 @@
 {-# LANGUAGE TypeFamilies #-}
 
 module Codec.Encryption.OpenPGP.Subpackets
-  ( -- * Safe subpacket construction
-    SafeSubpacket
-  , mkSafeSubpacket
-  , safePayload
-  , safeSubpacket
-
-    -- * Critical subpacket handling
-  , CriticalSubpacket
-  , mkCritical
-  , canBeCritical
+    ( -- * Safe subpacket construction
+      SafeSubpacket
+    , mkSafeSubpacket
+    , safePayload
+    , safeSubpacket
 
-    -- * Text normalization mode
-  , TextNormalizationMode(..)
+      -- * Critical subpacket handling
+    , CriticalSubpacket
+    , mkCritical
+    , canBeCritical
 
-    -- * Builder API for signature composition
-  , SigBuilder
-  , LegalSubpacket(..)
-  , legalSubpacket
-  , singleLegalSub
-  , consLegalSub
-  , listToLegalSubs
-  , sbSigType
-  , sbPubKeyAlgo
-  , sbHashAlgo
-  , sbHashedSubs
-  , sbUnhashedSubs
-  , sbSalt
-  , sbTextNormMode
-  , buildSigV4
-  , buildSigV6
-  , sigBuilderInit
-  , sigBuilderInitTyped
-  , sigBuilderInitRuntime
-  , sigBuilderInitV6
-  , sigBuilderInitV6Typed
-  , sigBuilderInitV6Runtime
-  , addHashedSubs
-  , addUnhashedSubs
-  , KnownPubKeyAlgorithm(..)
+      -- * Text normalization mode
+    , TextNormalizationMode (..)
 
-    -- * Private key wrapper (algorithm-specific)
-  , PrivateKeyFor(..)
+      -- * Builder API for signature composition
+    , SigBuilder
+    , LegalSubpacket (..)
+    , legalSubpacket
+    , singleLegalSub
+    , consLegalSub
+    , listToLegalSubs
+    , sbSigType
+    , sbPubKeyAlgo
+    , sbHashAlgo
+    , sbHashedSubs
+    , sbUnhashedSubs
+    , sbSalt
+    , sbTextNormMode
+    , buildSigV4
+    , buildSigV6
+    , sigBuilderInit
+    , sigBuilderInitTyped
+    , sigBuilderInitRuntime
+    , sigBuilderInitV6
+    , sigBuilderInitV6Typed
+    , sigBuilderInitV6Runtime
+    , addHashedSubs
+    , addUnhashedSubs
+    , KnownPubKeyAlgorithm (..)
 
-    -- * Subpacket list utilities
-  , HashedSubpackets
-  , UnhashedSubpackets
-  , consHashedSub
-  , singleHashedSub
-  , singleUnhashedSub
-  , listToHashedSubs
-  , listToUnhashedSubs
-  ) where
+      -- * Private key wrapper (algorithm-specific)
+    , PrivateKeyFor (..)
 
-import Data.Kind (Type)
-import Data.List.NonEmpty (NonEmpty)
-import Data.Proxy (Proxy(..))
-import Data.Word (Word16)
+      -- * Subpacket list utilities
+    , HashedSubpackets
+    , UnhashedSubpackets
+    , consHashedSub
+    , singleHashedSub
+    , singleUnhashedSub
+    , listToHashedSubs
+    , listToUnhashedSubs
+    ) where
 
 import qualified Crypto.PubKey.DSA as DSA
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import qualified Crypto.PubKey.Ed25519 as Ed25519
 import qualified Crypto.PubKey.Ed448 as Ed448
 import qualified Crypto.PubKey.RSA.Types as RSATypes
+import Data.Kind (Type)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Proxy (Proxy (..))
+import Data.Word (Word16)
 
+import Codec.Encryption.OpenPGP.Policy
+    ( HashAlgoAllowedFor
+    , HashAlgorithmW (..)
+    , OpenPGPRFC
+    , OpenPGPRFCW (..)
+    , SomeHashAlgorithmW (..)
+    , SomeOpenPGPRFCW (..)
+    , demoteHashAlgorithmW
+    , deprecatedHashAlgorithms
+    , policyForRFC
+    , policyGenerationDeprecations
+    , promoteHashAlgorithm
+    , promoteOpenPGPRFC
+    )
 import Codec.Encryption.OpenPGP.Types
-  ( Hashed
-  , Unhashed
-  , V4Sig
-  , V6Sig
-  , Fingerprint
-  , EightOctetKeyId
-  , ThirtyTwoBitTimeStamp
-  , MPI
-  , SigSubPacket(..)
-  , SigSubPacketPayload(..)
-  , SignaturePayload(..)
-  , SignatureSalt
-  , SigType
-  , HashAlgorithm
-  , SubpacketList(..)
-  )
+    ( EightOctetKeyId
+    , Fingerprint
+    , HashAlgorithm
+    , Hashed
+    , MPI
+    , SigSubPacket (..)
+    , SigSubPacketPayload (..)
+    , SigType
+    , SignaturePayload (..)
+    , SignatureSalt
+    , SubpacketList (..)
+    , ThirtyTwoBitTimeStamp
+    , Unhashed
+    , V4Sig
+    , V6Sig
+    )
 import Codec.Encryption.OpenPGP.Types.Internal.Base
-  ( PubKeyAlgorithm(..)
-  , IssuerFingerprintVersion(..)
-  )
-import Codec.Encryption.OpenPGP.Policy
-  ( HashAlgoAllowedFor
-  , OpenPGPRFC
-  , OpenPGPRFCW(..)
-  , SomeOpenPGPRFCW(..)
-  , HashAlgorithmW(..)
-  , SomeHashAlgorithmW(..)
-  , promoteOpenPGPRFC
-  , promoteHashAlgorithm
-  , demoteHashAlgorithmW
-  , policyForRFC
-  , policyGenerationDeprecations
-  , deprecatedHashAlgorithms
-  )
+    ( IssuerFingerprintVersion (..)
+    , PubKeyAlgorithm (..)
+    )
 
 -- | Type alias for readability: hashed subpacket list
 type HashedSubpackets v = SubpacketList Hashed v
@@ -119,19 +117,24 @@
 -- | Type alias for readability: unhashed subpacket list
 type UnhashedSubpackets v = SubpacketList Unhashed v
 
--- | A subpacket that has passed basic safety checks
--- (does not guarantee it's critical-safe; use CriticalSubpacket for that)
+{- | A subpacket that has passed basic safety checks
+(does not guarantee it's critical-safe; use CriticalSubpacket for that)
+-}
 newtype SafeSubpacket = SafeSubpacket SigSubPacket
-  deriving (Eq, Ord, Show)
+    deriving (Eq, Ord, Show)
 
--- | Create a safe subpacket from a payload and criticality flag
--- Basic validation that the payload is well-formed
-mkSafeSubpacket :: Bool -> SigSubPacketPayload -> Either String SafeSubpacket
+{- | Create a safe subpacket from a payload and criticality flag
+Basic validation that the payload is well-formed
+-}
+mkSafeSubpacket
+    :: Bool -> SigSubPacketPayload -> Either String SafeSubpacket
 mkSafeSubpacket crit payload = do
-  -- Validate criticality constraints: some subpackets should never be critical
-  if crit && not (canBeCritical payload)
-    then Left $ "Subpacket type cannot be marked critical: " ++ show payload
-    else Right (SafeSubpacket (SigSubPacket crit payload))
+    -- Validate criticality constraints: some subpackets should never be critical
+    if crit && not (canBeCritical payload)
+        then
+            Left $
+                "Subpacket type cannot be marked critical: " ++ show payload
+        else Right (SafeSubpacket (SigSubPacket crit payload))
 
 -- | Extract the payload from a safe subpacket
 safePayload :: SafeSubpacket -> SigSubPacketPayload
@@ -141,17 +144,22 @@
 safeSubpacket :: SafeSubpacket -> SigSubPacket
 safeSubpacket (SafeSubpacket ssp) = ssp
 
--- | A subpacket that is guaranteed to be both well-formed AND can be safely marked critical
--- Used to prevent accidentally marking non-critical-safe types as critical
+{- | A subpacket that is guaranteed to be both well-formed AND can be safely marked critical
+Used to prevent accidentally marking non-critical-safe types as critical
+-}
 newtype CriticalSubpacket = CriticalSubpacket SigSubPacket
-  deriving (Eq, Ord, Show)
+    deriving (Eq, Ord, Show)
 
--- | Create a critical subpacket only if the payload type allows it
--- RFC9580 section 5.2.3.5: only certain subpacket types can be marked critical
-mkCritical :: SigSubPacketPayload -> Either String CriticalSubpacket
+{- | Create a critical subpacket only if the payload type allows it
+RFC9580 section 5.2.3.5: only certain subpacket types can be marked critical
+-}
+mkCritical
+    :: SigSubPacketPayload -> Either String CriticalSubpacket
 mkCritical payload
-  | canBeCritical payload = Right $ CriticalSubpacket (SigSubPacket True payload)
-  | otherwise = Left $ "Cannot mark as critical: " ++ payloadType payload
+    | canBeCritical payload =
+        Right $ CriticalSubpacket (SigSubPacket True payload)
+    | otherwise =
+        Left $ "Cannot mark as critical: " ++ payloadType payload
   where
     payloadType (SigCreationTime _) = "SigCreationTime"
     payloadType (SigExpirationTime _) = "SigExpirationTime"
@@ -192,47 +200,52 @@
 canBeCritical OtherSigSub {} = True
 canBeCritical _ = False
 
--- | Wrapper GADT for algorithm-specific private keys
--- Encodes the algorithm at the type level to ensure type-safe key/builder matching
+{- | Wrapper GADT for algorithm-specific private keys
+Encodes the algorithm at the type level to ensure type-safe key/builder matching
+-}
 data PrivateKeyFor (algo :: PubKeyAlgorithm) where
-  RSAPrivateKey :: RSATypes.PrivateKey -> PrivateKeyFor 'RSA
-  Ed25519PrivateKey :: Ed25519.SecretKey -> PrivateKeyFor 'Ed25519
-  Ed448PrivateKey :: Ed448.SecretKey -> PrivateKeyFor 'Ed448
-  DSAPrivateKey :: DSA.PrivateKey -> PrivateKeyFor 'DSA
-  ECDSAPrivateKey :: ECDSA.PrivateKey -> PrivateKeyFor 'ECDSA
+    RSAPrivateKey :: RSATypes.PrivateKey -> PrivateKeyFor 'RSA
+    Ed25519PrivateKey :: Ed25519.SecretKey -> PrivateKeyFor 'Ed25519
+    Ed448PrivateKey :: Ed448.SecretKey -> PrivateKeyFor 'Ed448
+    DSAPrivateKey :: DSA.PrivateKey -> PrivateKeyFor 'DSA
+    ECDSAPrivateKey :: ECDSA.PrivateKey -> PrivateKeyFor 'ECDSA
 
 class KnownPubKeyAlgorithm (algo :: PubKeyAlgorithm) where
-  demotePubKeyAlgorithmT :: Proxy algo -> PubKeyAlgorithm
+    demotePubKeyAlgorithmT :: Proxy algo -> PubKeyAlgorithm
 
 instance KnownPubKeyAlgorithm 'RSA where
-  demotePubKeyAlgorithmT _ = RSA
+    demotePubKeyAlgorithmT _ = RSA
 
 instance KnownPubKeyAlgorithm 'DSA where
-  demotePubKeyAlgorithmT _ = DSA
+    demotePubKeyAlgorithmT _ = DSA
 
 instance KnownPubKeyAlgorithm 'ECDSA where
-  demotePubKeyAlgorithmT _ = ECDSA
+    demotePubKeyAlgorithmT _ = ECDSA
 
-instance KnownPubKeyAlgorithm 'EdDSA where
-  demotePubKeyAlgorithmT _ = EdDSA
+instance KnownPubKeyAlgorithm 'EdDSALegacy where
+    demotePubKeyAlgorithmT _ = EdDSALegacy
 
 instance KnownPubKeyAlgorithm 'Ed25519 where
-  demotePubKeyAlgorithmT _ = Ed25519
+    demotePubKeyAlgorithmT _ = Ed25519
 
 instance KnownPubKeyAlgorithm 'Ed448 where
-  demotePubKeyAlgorithmT _ = Ed448
+    demotePubKeyAlgorithmT _ = Ed448
 
--- | Legal subpackets encoded by placement and signature-version constraints.
--- This expands compile-time legality beyond bare hashed/unhashed staging.
+{- | Legal subpackets encoded by placement and signature-version constraints.
+This expands compile-time legality beyond bare hashed/unhashed staging.
+-}
 data LegalSubpacket (h :: Type) (v :: Type) where
-  -- RFC9580: signature creation time is a hashed subpacket.
-  LegalSigCreationTime :: ThirtyTwoBitTimeStamp -> LegalSubpacket Hashed v
-  -- v4 signatures use an issuer fingerprint subpacket with version marker 4 in hashed area.
-  LegalIssuerFingerprintV4 :: Fingerprint -> LegalSubpacket Hashed V4Sig
-  -- v6 signatures use an issuer fingerprint subpacket with version marker 6 in hashed area.
-  LegalIssuerFingerprintV6 :: Fingerprint -> LegalSubpacket Hashed V6Sig
-  -- Legacy issuer key ID subpacket is accepted only in v4 and only unhashed.
-  LegalIssuerV4 :: EightOctetKeyId -> LegalSubpacket Unhashed V4Sig
+    -- RFC9580: signature creation time is a hashed subpacket.
+    LegalSigCreationTime
+        :: ThirtyTwoBitTimeStamp -> LegalSubpacket Hashed v
+    -- v4 signatures use an issuer fingerprint subpacket with version marker 4 in hashed area.
+    LegalIssuerFingerprintV4
+        :: Fingerprint -> LegalSubpacket Hashed V4Sig
+    -- v6 signatures use an issuer fingerprint subpacket with version marker 6 in hashed area.
+    LegalIssuerFingerprintV6
+        :: Fingerprint -> LegalSubpacket Hashed V6Sig
+    -- Legacy issuer key ID subpacket is accepted only in v4 and only unhashed.
+    LegalIssuerV4 :: EightOctetKeyId -> LegalSubpacket Unhashed V4Sig
 
 legalSubpacket :: LegalSubpacket h v -> SigSubPacket
 legalSubpacket (LegalSigCreationTime ts) = SigSubPacket False (SigCreationTime ts)
@@ -243,7 +256,8 @@
 singleLegalSub :: LegalSubpacket h v -> SubpacketList h v
 singleLegalSub = SubpacketList . (: []) . legalSubpacket
 
-consLegalSub :: LegalSubpacket h v -> SubpacketList h v -> SubpacketList h v
+consLegalSub
+    :: LegalSubpacket h v -> SubpacketList h v -> SubpacketList h v
 consLegalSub legal (SubpacketList sps) = SubpacketList (legalSubpacket legal : sps)
 
 listToLegalSubs :: [LegalSubpacket h v] -> SubpacketList h v
@@ -258,248 +272,288 @@
 singleUnhashedSub sp = SubpacketList [sp]
 
 -- | Prepend a subpacket to a hashed subpacket list
-consHashedSub :: SigSubPacket -> HashedSubpackets v -> HashedSubpackets v
+consHashedSub
+    :: SigSubPacket -> HashedSubpackets v -> HashedSubpackets v
 consHashedSub sp (SubpacketList sps) = SubpacketList (sp : sps)
 
--- | Controls how text payload is normalized for CanonicalTextSig.
---
--- RFC 9580 §5.2.1.2 requires only CRLF normalization for type 0x01
--- signatures.  Trailing-whitespace stripping is only mandated by
--- §7 (Cleartext Signature Framework).  Use 'CleartextCompat' for
--- GnuPG-compatible behaviour when producing cleartext-armored
--- signatures; use 'RFC9580Strict' for inline text signatures.
+{- | Controls how text payload is normalized for CanonicalTextSig.
+
+RFC 9580 §5.2.1.2 requires only CRLF normalization for type 0x01
+signatures.  Trailing-whitespace stripping is only mandated by
+§7 (Cleartext Signature Framework).  Use 'CleartextCompat' for
+GnuPG-compatible behaviour when producing cleartext-armored
+signatures; use 'RFC9580Strict' for inline text signatures.
+-}
 data TextNormalizationMode
-  = RFC9580Strict
-    -- ^ CRLF normalization only; no trailing-whitespace stripping.
-    --   Correct for inline type 0x01 document signatures.
-  | CleartextCompat
-    -- ^ CRLF normalization *plus* per-line trailing-whitespace stripping.
-    --   Required by the Cleartext Signature Framework (RFC 9580 §7).
-  deriving (Eq, Show)
+    = {- | CRLF normalization only; no trailing-whitespace stripping.
+      Correct for inline type 0x01 document signatures.
+      -}
+      RFC9580Strict
+    | {- | CRLF normalization *plus* per-line trailing-whitespace stripping.
+      Required by the Cleartext Signature Framework (RFC 9580 §7).
+      -}
+      CleartextCompat
+    deriving (Eq, Show)
 
--- | Staged signature builder that enforces completion order
--- 
--- Usage pattern:
---   builder <- sigBuilderInit SigTypeBinary RSA SHA256
---   builder' <- addHashedSubs hashedList builder
---   finalPayload <- buildSigV4 sigMPIs (addUnhashedSubs unhashedList builder')
-data SigBuilder (hashedness :: Type) (v :: Type) (algo :: PubKeyAlgorithm) = SigBuilder
-  { sbSigType :: SigType
-  , sbHashAlgo :: HashAlgorithm
-  , sbHashedSubs :: [SigSubPacket]
-  , sbUnhashedSubs :: [SigSubPacket]
-  , sbSalt :: BuilderSalt v
-  , sbTextNormMode :: TextNormalizationMode
-    -- ^ Normalization mode for CanonicalTextSig payloads.
-    --   Defaults to 'CleartextCompat' for backward compatibility.
-  }
+{- | Staged signature builder that enforces completion order
 
+Usage pattern:
+  builder <- sigBuilderInit SigTypeBinary RSA SHA256
+  builder' <- addHashedSubs hashedList builder
+  finalPayload <- buildSigV4 sigMPIs (addUnhashedSubs unhashedList builder')
+-}
+data
+    SigBuilder
+        (hashedness :: Type)
+        (v :: Type)
+        (algo :: PubKeyAlgorithm)
+    = SigBuilder
+    { sbSigType :: SigType
+    , sbHashAlgo :: HashAlgorithm
+    , sbHashedSubs :: [SigSubPacket]
+    , sbUnhashedSubs :: [SigSubPacket]
+    , sbSalt :: BuilderSalt v
+    , sbTextNormMode :: TextNormalizationMode
+    {- ^ Normalization mode for CanonicalTextSig payloads.
+    Defaults to 'CleartextCompat' for backward compatibility.
+    -}
+    }
+
 type family BuilderSalt (v :: Type) where
-  BuilderSalt V4Sig = ()
-  BuilderSalt V6Sig = SignatureSalt
+    BuilderSalt V4Sig = ()
+    BuilderSalt V6Sig = SignatureSalt
 
-sbPubKeyAlgo :: forall hashedness v algo. KnownPubKeyAlgorithm algo => SigBuilder hashedness v algo -> PubKeyAlgorithm
+sbPubKeyAlgo
+    :: forall hashedness v algo
+     . KnownPubKeyAlgorithm algo
+    => SigBuilder hashedness v algo -> PubKeyAlgorithm
 sbPubKeyAlgo _ = demotePubKeyAlgorithmT (Proxy @algo)
 
--- | Initialize a builder for a v4 signature
--- Starts with no subpackets and must call addHashedSubs and addUnhashedSubs
-sigBuilderInit ::
-     forall algo
-   . KnownPubKeyAlgorithm algo
-  => SigType
-  -> HashAlgorithm
-  -> SigBuilder Hashed V4Sig algo
-sigBuilderInit st ha = SigBuilder
-  { sbSigType = st
-  , sbHashAlgo = ha
-  , sbHashedSubs = []
-  , sbUnhashedSubs = []
-  , sbSalt = ()
-  , sbTextNormMode = CleartextCompat
-  }
+{- | Initialize a builder for a v4 signature
+Starts with no subpackets and must call addHashedSubs and addUnhashedSubs
+-}
+sigBuilderInit
+    :: forall algo
+     . KnownPubKeyAlgorithm algo
+    => SigType
+    -> HashAlgorithm
+    -> SigBuilder Hashed V4Sig algo
+sigBuilderInit st ha =
+    SigBuilder
+        { sbSigType = st
+        , sbHashAlgo = ha
+        , sbHashedSubs = []
+        , sbUnhashedSubs = []
+        , sbSalt = ()
+        , sbTextNormMode = CleartextCompat
+        }
 
 -- | Initialize a builder for a v4 signature with compile-time RFC/hash policy enforcement.
-sigBuilderInitTyped ::
-     forall algo rfc h
-   . (KnownPubKeyAlgorithm algo, HashAlgoAllowedFor rfc h)
-  => OpenPGPRFCW rfc
-  -> SigType
-  -> HashAlgorithmW h
-  -> SigBuilder Hashed V4Sig algo
+sigBuilderInitTyped
+    :: forall algo rfc h
+     . (HashAlgoAllowedFor rfc h, KnownPubKeyAlgorithm algo)
+    => OpenPGPRFCW rfc
+    -> SigType
+    -> HashAlgorithmW h
+    -> SigBuilder Hashed V4Sig algo
 sigBuilderInitTyped _ st hashW =
-  sigBuilderInit @algo st (demoteHashAlgorithmW hashW)
+    sigBuilderInit @algo st (demoteHashAlgorithmW hashW)
 
--- | Initialize a v4 builder from runtime RFC/hash inputs with policy validation
--- and witness promotion at the API boundary.
-sigBuilderInitRuntime ::
-     forall algo
-   . KnownPubKeyAlgorithm algo
-  => OpenPGPRFC
-  -> SigType
-  -> HashAlgorithm
-  -> Either String (SigBuilder Hashed V4Sig algo)
+{- | Initialize a v4 builder from runtime RFC/hash inputs with policy validation
+and witness promotion at the API boundary.
+-}
+sigBuilderInitRuntime
+    :: forall algo
+     . KnownPubKeyAlgorithm algo
+    => OpenPGPRFC
+    -> SigType
+    -> HashAlgorithm
+    -> Either String (SigBuilder Hashed V4Sig algo)
 sigBuilderInitRuntime rfc st ha =
-  withGenerationHashWitness
-    rfc
-    ha
-    (\rfcW hashW -> sigBuilderInitTyped @algo rfcW st hashW)
+    withGenerationHashWitness
+        rfc
+        ha
+        (\rfcW hashW -> sigBuilderInitTyped @algo rfcW st hashW)
 
 -- | Initialize a builder for a v6 signature
-sigBuilderInitV6 ::
-     forall algo
-   . KnownPubKeyAlgorithm algo
-  => SigType
-  -> HashAlgorithm
-  -> SignatureSalt
-  -> SigBuilder Hashed V6Sig algo
-sigBuilderInitV6 st ha salt = SigBuilder
-  { sbSigType = st
-  , sbHashAlgo = ha
-  , sbHashedSubs = []
-  , sbUnhashedSubs = []
-  , sbSalt = salt
-  , sbTextNormMode = CleartextCompat
-  }
+sigBuilderInitV6
+    :: forall algo
+     . KnownPubKeyAlgorithm algo
+    => SigType
+    -> HashAlgorithm
+    -> SignatureSalt
+    -> SigBuilder Hashed V6Sig algo
+sigBuilderInitV6 st ha salt =
+    SigBuilder
+        { sbSigType = st
+        , sbHashAlgo = ha
+        , sbHashedSubs = []
+        , sbUnhashedSubs = []
+        , sbSalt = salt
+        , sbTextNormMode = CleartextCompat
+        }
 
 -- | Initialize a builder for a v6 signature with compile-time RFC/hash policy enforcement.
-sigBuilderInitV6Typed ::
-     forall algo rfc h
-   . (KnownPubKeyAlgorithm algo, HashAlgoAllowedFor rfc h)
-  => OpenPGPRFCW rfc
-  -> SigType
-  -> HashAlgorithmW h
-  -> SignatureSalt
-  -> SigBuilder Hashed V6Sig algo
+sigBuilderInitV6Typed
+    :: forall algo rfc h
+     . (HashAlgoAllowedFor rfc h, KnownPubKeyAlgorithm algo)
+    => OpenPGPRFCW rfc
+    -> SigType
+    -> HashAlgorithmW h
+    -> SignatureSalt
+    -> SigBuilder Hashed V6Sig algo
 sigBuilderInitV6Typed _ st hashW salt =
-  sigBuilderInitV6 @algo st (demoteHashAlgorithmW hashW) salt
+    sigBuilderInitV6 @algo st (demoteHashAlgorithmW hashW) salt
 
--- | Initialize a v6 builder from runtime RFC/hash inputs with policy validation
--- and witness promotion at the API boundary.
-sigBuilderInitV6Runtime ::
-     forall algo
-   . KnownPubKeyAlgorithm algo
-  => OpenPGPRFC
-  -> SigType
-  -> HashAlgorithm
-  -> SignatureSalt
-  -> Either String (SigBuilder Hashed V6Sig algo)
+{- | Initialize a v6 builder from runtime RFC/hash inputs with policy validation
+and witness promotion at the API boundary.
+-}
+sigBuilderInitV6Runtime
+    :: forall algo
+     . KnownPubKeyAlgorithm algo
+    => OpenPGPRFC
+    -> SigType
+    -> HashAlgorithm
+    -> SignatureSalt
+    -> Either String (SigBuilder Hashed V6Sig algo)
 sigBuilderInitV6Runtime rfc st ha salt =
-  withGenerationHashWitness
-    rfc
-    ha
-    (\rfcW hashW -> sigBuilderInitV6Typed @algo rfcW st hashW salt)
+    withGenerationHashWitness
+        rfc
+        ha
+        (\rfcW hashW -> sigBuilderInitV6Typed @algo rfcW st hashW salt)
 
-withGenerationHashWitness ::
-     OpenPGPRFC
-  -> HashAlgorithm
-  -> (forall rfc h. HashAlgoAllowedFor rfc h => OpenPGPRFCW rfc -> HashAlgorithmW h -> a)
-  -> Either String a
+withGenerationHashWitness
+    :: OpenPGPRFC
+    -> HashAlgorithm
+    -> ( forall rfc h
+          . HashAlgoAllowedFor rfc h
+         => OpenPGPRFCW rfc -> HashAlgorithmW h -> a
+       )
+    -> Either String a
 withGenerationHashWitness rfc ha mk
-  | ha `elem` deprecatedHashAlgorithms (policyGenerationDeprecations (policyForRFC rfc)) =
-      Left (hashPolicyDisallowedMessage rfc ha)
-  | otherwise =
-      case promoteHashAlgorithm ha of
-        Nothing ->
-          Left (hashNotTypedBuilderMessage ha)
-        Just (SomeHashAlgorithmW hashW) ->
-          case promoteOpenPGPRFC rfc of
-            SomeOpenPGPRFCW RFC2440W ->
-              Right $
-                case hashW of
-                  DeprecatedMD5W -> mk RFC2440W DeprecatedMD5W
-                  SHA1W -> mk RFC2440W SHA1W
-                  RIPEMD160W -> mk RFC2440W RIPEMD160W
-                  SHA256W -> mk RFC2440W SHA256W
-                  SHA384W -> mk RFC2440W SHA384W
-                  SHA512W -> mk RFC2440W SHA512W
-                  SHA224W -> mk RFC2440W SHA224W
-                  SHA3_256W -> mk RFC2440W SHA3_256W
-                  SHA3_512W -> mk RFC2440W SHA3_512W
-            SomeOpenPGPRFCW RFC4880W ->
-              case hashW of
-                SHA1W -> Right (mk RFC4880W SHA1W)
-                RIPEMD160W -> Right (mk RFC4880W RIPEMD160W)
-                SHA256W -> Right (mk RFC4880W SHA256W)
-                SHA384W -> Right (mk RFC4880W SHA384W)
-                SHA512W -> Right (mk RFC4880W SHA512W)
-                SHA224W -> Right (mk RFC4880W SHA224W)
-                SHA3_256W -> Right (mk RFC4880W SHA3_256W)
-                SHA3_512W -> Right (mk RFC4880W SHA3_512W)
-                DeprecatedMD5W ->
-                  Left (hashPolicyDisallowedMessage rfc ha)
-            SomeOpenPGPRFCW RFC9580W ->
-              case hashW of
-                SHA256W -> Right (mk RFC9580W SHA256W)
-                SHA384W -> Right (mk RFC9580W SHA384W)
-                SHA512W -> Right (mk RFC9580W SHA512W)
-                SHA224W -> Right (mk RFC9580W SHA224W)
-                SHA3_256W -> Right (mk RFC9580W SHA3_256W)
-                SHA3_512W -> Right (mk RFC9580W SHA3_512W)
-                DeprecatedMD5W ->
-                  Left (hashPolicyDisallowedMessage rfc ha)
-                SHA1W ->
-                  Left (hashPolicyDisallowedMessage rfc ha)
-                RIPEMD160W ->
-                  Left (hashPolicyDisallowedMessage rfc ha)
+    | ha
+        `elem` deprecatedHashAlgorithms
+            (policyGenerationDeprecations (policyForRFC rfc)) =
+        Left (hashPolicyDisallowedMessage rfc ha)
+    | otherwise =
+        case promoteHashAlgorithm ha of
+            Nothing ->
+                Left (hashNotTypedBuilderMessage ha)
+            Just (SomeHashAlgorithmW hashW) ->
+                case promoteOpenPGPRFC rfc of
+                    SomeOpenPGPRFCW RFC2440W ->
+                        Right $
+                            case hashW of
+                                DeprecatedMD5W -> mk RFC2440W DeprecatedMD5W
+                                SHA1W -> mk RFC2440W SHA1W
+                                RIPEMD160W -> mk RFC2440W RIPEMD160W
+                                SHA256W -> mk RFC2440W SHA256W
+                                SHA384W -> mk RFC2440W SHA384W
+                                SHA512W -> mk RFC2440W SHA512W
+                                SHA224W -> mk RFC2440W SHA224W
+                                SHA3_256W -> mk RFC2440W SHA3_256W
+                                SHA3_512W -> mk RFC2440W SHA3_512W
+                    SomeOpenPGPRFCW RFC4880W ->
+                        case hashW of
+                            SHA1W -> Right (mk RFC4880W SHA1W)
+                            RIPEMD160W -> Right (mk RFC4880W RIPEMD160W)
+                            SHA256W -> Right (mk RFC4880W SHA256W)
+                            SHA384W -> Right (mk RFC4880W SHA384W)
+                            SHA512W -> Right (mk RFC4880W SHA512W)
+                            SHA224W -> Right (mk RFC4880W SHA224W)
+                            SHA3_256W -> Right (mk RFC4880W SHA3_256W)
+                            SHA3_512W -> Right (mk RFC4880W SHA3_512W)
+                            DeprecatedMD5W ->
+                                Left (hashPolicyDisallowedMessage rfc ha)
+                    SomeOpenPGPRFCW RFC9580W ->
+                        case hashW of
+                            SHA256W -> Right (mk RFC9580W SHA256W)
+                            SHA384W -> Right (mk RFC9580W SHA384W)
+                            SHA512W -> Right (mk RFC9580W SHA512W)
+                            SHA224W -> Right (mk RFC9580W SHA224W)
+                            SHA3_256W -> Right (mk RFC9580W SHA3_256W)
+                            SHA3_512W -> Right (mk RFC9580W SHA3_512W)
+                            DeprecatedMD5W ->
+                                Left (hashPolicyDisallowedMessage rfc ha)
+                            SHA1W ->
+                                Left (hashPolicyDisallowedMessage rfc ha)
+                            RIPEMD160W ->
+                                Left (hashPolicyDisallowedMessage rfc ha)
 
-hashPolicyDisallowedMessage :: OpenPGPRFC -> HashAlgorithm -> String
+hashPolicyDisallowedMessage
+    :: OpenPGPRFC -> HashAlgorithm -> String
 hashPolicyDisallowedMessage rfc ha =
-  "signature hash algorithm disallowed by RFC policy (" ++ show rfc ++ "): " ++ show ha
+    "signature hash algorithm disallowed by RFC policy ("
+        ++ show rfc
+        ++ "): "
+        ++ show ha
 
 hashNotTypedBuilderMessage :: HashAlgorithm -> String
 hashNotTypedBuilderMessage ha =
-  "signature hash algorithm is not supported by typed builder API: " ++ show ha
+    "signature hash algorithm is not supported by typed builder API: "
+        ++ show ha
 
--- | Add hashed subpackets to a builder
--- Must be called before addUnhashedSubs
-addHashedSubs :: HashedSubpackets v -> SigBuilder Hashed v algo -> SigBuilder Unhashed v algo
-addHashedSubs (SubpacketList sps) builder = builder { sbHashedSubs = sps }
+{- | Add hashed subpackets to a builder
+Must be called before addUnhashedSubs
+-}
+addHashedSubs
+    :: HashedSubpackets v
+    -> SigBuilder Hashed v algo
+    -> SigBuilder Unhashed v algo
+addHashedSubs (SubpacketList sps) builder = builder {sbHashedSubs = sps}
 
--- | Add unhashed subpackets to a builder
--- Must be called after addHashedSubs
-addUnhashedSubs :: UnhashedSubpackets v -> SigBuilder Unhashed v algo -> SigBuilder Unhashed v algo
-addUnhashedSubs (SubpacketList sps) builder = builder { sbUnhashedSubs = sps }
+{- | Add unhashed subpackets to a builder
+Must be called after addHashedSubs
+-}
+addUnhashedSubs
+    :: UnhashedSubpackets v
+    -> SigBuilder Unhashed v algo
+    -> SigBuilder Unhashed v algo
+addUnhashedSubs (SubpacketList sps) builder = builder {sbUnhashedSubs = sps}
 
 -- | Build a v4 signature from a completed builder and MPI values
-buildSigV4 ::
-     KnownPubKeyAlgorithm algo
-  => SigBuilder Unhashed V4Sig algo
-  -> Word16
-  -> NonEmpty MPI
-  -> SignaturePayload
-buildSigV4 builder hashLeft mpis = SigV4
-  (sbSigType builder)
-  (sbPubKeyAlgo builder)
-  (sbHashAlgo builder)
-  (sbHashedSubs builder)
-  (sbUnhashedSubs builder)
-  hashLeft
-  mpis
+buildSigV4
+    :: KnownPubKeyAlgorithm algo
+    => SigBuilder Unhashed V4Sig algo
+    -> Word16
+    -> NonEmpty MPI
+    -> SignaturePayload
+buildSigV4 builder hashLeft mpis =
+    SigV4
+        (sbSigType builder)
+        (sbPubKeyAlgo builder)
+        (sbHashAlgo builder)
+        (sbHashedSubs builder)
+        (sbUnhashedSubs builder)
+        hashLeft
+        mpis
 
 -- | Build a v6 signature from a completed builder and MPI values
-buildSigV6 ::
-     KnownPubKeyAlgorithm algo
-  => SigBuilder Unhashed V6Sig algo
-  -> Word16
-  -> NonEmpty MPI
-  -> SignaturePayload
+buildSigV6
+    :: KnownPubKeyAlgorithm algo
+    => SigBuilder Unhashed V6Sig algo
+    -> Word16
+    -> NonEmpty MPI
+    -> SignaturePayload
 buildSigV6 builder hashLeft mpis =
-  SigV6
-    (sbSigType builder)
-    (sbPubKeyAlgo builder)
-    (sbHashAlgo builder)
-    (sbSalt builder)
-    (sbHashedSubs builder)
-    (sbUnhashedSubs builder)
-    hashLeft
-    mpis
+    SigV6
+        (sbSigType builder)
+        (sbPubKeyAlgo builder)
+        (sbHashAlgo builder)
+        (sbSalt builder)
+        (sbHashedSubs builder)
+        (sbUnhashedSubs builder)
+        hashLeft
+        mpis
 
--- | Convert a list to a hashed subpacket list
--- Used to bridge between list-based and phantom-typed APIs
+{- | Convert a list to a hashed subpacket list
+Used to bridge between list-based and phantom-typed APIs
+-}
 listToHashedSubs :: [SigSubPacket] -> HashedSubpackets v
 listToHashedSubs sps = SubpacketList sps
 
--- | Convert a list to an unhashed subpacket list
--- Used to bridge between list-based and phantom-typed APIs
+{- | Convert a list to an unhashed subpacket list
+Used to bridge between list-based and phantom-typed APIs
+-}
 listToUnhashedSubs :: [SigSubPacket] -> UnhashedSubpackets v
 listToUnhashedSubs sps = SubpacketList sps
diff --git a/Codec/Encryption/OpenPGP/Types/Internal/Base.hs b/Codec/Encryption/OpenPGP/Types/Internal/Base.hs
--- a/Codec/Encryption/OpenPGP/Types/Internal/Base.hs
+++ b/Codec/Encryption/OpenPGP/Types/Internal/Base.hs
@@ -45,8 +45,14 @@
     , V4Sig
     , V6Sig
     , ByteRange (..)
+    , rangeOffset
+    , rangeLength
     , WireRepSourceId (..)
     , WireRepRef (..)
+    , wireRepSourceId
+    , wireRepLength
+    , wireRepName
+    , wireRepWasOriginallyArmored
     , WireRepRefs
     , wireRepRef
     , namedWireRepRef
@@ -81,6 +87,8 @@
     , asSignaturePayloadOther
     , FutureVal (..)
     , SigSubPacket (..)
+    , sspCriticality
+    , sspPayload
     , SigSubPacketPayload (..)
     , ECCCurve (..)
     , IssuerFingerprintVersion (..)
@@ -512,7 +520,7 @@
     | ECDSA
     | ForbiddenElgamal
     | DH
-    | EdDSA
+    | EdDSALegacy
     | X25519
     | X448
     | Ed25519
@@ -543,7 +551,7 @@
     fromFVal ECDSA = 19
     fromFVal ForbiddenElgamal = 20
     fromFVal DH = 21
-    fromFVal EdDSA = 22
+    fromFVal EdDSALegacy = 22
     fromFVal X25519 = 25
     fromFVal X448 = 26
     fromFVal Ed25519 = 27
@@ -565,7 +573,7 @@
     toFVal 19 = ECDSA
     toFVal 20 = ForbiddenElgamal
     toFVal 21 = DH
-    toFVal 22 = EdDSA
+    toFVal 22 = EdDSALegacy
     toFVal 25 = X25519
     toFVal 26 = X448
     toFVal 27 = Ed25519
@@ -591,7 +599,7 @@
     pretty ECDSA = pretty "ECDSA"
     pretty ForbiddenElgamal = pretty "(forbidden) Elgamal"
     pretty DH = pretty "DH"
-    pretty EdDSA = pretty "EdDSA"
+    pretty EdDSALegacy = pretty "EdDSA (legacy)"
     pretty X25519 = pretty "X25519"
     pretty X448 = pretty "X448"
     pretty Ed25519 = pretty "Ed25519"
@@ -1443,7 +1451,7 @@
 
 instance A.FromJSON IssuerFingerprintVersion where
     parseJSON (A.Number n) =
-        case round n of
+        case round n :: Integer of
             4 -> pure IssuerFingerprintV4
             6 -> pure IssuerFingerprintV6
             _ -> mzero
diff --git a/Codec/Encryption/OpenPGP/Types/Internal/PKITypes.hs b/Codec/Encryption/OpenPGP/Types/Internal/PKITypes.hs
--- a/Codec/Encryption/OpenPGP/Types/Internal/PKITypes.hs
+++ b/Codec/Encryption/OpenPGP/Types/Internal/PKITypes.hs
@@ -122,6 +122,8 @@
     | ECDHPrivateKey ECDSA_PrivateKey
     | ECDSAPrivateKey ECDSA_PrivateKey
     | EdDSAPrivateKey EdSigningCurve B.ByteString
+    | Ed25519PrivateKey B.ByteString
+    | Ed448PrivateKey B.ByteString
     | X25519PrivateKey B.ByteString
     | X448PrivateKey B.ByteString
     | MLKEMPrivateKey B.ByteString
@@ -140,6 +142,10 @@
     pretty (ECDSAPrivateKey p) = pretty "ECDSA" <+> pretty p
     pretty (EdDSAPrivateKey c bs) =
         pretty c <+> pretty (bsToHexUpper (BL.fromStrict bs))
+    pretty (Ed25519PrivateKey bs) =
+        pretty "Ed25519" <+> pretty (bsToHexUpper (BL.fromStrict bs))
+    pretty (Ed448PrivateKey bs) =
+        pretty "Ed448" <+> pretty (bsToHexUpper (BL.fromStrict bs))
     pretty (X25519PrivateKey bs) = pretty "X25519" <+> pretty (bsToHexUpper (BL.fromStrict bs))
     pretty (X448PrivateKey bs) = pretty "X448" <+> pretty (bsToHexUpper (BL.fromStrict bs))
     pretty (MLKEMPrivateKey bs) =
@@ -158,6 +164,8 @@
     toJSON (ECDHPrivateKey k) = A.toJSON k
     toJSON (ECDSAPrivateKey k) = A.toJSON k
     toJSON (EdDSAPrivateKey c bs) = A.toJSON (c, B.unpack bs)
+    toJSON (Ed25519PrivateKey bs) = A.toJSON (B.unpack bs)
+    toJSON (Ed448PrivateKey bs) = A.toJSON (B.unpack bs)
     toJSON (X25519PrivateKey bs) = A.toJSON (B.unpack bs)
     toJSON (X448PrivateKey bs) = A.toJSON (B.unpack bs)
     toJSON (MLKEMPrivateKey bs) = A.toJSON (B.unpack bs)
diff --git a/Codec/Encryption/OpenPGP/Types/Internal/PacketClass.hs b/Codec/Encryption/OpenPGP/Types/Internal/PacketClass.hs
--- a/Codec/Encryption/OpenPGP/Types/Internal/PacketClass.hs
+++ b/Codec/Encryption/OpenPGP/Types/Internal/PacketClass.hs
@@ -15,6 +15,7 @@
 
 module Codec.Encryption.OpenPGP.Types.Internal.PacketClass where
 
+import Control.Error.Util (hush)
 import Control.Lens (makeLenses)
 import Data.ByteString.Lazy (ByteString)
 import qualified Data.ByteString.Lazy as BL
@@ -39,7 +40,7 @@
     fromPktMaybe :: Pkt -> Maybe a
     fromPktEither :: Pkt -> Either String a
 
-    fromPktMaybe = either (const Nothing) Just . fromPktEither
+    fromPktMaybe = hush . fromPktEither
     dynamicPacketCode = packetCode . packetType
 
 coercionError :: String -> Pkt -> Either String a
diff --git a/Codec/Encryption/OpenPGP/Types/Internal/Pkt.hs b/Codec/Encryption/OpenPGP/Types/Internal/Pkt.hs
--- a/Codec/Encryption/OpenPGP/Types/Internal/Pkt.hs
+++ b/Codec/Encryption/OpenPGP/Types/Internal/Pkt.hs
@@ -16,6 +16,7 @@
 
 module Codec.Encryption.OpenPGP.Types.Internal.Pkt where
 
+import Control.Error.Util (hush)
 import Control.Lens (makeLenses)
 import Data.Aeson (object, (.=))
 import qualified Data.Aeson as A
@@ -666,7 +667,7 @@
 pktToSomeKeyPktEither pkt = Left (NotAKeyPacket pkt)
 
 pktToSomeKeyPkt :: Pkt -> Maybe SomeKeyPkt
-pktToSomeKeyPkt = either (const Nothing) Just . pktToSomeKeyPktEither
+pktToSomeKeyPkt = hush . pktToSomeKeyPktEither
 
 pktToPublicKeyPktEither
     :: Pkt -> Either KeyPktCoercionError (KeyPkt 'PublicPkt)
@@ -675,7 +676,7 @@
 pktToPublicKeyPktEither pkt = Left (ExpectedPublicKeyPacket pkt)
 
 pktToPublicKeyPkt :: Pkt -> Maybe (KeyPkt 'PublicPkt)
-pktToPublicKeyPkt = either (const Nothing) Just . pktToPublicKeyPktEither
+pktToPublicKeyPkt = hush . pktToPublicKeyPktEither
 
 pktToSecretKeyPktEither
     :: Pkt -> Either KeyPktCoercionError (KeyPkt 'SecretPkt)
@@ -684,7 +685,7 @@
 pktToSecretKeyPktEither pkt = Left (ExpectedSecretKeyPacket pkt)
 
 pktToSecretKeyPkt :: Pkt -> Maybe (KeyPkt 'SecretPkt)
-pktToSecretKeyPkt = either (const Nothing) Just . pktToSecretKeyPktEither
+pktToSecretKeyPkt = hush . pktToSecretKeyPktEither
 
 {- | Convert secret key/subkey packets to their public-key packet forms.
 Non-secret packets are returned unchanged.
diff --git a/Codec/Encryption/OpenPGP/Types/Internal/TK.hs b/Codec/Encryption/OpenPGP/Types/Internal/TK.hs
--- a/Codec/Encryption/OpenPGP/Types/Internal/TK.hs
+++ b/Codec/Encryption/OpenPGP/Types/Internal/TK.hs
@@ -18,6 +18,7 @@
 
 import Control.Arrow ((&&&))
 import Control.Comonad (Comonad (..))
+import Control.Error.Util (note)
 import Control.Lens
     ( folded
     , makeLenses
@@ -34,13 +35,12 @@
 import Data.Function (on)
 import qualified Data.HashMap.Lazy as HashMap
 import Data.IxSet.Typed (IxSet)
-import qualified Data.IxSet.Typed as IxSet
 import Data.Kind (Type)
-import Data.List (find, nub, sort, sortOn)
+import Data.List (find, sortOn)
 import qualified Data.List.NonEmpty as NE
 import qualified Data.Map as Map
 import Data.Ord (comparing)
-import Data.Semigroup (Semigroup (..))
+import qualified Data.Set as Set
 import Data.Text (Text)
 import Data.Typeable (Typeable)
 import Data.Word (Word8)
@@ -92,7 +92,7 @@
 
 -- | Move to the next packet in the sequence
 zMoveNext :: PacketZipper a -> Maybe (PacketZipper a)
-zMoveNext (PacketZipper before current []) = Nothing
+zMoveNext (PacketZipper _before _current []) = Nothing
 zMoveNext (PacketZipper before current (x : xs)) =
     Just (PacketZipper (before ++ [current]) x xs)
 
@@ -311,7 +311,9 @@
     a <> b =
         TKUnknown
             (_tkuKey a)
-            (nub . sort $ _tkuRevs a ++ _tkuRevs b)
+            ( Set.toList $
+                Set.union (Set.fromList (_tkuRevs a)) (Set.fromList (_tkuRevs b))
+            )
             ((kvmerge `on` _tkuUIDs) a b)
             ((kvmerge `on` _tkuUAts) a b)
             ((ukvmerge `on` _tkuSubs) a b)
@@ -321,13 +323,15 @@
         ukvmerge x y =
             HashMap.toList
                 (HashMap.unionWith nsa (HashMap.fromList x) (HashMap.fromList y))
-        nsa x y = nub . sort $ x ++ y
+        nsa x y = Set.toList $ Set.union (Set.fromList x) (Set.fromList y)
 
 instance Semigroup (TK k) where
     a <> b =
         TK
             (_tkPrimaryKey a)
-            (nub . sort $ _tkRevs a ++ _tkRevs b)
+            ( Set.toList $
+                Set.union (Set.fromList (_tkRevs a)) (Set.fromList (_tkRevs b))
+            )
             ((kvmerge `on` _tkUIDs) a b)
             ((kvmerge `on` _tkUAts) a b)
             ((ukvmerge `on` _tkSubs) a b)
@@ -337,7 +341,7 @@
         ukvmerge x y =
             HashMap.toList
                 (HashMap.unionWith nsa (HashMap.fromList x) (HashMap.fromList y))
-        nsa x y = nub . sort $ x ++ y
+        nsa x y = Set.toList $ Set.union (Set.fromList x) (Set.fromList y)
 
 instance Semigroup SomeTK where
     SomePublicTK a <> SomePublicTK b = SomePublicTK (a <> b)
@@ -653,10 +657,10 @@
         refs = _tkPackets tkWithRefs
         (pkp, mska) = _tkuKey tk
         primaryPkt = someKeyPktToPkt (mkPrimaryKeyPkt pkp mska)
-    zipper <- case zFromList refs of
-        Just z -> Right z
-        Nothing ->
-            Left "no packet references available for TKUnknown structuring"
+    zipper <-
+        note
+            "no packet references available for TKUnknown structuring"
+            (zFromList refs)
     (primaryRef, z1') <-
         consumePktZ "primary key packet" primaryPkt zipper
     -- Move past the primary key to process its signatures and following packets
diff --git a/Data/Conduit/OpenPGP/Decrypt.hs b/Data/Conduit/OpenPGP/Decrypt.hs
--- a/Data/Conduit/OpenPGP/Decrypt.hs
+++ b/Data/Conduit/OpenPGP/Decrypt.hs
@@ -18,6 +18,7 @@
     , PKESKRecipientKey (..)
     , PKESKAttemptFailureKind (..)
     , PKESKAttemptFailure (..)
+    , DecryptStructureError (..)
     , DecryptOutcome (..)
     , DecryptReport (..)
     , DecryptSessionKeyResolutionReport (..)
@@ -25,6 +26,10 @@
     , PKESKResolverAttempt (..)
     , PKESKResolverAttemptAction (..)
     , decryptSEIPDv2Payload
+    , renderDecryptStructureError
+    , renderDecryptOutcome
+    , renderDecryptSessionKeyResolutionReport
+    , renderDecryptReport
     ) where
 
 import Control.Applicative ((<|>))
@@ -37,7 +42,6 @@
 import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
 import Control.Monad.Trans.Resource (MonadResource, MonadThrow)
 import qualified Crypto.Error as CE
-import qualified Crypto.Hash as CH
 import qualified Crypto.Hash.Algorithms as CHA
 import Crypto.KDF.HKDF (expand, extract)
 import Crypto.Number.Serialize (i2osp, os2ip)
@@ -50,8 +54,7 @@
 import qualified Crypto.PubKey.RSA.Types as RSATypes
 import Data.Bifunctor (first)
 import Data.Binary (get)
-import Data.Binary.Put (putWord64be, runPut)
-import Data.Bits (countLeadingZeros, shiftL, shiftR, xor)
+import Data.Bits (countLeadingZeros, shiftL)
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as BL
@@ -70,21 +73,28 @@
 import Data.List (intercalate, nub)
 import Data.List.NonEmpty (NonEmpty (..))
 import Data.Maybe (catMaybes, isNothing, mapMaybe)
-import Data.Word (Word16, Word64, Word8)
+import Data.Word (Word64, Word8)
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
 import Codec.Encryption.OpenPGP.BlockCipher
-    ( keySize
+    ( CipherError (..)
+    , keySize
     , renderCipherError
     )
 import Codec.Encryption.OpenPGP.CFB
-    ( calculateMDC
-    , decryptOpenPGPCfb
+    ( decryptOpenPGPCfb
     , decryptPreservingNonce
-    , validateSEIPD1MDC
     )
 import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)
-import Codec.Encryption.OpenPGP.Internal (leftPadTo)
+import Codec.Encryption.OpenPGP.Internal
+    ( checksum16
+    , checksum16Bytes
+    , chunksOf8
+    , edPointBytes
+    , encodeWord64be
+    , leftPadTo
+    , xorBS
+    )
 import Codec.Encryption.OpenPGP.Internal.CryptoAES
     ( withAESCipher
     )
@@ -93,12 +103,6 @@
     , deriveECDHKek
     , normalizeMontgomeryPublic
     )
-import Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2
-    ( aeadModeAndNonceSizeForSEIPDv2
-    , decryptSKESK6SessionKey
-    , deriveSKESK6KEK
-    , seipdv2SymmetricKeySize
-    )
 import Codec.Encryption.OpenPGP.Internal.RFC7253OCB
     ( decryptWithOCBRFC7253With
     )
@@ -116,6 +120,19 @@
     , skesk2SessionKey
     , string2Key
     )
+import Codec.Encryption.OpenPGP.SEIPDv1
+    ( calculateMDC
+    , renderMDCFailure
+    , validateSEIPD1MDC
+    )
+import Codec.Encryption.OpenPGP.SEIPDv2
+    ( SEIPDv2Failure (..)
+    , aeadModeAndNonceSizeForSEIPDv2
+    , decryptSKESK6SessionKey
+    , deriveSKESK6KEK
+    , renderSEIPDv2Failure
+    , seipdv2SymmetricKeySize
+    )
 import Codec.Encryption.OpenPGP.SecretKey (decryptPrivateKey)
 import Codec.Encryption.OpenPGP.Types
 import Data.Conduit.OpenPGP.Compression (conduitDecompress)
@@ -149,7 +166,7 @@
         -> DecryptStreamState 'FinishedDecryptPhase
     MalformedDecryptState
         :: RecursorState
-        -> String
+        -> DecryptStructureError
         -> DecryptStreamState 'MalformedDecryptPhase
 
 data SomeDecryptStreamState where
@@ -206,6 +223,13 @@
     | ResolverInvalidResponse String
     deriving (Eq, Show)
 
+data PKESKX25519V3UnwrapError
+    = PKESKX25519V3ParseError String
+    | PKESKX25519V3UnwrapError CipherError
+    | PKESKX25519V3KeySizeError CipherError
+    | PKESKX25519V3KeyLengthMismatch SymmetricAlgorithm Int Int
+    deriving (Eq, Show)
+
 data PKESKResolveRequest
     = PKESKResolveRequest
     { reqPKESK :: PKESKPayload
@@ -282,6 +306,13 @@
 (outcome, pkts) \<- runConduit $ source .| fuseBoth (conduitDecrypt opts) CL.consume
 @
 -}
+data DecryptStructureError
+    = DecryptStructureESKSEIPDMismatch
+    | DecryptStructureESKOrder
+    | DecryptStructureTrailingData
+    | DecryptStructureGeneric String
+    deriving (Eq, Show)
+
 data DecryptOutcome
     = {- | The integrity-terminating marker (MDC or SEIPD v2 final AEAD tag)
       was seen and no further packets arrived.  The message was
@@ -298,23 +329,10 @@
       packets were forwarded downstream unchanged.
       -}
       DecryptTrailingData
-    | {- | A structural packet-sequencing violation was detected.  The
-      'String' describes the specific violation:
-
-      * PKESK version does not match the SEIPD version (e.g. a v6 PKESK
-      preceding a SEIPDv1 payload, or a v4 SKESK preceding a SEIPDv2
-      payload).
-
-      * ESK packets arrived in the wrong order relative to the encrypted
-      data packet (e.g. a literal-data packet appeared between a PKESK
-      and the SEIPD it was intended to protect).
-
-      * A packet arrived after the message integrity boundary (trailing
-      data).  Under 'defaultDecryptPolicy' this is reported here; under
-      'lenientDecryptPolicy' it is reported as 'DecryptTrailingData'
-      instead.
+    | {- | A structural packet-sequencing violation was detected.  Use
+      'renderDecryptStructureError' to obtain a human-readable description.
       -}
-      DecryptMalformedStructure String
+      DecryptMalformedStructure DecryptStructureError
     deriving (Eq, Show)
 
 data DecryptSessionKeyResolutionPath
@@ -341,8 +359,9 @@
 data DecryptSessionKeyResolutionReport
     = DecryptSessionKeyResolutionReport
     { decryptSessionResolutionPath :: DecryptSessionKeyResolutionPath
-    , decryptSessionResolutionSKESKErrors :: [String]
-    , decryptSessionResolutionPKESKErrors :: [String]
+    , decryptSessionResolutionSKESKErrors
+        :: [SKESKSessionKeyResolutionError]
+    , decryptSessionResolutionPKESKErrors :: [PKESKAttemptFailure]
     , decryptSessionResolutionResolverAttempts
         :: [PKESKResolverAttempt]
     }
@@ -359,16 +378,16 @@
 -- | AEAD decryption context (Reader monad eliminates parameter threading)
 data AEADDecryptContext cipher
     = AEADDecryptContext
-    { aeadMode :: CCT.AEADMode
-    , aeadInfo :: B.ByteString
-    , aeadChunkSize :: Word8
-    , aeadNoncePrefix :: B.ByteString
-    , aeadCipher :: cipher
+    { _aeadMode :: CCT.AEADMode
+    , _aeadInfo :: B.ByteString
+    , _aeadChunkSize :: Word8
+    , _aeadNoncePrefix :: B.ByteString
+    , _aeadCipher :: cipher
     }
 
 -- | ReaderT wrapper for AEAD decryption computations
 type AEADDecrypt cipher =
-    ReaderT (AEADDecryptContext cipher) (Either String)
+    ReaderT (AEADDecryptContext cipher) (Either SEIPDv2Failure)
 
 conduitDecrypt
     :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)
@@ -677,7 +696,7 @@
                 ( SomeDecryptStreamState
                     ( MalformedDecryptState
                         s
-                        "Malformed encrypted packet sequence: ESK packets must immediately precede encrypted data"
+                        DecryptStructureESKOrder
                     )
                 , []
                 )
@@ -717,9 +736,7 @@
                                     ( SomeDecryptStreamState
                                         ( MalformedDecryptState
                                             s
-                                            ( "ESK/payload version mismatch: ESK packets present but none are version-aligned with "
-                                                ++ "legacy SED payload"
-                                            )
+                                            DecryptStructureESKSEIPDMismatch
                                         )
                                     , []
                                     )
@@ -729,6 +746,8 @@
                                         "Received unauthenticated SED (Symmetrically Encrypted Data) packet; \
                                         \RFC9580 policy requires integrity-protected SEIPD. \
                                         \Use lenientDecryptPolicy to permit legacy messages."
+                                when (_depth s > 0) $
+                                    fail "recursive encrypted/compressed messages are not allowed"
                                 (symalgo, sessionKey) <-
                                     resolveSessionKey
                                         s
@@ -757,9 +776,7 @@
                                     ( SomeDecryptStreamState
                                         ( MalformedDecryptState
                                             s
-                                            ( "ESK/payload version mismatch: ESK packets present but none are version-aligned with "
-                                                ++ "SEIPDv1 payload"
-                                            )
+                                            DecryptStructureESKSEIPDMismatch
                                         )
                                     , []
                                     )
@@ -767,6 +784,8 @@
                                 when (not (decryptAllowSEIPDv1 dp)) $
                                     fail
                                         "Received SEIPDv1 packet; decrypt policy requires SEIPDv2 only."
+                                when (_depth s > 0) $
+                                    fail "recursive encrypted/compressed messages are not allowed"
                                 (symalgo, sessionKey) <-
                                     resolveSessionKey
                                         s
@@ -796,15 +815,15 @@
                                     ( SomeDecryptStreamState
                                         ( MalformedDecryptState
                                             s
-                                            ( "ESK/payload version mismatch: ESK packets present but none are version-aligned with "
-                                                ++ "SEIPDv2 payload"
-                                            )
+                                            DecryptStructureESKSEIPDMismatch
                                         )
                                     , []
                                     )
                             else do
                                 checkDecryptSymmetricAlgo dp sa
                                 checkDecryptAEADAlgo dp aa
+                                when (_depth s > 0) $
+                                    fail "recursive encrypted/compressed messages are not allowed"
                                 (_, sessionKey) <-
                                     resolveSessionKey
                                         s
@@ -862,14 +881,14 @@
                         return (SomeDecryptStreamState (ActiveDecryptState s), [])
                     p ->
                         return (SomeDecryptStreamState (ActiveDecryptState s), [p])
-    push i (FinishedDecryptState s hadTrailing) =
+    push i (FinishedDecryptState s _hadTrailing) =
         if decryptRejectTrailingData (_decryptPolicy s)
             then
                 return
                     ( SomeDecryptStreamState
                         ( MalformedDecryptState
                             s
-                            "packet received after message integrity boundary"
+                            DecryptStructureTrailingData
                         )
                     , []
                     )
@@ -932,9 +951,11 @@
 checkInnerOutcome _ DecryptClean = pure ()
 checkInnerOutcome _ DecryptTrailingData = pure ()
 checkInnerOutcome NoIntegrityMarker DecryptTruncated = pure ()
-checkInnerOutcome _ (DecryptMalformedStructure reason) =
+checkInnerOutcome _ (DecryptMalformedStructure err) =
     fail
-        ("Inner encrypted payload had malformed structure: " ++ reason)
+        ( "Inner encrypted payload had malformed structure: "
+            ++ renderDecryptStructureError err
+        )
 
 decryptSEDP
     :: (MonadFail m, MonadIO m, MonadThrow m, MonadUnliftIO m)
@@ -981,7 +1002,7 @@
             Right x -> pure x
     decryptedWithoutMDC <-
         case validateSEIPD1MDC nonce decrypted of
-            Left err -> fail err
+            Left err -> fail (renderMDCFailure err)
             Right x -> pure x
     (innerOutcome, pkts) <-
         decryptInnerPackets
@@ -1018,7 +1039,7 @@
                 (BL.toStrict bs)
                 sessionKey
     case decrypted of
-        Left e -> fail e
+        Left e -> fail (renderSEIPDv2Failure e)
         Right cleartext -> do
             (innerOutcome, pkts) <-
                 decryptInnerPackets
@@ -1062,18 +1083,17 @@
     -> Salt
     -> B.ByteString
     -> SessionKey
-    -> Either String B.ByteString
+    -> Either SEIPDv2Failure B.ByteString
 decryptSEIPDv2Payload symalgo aeadalgo chunkSize salt encrypted (SessionKey sessionKey) = do
     when (chunkSize > 16) $
-        Left "SEIPD v2 chunk size octet must be between 0 and 16"
+        Left SEIPDv2InvalidChunkSize
     (mode, nonceSize) <- aeadModeAndNonceSize aeadalgo
     keyLen <- symKeySize symalgo
     let outputLen = keyLen + nonceSize - 8
     when (B.length (unSalt salt) /= 32) $
-        Left "SEIPD v2 salt must be exactly 32 octets"
+        Left SEIPDv2InvalidSaltLength
     when (B.length encrypted < 32) $
-        Left
-            "SEIPD v2 ciphertext must include at least one chunk tag and a final tag"
+        Left SEIPDv2CiphertextTooShort
     let info =
             B.pack
                 [0xd2, 2, fromFVal symalgo, fromFVal aeadalgo, chunkSize]
@@ -1083,6 +1103,7 @@
         noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)
     decryptSEIPDv2WithKey
         symalgo
+        aeadalgo
         mode
         chunkSize
         info
@@ -1092,30 +1113,33 @@
 
 decryptSEIPDv2WithKey
     :: SymmetricAlgorithm
+    -> AEADAlgorithm
     -> CCT.AEADMode
     -> Word8
     -> B.ByteString
     -> B.ByteString
     -> B.ByteString
     -> B.ByteString
-    -> Either String B.ByteString
-decryptSEIPDv2WithKey symalgo mode chunkSize info noncePrefix sessionKey encrypted =
+    -> Either SEIPDv2Failure B.ByteString
+decryptSEIPDv2WithKey symalgo aeadalgo mode chunkSize info noncePrefix sessionKey encrypted =
     withAESCipher
-        "SEIPD v2 decrypt currently supports AES-128/192/256 only"
+        SEIPDv2CipherInitFailed
+        (SEIPDv2UnsupportedSymmetricAlgorithm symalgo)
         symalgo
         sessionKey
-        (decryptChunks mode info chunkSize noncePrefix encrypted)
+        (decryptChunks aeadalgo mode info chunkSize noncePrefix encrypted)
 
 decryptChunks
     :: CCT.BlockCipher cipher
-    => CCT.AEADMode
+    => AEADAlgorithm
+    -> CCT.AEADMode
     -> B.ByteString
     -> Word8
     -> B.ByteString
     -> B.ByteString
     -> cipher
-    -> Either String B.ByteString
-decryptChunks mode info chunkSize noncePrefix encrypted cipher =
+    -> Either SEIPDv2Failure B.ByteString
+decryptChunks aeadalgo mode info chunkSize noncePrefix encrypted cipher =
     let ctx = AEADDecryptContext mode info chunkSize noncePrefix cipher
      in runReaderT decryptChunksWithReader ctx
   where
@@ -1129,8 +1153,7 @@
         go idx remaining acc totalPlain
             | B.length remaining < 2 * tagLen =
                 lift $
-                    Left
-                        "SEIPD v2 ciphertext is too short for chunk and final authentication tags"
+                    Left SEIPDv2CiphertextTooShort
             | otherwise = do
                 let hasMoreChunks = B.length remaining > chunkLen + 2 * tagLen
                     currentChunkLen =
@@ -1139,7 +1162,7 @@
                             else B.length remaining - 2 * tagLen
                 when (currentChunkLen < 0) $
                     lift $
-                        Left "SEIPD v2 malformed chunk lengths"
+                        Left SEIPDv2MalformedChunkLengths
                 let (chunkCiphertext, r1) = B.splitAt currentChunkLen remaining
                     (chunkTag, r2) = B.splitAt tagLen r1
                 plainChunk <-
@@ -1154,9 +1177,9 @@
                     else do
                         when (B.length r2 /= tagLen) $
                             lift $
-                                Left "SEIPD v2 missing final authentication tag"
+                                Left SEIPDv2MissingFinalTag
                         verifyFinalTagWithContext
-                            (idx + 1)
+                            (fromIntegral (idx + 1))
                             (totalPlain + B.length plainChunk)
                             r2
                         return (B.concat (reverse (plainChunk : acc)))
@@ -1167,14 +1190,14 @@
                 then
                     lift $
                         decryptWithOCBRFC7253With
-                            (\_ _ _ _ _ _ -> "SEIPD v2 chunk authentication failed")
+                            (\_ _ _ _ _ _ -> SEIPDv2ChunkAuthFailed aeadalgo idx)
                             cipher'
-                            (noncePrefix' <> encodeWord64be idx)
+                            (noncePrefix' <> encodeWord64be (fromIntegral idx))
                             info
                             chunkCiphertext
                             (mkAuthTag chunkTag)
                 else do
-                    aead <- initAEADWithContext idx
+                    aead <- initAEADWithContext (fromIntegral idx)
                     let mPlain =
                             CCT.aeadSimpleDecrypt
                                 aead
@@ -1182,7 +1205,7 @@
                                 chunkCiphertext
                                 (mkAuthTag chunkTag)
                     case mPlain of
-                        Nothing -> lift $ Left "SEIPD v2 chunk authentication failed"
+                        Nothing -> lift $ Left (SEIPDv2ChunkAuthFailed aeadalgo idx)
                         Just p -> return p
 
         verifyFinalTagWithContext idx totalPlain finalTag = do
@@ -1192,7 +1215,7 @@
                     plain <-
                         lift $
                             decryptWithOCBRFC7253With
-                                (\_ _ _ _ _ _ -> "SEIPD v2 chunk authentication failed")
+                                (\_ _ _ _ _ _ -> SEIPDv2FinalTagFailed aeadalgo)
                                 cipher'
                                 (noncePrefix' <> encodeWord64be idx)
                                 (info <> encodeWord64be (fromIntegral totalPlain))
@@ -1202,7 +1225,7 @@
                         then return ()
                         else
                             lift $
-                                Left "SEIPD v2 final authentication tag verification failed"
+                                Left (SEIPDv2FinalTagFailed aeadalgo)
                 else do
                     aead <- initAEADWithContext idx
                     let mEmpty =
@@ -1215,27 +1238,21 @@
                         Just p | B.null p -> return ()
                         _ ->
                             lift $
-                                Left "SEIPD v2 final authentication tag verification failed"
+                                Left (SEIPDv2FinalTagFailed aeadalgo)
 
         initAEADWithContext idx = do
             AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask
-            lift $
-                first show . CE.eitherCryptoError $
-                    CCT.aeadInit mode' cipher' (noncePrefix' <> encodeWord64be idx)
+            lift
+                $ first SEIPDv2CipherInitFailed
+                    . CE.eitherCryptoError
+                $ CCT.aeadInit mode' cipher' (noncePrefix' <> encodeWord64be idx)
 
 aeadModeAndNonceSize
-    :: AEADAlgorithm -> Either String (CCT.AEADMode, Int)
-aeadModeAndNonceSize =
-    aeadModeAndNonceSizeForSEIPDv2
-        "Unknown AEAD algorithm for SEIPD v2 decrypt"
-
-symKeySize :: SymmetricAlgorithm -> Either String Int
-symKeySize =
-    seipdv2SymmetricKeySize
-        "SEIPD v2 decrypt currently supports AES-128/192/256 only"
+    :: AEADAlgorithm -> Either SEIPDv2Failure (CCT.AEADMode, Int)
+aeadModeAndNonceSize = aeadModeAndNonceSizeForSEIPDv2
 
-encodeWord64be :: Word64 -> B.ByteString
-encodeWord64be = BL.toStrict . runPut . putWord64be
+symKeySize :: SymmetricAlgorithm -> Either SEIPDv2Failure Int
+symKeySize = seipdv2SymmetricKeySize
 
 mkAuthTag :: B.ByteString -> CCT.AuthTag
 mkAuthTag = CCT.AuthTag . BA.convert
@@ -1287,14 +1304,6 @@
         ClassifiedSKESKPayloadV4 _ -> Nothing
         ClassifiedSKESKPayloadV6 (SKESKPayloadV6 _ aa _ _ _ _) -> Just aa
 
-resolveSKESKSessionKey
-    :: BL.ByteString -> SKESKPayload -> Either String B.ByteString
-resolveSKESKSessionKey passphrase payload =
-    first renderSKESKSessionKeyResolutionError $
-        resolveSKESKSessionKeyTyped
-            passphrase
-            (classifySKESKPayload payload)
-
 data SKESKSessionKeyResolutionError
     = SKESKSessionKeyS2KError S2KError
     | SKESKSessionKeyOtherError String
@@ -1305,6 +1314,64 @@
 renderSKESKSessionKeyResolutionError (SKESKSessionKeyS2KError err) = renderS2KError err
 renderSKESKSessionKeyResolutionError (SKESKSessionKeyOtherError err) = err
 
+renderPKESKX25519V3UnwrapError
+    :: PKESKX25519V3UnwrapError -> String
+renderPKESKX25519V3UnwrapError (PKESKX25519V3ParseError err) = err
+renderPKESKX25519V3UnwrapError (PKESKX25519V3UnwrapError err) = renderCipherError err
+renderPKESKX25519V3UnwrapError (PKESKX25519V3KeySizeError err) = renderCipherError err
+renderPKESKX25519V3UnwrapError (PKESKX25519V3KeyLengthMismatch algo expected actual) =
+    "X25519 PKESKv3 unwrapped session key length mismatch for "
+        ++ show algo
+        ++ ": expected "
+        ++ show expected
+        ++ ", got "
+        ++ show actual
+
+renderDecryptStructureError :: DecryptStructureError -> String
+renderDecryptStructureError DecryptStructureESKSEIPDMismatch =
+    "ESK/payload version mismatch"
+renderDecryptStructureError DecryptStructureESKOrder =
+    "Malformed encrypted packet sequence: ESK packets must immediately precede encrypted data"
+renderDecryptStructureError DecryptStructureTrailingData =
+    "packet received after message integrity boundary"
+renderDecryptStructureError (DecryptStructureGeneric msg) = msg
+
+renderDecryptOutcome :: DecryptOutcome -> String
+renderDecryptOutcome DecryptClean = "clean"
+renderDecryptOutcome DecryptTruncated = "truncated"
+renderDecryptOutcome DecryptTrailingData = "trailing data"
+renderDecryptOutcome (DecryptMalformedStructure err) =
+    "malformed structure: " ++ renderDecryptStructureError err
+
+renderDecryptSessionKeyResolutionReport
+    :: DecryptSessionKeyResolutionReport -> String
+renderDecryptSessionKeyResolutionReport report =
+    "session key resolution: "
+        ++ show (decryptSessionResolutionPath report)
+        ++ "; SKESK errors: "
+        ++ unwords
+            ( map
+                renderSKESKSessionKeyResolutionError
+                (decryptSessionResolutionSKESKErrors report)
+            )
+        ++ "; PKESK errors: "
+        ++ unwords
+            ( map
+                pkeskAttemptFailureReason
+                (decryptSessionResolutionPKESKErrors report)
+            )
+
+renderDecryptReport :: DecryptReport -> String
+renderDecryptReport report =
+    "outcome: "
+        ++ renderDecryptOutcome (decryptReportOutcome report)
+        ++ "; resolutions: "
+        ++ unwords
+            ( map
+                renderDecryptSessionKeyResolutionReport
+                (decryptReportSessionKeyResolutions report)
+            )
+
 resolveSKESKSessionKeyTyped
     :: BL.ByteString
     -> ClassifiedSKESKPayload
@@ -1327,9 +1394,11 @@
     ikm <-
         first SKESKSessionKeyS2KError (string2Key s2k keyLen passphrase)
     kek <-
-        first SKESKSessionKeyOtherError (deriveSKESK6KEK sa aead ikm)
+        first
+            (SKESKSessionKeyOtherError . renderSEIPDv2Failure)
+            (deriveSKESK6KEK sa aead ikm)
     first
-        SKESKSessionKeyOtherError
+        (SKESKSessionKeyOtherError . renderSEIPDv2Failure)
         ( decryptSKESK6SessionKey
             sa
             aead
@@ -1399,7 +1468,7 @@
         => BL.ByteString
         -> [SKESKPayload]
         -> [PKESKPayload]
-        -> [String]
+        -> [SKESKSessionKeyResolutionError]
         -> m (SymmetricAlgorithm, SessionKey)
     resolveSKESKCandidates _ [] pkesks skeskErrs =
         resolvePKESKCandidates pkesks skeskErrs [] []
@@ -1410,7 +1479,10 @@
                     passphrase
                     rest
                     pkesks
-                    ((skeskErrPrefix skesk ++ err) : skeskErrs)
+                    ( SKESKSessionKeyOtherError
+                        (skeskErrPrefix skesk ++ renderSKESKSessionKeyResolutionError err)
+                        : skeskErrs
+                    )
             Right resolved -> do
                 emitResolutionReport
                     (mkResolutionReport DecryptResolvedViaSKESK skeskErrs [] [])
@@ -1419,22 +1491,33 @@
     resolveSKESKCandidate
         :: BL.ByteString
         -> SKESKPayload
-        -> Either String (SymmetricAlgorithm, SessionKey)
+        -> Either
+            SKESKSessionKeyResolutionError
+            (SymmetricAlgorithm, SessionKey)
     resolveSKESKCandidate passphrase skesk = do
         let skeskSymAlgo = skeskPayloadSymmetricAlgorithm skesk
             expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor
-        sessionKeyBytes <- resolveSKESKSessionKey passphrase skesk
+        sessionKeyBytes <-
+            resolveSKESKSessionKeyTyped
+                passphrase
+                (classifySKESKPayload skesk)
         case expectedSymAlgo of
             Just expected
                 | expected /= skeskSymAlgo ->
-                    Left "SKESK/encrypted-payload symmetric algorithm mismatch"
+                    Left
+                        ( SKESKSessionKeyOtherError
+                            "SKESK/encrypted-payload symmetric algorithm mismatch"
+                        )
             _ ->
                 case ( payloadExpectedAEADAlgorithm payloadFlavor
                      , skeskPayloadAEADAlgorithm skesk
                      ) of
                     (Just expectedAEAD, Just skeskAEAD)
                         | expectedAEAD /= skeskAEAD ->
-                            Left "SKESK/encrypted-payload AEAD algorithm mismatch"
+                            Left
+                                ( SKESKSessionKeyOtherError
+                                    "SKESK/encrypted-payload AEAD algorithm mismatch"
+                                )
                     _ -> Right (skeskSymAlgo, SessionKey sessionKeyBytes)
 
     skeskErrPrefix skesk = "[" ++ describeSKESK skesk ++ "] "
@@ -1442,24 +1525,17 @@
     resolvePKESKCandidates
         :: (MonadFail m, MonadIO m)
         => [PKESKPayload]
-        -> [String]
-        -> [String]
+        -> [SKESKSessionKeyResolutionError]
+        -> [PKESKAttemptFailure]
         -> [PKESKResolverAttempt]
         -> m (SymmetricAlgorithm, SessionKey)
     resolvePKESKCandidates [] [] [] _ =
         fail
             "Encrypted data packet has no usable preceding SKESK or PKESK packet"
-    resolvePKESKCandidates [] skeskErrs [] _ =
-        fail
-            ( "Encrypted data packet has no usable preceding SKESK or PKESK packet; "
-                ++ "candidate errors: "
-                ++ unwords (reverse skeskErrs)
-            )
     resolvePKESKCandidates [] skeskErrs pkeskErrs resolverAttempts =
         if allowManualPKESKPrompt
             then do
                 let expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor
-                    errs = skeskErrs ++ pkeskErrs
                 encodedSessionKey <-
                     BL.toStrict
                         <$> liftIO
@@ -1471,7 +1547,12 @@
                         fail
                             ( "Encrypted data packet has no usable preceding SKESK or PKESK packet; "
                                 ++ "candidate errors: "
-                                ++ unwords (reverse errs)
+                                ++ unwords
+                                    ( reverse
+                                        ( map renderSKESKSessionKeyResolutionError skeskErrs
+                                            ++ map pkeskAttemptFailureReason pkeskErrs
+                                        )
+                                    )
                                 ++ "; manual input failed: "
                                 ++ manualErr
                             )
@@ -1488,7 +1569,12 @@
                 fail
                     ( "Encrypted data packet has no usable preceding SKESK or PKESK packet; "
                         ++ "candidate errors: "
-                        ++ unwords (reverse (skeskErrs ++ pkeskErrs))
+                        ++ unwords
+                            ( reverse
+                                ( map renderSKESKSessionKeyResolutionError skeskErrs
+                                    ++ map pkeskAttemptFailureReason pkeskErrs
+                                )
+                            )
                     )
     resolvePKESKCandidates (pkesk : rest) skeskErrs pkeskErrs resolverAttempts = do
         let expectedSymAlgo = payloadExpectedSymmetricAlgorithm payloadFlavor
@@ -1513,12 +1599,24 @@
                     let resolverAttempts' = resolverAttemptsAcc ++ newAttempts
                         terminalError =
                             case reverse previousFailures of
-                                (latestFailure : _) -> errPrefix ++ pkeskAttemptFailureReason latestFailure
+                                (latestFailure : _) ->
+                                    PKESKAttemptFailure
+                                        { pkeskAttemptFailureKeyContext =
+                                            pkeskAttemptFailureKeyContext latestFailure
+                                        , pkeskAttemptFailureKind = PKESKAttemptUnwrapFailed
+                                        , pkeskAttemptFailureReason =
+                                            errPrefix ++ pkeskAttemptFailureReason latestFailure
+                                        }
                                 [] ->
-                                    errPrefix
-                                        ++ "no matching key context (callback probes: "
-                                        ++ callbackProbeSummary
-                                        ++ ")"
+                                    PKESKAttemptFailure
+                                        { pkeskAttemptFailureKeyContext = Nothing
+                                        , pkeskAttemptFailureKind = PKESKAttemptUnwrapFailed
+                                        , pkeskAttemptFailureReason =
+                                            errPrefix
+                                                ++ "no matching key context (callback probes: "
+                                                ++ callbackProbeSummary
+                                                ++ ")"
+                                        }
                      in resolvePKESKCandidates
                             rest
                             skeskErrs
@@ -1529,10 +1627,23 @@
                         let resolverAttempts' = resolverAttemptsAcc ++ newAttempts
                             terminalError =
                                 case reverse previousFailures of
-                                    (latestFailure : _) -> errPrefix ++ pkeskAttemptFailureReason latestFailure
+                                    (latestFailure : _) ->
+                                        PKESKAttemptFailure
+                                            { pkeskAttemptFailureKeyContext =
+                                                pkeskAttemptFailureKeyContext latestFailure
+                                            , pkeskAttemptFailureKind = PKESKAttemptUnwrapFailed
+                                            , pkeskAttemptFailureReason =
+                                                errPrefix ++ pkeskAttemptFailureReason latestFailure
+                                            }
                                     [] ->
-                                        errPrefix
-                                            ++ "key context callback repeated without yielding a usable key"
+                                        PKESKAttemptFailure
+                                            { pkeskAttemptFailureKeyContext =
+                                                recipientKeyContext keyInfo
+                                            , pkeskAttemptFailureKind = PKESKAttemptUnwrapFailed
+                                            , pkeskAttemptFailureReason =
+                                                errPrefix
+                                                    ++ "key context callback repeated without yielding a usable key"
+                                            }
                          in resolvePKESKCandidates
                                 rest
                                 skeskErrs
@@ -1587,8 +1698,8 @@
 
     mkResolutionReport
         :: DecryptSessionKeyResolutionPath
-        -> [String]
-        -> [String]
+        -> [SKESKSessionKeyResolutionError]
+        -> [PKESKAttemptFailure]
         -> [PKESKResolverAttempt]
         -> DecryptSessionKeyResolutionReport
     mkResolutionReport path skeskErrs pkeskErrs resolverAttempts =
@@ -2146,7 +2257,7 @@
                     (material : _) -> pure (encodeLegacyECDHSessionMaterial material)
                     [] ->
                         case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of
-                            Left err -> fail err
+                            Left err -> fail (renderCipherError err)
                             Right _ ->
                                 fail
                                     "legacy ECDH wrapped session key decrypted but decoded session material is malformed"
@@ -2191,7 +2302,7 @@
                                     kek <-
                                         either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)
                                     case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of
-                                        Left err -> fail err
+                                        Left err -> fail (renderCipherError err)
                                         Right decoded -> pure decoded
                                 EdDSAPubKey EdSigningCurve25519 _ -> do
                                     recipientSecretRaw <-
@@ -2214,9 +2325,13 @@
                                             fail
                                             pure
                                             (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)
-                                    let rfc6637Result =
+                                    let rfc6637Result :: Either CipherError B.ByteString
+                                        rfc6637Result =
                                             do
-                                                kek <- deriveECDHKek kdfHA kdfSA sharedSecret param
+                                                kek <-
+                                                    first
+                                                        CipherOperationFailed
+                                                        (deriveECDHKek kdfHA kdfSA sharedSecret param)
                                                 aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes
                                     case rfc6637Result of
                                         Right decoded -> pure decoded
@@ -2229,9 +2344,9 @@
                                                 Left x25519Err ->
                                                     fail
                                                         ( "ECDH PKESKv6 Curve25519 unwrap failed (RFC6637: "
-                                                            ++ rfc6637Err
+                                                            ++ renderCipherError rfc6637Err
                                                             ++ ", X25519: "
-                                                            ++ x25519Err
+                                                            ++ renderCipherError x25519Err
                                                             ++ ")"
                                                         )
                                 EdDSAPubKey EdSigningCurve448 _ ->
@@ -2281,7 +2396,7 @@
         let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString
             kek = deriveX25519Kek ephBytes recipientPublicRaw sharedSecret
         case aesKeyUnwrapRFC3394 AES128 kek wrappedSessionKeyBytes of
-            Left err -> fail err
+            Left err -> fail (renderCipherError err)
             Right decoded -> pure decoded
 
     extractX25519RecipientPublic recipientPKP =
@@ -2342,7 +2457,7 @@
         let sharedSecret = BA.convert (C448.dh ephPub recipientSecret) :: B.ByteString
             kek = deriveX448Kek ephBytes recipientPublicRaw sharedSecret
         case aesKeyUnwrapRFC3394 AES256 kek wrappedSessionKeyBytes of
-            Left err -> fail err
+            Left err -> fail (renderCipherError err)
             Right decoded -> pure decoded
 
     x25519UnwrapV3 recipientCtx mpis recipientSecretRaw = do
@@ -2374,17 +2489,21 @@
             -- RFC9580 interpretation: eskBytes = algo_byte || AES-KW(raw_session_key)
             rfc9580Result = do
                 (sessionAlgorithm, wrappedKey) <-
-                    parsePKESKv3X25519EskBytes eskBytes
-                rawKey <- aesKeyUnwrapRFC3394 AES128 kek9580 wrappedKey
-                expectedLen <- symmetricKeyLength sessionAlgorithm
+                    first
+                        PKESKX25519V3ParseError
+                        (parsePKESKv3X25519EskBytes eskBytes)
+                rawKey <-
+                    first
+                        PKESKX25519V3UnwrapError
+                        (aesKeyUnwrapRFC3394 AES128 kek9580 wrappedKey)
+                expectedLen <-
+                    first PKESKX25519V3KeySizeError (keySize sessionAlgorithm)
                 when (B.length rawKey /= expectedLen) $
                     Left
-                        ( "X25519 PKESKv3 unwrapped session key length mismatch for "
-                            ++ show sessionAlgorithm
-                            ++ ": expected "
-                            ++ show expectedLen
-                            ++ ", got "
-                            ++ show (B.length rawKey)
+                        ( PKESKX25519V3KeyLengthMismatch
+                            sessionAlgorithm
+                            expectedLen
+                            (B.length rawKey)
                         )
                 Right
                     ( B.singleton (fromIntegral (fromFVal sessionAlgorithm))
@@ -2393,7 +2512,7 @@
                     )
         case rfc9580Result of
             Right result -> pure result
-            Left rfc9580Err ->
+            Left err ->
                 -- Fallback: legacy ECDH interpretation where the full eskBytes is
                 -- AES-KW(algo || key || checksum || padding).  Try with the RFC9580
                 -- X25519 KEK and, when available, the RFC6637 ECDH KEK derived from
@@ -2420,7 +2539,7 @@
                         [] ->
                             fail
                                 ( "X25519 PKESKv3 unwrap failed (RFC9580: "
-                                    ++ rfc9580Err
+                                    ++ renderPKESKX25519V3UnwrapError err
                                     ++ "; legacy ECDH-style fallback also failed)"
                                 )
 
@@ -2454,10 +2573,6 @@
         ) =
         sizeField
 
-edPointBytes :: EdPoint -> B.ByteString
-edPointBytes (PrefixedNativeEPoint (EPoint x)) = i2osp x
-edPointBytes (NativeEPoint (EPoint x)) = i2osp x
-
 parseECDHPKESKMPIs
     :: NonEmpty MPI -> Either String (B.ByteString, B.ByteString)
 parseECDHPKESKMPIs (ephemeralMPI :| [wrappedMPI]) =
@@ -2472,17 +2587,15 @@
     }
     deriving (Eq)
 
-newtype LegacyECDHSessionKey = LegacyECDHSessionKey {unLegacyECDHSessionKey :: B.ByteString}
+newtype LegacyECDHSessionKey = LegacyECDHSessionKey B.ByteString
 
-newtype LegacyECDHSessionPadding = LegacyECDHSessionPadding
-    {unLegacyECDHSessionPadding :: B.ByteString}
+newtype LegacyECDHSessionPadding = LegacyECDHSessionPadding B.ByteString
 
 data LegacyECDHDecodedSessionMaterial
     = LegacyECDHDecodedSessionMaterial
-    { legacyECDHSessionAlgorithm :: SymmetricAlgorithm
-    , legacyECDHSessionKey :: LegacyECDHSessionKey
-    , legacyECDHSessionPadding :: LegacyECDHSessionPadding
-    }
+        SymmetricAlgorithm
+        LegacyECDHSessionKey
+        LegacyECDHSessionPadding
 
 candidateWrappedRFC3394CiphertextsForLegacyECDH
     :: LegacyECDHWrappedRFC3394Ciphertext
@@ -2519,7 +2632,8 @@
     when (B.length decoded < 3) $
         Left "legacy ECDH decoded session material is too short"
     let sessionAlgorithm = toFVal (B.head decoded)
-    sessionKeyLen <- symmetricKeyLength sessionAlgorithm
+    sessionKeyLen <-
+        first renderCipherError (keySize sessionAlgorithm)
     let payload = B.tail decoded
     when (B.length payload < sessionKeyLen + 2) $
         Left
@@ -2732,10 +2846,11 @@
     :: SymmetricAlgorithm
     -> B.ByteString
     -> B.ByteString
-    -> Either String B.ByteString
+    -> Either CipherError B.ByteString
 aesKeyUnwrapRFC3394 sa kek wrapped =
     withAESCipher
-        "ECDH PKESK currently supports AES KEK algorithms only"
+        (\err -> CipherInitFailed sa (show err))
+        (UnsupportedAlgorithm sa)
         sa
         kek
         unwrapWithCipher
@@ -2743,19 +2858,26 @@
     unwrapWithCipher
         :: CCT.BlockCipher cipher
         => cipher
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     unwrapWithCipher cipher = do
         when (B.length wrapped < 24 || B.length wrapped `mod` 8 /= 0) $
             Left
-                "ECDH wrapped session key must be at least 24 octets and a multiple of 8"
+                ( CipherOperationFailed
+                    "ECDH wrapped session key must be at least 24 octets and a multiple of 8"
+                )
         let (a0, rBytes) = B.splitAt 8 wrapped
             rs = chunksOf8 rBytes
         when (length rs < 2) $
             Left
-                "ECDH wrapped session key must contain at least two 64-bit blocks"
+                ( CipherOperationFailed
+                    "ECDH wrapped session key must contain at least two 64-bit blocks"
+                )
         (aFinal, rFinal) <- unwrapRounds cipher a0 rs
         when (aFinal /= B.replicate 8 0xA6) $
-            Left "ECDH wrapped session key integrity check failed"
+            Left
+                ( CipherOperationFailed
+                    "ECDH wrapped session key integrity check failed"
+                )
         Right (B.concat rFinal)
 
     unwrapRounds
@@ -2763,7 +2885,7 @@
         => cipher
         -> B.ByteString
         -> [B.ByteString]
-        -> Either String (B.ByteString, [B.ByteString])
+        -> Either CipherError (B.ByteString, [B.ByteString])
     unwrapRounds cipher aInit rsInit = goJ 5 aInit rsInit
       where
         n = length rsInit
@@ -2784,16 +2906,6 @@
                         rsNext = (ix (i - 1) .~ rNext) rsCurrent
                     goI (i - 1) aNext rsNext
 
-chunksOf8 :: B.ByteString -> [B.ByteString]
-chunksOf8 bs
-    | B.null bs = []
-    | otherwise =
-        let (h, t) = B.splitAt 8 bs
-         in h : chunksOf8 t
-
-xorBS :: B.ByteString -> B.ByteString -> B.ByteString
-xorBS a b = B.pack (B.zipWith xor a b)
-
 decodePKESKSessionKey
     :: Maybe SymmetricAlgorithm
     -> B.ByteString
@@ -2814,7 +2926,7 @@
                             ++ renderEncodedSessionKeyError decodeErr
                         )
                 Just expected -> do
-                    expectedLen <- symmetricKeyLength expected
+                    expectedLen <- first renderCipherError (keySize expected)
                     case decodeExpectedRawOrPaddedSessionKey
                         expected
                         expectedLen
@@ -2912,19 +3024,3 @@
         when (B.any (/= fromIntegral padLen) padBytes) $
             Left
                 "v6 ECDH decoded session material has invalid PKCS#7-style padding bytes"
-
-symmetricKeyLength :: SymmetricAlgorithm -> Either String Int
-symmetricKeyLength = first renderCipherError . keySize
-
-checksum16 :: B.ByteString -> Word16
-checksum16 =
-    fromIntegral
-        . B.foldl'
-            (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Integer))
-            0
-
-checksum16Bytes :: B.ByteString -> B.ByteString
-checksum16Bytes sessionKey =
-    B.pack [fromIntegral (chk `shiftR` 8), fromIntegral chk]
-  where
-    chk = checksum16 sessionKey
diff --git a/Data/Conduit/OpenPGP/Filter.hs b/Data/Conduit/OpenPGP/Filter.hs
--- a/Data/Conduit/OpenPGP/Filter.hs
+++ b/Data/Conduit/OpenPGP/Filter.hs
@@ -9,7 +9,6 @@
 module Data.Conduit.OpenPGP.Filter
     ( conduitPktFilter
     , conduitPktWithExtraFilter
-    , conduitTKFilter
     , FilterPredicates (..)
     , runPredicate
     ) where
@@ -17,27 +16,21 @@
 import Control.Monad.Trans.Reader (Reader, runReader)
 import Data.Conduit (ConduitT)
 import qualified Data.Conduit.List as CL
-import Data.Typeable (Typeable, eqT, (:~:) (Refl))
+import Data.Typeable (Typeable)
 import Data.Void (Void)
 
 import Codec.Encryption.OpenPGP.Types
 
 data FilterPredicates r a
-    = -- | fp for transferable keys
-      RTKFilterPredicate (Reader TKUnknown Bool)
-    | -- | fp for context-less packets
+    = -- | fp for context-less packets
       RPFilterPredicate (Reader Pkt Bool)
     | -- | generic filter predicate
       RFilterPredicate (Reader a Bool)
     | -- | generic filter predicate with additional context
       RPairFilterPredicate (Reader (r, a) Bool)
-{-# DEPRECATED RTKFilterPredicate "Use RFilterPredicate with SomeTK instead" #-}
 
 runPredicate
     :: forall r a. Typeable a => FilterPredicates r a -> a -> Bool
-runPredicate (RTKFilterPredicate e) = case eqT @a @TKUnknown of
-    Just Refl -> runReader e
-    Nothing -> const False
 runPredicate (RFilterPredicate e) = runReader e
 runPredicate _ = const False
 
@@ -49,21 +42,6 @@
 superPredicate (RPFilterPredicate e) p = runReader e p
 superPredicate (RFilterPredicate e) p = runReader e p
 superPredicate _ _ = False -- do not match incorrect type of packet
-
-{-# DEPRECATED
-    conduitTKFilter
-    "Use (CL.filter . runPredicate) with RFilterPredicate instead"
-    #-}
-conduitTKFilter
-    :: Monad m
-    => FilterPredicates Void TKUnknown
-    -> ConduitT TKUnknown TKUnknown m ()
-conduitTKFilter = CL.filter . superTKPredicate
-
-superTKPredicate
-    :: FilterPredicates Void TKUnknown -> TKUnknown -> Bool
-superTKPredicate (RTKFilterPredicate e) = runReader e
-superTKPredicate (RFilterPredicate e) = runReader e
 
 conduitPktWithExtraFilter
     :: Monad m => r -> FilterPredicates r Pkt -> ConduitT Pkt Pkt m ()
diff --git a/Data/Conduit/OpenPGP/Keyring.hs b/Data/Conduit/OpenPGP/Keyring.hs
--- a/Data/Conduit/OpenPGP/Keyring.hs
+++ b/Data/Conduit/OpenPGP/Keyring.hs
@@ -19,8 +19,6 @@
     , authSecretSubkeysAtReport
     , conduitToAuthSecretSubkeysAt
     , conduitToAuthSecretSubkeysAtReport
-    , conduitToTKsEither
-    , conduitToTKsDroppingEither
     , conduitToTKsWithWireRepEither
     , conduitToTKsDroppingWithWireRepEither
     , conduitDropErrorsAndNothings
@@ -97,8 +95,15 @@
     :: (Monad m)
     => ConduitT Pkt (Either TypedTKConduitError (Maybe SomeTK)) m ()
 conduitToSomeTKsEither =
-    conduitToTKsEither
+    CL.filter notTrustPacket
+        .| CL.map (: [])
+        .| fakecmAccumEither
+            finalizeParsingEither
+            (parseAChunkEither (anyTK True))
+            ([], Just (Nothing, anyTK True))
         .| CL.map toTypedSomeTKEither
+  where
+    notTrustPacket = not . isTrustPkt
 
 {- | Tolerant typed conduit (broken transferable-key chunks may be omitted),
 while still surfacing parse+conversion failures.
@@ -107,8 +112,15 @@
     :: (Monad m)
     => ConduitT Pkt (Either TypedTKConduitError (Maybe SomeTK)) m ()
 conduitToSomeTKsDroppingEither =
-    conduitToTKsDroppingEither
+    CL.filter notTrustPacket
+        .| CL.map (: [])
+        .| fakecmAccumEither
+            finalizeParsingEither
+            (parseAChunkEither (anyTK False))
+            ([], Just (Nothing, anyTK False))
         .| CL.map toTypedSomeTKEither
+  where
+    notTrustPacket = not . isTrustPkt
 
 toTypedSomeTKEither
     :: Either KeyringChunkParseError (Maybe TKUnknown)
@@ -404,29 +416,6 @@
         id
         sig
 
-{-# DEPRECATED conduitToTKsEither "Use conduitToSomeTKsEither instead" #-}
-conduitToTKsEither
-    :: Monad m
-    => ConduitT
-        Pkt
-        (Either KeyringChunkParseError (Maybe TKUnknown))
-        m
-        ()
-conduitToTKsEither = conduitToTKsEither' True
-
-{-# DEPRECATED
-    conduitToTKsDroppingEither
-    "Use conduitToSomeTKsDroppingEither instead"
-    #-}
-conduitToTKsDroppingEither
-    :: Monad m
-    => ConduitT
-        Pkt
-        (Either KeyringChunkParseError (Maybe TKUnknown))
-        m
-        ()
-conduitToTKsDroppingEither = conduitToTKsEither' False
-
 conduitToTKsWithWireRepEither
     :: Monad m
     => ConduitT
@@ -475,24 +464,6 @@
     :: Monad m => ConduitT (Either e (Maybe a)) a m ()
 conduitDropErrorsAndNothings =
     CL.mapMaybe (join . hush)
-
-conduitToTKsEither'
-    :: Monad m
-    => Bool
-    -> ConduitT
-        Pkt
-        (Either KeyringChunkParseError (Maybe TKUnknown))
-        m
-        ()
-conduitToTKsEither' intolerant =
-    CL.filter notTrustPacket
-        .| CL.map (: [])
-        .| fakecmAccumEither
-            finalizeParsingEither
-            (parseAChunkEither (anyTK intolerant))
-            ([], Just (Nothing, anyTK intolerant))
-  where
-    notTrustPacket = not . isTrustPkt
 
 conduitToTKsWithWireRepEither'
     :: Monad m
diff --git a/Data/Conduit/OpenPGP/Keyring/Instances.hs b/Data/Conduit/OpenPGP/Keyring/Instances.hs
--- a/Data/Conduit/OpenPGP/Keyring/Instances.hs
+++ b/Data/Conduit/OpenPGP/Keyring/Instances.hs
@@ -15,21 +15,14 @@
 import Control.Lens (folded, (^.), (^..), _1)
 import Data.Data.Lens (biplate)
 import Data.Either (rights)
-import Data.Function (on)
-import qualified Data.HashMap.Lazy as HashMap
 import Data.IxSet.Typed (Indexable (..), ixFun, ixList)
-import Data.List (nub, sort)
 import qualified Data.List.NonEmpty as NE
-import qualified Data.Map as Map
-import Data.Semigroup (Semigroup, (<>))
 import Data.Text (Text)
 
 import Codec.Encryption.OpenPGP.Fingerprint
     ( eightOctetKeyID
     , fingerprint
     )
-import Codec.Encryption.OpenPGP.Internal (issuer)
-import Codec.Encryption.OpenPGP.SignatureQualities (sigCT)
 import Codec.Encryption.OpenPGP.Types
 
 instance Indexable KeyringIxs TKUnknown where
diff --git a/hOpenPGP.cabal b/hOpenPGP.cabal
--- a/hOpenPGP.cabal
+++ b/hOpenPGP.cabal
@@ -1,6 +1,6 @@
 Cabal-version:       3.4
 Name:                hOpenPGP
-Version:             3.2.1
+Version:             3.3
 Synopsis:            native Haskell implementation of OpenPGP (RFC9580)
 Description:         native Haskell implementation of OpenPGP (RFC9580), with some backwards compatibility
 Homepage:            https://salsa.debian.org/clint/hOpenPGP
@@ -231,6 +231,8 @@
                      , Codec.Encryption.OpenPGP.Policy
                      , Codec.Encryption.OpenPGP.S2K
                      , Codec.Encryption.OpenPGP.SecretKey
+                     , Codec.Encryption.OpenPGP.SEIPDv1
+                     , Codec.Encryption.OpenPGP.SEIPDv2
                      , Codec.Encryption.OpenPGP.Serialize
                      , Codec.Encryption.OpenPGP.Signatures
                      , Codec.Encryption.OpenPGP.SignatureQualities
@@ -248,7 +250,6 @@
                      , Codec.Encryption.OpenPGP.Internal.CryptoAES
                      , Codec.Encryption.OpenPGP.Internal.CryptoCipherTypes
                      , Codec.Encryption.OpenPGP.Internal.CryptoECDH
-                     , Codec.Encryption.OpenPGP.Internal.CryptoSEIPDv2
                      , Codec.Encryption.OpenPGP.Internal.Crypton
                      , Codec.Encryption.OpenPGP.Internal.HOBlockCipher
                      , Codec.Encryption.OpenPGP.Internal.RFC7253OCB
@@ -281,6 +282,8 @@
                      , Codec.Encryption.OpenPGP.Policy
                      , Codec.Encryption.OpenPGP.S2K
                      , Codec.Encryption.OpenPGP.SecretKey
+                     , Codec.Encryption.OpenPGP.SEIPDv1
+                     , Codec.Encryption.OpenPGP.SEIPDv2
                      , Codec.Encryption.OpenPGP.Serialize
                      , Codec.Encryption.OpenPGP.Signatures
                      , Codec.Encryption.OpenPGP.SignatureQualities
@@ -337,4 +340,4 @@
 source-repository this
   type:     git
   location: https://salsa.debian.org/clint/hOpenPGP.git
-  tag:      v3.2.1
+  tag:      v3.3
diff --git a/tests/Tests/Common.hs b/tests/Tests/Common.hs
--- a/tests/Tests/Common.hs
+++ b/tests/Tests/Common.hs
@@ -81,10 +81,7 @@
     , conduitDecryptWithCandidatesCallbackAndPolicy
     , conduitDecryptWithDecryptPolicy
     , deriveECDHKekForTest
-    , deriveX25519KekForTest
-    , deriveX448KekForTest
     , doPkeyAndSkeyMatch
-    , encodeChecksum16
     , forceVersionedRecipientIdentifier
     , isPrecedingESK
     , mkPKESKSessionMaterialOrFail
@@ -108,9 +105,6 @@
 import Control.Monad (join, unless, void)
 import Control.Monad.Trans.Resource (ResourceT)
 import qualified Crypto.Error as CE
-import qualified Crypto.Hash as CH
-import qualified Crypto.Hash.Algorithms as CHA
-import Crypto.KDF.HKDF (expand, extract)
 import Crypto.Number.Serialize (os2ip)
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import qualified Crypto.PubKey.ECC.Types as ECCT
@@ -128,8 +122,6 @@
     , getWord8
     , runGetOrFail
     )
-import Data.Binary.Put (putWord64be, runPut)
-import Data.Bits (xor)
 import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Base16.Lazy as B16L
@@ -147,7 +139,7 @@
 import Data.Text (Text)
 import Data.Time.Clock (UTCTime)
 import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
-import Data.Word (Word32, Word64)
+import Data.Word (Word32)
 import System.IO.Unsafe (unsafePerformIO)
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit
@@ -157,13 +149,15 @@
     , assertFailure
     , testCase
     )
-import qualified "crypton" Crypto.Cipher.AES as AES
-import qualified "crypton" Crypto.Cipher.Types as CCT
 
 import Codec.Encryption.OpenPGP.Arbitrary ()
+import Codec.Encryption.OpenPGP.BlockCipher
+    ( renderCipherError
+    )
 import Codec.Encryption.OpenPGP.Compression (decompressPkt)
 import Codec.Encryption.OpenPGP.Encrypt
     ( PKESKSessionMaterial
+    , aesKeyWrapRFC3394
     , encodeOpenPGPSessionMaterial
     , mkPKESKSessionMaterial
     )
@@ -172,20 +166,22 @@
     , fingerprint
     )
 import Codec.Encryption.OpenPGP.Internal
-    ( curveFromCurve
-    , curveToCurveoidBS
+    ( checksum16Bytes
     , emptyPSC
     , lastPrimaryKey
     , lastSubkey
     , lastUIDorUAt
     )
+import Codec.Encryption.OpenPGP.Internal.CryptoECDH
+    ( buildECDHKDFParam
+    , deriveECDHKek
+    )
 import Codec.Encryption.OpenPGP.KeySelection (parseFingerprint)
 import Codec.Encryption.OpenPGP.Message
     ( ClearPayload
     , EncryptMessageOptions (..)
     , EncryptedPayload
     , MessageError (..)
-    , Passphrase
     , RecoveredSessionMaterial (..)
     , SessionMaterialExposure (..)
     , VersionedPKPayload
@@ -493,7 +489,7 @@
         expected =
             B.singleton (fromFVal AES256)
                 <> keyBytes
-                <> encodeChecksum16 keyBytes
+                <> checksum16Bytes keyBytes
     case encodeOpenPGPSessionMaterial AES256 (SessionKey keyBytes) of
         Left err ->
             assertFailure
@@ -581,17 +577,13 @@
     -> SymmetricAlgorithm
     -> B.ByteString
 buildECDHKDFParamForTest recipientPKP pka curve kdfHA kdfSA =
-    B.singleton (fromIntegral (B.length curveOid))
-        <> curveOid
-        <> B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA]
-        <> "Anonymous Sender    "
-        <> BL.toStrict (unFingerprint (fingerprint recipientPKP))
+    either
+        error
+        id
+        (buildECDHKDFParam recipientPKP pka pkey kdfHA kdfSA)
   where
-    curveOid =
-        either
-            (const B.empty)
-            id
-            (curveToCurveoidBS (curveFromCurve curve))
+    pkey =
+        ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve ECCT.PointO))
 
 buildCurve25519LegacyKdfParamForTest
     :: SomePKPayload
@@ -600,13 +592,13 @@
     -> SymmetricAlgorithm
     -> B.ByteString
 buildCurve25519LegacyKdfParamForTest recipientPKP pka kdfHA kdfSA =
-    B.singleton (fromIntegral (B.length curveOid))
-        <> curveOid
-        <> B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA]
-        <> "Anonymous Sender    "
-        <> BL.toStrict (unFingerprint (fingerprint recipientPKP))
+    either
+        error
+        id
+        (buildECDHKDFParam recipientPKP pka dummyKey kdfHA kdfSA)
   where
-    curveOid = "\x2b\x06\x01\x04\x01\x97\x55\x01\x05\x01"
+    dummyKey =
+        EdDSAPubKey EdSigningCurve25519 (PrefixedNativeEPoint (EPoint 0))
 
 deriveECDHKekForTest
     :: HashAlgorithm
@@ -615,58 +607,7 @@
     -> B.ByteString
     -> B.ByteString
 deriveECDHKekForTest kdfHA kdfSA sharedSecret kdfParam =
-    B.take (keyLengthForTest kdfSA) digest
-  where
-    digest =
-        case kdfHA of
-            SHA256 ->
-                BA.convert
-                    ( CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)
-                        :: CH.Digest CHA.SHA256
-                    )
-            SHA384 ->
-                BA.convert
-                    ( CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)
-                        :: CH.Digest CHA.SHA384
-                    )
-            SHA512 ->
-                BA.convert
-                    ( CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)
-                        :: CH.Digest CHA.SHA512
-                    )
-            _ ->
-                BA.convert
-                    ( CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)
-                        :: CH.Digest CHA.SHA256
-                    )
-
-deriveX448KekForTest
-    :: B.ByteString
-    -> B.ByteString
-    -> B.ByteString
-    -> B.ByteString
-deriveX448KekForTest ephemeralPublic recipientPublic sharedSecret =
-    let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
-        prk = extract @CHA.SHA512 B.empty ikm
-        info = "OpenPGP X448" :: B.ByteString
-     in expand @CHA.SHA512 prk info 32
-
-deriveX25519KekForTest
-    :: B.ByteString
-    -> B.ByteString
-    -> B.ByteString
-    -> B.ByteString
-deriveX25519KekForTest ephemeralPublic recipientPublic sharedSecret =
-    let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
-        prk = extract @CHA.SHA256 B.empty ikm
-        info = "OpenPGP X25519" :: B.ByteString
-     in expand @CHA.SHA256 prk info 16
-
-keyLengthForTest :: SymmetricAlgorithm -> Int
-keyLengthForTest AES128 = 16
-keyLengthForTest AES192 = 24
-keyLengthForTest AES256 = 32
-keyLengthForTest _ = 16
+    either error id (deriveECDHKek kdfHA kdfSA sharedSecret kdfParam)
 
 aesKeyWrapRFC3394ForTest
     :: SymmetricAlgorithm
@@ -674,62 +615,10 @@
     -> B.ByteString
     -> B.ByteString
 aesKeyWrapRFC3394ForTest sa kek plain =
-    case sa of
-        AES128 -> wrapWithCipher (initCipher kek :: AES.AES128) plain
-        AES192 -> wrapWithCipher (initCipher kek :: AES.AES192) plain
-        AES256 -> wrapWithCipher (initCipher kek :: AES.AES256) plain
-        _ -> error "unsupported KEK algorithm in test"
-  where
-    initCipher keyBytes =
-        case CE.eitherCryptoError (CCT.cipherInit keyBytes) of
-            Left err -> error ("cipher init failed: " ++ show err)
-            Right c -> c
-    wrapWithCipher cipher plainBytes =
-        let rs = chunksOf8ForTest plainBytes
-            n = length rs
-            a0 = B.replicate 8 0xA6
-            (aFinal, rFinal) =
-                foldl (\(a, r) j -> wrapRound cipher n j a r) (a0, rs) [0 .. 5]
-         in aFinal <> B.concat rFinal
-    wrapRound cipher n j a rs = foldl step (a, rs) [1 .. n]
-      where
-        step (aCurr, rCurr) i =
-            let b = CCT.ecbEncrypt cipher (aCurr <> (rCurr !! (i - 1)))
-                (aMsb, rLsb) = B.splitAt 8 b
-                t = fromIntegral (n * j + i) :: Word64
-                aNext = xorBSForTest aMsb (encodeWord64beForTest t)
-             in (aNext, replaceAtForTest (i - 1) rLsb rCurr)
-
-encodeWord64beForTest :: Word64 -> B.ByteString
-encodeWord64beForTest = BL.toStrict . runPut . putWord64be
-
-chunksOf8ForTest :: B.ByteString -> [B.ByteString]
-chunksOf8ForTest bs
-    | B.null bs = []
-    | otherwise =
-        let (h, t) = B.splitAt 8 bs
-         in h : chunksOf8ForTest t
-
-replaceAtForTest :: Int -> a -> [a] -> [a]
-replaceAtForTest idx x xs =
-    let (prefix, suffix) = splitAt idx xs
-     in case suffix of
-            [] -> xs
-            (_ : rest) -> prefix <> (x : rest)
-
-xorBSForTest :: B.ByteString -> B.ByteString -> B.ByteString
-xorBSForTest a b = B.pack (B.zipWith xor a b)
-
-encodeChecksum16 :: B.ByteString -> B.ByteString
-encodeChecksum16 bs =
-    B.pack
-        [fromIntegral (s `div` 256), fromIntegral (s `mod` 256)]
-  where
-    s =
-        B.foldl'
-            (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Int))
-            0
-            bs
+    either
+        (error . renderCipherError)
+        id
+        (aesKeyWrapRFC3394 sa kek plain)
 
 verifyMessageFromPackets
     :: PublicKeyring -> BL.ByteString -> [Either String Verification]
@@ -1013,7 +902,7 @@
                 V4
                 0
                 0
-                EdDSA
+                EdDSALegacy
                 ( EdDSAPubKey
                     EdSigningCurve25519
                     ( PrefixedNativeEPoint
@@ -1041,7 +930,7 @@
                 V6
                 0
                 0
-                EdDSA
+                EdDSALegacy
                 ( EdDSAPubKey
                     EdSigningCurve25519
                     (NativeEPoint (EPoint (os2ip publicKeyBytes)))
@@ -1067,7 +956,7 @@
                 V4
                 0
                 0
-                EdDSA
+                EdDSALegacy
                 ( EdDSAPubKey
                     EdSigningCurve448
                     ( PrefixedNativeEPoint
@@ -1095,7 +984,7 @@
                 V6
                 0
                 0
-                EdDSA
+                EdDSALegacy
                 ( EdDSAPubKey
                     EdSigningCurve448
                     (NativeEPoint (EPoint (os2ip publicKeyBytes)))
diff --git a/tests/Tests/Encryption.hs b/tests/Tests/Encryption.hs
--- a/tests/Tests/Encryption.hs
+++ b/tests/Tests/Encryption.hs
@@ -92,7 +92,6 @@
     , RecipientEncryptionTarget (..)
     , RecipientEncryptionTargetRejected (..)
     , RecipientEncryptionTargetsReport (..)
-    , RecipientPKESKVersionStrategy (..)
     , RecipientPKESKVersionStrategyW (..)
     , RecipientPayloadShape (..)
     , RecipientTargetRejectionReason (..)
@@ -102,6 +101,8 @@
     , buildPKESKv3PayloadForRecipient
     , canonicalizePKESKRecipientId
     , defaultRecipientPayloadShape
+    , deriveX25519Kek
+    , deriveX448Kek
     , encryptForRecipients
     , encryptForRecipientsLegacy
     , encryptForRecipientsWithCapabilityNegotiation
@@ -120,7 +121,10 @@
     , recipientVersionStrategyForProfileTyped
     )
 import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)
-import Codec.Encryption.OpenPGP.Internal (point2MBS)
+import Codec.Encryption.OpenPGP.Internal
+    ( checksum16Bytes
+    , point2MBS
+    )
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
     ( HOBlockCipher (..)
     )
@@ -145,6 +149,15 @@
     , skesk2SessionKey
     , string2Key
     )
+import Codec.Encryption.OpenPGP.SEIPDv1
+    ( mdcTrailerForSEIPDv1
+    , renderMDCFailure
+    , seipdv1NonceFromIV
+    , validateSEIPD1MDC
+    )
+import Codec.Encryption.OpenPGP.SEIPDv2
+    ( renderSEIPDv2Failure
+    )
 import Codec.Encryption.OpenPGP.SecretKey
     ( decryptPrivateKey
     , encryptPrivateKeyWithPolicyAndSaltAndIV
@@ -156,6 +169,7 @@
     ( DecryptKeyResolution (..)
     , DecryptOptions (..)
     , DecryptOutcome (..)
+    , DecryptStructureError (..)
     , PKESKRecipientKey (..)
     )
 import qualified Data.Conduit.OpenPGP.Decrypt as DCD
@@ -179,10 +193,7 @@
     , conduitDecryptWithDecryptPolicy
     , conduitDecryptWithPKESKContext
     , deriveECDHKekForTest
-    , deriveX25519KekForTest
-    , deriveX448KekForTest
     , doPkeyAndSkeyMatch
-    , encodeChecksum16
     , encryptMessageDefault
     , fixturePath
     , forceVersionedRecipientIdentifier
@@ -773,13 +784,13 @@
                 "encryptForRecipients rejects SEIPDv1 when recipients do not advertise MDC support"
                 testEncryptRecipientsRejectsSEIPDv1WhenRecipientsDoNotAdvertiseMDC
             , testCase
-                "recipientEncryptionTargetsFromTK returns only encryption-capable candidates"
+                "recipientEncryptionTargetsFromTKAtTimestamp returns only encryption-capable candidates"
                 testRecipientEncryptionTargetsFromTKIncludesOnlyEncryptionCapableKeys
             , testCase
-                "recipientEncryptionTargetFromTK prefers encryption-capable subkeys"
+                "recipientEncryptionTargetFromTKWithPolicy prefers encryption-capable subkeys"
                 testRecipientEncryptionTargetFromTKPrefersSubkeyOverPrimary
             , testCase
-                "recipientEncryptionTargetFromTK rejects TKs without encryptable key material"
+                "recipientEncryptionTargetFromTKWithPolicy rejects TKs without encryptable key material"
                 testRecipientEncryptionTargetFromTKRejectsTKWithoutEncryptableKeys
             , testCase
                 "recipientEncryptionTargetsReportFromTKAtTimestamp reports sign-only subkey rejection reasons"
@@ -797,7 +808,7 @@
                 "recipientEncryptionTargetsFromTKAtTimestamp extracts effective self-signature capabilities"
                 testRecipientEncryptionTargetsFromTKAtTimestampExtractsSelfSigCapabilities
             , testCase
-                "recipientEncryptionTargetFromTKAtTimestamp filters subkey self-signatures by timestamp"
+                "recipientEncryptionTargetFromTKAtTimestampWithPolicy filters subkey self-signatures by timestamp"
                 testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering
             , testCase
                 "recipientEncryptionTargetFromTKAtTimestampWithPolicy can prefer primary key"
@@ -806,10 +817,10 @@
                 "recipientEncryptionTargetFromTKAtTimestampWithPolicy can prefer newest key"
                 testRecipientEncryptionTargetFromTKAtTimestampWithPolicyPrefersNewest
             , testCase
-                "recipientEncryptionTargetsFromTK excludes subkeys with non-encrypt key flags"
+                "recipientEncryptionTargetsFromTKAtTimestamp excludes subkeys with non-encrypt key flags"
                 testRecipientEncryptionTargetsFromTKRejectsKeyWithNonEncryptFlags
             , testCase
-                "recipientEncryptionTargetsFromTK includes subkeys with no key flags advertised"
+                "recipientEncryptionTargetsFromTKAtTimestamp includes subkeys with no key flags advertised"
                 testRecipientEncryptionTargetsFromTKIncludesKeyWithNoFlags
             , testCase
                 "encryptForRecipients optionally emits one-pass signature packets"
@@ -1068,7 +1079,8 @@
     payloadOut <-
         either
             ( \err ->
-                assertFailure ("validateSEIPD1MDC failed: " ++ err)
+                assertFailure
+                    ("validateSEIPD1MDC failed: " ++ renderMDCFailure err)
                     >> pure mempty
             )
             pure
@@ -1300,10 +1312,8 @@
                 ( "Expected DecryptMalformedStructure but got exception: "
                     ++ show err
                 )
-        Right (DecryptMalformedStructure reason, _) ->
-            assertBool
-                ("Expected 'after message integrity boundary', got: " ++ reason)
-                ("after message integrity boundary" `isInfixOf` reason)
+        Right (DecryptMalformedStructure DecryptStructureTrailingData, _) ->
+            pure ()
         Right (other, _) ->
             assertFailure
                 ( "Expected strict policy to reject trailing data but got: "
@@ -1398,12 +1408,8 @@
                 ( "Expected DecryptMalformedStructure but got exception: "
                     ++ show err
                 )
-        Right (DecryptMalformedStructure reason, _) ->
-            assertBool
-                ("Expected ESK prelude shape failure, got: " ++ reason)
-                ( "ESK packets must immediately precede encrypted data"
-                    `isInfixOf` reason
-                )
+        Right (DecryptMalformedStructure DecryptStructureESKOrder, _) ->
+            pure ()
         Right (other, _) ->
             assertFailure
                 ("Expected DecryptMalformedStructure but got: " ++ show other)
@@ -1434,10 +1440,9 @@
                 ( "Expected DecryptMalformedStructure but got exception: "
                     ++ show err
                 )
-        Right (DecryptMalformedStructure reason, _) ->
-            assertBool
-                ("Expected payload/ESK alignment failure, got: " ++ reason)
-                ("version-aligned" `isInfixOf` reason)
+        Right
+            (DecryptMalformedStructure DecryptStructureESKSEIPDMismatch, _) ->
+                pure ()
         Right (other, _) ->
             assertFailure
                 ("Expected DecryptMalformedStructure but got: " ++ show other)
@@ -1874,7 +1879,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -1950,7 +1956,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -2066,7 +2073,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -2295,7 +2303,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -3812,17 +3821,19 @@
                 V6
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))
         sessionKey = SessionKey (B.replicate 32 0x19)
     sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey
     result <-
         buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial
     case result of
-        Left (UnsupportedRecipientAlgorithm EdDSA) -> pure ()
+        Left (UnsupportedRecipientAlgorithm EdDSALegacy) -> pure ()
         Left err ->
             assertFailure
-                ("Expected UnsupportedRecipientAlgorithm EdDSA, got " ++ show err)
+                ( "Expected UnsupportedRecipientAlgorithm EdDSALegacy, got "
+                    ++ show err
+                )
         Right payload ->
             assertFailure
                 ( "Expected UnsupportedRecipientAlgorithm, got payload: "
@@ -4192,7 +4203,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -4246,7 +4258,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     result <-
@@ -4329,7 +4342,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     let pkesk =
@@ -4420,7 +4434,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     let pkesk =
@@ -4519,7 +4534,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     let pkesk =
@@ -4573,7 +4589,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         sharedSecret =
             BA.convert
@@ -4636,7 +4652,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -4682,7 +4699,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         sharedSecret =
             BA.convert
@@ -4746,7 +4763,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     result <-
@@ -4814,7 +4832,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -4860,7 +4878,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -4945,7 +4964,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -4993,7 +5012,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -5045,7 +5065,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5120,7 +5140,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -5172,7 +5193,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5333,7 +5354,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5381,7 +5402,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -5468,7 +5490,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5516,7 +5538,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -5604,7 +5627,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5652,7 +5675,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     (report, decrypted) <-
@@ -5762,7 +5786,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5809,7 +5833,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     result <-
@@ -5880,7 +5905,7 @@
                 Right pk -> pk
         sharedSecret = BA.convert (C25519.dh recipientPub ephSecret) :: B.ByteString
         kek =
-            deriveX25519KekForTest
+            deriveX25519Kek
                 ephPublicRaw
                 recipientPublicRaw
                 sharedSecret
@@ -5963,7 +5988,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -6051,7 +6077,8 @@
             sessionKey
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -6097,7 +6124,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         sharedSecret =
             BA.convert
@@ -6160,7 +6187,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     result <-
@@ -6242,7 +6270,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         sharedSecret =
             BA.convert
@@ -6295,7 +6323,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -6390,7 +6419,7 @@
                       encodedSession =
                         B.singleton (fromFVal AES256)
                             <> sessionKeyBytes
-                            <> encodeChecksum16 sessionKeyBytes
+                            <> checksum16Bytes sessionKeyBytes
                             <> B.replicate 5 0
                       wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession
                    in (SessionKey sessionKeyBytes, wrappedSession)
@@ -6440,7 +6469,8 @@
                     sessionKey
                     (BL.toStrict (runPut (put literalBlock))) of
                     Left err ->
-                        assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                        assertFailure
+                            ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                             >> pure mempty
                     Right ct -> pure ct
             decrypted <-
@@ -6514,7 +6544,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         sharedSecret =
             BA.convert
@@ -6567,7 +6597,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     result <-
@@ -6606,7 +6637,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         recipientSecret =
             case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of
@@ -6626,7 +6657,7 @@
             BA.convert (C448.dh (C448.toPublic recipientSecret) ephSecret)
                 :: B.ByteString
         kek =
-            deriveX448KekForTest ephPublicRaw recipientPublicRaw sharedSecret
+            deriveX448Kek ephPublicRaw recipientPublicRaw sharedSecret
         wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession
         esk =
             ephPublicRaw
@@ -6677,7 +6708,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     decrypted <-
@@ -6710,7 +6742,7 @@
         encodedSession =
             B.singleton (fromFVal AES256)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
                 <> B.replicate 5 0
         recipientSecret =
             case CE.eitherCryptoError (C448.secretKey recipientSecretRaw) of
@@ -6731,7 +6763,7 @@
             BA.convert (C448.dh (C448.toPublic recipientSecret) ephSecret)
                 :: B.ByteString
         kek =
-            deriveX448KekForTest ephPublicRaw recipientPublicRaw sharedSecret
+            deriveX448Kek ephPublicRaw recipientPublicRaw sharedSecret
         wrappedSession = aesKeyWrapRFC3394ForTest AES256 kek encodedSession
         esk =
             shortEphemeral
@@ -6782,7 +6814,8 @@
             (SessionKey sessionKey)
             (BL.toStrict (runPut (put literalBlock))) of
             Left err ->
-                assertFailure ("encryptSEIPDv2Payload failed: " ++ err)
+                assertFailure
+                    ("encryptSEIPDv2Payload failed: " ++ renderSEIPDv2Failure err)
                     >> pure mempty
             Right ct -> pure ct
     result <-
@@ -6973,7 +7006,7 @@
     let encodedWithChecksum =
             B.singleton (fromFVal sa)
                 <> sessionKey
-                <> encodeChecksum16 sessionKey
+                <> checksum16Bytes sessionKey
     encryptedEsk <-
         case withSymmetricCipher sa kek $ \cipher ->
             paddedCfbEncrypt
@@ -7020,7 +7053,7 @@
         Left err ->
             assertFailure
                 ( "Expected passphrase SKESK force-v4 encryption to succeed, got: "
-                    ++ err
+                    ++ renderSEIPDv2Failure err
                 )
         Right
             ( SKESKPkt (SKESKPayloadV4Packet _)
@@ -7054,7 +7087,7 @@
         Left err ->
             assertFailure
                 ( "Expected passphrase SKESK prefer-v6 encryption to succeed, got: "
-                    ++ err
+                    ++ renderSEIPDv2Failure err
                 )
         Right
             ( SKESKPkt (SKESKPayloadV6Packet _)
diff --git a/tests/Tests/Keys.hs b/tests/Tests/Keys.hs
--- a/tests/Tests/Keys.hs
+++ b/tests/Tests/Keys.hs
@@ -9,7 +9,6 @@
 
 module Tests.Keys (keyAndVerificationTests) where
 
-import Control.Error.Util (isRight)
 import Crypto.Number.Serialize (os2ip)
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import Data.Bifunctor (first)
@@ -22,6 +21,7 @@
 import qualified Data.Conduit.Binary as CB
 import qualified Data.Conduit.List as CL
 import Data.Conduit.Serialization.Binary (conduitGet)
+import Data.Either (isRight)
 import Data.IxSet.Typed (getOne, size, (@=))
 import Data.List (find, isInfixOf)
 import Data.List.NonEmpty (NonEmpty (..))
@@ -75,7 +75,6 @@
     , decryptSecretKey
     , mkUnencryptedSKAddendum
     , reencryptSecretKey
-    , reinterpretUnknownSKeyForPKPayload
     )
 import Codec.Encryption.OpenPGP.Serialize
     ( getSecretKey
@@ -166,13 +165,13 @@
                     "ecd/256:F7708BADD6063224/174C CF12 C571 6D0E 527F  B50E F770 8BAD D606 3224"
                 )
             , testCase
-                "EdDSA key"
+                "EdDSALegacy key"
                 ( testPKAandSizeAndKeyIDandFingerprint
                     "sample-eddsa.pubkey"
                     "edd/256:8CFDE12197965A9A/C959 BDBA FA32 A2F8 9A15  3B67 8CFD E121 9796 5A9A"
                 )
             , testCase
-                "EdDSA secret key (projected to public key packet)"
+                "EdDSALegacy secret key (projected to public key packet)"
                 ( testPKAandSizeAndKeyIDandFingerprint
                     "ed25519.secretkey"
                     "edd/256:B05F9287601D5914/B6FE 0C23 12EB D832 0238  C252 B05F 9287 601D 5914"
@@ -343,9 +342,6 @@
                 "mkUnencryptedSKAddendum sets v6 unencrypted checksum to zero"
                 testMkUnencryptedSKAddendumUsesV6ChecksumConvention
             , testCase
-                "reinterpretUnknownSKeyForPKPayload decodes EdDSA unknown key material"
-                testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA
-            , testCase
                 "fromPrimaryKeyPktToSomeTK rejects subkey packets"
                 testFromPrimaryKeyPktToSomeTKRejectsSubkey
             , testCase
@@ -1802,7 +1798,7 @@
         pkSigV6PK =
             SigV6
                 KeyRevocationSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x11))
                 []
@@ -1812,7 +1808,7 @@
         pkSigV6Direct =
             SigV6
                 SignatureDirectlyOnAKey
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x12))
                 []
@@ -1822,7 +1818,7 @@
         pkSigV6Binding =
             SigV6
                 SubkeyBindingSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x13))
                 []
@@ -1835,7 +1831,7 @@
         skSigV6Binding =
             SigV6
                 SubkeyBindingSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x14))
                 []
@@ -1845,7 +1841,7 @@
         skSigV6Revocation =
             SigV6
                 SubkeyRevocationSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x15))
                 []
@@ -1855,7 +1851,7 @@
         skSigV6Direct =
             SigV6
                 SignatureDirectlyOnAKey
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x16))
                 []
@@ -1871,7 +1867,7 @@
         uidSigV6Generic =
             SigV6
                 GenericCert
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x17))
                 []
@@ -1881,7 +1877,7 @@
         uidSigV6Persona =
             SigV6
                 PersonaCert
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x18))
                 []
@@ -1891,7 +1887,7 @@
         uidSigV6Casual =
             SigV6
                 CasualCert
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x19))
                 []
@@ -1901,7 +1897,7 @@
         uidSigV6Positive =
             SigV6
                 PositiveCert
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x1a))
                 []
@@ -1911,7 +1907,7 @@
         uidSigV6CertRev =
             SigV6
                 CertRevocationSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x1b))
                 []
@@ -1921,7 +1917,7 @@
         uidSigV6Binding =
             SigV6
                 SubkeyBindingSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x1c))
                 []
@@ -2172,7 +2168,7 @@
                     V4
                     (ThirtyTwoBitTimeStamp 0)
                     0
-                    EdDSA
+                    EdDSALegacy
                     ( EdDSAPubKey
                         EdSigningCurve25519
                         ( PrefixedNativeEPoint
@@ -2207,7 +2203,7 @@
                 V4
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 ( EdDSAPubKey
                     EdSigningCurve448
                     ( PrefixedNativeEPoint
@@ -2284,34 +2280,6 @@
                 checksum
         Right other ->
             assertFailure ("unexpected addendum constructor: " ++ show other)
-
-testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA :: Assertion
-testReinterpretUnknownSKeyForPKPayloadDecodesEdDSA = do
-    let secretBytes = B.pack (0 : replicate 31 1)
-        pkp =
-            PKPayload
-                V4
-                (ThirtyTwoBitTimeStamp 0)
-                0
-                EdDSA
-                ( EdDSAPubKey
-                    EdSigningCurve25519
-                    ( PrefixedNativeEPoint
-                        (EPoint (os2ip (B.cons 0x40 (B.replicate 32 0x01))))
-                    )
-                )
-        unknown = UnknownSKey (runPut (put (MPI (os2ip secretBytes))))
-    case reinterpretUnknownSKeyForPKPayload pkp unknown of
-        Left err ->
-            assertFailure
-                ( "reinterpretUnknownSKeyForPKPayload should decode Ed25519 key: "
-                    ++ err
-                )
-        Right skey ->
-            assertEqual
-                "reinterpretUnknownSKeyForPKPayload should return typed EdDSA secret key"
-                (EdDSAPrivateKey EdSigningCurve25519 secretBytes)
-                skey
 
 testFromPrimaryKeyPktToSomeTKRejectsSubkey :: Assertion
 testFromPrimaryKeyPktToSomeTKRejectsSubkey = do
diff --git a/tests/Tests/MessageAndArmor.hs b/tests/Tests/MessageAndArmor.hs
--- a/tests/Tests/MessageAndArmor.hs
+++ b/tests/Tests/MessageAndArmor.hs
@@ -13,6 +13,8 @@
     , ArmorType (..)
     )
 import Control.Lens ((^.))
+import qualified Crypto.Error as CE
+import qualified Crypto.PubKey.Ed25519 as Ed25519
 import Data.Binary (get, put)
 import Data.Binary.Get (Get, runGetOrFail)
 import Data.Binary.Put
@@ -32,6 +34,7 @@
     )
 import Data.List (isInfixOf)
 import qualified Data.List.NonEmpty as NE
+import Data.Word (Word8)
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit
     ( Assertion
@@ -44,7 +47,6 @@
 import Codec.Encryption.OpenPGP.BlockCipher (keySize)
 import Codec.Encryption.OpenPGP.CFB
     ( decryptPreservingNonce
-    , validateSEIPD1MDC
     )
 import Codec.Encryption.OpenPGP.Compression (decompressPkt)
 import Codec.Encryption.OpenPGP.Encrypt
@@ -54,7 +56,10 @@
     ( eightOctetKeyID
     , fingerprint
     )
-import Codec.Encryption.OpenPGP.Internal (emptyPSC, lastLD)
+import Codec.Encryption.OpenPGP.Internal
+    ( PktStreamContext (..)
+    , emptyPSC
+    )
 import qualified Codec.Encryption.OpenPGP.Internal.Whitespace as WS
 import Codec.Encryption.OpenPGP.KeyringParser (parsePublicTKs)
 import Codec.Encryption.OpenPGP.Message
@@ -63,6 +68,14 @@
     , signatureV6SaltSizeForHashAlgorithm
     )
 import Codec.Encryption.OpenPGP.S2K (renderS2KError, string2Key)
+import Codec.Encryption.OpenPGP.SEIPDv1
+    ( renderMDCFailure
+    , validateSEIPD1MDC
+    )
+import Codec.Encryption.OpenPGP.SEIPDv2
+    ( SEIPDv2Failure (..)
+    , renderSEIPDv2Failure
+    )
 import Codec.Encryption.OpenPGP.Serialize
     ( armorPayloadsOfType
     , parsePkts
@@ -74,6 +87,7 @@
 import Codec.Encryption.OpenPGP.SerializeForSigs
     ( payloadForSig
     , payloadForSigWith
+    , putKeyforSigning
     , putPartialSigforSigning
     , putSigTrailer
     )
@@ -131,6 +145,7 @@
     , loadKeyring
     , loadUnencryptedRsaSigner
     , loadUnencryptedRsaSignerV6
+    , loadV6UnencryptedSecretKeyFixtureForProperty
     , mkTestKeyring
     , readFixtureLazy
     , readFixtureStrict
@@ -144,6 +159,32 @@
     , verifyTimelinePackets
     )
 
+extractEd25519SecretKey :: SKey -> IO Ed25519.SecretKey
+extractEd25519SecretKey skey =
+    case skey of
+        EdDSAPrivateKey EdSigningCurve25519 bs ->
+            case CE.eitherCryptoError (Ed25519.secretKey bs) of
+                Left err ->
+                    assertFailure
+                        ( "failed to initialize Ed25519 secret key from EdDSAPrivateKey: "
+                            ++ show err
+                        )
+                        >> fail "expected Ed25519 secret key"
+                Right edSk -> pure edSk
+        Ed25519PrivateKey bs ->
+            case CE.eitherCryptoError (Ed25519.secretKey bs) of
+                Left err ->
+                    assertFailure
+                        ( "failed to initialize Ed25519 secret key from Ed25519PrivateKey: "
+                            ++ show err
+                        )
+                        >> fail "expected Ed25519 secret key"
+                Right edSk -> pure edSk
+        _ ->
+            assertFailure
+                "v6-secret fixture did not contain Ed25519 secret key material"
+                >> fail "expected Ed25519 secret key"
+
 messageAndArmorTests :: TestTree
 messageAndArmorTests =
     testGroup
@@ -218,7 +259,7 @@
                 "detached v4 Ed25519 verifyAgainstKeys tolerates fake issuer hints"
                 testVerifyDetachedEd25519WithFakeIssuerHintAgainstKeys
             , testCase
-                "v4 EdDSA signatures verify with Ed25519 key algorithm identifier"
+                "v4 EdDSALegacy signatures verify with Ed25519 key algorithm identifier"
                 testVerifyV4EdDSASignatureWithEd25519KeyAlgorithm
             , testCase
                 "sign message convenience API"
@@ -268,6 +309,27 @@
             , testCase
                 "v6.txt.sig verifies against v6.txt with v6-secret.pgp.aa public key"
                 testV6DetachedVerification
+            , testCase
+                "v6-secret fixture creates and verifies detached Ed25519 SigV6"
+                testV6FixtureSignatureCreationAndVerification
+            , testCase
+                "v6-secret fixture creates and verifies binary message Ed25519 SigV6"
+                testV6FixtureBinaryMessageSigning
+            , testCase
+                "v6-secret fixture creates and verifies direct-key Ed25519 SigV6"
+                testV6FixtureDirectKeySignature
+            , testCase
+                "v6-secret fixture creates and verifies key-revocation Ed25519 SigV6"
+                testV6FixtureKeyRevocation
+            , testCase
+                "v6-secret fixture creates and verifies certification Ed25519 SigV6"
+                testV6FixtureCertification
+            , testCase
+                "v6.rev.aa ReasonForRevocation subpacket content"
+                (testV6RevocationReasonSubpacket "v6.rev.aa")
+            , testCase
+                "v6-encrypted.rev.aa ReasonForRevocation subpacket content"
+                (testV6RevocationReasonSubpacket "v6-encrypted.rev.aa")
             ]
         , testGroup
             "ASCII armor fixture group"
@@ -402,14 +464,14 @@
                 (fp "D2E0 81E9 3FDC A2E7 8B5F  C433 811D 9243 394B 79C1")
                 (fingerprint pkp)
             assertEqual
-                "v4 encrypted secret fixture should use EdDSA for its primary key"
-                EdDSA
+                "v4 encrypted secret fixture should use EdDSALegacy for its primary key"
+                EdDSALegacy
                 (_pkalgo pkp)
             assertEncryptedS2K "primary key" ska
             mapM_ (assertEncryptedS2K "subkey" . snd) secretSubkeys
             assertEqual
-                "v4 encrypted secret fixture should contain two EdDSA subkeys and one ECDH subkey"
-                [EdDSA, EdDSA, ECDH]
+                "v4 encrypted secret fixture should contain two EdDSALegacy subkeys and one ECDH subkey"
+                [EdDSALegacy, EdDSALegacy, ECDH]
                 (map (_pkalgo . fst) secretSubkeys)
         _ ->
             assertFailure
@@ -482,8 +544,8 @@
                 (fp "D2E0 81E9 3FDC A2E7 8B5F  C433 811D 9243 394B 79C1")
                 (fingerprint pkp)
             assertEqual
-                "v4 encrypted revocation fixture should use EdDSA for its public key"
-                EdDSA
+                "v4 encrypted revocation fixture should use EdDSALegacy for its public key"
+                EdDSALegacy
                 (_pkalgo pkp)
         _ ->
             assertFailure
@@ -569,7 +631,9 @@
                     >> fail "unexpected revocation fixture packet shape"
     case sig of
         SigV6 _ _ ha salt _ _ _ _ ->
-            case expectedV6SaltSizeForTest ha of
+            case fmap
+                (fromIntegral :: Word8 -> Int)
+                (signatureV6SaltSizeForHashAlgorithm ha) of
                 Nothing ->
                     assertFailure
                         ( fixture
@@ -704,10 +768,6 @@
         "msg1.asc should contain at least one parseable packet block"
         (any (not . null) packetBlocks)
 
-expectedV6SaltSizeForTest :: HashAlgorithm -> Maybe Int
-expectedV6SaltSizeForTest =
-    fmap fromIntegral . signatureV6SaltSizeForHashAlgorithm
-
 signatureHasIssuerFingerprintV6
     :: Fingerprint -> SignaturePayload -> Bool
 signatureHasIssuerFingerprintV6 expectedFp (SigV6 _ _ _ _ hashed unhashed _ _) =
@@ -924,7 +984,11 @@
             block of
             Left err ->
                 assertFailure
-                    ("AES-128 " ++ label ++ " encryption failed: " ++ err)
+                    ( "AES-128 "
+                        ++ label
+                        ++ " encryption failed: "
+                        ++ renderSEIPDv2Failure err
+                    )
                     >> fail "encryptSEIPDv2WithSKESKBlock failed"
             Right ps -> pure ps
     let encrypted = mkEncryptedPayload (runPut (put (Block packets)))
@@ -987,13 +1051,13 @@
         s2k
         passphraseBytes
         block of
-        Left err
-            | "EAX is currently unsupported by the crypton AEAD backend"
-                `isInfixOf` err ->
-                pure ()
-            | otherwise ->
-                assertFailure
-                    ("expected explicit EAX backend limitation, got: " ++ err)
+        Left (SEIPDv2UnsupportedAEADAlgorithm EAX) ->
+            pure ()
+        Left err ->
+            assertFailure
+                ( "expected explicit EAX backend limitation, got: "
+                    ++ renderSEIPDv2Failure err
+                )
         Right packets ->
             assertFailure
                 ( "expected AES-128 EAX encryption to fail explicitly, got packets: "
@@ -1020,16 +1084,12 @@
         )
         passphrase
         payload of
-        Left (MessageEncryptError err)
-            | "deprecated hash algorithm disallowed for modern message generation"
-                `isInfixOf` err ->
+        Left
+            (MessageEncryptFailureError (MessageEncryptDeprecatedS2KHash _)) ->
                 pure ()
-            | otherwise ->
-                assertFailure
-                    ("Expected deprecated modern S2K hash rejection, got: " ++ err)
         Left err ->
             assertFailure
-                ( "Expected MessageEncryptError for deprecated modern S2K hash, got "
+                ( "Expected deprecated modern S2K hash rejection, got: "
                     ++ show err
                 )
         Right _ ->
@@ -1143,7 +1203,8 @@
     cleartext <-
         case validateSEIPD1MDC nonce decrypted of
             Left err ->
-                assertFailure ("validateSEIPD1MDC failed: " ++ err)
+                assertFailure
+                    ("validateSEIPD1MDC failed: " ++ renderMDCFailure err)
                     >> fail "validateSEIPD1MDC failed"
             Right out -> pure out
     case parsePktsEither (BL.fromStrict cleartext) of
@@ -1260,14 +1321,15 @@
                     (BL.head (BL.drop midpoint raw) `xor` 0xFF)
                     (BL.drop (midpoint + 1) raw)
     case decryptMessage passphrase (mkEncryptedPayload tampered) of
-        Left (MessageDecryptFailureError (PayloadDecryptFailed msg))
-            | "MDC" `isInfixOf` msg -> pure ()
-            | otherwise ->
-                assertFailure
-                    ("Expected MDC-related PayloadDecryptFailed, got: " ++ msg)
+        Left
+            ( MessageDecryptFailureError
+                    (PayloadDecryptFailed (PayloadDecryptMDCFailed MDCDigestMismatch))
+                ) -> pure ()
         Left err ->
             assertFailure
-                ("Expected PayloadDecryptFailed with MDC error, got: " ++ show err)
+                ( "Expected PayloadDecryptMDCFailed MDCDigestMismatch, got: "
+                    ++ show err
+                )
         Right _ ->
             assertFailure
                 "Expected MDC tampering rejection, but decryption succeeded"
@@ -1287,10 +1349,10 @@
                     >> pure mempty
             Right bs -> pure bs
     case parsePkts signedMessage of
-        [LiteralDataPkt {}, SignaturePkt _] -> pure ()
+        [OnePassSignaturePkt {}, LiteralDataPkt {}, SignaturePkt _] -> pure ()
         _ ->
             assertFailure
-                "signing output should contain literal data and one signature packet"
+                "signing output should contain one-pass signature, literal data, and one signature packet"
 
 testSignMessageRSAV6 :: Assertion
 testSignMessageRSAV6 = do
@@ -1309,7 +1371,8 @@
             Right bs -> pure bs
     signaturePkt <-
         case parsePkts signedMessage of
-            [ LiteralDataPkt {}
+            [ OnePassSignaturePkt {}
+                , LiteralDataPkt {}
                 , sig@(SignaturePkt (SigV6 BinarySig RSA SHA512 salt _ _ _ _))
                 ] -> do
                     assertEqual
@@ -1319,7 +1382,7 @@
                     pure sig
             other ->
                 assertFailure
-                    ( "RSA SigV6 signing output should contain [LiteralDataPkt, RSA SigV6], got "
+                    ( "RSA SigV6 signing output should contain [OPS, LiteralDataPkt, RSA SigV6], got "
                         ++ show other
                     )
                     >> fail "unexpected RSA SigV6 signMessage output shape"
@@ -1364,13 +1427,14 @@
             Right bs -> pure bs
     signaturePkt <-
         case parsePkts signedMessage of
-            [ LiteralDataPkt {}
+            [ OnePassSignaturePkt {}
+                , LiteralDataPkt {}
                 , sig@(SignaturePkt (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _))
                 ] ->
                     pure sig
             other ->
                 assertFailure
-                    ( "Ed25519 signing output should contain [LiteralDataPkt, Ed25519 SigV4], got "
+                    ( "Ed25519 signing output should contain [OPS, LiteralDataPkt, Ed25519 SigV4], got "
                         ++ show other
                     )
                     >> fail "unexpected Ed25519 signMessage output shape"
@@ -1416,7 +1480,8 @@
             Right bs -> pure bs
     signaturePkt <-
         case parsePkts signedMessage of
-            [ LiteralDataPkt {}
+            [ OnePassSignaturePkt {}
+                , LiteralDataPkt {}
                 , sig@(SignaturePkt (SigV6 BinarySig PKA.Ed25519 SHA512 salt _ _ _ _))
                 ] -> do
                     assertEqual
@@ -1426,7 +1491,7 @@
                     pure sig
             other ->
                 assertFailure
-                    ( "Ed25519 SigV6 signing output should contain [LiteralDataPkt, Ed25519 SigV6], got "
+                    ( "Ed25519 SigV6 signing output should contain [OPS, LiteralDataPkt, Ed25519 SigV6], got "
                         ++ show other
                     )
                     >> fail "unexpected Ed25519 SigV6 signMessage output shape"
@@ -1465,7 +1530,7 @@
             runPut $ do
                 putWord8 4
                 putWord32be 0
-                putWord8 (fromFVal EdDSA)
+                putWord8 (fromFVal EdDSALegacy)
                 putWord8 (fromIntegral (B.length legacyEd25519Oid))
                 putByteString legacyEd25519Oid
                 putWord16be 256
@@ -1496,13 +1561,14 @@
             Right bs -> pure bs
     signaturePkt <-
         case parsePkts signedMessage of
-            [ LiteralDataPkt {}
+            [ OnePassSignaturePkt {}
+                , LiteralDataPkt {}
                 , sig@(SignaturePkt (SigV4 BinarySig PKA.Ed448 SHA512 _ _ _ _))
                 ] ->
                     pure sig
             other ->
                 assertFailure
-                    ( "Ed448 signing output should contain [LiteralDataPkt, Ed448 SigV4], got "
+                    ( "Ed448 signing output should contain [OPS, LiteralDataPkt, Ed448 SigV4], got "
                         ++ show other
                     )
                     >> fail "unexpected Ed448 signMessage output shape"
@@ -1548,7 +1614,8 @@
             Right bs -> pure bs
     signaturePkt <-
         case parsePkts signedMessage of
-            [ LiteralDataPkt {}
+            [ OnePassSignaturePkt {}
+                , LiteralDataPkt {}
                 , sig@(SignaturePkt (SigV6 BinarySig PKA.Ed448 SHA512 salt _ _ _ _))
                 ] -> do
                     assertEqual
@@ -1558,7 +1625,7 @@
                     pure sig
             other ->
                 assertFailure
-                    ( "Ed448 SigV6 signing output should contain [LiteralDataPkt, Ed448 SigV6], got "
+                    ( "Ed448 SigV6 signing output should contain [OPS, LiteralDataPkt, Ed448 SigV6], got "
                         ++ show other
                     )
                     >> fail "unexpected Ed448 SigV6 signMessage output shape"
@@ -1791,7 +1858,7 @@
         (payloadForSig BinarySig state)
     assertEqual
         "Canonical text signatures normalize line endings and trim trailing whitespace"
-        "line1\r\nline2\r\nline3\r\nline4"
+        "line1\r\nline2\rline3\r\nline4"
         (payloadForSig CanonicalTextSig state)
 
 testTextNormalizationModes :: Assertion
@@ -1948,7 +2015,7 @@
                 assertFailure ("failed to derive issuer key id: " ++ err)
                     >> fail "expected issuer key id"
             Right i -> pure i
-    let mixedPayload = "line1 \t\nline2\t \rline3\t \r\nline4 \t"
+    let mixedPayload = "line1 \nline2\t \r\nline3 \nline4 \t"
         normalizedPayload = "line1\r\nline2\r\nline3\r\nline4"
         hashed =
             [ SigSubPacket
@@ -2733,10 +2800,10 @@
                     >> pure mempty
             Right bs -> pure bs
     case parsePkts signedMessage of
-        [LiteralDataPkt {}, SignaturePkt _] -> pure ()
+        [OnePassSignaturePkt {}, LiteralDataPkt {}, SignaturePkt _] -> pure ()
         _ ->
             assertFailure
-                "convenience signing output should contain literal data and one signature packet"
+                "convenience signing output should contain one-pass signature, literal data, and one signature packet"
 
 testTypedVerifySurfaceMatchesLegacy :: Assertion
 testTypedVerifySurfaceMatchesLegacy = do
@@ -2848,13 +2915,14 @@
                     >> fail "expected signed Ed25519 payload"
             Right signedMessage ->
                 case parsePkts signedMessage of
-                    [ LiteralDataPkt {}
+                    [ OnePassSignaturePkt {}
+                        , LiteralDataPkt {}
                         , sig@(SignaturePkt (SigV4 BinarySig PKA.Ed25519 SHA512 _ _ _ _))
                         ] ->
                             pure sig
                     other ->
                         assertFailure
-                            ( "Expected [LiteralDataPkt, Ed25519 SigV4] for Ed25519-key-algorithm verification test, got "
+                            ( "Expected [OPS, LiteralDataPkt, Ed25519 SigV4] for Ed25519-key-algorithm verification test, got "
                                 ++ show other
                             )
                             >> fail "unexpected Ed25519-key-algorithm signature shape"
@@ -2952,3 +3020,347 @@
                     ++ renderVerificationError err
                 )
         Right _ -> pure ()
+
+testV6FixtureSignatureCreationAndVerification :: Assertion
+testV6FixtureSignatureCreationAndVerification = do
+    fixtureResult <- loadV6UnencryptedSecretKeyFixtureForProperty
+    (pkp, _ska, skey) <-
+        case fixtureResult of
+            Left err ->
+                assertFailure
+                    ("failed to load v6 Ed25519 secret fixture: " ++ err)
+                    >> fail "expected v6 Ed25519 secret fixture"
+            Right x -> pure x
+    edSecretKey <- extractEd25519SecretKey skey
+    let payload = "v6 fixture detached signature payload"
+        salt = SignatureSalt (BL.fromStrict (B.replicate 32 0xAB))
+        hashed = [SigSubPacket False (SigCreationTime 0)]
+        unhashed =
+            [ SigSubPacket
+                False
+                (IssuerFingerprint IssuerFingerprintV6 (fingerprint pkp))
+            ]
+    sigPayload <-
+        case signDataWithEd25519V6
+            BinarySig
+            salt
+            edSecretKey
+            hashed
+            unhashed
+            payload of
+            Left err ->
+                assertFailure
+                    ( "v6 fixture Ed25519 detached signing failed: "
+                        ++ renderSignError err
+                    )
+                    >> fail "expected v6 fixture signature"
+            Right sig -> pure sig
+    let keyring =
+            [ TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = []
+                , _tkUIDs = []
+                , _tkUAts = []
+                , _tkSubs = []
+                }
+            ]
+        state =
+            emptyPSC
+                { lastLD = LiteralDataPkt BinaryData BL.empty 0 payload
+                }
+    case verifySigWith
+        defaultVerificationPolicy
+        (verifyAgainstKeys keyring)
+        (SignaturePkt sigPayload)
+        state
+        Nothing of
+        Left err ->
+            assertFailure
+                ( "v6 fixture Ed25519 detached signature should verify: "
+                    ++ renderVerificationError err
+                )
+        Right _ -> pure ()
+
+testV6FixtureBinaryMessageSigning :: Assertion
+testV6FixtureBinaryMessageSigning = do
+    fixtureResult <- loadV6UnencryptedSecretKeyFixtureForProperty
+    (pkp, _ska, skey) <-
+        case fixtureResult of
+            Left err ->
+                assertFailure ("failed to load v6 secret fixture: " ++ err)
+                    >> fail "expected v6 secret fixture"
+            Right x -> pure x
+    edSecretKey <- extractEd25519SecretKey skey
+    let payload = "v6 fixture binary message signing payload"
+        salt = SignatureSalt (BL.fromStrict (B.replicate 32 0xCD))
+        hashed = [SigSubPacket False (SigCreationTime 0)]
+        unhashed =
+            [ SigSubPacket
+                False
+                (IssuerFingerprint IssuerFingerprintV6 (fingerprint pkp))
+            ]
+    sigPayload <-
+        case signDataWithEd25519V6
+            BinarySig
+            salt
+            edSecretKey
+            hashed
+            unhashed
+            payload of
+            Left err ->
+                assertFailure
+                    ( "v6 fixture Ed25519 binary signing failed: "
+                        ++ renderSignError err
+                    )
+                    >> fail "expected v6 fixture signature"
+            Right sig -> pure sig
+    let keyring =
+            [ TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = []
+                , _tkUIDs = []
+                , _tkUAts = []
+                , _tkSubs = []
+                }
+            ]
+        state =
+            emptyPSC
+                { lastLD = LiteralDataPkt BinaryData BL.empty 0 payload
+                }
+    case verifySigWith
+        defaultVerificationPolicy
+        (verifyAgainstKeys keyring)
+        (SignaturePkt sigPayload)
+        state
+        Nothing of
+        Left err ->
+            assertFailure
+                ( "v6 fixture Ed25519 binary message signature should verify: "
+                    ++ renderVerificationError err
+                )
+        Right _ -> pure ()
+
+testV6FixtureDirectKeySignature :: Assertion
+testV6FixtureDirectKeySignature = do
+    fixtureResult <- loadV6UnencryptedSecretKeyFixtureForProperty
+    (pkp, _ska, skey) <-
+        case fixtureResult of
+            Left err ->
+                assertFailure ("failed to load v6 secret fixture: " ++ err)
+                    >> fail "expected v6 secret fixture"
+            Right x -> pure x
+    edSecretKey <- extractEd25519SecretKey skey
+    let salt = SignatureSalt (BL.fromStrict (B.replicate 32 0xEF))
+        hashed = [SigSubPacket False (SigCreationTime 0)]
+        unhashed =
+            [ SigSubPacket
+                False
+                (IssuerFingerprint IssuerFingerprintV6 (fingerprint pkp))
+            ]
+        keypayload = runPut (putKeyforSigning (PublicKeyPkt pkp))
+    sigPayload <-
+        case signDataWithEd25519V6
+            SignatureDirectlyOnAKey
+            salt
+            edSecretKey
+            hashed
+            unhashed
+            keypayload of
+            Left err ->
+                assertFailure
+                    ( "v6 fixture direct-key signing failed: "
+                        ++ renderSignError err
+                    )
+                    >> fail "expected v6 fixture direct-key signature"
+            Right sig -> pure sig
+    let keyring =
+            [ TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = []
+                , _tkUIDs = []
+                , _tkUAts = []
+                , _tkSubs = []
+                }
+            ]
+        state =
+            emptyPSC
+                { lastPrimaryKey = PublicKeyPkt pkp
+                }
+    case verifySigWith
+        defaultVerificationPolicy
+        (verifyAgainstKeys keyring)
+        (SignaturePkt sigPayload)
+        state
+        Nothing of
+        Left err ->
+            assertFailure
+                ( "v6 fixture direct-key signature should verify: "
+                    ++ renderVerificationError err
+                )
+        Right _ -> pure ()
+
+testV6FixtureKeyRevocation :: Assertion
+testV6FixtureKeyRevocation = do
+    fixtureResult <- loadV6UnencryptedSecretKeyFixtureForProperty
+    (pkp, _ska, skey) <-
+        case fixtureResult of
+            Left err ->
+                assertFailure ("failed to load v6 secret fixture: " ++ err)
+                    >> fail "expected v6 secret fixture"
+            Right x -> pure x
+    edSecretKey <- extractEd25519SecretKey skey
+    let salt = SignatureSalt (BL.fromStrict (B.replicate 32 0x12))
+        hashed =
+            [ SigSubPacket False (SigCreationTime 0)
+            , SigSubPacket
+                False
+                (ReasonForRevocation KeySuperseded "v6 fixture test")
+            ]
+        unhashed =
+            [ SigSubPacket
+                False
+                (IssuerFingerprint IssuerFingerprintV6 (fingerprint pkp))
+            ]
+        keypayload = runPut (putKeyforSigning (PublicKeyPkt pkp))
+    sigPayload <-
+        case signDataWithEd25519V6
+            KeyRevocationSig
+            salt
+            edSecretKey
+            hashed
+            unhashed
+            keypayload of
+            Left err ->
+                assertFailure
+                    ( "v6 fixture key-revocation signing failed: "
+                        ++ renderSignError err
+                    )
+                    >> fail "expected v6 fixture key-revocation signature"
+            Right sig -> pure sig
+    let keyring =
+            [ TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = []
+                , _tkUIDs = []
+                , _tkUAts = []
+                , _tkSubs = []
+                }
+            ]
+        state =
+            emptyPSC
+                { lastPrimaryKey = PublicKeyPkt pkp
+                }
+    case verifySigWith
+        defaultVerificationPolicy
+        (verifyAgainstKeys keyring)
+        (SignaturePkt sigPayload)
+        state
+        Nothing of
+        Left err ->
+            assertFailure
+                ( "v6 fixture key-revocation signature should verify: "
+                    ++ renderVerificationError err
+                )
+        Right _ -> pure ()
+
+testV6FixtureCertification :: Assertion
+testV6FixtureCertification = do
+    fixtureResult <- loadV6UnencryptedSecretKeyFixtureForProperty
+    (pkp, _ska, skey) <-
+        case fixtureResult of
+            Left err ->
+                assertFailure ("failed to load v6 secret fixture: " ++ err)
+                    >> fail "expected v6 secret fixture"
+            Right x -> pure x
+    edSecretKey <- extractEd25519SecretKey skey
+    let uid = UserId "v6 Fixture User"
+        uidText = let UserId t = uid in t
+        salt = SignatureSalt (BL.fromStrict (B.replicate 32 0x34))
+        hashed =
+            [ SigSubPacket False (SigCreationTime 0)
+            , SigSubPacket False (PrimaryUserId True)
+            ]
+        unhashed =
+            [ SigSubPacket
+                False
+                (IssuerFingerprint IssuerFingerprintV6 (fingerprint pkp))
+            ]
+        state =
+            emptyPSC
+                { lastPrimaryKey = PublicKeyPkt pkp
+                , lastUIDorUAt = UserIdPkt uidText
+                }
+        payload = payloadForSig GenericCert state
+    sigPayload <-
+        case signDataWithEd25519V6
+            GenericCert
+            salt
+            edSecretKey
+            hashed
+            unhashed
+            payload of
+            Left err ->
+                assertFailure
+                    ( "v6 fixture certification signing failed: "
+                        ++ renderSignError err
+                    )
+                    >> fail "expected v6 fixture certification signature"
+            Right sig -> pure sig
+    let keyring =
+            [ TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = []
+                , _tkUIDs = [(uidText, [])]
+                , _tkUAts = []
+                , _tkSubs = []
+                }
+            ]
+    case verifySigWith
+        defaultVerificationPolicy
+        (verifyAgainstKeys keyring)
+        (SignaturePkt sigPayload)
+        state
+        Nothing of
+        Left err ->
+            assertFailure
+                ( "v6 fixture certification signature should verify: "
+                    ++ renderVerificationError err
+                )
+        Right _ -> pure ()
+
+testV6RevocationReasonSubpacket :: FilePath -> Assertion
+testV6RevocationReasonSubpacket fixture = do
+    armors <- loadArmor fixture
+    payload <-
+        case armors of
+            [Armor ArmorPublicKeyBlock _ p] -> pure p
+            _ ->
+                assertFailure
+                    (fixture ++ " should contain one armored public-key payload")
+                    >> fail "expected one armored payload"
+    let packets = parsePkts payload
+    case packets of
+        [ PublicKeyPkt _
+            , SignaturePkt (SigV6 KeyRevocationSig _ _ _ hashed unhashed _ _)
+            ] ->
+                case [ rc
+                     | SigSubPacket _ (ReasonForRevocation rc _) <-
+                        hashed ++ unhashed
+                     ] of
+                    (rc : _) ->
+                        assertEqual
+                            ( fixture
+                                ++ " ReasonForRevocation code should be NoReason"
+                            )
+                            NoReason
+                            rc
+                    [] ->
+                        assertFailure
+                            ( fixture
+                                ++ " SigV6 key-revocation signature should include a ReasonForRevocation subpacket"
+                            )
+        _ ->
+            assertFailure
+                ( fixture
+                    ++ " should parse as [PublicKeyPkt, SignaturePkt SigV6 KeyRevocationSig]"
+                )
diff --git a/tests/Tests/Serialization.hs b/tests/Tests/Serialization.hs
--- a/tests/Tests/Serialization.hs
+++ b/tests/Tests/Serialization.hs
@@ -9,9 +9,11 @@
 import Control.Applicative ((<|>))
 import Control.Lens ((^.))
 import Control.Monad (forM_)
+import qualified Crypto.Error as CE
 import Crypto.Number.Serialize (os2ip)
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import qualified Crypto.PubKey.ECC.Types as ECCT
+import qualified Crypto.PubKey.Ed25519 as Ed25519
 import Data.Binary (Get, get, put)
 import Data.Binary.Put
     ( putByteString
@@ -45,7 +47,11 @@
     ( eightOctetKeyID
     , fingerprint
     )
-import Codec.Encryption.OpenPGP.Internal (emptyPSC, point2MBS)
+import Codec.Encryption.OpenPGP.Internal
+    ( PktStreamContext (..)
+    , emptyPSC
+    , point2MBS
+    )
 import Codec.Encryption.OpenPGP.KeyringParser
     ( parseTKsWithWireRep
     )
@@ -59,6 +65,10 @@
     )
 import Codec.Encryption.OpenPGP.Signatures
     ( VerificationError (..)
+    , renderSignError
+    , renderVerificationError
+    , signDataWithEd25519V6
+    , verifyAgainstKeys
     , verifySigWith
     )
 import Codec.Encryption.OpenPGP.Types
@@ -66,10 +76,37 @@
 import Tests.Common
     ( armorPayload
     , loadArmor
+    , loadV6UnencryptedSecretKeyFixtureForProperty
     , readFixturePayload
     , runGet
     )
 
+extractEd25519SecretKeyFromSKey :: SKey -> IO Ed25519.SecretKey
+extractEd25519SecretKeyFromSKey skey =
+    case skey of
+        EdDSAPrivateKey EdSigningCurve25519 bs ->
+            case CE.eitherCryptoError (Ed25519.secretKey bs) of
+                Left err ->
+                    assertFailure
+                        ( "failed to initialize Ed25519 secret key from EdDSAPrivateKey: "
+                            ++ show err
+                        )
+                        >> fail "expected Ed25519 secret key"
+                Right edSk -> pure edSk
+        Ed25519PrivateKey bs ->
+            case CE.eitherCryptoError (Ed25519.secretKey bs) of
+                Left err ->
+                    assertFailure
+                        ( "failed to initialize Ed25519 secret key from Ed25519PrivateKey: "
+                            ++ show err
+                        )
+                        >> fail "expected Ed25519 secret key"
+                Right edSk -> pure edSk
+        _ ->
+            assertFailure
+                "v6-secret fixture did not contain Ed25519 secret key material"
+                >> fail "expected Ed25519 secret key"
+
 serializationTests :: TestTree
 serializationTests =
     testGroup
@@ -399,6 +436,9 @@
             , testCase
                 "v6-secret fixture derives eight-octet key-id from fingerprint prefix"
                 testV6SecretFixtureDerivesEightOctetKeyID
+            , testCase
+                "v6-secret fixture creates SigV6 with multiple hash algorithms"
+                testV6SecretFixtureMultiHashSignatures
             ]
         ]
 
@@ -495,7 +535,7 @@
                 V6
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))
         encoded = runPut (put (PublicKeyPkt pkp))
     assertEqual
@@ -574,13 +614,13 @@
                 V6
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))
         sig =
             SignaturePkt
                 ( SigV6
                     BinarySig
-                    EdDSA
+                    EdDSALegacy
                     SHA512
                     (SignatureSalt (BL.replicate 32 0))
                     [ SigSubPacket
@@ -1055,10 +1095,6 @@
                     ++ show other
                 )
 
-expectedV6SaltSizeForTest :: HashAlgorithm -> Maybe Int
-expectedV6SaltSizeForTest =
-    fmap fromIntegral . signatureV6SaltSizeForHashAlgorithm
-
 signatureHasIssuerFingerprintV6
     :: Fingerprint -> SignaturePayload -> Bool
 signatureHasIssuerFingerprintV6 expectedFp (SigV6 _ _ _ _ hashed unhashed _ _) =
@@ -1107,7 +1143,9 @@
         (not (null signatures))
     mapM_
         ( \(_, ha, salt) ->
-            case expectedV6SaltSizeForTest ha of
+            case fmap
+                (fromIntegral :: Word8 -> Int)
+                (signatureV6SaltSizeForHashAlgorithm ha) of
                 Nothing ->
                     assertFailure
                         ( "SigV6 in v6-secret.pgp.aa uses unsupported salt hash algorithm: "
@@ -1169,3 +1207,76 @@
         "v6 eight-octet key-id should be the high-order 64 bits of the fingerprint"
         expectedKeyId
         derivedKeyId
+
+testV6SecretFixtureMultiHashSignatures :: Assertion
+testV6SecretFixtureMultiHashSignatures = do
+    fixtureResult <- loadV6UnencryptedSecretKeyFixtureForProperty
+    (pkp, _ska, skey) <-
+        case fixtureResult of
+            Left err ->
+                assertFailure
+                    ("failed to load v6 secret fixture: " ++ err)
+                    >> fail "expected v6 secret fixture"
+            Right x -> pure x
+    edSecretKey <- extractEd25519SecretKeyFromSKey skey
+    let payload = "v6 multi-hash fixture payload"
+        keyring =
+            [ TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = []
+                , _tkUIDs = []
+                , _tkUAts = []
+                , _tkSubs = []
+                }
+            ]
+        state =
+            emptyPSC
+                { lastLD = LiteralDataPkt BinaryData BL.empty 0 payload
+                }
+        testHash ha = do
+            salt <- case signatureV6SaltSizeForHashAlgorithm ha of
+                Nothing ->
+                    assertFailure
+                        ( "v6 multi-hash fixture does not define salt size for: "
+                            ++ show ha
+                        )
+                        >> fail "expected defined salt size"
+                Just sz -> pure (SignatureSalt (BL.replicate (fromIntegral sz) 0xAB))
+            let hashed = [SigSubPacket False (SigCreationTime 0)]
+                unhashed =
+                    [ SigSubPacket
+                        False
+                        (IssuerFingerprint IssuerFingerprintV6 (fingerprint pkp))
+                    ]
+            sigPayload <-
+                case signDataWithEd25519V6
+                    BinarySig
+                    salt
+                    edSecretKey
+                    hashed
+                    unhashed
+                    payload of
+                    Left err ->
+                        assertFailure
+                            ( "v6 fixture signing failed for "
+                                ++ show ha
+                                ++ ": "
+                                ++ renderSignError err
+                            )
+                            >> fail "expected v6 fixture signature"
+                    Right sig -> pure sig
+            case verifySigWith
+                defaultVerificationPolicy
+                (verifyAgainstKeys keyring)
+                (SignaturePkt sigPayload)
+                state
+                Nothing of
+                Left err ->
+                    assertFailure
+                        ( "v6 fixture signature should verify for "
+                            ++ show ha
+                            ++ ": "
+                            ++ renderVerificationError err
+                        )
+                Right _ -> pure ()
+    mapM_ testHash [SHA512]
diff --git a/tests/Tests/Utilities.hs b/tests/Tests/Utilities.hs
--- a/tests/Tests/Utilities.hs
+++ b/tests/Tests/Utilities.hs
@@ -1558,7 +1558,7 @@
                 V4
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 ( EdDSAPubKey
                     EdSigningCurve25519
                     ( PrefixedNativeEPoint
@@ -1590,12 +1590,12 @@
                 V6
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))
         invalidSig =
             SigV6
                 GenericCert
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x01))
                 []
@@ -1626,12 +1626,12 @@
                 V6
                 (ThirtyTwoBitTimeStamp 0)
                 0
-                EdDSA
+                EdDSALegacy
                 (EdDSAPubKey EdSigningCurve25519 (NativeEPoint (EPoint 1)))
         allowedSig =
             SigV6
                 KeyRevocationSig
-                EdDSA
+                EdDSALegacy
                 SHA512
                 (SignatureSalt (BL.replicate 32 0x02))
                 []
