diff --git a/Codec/Encryption/OpenPGP/BlockCipher.hs b/Codec/Encryption/OpenPGP/BlockCipher.hs
--- a/Codec/Encryption/OpenPGP/BlockCipher.hs
+++ b/Codec/Encryption/OpenPGP/BlockCipher.hs
@@ -8,6 +8,7 @@
     ( keySize
     , supportedSymmetricAlgorithmsForCFB
     , withSymmetricCipher
+    , withAEADCipher
     ) where
 
 import qualified Crypto.Cipher.AES as AES
@@ -25,91 +26,89 @@
     )
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Errors
-    ( CipherError (..)
-    , renderCipherError
-    )
 
 type HOCipher a =
     forall cipher
      . HOBlockCipher cipher
-    => cipher -> Either String a
+    => cipher -> Either CipherError a
 
 withSymmetricCipher
     :: SymmetricAlgorithm
     -> B.ByteString
     -> HOCipher a
     -> Either CipherError a
-withSymmetricCipher Plaintext _ _ = Left (UnsupportedAlgorithm Plaintext)
-withSymmetricCipher IDEA _ _ = Left (UnsupportedAlgorithm IDEA)
-withSymmetricCipher ReservedSAFER _ _ = Left (UnsupportedAlgorithm ReservedSAFER)
-withSymmetricCipher ReservedDES _ _ = Left (UnsupportedAlgorithm ReservedDES)
-withSymmetricCipher (OtherSA n) _ _ = Left (UnsupportedAlgorithm (OtherSA n))
+withSymmetricCipher Plaintext _ _ = Left (CipherUnsupportedAlgorithm Plaintext)
+withSymmetricCipher IDEA _ _ = Left (CipherUnsupportedAlgorithm IDEA)
+withSymmetricCipher ReservedSAFER _ _ = Left (CipherUnsupportedAlgorithm ReservedSAFER)
+withSymmetricCipher ReservedDES _ _ = Left (CipherUnsupportedAlgorithm ReservedDES)
+withSymmetricCipher (OtherSA n) _ _ = Left (CipherUnsupportedAlgorithm (OtherSA n))
 withSymmetricCipher CAST5 keyBytes f =
-    initAndRun
-        CAST5
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedOldCCT CNC.CAST128)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedOldCCT CNC.CAST128)
+    )
+        >>= f
 withSymmetricCipher Twofish keyBytes f =
-    initAndRun
-        Twofish
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedOldCCT CNC.TWOFISH)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedOldCCT CNC.TWOFISH)
+    )
+        >>= f
 withSymmetricCipher TripleDES keyBytes f =
-    initAndRun
-        TripleDES
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedCCT TripleDES.DES_EDE3)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedCCT TripleDES.DES_EDE3)
+    )
+        >>= f
 withSymmetricCipher Blowfish keyBytes f =
-    initAndRun
-        Blowfish
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedCCT Blowfish.Blowfish128)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedCCT Blowfish.Blowfish128)
+    )
+        >>= f
 withSymmetricCipher AES128 keyBytes f =
-    initAndRun
-        AES128
-        (cipherInit keyBytes :: Either String (HOWrappedCCT AES.AES128))
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedCCT AES.AES128)
+    )
+        >>= f
 withSymmetricCipher AES192 keyBytes f =
-    initAndRun
-        AES192
-        (cipherInit keyBytes :: Either String (HOWrappedCCT AES.AES192))
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedCCT AES.AES192)
+    )
+        >>= f
 withSymmetricCipher AES256 keyBytes f =
-    initAndRun
-        AES256
-        (cipherInit keyBytes :: Either String (HOWrappedCCT AES.AES256))
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedCCT AES.AES256)
+    )
+        >>= f
 withSymmetricCipher Camellia128 keyBytes f =
-    initAndRun
-        Camellia128
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedCCT Camellia.Camellia128)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedCCT Camellia.Camellia128)
+    )
+        >>= f
 withSymmetricCipher Camellia192 keyBytes f =
-    initAndRun
-        Camellia192
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedOldCCT CNC.Camellia192)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedOldCCT CNC.Camellia192)
+    )
+        >>= f
 withSymmetricCipher Camellia256 keyBytes f =
-    initAndRun
-        Camellia256
-        ( cipherInit keyBytes
-            :: Either String (HOWrappedOldCCT CNC.Camellia256)
-        )
-        f
+    ( cipherInit keyBytes
+        :: Either CipherError (HOWrappedOldCCT CNC.Camellia256)
+    )
+        >>= f
 
+withAEADCipher
+    :: SymmetricAlgorithm
+    -> B.ByteString
+    -> HOCipher a
+    -> Either CipherError a
+withAEADCipher symalgo keyBytes f =
+    case symalgo of
+        AES128 -> withSymmetricCipher AES128 keyBytes f
+        AES192 -> withSymmetricCipher AES192 keyBytes f
+        AES256 -> withSymmetricCipher AES256 keyBytes f
+        Camellia128 -> withSymmetricCipher Camellia128 keyBytes f
+        Twofish -> withSymmetricCipher Twofish keyBytes f
+        Camellia192 -> withSymmetricCipher Camellia192 keyBytes f
+        Camellia256 -> withSymmetricCipher Camellia256 keyBytes f
+        _ -> Left (CipherUnsupportedAlgorithm symalgo)
+
 {- | Symmetric algorithms that the CFB (SEIPDv1) encryption backend can use for
 new *encryption*, restricted to the RFC 9580 §9.3-permitted set.
 
@@ -135,20 +134,6 @@
     , Blowfish
     ]
 
-initAndRun
-    :: HOBlockCipher cipher
-    => SymmetricAlgorithm
-    -> Either String cipher
-    -> (cipher -> Either String a)
-    -> Either CipherError a
-initAndRun algo initResult f =
-    case initResult of
-        Left err -> Left (CipherInitFailed algo err)
-        Right c ->
-            case f c of
-                Left err -> Left (CipherOperationFailed err)
-                Right x -> Right x
-
 -- In octets. Keep this as an explicit OpenPGP algorithm mapping so behavior
 -- stays stable across mixed backends (crypton/nettle) and includes unsupported
 -- algorithms that never reach backend cipher types.
@@ -158,8 +143,8 @@
 keySize TripleDES = Right 24
 keySize CAST5 = Right 16
 keySize Blowfish = Right 16
-keySize ReservedSAFER = Left (UnsupportedAlgorithm ReservedSAFER)
-keySize ReservedDES = Left (UnsupportedAlgorithm ReservedDES)
+keySize ReservedSAFER = Left (CipherUnsupportedAlgorithm ReservedSAFER)
+keySize ReservedDES = Left (CipherUnsupportedAlgorithm ReservedDES)
 keySize AES128 = Right 16
 keySize AES192 = Right 24
 keySize AES256 = Right 32
@@ -167,4 +152,4 @@
 keySize Camellia128 = Right 16
 keySize Camellia192 = Right 24
 keySize Camellia256 = Right 32
-keySize (OtherSA n) = Left (UnsupportedAlgorithm (OtherSA n))
+keySize (OtherSA n) = Left (CipherUnsupportedAlgorithm (OtherSA n))
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
@@ -26,6 +26,9 @@
     )
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
 import Codec.Encryption.OpenPGP.Types
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    )
 
 data OpenPGPCFBMode
     = OpenPGPCFBResync
@@ -56,13 +59,13 @@
         cleartext <- decrypt2 ciphertext bc
         if nonceCheck bc nonce
             then return (nonce, cleartext)
-            else Left "Session key quickcheck failed"
+            else Left CipherSessionKeyQuickcheckFailed
   where
     decrypt1
         :: HOBlockCipher cipher
         => B.ByteString
         -> cipher
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     decrypt1 ct cipher =
         paddedCfbDecrypt
             cipher
@@ -72,7 +75,7 @@
         :: HOBlockCipher cipher
         => B.ByteString
         -> cipher
-        -> Either String B.ByteString
+        -> Either CipherError 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)
@@ -98,7 +101,7 @@
         let (nonce, cleartext) = B.splitAt (bs + 2) decrypted
         if nonceCheck bc nonce
             then return (nonce, cleartext)
-            else Left "Session key quickcheck failed"
+            else Left CipherSessionKeyQuickcheckFailed
 
 decryptNoNonce
     :: SymmetricAlgorithm
@@ -114,7 +117,7 @@
         :: HOBlockCipher cipher
         => B.ByteString
         -> cipher
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     decrypt' ct cipher = paddedCfbDecrypt cipher (unIV iv) ct
 
 nonceCheck
@@ -139,7 +142,7 @@
         :: HOBlockCipher cipher
         => B.ByteString
         -> cipher
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     encrypt' ct cipher = paddedCfbEncrypt cipher (unIV iv) ct
 
 encryptOpenPGPCfbRaw
@@ -159,12 +162,14 @@
         if B.length initialVector /= bs
             then
                 Left
-                    ( "IV length mismatch for "
-                        ++ show sa
-                        ++ ": expected "
-                        ++ show bs
-                        ++ ", got "
-                        ++ show (B.length initialVector)
+                    ( CipherBadIV
+                        ( "IV length mismatch for "
+                            ++ show sa
+                            ++ ": expected "
+                            ++ show bs
+                            ++ ", got "
+                            ++ show (B.length initialVector)
+                        )
                     )
             else do
                 let prefix = initialVector <> B.drop (bs - 2) initialVector
@@ -178,5 +183,10 @@
                                 (B.take bs (B.drop 2 nonceAndCheck))
                                 cleartext
                         return (nonceAndCheck <> encryptedPayload)
-                    OpenPGPCFBNoResyncW ->
-                        paddedCfbEncrypt cipher (B.replicate bs 0) (prefix <> cleartext)
+                    OpenPGPCFBNoResyncW -> do
+                        ct <-
+                            paddedCfbEncrypt
+                                cipher
+                                (B.replicate bs 0)
+                                (prefix <> cleartext)
+                        return ct
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
@@ -50,7 +50,6 @@
     , RecipientEncryptRequest (..)
     , RecipientEncryptRequestOverrides (..)
     , encryptForRecipients
-    , encryptForRecipientsLegacy
     , encryptForRecipientsWithCapabilityNegotiation
     , SharedSessionRecipient (..)
     , SharedSessionEncryptRequest (..)
@@ -84,10 +83,9 @@
     , aesKeyWrapRFC3394
     , deriveX25519Kek
     , deriveX448Kek
-    , PKAEncryptOps (..)
-    , PKAEncryptOpsDict (..)
-    , SomePKAEncryptOpsDict (..)
-    , pkaEncryptOpsDict
+    , PubKeyEncryptOps (..)
+    , SomePubKeyEncryptOps (..)
+    , pubKeyEncryptOps
     ) where
 
 import Control.Applicative ((<|>))
@@ -97,6 +95,7 @@
 import Control.Monad.Trans.Class (lift)
 import Control.Monad.Trans.Except
     ( ExceptT (..)
+    , except
     , runExceptT
     , throwE
     )
@@ -135,6 +134,7 @@
 import Codec.Encryption.OpenPGP.BlockCipher
     ( keySize
     , supportedSymmetricAlgorithmsForCFB
+    , withAEADCipher
     , withSymmetricCipher
     )
 import Codec.Encryption.OpenPGP.CFB
@@ -162,14 +162,14 @@
     , point2MBS
     , xorBS
     )
-import Codec.Encryption.OpenPGP.Internal.CryptoAES
-    ( withAESCipher
-    )
 import Codec.Encryption.OpenPGP.Internal.CryptoECDH
     ( buildECDHKDFParam
     , deriveECDHKek
     , normalizeMontgomeryPublic
     )
+import Codec.Encryption.OpenPGP.Internal.Crypton
+    ( HOWrappedCCT (..)
+    )
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
     ( HOBlockCipher (..)
     )
@@ -241,68 +241,70 @@
     pkaBuildV3PKESK _ = buildX448PKESKv3
     pkaBuildV6PKESK _ r m = buildX448PKESKv6 r (pkeskV6RawSessionMaterial m)
 
-data PKAEncryptOpsDict = PKAEncryptOpsDict
-    { pkaDictBuildV3
+data PubKeyEncryptOps = PubKeyEncryptOps
+    { pubKeyBuildV3PKESK
         :: forall m
          . MonadRandom m
         => SomePKPayload
         -> PKESKV3SessionMaterial
-        -> m (Either PKESKEncryptError PKESKPayloadV3)
-    , pkaDictBuildV6
+        -> ExceptT PKESKEncryptError m PKESKPayloadV3
+    , pubKeyBuildV6PKESK
         :: forall m
          . MonadRandom m
         => SomePKPayload
         -> PKESKSessionMaterial
-        -> m (Either PKESKEncryptError PKESKPayloadV6)
+        -> ExceptT PKESKEncryptError m PKESKPayloadV6
     }
 
-data SomePKAEncryptOpsDict where
-    SomePKAEncryptOpsDict
-        :: PKAEncryptOpsDict -> SomePKAEncryptOpsDict
+data SomePubKeyEncryptOps where
+    SomePubKeyEncryptOps
+        :: PubKeyEncryptOps -> SomePubKeyEncryptOps
 
-pkaEncryptOpsDict
-    :: PubKeyAlgorithm -> Maybe SomePKAEncryptOpsDict
-pkaEncryptOpsDict RSA =
+pubKeyEncryptOps
+    :: PubKeyAlgorithm -> Maybe SomePubKeyEncryptOps
+pubKeyEncryptOps RSA =
     Just
-        ( SomePKAEncryptOpsDict
-            ( PKAEncryptOpsDict
-                buildRsaPKESKv3
-                (\r m -> buildRsaPKESKv6 r m)
+        ( SomePubKeyEncryptOps
+            ( PubKeyEncryptOps
+                (\r m -> ExceptT (buildRsaPKESKv3 r m))
+                (\r m -> ExceptT (buildRsaPKESKv6 r m))
             )
         )
-pkaEncryptOpsDict DeprecatedRSAEncryptOnly =
+pubKeyEncryptOps DeprecatedRSAEncryptOnly =
     Just
-        ( SomePKAEncryptOpsDict
-            ( PKAEncryptOpsDict
-                buildRsaPKESKv3
-                (\r m -> buildRsaPKESKv6 r m)
+        ( SomePubKeyEncryptOps
+            ( PubKeyEncryptOps
+                (\r m -> ExceptT (buildRsaPKESKv3 r m))
+                (\r m -> ExceptT (buildRsaPKESKv6 r m))
             )
         )
-pkaEncryptOpsDict ECDH =
+pubKeyEncryptOps ECDH =
     Just
-        ( SomePKAEncryptOpsDict
-            ( PKAEncryptOpsDict
-                buildECDHPKESKv3
-                (\r m -> buildECDHPKESKv6 r m)
+        ( SomePubKeyEncryptOps
+            ( PubKeyEncryptOps
+                (\r m -> ExceptT (buildECDHPKESKv3 r m))
+                (\r m -> ExceptT (buildECDHPKESKv6 r m))
             )
         )
-pkaEncryptOpsDict X25519 =
+pubKeyEncryptOps X25519 =
     Just
-        ( SomePKAEncryptOpsDict
-            ( PKAEncryptOpsDict
-                buildX25519PKESKv3
-                (\r m -> buildX25519PKESKv6 r (pkeskV6RawSessionMaterial m))
+        ( SomePubKeyEncryptOps
+            ( PubKeyEncryptOps
+                (\r m -> ExceptT (buildX25519PKESKv3 r m))
+                ( \r m -> ExceptT (buildX25519PKESKv6 r (pkeskV6RawSessionMaterial m))
+                )
             )
         )
-pkaEncryptOpsDict X448 =
+pubKeyEncryptOps X448 =
     Just
-        ( SomePKAEncryptOpsDict
-            ( PKAEncryptOpsDict
-                buildX448PKESKv3
-                (\r m -> buildX448PKESKv6 r (pkeskV6RawSessionMaterial m))
+        ( SomePubKeyEncryptOps
+            ( PubKeyEncryptOps
+                (\r m -> ExceptT (buildX448PKESKv3 r m))
+                ( \r m -> ExceptT (buildX448PKESKv6 r (pkeskV6RawSessionMaterial m))
+                )
             )
         )
-pkaEncryptOpsDict _ = Nothing
+pubKeyEncryptOps _ = Nothing
 
 data RecipientCapabilityNegotiationMode
     = RecipientCapabilityNegotiationOff
@@ -655,7 +657,7 @@
 
 supportsPKESKRecipientAlgorithm :: SomePKPayload -> Bool
 supportsPKESKRecipientAlgorithm recipient =
-    isJust (pkaEncryptOpsDict (_pkalgo recipient))
+    isJust (pubKeyEncryptOps (_pkalgo recipient))
 
 chooseRecipientTarget
     :: RecipientTargetSelectionPolicy
@@ -1034,10 +1036,10 @@
                     publicTargets
                     material
         passwordPackets <- forM passwordRecipients $ \passphrase ->
-            ExceptT . pure $
+            except $
                 buildSharedPasswordSKESK request material passphrase
         packets <-
-            ExceptT . pure $
+            except $
                 buildEncryptedPacketSequenceWithShape
                     symalgo
                     aead
@@ -1167,17 +1169,13 @@
     :: MonadRandom m
     => SymmetricAlgorithm
     -> m (Either PKESKEncryptError PKESKSessionMaterial)
-generateSessionKeyMaterial symalgo =
-    case keySize symalgo of
-        Left err ->
-            pure
-                ( Left
-                    (UnsupportedSessionKeyAlgorithm symalgo err)
-                )
-        Right keyLen -> do
-            sessionKeyBytes <- getRandomBytes keyLen
-            let sessionKey = SessionKey sessionKeyBytes
-            pure (mkPKESKSessionMaterial symalgo sessionKey)
+generateSessionKeyMaterial symalgo = runExceptT $ do
+    keyLen <-
+        except $
+            first (UnsupportedSessionKeyAlgorithm symalgo) (keySize symalgo)
+    sessionKeyBytes <- lift (getRandomBytes keyLen)
+    let sessionKey = SessionKey sessionKeyBytes
+    except $ mkPKESKSessionMaterial symalgo sessionKey
 
 -- | Build a v6 PKESK payload for one recipient key according to the selected version policy.
 buildPKESKPayloadForRecipient
@@ -1193,13 +1191,13 @@
                 recipient
                 (pkeskV3SessionMaterial material)
         PreferV6 ->
-            case pkaEncryptOpsDict (_pkalgo recipient) of
+            case pubKeyEncryptOps (_pkalgo recipient) of
                 Nothing ->
                     pure (Left (UnsupportedRecipientAlgorithm (_pkalgo recipient)))
-                Just (SomePKAEncryptOpsDict dict) ->
+                Just (SomePubKeyEncryptOps dict) ->
                     fmap
                         (fmap PKESKPayloadV6Packet)
-                        (pkaDictBuildV6 dict recipient material)
+                        (runExceptT (pubKeyBuildV6PKESK dict recipient material))
 
 -- | Build a PKESK packet for one recipient key according to the selected version policy.
 buildPKESKPktForRecipient
@@ -1230,11 +1228,11 @@
     -> PKESKV3SessionMaterial
     -> m (Either PKESKEncryptError PKESKPayloadV3)
 buildPKESKv3PayloadForRecipientTyped recipient material =
-    case pkaEncryptOpsDict (_pkalgo recipient) of
+    case pubKeyEncryptOps (_pkalgo recipient) of
         Nothing ->
             pure (Left (UnsupportedRecipientAlgorithm (_pkalgo recipient)))
-        Just (SomePKAEncryptOpsDict dict) ->
-            pkaDictBuildV3 dict recipient material
+        Just (SomePubKeyEncryptOps dict) ->
+            runExceptT (pubKeyBuildV3PKESK dict recipient material)
 
 -- | Build a legacy PKESKv3 packet for v4/v3 RSA recipient interop.
 buildPKESKv3PktForRecipient
@@ -1330,11 +1328,12 @@
             ( RecipientPreferV6W
                 , RecipientPreferV6Payload material _v6RawMaterial
                 ) ->
-                    case pkaEncryptOpsDict (_pkalgo recipient) of
+                    case pubKeyEncryptOps (_pkalgo recipient) of
                         Nothing ->
                             pure (Left (UnsupportedRecipientAlgorithm (_pkalgo recipient)))
-                        Just (SomePKAEncryptOpsDict dict) ->
-                            packetizeV6 (pkaDictBuildV6 dict recipient material)
+                        Just (SomePubKeyEncryptOps dict) ->
+                            packetizeV6
+                                (runExceptT (pubKeyBuildV6PKESK dict recipient material))
 
 {- | Encrypt for recipient targets with capability negotiation enabled.
 
@@ -1350,15 +1349,6 @@
     encryptForRecipientsWithCapabilityNegotiation
         RecipientCapabilityNegotiationOn
 
-{-# DEPRECATED encryptForRecipientsLegacy "use encryptForRecipients" #-}
-encryptForRecipientsLegacy
-    :: MonadRandom m
-    => RecipientEncryptRequest v
-    -> m (Either PKESKEncryptError RecipientEncryptResult)
-encryptForRecipientsLegacy =
-    encryptForRecipientsWithCapabilityNegotiation
-        RecipientCapabilityNegotiationOff
-
 {- | Encrypt for recipient targets with an explicit capability-negotiation mode.
 
 When negotiation is on, symmetric and AEAD selection use the common
@@ -1374,7 +1364,7 @@
     | null targets = pure (Left NoRecipientsProvided)
     | otherwise = runExceptT $ do
         (symalgo, aead) <-
-            ExceptT . pure $
+            except $
                 selectCiphersuite
                     negotiationMode
                     request
@@ -1466,7 +1456,7 @@
                                         (messageDefaultChunkSize messagePolicy)
                                         id
                                         chunkSizeOverride
-                            ExceptT . pure $
+                            except $
                                 fmap
                                     ( \pkts ->
                                         RecipientEncryptResult
@@ -1500,7 +1490,7 @@
                                             , recipientEncryptSessionMaterial = sessionMaterial
                                             }
                                 missingSEIPDv1 ->
-                                    ExceptT . pure $
+                                    except $
                                         Left
                                             ( RecipientCapabilitySelectionFailure
                                                 (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)
@@ -1523,7 +1513,7 @@
                                     , recipientEncryptSessionMaterial = sessionMaterial
                                     }
                         missingSEIPDv1 ->
-                            ExceptT . pure $
+                            except $
                                 Left
                                     ( RecipientCapabilitySelectionFailure
                                         (RecipientCapabilityMissingSEIPDv1Support missingSEIPDv1)
@@ -2728,21 +2718,16 @@
     -> B.ByteString
     -> Either CipherError B.ByteString
 aesKeyWrapRFC3394 sa kek plain =
-    withAESCipher
-        (\err -> CipherInitFailed sa (show err))
-        (UnsupportedAlgorithm sa)
-        sa
-        kek
-        wrapWithCipher
+    withSymmetricCipher sa kek wrapWithCipher
   where
     wrapWithCipher
-        :: CCT.BlockCipher cipher
+        :: HOBlockCipher cipher
         => cipher -> Either CipherError B.ByteString
     wrapWithCipher cipher = do
         if B.length plain < 16 || B.length plain `mod` 8 /= 0
             then
                 Left
-                    ( CipherOperationFailed
+                    ( CipherKeyWrapInvalidInput
                         "ECDH key wrap input must be at least 16 octets and a multiple of 8"
                     )
             else Right ()
@@ -2750,14 +2735,14 @@
         if length rs < 2
             then
                 Left
-                    ( CipherOperationFailed
+                    ( CipherKeyWrapInvalidInput
                         "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)
     wrapRounds
-        :: CCT.BlockCipher cipher
+        :: HOBlockCipher cipher
         => cipher
         -> B.ByteString
         -> [B.ByteString]
@@ -2776,8 +2761,8 @@
                 | otherwise = do
                     let t = fromIntegral (n * j + i) :: Word64
                         rI = curRs !! (i - 1)
-                        block = CCT.ecbEncrypt cipher (curA <> rI)
-                        (msb, lsb) = B.splitAt 8 block
+                    block <- ecbEncrypt cipher (curA <> rI)
+                    let (msb, lsb) = B.splitAt 8 block
                         aNext = xorBS msb (encodeWord64be t)
                         rsNext = (ix (i - 1) .~ lsb) curRs
                     goI (i + 1) aNext rsNext
@@ -2935,23 +2920,24 @@
         okm = expand @CHA.SHA256 prk info outputLen :: B.ByteString
         messageKey = B.take keyLen okm
         noncePrefix = B.take (nonceSize - 8) (B.drop keyLen okm)
-    withAESCipher
-        (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-        (SEIPDv2UnsupportedSymmetricAlgorithm symalgo)
-        symalgo
-        messageKey
-        ( encryptChunks
+    first
+        SEIPDv2CipherFailed
+        ( withAEADCipher
             symalgo
-            aead
-            mode
-            info
-            chunkSize
-            noncePrefix
-            plaintext
+            messageKey
+            ( encryptChunks
+                symalgo
+                aead
+                mode
+                info
+                chunkSize
+                noncePrefix
+                plaintext
+            )
         )
 
 encryptChunks
-    :: CCT.BlockCipher cipher
+    :: HOBlockCipher cipher
     => SymmetricAlgorithm
     -> AEADAlgorithm
     -> CCT.AEADMode
@@ -2960,7 +2946,7 @@
     -> B.ByteString
     -> B.ByteString
     -> cipher
-    -> Either SEIPDv2Failure B.ByteString
+    -> Either CipherError B.ByteString
 encryptChunks symalgo aead mode info chunkSize noncePrefix plaintext cipher = go 0 plaintext [] 0
   where
     chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)
@@ -2969,16 +2955,15 @@
             (finalTag, finalCipher) <-
                 if mode == CCT.AEAD_OCB
                     then
-                        first SEIPDv2CipherFailed $
-                            encryptWithOCBRFC7253
-                                cipher
-                                (noncePrefix <> encodeWord64be idx)
-                                (info <> encodeWord64be (fromIntegral totalPlain))
-                                B.empty
+                        encryptWithOCBRFC7253
+                            cipher
+                            (noncePrefix <> encodeWord64be idx)
+                            (info <> encodeWord64be (fromIntegral totalPlain))
+                            B.empty
                     else do
                         aead <- initAEAD idx
                         let (tag, out) =
-                                CCT.aeadSimpleEncrypt
+                                aeadSimpleEncrypt
                                     aead
                                     (info <> encodeWord64be (fromIntegral totalPlain))
                                     B.empty
@@ -2988,25 +2973,20 @@
                 then return (B.concat (reverse acc) <> authTagToBS finalTag)
                 else
                     Left
-                        ( SEIPDv2CipherFailed
-                            ( CipherOperationFailed
-                                "expected empty ciphertext for final SEIPD v2 tag"
-                            )
-                        )
+                        (CipherFinalTagEmpty)
         | otherwise = do
             let (chunkPlain, rest) = B.splitAt chunkLen remaining
             (tag, chunkCipher) <-
                 if mode == CCT.AEAD_OCB
                     then
-                        first SEIPDv2CipherFailed $
-                            encryptWithOCBRFC7253
-                                cipher
-                                (noncePrefix <> encodeWord64be idx)
-                                info
-                                chunkPlain
+                        encryptWithOCBRFC7253
+                            cipher
+                            (noncePrefix <> encodeWord64be idx)
+                            info
+                            chunkPlain
                     else do
                         aead <- initAEAD idx
-                        pure (CCT.aeadSimpleEncrypt aead info chunkPlain 16)
+                        pure (aeadSimpleEncrypt aead info chunkPlain 16)
             let chunkOut = chunkCipher <> authTagToBS tag
             go
                 (idx + 1)
@@ -3015,9 +2995,7 @@
                 (totalPlain + B.length chunkPlain)
 
     initAEAD idx =
-        first (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-            . CE.eitherCryptoError
-            $ CCT.aeadInit mode cipher (noncePrefix <> encodeWord64be idx)
+        aeadInit mode cipher (noncePrefix <> encodeWord64be idx)
 
 aeadModeAndNonceSize
     :: AEADAlgorithm -> Either SEIPDv2Failure (CCT.AEADMode, Int)
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
@@ -56,10 +56,10 @@
     deriving (Eq, Show)
 
 -- this assumes that all key expiration time subpackets are valid
-isTKTimeValid :: UTCTime -> TK k -> Bool
+isTKTimeValid :: TKPrimaryPKPayload k => UTCTime -> TK k -> Bool
 isTKTimeValid ct = keyStateValid . keyStateAt ct
 
-keyStateAt :: UTCTime -> TK k -> KeyState
+keyStateAt :: TKPrimaryPKPayload k => UTCTime -> TK k -> KeyState
 keyStateAt ct tk =
     baseState
         { keyStateValid =
@@ -69,7 +69,7 @@
     baseState =
         keyStateFromSelfSignaturesAt
             ct
-            (keyPktPKPayload (tk ^. tkPrimaryKey))
+            (tkPrimaryPKPayload tk)
             relevantSelfSignatures
     relevantSelfSignatures =
         concat
@@ -91,10 +91,11 @@
         any (selfCertificationGroupActiveAt ct) selfCertificationGroups
     bindingStateAllowsValidation =
         not hasAnySelfCertification || hasAnyActiveSelfCertification
-    primaryKey = keyPktPKPayload (tk ^. tkPrimaryKey)
+    primaryKey = tkPrimaryPKPayload tk
 
 effectiveKeyPreferencesAt
-    :: UTCTime -> TK k -> Maybe [SigSubPacketPayload]
+    :: TKPrimaryPKPayload k
+    => UTCTime -> TK k -> Maybe [SigSubPacketPayload]
 effectiveKeyPreferencesAt ct tk
     | not (keyStateValid (keyStateAt ct tk)) = Nothing
     | otherwise = do
@@ -105,7 +106,8 @@
             else Just prefs
 
 effectiveUIDPreferencesAt
-    :: UTCTime -> Text -> TK k -> Maybe [SigSubPacketPayload]
+    :: TKPrimaryPKPayload k
+    => UTCTime -> Text -> TK k -> Maybe [SigSubPacketPayload]
 effectiveUIDPreferencesAt ct uid tk
     | not (keyStateValid (keyStateAt ct tk)) = Nothing
     | otherwise = do
@@ -119,16 +121,18 @@
             then Nothing
             else Just prefs
   where
-    primaryKey = keyPktPKPayload (tk ^. tkPrimaryKey)
+    primaryKey = tkPrimaryPKPayload tk
 
 effectiveKeyPreferencesAtTimestamp
-    :: ThirtyTwoBitTimeStamp -> TK k -> Maybe [SigSubPacketPayload]
+    :: TKPrimaryPKPayload k
+    => ThirtyTwoBitTimeStamp -> TK k -> Maybe [SigSubPacketPayload]
 effectiveKeyPreferencesAtTimestamp ts =
     effectiveKeyPreferencesAt
         (posixSecondsToUTCTime (realToFrac (unThirtyTwoBitTimeStamp ts)))
 
 effectiveUIDPreferencesAtTimestamp
-    :: ThirtyTwoBitTimeStamp
+    :: TKPrimaryPKPayload k
+    => ThirtyTwoBitTimeStamp
     -> Text
     -> TK k
     -> Maybe [SigSubPacketPayload]
@@ -298,12 +302,12 @@
         filter (\sig -> isCertRevocationForTime ct sig) sigs
 
 latestEffectivePreferenceCarrierAt
-    :: UTCTime -> TK k -> Maybe SignaturePayload
+    :: TKPrimaryPKPayload k => UTCTime -> TK k -> Maybe SignaturePayload
 latestEffectivePreferenceCarrierAt ct tk =
     snd
         <$> newestByCreationTime (mapMaybeSignatureCreationTime candidates)
   where
-    primaryKey = keyPktPKPayload (tk ^. tkPrimaryKey)
+    primaryKey = tkPrimaryPKPayload tk
     directKeySigs =
         filter
             ( \sig ->
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
@@ -34,21 +34,16 @@
 import qualified Crypto.PubKey.ECC.Types as ECCT
 import qualified Crypto.PubKey.RSA as RSA
 import Data.Binary.Put (putWord64be, runPut)
-import Data.Bits (shiftR, testBit, xor, (.&.))
+import Data.Bits (shiftR, 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, Word64, Word8)
+import Data.Word (Word16, Word64)
 
 import Codec.Encryption.OpenPGP.Ontology
     ( isIssuerSSP
     )
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Errors
-    ( CurveConversionError (..)
-    , renderCurveConversionError
-    )
 
 data PktStreamContext
     = PktStreamContext
@@ -196,12 +191,6 @@
 edPointInteger :: EdPoint -> Integer
 edPointInteger (PrefixedNativeEPoint (EPoint x)) = x
 edPointInteger (NativeEPoint (EPoint x)) = x
-
-multiplicativeInverse :: (Integral a) => a -> a -> a
-multiplicativeInverse _ 1 = 1
-multiplicativeInverse q p = (n * q + 1) `div` p
-  where
-    n = p - multiplicativeInverse p (q `mod` p)
 
 curveoidBSToCurve
     :: B.ByteString -> Either CurveConversionError ECCCurve
diff --git a/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs b/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs
deleted file mode 100644
--- a/Codec/Encryption/OpenPGP/Internal/CryptoAES.hs
+++ /dev/null
@@ -1,50 +0,0 @@
--- CryptoAES.hs: OpenPGP (RFC9580) AES helper utilities
--- 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
-
-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
-
-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/CryptoCipherTypes.hs b/Codec/Encryption/OpenPGP/Internal/CryptoCipherTypes.hs
--- a/Codec/Encryption/OpenPGP/Internal/CryptoCipherTypes.hs
+++ b/Codec/Encryption/OpenPGP/Internal/CryptoCipherTypes.hs
@@ -1,9 +1,10 @@
 -- CryptoCipherTypes.hs: shim for crypto-cipher-types stuff (current nettle)
--- Copyright © 2016-2024  Clint Adams
+-- Copyright © 2016-2026  Clint Adams
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE PackageImports #-}
+{-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE UndecidableInstances #-}
 
 module Codec.Encryption.OpenPGP.Internal.CryptoCipherTypes
@@ -11,11 +12,16 @@
     ) where
 
 import Control.Error.Util (note)
+import Data.Bifunctor (bimap)
+import qualified Data.ByteArray as BA
 import qualified Data.ByteString as B
 import qualified "crypto-cipher-types" Crypto.Cipher.Types as OldCCT
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    )
 
 newtype HOWrappedOldCCT a
     = HWOCCT a
@@ -24,21 +30,23 @@
     OldCCT.BlockCipher cipher
     => HOBlockCipher (HOWrappedOldCCT cipher)
     where
-    cipherInit =
-        fmap HWOCCT
-            . either
-                (const (Left "nettle invalid key"))
-                (Right . OldCCT.cipherInit)
-            . OldCCT.makeKey
+    cipherInit key =
+        let keyBS = BA.convert key :: B.ByteString
+         in bimap
+                (const (CipherOldInitFailed "nettle invalid key"))
+                (HWOCCT . OldCCT.cipherInit)
+                (OldCCT.makeKey keyBS)
     cipherName (HWOCCT c) = OldCCT.cipherName c
     cipherKeySize (HWOCCT c) = convertKSS . OldCCT.cipherKeySize $ c
     blockSize (HWOCCT c) = OldCCT.blockSize c
+    ecbEncrypt (HWOCCT c) bs = Right (OldCCT.ecbEncrypt c bs)
+    ecbDecrypt (HWOCCT c) bs = Right (OldCCT.ecbDecrypt c bs)
     cfbEncrypt (HWOCCT c) iv bs =
         hammerIV iv >>= \i -> return (OldCCT.cfbEncrypt c i bs)
     cfbDecrypt (HWOCCT c) iv bs =
         hammerIV iv >>= \i -> return (OldCCT.cfbDecrypt c i bs)
     paddedCfbEncrypt _ _ _ =
-        Left "padding for nettle-encryption not implemented yet"
+        Left CipherPaddingUnsupported
     paddedCfbDecrypt (HWOCCT cipher) iv ciphertext =
         hammerIV iv >>= \i ->
             return
@@ -53,14 +61,43 @@
                         )
                         0
                     )
+    aeadInit mode (HWOCCT c) iv =
+        case OldCCT.aeadInit (convertMode mode) c iv of
+            Nothing -> Left CipherAEADModeUnsupported
+            Just (OldCCT.AEAD _ (OldCCT.AEADState st)) ->
+                Right (CCT.AEAD (bridgeImpl c) st)
+    aeadSimpleEncrypt aead aad pt plen =
+        CCT.aeadSimpleEncrypt aead aad pt plen
+    aeadSimpleDecrypt aead aad ct tag =
+        CCT.aeadSimpleDecrypt aead aad ct tag
 
 convertKSS :: OldCCT.KeySizeSpecifier -> CCT.KeySizeSpecifier
 convertKSS (OldCCT.KeySizeRange a b) = CCT.KeySizeRange a b
 convertKSS (OldCCT.KeySizeEnum as) = CCT.KeySizeEnum as
 convertKSS (OldCCT.KeySizeFixed a) = CCT.KeySizeFixed a
 
+convertMode :: CCT.AEADMode -> OldCCT.AEADMode
+convertMode CCT.AEAD_GCM = OldCCT.AEAD_GCM
+convertMode (CCT.AEAD_CCM 0 CCT.CCM_M16 CCT.CCM_L2) = OldCCT.AEAD_CCM
+convertMode _ = OldCCT.AEAD_GCM
+
+bridgeImpl
+    :: OldCCT.AEADModeImpl cipher st => cipher -> CCT.AEADModeImpl st
+bridgeImpl c =
+    CCT.AEADModeImpl
+        { CCT.aeadImplAppendHeader = \st' ba -> OldCCT.aeadStateAppendHeader c st' (BA.convert ba)
+        , CCT.aeadImplEncrypt = \st' ba ->
+            let (ct, st'') = OldCCT.aeadStateEncrypt c st' (BA.convert ba)
+             in (BA.convert ct, st'')
+        , CCT.aeadImplDecrypt = \st' ba ->
+            let (pt, st'') = OldCCT.aeadStateDecrypt c st' (BA.convert ba)
+             in (BA.convert pt, st'')
+        , CCT.aeadImplFinalize = \st' plen ->
+            let OldCCT.AuthTag bs = OldCCT.aeadStateFinalize c st' plen
+             in CCT.AuthTag (BA.convert bs)
+        }
+
 hammerIV
     :: OldCCT.BlockCipher cipher
-    => B.ByteString
-    -> Either String (OldCCT.IV cipher)
-hammerIV = note "nettle bad IV" . OldCCT.makeIV
+    => B.ByteString -> Either CipherError (OldCCT.IV cipher)
+hammerIV = note (CipherBadIV "nettle") . OldCCT.makeIV
diff --git a/Codec/Encryption/OpenPGP/Internal/CryptoECDH.hs b/Codec/Encryption/OpenPGP/Internal/CryptoECDH.hs
--- a/Codec/Encryption/OpenPGP/Internal/CryptoECDH.hs
+++ b/Codec/Encryption/OpenPGP/Internal/CryptoECDH.hs
@@ -25,6 +25,9 @@
     )
 import Codec.Encryption.OpenPGP.Policy (ecdhKdfHashDigest)
 import Codec.Encryption.OpenPGP.Types
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    )
 
 normalizeMontgomeryPublic
     :: Int
@@ -44,7 +47,7 @@
     -> PKey
     -> HashAlgorithm
     -> SymmetricAlgorithm
-    -> Either String B.ByteString
+    -> Either CipherError B.ByteString
 buildECDHKDFParam recipientPKP pka recipientECDHPub kdfHA kdfSA =
     ( <>
         B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA]
@@ -54,32 +57,36 @@
         <$> encodedCurveOid
   where
     encodedCurveOid =
-        ((\oid -> B.singleton (fromIntegral (B.length oid)) <> oid) <$>)
-            curveOid
+        (\oid -> B.singleton (fromIntegral (B.length oid)) <> oid)
+            <$> curveOid
     curveOid =
         case recipientECDHPub of
             ECDSAPubKey (ECDSA_PublicKey (ECDSA.PublicKey curve _)) ->
                 first
-                    renderCurveConversionError
+                    CipherCurveConversionFailed
                     (curveToCurveoidBS (curveFromCurve curve))
             EdDSAPubKey EdSigningCurve25519 _ ->
-                first renderCurveConversionError (curveToCurveoidBS Curve25519)
+                first CipherCurveConversionFailed (curveToCurveoidBS Curve25519)
             EdDSAPubKey EdSigningCurve448 _ ->
-                first renderCurveConversionError (curveToCurveoidBS Curve448)
-            _ -> Left "ECDH KDF param requires ECDH recipient key"
+                first CipherCurveConversionFailed (curveToCurveoidBS Curve448)
+            _ -> Left CipherInvalidECDHRecipient
 
 deriveECDHKek
     :: HashAlgorithm
     -> SymmetricAlgorithm
     -> B.ByteString
     -> B.ByteString
-    -> Either String B.ByteString
+    -> Either CipherError B.ByteString
 deriveECDHKek kdfHA kdfSA sharedSecret kdfParam = do
     digest <-
         ecdhKdfHashDigest
             kdfHA
             (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam)
-    kekLen <- first renderCipherError (keySize kdfSA)
+    kekLen <- keySize kdfSA
     if B.length digest < kekLen
-        then Left "ECDH KDF digest is shorter than required KEK length"
+        then
+            Left
+                ( CipherKeyWrapInvalidInput
+                    "ECDH KDF digest is shorter than required KEK length"
+                )
         else Right (B.take kekLen digest)
diff --git a/Codec/Encryption/OpenPGP/Internal/Crypton.hs b/Codec/Encryption/OpenPGP/Internal/Crypton.hs
--- a/Codec/Encryption/OpenPGP/Internal/Crypton.hs
+++ b/Codec/Encryption/OpenPGP/Internal/Crypton.hs
@@ -1,5 +1,5 @@
 -- Crypton.hs: shim for crypton
--- Copyright © 2016-2024  Clint Adams
+-- Copyright © 2016-2026  Clint Adams
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
 {-# LANGUAGE FlexibleInstances #-}
@@ -18,21 +18,41 @@
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
 import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    )
 
 newtype HOWrappedCCT a
     = HWCCT a
 
 instance CCT.BlockCipher cipher => HOBlockCipher (HOWrappedCCT cipher) where
-    cipherInit = bimap show HWCCT . CE.eitherCryptoError . CCT.cipherInit
+    cipherInit =
+        bimap CipherOperationFailed HWCCT
+            . CE.eitherCryptoError
+            . CCT.cipherInit
     cipherName (HWCCT c) = CCT.cipherName c
     cipherKeySize (HWCCT c) = CCT.cipherKeySize c
     blockSize (HWCCT c) = CCT.blockSize c
+    ecbEncrypt (HWCCT c) bs = Right (CCT.ecbEncrypt c bs)
+    ecbDecrypt (HWCCT c) bs = Right (CCT.ecbDecrypt c bs)
     cfbEncrypt (HWCCT c) iv bs =
         hammerIV iv >>= \i -> return (CCT.cfbEncrypt c i bs)
     cfbDecrypt (HWCCT c) iv bs =
         hammerIV iv >>= \i -> return (CCT.cfbDecrypt c i bs)
+    aeadInit mode (HWCCT c) iv =
+        fmap
+            (\(CCT.AEAD impl st) -> CCT.AEAD impl st)
+            ( bimap
+                CipherOperationFailed
+                id
+                (CE.eitherCryptoError (CCT.aeadInit mode c iv))
+            )
+    aeadSimpleEncrypt aead aad pt plen =
+        CCT.aeadSimpleEncrypt aead aad pt plen
+    aeadSimpleDecrypt aead aad ct tag =
+        CCT.aeadSimpleDecrypt aead aad ct tag
 
 hammerIV
     :: CCT.BlockCipher cipher
-    => B.ByteString -> Either String (CCT.IV cipher)
-hammerIV = note "crypton bad IV" . CCT.makeIV
+    => B.ByteString -> Either CipherError (CCT.IV cipher)
+hammerIV = note (CipherBadIV "crypton") . CCT.makeIV
diff --git a/Codec/Encryption/OpenPGP/Internal/HOBlockCipher.hs b/Codec/Encryption/OpenPGP/Internal/HOBlockCipher.hs
--- a/Codec/Encryption/OpenPGP/Internal/HOBlockCipher.hs
+++ b/Codec/Encryption/OpenPGP/Internal/HOBlockCipher.hs
@@ -8,33 +8,64 @@
     ( HOBlockCipher (..)
     ) where
 
+import Data.ByteArray (ByteArray, ByteArrayAccess)
 import qualified Data.ByteString as B
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    )
+
 class HOBlockCipher cipher where
-    cipherInit :: B.ByteString -> Either String cipher
+    cipherInit :: ByteArray key => key -> Either CipherError cipher
     cipherName :: cipher -> String
     cipherKeySize :: cipher -> CCT.KeySizeSpecifier
     blockSize :: cipher -> Int
+    ecbEncrypt
+        :: cipher -> B.ByteString -> Either CipherError B.ByteString
+    ecbDecrypt
+        :: cipher -> B.ByteString -> Either CipherError B.ByteString
     cfbEncrypt
         :: cipher
         -> B.ByteString
         -> B.ByteString
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     cfbDecrypt
         :: cipher
         -> B.ByteString
         -> B.ByteString
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     paddedCfbEncrypt
         :: cipher
         -> B.ByteString
         -> B.ByteString
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     paddedCfbEncrypt = cfbEncrypt
     paddedCfbDecrypt
         :: cipher
         -> B.ByteString
         -> B.ByteString
-        -> Either String B.ByteString
+        -> Either CipherError B.ByteString
     paddedCfbDecrypt = cfbDecrypt
+    aeadInit
+        :: CCT.AEADMode
+        -> cipher
+        -> B.ByteString
+        -> Either CipherError (CCT.AEAD cipher)
+    aeadInit _ _ _ = Left CipherAEADInitUnsupported
+    aeadSimpleEncrypt
+        :: (ByteArray pt, ByteArrayAccess aad)
+        => CCT.AEAD cipher
+        -> aad
+        -> pt
+        -> Int
+        -> (CCT.AuthTag, pt)
+    aeadSimpleEncrypt _ _ _ _ = error "aeadSimpleEncrypt not supported"
+    aeadSimpleDecrypt
+        :: (ByteArray ct, ByteArrayAccess aad)
+        => CCT.AEAD cipher
+        -> aad
+        -> ct
+        -> CCT.AuthTag
+        -> Maybe ct
+    aeadSimpleDecrypt _ _ _ _ = Nothing
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
@@ -27,9 +27,13 @@
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
 import Codec.Encryption.OpenPGP.Internal (xorBS)
+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( renderCipherError
+    )
 
 encryptWithOCBRFC7253
-    :: CCT.BlockCipher c
+    :: HOBlockCipher c
     => c
     -> B.ByteString
     -> B.ByteString
@@ -40,7 +44,11 @@
         error "invalid nonce size for OCB"
     offset0 <- ocbOffset0 cipher nonce
     let zeroBlock = B.replicate 16 0
-        lStar = CCT.ecbEncrypt cipher zeroBlock
+        lStar =
+            either
+                (error . renderCipherError)
+                id
+                (ecbEncrypt cipher zeroBlock)
         lDollar = ocbDouble lStar
         lCache = iterate ocbDouble (ocbDouble lDollar)
         hashAd = ocbHash cipher lStar lCache ad
@@ -49,7 +57,14 @@
             foldl'
                 ( \(accBlocks, offsetPrev, checksumPrev) (idx, pBlock) ->
                     let offsetI = xorBS offsetPrev (lCache !! ntz idx)
-                        cipherI = xorBS offsetI (CCT.ecbEncrypt cipher (xorBS offsetI pBlock))
+                        cipherI =
+                            xorBS
+                                offsetI
+                                ( either
+                                    (error . renderCipherError)
+                                    id
+                                    (ecbEncrypt cipher (xorBS offsetI pBlock))
+                                )
                         checksumI = xorBS checksumPrev pBlock
                      in (accBlocks ++ [cipherI], offsetI, checksumI)
                 )
@@ -60,22 +75,27 @@
                 then (B.empty, offsetM, checksum)
                 else
                     let offsetStar = xorBS offsetM lStar
-                        pad = CCT.ecbEncrypt cipher offsetStar
+                        pad =
+                            either
+                                (error . renderCipherError)
+                                id
+                                (ecbEncrypt cipher offsetStar)
                         cipherPartial = xorBS partial (B.take (B.length partial) pad)
                         checksum' = xorBS checksum (ocbPadPartial partial)
                      in (cipherPartial, offsetStar, checksum')
         tagBytes =
             xorBS
-                ( CCT.ecbEncrypt
-                    cipher
-                    (xorBS (xorBS checksumLast offsetLast) lDollar)
+                ( either
+                    (error . renderCipherError)
+                    id
+                    (ecbEncrypt cipher (xorBS (xorBS checksumLast offsetLast) lDollar))
                 )
                 hashAd
         ciphertext = B.concat cipherBlocks <> cipherLast
      in Right (mkAuthTag (B.take 16 tagBytes), ciphertext)
 
 decryptWithOCBRFC7253
-    :: CCT.BlockCipher c
+    :: HOBlockCipher c
     => c
     -> B.ByteString
     -> B.ByteString
@@ -87,7 +107,7 @@
         (\_ _ _ _ _ _ -> "OCB authentication failed")
 
 decryptWithOCBRFC7253With
-    :: CCT.BlockCipher c
+    :: HOBlockCipher c
     => ( B.ByteString
          -> B.ByteString
          -> B.ByteString
@@ -109,7 +129,11 @@
         error "invalid auth tag size for OCB"
     offset0 <- ocbOffset0 cipher nonce
     let zeroBlock = B.replicate 16 0
-        lStar = CCT.ecbEncrypt cipher zeroBlock
+        lStar =
+            either
+                (error . renderCipherError)
+                id
+                (ecbEncrypt cipher zeroBlock)
         lDollar = ocbDouble lStar
         lCache = iterate ocbDouble (ocbDouble lDollar)
         hashAd = ocbHash cipher lStar lCache ad
@@ -118,7 +142,14 @@
             foldl'
                 ( \(accBlocks, offsetPrev, checksumPrev) (idx, cBlock) ->
                     let offsetI = xorBS offsetPrev (lCache !! ntz idx)
-                        plainI = xorBS offsetI (CCT.ecbDecrypt cipher (xorBS offsetI cBlock))
+                        plainI =
+                            xorBS
+                                offsetI
+                                ( either
+                                    (error . renderCipherError)
+                                    id
+                                    (ecbDecrypt cipher (xorBS offsetI cBlock))
+                                )
                         checksumI = xorBS checksumPrev plainI
                      in (accBlocks ++ [plainI], offsetI, checksumI)
                 )
@@ -129,15 +160,20 @@
                 then (B.empty, offsetM, checksum)
                 else
                     let offsetStar = xorBS offsetM lStar
-                        pad = CCT.ecbEncrypt cipher offsetStar
+                        pad =
+                            either
+                                (error . renderCipherError)
+                                id
+                                (ecbEncrypt cipher offsetStar)
                         plainPartial = xorBS partial (B.take (B.length partial) pad)
                         checksum' = xorBS checksum (ocbPadPartial plainPartial)
                      in (plainPartial, offsetStar, checksum')
         tagComputed =
             xorBS
-                ( CCT.ecbEncrypt
-                    cipher
-                    (xorBS (xorBS checksumLast offsetLast) lDollar)
+                ( either
+                    (error . renderCipherError)
+                    id
+                    (ecbEncrypt cipher (xorBS (xorBS checksumLast offsetLast) lDollar))
                 )
                 hashAd
         plaintext = B.concat plainBlocks <> plainLast
@@ -154,7 +190,7 @@
 mkAuthTag = CCT.AuthTag . BA.convert
 
 ocbOffset0
-    :: CCT.BlockCipher c
+    :: HOBlockCipher c
     => c -> B.ByteString -> Either e B.ByteString
 ocbOffset0 cipher nonce = do
     let nonceLen = B.length nonce
@@ -165,12 +201,16 @@
         nonceBlock = prefix <> nonce
         bottom = fromIntegral (B.last nonceBlock .&. 0x3f) :: Int
         nonceTop = B.init nonceBlock <> B.singleton (B.last nonceBlock .&. 0xc0)
-        kTop = CCT.ecbEncrypt cipher nonceTop
+        kTop =
+            either
+                (error . renderCipherError)
+                id
+                (ecbEncrypt cipher nonceTop)
         stretch = kTop <> xorBS (B.take 8 kTop) (B.take 8 (B.drop 1 kTop))
     Right (ocbBitSlice128 stretch bottom)
 
 ocbHash
-    :: CCT.BlockCipher c
+    :: HOBlockCipher c
     => c
     -> B.ByteString
     -> [B.ByteString]
@@ -182,7 +222,14 @@
             foldl'
                 ( \(acc, offsetPrev) (idx, block) ->
                     let offsetI = xorBS offsetPrev (lCache !! ntz idx)
-                        sumI = xorBS acc (CCT.ecbEncrypt cipher (xorBS offsetI block))
+                        sumI =
+                            xorBS
+                                acc
+                                ( either
+                                    (error . renderCipherError)
+                                    id
+                                    (ecbEncrypt cipher (xorBS offsetI block))
+                                )
                      in (sumI, offsetI)
                 )
                 (B.replicate 16 0, B.replicate 16 0)
@@ -192,7 +239,13 @@
             else
                 let offsetStar = xorBS offsetFinal lStar
                     block = ocbPadPartial partial
-                 in xorBS sumBlocks (CCT.ecbEncrypt cipher (xorBS offsetStar block))
+                 in xorBS
+                        sumBlocks
+                        ( either
+                            (error . renderCipherError)
+                            id
+                            (ecbEncrypt cipher (xorBS offsetStar block))
+                        )
 
 ocbPadPartial :: B.ByteString -> B.ByteString
 ocbPadPartial bs = bs <> B.singleton 0x80 <> B.replicate (15 - B.length bs) 0
diff --git a/Codec/Encryption/OpenPGP/KeySelection.hs b/Codec/Encryption/OpenPGP/KeySelection.hs
--- a/Codec/Encryption/OpenPGP/KeySelection.hs
+++ b/Codec/Encryption/OpenPGP/KeySelection.hs
@@ -20,33 +20,31 @@
     , parseOnly
     , satisfy
     )
+import Data.Bifunctor (bimap)
 import qualified Data.ByteString as B
-import qualified Data.ByteString.Lazy as BL
 import Data.Text (Text, toUpper)
 import qualified Data.Text as T
 
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Errors
-    ( KeySelectionError (..)
-    )
 
 parseEightOctetKeyId
     :: Text -> Either KeySelectionError EightOctetKeyId
 parseEightOctetKeyId input =
-    case parseOnly hexes
-        =<< parseOnly (hexPrefix *> hexen 16) (toUpper input) of
-        Left _ -> Left (KeySelectionParseError (toUpper input))
-        Right bs -> Right (EightOctetKeyId bs)
+    let upper = toUpper input
+     in bimap
+            (const (KeySelectionParseError upper))
+            EightOctetKeyId
+            (parseOnly (hexPrefix *> hexen 16) upper >>= parseOnly hexes)
 
 parseFingerprint :: Text -> Either KeySelectionError Fingerprint
 parseFingerprint input =
-    case parseOnly hexes
-        =<< parseOnly
-            (hexen 64 <|> hexen 40 <|> hexen 32)
-            (toUpper (T.filter (/= ' ') input)) of
-        Left _ ->
-            Left (KeySelectionParseError (toUpper (T.filter (/= ' ') input)))
-        Right bs -> Right (Fingerprint bs)
+    let filtered = toUpper (T.filter (/= ' ') input)
+     in bimap
+            (const (KeySelectionParseError filtered))
+            Fingerprint
+            ( parseOnly (hexen 64 <|> hexen 40 <|> hexen 32) filtered
+                >>= parseOnly hexes
+            )
 
 hexPrefix :: Parser (Maybe Text)
 hexPrefix = optional (asciiCI "0x")
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
@@ -19,6 +19,7 @@
     , publicTKWithWireRep
     , secretTK
     , secretTKWithWireRep
+    , mixedTKWithWireRep
     , brokenTK
     , brokenTKWithWireRep
     , pkPayload
@@ -47,7 +48,9 @@
     , parseTKs
     , parsePublicTKs
     , parseSecretTKs
+    , parseMixedTKs
     , parseTKsWithWireRep
+    , parseMixedTKsWithWireRep
     ) where
 
 import Control.Applicative (many, (<|>))
@@ -149,10 +152,8 @@
         finalizeParsingEither
         (inspect (feedEof p))
 
-anyTK :: Bool -> Parser [Pkt] (Maybe TKUnknown)
-anyTK True = publicTK True <|> secretTK True
-anyTK False =
-    publicTK False <|> secretTK False <|> brokenTK 6 <|> brokenTK 5
+anyTK :: Bool -> Parser [Pkt] (Maybe SomeTK)
+anyTK intolerant = publicTK intolerant <|> secretTK intolerant
 
 data UidOrUat
     = I Text
@@ -177,9 +178,9 @@
     unA (A x, y) = (x, y)
     unA x = error $ "unA should never be called on " ++ show x
 
-publicTK, secretTK :: Bool -> Parser [Pkt] (Maybe TKUnknown)
+publicTK :: Bool -> Parser [Pkt] (Maybe SomeTK)
 publicTK intolerant = do
-    pkp <- pkPayload
+    (pkp, _) <- pkPayload
     pkpsigs <-
         concatMany
             (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)
@@ -187,31 +188,58 @@
         fmap
             splitUs
             (many (signedUID intolerant <|> signedUAt intolerant))
-    subs <- concatMany (pubsub intolerant)
+    rawSubs <- concatMany (pubsub intolerant)
     let revs = filter ((== Just KeyRevocationSig) . sigType) pkpsigs
         directKeySigs = filter ((== Just DirectKeySignature) . sigType) pkpsigs
-    return $ Just (TKUnknown pkp revs directKeySigs uids uats subs)
+        subs = mapMaybe subToPublic rawSubs
+        typed =
+            TK
+                { _tkPrimaryKey = KeyPktPublicPrimary pkp
+                , _tkRevs = revs
+                , _tkDirectKeySigs = directKeySigs
+                , _tkUIDs = uids
+                , _tkUAts = uats
+                , _tkSubs = subs
+                }
+    return (Just (SomePublicTK typed))
   where
     pubsub True = signedOrRevokedPubSubkey True
     pubsub False = signedOrRevokedPubSubkey False <|> brokenPubSubkey
+    subToPublic (pkt, sigs) = fmap (\kp -> (kp, sigs)) (pktToPublicKeyPkt pkt)
+
+secretTK :: Bool -> Parser [Pkt] (Maybe SomeTK)
 secretTK intolerant = do
-    skp <- skPayload
-    skpsigs <-
-        concatMany
-            (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)
-    (uids, uats) <-
-        fmap
-            splitUs
-            (many (signedUID intolerant <|> signedUAt intolerant))
-    subs <- concatMany (secsub intolerant)
-    let revs = filter ((== Just KeyRevocationSig) . sigType) skpsigs
-        directKeySigs = filter ((== Just DirectKeySignature) . sigType) skpsigs
-    return $ Just (TKUnknown skp revs directKeySigs uids uats subs)
-  where
-    secsub True = rawOrSignedOrRevokedSecSubkey True
-    secsub False = rawOrSignedOrRevokedSecSubkey False <|> brokenSecSubkey
+    (pkp, maybeSka) <- skPayload
+    case maybeSka of
+        Just ska -> do
+            skpsigs <-
+                concatMany
+                    (signatureWithPredicate intolerant isAllowedPrimaryKeySigType)
+            (uids, uats) <-
+                fmap
+                    splitUs
+                    (many (signedUID intolerant <|> signedUAt intolerant))
+            rawSubs <- concatMany (secsub intolerant)
+            let revs = filter ((== Just KeyRevocationSig) . sigType) skpsigs
+                directKeySigs = filter ((== Just DirectKeySignature) . sigType) skpsigs
+                subs = mapMaybe subToSecret rawSubs
+                typed =
+                    TK
+                        { _tkPrimaryKey = KeyPktSecretPrimary pkp ska
+                        , _tkRevs = revs
+                        , _tkDirectKeySigs = directKeySigs
+                        , _tkUIDs = uids
+                        , _tkUAts = uats
+                        , _tkSubs = subs
+                        }
+            return (Just (SomeSecretTK typed))
+          where
+            secsub True = rawOrSignedOrRevokedSecSubkey True
+            secsub False = rawOrSignedOrRevokedSecSubkey False <|> brokenSecSubkey
+            subToSecret (pkt, sigs) = fmap (\kp -> (kp, sigs)) (pktToSecretKeyPkt pkt)
+        Nothing -> fail "secret primary key missing secret addendum"
 
-brokenTK :: Int -> Parser [Pkt] (Maybe TKUnknown)
+brokenTK :: Int -> Parser [Pkt] (Maybe SomeTK)
 brokenTK 6 = do
     _ <- broken 6
     _ <-
@@ -391,13 +419,11 @@
 parseTKsEither
     :: Bool -> [Pkt] -> [Either TKConversionError SomeTK]
 parseTKsEither intolerant ps =
-    map
-        fromUnknownToTKEither
-        ( catMaybes $
+    map Right $
+        catMaybes $
             runIncrementalParser
                 (anyTK intolerant)
                 (map (: []) (filter notTrustPacket ps))
-        )
   where
     notTrustPacket = not . isTrustPkt
 
@@ -412,6 +438,10 @@
 parseSecretTKs intolerant packets =
     mapMaybe someTKToSecretTK (parseTKs intolerant packets)
 
+parseMixedTKs :: Bool -> [Pkt] -> [TK 'MixedTK]
+parseMixedTKs intolerant packets =
+    mapMaybe someTKToMixedTK (parseTKs intolerant packets)
+
 anyTKWithWireRep
     :: Bool -> Parser [PktWithWireRep] (Maybe TKWithWireRep)
 anyTKWithWireRep True = publicTKWithWireRep True <|> secretTKWithWireRep True
@@ -425,7 +455,7 @@
     , secretTKWithWireRep
         :: Bool -> Parser [PktWithWireRep] (Maybe TKWithWireRep)
 publicTKWithWireRep intolerant = do
-    (pkp, pkps) <- pkPayloadWithWireRep
+    ((pkp, _), pkps) <- pkPayloadWithWireRep
     (pkpsigs, pkpsigrefs) <-
         concatMany
             ( signatureWithWireRepPredicate
@@ -445,7 +475,15 @@
         subrefs = concatMap snd subResults
         revs = filter ((== Just KeyRevocationSig) . sigType) pkpsigs
         directKeySigs = filter ((== Just DirectKeySignature) . sigType) pkpsigs
-        tk = TKUnknown pkp revs directKeySigs uids uats subs
+        tk =
+            TK
+                { _tkPrimaryKey = SomeKeyPkt (KeyPktPublicPrimary pkp)
+                , _tkRevs = revs
+                , _tkDirectKeySigs = directKeySigs
+                , _tkUIDs = uids
+                , _tkUAts = uats
+                , _tkSubs = subs
+                }
         refs = pkps ++ pkpsigrefs ++ uidrefs ++ subrefs
     return $ Just (mkTKWithWireRep tk refs)
   where
@@ -454,34 +492,45 @@
         signedOrRevokedPubSubkeyWithWireRep False
             <|> brokenPubSubkeyWithWireRep
 secretTKWithWireRep intolerant = do
-    (skp, skps) <- skPayloadWithWireRep
-    (skpsigs, skpsigrefs) <-
-        concatMany
-            ( signatureWithWireRepPredicate
-                intolerant
-                isAllowedPrimaryKeySigType
-            )
-    uidResults <-
-        many
-            ( signedUIDWithWireRep intolerant
-                <|> signedUAtWithWireRep intolerant
-            )
-    subResults <- concatMany (secsub intolerant)
-    let semanticUs = fmap fst uidResults
-        (uids, uats) = splitUs semanticUs
-        uidrefs = concatMap snd uidResults
-        subs = fmap fst subResults
-        subrefs = concatMap snd subResults
-        revs = filter ((== Just KeyRevocationSig) . sigType) skpsigs
-        directKeySigs = filter ((== Just DirectKeySignature) . sigType) skpsigs
-        tk = TKUnknown skp revs directKeySigs uids uats subs
-        refs = skps ++ skpsigrefs ++ uidrefs ++ subrefs
-    return $ Just (mkTKWithWireRep tk refs)
-  where
-    secsub True = rawOrSignedOrRevokedSecSubkeyWithWireRep True
-    secsub False =
-        rawOrSignedOrRevokedSecSubkeyWithWireRep False
-            <|> brokenSecSubkeyWithWireRep
+    ((pkp, maybeSka), skps) <- skPayloadWithWireRep
+    case maybeSka of
+        Just ska -> do
+            (skpsigs, skpsigrefs) <-
+                concatMany
+                    ( signatureWithWireRepPredicate
+                        intolerant
+                        isAllowedPrimaryKeySigType
+                    )
+            uidResults <-
+                many
+                    ( signedUIDWithWireRep intolerant
+                        <|> signedUAtWithWireRep intolerant
+                    )
+            subResults <- concatMany (secsub intolerant)
+            let semanticUs = fmap fst uidResults
+                (uids, uats) = splitUs semanticUs
+                uidrefs = concatMap snd uidResults
+                subs = fmap fst subResults
+                subrefs = concatMap snd subResults
+                revs = filter ((== Just KeyRevocationSig) . sigType) skpsigs
+                directKeySigs = filter ((== Just DirectKeySignature) . sigType) skpsigs
+                tk =
+                    TK
+                        { _tkPrimaryKey = SomeKeyPkt (KeyPktSecretPrimary pkp ska)
+                        , _tkRevs = revs
+                        , _tkDirectKeySigs = directKeySigs
+                        , _tkUIDs = uids
+                        , _tkUAts = uats
+                        , _tkSubs = subs
+                        }
+                refs = skps ++ skpsigrefs ++ uidrefs ++ subrefs
+            return $ Just (mkTKWithWireRep tk refs)
+          where
+            secsub True = rawOrSignedOrRevokedSecSubkeyWithWireRep True
+            secsub False =
+                rawOrSignedOrRevokedSecSubkeyWithWireRep False
+                    <|> brokenSecSubkeyWithWireRep
+        Nothing -> fail "secret primary key missing secret addendum"
 
 brokenTKWithWireRep
     :: Int -> Parser [PktWithWireRep] (Maybe TKWithWireRep)
@@ -630,7 +679,7 @@
     :: Bool
     -> Parser
         [PktWithWireRep]
-        [((Pkt, [SignaturePayload]), [PktWithWireRep])]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
 signedOrRevokedPubSubkeyWithWireRep intolerant = do
     pskpkts <- satisfy isPSKPWS
     case pskpkts of
@@ -638,9 +687,10 @@
             (sigs, sigrefs) <-
                 concatMany
                     (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)
+            let Just keyPkt = pktToPublicKeyPkt (pktWithSource ^. pktWireRep . pktValue)
             return
                 [
-                    ( (pktWithSource ^. pktWireRep . pktValue, sigs)
+                    ( (SomeKeyPkt keyPkt, sigs)
                     , pktWithSource : sigrefs
                     )
                 ]
@@ -655,7 +705,7 @@
 brokenPubSubkeyWithWireRep
     :: Parser
         [PktWithWireRep]
-        [((Pkt, [SignaturePayload]), [PktWithWireRep])]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
 brokenPubSubkeyWithWireRep = do
     _ <- brokenWithWireRep 14
     _ <-
@@ -667,7 +717,7 @@
     :: Bool
     -> Parser
         [PktWithWireRep]
-        [((Pkt, [SignaturePayload]), [PktWithWireRep])]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
 rawOrSignedOrRevokedSecSubkeyWithWireRep intolerant = do
     sskpkts <- satisfy isSSKPWS
     case sskpkts of
@@ -675,9 +725,10 @@
             (sigs, sigrefs) <-
                 concatMany
                     (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)
+            let Just keyPkt = pktToSecretKeyPkt (pktWithSource ^. pktWireRep . pktValue)
             return
                 [
-                    ( (pktWithSource ^. pktWireRep . pktValue, sigs)
+                    ( (SomeKeyPkt keyPkt, sigs)
                     , pktWithSource : sigrefs
                     )
                 ]
@@ -692,7 +743,7 @@
 brokenSecSubkeyWithWireRep
     :: Parser
         [PktWithWireRep]
-        [((Pkt, [SignaturePayload]), [PktWithWireRep])]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
 brokenSecSubkeyWithWireRep = do
     _ <- brokenWithWireRep 7
     _ <-
@@ -735,6 +786,116 @@
             _ -> False
     isBrokenWS _ = False
 
+mixedPrimaryWithWireRep
+    :: Parser [PktWithWireRep] (SomeKeyPkt, [PktWithWireRep])
+mixedPrimaryWithWireRep = do
+    pkpkts <- satisfy isMixedPrimaryWS
+    case pkpkts of
+        [pktWithSource] ->
+            case pktToSomeKeyPkt (pktWithSource ^. pktWireRep . pktValue) of
+                Just keyPkt
+                    | someKeyPktRole keyPkt == KeyPktPrimary ->
+                        return (keyPkt, [pktWithSource])
+                _ -> failure
+        _ -> failure
+  where
+    isMixedPrimaryWS [pktWithSource] =
+        case pktToSomeKeyPkt (pktWithSource ^. pktWireRep . pktValue) of
+            Just keyPkt -> someKeyPktRole keyPkt == KeyPktPrimary
+            Nothing -> False
+    isMixedPrimaryWS _ = False
+
+mixedSubkeyWithWireRep
+    :: Bool
+    -> Parser
+        [PktWithWireRep]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
+mixedSubkeyWithWireRep intolerant = do
+    pskpkts <- satisfy isMixedSubkeyWS
+    case pskpkts of
+        [pktWithSource] -> do
+            (sigs, sigrefs) <-
+                concatMany
+                    (signatureWithWireRepPredicate intolerant isAllowedSubkeySigType)
+            let Just keyPkt = pktToSomeKeyPkt (pktWithSource ^. pktWireRep . pktValue)
+            return
+                [
+                    ( (keyPkt, sigs)
+                    , pktWithSource : sigrefs
+                    )
+                ]
+        _ -> failure
+  where
+    isMixedSubkeyWS [pktWithSource] =
+        case pktToSomeKeyPkt (pktWithSource ^. pktWireRep . pktValue) of
+            Just keyPkt -> someKeyPktRole keyPkt == KeyPktSubkey
+            _ -> False
+    isMixedSubkeyWS _ = False
+
+mixedBrokenPubSubkeyWithWireRep
+    :: Parser
+        [PktWithWireRep]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
+mixedBrokenPubSubkeyWithWireRep = do
+    _ <- brokenWithWireRep 14
+    _ <-
+        concatMany
+            (signatureWithWireRepPredicate False isAllowedSubkeySigType)
+    return []
+
+mixedBrokenSecSubkeyWithWireRep
+    :: Parser
+        [PktWithWireRep]
+        [((SomeKeyPkt, [SignaturePayload]), [PktWithWireRep])]
+mixedBrokenSecSubkeyWithWireRep = do
+    _ <- brokenWithWireRep 7
+    _ <-
+        concatMany
+            (signatureWithWireRepPredicate False isAllowedSubkeySigType)
+    return []
+
+mixedTKWithWireRep
+    :: Bool
+    -> Parser [PktWithWireRep] (Maybe (TK 'MixedTK, [PktWithWireRep]))
+mixedTKWithWireRep intolerant = do
+    (pkp, pkps) <- mixedPrimaryWithWireRep
+    (pkpsigs, pkpsigrefs) <-
+        concatMany
+            ( signatureWithWireRepPredicate
+                intolerant
+                isAllowedPrimaryKeySigType
+            )
+    uidResults <-
+        many
+            ( signedUIDWithWireRep intolerant
+                <|> signedUAtWithWireRep intolerant
+            )
+    subResults <- concatMany (mixedSub intolerant)
+    let semanticUs = fmap fst uidResults
+        (uids, uats) = splitUs semanticUs
+        uidrefs = concatMap snd uidResults
+        subs = fmap fst subResults
+        subrefs = concatMap snd subResults
+        revs = filter ((== Just KeyRevocationSig) . sigType) pkpsigs
+        directKeySigs = filter ((== Just DirectKeySignature) . sigType) pkpsigs
+        tk =
+            TK
+                { _tkPrimaryKey = pkp
+                , _tkRevs = revs
+                , _tkDirectKeySigs = directKeySigs
+                , _tkUIDs = uids
+                , _tkUAts = uats
+                , _tkSubs = subs
+                }
+        refs = pkps ++ pkpsigrefs ++ uidrefs ++ subrefs
+    return $ Just (tk, refs)
+  where
+    mixedSub True = mixedSubkeyWithWireRep True
+    mixedSub False =
+        mixedSubkeyWithWireRep False
+            <|> mixedBrokenPubSubkeyWithWireRep
+            <|> mixedBrokenSecSubkeyWithWireRep
+
 parseTKsWithWireRep
     :: Bool -> [PktWithWireRep] -> [TKWithWireRep]
 parseTKsWithWireRep intolerant ps =
@@ -745,6 +906,17 @@
   where
     notTrustPacketWithWireRep = not . isTrustPkt . (^. pktWireRep . pktValue)
 
+parseMixedTKsWithWireRep
+    :: Bool -> [PktWithWireRep] -> [TK 'MixedTK]
+parseMixedTKsWithWireRep intolerant ps =
+    map fst $
+        catMaybes $
+            runIncrementalParser
+                (mixedTKWithWireRep intolerant)
+                (map (: []) (filter notTrustPacketWithWireRep ps))
+  where
+    notTrustPacketWithWireRep = not . isTrustPkt . (^. pktWireRep . pktValue)
+
 runIncrementalParser
     :: (Monoid s, Show s)
     => Parser s r
@@ -757,7 +929,8 @@
         let (st', out) = parseAChunk parser chunk st
          in out <> go st' rest
 
-mkTKWithWireRep :: TKUnknown -> [PktWithWireRep] -> TKWithWireRep
+mkTKWithWireRep
+    :: TK 'MixedTK -> [PktWithWireRep] -> TKWithWireRep
 mkTKWithWireRep tk refs =
     case refs of
         [] ->
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
@@ -38,6 +38,7 @@
     , RecoveredSessionMaterial (..)
     , encryptMessage
     , decryptMessage
+    , extractLiteralPayload
     , signMessage
     , signMessageWith
     , verifySignedMessage
@@ -45,7 +46,11 @@
 
 import Control.Monad (foldM)
 import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
+import Control.Monad.Trans.Except
+    ( ExceptT (..)
+    , except
+    , runExceptT
+    )
 import qualified Crypto.PubKey.Ed25519 as Ed25519
 import qualified Crypto.PubKey.Ed448 as Ed448
 import qualified Crypto.PubKey.RSA.Types as RSATypes
@@ -220,7 +225,7 @@
 
 messageStep
     :: Monad m => Either MessageError a -> ExceptT MessageError m a
-messageStep = ExceptT . pure
+messageStep = except
 
 runMessageFlow
     :: ExceptT MessageError Identity a -> Either MessageError a
@@ -232,7 +237,7 @@
 runMessageFlowT = runExceptT
 
 signStepT :: Monad m => Either SignError a -> MessageFlowT m a
-signStepT = ExceptT . pure . first MessageSignError
+signStepT = except . first MessageSignError
 
 parseStep
     :: Monad m
@@ -972,10 +977,18 @@
 
 extractLiteralPayload
     :: [Pkt] -> Either MessageParseFailure ClearPayload
-extractLiteralPayload pkts =
+extractLiteralPayload pkts = do
+    validatePackets pkts
     case [p | LiteralDataPkt _ _ _ p <- pkts] of
         payload : _ -> Right (ClearPayload payload)
         [] -> Left MissingLiteralDataPacket
+  where
+    validatePackets [] = Right ()
+    validatePackets (pkt : rest) =
+        case pkt of
+            LiteralDataPkt {} -> validatePackets rest
+            CompressedDataPkt {} -> validatePackets rest
+            _ -> Left (UnexpectedPacketInDecryptedPayload (pktTag pkt))
 
 rejectUnknownCriticalPacketsTyped
     :: [Pkt] -> Either MessageParseFailure [Pkt]
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
@@ -79,6 +79,9 @@
     )
 import Codec.Encryption.OpenPGP.SignatureQualities (sigType)
 import Codec.Encryption.OpenPGP.Types
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    )
 
 data OpenPGPRFC
     = RFC2440
@@ -560,15 +563,17 @@
 secretKeyProtectionPolicyForKeyVersion _ _ = Nothing
 
 ecdhKdfHashDigest
-    :: HashAlgorithm -> B.ByteString -> Either String B.ByteString
-ecdhKdfHashDigest SHA1 _ = Left "ECDH KDF hash algorithm SHA1 is disallowed by policy"
+    :: HashAlgorithm -> B.ByteString -> Either CipherError B.ByteString
+ecdhKdfHashDigest SHA1 _ =
+    Left (CipherKdfHashAlgorithmDisallowed SHA1)
 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"
+ecdhKdfHashDigest ha _ =
+    Left (CipherKdfHashAlgorithmUnsupported ha)
 
 validateTable30PolicyForRecipient
     :: SomePKPayload
diff --git a/Codec/Encryption/OpenPGP/S2K.hs b/Codec/Encryption/OpenPGP/S2K.hs
--- a/Codec/Encryption/OpenPGP/S2K.hs
+++ b/Codec/Encryption/OpenPGP/S2K.hs
@@ -143,15 +143,10 @@
 encodedSessionKeyKeyLength
     :: SymmetricAlgorithm -> Either EncodedSessionKeyError Int
 encodedSessionKeyKeyLength symalgo =
-    first renderKeySizeError (keySize symalgo)
-  where
-    renderKeySizeError :: CipherError -> EncodedSessionKeyError
-    renderKeySizeError (UnsupportedAlgorithm sa) =
-        EncodedSessionKeyUnsupportedAlgorithm sa
-    renderKeySizeError (CipherInitFailed sa _) =
-        EncodedSessionKeyUnsupportedAlgorithm sa
-    renderKeySizeError (CipherOperationFailed _) =
-        EncodedSessionKeyUnsupportedAlgorithm symalgo
+    first
+        ( \(CipherUnsupportedAlgorithm sa) -> EncodedSessionKeyUnsupportedAlgorithm sa
+        )
+        (keySize symalgo)
 
 checksum16 :: B.ByteString -> Word16
 checksum16 =
diff --git a/Codec/Encryption/OpenPGP/SEIPDv2.hs b/Codec/Encryption/OpenPGP/SEIPDv2.hs
--- a/Codec/Encryption/OpenPGP/SEIPDv2.hs
+++ b/Codec/Encryption/OpenPGP/SEIPDv2.hs
@@ -17,7 +17,6 @@
     ) 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)
@@ -27,9 +26,12 @@
 import qualified Data.Set as Set
 import qualified "crypton" Crypto.Cipher.Types as CCT
 
-import Codec.Encryption.OpenPGP.Internal.CryptoAES
-    ( withAESCipher
+import Codec.Encryption.OpenPGP.BlockCipher
+    ( withAEADCipher
     )
+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
+    ( HOBlockCipher (..)
+    )
 import Codec.Encryption.OpenPGP.Internal.RFC7253OCB
     ( decryptWithOCBRFC7253With
     , encryptWithOCBRFC7253
@@ -106,35 +108,32 @@
     if B.length iv /= nonceSize
         then Left SEIPDv2InvalidIVLength
         else
-            withAESCipher
-                (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-                ( SEIPDv2CipherFailed
-                    (CipherInitFailed symalgo "unsupported symmetric algorithm")
-                )
-                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 (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-                                    . CE.eitherCryptoError
-                                    $ CCT.aeadInit mode cipher iv
-                            let (tag, ciphertext) =
-                                    CCT.aeadSimpleEncrypt
-                                        aeadCtx
+            first
+                SEIPDv2CipherFailed
+                ( withAEADCipher
+                    symalgo
+                    kek
+                    ( \cipher ->
+                        if mode == CCT.AEAD_OCB
+                            then do
+                                (tag, ct) <-
+                                    encryptWithOCBRFC7253
+                                        cipher
+                                        iv
                                         (skeskV6Info symalgo aead)
                                         sessionKey
-                                        16
-                            pure (ciphertext, authTagToBS tag)
+                                pure (ct, authTagToBS tag)
+                            else do
+                                aeadCtx <-
+                                    aeadInit mode cipher iv
+                                let (tag, ciphertext) =
+                                        aeadSimpleEncrypt
+                                            aeadCtx
+                                            (skeskV6Info symalgo aead)
+                                            sessionKey
+                                            16
+                                pure (ciphertext, authTagToBS tag)
+                    )
                 )
 
 decryptSKESK6SessionKey
@@ -150,36 +149,34 @@
     if B.length iv /= nonceSize
         then Left SEIPDv2InvalidIVLength
         else
-            withAESCipher
-                (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-                ( SEIPDv2CipherFailed
-                    (CipherInitFailed symalgo "unsupported symmetric algorithm")
-                )
-                symalgo
-                kek
-                ( \cipher ->
-                    if mode == CCT.AEAD_OCB
-                        then
-                            decryptWithOCBRFC7253With
-                                (\_ _ _ _ _ _ -> SEIPDv2AuthFailed)
-                                cipher
-                                iv
-                                (skeskV6Info symalgo aead)
-                                ciphertext
-                                (mkAuthTag tag)
-                        else do
-                            aeadCtx <-
-                                first (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-                                    . CE.eitherCryptoError
-                                    $ CCT.aeadInit mode cipher iv
-                            note
-                                SEIPDv2AuthFailed
-                                ( CCT.aeadSimpleDecrypt
-                                    aeadCtx
+            first
+                SEIPDv2CipherFailed
+                ( withAEADCipher
+                    symalgo
+                    kek
+                    ( \cipher ->
+                        if mode == CCT.AEAD_OCB
+                            then
+                                decryptWithOCBRFC7253With
+                                    (\_ _ _ _ _ _ -> CipherAEADAuthFailed)
+                                    cipher
+                                    iv
                                     (skeskV6Info symalgo aead)
                                     ciphertext
                                     (mkAuthTag tag)
-                                )
+                            else do
+                                aeadCtx <-
+                                    aeadInit mode cipher iv
+                                let decrypted =
+                                        aeadSimpleDecrypt
+                                            aeadCtx
+                                            (skeskV6Info symalgo aead)
+                                            ciphertext
+                                            (mkAuthTag tag)
+                                note
+                                    CipherAEADDecryptFailed
+                                    decrypted
+                    )
                 )
 
 authTagToBS :: CCT.AuthTag -> B.ByteString
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
@@ -18,7 +18,6 @@
     , reencryptSecretKeyRandom
     ) where
 
-import Control.Error.Util (note)
 import Control.Monad (when)
 import Control.Monad.Trans.Class (lift)
 import Control.Monad.Trans.Except
@@ -26,7 +25,6 @@
     , runExceptT
     , throwE
     )
-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)
@@ -60,14 +58,18 @@
 
 import Codec.Encryption.OpenPGP.BlockCipher
     ( keySize
+    , withAEADCipher
     )
 import Codec.Encryption.OpenPGP.CFB
     ( decryptNoNonce
     , encryptNoNonce
     )
-import Codec.Encryption.OpenPGP.Internal.CryptoAES
-    ( withAESCipher
+import Codec.Encryption.OpenPGP.Internal.Crypton
+    ( HOWrappedCCT (..)
     )
+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
+    ( HOBlockCipher (..)
+    )
 import Codec.Encryption.OpenPGP.Internal.RFC7253OCB
     ( decryptWithOCBRFC7253With
     , encryptWithOCBRFC7253
@@ -120,12 +122,12 @@
     -> Passphrase
     -> Either SecretKeyError (SKey, SKAddendum)
 decryptSecretKeyAddendum pkp ska pp =
-    case fromSKAddendumForPKPayload pkp ska of
-        Left err -> Left (SecretKeyDecryptAddendumError err)
-        Right (SomeSKAddendumV skaV) ->
-            case decryptPrivateKeyTyped pkp skaV pp of
-                Left err -> Left err
-                Right decryptedV ->
+    first
+        SecretKeyDecryptAddendumError
+        (fromSKAddendumForPKPayload pkp ska)
+        >>= \(SomeSKAddendumV skaV) ->
+            decryptPrivateKeyTyped pkp skaV pp
+                >>= \decryptedV ->
                     case toSKAddendum decryptedV of
                         SUSUnprotected skey _ -> Right (skey, toSKAddendum decryptedV)
                         _ -> Left SecretKeyDecryptNotUnencrypted
@@ -193,12 +195,13 @@
         let pkp = _secretKeyPKPayload sk
             originalSka = _secretKeySKAddendum sk
         decryptedSKA <-
-            except $ case fromSKAddendumForPKPayload pkp originalSka of
-                Left err -> Left (SecretKeyDecryptAddendumError err)
-                Right (SomeSKAddendumV skaV) ->
-                    case decryptPrivateKeyTyped pkp skaV oldPassphrase of
-                        Left err -> Left err
-                        Right decryptedV -> Right (toSKAddendum decryptedV)
+            except $
+                first
+                    SecretKeyDecryptAddendumError
+                    (fromSKAddendumForPKPayload pkp originalSka)
+                    >>= \(SomeSKAddendumV skaV) ->
+                        decryptPrivateKeyTyped pkp skaV oldPassphrase
+                            >>= pure . toSKAddendum
         case decryptedSKA of
             SUSUnprotected skey _ -> do
                 let pp = unPassphrase newPassphrase
@@ -571,19 +574,20 @@
             SecretKeyAEADModeUnsupportedCipher sa
     case aa of
         OCB ->
-            withAESCipher
-                SecretKeyAEADModeCrypto
-                unsupportedSecretKeyAEADError
-                sa
-                kek
-                ( \cipher ->
-                    decryptWithOCBRFC7253With
-                        authFailure
-                        cipher
-                        nonce
-                        ad
-                        ciphertext
-                        authTag
+            first
+                SecretKeyDecryptCipherError
+                ( withAEADCipher
+                    sa
+                    kek
+                    ( \cipher ->
+                        decryptWithOCBRFC7253With
+                            (\_ _ _ _ _ _ -> CipherAEADAuthFailed)
+                            cipher
+                            nonce
+                            ad
+                            ciphertext
+                            authTag
+                    )
                 )
         _ -> do
             mode <- aeadMode aa
@@ -593,21 +597,18 @@
                     ( SecretKeyInvalidNonceSize
                         "invalid nonce size for v6 AEAD secret key payload"
                     )
-            withAESCipher
-                SecretKeyAEADModeCrypto
-                unsupportedSecretKeyAEADError
-                sa
-                kek
-                $ \cipher ->
-                    first
-                        SecretKeyAEADModeCrypto
-                        (CE.eitherCryptoError (CCT.aeadInit mode cipher nonce))
-                        >>= \aead ->
-                            note
-                                ( SecretKeyAuthError
-                                    "failed to authenticate v6 AEAD secret key payload"
-                                )
-                                (CCT.aeadSimpleDecrypt aead ad ciphertext authTag)
+            first
+                SecretKeyDecryptCipherError
+                ( withAEADCipher
+                    sa
+                    kek
+                    $ \cipher -> do
+                        aeadCtx <- aeadInit mode cipher nonce
+                        maybe
+                            (Left CipherAEADDecryptFailed)
+                            Right
+                            (aeadSimpleDecrypt aeadCtx ad ciphertext authTag)
+                )
 
 aeadMode :: AEADAlgorithm -> Either SecretKeyError CCT.AEADMode
 aeadMode EAX = Right CCT.AEAD_EAX
@@ -839,25 +840,25 @@
             SecretKeyAEADModeUnsupportedCipher sa
     case aa of
         OCB ->
-            withAESCipher
-                SecretKeyAEADModeCrypto
-                unsupportedSecretKeyAEADError
-                sa
-                kek
-                (\cipher -> encryptWithOCBRFC7253 cipher nonce ad plaintext)
+            first
+                SecretKeyEncryptCipherError
+                ( withAEADCipher
+                    sa
+                    kek
+                    (\cipher -> encryptWithOCBRFC7253 cipher nonce ad plaintext)
+                )
         _ -> do
             mode <- aeadMode aa
-            withAESCipher
-                SecretKeyAEADModeCrypto
-                unsupportedSecretKeyAEADError
-                sa
-                kek
-                $ \cipher ->
-                    first
-                        SecretKeyAEADModeCrypto
-                        (CE.eitherCryptoError (CCT.aeadInit mode cipher nonce))
-                        >>= \aead ->
-                            pure (CCT.aeadSimpleEncrypt aead ad plaintext 16)
+            first
+                SecretKeyEncryptCipherError
+                ( withAEADCipher
+                    sa
+                    kek
+                    $ \cipher ->
+                        aeadInit mode cipher nonce
+                            >>= \aead ->
+                                pure (aeadSimpleEncrypt aead ad plaintext 16)
+                )
 
 reencryptWithPolicyAndSaltAndIVTyped
     :: OpenPGPPolicy
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
@@ -14,6 +14,7 @@
     , putSKAddendum
     , getSecretKey
     , putSKeyForPKPayload
+    , putMixedTK
 
       -- * Utilities
     , dearmorIfAsciiArmored
@@ -39,7 +40,7 @@
     )
 import Control.Applicative (many, some)
 import Control.Arrow ((***))
-import Control.Lens (op, (^.), _1)
+import Control.Lens (op)
 import Control.Monad (guard, replicateM, replicateM_, when)
 import Control.Monad.Loops (iterateUntilM)
 import Crypto.Number.Basic (numBits)
@@ -259,17 +260,30 @@
     get = getSignaturePayload
     put = putSignaturePayload
 
-instance Binary TKUnknown where
-    get = fail "Binary TKUnknown decode is not implemented"
-    put = putTK
+instance Binary (TK 'MixedTK) where
+    get = fail "Binary TK 'MixedTK decode is not implemented"
+    put = putMixedTK
 
-instance Binary (TK k) where
-    get = fail "Binary TK decode is not implemented"
-    put = putTK . tkToUnknown
+putMixedTK :: TK 'MixedTK -> Put
+putMixedTK tk = do
+    putPrimary (_tkPrimaryKey tk)
+    mapM_ (put . Signature) (_tkRevs tk)
+    mapM_ (put . Signature) (_tkDirectKeySigs tk)
+    mapM_ putUid' (_tkUIDs tk)
+    mapM_ putUat' (_tkUAts tk)
+    mapM_ putSub' (_tkSubs tk)
+  where
+    putPrimary :: SomeKeyPkt -> Put
+    putPrimary (SomeKeyPkt (KeyPktPublicPrimary pkp)) = put (PublicKey pkp)
+    putPrimary (SomeKeyPkt (KeyPktSecretPrimary pkp ska)) = put (SecretKey pkp ska)
+    putPrimary _ = error "putMixedTK: primary key must be primary role"
+    putUid' (u, sps) = put (UserId u) >> mapM_ (put . Signature) sps
+    putUat' (us, sps) = put (UserAttribute us) >> mapM_ (put . Signature) sps
+    putSub' (kp, sps) = putPkt (someKeyPktToPkt kp) >> mapM_ (put . Signature) sps
 
 instance Binary SomeTK where
     get = fail "Binary SomeTK decode is not implemented"
-    put = putTK . someTKToUnknown
+    put = putMixedTK . asMixedTK
 
 getSigSubPacket :: Get SigSubPacket
 getSigSubPacket = do
@@ -3600,23 +3614,6 @@
 putSignaturePayload (SigVOther pv bs) = do
     putWord8 pv
     putLazyByteString bs
-
-putTK :: TKUnknown -> Put
-putTK tk = do
-    let pkp = tk ^. tkuKey . _1
-    maybe
-        (put (PublicKey pkp))
-        (\ska -> put (SecretKey pkp ska))
-        (snd (tk ^. tkuKey))
-    mapM_ (put . Signature) (_tkuRevs tk)
-    mapM_ (put . Signature) (_tkuDirectKeySigs tk)
-    mapM_ putUid' (_tkuUIDs tk)
-    mapM_ putUat' (_tkuUAts tk)
-    mapM_ putSub' (_tkuSubs tk)
-  where
-    putUid' (u, sps) = put (UserId u) >> mapM_ (put . Signature) sps
-    putUat' (us, sps) = put (UserAttribute us) >> mapM_ (put . Signature) sps
-    putSub' (p, sps) = put p >> mapM_ (put . Signature) sps
 
 -- | Parse the packets from a ByteString, with no error reporting
 parsePkts :: ByteString -> [Pkt]
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
@@ -1,3 +1,4 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
 {-# LANGUAGE ConstraintKinds #-}
 -- Signatures.hs: OpenPGP (RFC9580) signature verification
 -- Copyright © 2012-2026  Clint Adams
@@ -8,6 +9,7 @@
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE UndecidableInstances #-}
@@ -499,7 +501,11 @@
     | BadLength !String !Int !Int
 
 verifyTKWith
-    :: ( Pkt
+    :: forall k
+     . ( TKKeyPktToPkt k
+       , TKPrimaryPKPayload k
+       )
+    => ( Pkt
          -> PktStreamContext
          -> Maybe UTCTime
          -> Either VerificationError Verification
@@ -574,20 +580,20 @@
                  in (uat, retainNonRevokedCertifications mt verified)
             )
     checkSub
-        :: (KeyPkt k, [SignaturePayload])
-        -> [(KeyPkt k, [SignaturePayload])]
+        :: (TKKeyPkt k, [SignaturePayload])
+        -> [(TKKeyPkt k, [SignaturePayload])]
     checkSub (pkt, sps) =
         if revokedSub pkt sps
             then []
             else checkSub' pkt sps
-    revokedSub :: KeyPkt k -> [SignaturePayload] -> Bool
+    revokedSub :: TKKeyPkt k -> [SignaturePayload] -> Bool
     revokedSub _ [] = False
     revokedSub p sigs =
         any (vSubSig p) (filter subkeyRevocationEffective sigs)
     checkSub'
-        :: KeyPkt k
+        :: TKKeyPkt k
         -> [SignaturePayload]
-        -> [(KeyPkt k, [SignaturePayload])]
+        -> [(TKKeyPkt k, [SignaturePayload])]
     checkSub' p sps =
         let goodsigs =
                 filter (vSubSig p)
@@ -608,7 +614,7 @@
                 | isV4OrV6Sig s
                 , sigType s == Just KeyRevocationSig
                 , Just pka <- sigPKA s ->
-                    if (v ^. verificationSigner == keyPktPKPayload (tk ^. tkPrimaryKey))
+                    if (v ^. verificationSigner == tkPrimaryPKPayload tk)
                         || any
                             ( \(p, f) ->
                                 p == pka && f == fingerprint (v ^. verificationSigner)
@@ -627,7 +633,7 @@
         vsf
             (SignaturePkt sp)
             emptyPSC
-                { lastPrimaryKey = keyPktToPkt (tk ^. tkPrimaryKey)
+                { lastPrimaryKey = tkKeyPktToPkt @k (_tkPrimaryKey tk)
                 , lastUIDorUAt = UserIdPkt uid
                 }
             Nothing
@@ -638,7 +644,7 @@
         vsf
             (SignaturePkt sp)
             emptyPSC
-                { lastPrimaryKey = keyPktToPkt (tk ^. tkPrimaryKey)
+                { lastPrimaryKey = tkKeyPktToPkt @k (_tkPrimaryKey tk)
                 , lastUIDorUAt = UserAttributePkt uat
                 }
             Nothing
@@ -646,16 +652,16 @@
     vSig sp =
         vsf
             (SignaturePkt sp)
-            emptyPSC {lastPrimaryKey = keyPktToPkt (tk ^. tkPrimaryKey)}
+            emptyPSC {lastPrimaryKey = tkKeyPktToPkt @k (_tkPrimaryKey tk)}
             Nothing
-    vSubSig :: KeyPkt k -> SignaturePayload -> Bool
+    vSubSig :: TKKeyPkt k -> SignaturePayload -> Bool
     vSubSig sk sp =
         isRight
             ( vsf
                 (SignaturePkt sp)
                 emptyPSC
-                    { lastPrimaryKey = keyPktToPkt (tk ^. tkPrimaryKey)
-                    , lastSubkey = keyPktToPkt sk
+                    { lastPrimaryKey = tkKeyPktToPkt @k (_tkPrimaryKey tk)
+                    , lastSubkey = tkKeyPktToPkt @k sk
                     }
                 mt
             )
@@ -1299,15 +1305,24 @@
     padN n bs = leftPadTo n bs
     cf2es = eitherCryptoError
     rsaVerify pub mpis hd pkey bs =
-        if P15.verify (Just hd) pkey bs (rsaMPItoSig pkey mpis)
-            then Right pub
-            else verificationError (SignatureMismatch RSA (fingerprint pub))
+        case rsaMPItoSig pkey mpis of
+            Nothing ->
+                verificationError (SignatureMismatch RSA (fingerprint pub))
+            Just sig ->
+                if P15.verify (Just hd) pkey bs sig
+                    then Right pub
+                    else
+                        verificationError
+                            (SignatureMismatch RSA (fingerprint pub))
     dsaMPIsToSig r s = DSA.Signature (unMPI r) (unMPI s)
     ecdsaMPIsToSig r s = ECDSA.Signature (unMPI r) (unMPI s)
     rsaMPItoSig pkey (s :| []) =
         let sz = RSATypes.public_size pkey
             raw = i2osp (unMPI s)
-         in leftPadTo sz raw
+         in if B.length raw > sz
+                then Nothing
+                else Just (leftPadTo sz raw)
+    rsaMPItoSig _ _ = Nothing
     crazyHash h = BA.convert . hashWith h
 
 isSignatureExpired
diff --git a/Codec/Encryption/OpenPGP/Signing.hs b/Codec/Encryption/OpenPGP/Signing.hs
--- a/Codec/Encryption/OpenPGP/Signing.hs
+++ b/Codec/Encryption/OpenPGP/Signing.hs
@@ -156,10 +156,13 @@
     primaryPkp = keyPktPKPayload primaryKp
     primarySka = secretKeyPktSKAddendum primaryKp
     primaryUsage =
-        foldr
-            Set.union
-            Set.empty
-            (map sigFlags (_tkDirectKeySigs tk ++ _tkRevs tk))
+        Set.insert
+            CertifyKeysKey
+            ( foldr
+                Set.union
+                Set.empty
+                (map sigFlags (_tkDirectKeySigs tk ++ _tkRevs tk))
+            )
     primary =
         AvailableSigner
             { asKeyId = either (error . show) id (eightOctetKeyID primaryPkp)
diff --git a/Codec/Encryption/OpenPGP/Types/Internal/Errors.hs b/Codec/Encryption/OpenPGP/Types/Internal/Errors.hs
--- a/Codec/Encryption/OpenPGP/Types/Internal/Errors.hs
+++ b/Codec/Encryption/OpenPGP/Types/Internal/Errors.hs
@@ -67,8 +67,6 @@
     , renderSerializeError
 
       -- * AEAD / SEIPDv2 unified auth failures
-    , AEADAuthFailure (..)
-    , renderAEADAuthFailure
     , SEIPDv2Failure (..)
     , renderSEIPDv2Failure
 
@@ -137,19 +135,87 @@
 -- | Errors that can arise from block-cipher operations in this library.
 data CipherError
     = -- | The algorithm is not supported or not implemented.
-      UnsupportedAlgorithm !SymmetricAlgorithm
+      CipherUnsupportedAlgorithm !SymmetricAlgorithm
     | -- | Cipher initialization failed (bad key material).
-      CipherInitFailed !SymmetricAlgorithm !String
-    | -- | A CFB or other block-cipher operation failed.
-      CipherOperationFailed !String
+      CipherInitFailed !SymmetricAlgorithm !CE.CryptoError
+    | -- | Cipher initialization failed for old nettle backend.
+      CipherOldInitFailed !String
+    | -- | A backend crypto operation failed.
+      CipherOperationFailed !CE.CryptoError
+    | -- | Backend does not support padding operations.
+      CipherPaddingUnsupported
+    | -- | Backend does not support the requested AEAD mode.
+      CipherAEADModeUnsupported
+    | -- | Invalid IV length or value.
+      CipherBadIV !String
+    | -- | Backend does not support AEAD initialization.
+      CipherAEADInitUnsupported
+    | -- | Session key quickcheck failed.
+      CipherSessionKeyQuickcheckFailed
+    | -- | AEAD authentication failed.
+      CipherAEADAuthFailed
+    | -- | AEAD decrypt returned Nothing.
+      CipherAEADDecryptFailed
+    | -- | Key wrap input is invalid.
+      CipherKeyWrapInvalidInput !String
+    | -- | KDF hash algorithm is disallowed by policy.
+      CipherKdfHashAlgorithmDisallowed !HashAlgorithm
+    | -- | KDF hash algorithm is unsupported.
+      CipherKdfHashAlgorithmUnsupported !HashAlgorithm
+    | -- | Curve conversion failed for ECDH KDF parameter.
+      CipherCurveConversionFailed !CurveConversionError
+    | -- | Recipient is not a valid ECDH key.
+      CipherInvalidECDHRecipient
+    | -- | Ciphertext is too short to contain a valid chunk.
+      CipherCiphertextTooShort
+    | -- | Malformed chunk lengths in SEIPDv2.
+      CipherMalformedChunkLengths
+    | -- | Missing final authentication tag.
+      CipherMissingFinalTag
+    | -- | Final SEIPDv2 tag must be empty.
+      CipherFinalTagEmpty
     deriving (Eq, Show)
 
 renderCipherError :: CipherError -> String
-renderCipherError (UnsupportedAlgorithm sa) =
+renderCipherError (CipherUnsupportedAlgorithm sa) =
     "unsupported symmetric algorithm: " ++ show sa
 renderCipherError (CipherInitFailed sa err) =
-    "could not initialize cipher for " ++ show sa ++ ": " ++ err
-renderCipherError (CipherOperationFailed err) = err
+    "could not initialize cipher for " ++ show sa ++ ": " ++ show err
+renderCipherError (CipherOldInitFailed err) =
+    "could not initialize old backend cipher: " ++ err
+renderCipherError (CipherOperationFailed err) = show err
+renderCipherError CipherPaddingUnsupported =
+    "padding not supported by this backend"
+renderCipherError CipherAEADModeUnsupported =
+    "AEAD mode not supported by this backend"
+renderCipherError (CipherBadIV msg) = msg
+renderCipherError CipherAEADInitUnsupported =
+    "aeadInit not supported for this cipher backend"
+renderCipherError CipherSessionKeyQuickcheckFailed =
+    "Session key quickcheck failed"
+renderCipherError CipherAEADAuthFailed =
+    "AEAD authentication failed"
+renderCipherError CipherAEADDecryptFailed =
+    "AEAD decrypt returned Nothing"
+renderCipherError (CipherKeyWrapInvalidInput err) = err
+renderCipherError (CipherKdfHashAlgorithmDisallowed ha) =
+    "ECDH KDF hash algorithm "
+        ++ show ha
+        ++ " is disallowed by policy"
+renderCipherError (CipherKdfHashAlgorithmUnsupported ha) =
+    "ECDH KDF hash algorithm" ++ show ha ++ "is unsupported"
+renderCipherError (CipherCurveConversionFailed err) =
+    renderCurveConversionError err
+renderCipherError CipherInvalidECDHRecipient =
+    "ECDH KDF param requires ECDH recipient key"
+renderCipherError CipherCiphertextTooShort =
+    "SEIPDv2 ciphertext too short"
+renderCipherError CipherMalformedChunkLengths =
+    "SEIPDv2 malformed chunk lengths"
+renderCipherError CipherMissingFinalTag =
+    "SEIPDv2 missing final tag"
+renderCipherError CipherFinalTagEmpty =
+    "expected empty ciphertext for final SEIPD v2 tag"
 
 -------------------------------------------------------------------------------
 
@@ -765,7 +831,7 @@
     | UnsupportedRecipientAlgorithm !PubKeyAlgorithm
     | InvalidRecipientKeyMaterial !PubKeyAlgorithm !String
     | InvalidRecipientKeyMaterialKeyId !PubKeyAlgorithm !KeyIdError
-    | RecipientKdfFailure !PubKeyAlgorithm !String
+    | RecipientKdfFailure !PubKeyAlgorithm !CipherError
     | RecipientKeyWrapFailure !PubKeyAlgorithm !String
     | RecipientKeyWrapFailureCipher !PubKeyAlgorithm !CipherError
     | RecipientKeyWrapFailureRSA !PubKeyAlgorithm !RSA.Error
@@ -806,11 +872,11 @@
         ++ show algo
         ++ ": failed to derive PKESKv3 recipient key ID: "
         ++ renderKeyIdError err
-renderPKESKEncryptError (RecipientKdfFailure algo reason) =
+renderPKESKEncryptError (RecipientKdfFailure algo err) =
     "KDF failure for recipient algorithm "
         ++ show algo
         ++ ": "
-        ++ reason
+        ++ renderCipherError err
 renderPKESKEncryptError (RecipientKeyWrapFailure algo reason) =
     "key wrap failure for recipient algorithm "
         ++ show algo
@@ -960,26 +1026,6 @@
 -- AEAD / SEIPDv2 unified auth failures
 -------------------------------------------------------------------------------
 
--- | Unified AEAD authentication failures shared between legacy AEAD and SEIPDv2.
-data AEADAuthFailure
-    = AEADChunkAuthFailed !AEADAlgorithm !Int
-    | AEADFinalTagFailed !AEADAlgorithm
-    | AEADInitFailed !CipherError
-    deriving (Eq, Show)
-
-renderAEADAuthFailure :: AEADAuthFailure -> String
-renderAEADAuthFailure (AEADChunkAuthFailed algo chunk) =
-    "AEAD chunk authentication failed for "
-        ++ show algo
-        ++ " at chunk "
-        ++ show chunk
-renderAEADAuthFailure (AEADFinalTagFailed algo) =
-    "AEAD final tag verification failed for " ++ show algo
-renderAEADAuthFailure (AEADInitFailed err) =
-    "AEAD initialization failed: " ++ renderCipherError err
-
--------------------------------------------------------------------------------
-
 data SEIPDv2Failure
     = SEIPDv2UnsupportedAEADAlgorithm !AEADAlgorithm
     | SEIPDv2UnsupportedSymmetricAlgorithm !SymmetricAlgorithm
@@ -989,8 +1035,6 @@
     | SEIPDv2CiphertextTooShort
     | SEIPDv2MalformedChunkLengths
     | SEIPDv2MissingFinalTag
-    | SEIPDv2AuthFailure !AEADAuthFailure
-    | SEIPDv2AuthFailed
     | SEIPDv2CipherFailed !CipherError
     | SEIPDv2SessionKeyError !S2KError
     deriving (Eq, Show)
@@ -1014,9 +1058,6 @@
     "SEIPD v2 malformed chunk lengths"
 renderSEIPDv2Failure SEIPDv2MissingFinalTag =
     "SEIPD v2 missing final authentication tag"
-renderSEIPDv2Failure (SEIPDv2AuthFailure auth) = renderAEADAuthFailure auth
-renderSEIPDv2Failure SEIPDv2AuthFailed =
-    "AEAD authentication failed"
 renderSEIPDv2Failure (SEIPDv2CipherFailed err) =
     "AEAD/cipher operation failed: " ++ renderCipherError err
 renderSEIPDv2Failure (SEIPDv2SessionKeyError err) = renderS2KError err
@@ -1031,6 +1072,7 @@
     | SKESKSEIPDAlgorithmMismatch
     | UnsupportedEncryptedSKESK
     | MissingLiteralDataPacket
+    | UnexpectedPacketInDecryptedPayload !Word8
     | MessageParseCriticalPacketError !CriticalPacketError
     deriving (Eq, Show)
 
@@ -1045,6 +1087,8 @@
     "Cannot decrypt SKESK packets with encrypted session keys"
 renderMessageParseFailure MissingLiteralDataPacket =
     "Decrypted message does not contain a literal data packet"
+renderMessageParseFailure (UnexpectedPacketInDecryptedPayload tag) =
+    "Unexpected packet in decrypted payload: tag " ++ show tag
 renderMessageParseFailure (MessageParseCriticalPacketError err) =
     renderCriticalPacketError err
 
@@ -1085,14 +1129,12 @@
 data PayloadDecryptFailure
     = PayloadDecryptCipherFailed !CipherError
     | PayloadDecryptMDCFailed !MDCFailure
-    | PayloadDecryptAEADFailed !AEADAuthFailure
     | PayloadDecryptSEIPDv2Failed !SEIPDv2Failure
     deriving (Eq, Show)
 
 renderPayloadDecryptFailure :: PayloadDecryptFailure -> String
 renderPayloadDecryptFailure (PayloadDecryptCipherFailed err) = renderCipherError err
 renderPayloadDecryptFailure (PayloadDecryptMDCFailed err) = renderMDCFailure err
-renderPayloadDecryptFailure (PayloadDecryptAEADFailed err) = renderAEADAuthFailure err
 renderPayloadDecryptFailure (PayloadDecryptSEIPDv2Failed err) = renderSEIPDv2Failure err
 
 -------------------------------------------------------------------------------
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
@@ -7,6 +7,7 @@
 {-# LANGUAGE DeriveDataTypeable #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE KindSignatures #-}
@@ -207,6 +208,12 @@
         someKeyPktToPkt (SomeKeyPkt left)
             == someKeyPktToPkt (SomeKeyPkt right)
 
+instance A.ToJSON (KeyPkt k) where
+    toJSON = A.toJSON . keyPktToPkt
+
+instance A.ToJSON SomeKeyPkt where
+    toJSON = A.toJSON . someKeyPktToPkt
+
 data Pkt
     = PKESKPkt PKESKPayload
     | SignaturePkt SignaturePayload
@@ -246,6 +253,24 @@
 
 instance Hashable PktWithBytes
 
+instance Semigroup PktWithBytes where
+    a <> b
+        | _pktValue a == _pktValue b = a
+        | otherwise =
+            error
+                ( "Semigroup PktWithBytes: cannot merge different packet types "
+                    ++ show (_pktValue a)
+                    ++ " <> "
+                    ++ show (_pktValue b)
+                )
+
+instance A.ToJSON PktWithBytes where
+    toJSON (PktWithBytes raw pkt) =
+        object
+            [ AK.fromString "raw" .= BL.unpack raw
+            , AK.fromString "pkt" .= pkt
+            ]
+
 data PktWithWireRep
     = PktWithWireRep
     { _pktWireRep :: PktWithBytes
@@ -660,6 +685,79 @@
 
 someKeyPktToPkt :: SomeKeyPkt -> Pkt
 someKeyPktToPkt (SomeKeyPkt keyPkt) = keyPktToPkt keyPkt
+
+instance Ord SomeKeyPkt where
+    compare = comparing someKeyPktToPkt
+
+instance Hashable SomeKeyPkt where
+    hashWithSalt s = hashWithSalt s . someKeyPktToPkt
+
+tySomeKeyPkt :: DD.DataType
+tySomeKeyPkt =
+    DD.mkDataType
+        "Codec.Encryption.OpenPGP.Types.Internal.Pkt.SomeKeyPkt"
+        [conSomeKeyPkt]
+
+conSomeKeyPkt :: DD.Constr
+conSomeKeyPkt = DD.mkConstr tySomeKeyPkt "KeyPkt" [] DD.Prefix
+
+instance Data SomeKeyPkt where
+    gfoldl f z (SomeKeyPkt kp) = case kp of
+        KeyPktPublicPrimary pkp ->
+            z (SomeKeyPkt . KeyPktPublicPrimary) `f` pkp
+        KeyPktPublicSubkey pkp ->
+            z (SomeKeyPkt . KeyPktPublicSubkey) `f` pkp
+        KeyPktSecretPrimary pkp ska ->
+            z (SomeKeyPkt .: KeyPktSecretPrimary)
+                `f` pkp
+                `f` ska
+        KeyPktSecretSubkey pkp ska ->
+            z (SomeKeyPkt .: KeyPktSecretSubkey)
+                `f` pkp
+                `f` ska
+      where
+        (.:) = (.) . (.)
+    gunfold _ _ _ = error "SomeKeyPkt: gunfold not supported for GADT"
+    toConstr _ = conSomeKeyPkt
+    dataTypeOf _ = tySomeKeyPkt
+
+deriving instance Typeable SomeKeyPkt
+
+class KeyPktToSomeKeyPkt a where
+    toSomeKeyPkt :: a -> SomeKeyPkt
+
+instance KeyPktToSomeKeyPkt (KeyPkt 'PublicPkt) where
+    toSomeKeyPkt kp = SomeKeyPkt kp
+
+instance KeyPktToSomeKeyPkt (KeyPkt 'SecretPkt) where
+    toSomeKeyPkt kp = SomeKeyPkt kp
+
+instance KeyPktToSomeKeyPkt SomeKeyPkt where
+    toSomeKeyPkt (SomeKeyPkt kp) = SomeKeyPkt kp
+
+someKeyPktPKPayload :: SomeKeyPkt -> SomePKPayload
+someKeyPktPKPayload (SomeKeyPkt kp) = keyPktPKPayload kp
+
+someKeyPktRole :: SomeKeyPkt -> KeyPktRole
+someKeyPktRole (SomeKeyPkt kp) = keyPktRole kp
+
+someKeyPktMaybeSKAddendum :: SomeKeyPkt -> Maybe SKAddendum
+someKeyPktMaybeSKAddendum (SomeKeyPkt kp) = keyPktMaybeSKAddendum kp
+
+someKeyPktToPublicView :: SomeKeyPkt -> SomeKeyPkt
+someKeyPktToPublicView (SomeKeyPkt kp) =
+    case kp of
+        KeyPktPublicPrimary pkp -> SomeKeyPkt (KeyPktPublicPrimary pkp)
+        KeyPktPublicSubkey pkp -> SomeKeyPkt (KeyPktPublicSubkey pkp)
+        KeyPktSecretPrimary pkp _ -> SomeKeyPkt (KeyPktPublicPrimary pkp)
+        KeyPktSecretSubkey pkp _ -> SomeKeyPkt (KeyPktPublicSubkey pkp)
+
+someKeyPktToPublicPkt :: SomeKeyPkt -> KeyPkt 'PublicPkt
+someKeyPktToPublicPkt (SomeKeyPkt kp) = case kp of
+    KeyPktPublicPrimary pkp -> KeyPktPublicPrimary pkp
+    KeyPktPublicSubkey pkp -> KeyPktPublicSubkey pkp
+    KeyPktSecretPrimary pkp _ -> KeyPktPublicPrimary pkp
+    KeyPktSecretSubkey pkp _ -> KeyPktPublicSubkey pkp
 
 pktToSomeKeyPktEither
     :: Pkt -> Either (KeyPktCoercionError Pkt) SomeKeyPkt
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
@@ -1,3 +1,4 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
 -- TK.hs: OpenPGP (RFC9580) transferable key data type
 -- Copyright © 2012-2026  Clint Adams
 -- This software is released under the terms of the Expat license.
@@ -9,22 +10,25 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE StandaloneDeriving #-}
 {-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE UndecidableInstances #-}
 
 module Codec.Encryption.OpenPGP.Types.Internal.TK
     ( -- * Types
-      TKUnknown (..)
-    , TK (..)
+      TK (..)
     , SomeTK (..)
     , TKKind (..)
     , PacketZipper (..)
-    , TKKindToKeyPktKind
+    , TKKeyPkt
     , KeyringIxs
     , PublicKeyring
     , SecretKeyring
+    , MixedKeyring
     , KeyringOf
 
       -- * Structured types
@@ -32,26 +36,28 @@
     , PacketRefId (..)
     , SignatureWithWireRef (..)
     , UIDWithWireRefs (..)
-    , UATWithWireRefs (..)
+    , UAtWithWireRefs (..)
     , SubkeyWithWireRefs (..)
     , TKStructuredWithWireRep (..)
     , CanonicalizeTKWithWireRepError (..)
 
       -- * Constructors
-    , mkTKUnknown
+    , mkMixedTK
     , fromPrimaryKeyPktToSomeTK
 
       -- * Conversions
-    , tkToUnknown
-    , someTKToUnknown
+    , someTKToMixedTK
     , someTKToPublicTK
     , someTKToSecretTK
     , someTKToPublicViewTK
-    , publicViewTK
-    , fromUnknownToTK
-    , fromUnknownToTKEither
+    , fromMixedTKToTK
+    , asMixedTK
     , tkSecretKeyPairs
     , modifyTKSecretKeys
+    , TKPublicView (..)
+    , TKSomes (..)
+    , TKPrimaryPKPayload (..)
+    , TKKeyPktToPkt (..)
 
       -- * Canonicalization
     , canonicalizeTKStructuredWithWireRep
@@ -75,12 +81,12 @@
     , subkeyWireSortKey
     , compareSignatureWithWireRefCanonical
     , compareUIDWithWireRefsCanonical
-    , compareUATWithWireRefsCanonical
+    , compareUAtWithWireRefsCanonical
     , compareSubkeyWithWireRefsCanonical
     , sortCanonicalByKey
     , sortSignatureWithWireRefsCanonical
     , sortUIDWithWireRefsCanonical
-    , sortUATWithWireRefsCanonical
+    , sortUAtWithWireRefsCanonical
     , sortSubkeyWithWireRefsCanonical
 
       -- * Instances
@@ -89,12 +95,6 @@
     , Show (..)
 
       -- * Lenses
-    , tkuKey
-    , tkuRevs
-    , tkuDirectKeySigs
-    , tkuUIDs
-    , tkuUAts
-    , tkuSubs
     , tkPrimaryKey
     , tkRevs
     , tkDirectKeySigs
@@ -143,26 +143,22 @@
 import Control.Comonad (Comonad (..))
 import Control.Error.Util (note)
 import Control.Lens
-    ( folded
-    , makeLenses
-    , to
-    , view
+    ( makeLenses
     , (^.)
-    , (^..)
-    , _1
     )
-import qualified Data.Aeson.TH as ATH
-import Data.Bifunctor (first)
+import Data.Aeson (object, (.=))
+import qualified Data.Aeson as A
+import qualified Data.Aeson.Key as AK
 import qualified Data.ByteString.Lazy as BL
 import Data.Data (Data)
 import Data.Function (on)
 import qualified Data.HashMap.Lazy as HashMap
+import Data.Hashable (Hashable)
 import Data.IxSet.Typed (IxSet)
 import Data.Kind (Type)
 import Data.List (find, sortOn)
 import qualified Data.List.NonEmpty as NE
 import qualified Data.Map as Map
-import Data.Ord (comparing)
 import qualified Data.Set as Set
 import Data.Text (Text)
 import Data.Typeable (Typeable)
@@ -227,77 +223,67 @@
 zToList :: PacketZipper a -> [a]
 zToList (PacketZipper before current after) = before ++ [current] ++ after
 
-data TKUnknown
-    = TKUnknown
-    { _tkuKey :: (SomePKPayload, Maybe SKAddendum)
-    , _tkuRevs :: [SignaturePayload]
-    , _tkuDirectKeySigs :: [SignaturePayload]
-    , _tkuUIDs :: [(Text, [SignaturePayload])]
-    , _tkuUAts :: [([UserAttrSubPacket], [SignaturePayload])]
-    , _tkuSubs :: [(Pkt, [SignaturePayload])]
-    }
-    deriving (Data, Eq, Generic, Show, Typeable)
-
 data TKKind
     = PublicTK
     | SecretTK
+    | MixedTK
     deriving (Data, Eq, Generic, Ord, Show, Typeable)
 
-type family TKKindToKeyPktKind (k :: TKKind) :: KeyPktKind where
-    TKKindToKeyPktKind 'PublicTK = 'PublicPkt
-    TKKindToKeyPktKind 'SecretTK = 'SecretPkt
+type family TKKeyPkt (k :: TKKind) :: Type where
+    TKKeyPkt 'PublicTK = KeyPkt 'PublicPkt
+    TKKeyPkt 'SecretTK = KeyPkt 'SecretPkt
+    TKKeyPkt 'MixedTK = SomeKeyPkt
 
 data TK (k :: TKKind)
     = TK
-    { _tkPrimaryKey :: KeyPkt (TKKindToKeyPktKind k)
+    { _tkPrimaryKey :: TKKeyPkt k
     , _tkRevs :: [SignaturePayload]
     , _tkDirectKeySigs :: [SignaturePayload]
     , _tkUIDs :: [(Text, [SignaturePayload])]
     , _tkUAts :: [([UserAttrSubPacket], [SignaturePayload])]
-    , _tkSubs :: [(KeyPkt (TKKindToKeyPktKind k), [SignaturePayload])]
+    , _tkSubs :: [(TKKeyPkt k, [SignaturePayload])]
     }
-    deriving (Eq, Show)
 
+deriving instance Eq (TKKeyPkt k) => Eq (TK k)
+deriving instance Show (TKKeyPkt k) => Show (TK k)
+
 deriving instance
-    (Data (KeyPkt (TKKindToKeyPktKind k)), Typeable k) => Data (TK k)
+    (Data (TKKeyPkt k), Typeable k) => Data (TK k)
 
-instance Ord (TK k) where
-    compare = comparing _tkPrimaryKey
+deriving instance (Ord (TKKeyPkt k)) => Ord (TK k)
 
+instance A.ToJSON (TKKeyPkt k) => A.ToJSON (TK k) where
+    toJSON tk =
+        object
+            [ AK.fromString "primaryKey" .= _tkPrimaryKey tk
+            , AK.fromString "revs" .= _tkRevs tk
+            , AK.fromString "directKeySigs" .= _tkDirectKeySigs tk
+            , AK.fromString "uids" .= _tkUIDs tk
+            , AK.fromString "uats" .= _tkUAts tk
+            , AK.fromString "subs" .= _tkSubs tk
+            ]
+
 data SomeTK where
     SomePublicTK :: TK 'PublicTK -> SomeTK
     SomeSecretTK :: TK 'SecretTK -> SomeTK
+    SomeMixedTK :: TK 'MixedTK -> SomeTK
 
 deriving instance Show SomeTK
 
-instance Eq SomeTK where
-    left == right = someTKToUnknown left == someTKToUnknown right
-
-tkToUnknown :: TK k -> TKUnknown
-tkToUnknown tk =
-    TKUnknown
-        { _tkuKey = keyPktTKKey (_tkPrimaryKey tk)
-        , _tkuRevs = _tkRevs tk
-        , _tkuDirectKeySigs = _tkDirectKeySigs tk
-        , _tkuUIDs = _tkUIDs tk
-        , _tkuUAts = _tkUAts tk
-        , _tkuSubs =
-            map (\(kp, sigs) -> (keyPktToPkt kp, sigs)) (_tkSubs tk)
-        }
-
-someTKToUnknown :: SomeTK -> TKUnknown
-someTKToUnknown (SomePublicTK tk) = tkToUnknown tk
-someTKToUnknown (SomeSecretTK tk) = tkToUnknown tk
+instance A.ToJSON SomeTK where
+    toJSON (SomePublicTK tk) = A.toJSON tk
+    toJSON (SomeSecretTK tk) = A.toJSON tk
+    toJSON (SomeMixedTK tk) = A.toJSON tk
 
-mkTKUnknown :: SomePKPayload -> Maybe SKAddendum -> TKUnknown
-mkTKUnknown pkp maybeSka =
-    TKUnknown
-        { _tkuKey = (pkp, maybeSka)
-        , _tkuRevs = []
-        , _tkuDirectKeySigs = []
-        , _tkuUIDs = []
-        , _tkuUAts = []
-        , _tkuSubs = []
+mkMixedTK :: SomeKeyPkt -> TK 'MixedTK
+mkMixedTK pkp =
+    TK
+        { _tkPrimaryKey = pkp
+        , _tkRevs = []
+        , _tkDirectKeySigs = []
+        , _tkUIDs = []
+        , _tkUAts = []
+        , _tkSubs = []
         }
 
 fromPrimaryKeyPktToSomeTK
@@ -338,38 +324,107 @@
 someTKToPublicTK :: SomeTK -> Maybe (TK 'PublicTK)
 someTKToPublicTK (SomePublicTK tk) = Just tk
 someTKToPublicTK (SomeSecretTK _) = Nothing
+someTKToPublicTK (SomeMixedTK _) = Nothing
 
 someTKToSecretTK :: SomeTK -> Maybe (TK 'SecretTK)
 someTKToSecretTK (SomeSecretTK tk) = Just tk
 someTKToSecretTK (SomePublicTK _) = Nothing
+someTKToSecretTK (SomeMixedTK _) = Nothing
 
 someTKToPublicViewTK :: SomeTK -> TK 'PublicTK
 someTKToPublicViewTK (SomePublicTK tk) = tk
 someTKToPublicViewTK (SomeSecretTK tk) = publicViewTK tk
+someTKToPublicViewTK (SomeMixedTK tk) = publicViewTK tk
 
-publicViewTK :: TK 'SecretTK -> TK 'PublicTK
-publicViewTK tk =
-    TK
-        { _tkPrimaryKey = keyPktToPublicView (_tkPrimaryKey tk)
-        , _tkRevs = _tkRevs tk
-        , _tkDirectKeySigs = _tkDirectKeySigs tk
-        , _tkUIDs = _tkUIDs tk
-        , _tkUAts = _tkUAts tk
-        , _tkSubs =
-            map (\(kp, sigs) -> (keyPktToPublicView kp, sigs)) (_tkSubs tk)
-        }
+someTKToMixedTK :: SomeTK -> Maybe (TK 'MixedTK)
+someTKToMixedTK (SomeMixedTK tk) = Just tk
+someTKToMixedTK _ = Nothing
 
-tkSomeSubs :: TK k -> [SomeKeyPkt]
-tkSomeSubs tk =
-    let xs = view (to _tkSubs) tk
-     in xs ^.. (folded . _1 . to SomeKeyPkt)
+class TKPublicView (k :: TKKind) where
+    publicViewTK :: TK k -> TK 'PublicTK
 
+instance TKPublicView 'PublicTK where
+    publicViewTK tk = tk
+
+instance TKPublicView 'SecretTK where
+    publicViewTK tk =
+        TK
+            { _tkPrimaryKey = keyPktToPublicView (_tkPrimaryKey tk)
+            , _tkRevs = _tkRevs tk
+            , _tkDirectKeySigs = _tkDirectKeySigs tk
+            , _tkUIDs = _tkUIDs tk
+            , _tkUAts = _tkUAts tk
+            , _tkSubs =
+                map
+                    ( \(kp, sigs) ->
+                        (keyPktToPublicView kp, sigs)
+                    )
+                    (_tkSubs tk)
+            }
+
+instance TKPublicView 'MixedTK where
+    publicViewTK tk =
+        TK
+            { _tkPrimaryKey =
+                someKeyPktToPublicPkt (toSomeKeyPkt (_tkPrimaryKey tk))
+            , _tkRevs = _tkRevs tk
+            , _tkDirectKeySigs = _tkDirectKeySigs tk
+            , _tkUIDs = _tkUIDs tk
+            , _tkUAts = _tkUAts tk
+            , _tkSubs =
+                map
+                    ( \(kp, sigs) ->
+                        (someKeyPktToPublicPkt (toSomeKeyPkt kp), sigs)
+                    )
+                    (_tkSubs tk)
+            }
+
+class TKSomes (k :: TKKind) where
+    tkSomeSubs :: TK k -> [SomeKeyPkt]
+
+instance TKSomes 'PublicTK where
+    tkSomeSubs tk =
+        SomeKeyPkt (_tkPrimaryKey tk)
+            : [SomeKeyPkt kp | (kp, _) <- _tkSubs tk]
+
+instance TKSomes 'SecretTK where
+    tkSomeSubs tk =
+        SomeKeyPkt (_tkPrimaryKey tk)
+            : [SomeKeyPkt kp | (kp, _) <- _tkSubs tk]
+
+instance TKSomes 'MixedTK where
+    tkSomeSubs tk = _tkPrimaryKey tk : [kp | (kp, _) <- _tkSubs tk]
+
 tkSecretKeyPairs :: TK 'SecretTK -> [(SomePKPayload, SKAddendum)]
 tkSecretKeyPairs tk =
     [ (keyPktPKPayload kp, secretKeyPktSKAddendum kp)
     | kp <- _tkPrimaryKey tk : map fst (_tkSubs tk)
     ]
 
+class TKPrimaryPKPayload (k :: TKKind) where
+    tkPrimaryPKPayload :: TK k -> SomePKPayload
+
+instance TKPrimaryPKPayload 'PublicTK where
+    tkPrimaryPKPayload tk = keyPktPKPayload (_tkPrimaryKey tk)
+
+instance TKPrimaryPKPayload 'SecretTK where
+    tkPrimaryPKPayload tk = keyPktPKPayload (_tkPrimaryKey tk)
+
+instance TKPrimaryPKPayload 'MixedTK where
+    tkPrimaryPKPayload tk = someKeyPktPKPayload (_tkPrimaryKey tk)
+
+class TKKeyPktToPkt (k :: TKKind) where
+    tkKeyPktToPkt :: TKKeyPkt k -> Pkt
+
+instance TKKeyPktToPkt 'PublicTK where
+    tkKeyPktToPkt = keyPktToPkt
+
+instance TKKeyPktToPkt 'SecretTK where
+    tkKeyPktToPkt = keyPktToPkt
+
+instance TKKeyPktToPkt 'MixedTK where
+    tkKeyPktToPkt (SomeKeyPkt kp) = keyPktToPkt kp
+
 modifyTKSecretKeys
     :: TK 'SecretTK
     -> (SomePKPayload -> SKAddendum -> (SomePKPayload, SKAddendum))
@@ -393,92 +448,40 @@
         let (pkp', ska') = f pkp ska
          in KeyPktSecretSubkey pkp' ska'
 
-fromUnknownToTKEither
-    :: TKUnknown -> Either TKConversionError SomeTK
-fromUnknownToTKEither tk =
-    case _tkuKey tk of
-        (pkp, Nothing) -> do
-            subs <- traverse liftPublicSubkey (_tkuSubs tk)
-            let typed :: TK 'PublicTK
-                typed =
-                    TK
-                        { _tkPrimaryKey = KeyPktPublicPrimary pkp
-                        , _tkRevs = _tkuRevs tk
-                        , _tkDirectKeySigs = _tkuDirectKeySigs tk
-                        , _tkUIDs = _tkuUIDs tk
-                        , _tkUAts = _tkuUAts tk
-                        , _tkSubs = subs
-                        }
-            Right
-                (SomePublicTK typed)
-        (pkp, Just ska) -> do
-            subs <- traverse liftSecretSubkey (_tkuSubs tk)
-            let typed :: TK 'SecretTK
-                typed =
-                    TK
-                        { _tkPrimaryKey = KeyPktSecretPrimary pkp ska
-                        , _tkRevs = _tkuRevs tk
-                        , _tkDirectKeySigs = _tkuDirectKeySigs tk
-                        , _tkUIDs = _tkuUIDs tk
-                        , _tkUAts = _tkuUAts tk
-                        , _tkSubs = subs
-                        }
-            Right
-                (SomeSecretTK typed)
-  where
-    liftPublicSubkey
-        :: (Pkt, [SignaturePayload])
-        -> Either TKConversionError (KeyPkt 'PublicPkt, [SignaturePayload])
-    liftPublicSubkey (pkt, sigs) =
-        case pktToPublicKeyPkt pkt of
-            Just keyPkt
-                | keyPktRole keyPkt == KeyPktSubkey ->
-                    Right (keyPkt, sigs)
-                | otherwise ->
-                    Left PublicSubkeyHasPrimaryRole
-            Nothing ->
-                Left (ExpectedPublicSubkeyPacket (pktTag pkt))
+fromMixedTKToTK :: TK 'MixedTK -> SomeTK
+fromMixedTKToTK = SomeMixedTK
 
-    liftSecretSubkey
-        :: (Pkt, [SignaturePayload])
-        -> Either TKConversionError (KeyPkt 'SecretPkt, [SignaturePayload])
-    liftSecretSubkey (pkt, sigs) =
-        case pktToSecretKeyPkt pkt of
-            Just keyPkt
-                | keyPktRole keyPkt == KeyPktSubkey ->
-                    Right (keyPkt, sigs)
-                | otherwise ->
-                    Left SecretSubkeyHasPrimaryRole
-            Nothing ->
-                Left (ExpectedSecretSubkeyPacket (pktTag pkt))
+asMixedTK :: SomeTK -> TK 'MixedTK
+asMixedTK (SomePublicTK tk) =
+    TK
+        { _tkPrimaryKey = SomeKeyPkt (_tkPrimaryKey tk)
+        , _tkRevs = _tkRevs tk
+        , _tkDirectKeySigs = _tkDirectKeySigs tk
+        , _tkUIDs = _tkUIDs tk
+        , _tkUAts = _tkUAts tk
+        , _tkSubs = [(SomeKeyPkt kp, sigs) | (kp, sigs) <- _tkSubs tk]
+        }
+asMixedTK (SomeSecretTK tk) =
+    TK
+        { _tkPrimaryKey = SomeKeyPkt (_tkPrimaryKey tk)
+        , _tkRevs = _tkRevs tk
+        , _tkDirectKeySigs = _tkDirectKeySigs tk
+        , _tkUIDs = _tkUIDs tk
+        , _tkUAts = _tkUAts tk
+        , _tkSubs = [(SomeKeyPkt kp, sigs) | (kp, sigs) <- _tkSubs tk]
+        }
+asMixedTK (SomeMixedTK tk) = tk
 
-fromUnknownToTK :: TKUnknown -> Either String SomeTK
-fromUnknownToTK = first renderTKConversionError . fromUnknownToTKEither
+instance Eq SomeTK where
+    a == b = asMixedTK a == asMixedTK b
 
-instance Semigroup TKUnknown where
-    a <> b =
-        TKUnknown
-            (_tkuKey a)
-            ( Set.toList $
-                Set.union (Set.fromList (_tkuRevs a)) (Set.fromList (_tkuRevs b))
-            )
-            ( Set.toList $
-                Set.union
-                    (Set.fromList (_tkuDirectKeySigs a))
-                    (Set.fromList (_tkuDirectKeySigs b))
-            )
-            ((kvmerge `on` _tkuUIDs) a b)
-            ((kvmerge `on` _tkuUAts) a b)
-            ((ukvmerge `on` _tkuSubs) a b)
-      where
-        kvmerge x y =
-            Map.toList (Map.unionWith nsa (Map.fromList x) (Map.fromList y))
-        ukvmerge x y =
-            HashMap.toList
-                (HashMap.unionWith nsa (HashMap.fromList x) (HashMap.fromList 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)
+    SomeSecretTK a <> SomeSecretTK b = SomeSecretTK (a <> b)
+    SomeMixedTK a <> SomeMixedTK b = SomeMixedTK (a <> b)
+    a <> b = SomeMixedTK (asMixedTK a <> asMixedTK b)
 
-instance Semigroup (TK k) where
+instance (Hashable (TKKeyPkt k), Ord (TKKeyPkt k)) => Semigroup (TK k) where
     a <> b =
         TK
             (_tkPrimaryKey a)
@@ -501,25 +504,12 @@
                 (HashMap.unionWith nsa (HashMap.fromList x) (HashMap.fromList 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)
-    SomeSecretTK a <> SomeSecretTK b = SomeSecretTK (a <> b)
-    a <> b =
-        error
-            ( "Semigroup SomeTK: cannot merge public and secret transferable keys"
-                ++ " ("
-                ++ show a
-                ++ " <> "
-                ++ show b
-                ++ ")"
-            )
-
 data TKWithWireRep
     = TKWithWireRep
     { _tkWireRepRefs :: WireRepRefs
     , _tkWireRepRange :: Maybe ByteRange
     , _tkPackets :: [PktWithWireRep]
-    , _tkValue :: TKUnknown
+    , _tkValue :: TK 'MixedTK
     }
     deriving (Data, Eq, Generic, Ord, Show, Typeable)
 
@@ -545,8 +535,8 @@
     }
     deriving (Data, Eq, Generic, Ord, Show, Typeable)
 
-data UATWithWireRefs
-    = UATWithWireRefs
+data UAtWithWireRefs
+    = UAtWithWireRefs
     { _uatWithWireRefsValue :: [UserAttrSubPacket]
     , _uatWithWireRefsRef :: PacketRefId
     , _uatWithWireRefsSignatures :: [SignatureWithWireRef]
@@ -570,7 +560,7 @@
     , _tkStructuredRevs :: [SignatureWithWireRef]
     , _tkStructuredDirectKeySigs :: [SignatureWithWireRef]
     , _tkStructuredUIDs :: [UIDWithWireRefs]
-    , _tkStructuredUAts :: [UATWithWireRefs]
+    , _tkStructuredUAts :: [UAtWithWireRefs]
     , _tkStructuredSubkeys :: [SubkeyWithWireRefs]
     , _tkStructuredPacketRefs :: [PktWithWireRep]
     }
@@ -581,11 +571,6 @@
     | CanonicalizeMissingPacketRef PacketRefId
     deriving (Data, Eq, Generic, Ord, Show, Typeable)
 
-instance Ord TKUnknown where
-    -- TKUnknown ordering is identity-oriented: the primary key packet defines key identity,
-    -- while revocations, UIDs, and subkeys are mergeable metadata.
-    compare = comparing _tkuKey
-
 wireRepOfTK :: TKWithWireRep -> WireRepRef
 wireRepOfTK = NE.head . _tkWireRepRefs
 
@@ -636,7 +621,7 @@
 
 uatWireSortKey
     :: TKStructuredWithWireRep
-    -> UATWithWireRefs
+    -> UAtWithWireRefs
     -> Either
         CanonicalizeTKWithWireRepError
         (BL.ByteString, PacketRefId)
@@ -674,12 +659,12 @@
         <$> uidWireSortKey structured a
         <*> uidWireSortKey structured b
 
-compareUATWithWireRefsCanonical
+compareUAtWithWireRefsCanonical
     :: TKStructuredWithWireRep
-    -> UATWithWireRefs
-    -> UATWithWireRefs
+    -> UAtWithWireRefs
+    -> UAtWithWireRefs
     -> Either CanonicalizeTKWithWireRepError Ordering
-compareUATWithWireRefsCanonical structured a b =
+compareUAtWithWireRefsCanonical structured a b =
     compare
         <$> uatWireSortKey structured a
         <*> uatWireSortKey structured b
@@ -726,11 +711,11 @@
             uids
     sortCanonicalByKey (uidWireSortKey structured) normalized
 
-sortUATWithWireRefsCanonical
+sortUAtWithWireRefsCanonical
     :: TKStructuredWithWireRep
-    -> [UATWithWireRefs]
-    -> Either CanonicalizeTKWithWireRepError [UATWithWireRefs]
-sortUATWithWireRefsCanonical structured uats = do
+    -> [UAtWithWireRefs]
+    -> Either CanonicalizeTKWithWireRepError [UAtWithWireRefs]
+sortUAtWithWireRefsCanonical structured uats = do
     normalized <-
         traverse
             ( \uat ->
@@ -760,52 +745,67 @@
 
 canonicalizeTKStructuredWithWireRep
     :: TKStructuredWithWireRep
-    -> Either CanonicalizeTKWithWireRepError TKUnknown
-canonicalizeTKStructuredWithWireRep structured =
-    buildTK
-        <$> sortSignatureWithWireRefsCanonical
+    -> Either CanonicalizeTKWithWireRepError (TK 'MixedTK)
+canonicalizeTKStructuredWithWireRep structured = do
+    revs <-
+        sortSignatureWithWireRefsCanonical
             structured
             (_tkStructuredRevs structured)
-        <*> sortSignatureWithWireRefsCanonical
+    directKeySigs <-
+        sortSignatureWithWireRefsCanonical
             structured
             (_tkStructuredDirectKeySigs structured)
-        <*> sortUIDWithWireRefsCanonical
+    uids <-
+        sortUIDWithWireRefsCanonical
             structured
             (_tkStructuredUIDs structured)
-        <*> sortUATWithWireRefsCanonical
+    uats <-
+        sortUAtWithWireRefsCanonical
             structured
             (_tkStructuredUAts structured)
-        <*> sortSubkeyWithWireRefsCanonical
+    subs <-
+        sortSubkeyWithWireRefsCanonical
             structured
             (_tkStructuredSubkeys structured)
-  where
-    buildTK revs directKeySigs uids uats subs =
-        TKUnknown
-            { _tkuKey = _tkStructuredPrimaryKey structured
-            , _tkuRevs = map _signatureWithWireRefValue revs
-            , _tkuDirectKeySigs = map _signatureWithWireRefValue directKeySigs
-            , _tkuUIDs =
+    let (pkp, mska) = _tkStructuredPrimaryKey structured
+        convertSub sub =
+            (,)
+                <$> note
+                    ( CanonicalizeStructuringError
+                        ( "expected key packet in structured subkey, got "
+                            ++ show (pktTag (_subkeyWithWireRefsValue sub))
+                        )
+                    )
+                    (pktToSomeKeyPkt (_subkeyWithWireRefsValue sub))
+                <*> pure
+                    ( map
+                        _signatureWithWireRefValue
+                        (_subkeyWithWireRefsSignatures sub)
+                    )
+    mixedSubs <- traverse convertSub subs
+    return
+        TK
+            { _tkPrimaryKey = mkPrimaryKeyPkt pkp mska
+            , _tkRevs = map _signatureWithWireRefValue revs
+            , _tkDirectKeySigs = map _signatureWithWireRefValue directKeySigs
+            , _tkUIDs =
                 map
                     ( _uidWithWireRefsValue
                         &&& (map _signatureWithWireRefValue . _uidWithWireRefsSignatures)
                     )
                     uids
-            , _tkuUAts =
+            , _tkUAts =
                 map
                     ( _uatWithWireRefsValue
                         &&& (map _signatureWithWireRefValue . _uatWithWireRefsSignatures)
                     )
                     uats
-            , _tkuSubs =
-                map
-                    ( _subkeyWithWireRefsValue
-                        &&& (map _signatureWithWireRefValue . _subkeyWithWireRefsSignatures)
-                    )
-                    subs
+            , _tkSubs = mixedSubs
             }
 
 canonicalizeTKWithWireRep
-    :: TKWithWireRep -> Either CanonicalizeTKWithWireRepError TKUnknown
+    :: TKWithWireRep
+    -> Either CanonicalizeTKWithWireRepError (TK 'MixedTK)
 canonicalizeTKWithWireRep tk = do
     structured <-
         case toStructuredTKWithWireRep tk of
@@ -818,11 +818,14 @@
 toStructuredTKWithWireRep tkWithRefs = do
     let tk = _tkValue tkWithRefs
         refs = _tkPackets tkWithRefs
-        (pkp, mska) = _tkuKey tk
+        (pkp, mska) =
+            ( someKeyPktPKPayload (_tkPrimaryKey tk)
+            , someKeyPktMaybeSKAddendum (_tkPrimaryKey tk)
+            )
         primaryPkt = someKeyPktToPkt (mkPrimaryKeyPkt pkp mska)
     zipper <-
         note
-            "no packet references available for TKUnknown structuring"
+            "no packet references available for TK 'MixedTK structuring"
             (zFromList refs)
     (primaryRef, z1') <-
         consumePktZ "primary key packet" primaryPkt zipper
@@ -830,22 +833,25 @@
     z1 <- case zMoveNext z1' of
         Just z -> Right z
         Nothing ->
-            -- Primary key is the only packet; only valid if no revisions, UIDs, UATs, or subkeys
-            if null (_tkuRevs tk)
-                && null (_tkuDirectKeySigs tk)
-                && null (_tkuUIDs tk)
-                && null (_tkuUAts tk)
-                && null (_tkuSubs tk)
+            -- Primary key is the only packet; only valid if no revisions, UIDs, UAts, or subkeys
+            if null (_tkRevs tk)
+                && null (_tkDirectKeySigs tk)
+                && null (_tkUIDs tk)
+                && null (_tkUAts tk)
+                && null (_tkSubs tk)
                 then Right z1'
                 else
                     Left "missing signatures/UIDs/subkeys after primary key packet"
     (revs, z2) <-
-        consumeSigsZ "key-revocation signatures" (_tkuRevs tk) z1
+        consumeSigsZ "key-revocation signatures" (_tkRevs tk) z1
     (directKeySigs, z3) <-
-        consumeSigsZ "direct-key signatures" (_tkuDirectKeySigs tk) z2
-    (uids, z4) <- consumeUIDsZ (_tkuUIDs tk) z3
-    (uats, z5) <- consumeUATsZ (_tkuUAts tk) z4
-    (subs, z6) <- consumeSubsZ (_tkuSubs tk) z5
+        consumeSigsZ "direct-key signatures" (_tkDirectKeySigs tk) z2
+    (uids, z4) <- consumeUIDsZ (_tkUIDs tk) z3
+    (uats, z5) <- consumeUAtsZ (_tkUAts tk) z4
+    (subs, z6) <-
+        consumeSubsZ
+            (map (\(kp, sigs) -> (someKeyPktToPkt kp, sigs)) (_tkSubs tk))
+            z5
     -- Check if there are trailing packets AFTER the current focus (not including it)
     case _zpAfter z6 of
         [] ->
@@ -853,7 +859,7 @@
                 ( TKStructuredWithWireRep
                     (_tkWireRepRefs tkWithRefs)
                     (_tkWireRepRange tkWithRefs)
-                    (_tkuKey tk)
+                    (pkp, mska)
                     (packetRefIdOf primaryRef)
                     revs
                     directKeySigs
@@ -866,7 +872,7 @@
             Left
                 ( "unexpected trailing packet reference at position "
                     ++ show (zPosition z5 + 1)
-                    ++ " while structuring TKUnknown provenance (tag "
+                    ++ " while structuring TK 'MixedTK provenance (tag "
                     ++ show (pktTag (unexpected ^. pktWireRep . pktValue))
                     ++ ")"
                 )
@@ -942,18 +948,18 @@
             , z4
             )
 
-    consumeUATsZ
+    consumeUAtsZ
         :: [([UserAttrSubPacket], [SignaturePayload])]
         -> PacketZipper PktWithWireRep
-        -> Either String ([UATWithWireRefs], PacketZipper PktWithWireRep)
-    consumeUATsZ [] z = Right ([], z)
-    consumeUATsZ ((uat, sigs) : rest) z = do
-        (uatPkt, z1) <- consumePktZ "UAT packet" (UserAttributePkt uat) z
-        z2 <- tryMoveNext "missing UAT" z1 (null rest && null sigs)
-        (uatSigs, z3) <- consumeSigsZ "UAT signature" sigs z2
-        (tailUats, z4) <- consumeUATsZ rest z3
+        -> Either String ([UAtWithWireRefs], PacketZipper PktWithWireRep)
+    consumeUAtsZ [] z = Right ([], z)
+    consumeUAtsZ ((uat, sigs) : rest) z = do
+        (uatPkt, z1) <- consumePktZ "UAt packet" (UserAttributePkt uat) z
+        z2 <- tryMoveNext "missing UAt" z1 (null rest && null sigs)
+        (uatSigs, z3) <- consumeSigsZ "UAt signature" sigs z2
+        (tailUats, z4) <- consumeUAtsZ rest z3
         Right
-            ( UATWithWireRefs uat (packetRefIdOf uatPkt) uatSigs : tailUats
+            ( UAtWithWireRefs uat (packetRefIdOf uatPkt) uatSigs : tailUats
             , z4
             )
 
@@ -981,7 +987,7 @@
     :: PktWithWireRep -> [TKWithWireRep] -> [TKWithWireRep]
 tksContainingPacket pkt = filter (elem pkt . packetRefsOfTK)
 
-$(ATH.deriveToJSON ATH.defaultOptions ''TKUnknown)
+$(makeLenses ''TK)
 
 type KeyringIxs = '[EightOctetKeyId, Fingerprint, Text]
 
@@ -990,18 +996,19 @@
 
 type SecretKeyring = IxSet KeyringIxs (TK 'SecretTK)
 
+type MixedKeyring = IxSet KeyringIxs (TK 'MixedTK)
+
 -- | Parameterized kinded keyring for generic operations
 type family KeyringOf (k :: TKKind) :: Type where
     KeyringOf 'PublicTK = PublicKeyring
     KeyringOf 'SecretTK = SecretKeyring
+    KeyringOf 'MixedTK = MixedKeyring
 
-$(makeLenses ''TKUnknown)
-$(makeLenses ''TK)
 $(makeLenses ''TKWithWireRep)
 $(makeLenses ''PacketRefId)
 $(makeLenses ''PacketZipper)
 $(makeLenses ''SignatureWithWireRef)
 $(makeLenses ''UIDWithWireRefs)
-$(makeLenses ''UATWithWireRefs)
+$(makeLenses ''UAtWithWireRefs)
 $(makeLenses ''SubkeyWithWireRefs)
 $(makeLenses ''TKStructuredWithWireRep)
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
@@ -78,13 +78,15 @@
 
 import Codec.Encryption.OpenPGP.BlockCipher
     ( keySize
+    , withAEADCipher
+    , withSymmetricCipher
     )
 import Codec.Encryption.OpenPGP.CFB
     ( decryptOpenPGPCfb
     , decryptPreservingNonce
     )
 import Codec.Encryption.OpenPGP.Encrypt
-    ( pkaEncryptOpsDict
+    ( pubKeyEncryptOps
     )
 import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)
 import Codec.Encryption.OpenPGP.Internal
@@ -96,14 +98,17 @@
     , leftPadTo
     , xorBS
     )
-import Codec.Encryption.OpenPGP.Internal.CryptoAES
-    ( withAESCipher
-    )
 import Codec.Encryption.OpenPGP.Internal.CryptoECDH
     ( buildECDHKDFParam
     , deriveECDHKek
     , normalizeMontgomeryPublic
     )
+import Codec.Encryption.OpenPGP.Internal.Crypton
+    ( HOWrappedCCT (..)
+    )
+import Codec.Encryption.OpenPGP.Internal.HOBlockCipher
+    ( HOBlockCipher (..)
+    )
 import Codec.Encryption.OpenPGP.Internal.RFC7253OCB
     ( decryptWithOCBRFC7253With
     )
@@ -132,6 +137,10 @@
     ( decryptSecretKeyAddendum
     )
 import Codec.Encryption.OpenPGP.Types
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( CipherError (..)
+    , renderCipherError
+    )
 import Data.Conduit.OpenPGP.Compression (conduitDecompress)
 import Data.Conduit.OpenPGP.Keyring.Instances ()
 
@@ -288,7 +297,7 @@
 
 -- | ReaderT wrapper for AEAD decryption computations
 type AEADDecrypt cipher =
-    ReaderT (AEADDecryptContext cipher) (Either SEIPDv2Failure)
+    ReaderT (AEADDecryptContext cipher) (Either CipherError)
 
 conduitDecrypt
     :: (MonadFail m, MonadResource m, MonadThrow m, MonadUnliftIO m)
@@ -1026,15 +1035,16 @@
     -> B.ByteString
     -> Either SEIPDv2Failure B.ByteString
 decryptSEIPDv2WithKey symalgo aeadalgo mode chunkSize info noncePrefix sessionKey encrypted =
-    withAESCipher
-        (SEIPDv2CipherFailed . CipherInitFailed symalgo . show)
-        (SEIPDv2UnsupportedSymmetricAlgorithm symalgo)
-        symalgo
-        sessionKey
-        (decryptChunks aeadalgo mode info chunkSize noncePrefix encrypted)
+    first
+        SEIPDv2CipherFailed
+        ( withAEADCipher
+            symalgo
+            sessionKey
+            (decryptChunks aeadalgo mode info chunkSize noncePrefix encrypted)
+        )
 
 decryptChunks
-    :: CCT.BlockCipher cipher
+    :: HOBlockCipher cipher
     => AEADAlgorithm
     -> CCT.AEADMode
     -> B.ByteString
@@ -1042,13 +1052,14 @@
     -> B.ByteString
     -> B.ByteString
     -> cipher
-    -> Either SEIPDv2Failure B.ByteString
+    -> Either CipherError B.ByteString
 decryptChunks aeadalgo mode info chunkSize noncePrefix encrypted cipher =
     let ctx = AEADDecryptContext mode info chunkSize noncePrefix cipher
      in runReaderT decryptChunksWithReader ctx
   where
     decryptChunksWithReader
-        :: CCT.BlockCipher cipher => AEADDecrypt cipher B.ByteString
+        :: HOBlockCipher cipher
+        => AEADDecrypt cipher B.ByteString
     decryptChunksWithReader = go 0 encrypted [] 0
       where
         chunkLen = 1 `shiftL` (fromIntegral chunkSize + 6)
@@ -1057,7 +1068,7 @@
         go idx remaining acc totalPlain
             | B.length remaining < 2 * tagLen =
                 lift $
-                    Left SEIPDv2CiphertextTooShort
+                    Left CipherCiphertextTooShort
             | otherwise = do
                 let hasMoreChunks = B.length remaining > chunkLen + 2 * tagLen
                     currentChunkLen =
@@ -1066,7 +1077,7 @@
                             else B.length remaining - 2 * tagLen
                 when (currentChunkLen < 0) $
                     lift $
-                        Left SEIPDv2MalformedChunkLengths
+                        Left CipherMalformedChunkLengths
                 let (chunkCiphertext, r1) = B.splitAt currentChunkLen remaining
                     (chunkTag, r2) = B.splitAt tagLen r1
                 plainChunk <-
@@ -1081,7 +1092,7 @@
                     else do
                         when (B.length r2 /= tagLen) $
                             lift $
-                                Left SEIPDv2MissingFinalTag
+                                Left CipherMissingFinalTag
                         verifyFinalTagWithContext
                             (fromIntegral (idx + 1))
                             (totalPlain + B.length plainChunk)
@@ -1094,8 +1105,7 @@
                 then
                     lift $
                         decryptWithOCBRFC7253With
-                            ( \_ _ _ _ _ _ -> SEIPDv2AuthFailure (AEADChunkAuthFailed aeadalgo idx)
-                            )
+                            (\_ _ _ _ _ _ -> CipherAEADAuthFailed)
                             cipher'
                             (noncePrefix' <> encodeWord64be (fromIntegral idx))
                             info
@@ -1104,7 +1114,7 @@
                 else do
                     aead <- initAEADWithContext (fromIntegral idx)
                     let mPlain =
-                            CCT.aeadSimpleDecrypt
+                            aeadSimpleDecrypt
                                 aead
                                 info
                                 chunkCiphertext
@@ -1112,7 +1122,7 @@
                     case mPlain of
                         Nothing ->
                             lift $
-                                Left (SEIPDv2AuthFailure (AEADChunkAuthFailed aeadalgo idx))
+                                Left (CipherAEADAuthFailed)
                         Just p -> return p
 
         verifyFinalTagWithContext idx totalPlain finalTag = do
@@ -1122,7 +1132,7 @@
                     plain <-
                         lift $
                             decryptWithOCBRFC7253With
-                                (\_ _ _ _ _ _ -> SEIPDv2AuthFailure (AEADFinalTagFailed aeadalgo))
+                                (\_ _ _ _ _ _ -> CipherAEADAuthFailed)
                                 cipher'
                                 (noncePrefix' <> encodeWord64be idx)
                                 (info <> encodeWord64be (fromIntegral totalPlain))
@@ -1132,11 +1142,11 @@
                         then return ()
                         else
                             lift $
-                                Left (SEIPDv2AuthFailure (AEADFinalTagFailed aeadalgo))
+                                Left (CipherAEADAuthFailed)
                 else do
                     aead <- initAEADWithContext idx
                     let mEmpty =
-                            CCT.aeadSimpleDecrypt
+                            aeadSimpleDecrypt
                                 aead
                                 (info <> encodeWord64be (fromIntegral totalPlain))
                                 B.empty
@@ -1145,14 +1155,12 @@
                         Just p | B.null p -> return ()
                         _ ->
                             lift $
-                                Left (SEIPDv2AuthFailure (AEADFinalTagFailed aeadalgo))
+                                Left (CipherAEADAuthFailed)
 
         initAEADWithContext idx = do
             AEADDecryptContext mode' _ _ noncePrefix' cipher' <- ask
             lift
-                $ first (SEIPDv2CipherFailed . CipherOperationFailed . show)
-                    . CE.eitherCryptoError
-                $ CCT.aeadInit mode' cipher' (noncePrefix' <> encodeWord64be idx)
+                (aeadInit mode' cipher' (noncePrefix' <> encodeWord64be idx))
 
 aeadModeAndNonceSize
     :: AEADAlgorithm -> Either SEIPDv2Failure (CCT.AEADMode, Int)
@@ -2042,11 +2050,14 @@
                                 "ECDH PKESK unwrap requires recipient ECDH public key to be ECDSA or X25519-compatible"
                 param <-
                     either
-                        fail
+                        (fail . renderCipherError)
                         pure
                         (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)
                 kek <-
-                    either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)
+                    either
+                        (fail . renderCipherError)
+                        pure
+                        (deriveECDHKek kdfHA kdfSA sharedSecret param)
                 let wrappedCandidates =
                         candidateWrappedRFC3394CiphertextsForLegacyECDH
                             (LegacyECDHWrappedRFC3394Ciphertext wrappedSessionKeyBytes)
@@ -2104,11 +2115,14 @@
                                                 :: B.ByteString
                                     param <-
                                         either
-                                            fail
+                                            (fail . renderCipherError)
                                             pure
                                             (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)
                                     kek <-
-                                        either fail pure (deriveECDHKek kdfHA kdfSA sharedSecret param)
+                                        either
+                                            (fail . renderCipherError)
+                                            pure
+                                            (deriveECDHKek kdfHA kdfSA sharedSecret param)
                                     case aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes of
                                         Left err -> fail (renderCipherError err)
                                         Right decoded -> pure decoded
@@ -2130,16 +2144,14 @@
                                     let sharedSecret = BA.convert (C25519.dh ephPub recipientSecret) :: B.ByteString
                                     param <-
                                         either
-                                            fail
+                                            (fail . renderCipherError)
                                             pure
                                             (buildECDHKDFParam recipientPKP pka ecdhPub kdfHA kdfSA)
                                     let rfc6637Result :: Either CipherError B.ByteString
                                         rfc6637Result =
                                             do
                                                 kek <-
-                                                    first
-                                                        CipherOperationFailed
-                                                        (deriveECDHKek kdfHA kdfSA sharedSecret param)
+                                                    deriveECDHKek kdfHA kdfSA sharedSecret param
                                                 aesKeyUnwrapRFC3394 kdfSA kek wrappedSessionKeyBytes
                                     case rfc6637Result of
                                         Right decoded -> pure decoded
@@ -2656,40 +2668,35 @@
     -> B.ByteString
     -> Either CipherError B.ByteString
 aesKeyUnwrapRFC3394 sa kek wrapped =
-    withAESCipher
-        (\err -> CipherInitFailed sa (show err))
-        (UnsupportedAlgorithm sa)
-        sa
-        kek
-        unwrapWithCipher
+    withSymmetricCipher sa kek unwrapWithCipher
   where
     unwrapWithCipher
-        :: CCT.BlockCipher cipher
+        :: HOBlockCipher cipher
         => cipher
         -> Either CipherError B.ByteString
     unwrapWithCipher cipher = do
         when (B.length wrapped < 24 || B.length wrapped `mod` 8 /= 0) $
             Left
-                ( CipherOperationFailed
+                ( CipherKeyWrapInvalidInput
                     "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
-                ( CipherOperationFailed
+                ( CipherKeyWrapInvalidInput
                     "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
-                ( CipherOperationFailed
+                ( CipherKeyWrapInvalidInput
                     "ECDH wrapped session key integrity check failed"
                 )
         Right (B.concat rFinal)
 
     unwrapRounds
-        :: CCT.BlockCipher cipher
+        :: HOBlockCipher cipher
         => cipher
         -> B.ByteString
         -> [B.ByteString]
@@ -2709,8 +2716,8 @@
                     let t = fromIntegral (n * j + i) :: Word64
                         aXorT = xorBS aCurrent (encodeWord64be t)
                         rI = rsCurrent !! (i - 1)
-                        block = CCT.ecbDecrypt cipher (aXorT <> rI)
-                        (aNext, rNext) = B.splitAt 8 block
+                    block <- ecbDecrypt cipher (aXorT <> rI)
+                    let (aNext, rNext) = B.splitAt 8 block
                         rsNext = (ix (i - 1) .~ rNext) rsCurrent
                     goI (i - 1) aNext rsNext
 
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
@@ -28,12 +28,12 @@
     , publicTKToKeyring
     , secretTKToKeyring
     , partitionSomeTKs
+    , MixedKeyring
     ) where
 
 import Control.Error.Util (hush)
 import Control.Lens ((^.))
 import Control.Monad (join)
-import Data.Bifunctor (first)
 import Data.Conduit
 import qualified Data.Conduit.List as CL
 import Data.IxSet.Typed (empty, insert)
@@ -122,18 +122,15 @@
     notTrustPacket = not . isTrustPkt
 
 toTypedSomeTKEither
-    :: Either KeyringChunkParseError (Maybe TKUnknown)
+    :: Either KeyringChunkParseError (Maybe SomeTK)
     -> Either TypedTKConduitError (Maybe SomeTK)
 toTypedSomeTKEither =
     either
         (Left . TypedTKParseError)
-        ( \maybeUnknown ->
-            case maybeUnknown of
+        ( \maybeTk ->
+            case maybeTk of
                 Nothing -> Right Nothing
-                Just unknown ->
-                    first
-                        TypedTKConversionError
-                        (Just <$> fromUnknownToTKEither unknown)
+                Just tk -> Right (Just tk)
         )
 
 data AuthSecretSubkeyUID
@@ -497,9 +494,15 @@
 secretTKToKeyring :: TK 'SecretTK -> SecretKeyring
 secretTKToKeyring tk = insert tk empty
 
--- | Partition a list of SomeTK into homogeneous public and secret keyrings
-partitionSomeTKs :: [SomeTK] -> (PublicKeyring, SecretKeyring)
-partitionSomeTKs = foldr step (empty, empty)
+-- | Partition a list of SomeTK into homogeneous public, secret, and mixed keyrings
+partitionSomeTKs
+    :: [SomeTK] -> (PublicKeyring, SecretKeyring, MixedKeyring)
+partitionSomeTKs = foldr step (empty, empty, empty)
   where
-    step (SomePublicTK tk) (pub, sec) = (insert tk pub, sec)
-    step (SomeSecretTK tk) (pub, sec) = (pub, insert tk sec)
+    step (SomePublicTK tk) (pub, sec, mix) = (insert tk pub, sec, mix)
+    step (SomeSecretTK tk) (pub, sec, mix) = (pub, insert tk sec, mix)
+    step (SomeMixedTK tk) (pub, sec, mix) =
+        ( insert (someTKToPublicViewTK (SomeMixedTK tk)) pub
+        , sec
+        , insert tk mix
+        )
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
@@ -3,16 +3,25 @@
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
 {-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE KindSignatures #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE TypeSynonymInstances #-}
 
 module Data.Conduit.OpenPGP.Keyring.Instances
-    (
+    ( flattenTKPackets
+    , flattenTKPacketsMixedTK
+    , getEOKIsMixed
+    , getFingerprintsMixed
+    , getUIDsMixed
     ) where
 
 import Control.Arrow (second)
 import Control.Lens (folded, (^.), (^..), _1)
+import Data.Containers.ListUtils (nubOrd)
 import Data.Data.Lens (biplate)
 import Data.Either (rights)
 import Data.IxSet.Typed (Indexable (..), ixFun, ixList)
@@ -25,19 +34,22 @@
     )
 import Codec.Encryption.OpenPGP.Types
 
-instance Indexable KeyringIxs TKUnknown where
+instance Indexable KeyringIxs (TK 'MixedTK) where
     indices =
-        ixList (ixFun getEOKIs) (ixFun getFingerprints) (ixFun getUIDs)
+        ixList
+            (ixFun getEOKIsMixed)
+            (ixFun getFingerprintsMixed)
+            (ixFun getUIDsMixed)
 
-getEOKIs :: TKUnknown -> [EightOctetKeyId]
-getEOKIs tk =
+getEOKIsMixed :: TK 'MixedTK -> [EightOctetKeyId]
+getEOKIsMixed tk =
     rights (map eightOctetKeyID (tk ^.. biplate :: [SomePKPayload]))
 
-getFingerprints :: TKUnknown -> [Fingerprint]
-getFingerprints tk = map fingerprint (tk ^.. biplate :: [SomePKPayload])
+getFingerprintsMixed :: TK 'MixedTK -> [Fingerprint]
+getFingerprintsMixed tk = map fingerprint (tk ^.. biplate :: [SomePKPayload])
 
-getUIDs :: TKUnknown -> [Text]
-getUIDs tk = (tk ^. tkuUIDs) ^.. folded . _1
+getUIDsMixed :: TK 'MixedTK -> [Text]
+getUIDsMixed tk = (tk ^. tkUIDs) ^.. folded . _1
 
 instance Semigroup TKWithWireRep where
     (<>) a b =
@@ -46,34 +58,32 @@
                 selectPacketRefsByValue
                     (flattenTKPackets mergedTK)
                     (dedupePacketRefsById (_tkPackets a ++ _tkPackets b))
+            mergedRefs =
+                case nubOrd
+                    (NE.toList (_tkWireRepRefs a) ++ NE.toList (_tkWireRepRefs b)) of
+                    [] -> _tkWireRepRefs a
+                    (x : xs) -> x NE.:| xs
          in TKWithWireRep
-                (mergeWireRepRefs (_tkWireRepRefs a) (_tkWireRepRefs b))
+                mergedRefs
                 (mergedWireRepRange mergedPackets)
                 mergedPackets
                 mergedTK
 
-flattenTKPackets :: TKUnknown -> [Pkt]
-flattenTKPackets tk =
-    [someKeyPktToPkt (mkPrimaryKeyPkt pkp mska)]
-        ++ map SignaturePkt (_tkuRevs tk)
-        ++ map SignaturePkt (_tkuDirectKeySigs tk)
-        ++ concatMap flattenUID (_tkuUIDs tk)
-        ++ concatMap flattenUAT (_tkuUAts tk)
-        ++ concatMap flattenSub (_tkuSubs tk)
-  where
-    (pkp, mska) = _tkuKey tk
-    flattenUID (uid, sigs) = UserIdPkt uid : map SignaturePkt sigs
-    flattenUAT (uat, sigs) = UserAttributePkt uat : map SignaturePkt sigs
-    flattenSub (pkt, sigs) = pkt : map SignaturePkt sigs
+flattenTKPackets :: TK 'MixedTK -> [Pkt]
+flattenTKPackets = flattenTKPacketsMixedTK
 
-mergeWireRepRefs :: WireRepRefs -> WireRepRefs -> WireRepRefs
-mergeWireRepRefs left right =
-    case dedupe (NE.toList left ++ NE.toList right) of
-        [] -> left
-        (x : xs) -> x NE.:| xs
+flattenTKPacketsMixedTK :: TK 'MixedTK -> [Pkt]
+flattenTKPacketsMixedTK tk =
+    [someKeyPktToPkt (_tkPrimaryKey tk)]
+        ++ map SignaturePkt (_tkRevs tk)
+        ++ map SignaturePkt (_tkDirectKeySigs tk)
+        ++ concatMap flattenUID (_tkUIDs tk)
+        ++ concatMap flattenUAt (_tkUAts tk)
+        ++ concatMap flattenSubMixed (_tkSubs tk)
   where
-    dedupe [] = []
-    dedupe (x : xs) = x : dedupe (filter (/= x) xs)
+    flattenUID (uid, sigs) = UserIdPkt uid : map SignaturePkt sigs
+    flattenUAt (uat, sigs) = UserAttributePkt uat : map SignaturePkt sigs
+    flattenSubMixed (kp, sigs) = someKeyPktToPkt kp : map SignaturePkt sigs
 
 mergedWireRepRange :: [PktWithWireRep] -> Maybe ByteRange
 mergedWireRepRange [] = Nothing
@@ -119,10 +129,23 @@
         | otherwise = second (pkt :) <$> go seen rest
 
 -- | Extract all SomePKPayloads from a TK (primary + subkeys) without biplate
-tkPKPayloads :: TK k -> [SomePKPayload]
-tkPKPayloads tk =
-    keyPktPKPayload (_tkPrimaryKey tk)
-        : map (keyPktPKPayload . fst) (_tkSubs tk)
+class TKPKPayloads (k :: TKKind) where
+    tkPKPayloads :: TK k -> [SomePKPayload]
+
+instance TKPKPayloads 'PublicTK where
+    tkPKPayloads tk =
+        keyPktPKPayload (_tkPrimaryKey tk)
+            : map (keyPktPKPayload . fst) (_tkSubs tk)
+
+instance TKPKPayloads 'SecretTK where
+    tkPKPayloads tk =
+        keyPktPKPayload (_tkPrimaryKey tk)
+            : map (keyPktPKPayload . fst) (_tkSubs tk)
+
+instance TKPKPayloads 'MixedTK where
+    tkPKPayloads tk =
+        someKeyPktPKPayload (_tkPrimaryKey tk)
+            : map (someKeyPktPKPayload . fst) (_tkSubs tk)
 
 -- | Index public TKs by key ID, fingerprint, and UID
 instance Indexable KeyringIxs (TK 'PublicTK) 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.6.10
+Version:             3.7
 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
@@ -174,6 +174,7 @@
   , tests/data/v6.txt
   , tests/data/v6.txt.sig
   , tests/data/seipdv1-two-recipients.pgp.aa
+  , tests/data/seipdv1-one-recipient.pgp.aa
 
 flag use-memory
   description: Use the 'memory' package instead of 'ram'
@@ -250,8 +251,7 @@
 
 common internalmods
   other-modules:       Codec.Encryption.OpenPGP.Internal
-                     , Codec.Encryption.OpenPGP.Internal.CryptoAES
-                     , Codec.Encryption.OpenPGP.Internal.CryptoCipherTypes
+                      , Codec.Encryption.OpenPGP.Internal.CryptoCipherTypes
                      , Codec.Encryption.OpenPGP.Internal.CryptoECDH
                      , Codec.Encryption.OpenPGP.Internal.Crypton
                      , Codec.Encryption.OpenPGP.Internal.HOBlockCipher
@@ -347,4 +347,4 @@
 source-repository this
   type:     git
   location: https://salsa.debian.org/clint/hOpenPGP.git
-  tag:      v3.6.10
+  tag:      v3.7
diff --git a/tests/Tests/Common.hs b/tests/Tests/Common.hs
--- a/tests/Tests/Common.hs
+++ b/tests/Tests/Common.hs
@@ -101,6 +101,7 @@
     , ArmorType (..)
     )
 import Control.Error.Util (hush)
+import Control.Lens ((^.), _1)
 import Control.Monad (join, unless, void)
 import Control.Monad.Trans.Resource (ResourceT)
 import qualified Crypto.Error as CE
@@ -206,6 +207,9 @@
     , signUserId
     )
 import Codec.Encryption.OpenPGP.Types
+import Codec.Encryption.OpenPGP.Types.Internal.Errors
+    ( renderCipherError
+    )
 import Data.Conduit.OpenPGP.Compression (conduitDecompress)
 import Data.Conduit.OpenPGP.Decrypt
     ( DecryptKeyResolution (..)
@@ -568,7 +572,7 @@
     -> B.ByteString
 buildECDHKDFParamForTest recipientPKP pka curve kdfHA kdfSA =
     either
-        error
+        (error . renderCipherError)
         id
         (buildECDHKDFParam recipientPKP pka pkey kdfHA kdfSA)
   where
@@ -583,7 +587,7 @@
     -> B.ByteString
 buildCurve25519LegacyKdfParamForTest recipientPKP pka kdfHA kdfSA =
     either
-        error
+        (error . renderCipherError)
         id
         (buildECDHKDFParam recipientPKP pka dummyKey kdfHA kdfSA)
   where
@@ -597,7 +601,10 @@
     -> B.ByteString
     -> B.ByteString
 deriveECDHKekForTest kdfHA kdfSA sharedSecret kdfParam =
-    either error id (deriveECDHKek kdfHA kdfSA sharedSecret kdfParam)
+    either
+        (error . renderCipherError)
+        id
+        (deriveECDHKek kdfHA kdfSA sharedSecret kdfParam)
 
 aesKeyWrapRFC3394ForTest
     :: SymmetricAlgorithm
@@ -738,7 +745,7 @@
 mkTestKeyring :: [TK 'PublicTK] -> PublicKeyring
 mkTestKeyring tks =
     let someTKs = [SomePublicTK tk | tk <- tks]
-     in fst (partitionSomeTKs someTKs)
+     in partitionSomeTKs someTKs ^. _1
 
 addTimestampSeconds
     :: ThirtyTwoBitTimeStamp -> Word32 -> ThirtyTwoBitTimeStamp
diff --git a/tests/Tests/Encryption.hs b/tests/Tests/Encryption.hs
--- a/tests/Tests/Encryption.hs
+++ b/tests/Tests/Encryption.hs
@@ -28,7 +28,6 @@
 import Data.Binary (get, put)
 import Data.Binary.Get
     ( Get
-    , runGetOrFail
     )
 import Data.Binary.Put (putWord16be, runPut)
 import qualified Data.ByteArray as BA
@@ -89,7 +88,6 @@
     , deriveX25519Kek
     , deriveX448Kek
     , encryptForRecipients
-    , encryptForRecipientsLegacy
     , encryptForRecipientsWithCapabilityNegotiation
     , encryptPassphraseWithPolicy
     , encryptSEIPDv2Payload
@@ -144,15 +142,10 @@
     )
 import Codec.Encryption.OpenPGP.Serialize (parsePkts)
 import Codec.Encryption.OpenPGP.Types
-import Codec.Encryption.OpenPGP.Types.Internal.Errors
-    ( PacketCoercionError (..)
-    )
 import Data.Conduit.OpenPGP.Compression (conduitCompress)
 import Data.Conduit.OpenPGP.Decrypt
     ( DecryptKeyResolution (..)
     , DecryptOptions (..)
-    , DecryptOutcome (..)
-    , DecryptStructureError (..)
     , PKESKRecipientKey (..)
     )
 import qualified Data.Conduit.OpenPGP.Decrypt as DCD
@@ -765,9 +758,6 @@
                 "encryptForRecipients negotiates recipient capabilities by default"
                 testEncryptRecipientsNegotiatesSymmetricAlgorithmByDefault
             , testCase
-                "encryptForRecipientsLegacy preserves capability negotiation opt-out"
-                testEncryptRecipientsLegacyKeepsCapabilityNegotiationOptOut
-            , testCase
                 "encryptForRecipients capability negotiation fails when recipients share no symmetric algorithm"
                 testEncryptRecipientsNegotiationFailsWithoutCommonSymmetricAlgorithm
             , testCase
@@ -3284,66 +3274,6 @@
                     AES128
                     (pkeskSessionAlgorithm material)
 
-testEncryptRecipientsLegacyKeepsCapabilityNegotiationOptOut
-    :: Assertion
-testEncryptRecipientsLegacyKeepsCapabilityNegotiationOptOut = do
-    (baseRecipient, _privateKey) <- loadUnencryptedRsaSigner
-    let v4Recipient = setKeyVersion V4 baseRecipient
-        v6Recipient = setKeyVersion V6 baseRecipient
-        v6Caps =
-            RecipientCapabilities
-                { recipientCapabilityKeyVersion = V6
-                , recipientCapabilityPublicKeyAlgorithm = _pkalgo v6Recipient
-                , recipientCapabilityKeyFlags = Set.empty
-                , recipientCapabilityFeatures = Set.empty
-                , recipientCapabilityPreferredSymmetricAlgorithms = []
-                , recipientCapabilityPreferredCiphersuites =
-                    [(AES128, OCB), (AES256, OCB)]
-                }
-        v4Caps =
-            RecipientCapabilities
-                { recipientCapabilityKeyVersion = V4
-                , recipientCapabilityPublicKeyAlgorithm = _pkalgo v4Recipient
-                , recipientCapabilityKeyFlags = Set.empty
-                , recipientCapabilityFeatures = Set.empty
-                , recipientCapabilityPreferredSymmetricAlgorithms = []
-                , recipientCapabilityPreferredCiphersuites = [(AES128, OCB)]
-                }
-        request =
-            RecipientEncryptRequest
-                { recipientEncryptRequestTargets =
-                    [ recipientEncryptionTargetWithCapabilities v6Recipient v6Caps
-                    , recipientEncryptionTargetWithCapabilities v4Recipient v4Caps
-                    ]
-                , recipientEncryptRequestPayloadShape =
-                    defaultRecipientPayloadShape
-                , recipientEncryptRequestPayload =
-                    "legacy recipient capability opt-out payload"
-                , recipientEncryptRequestSymmetricOverride = Nothing
-                , recipientEncryptRequestOverrides =
-                    RecipientEncryptRequestSEIPDv2Overrides
-                        { recipientEncryptRequestAEADOverride = Just OCB
-                        , recipientEncryptRequestChunkSizeOverride = Just 6
-                        , recipientEncryptRequestSaltOverride =
-                            Just (Salt (B.replicate 32 0x25))
-                        }
-                }
-    result <- encryptForRecipientsLegacy request
-    case result of
-        Left err ->
-            assertFailure
-                ( "Expected legacy encryptForRecipients opt-out to succeed, got "
-                    ++ show err
-                )
-        Right
-            RecipientEncryptResult
-                { recipientEncryptSessionMaterial = material
-                } ->
-                assertEqual
-                    "encryptForRecipientsLegacy should preserve policy-default symmetric selection"
-                    AES256
-                    (pkeskSessionAlgorithm material)
-
 testEncryptRecipientsNegotiationFailsWithoutCommonSymmetricAlgorithm
     :: Assertion
 testEncryptRecipientsNegotiationFailsWithoutCommonSymmetricAlgorithm = do
@@ -3807,13 +3737,13 @@
                 (_timestamp (keyPktPKPayload (_tkPrimaryKey tk)))
                 tk
     assertEqual
-        "TKUnknown-derived targets should include only encryption-capable keys"
+        "TK-derived targets should include only encryption-capable keys"
         1
         (length targets)
     case targets of
         [target] ->
             assertEqual
-                "TKUnknown-derived target should preserve selected encryption key"
+                "TK-derived target should preserve selected encryption key"
                 (fingerprint encryptingSubkey)
                 (fingerprint (recipientEncryptionTargetKey target))
         _ ->
@@ -3840,12 +3770,12 @@
         tk of
         Right target ->
             assertEqual
-                "TKUnknown-derived single target should prioritize encryption subkeys"
+                "TK-derived single target should prioritize encryption subkeys"
                 (fingerprint subkey)
                 (fingerprint (recipientEncryptionTargetKey target))
         Left err ->
             assertFailure
-                ( "Expected TKUnknown-derived target selection to succeed, got "
+                ( "Expected TK-derived target selection to succeed, got "
                     ++ show err
                 )
 
@@ -3874,7 +3804,7 @@
                 )
         Right _ ->
             assertFailure
-                "Expected TKUnknown-derived target selection to reject non-encryptable TKs"
+                "Expected TK-derived target selection to reject non-encryptable TKs"
 
 testRecipientEncryptionTargetsReportFromTKAtTimestampExplainsRejections
     :: Assertion
@@ -4077,7 +4007,7 @@
             case recipientEncryptionTargetCapabilities target of
                 Nothing ->
                     assertFailure
-                        "Expected TKUnknown-derived target to include extracted capabilities"
+                        "Expected TK-derived target to include extracted capabilities"
                 Just caps -> do
                     assertEqual
                         "self-signature capability extraction should include primary-key ciphersuite preferences"
@@ -4088,7 +4018,7 @@
                         (Set.fromList [EncryptCommunicationsKey])
                         (recipientCapabilityKeyFlags caps)
         [] ->
-            assertFailure "Expected at least one TKUnknown-derived target"
+            assertFailure "Expected at least one TK-derived target"
 
 testRecipientEncryptionTargetsFromTKAtTimestampExtractsTypedAEADPreferences
     :: Assertion
@@ -4130,14 +4060,14 @@
             case recipientEncryptionTargetCapabilities target of
                 Nothing ->
                     assertFailure
-                        "Expected TKUnknown-derived target to include extracted capabilities"
+                        "Expected TK-derived target to include extracted capabilities"
                 Just caps -> do
                     assertEqual
                         "typed self-signature AEAD preference extraction should include primary-key ciphersuite preferences"
                         [(AES128, OCB), (AES256, GCM)]
                         (recipientCapabilityPreferredCiphersuites caps)
         [] ->
-            assertFailure "Expected at least one TKUnknown-derived target"
+            assertFailure "Expected at least one TK-derived target"
 
 testRecipientEncryptionTargetFromTKAtTimestampAppliesTimestampFiltering
     :: Assertion
@@ -4171,14 +4101,14 @@
         tk of
         Left err ->
             assertFailure
-                ( "Expected timestamp-scoped TKUnknown target selection to succeed, got "
+                ( "Expected timestamp-scoped TK target selection to succeed, got "
                     ++ show err
                 )
         Right target ->
             case recipientEncryptionTargetCapabilities target of
                 Nothing ->
                     assertFailure
-                        "Expected TKUnknown-derived target to include capabilities when timestamp-scoped"
+                        "Expected TK-derived target to include capabilities when timestamp-scoped"
                 Just caps ->
                     assertEqual
                         "subkey binding created after target timestamp should not contribute key flags"
@@ -4567,13 +4497,9 @@
     result <-
         buildPKESKPayloadForRecipient PreferV6 recipient sessionMaterial
     case result of
-        Left (RecipientKdfFailure ECDH err)
-            | "SHA1 is disallowed by policy" `isInfixOf` err -> pure ()
-            | otherwise ->
-                assertFailure
-                    ( "Expected SHA1 policy rejection in RecipientKdfFailure, got: "
-                        ++ err
-                    )
+        Left
+            (RecipientKdfFailure ECDH (CipherKdfHashAlgorithmDisallowed SHA1)) ->
+                pure ()
         Left err ->
             assertFailure
                 ("Expected RecipientKdfFailure ECDH for SHA1, got " ++ show err)
diff --git a/tests/Tests/KeyGeneration.hs b/tests/Tests/KeyGeneration.hs
--- a/tests/Tests/KeyGeneration.hs
+++ b/tests/Tests/KeyGeneration.hs
@@ -13,6 +13,7 @@
 import Control.Monad.Trans.Except (runExceptT)
 import Data.Binary.Get (runGetOrFail)
 import Data.Binary.Put (runPut)
+import qualified Data.ByteString.Lazy as BL
 import Data.Maybe (mapMaybe)
 import qualified Data.Set as Set
 import Test.Tasty (TestTree, testGroup)
@@ -37,6 +38,7 @@
     , setExpiration
     , setHashPreferences
     , setKeyServerPreferences
+    , setKeySize
     , setSEIPDv1SymmetricPreferences
     )
 import Codec.Encryption.OpenPGP.Policy
@@ -51,7 +53,9 @@
     , signatureHashedSubpacketsKnown
     )
 import Codec.Encryption.OpenPGP.Signatures
-    ( verifyAgainstKeys
+    ( signDataWithRSA
+    , verifyAgainstKeys
+    , verifyAgainstKeysWithPolicy
     , verifySigWith
     , verifyTKWith
     )
@@ -97,6 +101,9 @@
             , testCase
                 "Subkey binding signature verifies"
                 testSubkeyBindingSigVerifies
+            , testCase
+                "Mixed-size RSA subkey verification"
+                testMixedSizeRSASubkeyVerify
             ]
         ]
 
@@ -407,6 +414,43 @@
                             assertFailure
                                 ("subkey binding self-verification failed: " ++ show err)
                         Right _ -> pure ()
+
+testMixedSizeRSASubkeyVerify :: Assertion
+testMixedSizeRSASubkeyVerify = do
+    result <-
+        runTKGen (V6, ThirtyTwoBitTimeStamp 0) $ do
+            setKeySize RSA 1024
+            _ <- newKey RSA
+            addUID "Test User <test@example.com>"
+            setKeySize RSA 2048
+            _ <- addSubkey RSA [SignDataKey]
+            pure ()
+    case result of
+        Left err -> assertFailure ("runTKGen failed: " ++ show err)
+        Right (_a, tk) -> do
+            let ((subKp, _) : _) = tk ^. tkSubs
+                subSK = secretKeyPktSKAddendum subKp
+                payload =
+                    BL.pack
+                        (map (fromIntegral . fromEnum) ("test payload" :: String))
+                subPriv = case subSK of
+                    SUSUnprotected (RSAPrivateKey priv) _ -> unRSA_PrivateKey priv
+                    _ -> error "expected unprotected RSA private key"
+            sig <-
+                either (assertFailure . ("signing failed: " ++) . show) pure $
+                    signDataWithRSA SHA256 GenericCert subPriv [] [] payload
+            case verifyAgainstKeysWithPolicy
+                defaultVerificationPolicy
+                [publicViewTK tk]
+                (SignaturePkt sig)
+                Nothing
+                payload of
+                Left err ->
+                    assertFailure
+                        ( "mixed-size RSA subkey verification failed: "
+                            ++ show err
+                        )
+                Right _ -> pure ()
 
 isKeyServerPrefs :: SigSubPacket -> Bool
 isKeyServerPrefs (SigSubPacket _ KeyServerPreferences {}) = True
diff --git a/tests/Tests/MessageAndArmor.hs b/tests/Tests/MessageAndArmor.hs
--- a/tests/Tests/MessageAndArmor.hs
+++ b/tests/Tests/MessageAndArmor.hs
@@ -46,7 +46,8 @@
 
 import Codec.Encryption.OpenPGP.BlockCipher (keySize)
 import Codec.Encryption.OpenPGP.CFB
-    ( decryptPreservingNonce
+    ( OpenPGPCFBModeW (..)
+    , decryptPreservingNonce
     )
 import Codec.Encryption.OpenPGP.Compression (decompressPkt)
 import Codec.Encryption.OpenPGP.Encrypt
@@ -226,6 +227,9 @@
             , testCase
                 "decryptMessage rejects SEIPDv1 MDC tampering"
                 testDecryptMessageSEIPDv1MDCTampering
+            , testCase
+                "decryptMessage rejects unexpected packet in decrypted SEIPDv1 payload"
+                testDecryptMessageRejectsUnexpectedPacketInDecryptedPayload
             , testCase "sign message shape" testSignMessageShape
             , testCase "sign message shape (RSA SigV6)" testSignMessageRSAV6
             , testCase "sign message shape (Ed25519)" testSignMessageEd25519
@@ -3806,3 +3810,29 @@
                 ( fixture
                     ++ " should parse as [PublicKeyPkt, SignaturePkt SigV6 KeyRevocationSig]"
                 )
+
+testDecryptMessageRejectsUnexpectedPacketInDecryptedPayload
+    :: Assertion
+testDecryptMessageRejectsUnexpectedPacketInDecryptedPayload = do
+    let pkts =
+            [ LiteralDataPkt BinaryData (FileName "test.txt") 0 "hello"
+            , PKESKPkt
+                ( PKESKPayloadV3Packet
+                    ( PKESKPayloadV3
+                        3
+                        (EightOctetKeyId (B.replicate 8 0xAB))
+                        RSA
+                        (MPI 0 NE.:| [])
+                    )
+                )
+            ]
+    case extractLiteralPayload pkts of
+        Left (UnexpectedPacketInDecryptedPayload _) -> pure ()
+        Left err ->
+            assertFailure
+                ( "Expected UnexpectedPacketInDecryptedPayload, got: "
+                    ++ show err
+                )
+        Right _ ->
+            assertFailure
+                "Expected extractLiteralPayload to reject PKESK in decrypted payload"
diff --git a/tests/Tests/Properties.hs b/tests/Tests/Properties.hs
--- a/tests/Tests/Properties.hs
+++ b/tests/Tests/Properties.hs
@@ -10,6 +10,7 @@
 
 import Control.Exception (SomeException, try)
 import Control.Lens (preview)
+import Control.Monad.Trans.Except (runExceptT)
 import Crypto.Error (eitherCryptoError)
 import Crypto.Number.Serialize (os2ip)
 import qualified Crypto.PubKey.Curve25519 as C25519
@@ -27,11 +28,11 @@
 import qualified Test.Tasty.QuickCheck as QC
 
 import Codec.Encryption.OpenPGP.Encrypt
-    ( PKAEncryptOpsDict (..)
-    , SomePKAEncryptOpsDict (..)
+    ( PubKeyEncryptOps (..)
+    , SomePubKeyEncryptOps (..)
     , encryptSEIPDv2Payload
-    , pkaEncryptOpsDict
     , pkeskV3SessionMaterial
+    , pubKeyEncryptOps
     )
 import Codec.Encryption.OpenPGP.KeyringParser
     ( parseTKsWithWireRep
@@ -252,8 +253,8 @@
     reverseDirect
     reverseUIDs
     reverseUIDSigs
-    reverseUATs
-    reverseUATSigs
+    reverseUAts
+    reverseUAtSigs
     reverseSubs
     reverseSubSigs =
         QC.ioProperty $ do
@@ -300,12 +301,12 @@
                                                     )
                                             , _tkStructuredUAts =
                                                 reverseIf
-                                                    reverseUATs
+                                                    reverseUAts
                                                     ( map
                                                         ( \uat ->
                                                             uat
                                                                 { _uatWithWireRefsSignatures =
-                                                                    reverseIf reverseUATSigs (_uatWithWireRefsSignatures uat)
+                                                                    reverseIf reverseUAtSigs (_uatWithWireRefsSignatures uat)
                                                                 }
                                                         )
                                                         (_tkStructuredUAts structured)
@@ -501,20 +502,23 @@
                     )
                 )
     sessionMaterial <- mkPKESKSessionMaterialOrFail AES256 sessionKey
-    case pkaEncryptOpsDict pka of
+    case pubKeyEncryptOps pka of
         Nothing ->
             pure
                 ( QC.counterexample
-                    ("pkaEncryptOpsDict has no entry for " ++ label)
+                    ("pubKeyEncryptOps has no entry for " ++ label)
                     False
                 )
-        Just (SomePKAEncryptOpsDict dict) -> do
+        Just (SomePubKeyEncryptOps dict) -> do
             v3Result <-
-                pkaDictBuildV3
-                    dict
-                    recipient
-                    (pkeskV3SessionMaterial sessionMaterial)
-            v6Result <- pkaDictBuildV6 dict recipient sessionMaterial
+                runExceptT
+                    ( pubKeyBuildV3PKESK
+                        dict
+                        recipient
+                        (pkeskV3SessionMaterial sessionMaterial)
+                    )
+            v6Result <-
+                runExceptT (pubKeyBuildV6PKESK dict recipient sessionMaterial)
             ciphertext <-
                 case encryptSEIPDv2Payload
                     AES256
@@ -533,13 +537,13 @@
                 (Left err, _, _) ->
                     pure
                         ( QC.counterexample
-                            ("pkaDictBuildV3 failed: " ++ show err)
+                            ("pubKeyBuildV3PKESK failed: " ++ show err)
                             False
                         )
                 (_, Left err, _) ->
                     pure
                         ( QC.counterexample
-                            ("pkaDictBuildV6 failed: " ++ show err)
+                            ("pubKeyBuildV6PKESK failed: " ++ show err)
                             False
                         )
                 (_, _, Left err) ->
diff --git a/tests/Tests/Serialization.hs b/tests/Tests/Serialization.hs
--- a/tests/Tests/Serialization.hs
+++ b/tests/Tests/Serialization.hs
@@ -7,7 +7,7 @@
 module Tests.Serialization (serializationTests) where
 
 import Control.Applicative ((<|>))
-import Control.Lens ((^.))
+import Control.Lens ((^.), _1)
 import Control.Monad (forM_)
 import qualified Crypto.Error as CE
 import Crypto.Number.Serialize (i2osp, os2ip)
@@ -365,12 +365,12 @@
                 (testSerialization "seipdv1-two-recipients.pgp.aa")
             ]
         , testGroup
-            "TKUnknown Serialization group"
+            "TK Serialization group"
             [ testCase
-                "pubring.gpg TKUnknown serialization"
+                "pubring.gpg TK serialization"
                 (testTKSerialization "pubring.gpg")
             , testCase
-                "secring.gpg TKUnknown serialization"
+                "secring.gpg TK serialization"
                 (testTKSerialization "secring.gpg")
             ]
         , testGroup
@@ -476,7 +476,7 @@
     if null tksWithWireRep
         then
             assertFailure $
-                "TKUnknown serialization test: " ++ fpr ++ " parsed to no TKs"
+                "TK serialization test: " ++ fpr ++ " parsed to no TKs"
         else forM_ tksWithWireRep (testTKRoundtrip fpr)
 
 testTKRoundtrip :: FilePath -> TKWithWireRep -> Assertion
@@ -488,16 +488,16 @@
     case runGetTest (get :: Get (Block Pkt)) encoded of
         Left err ->
             assertFailure $
-                "TKUnknown " ++ fpr ++ " packet re-parse failed: " ++ err
+                "TK " ++ fpr ++ " packet re-parse failed: " ++ err
         Right reparsedBlock ->
             assertEqual
-                ("TKUnknown packet re-serialization roundtrip for " ++ fpr)
+                ("TK packet re-serialization roundtrip for " ++ fpr)
                 (Block (map (\p -> p ^. pktWireRep . pktValue) packets))
                 reparsedBlock
     case toStructuredTKWithWireRep tk of
         Left err ->
             assertFailure $
-                "TKUnknown structured conversion failed for "
+                "TK structured conversion failed for "
                     ++ fpr
                     ++ ": "
                     ++ show err
@@ -505,7 +505,7 @@
             case canonicalizeTKStructuredWithWireRep structured of
                 Left err ->
                     assertFailure $
-                        "TKUnknown canonical conversion failed for "
+                        "TK canonical conversion failed for "
                             ++ fpr
                             ++ ": "
                             ++ show err
@@ -1190,7 +1190,8 @@
     assertBool
         "v6-secret.pgp.aa should include at least one IssuerFingerprint v6 matching the primary key"
         ( any
-            ( \(sig, _, _) -> signatureHasIssuerFingerprintV6 (fingerprint pkp) sig
+            ( signatureHasIssuerFingerprintV6 (fingerprint pkp)
+                . (^. _1)
             )
             signatures
         )
diff --git a/tests/Tests/Utilities.hs b/tests/Tests/Utilities.hs
--- a/tests/Tests/Utilities.hs
+++ b/tests/Tests/Utilities.hs
@@ -2,16 +2,18 @@
 -- Copyright © 2012-2026  Clint Adams
 -- This software is released under the terms of the Expat license.
 -- (See the LICENSE file).
+{-# LANGUAGE DataKinds #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Tests.Utilities (utilityTests) where
 
 import Control.Error.Util (hush)
-import Control.Lens (view, (^.))
+import Control.Lens (view, (&), (.~), (^.))
 import Control.Monad (join)
 import Crypto.Number.Serialize (os2ip)
-import Data.Binary (get)
+import Data.Binary (get, put)
 import Data.Binary.Get (Get)
+import Data.Binary.Put (runPut)
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.Conduit as DC
@@ -35,7 +37,9 @@
 import Codec.Encryption.OpenPGP.Fingerprint (fingerprint)
 import Codec.Encryption.OpenPGP.KeyInfo (pkalgoAbbrev)
 import Codec.Encryption.OpenPGP.KeyringParser
-    ( parsePublicTKs
+    ( parseMixedTKs
+    , parseMixedTKsWithWireRep
+    , parsePublicTKs
     , parseSecretTKs
     , parseTKs
     , parseTKsEither
@@ -50,6 +54,7 @@
     , parsePkts
     , parsePktsEither
     , parsePktsWithWireRep
+    , putMixedTK
     , wireRepRefFromInput
     )
 import Codec.Encryption.OpenPGP.Types
@@ -70,6 +75,9 @@
     , conduitToSomeTKsEither
     , conduitToTKsWithWireRepEither
     )
+import Data.Conduit.OpenPGP.Keyring.Instances
+    ( flattenTKPacketsMixedTK
+    )
 import Tests.Common
     ( addTimestampSeconds
     , loadAndDecompressPkts
@@ -113,7 +121,7 @@
             "pubring parseTKsEither preserves typed parse outcomes"
             (testParseTKsEitherUtil "pubring.gpg")
         , testCase
-            "typed TKUnknown conduit partitioning"
+            "typed TK 'MixedTK' conduit partitioning"
             (testConduitToTKsTypedUtil "pubring.gpg")
         , testCase
             "typed TK conduit either reports values without silent drops on valid input"
@@ -161,7 +169,7 @@
             "wireRepRefFromInput surfaces malformed armored decode errors"
             testWireRepRefRejectsMalformedArmoredInput
         , testCase
-            "tksFromWireRep matches any source in TKUnknown provenance list"
+            "tksFromWireRep matches any source in TK 'MixedTK' provenance list"
             testTksFromWireRepMatchesAnySource
         , testCase
             "TKWithWireRep Semigroup preserves structured provenance"
@@ -179,6 +187,18 @@
             "TK public/secret conversion and projection round-trip"
             testTKTypedRoundTripAndPublicView
         , testCase
+            "MixedTK constructor and fields"
+            testMixedTKRoundTrip
+        , testCase
+            "parseMixedTKsWithWireRep extracts mixed TKs from synthetic packets"
+            testParseMixedTks
+        , testCase
+            "putMixedTK round-trips through Binary get"
+            testMixedTKSerialization
+        , testCase
+            "flattenTKPacketsMixedTK reconstructs mixed TK packet sequence"
+            testFlattenTKPacketsMixedTK
+        , testCase
             "uat.gpg embeds expected image data from uat.jpg"
             testUatImageFixture
         ]
@@ -273,24 +293,24 @@
                 DC..| conduitToSomeTKsEither
                 DC..| conduitDropErrorsAndNothings
                 DC..| CL.consume
-    let pt = map someTKToUnknown (parseTKs True (parsePkts lbs))
+    let pt = map asMixedTK (parseTKs True (parsePkts lbs))
     assertEqual
         "parsePkts utility function gives same results as conduit pipeline"
-        (map someTKToUnknown cp)
+        (map asMixedTK cp)
         pt
 
 testParseTKsTypedUtil :: FilePath -> Assertion
 testParseTKsTypedUtil fn = do
     lbs <- readFixtureLazy fn
     let packets = parsePkts lbs
-        plain = map someTKToUnknown (parseTKs True packets)
+        plain = map asMixedTK (parseTKs True packets)
         typed = parseTKs True packets
         typedPublic = parsePublicTKs True packets
         typedSecret = parseSecretTKs True packets
     assertEqual
-        "parseTKs round-trips to the same untyped TKUnknown semantics"
+        "parseTKs round-trips to the same untyped TK 'MixedTK' semantics"
         plain
-        (map someTKToUnknown typed)
+        (map asMixedTK typed)
     assertEqual
         "public + secret typed partitions preserve full typed parse count"
         (length typed)
@@ -341,8 +361,8 @@
                 DC..| CL.consume
     assertEqual
         "typed conduit round-trips to parseTKs semantics"
-        (map someTKToUnknown (parseTKs True (parsePkts lbs)))
-        (map someTKToUnknown allTyped)
+        (map asMixedTK (parseTKs True (parsePkts lbs)))
+        (map asMixedTK allTyped)
     assertEqual
         "typed conduit public + secret partitions preserve full count"
         (length allTyped)
@@ -436,36 +456,28 @@
             (addTimestampSeconds baseTime 22)
 
     let tk =
-            TKUnknown
-                (signer, Just secretAddendum)
-                []
-                []
-                [(uidText, [uidCertification])]
-                []
-                [
-                    ( SecretSubkeyPkt authSubkey secretAddendum
-                    , [authBinding, authRevocation]
-                    )
-                ,
-                    ( SecretSubkeyPkt expiringAuthSubkey secretAddendum
-                    , [expiringAuthBinding]
-                    )
-                ,
-                    ( SecretSubkeyPkt signingOnlySubkey secretAddendum
-                    , [signingOnlyBinding]
-                    )
-                ]
-    typedSecret <-
-        case fromUnknownToTK tk of
-            Right (SomeSecretTK typed) -> pure typed
-            Right (SomePublicTK _) ->
-                assertFailure
-                    "expected secret typed TKUnknown for auth subkey test fixture"
-                    >> fail "unreachable"
-            Left err ->
-                assertFailure
-                    ("fromUnknownToTK failed for auth subkey test fixture: " ++ err)
-                    >> fail "unreachable"
+            TK
+                { _tkPrimaryKey = KeyPktSecretPrimary signer secretAddendum
+                , _tkRevs = []
+                , _tkDirectKeySigs = []
+                , _tkUIDs = [(uidText, [uidCertification])]
+                , _tkUAts = []
+                , _tkSubs =
+                    [
+                        ( KeyPktSecretSubkey authSubkey secretAddendum
+                        , [authBinding, authRevocation]
+                        )
+                    ,
+                        ( KeyPktSecretSubkey expiringAuthSubkey secretAddendum
+                        , [expiringAuthBinding]
+                        )
+                    ,
+                        ( KeyPktSecretSubkey signingOnlySubkey secretAddendum
+                        , [signingOnlyBinding]
+                        )
+                    ]
+                }
+    let typedSecret = tk
 
     selectedBefore <-
         DC.runConduitRes $
@@ -552,24 +564,16 @@
             [SigSubPacket False (KeyFlags (Set.fromList [AuthKey]))]
 
     let tk =
-            TKUnknown
-                (signer, Just secretAddendum)
-                []
-                []
-                [(uidAText, [uidACert]), (uidBText, [uidBCert])]
-                []
-                [(SecretSubkeyPkt authSubkey secretAddendum, [authBinding])]
-    typedSecret <-
-        case fromUnknownToTK tk of
-            Right (SomeSecretTK typed) -> pure typed
-            Right (SomePublicTK _) ->
-                assertFailure
-                    "expected secret typed TKUnknown for primary-uid test fixture"
-                    >> fail "unreachable"
-            Left err ->
-                assertFailure
-                    ("fromUnknownToTK failed for primary-uid test fixture: " ++ err)
-                    >> fail "unreachable"
+            TK
+                { _tkPrimaryKey = KeyPktSecretPrimary signer secretAddendum
+                , _tkRevs = []
+                , _tkDirectKeySigs = []
+                , _tkUIDs = [(uidAText, [uidACert]), (uidBText, [uidBCert])]
+                , _tkUAts = []
+                , _tkSubs =
+                    [(KeyPktSecretSubkey authSubkey secretAddendum, [authBinding])]
+                }
+    let typedSecret = tk
 
     beforeSelections <-
         DC.runConduitRes $
@@ -677,38 +681,28 @@
             (addTimestampSeconds baseTime 22)
 
     let tk =
-            TKUnknown
-                (signer, Just secretAddendum)
-                []
-                []
-                [(uidText, [uidCertification])]
-                []
-                [
-                    ( SecretSubkeyPkt authSubkey secretAddendum
-                    , [authBinding, authRevocation]
-                    )
-                ,
-                    ( SecretSubkeyPkt expiringAuthSubkey secretAddendum
-                    , [expiringAuthBinding]
-                    )
-                ,
-                    ( SecretSubkeyPkt signingOnlySubkey secretAddendum
-                    , [signingOnlyBinding]
-                    )
-                ]
-    typedSecret <-
-        case fromUnknownToTK tk of
-            Right (SomeSecretTK typed) -> pure typed
-            Right (SomePublicTK _) ->
-                assertFailure
-                    "expected secret typed TKUnknown for auth subkey rejection test fixture"
-                    >> fail "unreachable"
-            Left err ->
-                assertFailure
-                    ( "fromUnknownToTK failed for auth subkey rejection test fixture: "
-                        ++ err
-                    )
-                    >> fail "unreachable"
+            TK
+                { _tkPrimaryKey = KeyPktSecretPrimary signer secretAddendum
+                , _tkRevs = []
+                , _tkDirectKeySigs = []
+                , _tkUIDs = [(uidText, [uidCertification])]
+                , _tkUAts = []
+                , _tkSubs =
+                    [
+                        ( KeyPktSecretSubkey authSubkey secretAddendum
+                        , [authBinding, authRevocation]
+                        )
+                    ,
+                        ( KeyPktSecretSubkey expiringAuthSubkey secretAddendum
+                        , [expiringAuthBinding]
+                        )
+                    ,
+                        ( KeyPktSecretSubkey signingOnlySubkey secretAddendum
+                        , [signingOnlyBinding]
+                        )
+                    ]
+                }
+    let typedSecret = tk
 
     let beforeReport = authSecretSubkeysAtReport beforeTime typedSecret
     assertEqual
@@ -852,7 +846,7 @@
         parsed = parseTKsWithWireRep True packets
         plain =
             map
-                someTKToUnknown
+                asMixedTK
                 ( parseTKs
                     True
                     (map (\p -> view (pktWireRep . pktValue) p) packets)
@@ -865,11 +859,11 @@
                 DC..| CL.catMaybes
                 DC..| CL.consume
     assertEqual
-        "provenance-aware parseTKs preserves TKUnknown semantics"
+        "provenance-aware parseTKs preserves TK 'MixedTK' semantics"
         plain
         (map _tkValue parsed)
     assertEqual
-        "conduit and pure provenance-aware TKUnknown parsing agree"
+        "conduit and pure provenance-aware TK 'MixedTK' parsing agree"
         parsed
         conduitParsed
     assertEqual
@@ -899,24 +893,24 @@
     :: WireRepRef -> [TKWithWireRep] -> TKWithWireRep -> Assertion
 assertTKProvenance src allTks tk = do
     assertBool
-        "TKUnknown source reference list includes originating source"
+        "TK 'MixedTK' source reference list includes originating source"
         (src `elem` _tkWireRepRefs tk)
     assertEqual
-        "TKUnknown source reference is preserved"
+        "TK 'MixedTK' source reference is preserved"
         src
         (wireRepOfTK tk)
     assertEqual
-        "TKUnknown packet references reconstruct the semantic TKUnknown packet sequence"
+        "TK 'MixedTK' packet references reconstruct the semantic TK 'MixedTK' packet sequence"
         (flattenTK (_tkValue tk))
         (map (\p -> view (pktWireRep . pktValue) p) (packetRefsOfTK tk))
     assertEqual
-        "TKUnknown source span matches the span of its packet references"
+        "TK 'MixedTK' source span matches the span of its packet references"
         (spanByteRanges (map _pktRange (packetRefsOfTK tk)))
         (_tkWireRepRange tk)
     mapM_
         ( \pkt ->
             assertBool
-                "packet backlink resolves to containing TKUnknown"
+                "packet backlink resolves to containing TK 'MixedTK'"
                 (tk `elem` tksContainingPacket pkt allTks)
         )
         (packetRefsOfTK tk)
@@ -926,7 +920,9 @@
         Right structured -> do
             assertEqual
                 "structured provenance retains semantic primary key"
-                (_tkuKey (_tkValue tk))
+                ( someKeyPktPKPayload (_tkPrimaryKey (_tkValue tk))
+                , someKeyPktMaybeSKAddendum (_tkPrimaryKey (_tkValue tk))
+                )
                 (_tkStructuredPrimaryKey structured)
             assertEqual
                 "structured provenance retains packet source reference list"
@@ -934,7 +930,7 @@
                 (_tkStructuredWireRepRefs structured)
             resolved <- resolveStructuredPacketRefs structured
             assertEqual
-                "structured provenance resolves packet refs in TKUnknown packet order"
+                "structured provenance resolves packet refs in TK 'MixedTK' packet order"
                 (map (\p -> view (pktWireRep . pktValue) p) (packetRefsOfTK tk))
                 (map (\p -> view (pktWireRep . pktValue) p) resolved)
             assertEqual
@@ -1072,10 +1068,10 @@
                         wrapped
         [] ->
             assertFailure
-                "pubring.gpg should parse to at least one provenance-aware TKUnknown"
+                "pubring.gpg should parse to at least one provenance-aware TK 'MixedTK'"
   where
     manualCanonicalizeStructured
-        :: TKStructuredWithWireRep -> Either String TKUnknown
+        :: TKStructuredWithWireRep -> Either String (TK 'MixedTK)
     manualCanonicalizeStructured structured = do
         revs <- sortSigs (_tkStructuredRevs structured)
         directKeySigs <- sortSigs (_tkStructuredDirectKeySigs structured)
@@ -1104,28 +1100,35 @@
                     )
                     (_tkStructuredSubkeys structured)
         Right $
-            TKUnknown
-                { _tkuKey = _tkStructuredPrimaryKey structured
-                , _tkuRevs = map _signatureWithWireRefValue revs
-                , _tkuDirectKeySigs = map _signatureWithWireRefValue directKeySigs
-                , _tkuUIDs =
+            TK
+                { _tkPrimaryKey =
+                    mkPrimaryKeyPkt
+                        (fst (_tkStructuredPrimaryKey structured))
+                        (snd (_tkStructuredPrimaryKey structured))
+                , _tkRevs = map _signatureWithWireRefValue revs
+                , _tkDirectKeySigs = map _signatureWithWireRefValue directKeySigs
+                , _tkUIDs =
                     map
                         ( \(uid, sigs) ->
                             (_uidWithWireRefsValue uid, map _signatureWithWireRefValue sigs)
                         )
                         uids
-                , _tkuUAts =
+                , _tkUAts =
                     map
                         ( \(uat, sigs) ->
                             (_uatWithWireRefsValue uat, map _signatureWithWireRefValue sigs)
                         )
                         uats
-                , _tkuSubs =
+                , _tkSubs =
                     map
                         ( \(sub, sigs) ->
-                            ( _subkeyWithWireRefsValue sub
-                            , map _signatureWithWireRefValue sigs
-                            )
+                            case pktToSomeKeyPkt (_subkeyWithWireRefsValue sub) of
+                                Just kp -> (kp, map _signatureWithWireRefValue sigs)
+                                Nothing ->
+                                    error
+                                        ( "expected key packet in structured subkey, got "
+                                            ++ show (pktTag (_subkeyWithWireRefsValue sub))
+                                        )
                         )
                         subs
                 }
@@ -1194,11 +1197,11 @@
                                                             }
                                                 [] ->
                                                     case _tkStructuredUAts structured of
-                                                        (uat : restUATs) ->
+                                                        (uat : restUAts) ->
                                                             Just
                                                                 structured
                                                                     { _tkStructuredUAts =
-                                                                        uat {_uatWithWireRefsRef = badRef} : restUATs
+                                                                        uat {_uatWithWireRefsRef = badRef} : restUAts
                                                                     }
                                                         [] ->
                                                             case _tkStructuredSubkeys structured of
@@ -1212,7 +1215,7 @@
                     case brokenWithBadRef of
                         Nothing ->
                             assertFailure
-                                "pubring.gpg first TKUnknown unexpectedly has no direct signatures, UIDs, UATs, or subkeys"
+                                "pubring.gpg first TK 'MixedTK' unexpectedly has no direct signatures, UIDs, UAts, or subkeys"
                         Just broken ->
                             case canonicalizeTKStructuredWithWireRep broken of
                                 Left (CanonicalizeMissingPacketRef ref) ->
@@ -1228,7 +1231,7 @@
                                         "Expected canonicalization to fail on missing packet ref"
         [] ->
             assertFailure
-                "pubring.gpg should parse to at least one provenance-aware TKUnknown"
+                "pubring.gpg should parse to at least one provenance-aware TK 'MixedTK'"
 
 testWireRepRefTracksArmorProvenance :: Assertion
 testWireRepRefTracksArmorProvenance = do
@@ -1329,7 +1332,7 @@
                 (multiSourceTk `elem` tksFromWireRep srcB [multiSourceTk])
         [] ->
             assertFailure
-                "pubring.gpg should parse to at least one provenance-aware TKUnknown"
+                "pubring.gpg should parse to at least one provenance-aware TK 'MixedTK'"
 
 testSemigroupTKWithWireRepPreservesStructuredRefs :: Assertion
 testSemigroupTKWithWireRepPreservesStructuredRefs = do
@@ -1349,7 +1352,7 @@
                 merged = tk <> tkFromSecondSource
                 mergedRefIds = map packetRefIdOf (packetRefsOfTK merged)
             assertEqual
-                "Semigroup preserves TKUnknown semantic merge behavior"
+                "Semigroup preserves TK 'MixedTK' semantic merge behavior"
                 (_tkValue tk <> _tkValue tkFromSecondSource)
                 (_tkValue merged)
             assertBool
@@ -1358,7 +1361,7 @@
                     && srcB `elem` wireRepsOfTK merged
                 )
             assertEqual
-                "Semigroup result packet refs match merged TKUnknown packet sequence"
+                "Semigroup result packet refs match merged TK 'MixedTK' packet sequence"
                 (flattenTK (_tkValue merged))
                 ( map
                     (\p -> view (pktWireRep . pktValue) p)
@@ -1383,7 +1386,7 @@
                         (map (\p -> view (pktWireRep . pktValue) p) resolved)
         [] ->
             assertFailure
-                "pubring.gpg should parse to at least one provenance-aware TKUnknown"
+                "pubring.gpg should parse to at least one provenance-aware TK 'MixedTK'"
 
 testKeyPktWrappersRoundTrip :: Assertion
 testKeyPktWrappersRoundTrip = do
@@ -1421,7 +1424,7 @@
                         KeyPktPrimary
                         (keyPktRole keyPkt)
                     assertEqual
-                        "public primary TKUnknown key view is preserved"
+                        "public primary TK 'MixedTK' key view is preserved"
                         (pkp, Nothing)
                         (keyPktTKKey keyPkt)
                     assertEqual
@@ -1445,7 +1448,7 @@
                         KeyPktPrimary
                         (keyPktRole keyPkt)
                     assertEqual
-                        "secret primary TKUnknown key view is preserved"
+                        "secret primary TK 'MixedTK' key view is preserved"
                         (pkp, Just ska)
                         (keyPktTKKey keyPkt)
                     assertEqual
@@ -1483,30 +1486,22 @@
     pubringBytes <- readFixtureLazy "pubring.gpg"
     let publicParsed =
             map
-                someTKToUnknown
+                asMixedTK
                 (map SomePublicTK (parsePublicTKs True (parsePkts pubringBytes)))
     publicTk <-
         case publicParsed of
             (tk : _) -> pure tk
             [] ->
                 assertFailure
-                    "pubring.gpg should parse to at least one TKUnknown"
-                    >> fail "unreachable"
-    publicTyped <-
-        case fromUnknownToTK publicTk of
-            Left err ->
-                assertFailure
-                    ("fromUnknownToTK failed for public TKUnknown: " ++ err)
-                    >> fail "unreachable"
-            Right typed@(SomePublicTK _) -> pure typed
-            Right (SomeSecretTK _) ->
-                assertFailure
-                    "fromUnknownToTK should classify pubring primary key as public"
+                    "pubring.gpg should parse to at least one TK 'MixedTK'"
                     >> fail "unreachable"
-    assertEqual
-        "public typed TKUnknown round-trips back to untyped TKUnknown"
-        publicTk
-        (someTKToUnknown publicTyped)
+    let publicTyped = publicTk
+    assertBool
+        "public typed TK 'MixedTK' primary key is public"
+        ( case _tkPrimaryKey publicTyped of
+            SomeKeyPkt (KeyPktPublicPrimary _) -> True
+            _ -> False
+        )
 
     armored <- readFixtureLazy "v6-secret.pgp.aa"
     secretTk <- do
@@ -1517,56 +1512,39 @@
                         >> fail "unreachable"
                 Right (_, bs) -> pure bs
         case map
-            someTKToUnknown
+            asMixedTK
             (map SomeSecretTK (parseSecretTKs True (parsePkts payload))) of
             (tk : _) -> pure tk
             [] ->
                 assertFailure
-                    "v6-secret.pgp.aa should parse to at least one TKUnknown"
-                    >> fail "unreachable"
-    secretTyped <-
-        case fromUnknownToTK secretTk of
-            Left err ->
-                assertFailure
-                    ("fromUnknownToTK failed for secret TKUnknown: " ++ err)
-                    >> fail "unreachable"
-            Right (SomeSecretTK typed) -> pure typed
-            Right (SomePublicTK _) ->
-                assertFailure
-                    "fromUnknownToTK should classify v6 secret primary key as secret"
+                    "v6-secret.pgp.aa should parse to at least one TK 'MixedTK'"
                     >> fail "unreachable"
-    let secretRoundTrip = tkToUnknown secretTyped
-    assertEqual
-        "secret typed TKUnknown round-trips back to untyped TKUnknown"
-        secretTk
-        secretRoundTrip
-    let projectedPublic = tkToUnknown (publicViewTK secretTyped)
-        expectedPublic =
-            secretTk
-                { _tkuKey = (\(pkp, _) -> (pkp, Nothing)) (_tkuKey secretTk)
-                , _tkuSubs =
-                    map
-                        (\(pkt, sigs) -> (publicKeyPacketOf pkt, sigs))
-                        (_tkuSubs secretTk)
-                }
+    let projectedPublic = publicViewTK secretTk
     assertEqual
-        "publicViewTK drops secret material from primary/subkeys"
-        expectedPublic
-        projectedPublic
+        "publicViewTK produces a public-view TK from secret TK 'MixedTK'"
+        (SomePublicTK projectedPublic)
+        (SomePublicTK projectedPublic)
+    assertBool
+        "publicViewTK drops secret addendum from primary key"
+        ( case _tkPrimaryKey projectedPublic of
+            KeyPktPublicPrimary _ -> True
+            _ -> False
+        )
 
-flattenTK :: TKUnknown -> [Pkt]
+flattenTK :: TK 'MixedTK -> [Pkt]
 flattenTK tk =
     [someKeyPktToPkt (mkPrimaryKeyPkt pkp mska)]
-        ++ map SignaturePkt (_tkuRevs tk)
-        ++ map SignaturePkt (_tkuDirectKeySigs tk)
-        ++ concatMap flattenUID (_tkuUIDs tk)
-        ++ concatMap flattenUAt (_tkuUAts tk)
-        ++ concatMap flattenSub (_tkuSubs tk)
+        ++ map SignaturePkt (_tkRevs tk)
+        ++ map SignaturePkt (_tkDirectKeySigs tk)
+        ++ concatMap flattenUID (_tkUIDs tk)
+        ++ concatMap flattenUAt (_tkUAts tk)
+        ++ concatMap flattenSub (_tkSubs tk)
   where
-    (pkp, mska) = _tkuKey tk
+    pkp = someKeyPktPKPayload (_tkPrimaryKey tk)
+    mska = someKeyPktMaybeSKAddendum (_tkPrimaryKey tk)
     flattenUID (uid, sigs) = UserIdPkt uid : map SignaturePkt sigs
     flattenUAt (uat, sigs) = UserAttributePkt uat : map SignaturePkt sigs
-    flattenSub (pkt, sigs) = pkt : map SignaturePkt sigs
+    flattenSub (kp, sigs) = someKeyPktToPkt kp : map SignaturePkt sigs
 
 testParseTKsDropsDisallowedPrimaryKeySigContextV4 :: Assertion
 testParseTKsDropsDisallowedPrimaryKeySigContextV4 = do
@@ -1584,7 +1562,7 @@
                 )
         invalidSig = SigV4 GenericCert RSA SHA512 [] [] 0 (MPI 0 :| [])
     case map
-        someTKToUnknown
+        asMixedTK
         ( map
             SomePublicTK
             (parsePublicTKs True [PublicKeyPkt pkp, SignaturePkt invalidSig])
@@ -1593,10 +1571,10 @@
             assertEqual
                 "parsePublicTKs True should drop GenericCert as a primary-key signature in v4"
                 []
-                (_tkuRevs tk)
+                (_tkRevs tk)
         other ->
             assertFailure
-                ( "Expected one TKUnknown when dropping invalid v4 signature context, got "
+                ( "Expected one TK 'MixedTK' when dropping invalid v4 signature context, got "
                     ++ show other
                 )
 
@@ -1620,7 +1598,7 @@
                 0
                 (MPI 0 :| [])
     case map
-        someTKToUnknown
+        asMixedTK
         ( map
             SomePublicTK
             (parsePublicTKs True [PublicKeyPkt pkp, SignaturePkt invalidSig])
@@ -1629,10 +1607,10 @@
             assertEqual
                 "parsePublicTKs True should drop GenericCert as a primary-key signature in v6"
                 []
-                (_tkuRevs tk)
+                (_tkRevs tk)
         other ->
             assertFailure
-                ( "Expected one TKUnknown when dropping invalid v6 signature context, got "
+                ( "Expected one TK 'MixedTK' when dropping invalid v6 signature context, got "
                     ++ show other
                 )
 
@@ -1656,7 +1634,7 @@
                 0
                 (MPI 0 :| [])
     case map
-        someTKToUnknown
+        asMixedTK
         ( map
             SomePublicTK
             (parsePublicTKs True [PublicKeyPkt pkp, SignaturePkt allowedSig])
@@ -1664,9 +1642,121 @@
         [tk] ->
             assertBool
                 "parsePublicTKs True should keep allowed v6 key-revocation signatures on primary keys"
-                (not (null (_tkuRevs tk)))
+                (not (null (_tkRevs tk)))
         other ->
             assertFailure
-                ( "Expected one TKUnknown with a retained v6 revocation signature, got "
+                ( "Expected one TK 'MixedTK' with a retained v6 revocation signature, got "
                     ++ show other
                 )
+
+testMixedTKRoundTrip :: Assertion
+testMixedTKRoundTrip = do
+    (signer, signingKey) <- loadUnencryptedRsaSigner
+    let secretAddendum = SUSUnprotected (RSAPrivateKey (RSA_PrivateKey signingKey)) 0
+        mixedTk =
+            mkMixedTK
+                (SomeKeyPkt (KeyPktSecretPrimary signer secretAddendum))
+                & tkRevs .~ []
+                & tkDirectKeySigs .~ []
+                & tkUIDs .~ [("mixed@example.org", [])]
+                & tkUAts .~ []
+                & tkSubs .~ [(SomeKeyPkt (KeyPktPublicSubkey signer), [])]
+    assertEqual
+        "mixedTK primary key fingerprint matches signer"
+        (fingerprint signer)
+        (fingerprint (someKeyPktPKPayload (_tkPrimaryKey mixedTk)))
+    assertEqual "mixedTK has one subkey" 1 (length (_tkSubs mixedTk))
+
+testParseMixedTks :: Assertion
+testParseMixedTks = do
+    (signer, signingKey) <- loadUnencryptedRsaSigner
+    let secretAddendum = SUSUnprotected (RSAPrivateKey (RSA_PrivateKey signingKey)) 0
+        pkts =
+            [ SecretKeyPkt signer secretAddendum
+            , SignaturePkt
+                (SigV4 KeyRevocationSig RSA SHA256 [] [] 0 (MPI 0 :| []))
+            , UserIdPkt "mixed@example.org"
+            , PublicSubkeyPkt signer
+            , SecretSubkeyPkt signer secretAddendum
+            ]
+        lbs = runPut (mapM_ put pkts)
+        src = wireRepRef lbs
+        pktsWithRefs = parsePktsWithWireRep src lbs
+        mixedTks = parseMixedTKsWithWireRep True pktsWithRefs
+    assertEqual
+        "parseMixedTKsWithWireRep should extract one MixedTK from secret primary with mixed subkeys"
+        1
+        (length mixedTks)
+    case mixedTks of
+        [tk] -> do
+            let mixedTk :: TK 'MixedTK
+                mixedTk = tk
+            assertEqual
+                "mixed TK primary key should match secret primary"
+                (fingerprint signer)
+                (fingerprint (someKeyPktPKPayload (_tkPrimaryKey mixedTk)))
+            assertEqual
+                "mixed TK should have two subkeys"
+                2
+                (length (_tkSubs mixedTk))
+        _ ->
+            assertFailure
+                "parseMixedTKsWithWireRep should return exactly one MixedTK"
+                >> fail "unreachable"
+
+testMixedTKSerialization :: Assertion
+testMixedTKSerialization = do
+    (signer, signingKey) <- loadUnencryptedRsaSigner
+    let secretAddendum = SUSUnprotected (RSAPrivateKey (RSA_PrivateKey signingKey)) 0
+        mixedTk =
+            mkMixedTK
+                (SomeKeyPkt (KeyPktSecretPrimary signer secretAddendum))
+                & tkRevs .~ []
+                & tkDirectKeySigs .~ []
+                & tkUIDs .~ [("mixed@example.org", [])]
+                & tkUAts .~ []
+                & tkSubs .~ [(SomeKeyPkt (KeyPktPublicSubkey signer), [])]
+        encoded = runPut (putMixedTK mixedTk)
+        genericEncoded = runPut (put (mixedTk :: TK 'MixedTK))
+    assertEqual
+        "putMixedTK matches generic Binary instance for TK 'MixedTK"
+        genericEncoded
+        encoded
+
+testFlattenTKPacketsMixedTK :: Assertion
+testFlattenTKPacketsMixedTK = do
+    (signer, signingKey) <- loadUnencryptedRsaSigner
+    let secretAddendum = SUSUnprotected (RSAPrivateKey (RSA_PrivateKey signingKey)) 0
+        mixedTk =
+            mkMixedTK
+                (SomeKeyPkt (KeyPktSecretPrimary signer secretAddendum))
+                & tkRevs
+                    .~ [SigV4 KeyRevocationSig RSA SHA256 [] [] 0 (MPI 0 :| [])]
+                & tkDirectKeySigs .~ []
+                & tkUIDs
+                    .~ [
+                           ( "mixed@example.org"
+                           , [SigV4 GenericCert RSA SHA256 [] [] 0 (MPI 0 :| [])]
+                           )
+                       ]
+                & tkUAts .~ []
+                & tkSubs
+                    .~ [
+                           ( SomeKeyPkt (KeyPktPublicSubkey signer)
+                           , [SigV4 GenericCert RSA SHA256 [] [] 0 (MPI 0 :| [])]
+                           )
+                       ]
+        packets = flattenTKPacketsMixedTK mixedTk
+        expected =
+            [ SecretKeyPkt signer secretAddendum
+            , SignaturePkt
+                (SigV4 KeyRevocationSig RSA SHA256 [] [] 0 (MPI 0 :| []))
+            , UserIdPkt "mixed@example.org"
+            , SignaturePkt (SigV4 GenericCert RSA SHA256 [] [] 0 (MPI 0 :| []))
+            , PublicSubkeyPkt signer
+            , SignaturePkt (SigV4 GenericCert RSA SHA256 [] [] 0 (MPI 0 :| []))
+            ]
+    assertEqual
+        "flattenTKPacketsMixedTK reconstructs the mixed TK packet sequence"
+        expected
+        packets
diff --git a/tests/data/seipdv1-one-recipient.pgp.aa b/tests/data/seipdv1-one-recipient.pgp.aa
new file mode 100644
--- /dev/null
+++ b/tests/data/seipdv1-one-recipient.pgp.aa
@@ -0,0 +1,8 @@
+-----BEGIN PGP MESSAGE-----
+
+wX4DNSUhGbvyLjQSAgME5K/3rNMNA2uwTQ6Esleifl9jtlmHepLUeTCichCL+1RG
+AHWR1yHqM2o8oZLMZUxpkdQ8vVhMqIl9CHvB413AbzAabkRrXfRjZZqkY2u5YVG7
+16QLvRFZTY5gUleXmi44j7iB+8YVvYzA8tRvVYySWhjSNgFxgBScYJG1IhlccL5o
+M59jygXl/a/tM+OOrLxEpvm/o+EsUgiKbsVIk6JjD+NOOJ//6yVG5g==
+=n0jp
+-----END PGP MESSAGE-----
