packages feed

hOpenPGP-3.0.0: tests/Tests/Common.hs

-- Common.hs: hOpenPGP test suite
-- Copyright © 2012-2026  Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE TypeApplications #-}

module Tests.Common
  ( addTimestampSeconds
  , armorPayload
  , assertFalse
  , assertTrue
  , collectSecretKeyInfos
  , conduitDecryptWithPKESKContext
  , loadArmor
  , loadAndDecompressPkts
  , loadSEIPDv2FixtureWithV4Secret
  , loadUnencryptedRsaSigner
  , loadV4EncryptedSecretKeyFixtureForProperty
  , loadV6UnencryptedSecretKeyFixtureForProperty
  , prependUnusableLatestPKESK
  , readFixtureLazy
  , readFixturePackets
  , readFixturePayload
  , reorderPrecedingPKESKs
  , reverseIf
  , runGet -- FIXME: this is confusing
  , selectRecipientKeyInfo
  , setKeyTimestamp
  , signCertificationAt
  , signSubkeyBindingWithRSAExtrasAt
  , signSubkeyRevocationWithRSAAt
  , timestampToUTCTime
  , assertSingleFailureContainsTimeline
  , assertSingleSignerFingerprint
  , encryptMessageDefault
  , expectV4PKPayload
  , expectV6PKPayload
  , extractV4SignatureAlgorithmFields
  , fp
  , loadKeyring
  , loadDeterministicEd25519Signer
  , loadDeterministicEd25519SignerV6
  , loadDeterministicEd448Signer
  , loadDeterministicEd448SignerV6
  , loadUnencryptedRsaSignerV6
  , messageIssuerSubpacketsAt
  , mkTestKeyring
  , setPKAlgorithm
  , signBinaryMessageWithRSAAt
  , signBinaryMessageWithEd25519At
  , signKeyRevocationWithReasonAt
  , signKeyRevocationWithReasonAndExtrasAt
  , signSubkeyBindingWithRSAAt
  , verifyTimelinePackets
  , signCertificationRevocationWithEd25519At
  , signCertificationWithEd25519At
  , verificationFixtureGroup
  , verifyMessageFromBytestring
  , verifyMessageFromBytestringBatch
  , verifyMessageFromPackets
  , verifyMessageFromPacketsBatch
  , certificateVerificationFixtures
  , fixturePath
  , messageVerificationFixtures
  , readPKIPassphrase
  , setKeyVersion
  , signCertificationRevocationAt
  , aesKeyWrapRFC3394ForTest
  , assertX25519EskShape
  , assertX448EskShape
  , buildCurve25519LegacyKdfParamForTest
  , buildECDHKDFParamForTest
  , cgp
  , conduitDecrypt -- FIXME: this is confusing
  , conduitDecryptChecked
  , conduitDecryptCheckedWithDecryptPolicy
  , conduitDecryptWithCandidatesCallbackAndPolicy
  , conduitDecryptWithDecryptPolicy
  , deriveECDHKekForTest
  , deriveX25519KekForTest
  , deriveX448KekForTest
  , doPkeyAndSkeyMatch
  , encodeChecksum16
  , forceVersionedRecipientIdentifier
  , isPrecedingESK
  , mkPKESKSessionMaterialOrFail
  , readFixtureStrict
  , selectRecipientKeyInfoByRawRecipientId
  , signDirectKeyWithRSAExtrasAt
  , testEncodeOpenPGPSessionMaterial
  , testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized
  , testSEIPDv2ForV4KeyArmor
  , testSEIPDv2TwoRecipientsArmor
  , testSEIPDv2ThreeRecipientsArmor
  )
where

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)

import Codec.Encryption.OpenPGP.Arbitrary ()
import qualified Codec.Encryption.OpenPGP.ASCIIArmor as AA
import Codec.Encryption.OpenPGP.ASCIIArmor.Types (Armor(..), ArmorType(..))
import Codec.Encryption.OpenPGP.Compression (decompressPkt)
import Codec.Encryption.OpenPGP.Encrypt
  ( PKESKSessionMaterial
  , mkPKESKSessionMaterial
  , encodeOpenPGPSessionMaterial
  )

import Codec.Encryption.OpenPGP.Fingerprint (eightOctetKeyID, fingerprint)
import Codec.Encryption.OpenPGP.Internal
  ( curveFromCurve
  , curveToCurveoidBS
  , emptyPSC
  , lastPrimaryKey
  , lastUIDorUAt
  , lastSubkey
  )
import Codec.Encryption.OpenPGP.KeySelection (parseFingerprint)
import Codec.Encryption.OpenPGP.Message
  ( asV4PKPayload
  , asV6PKPayload
  , ClearPayload
  , EncryptedPayload
  , encryptMessage
  , EncryptMessageOptions(..)
  , Passphrase
  , MessageError(..)
  , RecoveredSessionMaterial(..)
  , SessionMaterialExposure(..)
  , VersionedPKPayload
  )
import Codec.Encryption.OpenPGP.SecretKey
  ( decryptPrivateKey
  )
import Codec.Encryption.OpenPGP.Serialize
  ( dearmorIfAsciiArmored
  , parsePkts
  )
import Codec.Encryption.OpenPGP.SerializeForSigs (payloadForSig)
import Codec.Encryption.OpenPGP.Signatures
  ( VerificationError(..)
  , renderVerificationError
  , renderSignError
  , signCertificationWithRSA
  , signCertRevocationWithRSA
  , signSubkeyRevocationWithRSA
  , signDataWithRSA
  , signDataWithEd25519
  , signDirectKeyWithRSA
  , signKeyRevocationWithRSA
  )
import Codec.Encryption.OpenPGP.Types
import Control.Monad (unless, void)
import qualified "crypton" Crypto.Cipher.AES as AES
import qualified "crypton" Crypto.Cipher.Types as CCT
import qualified Crypto.Error as CE
import qualified Crypto.Hash as CH
import qualified Crypto.Hash.Algorithms as CHA
import Crypto.KDF.HKDF (expand, extract)
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Crypto.PubKey.Ed448 as Ed448
import qualified Crypto.PubKey.RSA.PKCS15 as P15
import Control.Monad.Trans.Resource (ResourceT)
import Crypto.Number.Serialize (os2ip)
import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
import qualified Crypto.PubKey.RSA as RSA
import qualified Data.ByteArray as BA
import Data.Bifunctor (bimap, first)
import Data.Binary (get)
import Data.Binary.Get
  ( Get
  , getLazyByteString
  , getRemainingLazyByteString
  , getWord16be
  , getWord8
  , runGetOrFail
  )
import Data.Binary.Put (putWord64be, runPut)
import Data.Bits (xor)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC8
import qualified Data.ByteString.Base16.Lazy as B16L
import Data.Char (toUpper)
import Data.Conduit.OpenPGP.Compression (conduitDecompress)
import Data.Conduit.OpenPGP.Decrypt
  ( DecryptKeyResolution(..)
  , DecryptOptions(..)
  , PKESKRecipientKey(..)
  , DecryptOutcome(..)
  )
import qualified Data.Conduit.OpenPGP.Decrypt as DCD
import Codec.Encryption.OpenPGP.Policy
  ( DecryptPolicy
  , defaultDecryptPolicy
  )
import Data.Conduit.OpenPGP.Keyring
  ( conduitToPublicViewTKs
  , sinkPublicKeyringMap
  , partitionSomeTKs
  )
import Data.Conduit.OpenPGP.Message
  ( VerificationOptions(..)
  , VerificationPolicy(..)
  , defaultVerificationOptions
  , VerificationMode(..)
  , verifyMessage
  , verifyMessagePackets
  )
import Data.Conduit.Serialization.Binary (conduitGet)
import Data.List (isInfixOf)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (catMaybes, listToMaybe)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Word (Word32, Word64)

import qualified Data.Conduit as DC
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL

import qualified Crypto.PubKey.ECC.Types as ECCT

-- Test assertion helpers
assertTrue :: String -> Bool -> Assertion
assertTrue msg b = assertBool msg b

assertFalse :: String -> Bool -> Assertion
assertFalse msg b = assertBool msg (not b)

fixturePath :: FilePath -> FilePath
fixturePath file = "tests/data/" ++ file

readFixtureLazy :: FilePath -> IO BL.ByteString
readFixtureLazy = BL.readFile . fixturePath

readFixtureStrict :: FilePath -> IO B.ByteString
readFixtureStrict = B.readFile . fixturePath

readFixturePackets :: FilePath -> IO [Pkt]
readFixturePackets file =
  DC.runConduitRes $ CB.sourceFile (fixturePath file) DC..| conduitGet get DC..| CL.consume

readFixtureDecompressedPackets :: FilePath -> IO [Pkt]
readFixtureDecompressedPackets file =
  DC.runConduitRes $
  CB.sourceFile (fixturePath file) DC..| conduitGet get DC..| conduitDecompress DC..| CL.consume

loadFirstArmor :: FilePath -> IO Armor
loadFirstArmor file = do
  armors <- loadArmor file
  case armors of
    (a:_) -> pure a
    [] -> assertFailure (file ++ " armor file contained no armor blocks") >> fail "expected armor block"

readPKIPassphrase :: IO BL.ByteString
readPKIPassphrase = readFixtureLazy "pki-password.txt"

-- this needs a better name
runGet :: Get a -> BL.ByteString -> Either String a
runGet g bs = bimap (\(_, _, x) -> x) (\(_, _, x) -> x) (runGetOrFail g bs)

extractV4SignatureAlgorithmFields ::
     BL.ByteString -> Either String (PubKeyAlgorithm, B.ByteString)
extractV4SignatureAlgorithmFields =
  runGet $ do
    version <- getWord8
    if version /= 4
      then fail ("expected v4 signature payload, got version " ++ show version)
      else do
        _ <- getWord8 -- sig type
        pka <- getWord8
        _ <- getWord8 -- hash algo
        hlen <- getWord16be
        _ <- getLazyByteString (fromIntegral hlen)
        ulen <- getWord16be
        _ <- getLazyByteString (fromIntegral ulen)
        _ <- getWord16be -- left16
        algorithmFields <- getRemainingLazyByteString
        pure (toFVal pka, BL.toStrict algorithmFields)

conduitDecrypt ::
     (String -> IO BL.ByteString)
  -> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecrypt cb =
  void $
  DCD.conduitDecrypt
    DecryptOptions
      { decryptOptionsKeyResolution = DecryptWithoutPKESK
      , decryptOptionsPolicy = defaultDecryptPolicy
      , decryptOptionsPassphraseCallback = cb
      }

conduitDecryptWithPKESKContext ::
     (Pkt -> IO (Maybe PKESKRecipientKey))
  -> (String -> IO BL.ByteString)
  -> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecryptWithPKESKContext pkcb cb =
  void $
  DCD.conduitDecrypt
    DecryptOptions
      { decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback (asPKESKUnwrapCandidatesCallback pkcb)
      , decryptOptionsPolicy = defaultDecryptPolicy
      , decryptOptionsPassphraseCallback = cb
      }

conduitDecryptWithDecryptPolicy ::
     DecryptPolicy
  -> (Pkt -> IO (Maybe PKESKRecipientKey))
  -> (String -> IO BL.ByteString)
  -> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecryptWithDecryptPolicy dp pkcb cb =
  void $
  DCD.conduitDecrypt
    DecryptOptions
      { decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback (asPKESKUnwrapCandidatesCallback pkcb)
      , decryptOptionsPolicy = dp
      , decryptOptionsPassphraseCallback = cb
      }

conduitDecryptChecked ::
     (String -> IO BL.ByteString)
  -> DC.ConduitT Pkt Pkt (ResourceT IO) DecryptOutcome
conduitDecryptChecked cb =
  DCD.conduitDecrypt
    DecryptOptions
      { decryptOptionsKeyResolution = DecryptWithoutPKESK
      , decryptOptionsPolicy = defaultDecryptPolicy
      , decryptOptionsPassphraseCallback = cb
      }

conduitDecryptCheckedWithDecryptPolicy ::
     DecryptPolicy
  -> (Pkt -> IO (Maybe PKESKRecipientKey))
  -> (String -> IO BL.ByteString)
  -> DC.ConduitT Pkt Pkt (ResourceT IO) DecryptOutcome
conduitDecryptCheckedWithDecryptPolicy dp pkcb cb =
  DCD.conduitDecrypt
    DecryptOptions
      { decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback (asPKESKUnwrapCandidatesCallback pkcb)
      , decryptOptionsPolicy = dp
      , decryptOptionsPassphraseCallback = cb
      }

conduitDecryptWithCandidatesCallbackAndPolicy ::
     DecryptPolicy
  -> (KeyIdentifier -> PubKeyAlgorithm -> IO [PKESKRecipientKey])
  -> (String -> IO BL.ByteString)
  -> DC.ConduitT Pkt Pkt (ResourceT IO) ()
conduitDecryptWithCandidatesCallbackAndPolicy dp candCb cb =
  void $
  DCD.conduitDecrypt
    DecryptOptions
      { decryptOptionsKeyResolution = DecryptWithUnwrapCandidatesCallback candCb
      , decryptOptionsPolicy = dp
      , decryptOptionsPassphraseCallback = cb
      }

asPKESKUnwrapCandidatesCallback ::
     (Pkt -> IO (Maybe PKESKRecipientKey))
  -> KeyIdentifier
  -> PubKeyAlgorithm
  -> IO [PKESKRecipientKey]
asPKESKUnwrapCandidatesCallback pkcb keyIdentifier pka = do
  mk <- pkcb (pkeskProbePacket keyIdentifier pka)
  pure (maybe [] (: []) mk)

pkeskProbePacket :: KeyIdentifier -> PubKeyAlgorithm -> Pkt
pkeskProbePacket keyIdentifier pka =
  case keyIdentifier of
    KeyIdentifierWildcard ->
      PKESKPkt
        (PKESKPayloadV3Packet
           (PKESKPayloadV3 3 (EightOctetKeyId (BL.replicate 8 0)) pka (MPI 0 :| [])))
    KeyIdentifierEightOctet rid ->
      PKESKPkt
        (PKESKPayloadV3Packet
           (PKESKPayloadV3 3 rid pka (MPI 0 :| [])))
    KeyIdentifierFingerprint rid ->
      PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 (unFingerprint rid) pka mempty))

readFixturePayload :: FilePath -> IO BL.ByteString
readFixturePayload fpr = do
  bs <- BL.readFile ("tests/data/" ++ fpr)
  case dearmorIfAsciiArmored bs of
    Left err -> assertFailure ("ASCII armor decode failed for " ++ fpr ++ ": " ++ err) >> pure mempty
    Right (_, payload) -> pure payload

testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized :: Assertion
testParsedRSASecretKeyPKCS15DecryptNotMessageNotRecognized = do
  secretPackets <-
    DC.runConduitRes $
    CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume
  (publicKey, privateKey) <-
    case secretPackets of
      (SecretKeyPkt pkp ska:_) ->
        case (_pubkey pkp, ska) of
          (RSAPubKey (RSA_PublicKey pub), SUUnencrypted (RSAPrivateKey (RSA_PrivateKey prv)) _) ->
            pure (pub, prv)
          _ ->
            assertFailure "unencrypted.seckey did not contain a parseable unencrypted RSA key pair" >>
            fail "expected RSA key pair from parsed secret key packet"
      _ ->
        assertFailure "unencrypted.seckey did not begin with a secret key packet" >>
        fail "expected secret key packet"
  let plaintext = "pkcs1-v1.5 regression payload" :: B.ByteString
  encrypted <- (P15.encrypt publicKey plaintext :: IO (Either RSA.Error B.ByteString))
  ciphertext <-
    case encrypted of
      Left err ->
        assertFailure ("RSA PKCS#1 v1.5 encryption failed: " ++ show err) >> pure mempty
      Right ct -> pure ct
  decrypted <- (P15.decryptSafer privateKey ciphertext :: IO (Either RSA.Error B.ByteString))
  case decrypted of
    Left RSA.MessageNotRecognized ->
      assertFailure "parsed RSA private key decryption failed with MessageNotRecognized"
    Left err ->
      assertFailure ("parsed RSA private key decryption failed: " ++ show err)
    Right got ->
      assertEqual
        "parsed RSA private key decrypts PKCS#1 v1.5 payload"
        plaintext
        got

testEncodeOpenPGPSessionMaterial :: Assertion
testEncodeOpenPGPSessionMaterial = do
  let keyBytes = B.pack [1 .. 32]
      expected =
        B.singleton (fromFVal AES256) <> keyBytes <> encodeChecksum16 keyBytes
  case encodeOpenPGPSessionMaterial AES256 (SessionKey keyBytes) of
    Left err ->
      assertFailure ("encodeOpenPGPSessionMaterial failed: " ++ show err)
    Right encoded ->
      assertEqual "OpenPGP session material encoding" expected encoded

mkPKESKSessionMaterialOrFail ::
     SymmetricAlgorithm -> SessionKey -> IO PKESKSessionMaterial
mkPKESKSessionMaterialOrFail symalgo sessionKey =
  case mkPKESKSessionMaterial symalgo sessionKey of
    Left err ->
      assertFailure ("mkPKESKSessionMaterial failed: " ++ show err) >>
      fail "mkPKESKSessionMaterial failed"
    Right material -> pure material

armorPayload :: Armor -> BL.ByteString
armorPayload (Armor _ _ bs) = BL.fromStrict (BLC8.toStrict bs)
armorPayload (ClearSigned _ _ inner) = armorPayload inner

selectRecipientKeyInfo :: Pkt -> [PKESKRecipientKey] -> Maybe PKESKRecipientKey
selectRecipientKeyInfo (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _))) keyInfos =
  listToMaybe
    [ keyInfo
    | keyInfo <- keyInfos
    , supportsPKESKAlgorithm pka keyInfo
    , matchesRecipientIdentifier rid keyInfo
    ]
selectRecipientKeyInfo _ keyInfos = listToMaybe keyInfos

supportsPKESKAlgorithm :: PubKeyAlgorithm -> PKESKRecipientKey -> Bool
supportsPKESKAlgorithm pka keyInfo =
  case pkeskRecipientSKey keyInfo of
    RSAPrivateKey {} -> pka == RSA
    ECDHPrivateKey {} -> pka == ECDH || pka == X25519
    X25519PrivateKey {} -> pka == X25519
    X448PrivateKey {} -> pka == X448
    _ -> False

matchesRecipientIdentifier :: BL.ByteString -> PKESKRecipientKey -> Bool
matchesRecipientIdentifier rid keyInfo =
  case pkeskRecipientPKPayload keyInfo of
    Nothing -> False
    Just pkp ->
      let fingerprintBytes = BL.toStrict (unFingerprint (fingerprint pkp))
          identifier = BL.toStrict rid
       in identifier == fingerprintBytes ||
          identifier == B.cons 0x04 fingerprintBytes ||
          identifier == B.cons 0x06 fingerprintBytes

buildECDHKDFParamForTest ::
     SomePKPayload
  -> PubKeyAlgorithm
  -> ECCT.Curve
  -> HashAlgorithm
  -> SymmetricAlgorithm
  -> B.ByteString
buildECDHKDFParamForTest recipientPKP pka curve kdfHA kdfSA =
  B.singleton (fromIntegral (B.length curveOid)) <> curveOid <>
  B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA] <>
  "Anonymous Sender    " <>
  BL.toStrict (unFingerprint (fingerprint recipientPKP))
  where
    curveOid = either (const B.empty) id (curveToCurveoidBS (curveFromCurve curve))

buildCurve25519LegacyKdfParamForTest ::
     SomePKPayload
  -> PubKeyAlgorithm
  -> HashAlgorithm
  -> SymmetricAlgorithm
  -> B.ByteString
buildCurve25519LegacyKdfParamForTest recipientPKP pka kdfHA kdfSA =
  B.singleton (fromIntegral (B.length curveOid)) <> curveOid <>
  B.pack [fromFVal pka, 0x03, 0x01, fromFVal kdfHA, fromFVal kdfSA] <>
  "Anonymous Sender    " <>
  BL.toStrict (unFingerprint (fingerprint recipientPKP))
  where
    curveOid = "\x2b\x06\x01\x04\x01\x97\x55\x01\x05\x01"

deriveECDHKekForTest ::
     HashAlgorithm
  -> SymmetricAlgorithm
  -> B.ByteString
  -> B.ByteString
  -> B.ByteString
deriveECDHKekForTest kdfHA kdfSA sharedSecret kdfParam =
  B.take (keyLengthForTest kdfSA) digest
  where
    digest =
      case kdfHA of
        SHA256 -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA256)
        SHA384 -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA384)
        SHA512 -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA512)
        _ -> BA.convert (CH.hash (B.pack [0, 0, 0, 1] <> sharedSecret <> kdfParam) :: CH.Digest CHA.SHA256)

deriveX448KekForTest ::
     B.ByteString
  -> B.ByteString
  -> B.ByteString
  -> B.ByteString
deriveX448KekForTest ephemeralPublic recipientPublic sharedSecret =
  let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
      prk = extract @CHA.SHA512 B.empty ikm
      info = "OpenPGP X448" :: B.ByteString
   in expand @CHA.SHA512 prk info 32

deriveX25519KekForTest ::
     B.ByteString
  -> B.ByteString
  -> B.ByteString
  -> B.ByteString
deriveX25519KekForTest ephemeralPublic recipientPublic sharedSecret =
  let ikm = ephemeralPublic <> recipientPublic <> sharedSecret
      prk = extract @CHA.SHA256 B.empty ikm
      info = "OpenPGP X25519" :: B.ByteString
   in expand @CHA.SHA256 prk info 16

keyLengthForTest :: SymmetricAlgorithm -> Int
keyLengthForTest AES128 = 16
keyLengthForTest AES192 = 24
keyLengthForTest AES256 = 32
keyLengthForTest _ = 16

aesKeyWrapRFC3394ForTest ::
     SymmetricAlgorithm -> B.ByteString -> B.ByteString -> B.ByteString
aesKeyWrapRFC3394ForTest sa kek plain =
  case sa of
    AES128 -> wrapWithCipher (initCipher kek :: AES.AES128) plain
    AES192 -> wrapWithCipher (initCipher kek :: AES.AES192) plain
    AES256 -> wrapWithCipher (initCipher kek :: AES.AES256) plain
    _ -> error "unsupported KEK algorithm in test"
  where
    initCipher keyBytes =
      case CE.eitherCryptoError (CCT.cipherInit keyBytes) of
        Left err -> error ("cipher init failed: " ++ show err)
        Right c -> c
    wrapWithCipher cipher plainBytes =
      let rs = chunksOf8ForTest plainBytes
          n = length rs
          a0 = B.replicate 8 0xA6
          (aFinal, rFinal) = foldl (\(a, r) j -> wrapRound cipher n j a r) (a0, rs) [0 .. 5]
       in aFinal <> B.concat rFinal
    wrapRound cipher n j a rs = foldl step (a, rs) [1 .. n]
      where
        step (aCurr, rCurr) i =
          let b = CCT.ecbEncrypt cipher (aCurr <> (rCurr !! (i - 1)))
              (aMsb, rLsb) = B.splitAt 8 b
              t = fromIntegral (n * j + i) :: Word64
              aNext = xorBSForTest aMsb (encodeWord64beForTest t)
           in (aNext, replaceAtForTest (i - 1) rLsb rCurr)

encodeWord64beForTest :: Word64 -> B.ByteString
encodeWord64beForTest = BL.toStrict . runPut . putWord64be

chunksOf8ForTest :: B.ByteString -> [B.ByteString]
chunksOf8ForTest bs
  | B.null bs = []
  | otherwise =
      let (h, t) = B.splitAt 8 bs
       in h : chunksOf8ForTest t

replaceAtForTest :: Int -> a -> [a] -> [a]
replaceAtForTest idx x xs =
  let (prefix, suffix) = splitAt idx xs
   in case suffix of
        [] -> xs
        (_:rest) -> prefix <> (x : rest)

xorBSForTest :: B.ByteString -> B.ByteString -> B.ByteString
xorBSForTest a b = B.pack (B.zipWith xor a b)

encodeChecksum16 :: B.ByteString -> B.ByteString
encodeChecksum16 bs =
  B.pack
    [fromIntegral (s `div` 256), fromIntegral (s `mod` 256)]
  where
    s = B.foldl' (\acc octet -> (acc + fromIntegral octet) `mod` (65536 :: Int)) 0 bs

verifyMessageFromPackets :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromPackets keyring signedMessage =
  map (either (Left . renderVerificationError) Right) $
  verifyMessagePackets
    defaultVerificationOptions
      { verificationPolicy = VerifyInformational
      , verificationMode = VerificationStreaming
      }
    keyring
    (concatMap (either (const []) id . decompressPkt) (parsePkts signedMessage))

verifyMessageFromPacketsBatch :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromPacketsBatch keyring signedMessage =
  map (either (Left . renderVerificationError) Right) $
  verifyMessagePackets
    defaultVerificationOptions
      { verificationPolicy = VerifyInformational
      , verificationMode = VerificationBatch
      }
    keyring
    (concatMap (either (const []) id . decompressPkt) (parsePkts signedMessage))

verifyMessageFromBytestring :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromBytestring keyring signedMessage =
  map (either (Left . renderVerificationError) Right) $
  verifyMessage
    defaultVerificationOptions
      { verificationPolicy = VerifyInformational
      , verificationMode = VerificationStreaming
      }
    keyring
    signedMessage

verifyMessageFromBytestringBatch :: PublicKeyring -> BL.ByteString -> [Either String Verification]
verifyMessageFromBytestringBatch keyring signedMessage =
  map (either (Left . renderVerificationError) Right) $
  verifyMessage
    defaultVerificationOptions
      { verificationPolicy = VerifyInformational
      , verificationMode = VerificationBatch
      }
    keyring
    signedMessage

assertMessageVerification ::
     (PublicKeyring -> BL.ByteString -> [Either String Verification])
  -> FilePath
  -> FilePath
  -> [Fingerprint]
  -> Assertion
assertMessageVerification verifier keyringFile messageFile issuers = do
  kr <- loadKeyring keyringFile
  signedMessage <- readFixtureLazy messageFile
  let verification = verifier kr signedMessage
      actual = map (fmap (fingerprint . _verificationSigner)) verification
  assertEqual
    (keyringFile ++ " for " ++ messageFile)
    (map Right issuers)
    actual

loadKeyring :: FilePath -> IO PublicKeyring
loadKeyring keyring =
  DC.runConduitRes $
  CB.sourceFile (fixturePath keyring) DC..| conduitGet get DC..| conduitToPublicViewTKs DC..|
  sinkPublicKeyringMap

loadAndDecompressPkts :: FilePath -> IO [Pkt]
loadAndDecompressPkts = readFixtureDecompressedPackets

loadArmor :: FilePath -> IO [Armor]
loadArmor file = do
  armored <- readFixtureLazy file
  case AA.decodeLazy armored :: Either String [Armor] of
    Left err ->
      assertFailure ("Failed to decode armored fixture " ++ file ++ ": " ++ err) >> pure []
    Right armors -> pure armors

encryptMessageDefault ::
     SessionMaterialExposure
  -> Passphrase
  -> ClearPayload
  -> Either
       MessageError
       (EncryptedPayload, Maybe RecoveredSessionMaterial)
encryptMessageDefault exposure passphrase payload =
  encryptMessage
    (RFC9580EncryptMessageOptions
       { rfc9580EncryptMessageExposure = exposure
       , rfc9580EncryptMessageSymmetricAlgorithm = AES256
       , rfc9580EncryptMessageS2K = Argon2 (Salt16 (B.pack [0x80 .. 0x8f])) 1 4 15
       , rfc9580EncryptMessageIV = IV "0123456789ABCDEF"
       })
    passphrase
    payload

mkTestKeyring :: [TKUnknown] -> PublicKeyring
mkTestKeyring tks =
  let someTKs = [stk | Right stk <- map fromUnknownToTK tks]
  in fst (partitionSomeTKs someTKs)

addTimestampSeconds :: ThirtyTwoBitTimeStamp -> Word32 -> ThirtyTwoBitTimeStamp
addTimestampSeconds (ThirtyTwoBitTimeStamp ts) seconds = ThirtyTwoBitTimeStamp (ts + seconds)

timestampToUTCTime :: ThirtyTwoBitTimeStamp -> UTCTime
timestampToUTCTime = posixSecondsToUTCTime . realToFrac . unThirtyTwoBitTimeStamp

signCertificationAt ::
     SomePKPayload
  -> RSA.PrivateKey
  -> UserId
  -> ThirtyTwoBitTimeStamp
  -> [SigSubPacket]
  -> IO SignaturePayload
signCertificationAt signer signingKey uid creationTime hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signCertificationWithRSA
         GenericCert
         signer
         uid
         (hashedExtras ++ hashed)
         unhashed
         signingKey of
    Left err ->
      assertFailure ("failed to sign certification: " ++ renderSignError err) >>
      fail "expected certification signature"
    Right sigPayload -> pure sigPayload

signCertificationRevocationAt ::
     SomePKPayload
  -> RSA.PrivateKey
  -> UserId
  -> ThirtyTwoBitTimeStamp
  -> [SigSubPacket]
  -> IO SignaturePayload
signCertificationRevocationAt signer signingKey uid creationTime hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signCertRevocationWithRSA
         signer
         uid
         (hashedExtras ++ hashed)
         unhashed
         signingKey of
    Left err ->
      assertFailure ("failed to sign certification revocation: " ++ renderSignError err) >>
      fail "expected certification revocation signature"
    Right sigPayload -> pure sigPayload

messageIssuerSubpacketsAt ::
     SomePKPayload
  -> ThirtyTwoBitTimeStamp
  -> IO ([SigSubPacket], [SigSubPacket])
messageIssuerSubpacketsAt signer creationTime =
  case eightOctetKeyID signer of
    Left err ->
      assertFailure ("failed to derive issuer key id for timeline test: " ++ err) >>
      fail "expected issuer key id"
    Right issuerKeyId ->
      pure
        ( [ SigSubPacket False (SigCreationTime creationTime)
          , SigSubPacket False (IssuerFingerprint IssuerFingerprintV4 (fingerprint signer))
          ]
        , [SigSubPacket False (Issuer issuerKeyId)]
        )

loadUnencryptedRsaSigner :: IO (SomePKPayload, RSA.PrivateKey)
loadUnencryptedRsaSigner = do
  secretPackets <-
    DC.runConduitRes $
    CB.sourceFile "tests/data/unencrypted.seckey" DC..| conduitGet get DC..| CL.consume
  case secretPackets of
    (SecretKeyPkt pkp ska:_) ->
      case ska of
        SUUnencrypted (RSAPrivateKey (RSA_PrivateKey privateKey)) _ ->
          pure (pkp, privateKey)
        _ ->
          assertFailure "unencrypted.seckey did not contain an unencrypted RSA key" >>
          fail "expected decrypted RSA private key"
    _ ->
      assertFailure "unencrypted.seckey did not begin with a secret key packet" >>
      fail "expected secret key packet"

loadUnencryptedRsaSignerV6 :: IO (SomePKPayload, RSA.PrivateKey)
loadUnencryptedRsaSignerV6 = do
  (signer, signingKey) <- loadUnencryptedRsaSigner
  pure (setKeyVersion V6 signer, signingKey)

setKeyVersion :: KeyVersion -> SomePKPayload -> SomePKPayload
setKeyVersion keyVersion (PKPayload _ ts v3e pka pubkey) =
  PKPayload keyVersion ts v3e pka pubkey

setKeyTimestamp :: ThirtyTwoBitTimeStamp -> SomePKPayload -> SomePKPayload
setKeyTimestamp ts (PKPayload keyVersion _ v3e pka pubkey) =
  PKPayload keyVersion ts v3e pka pubkey

setPKAlgorithm :: PubKeyAlgorithm -> SomePKPayload -> SomePKPayload
setPKAlgorithm algorithm (PKPayload keyVersion ts v3e _ pubkey) =
  PKPayload keyVersion ts v3e algorithm pubkey

expectV4PKPayload :: String -> SomePKPayload -> IO (VersionedPKPayload V4)
expectV4PKPayload label pk =
  case asV4PKPayload pk of
    Left err ->
      assertFailure (label ++ " should have a v4 PKPayload: " ++ err) >>
      fail "expected v4 PKPayload"
    Right v4pk -> pure v4pk

expectV6PKPayload :: String -> SomePKPayload -> IO (VersionedPKPayload V6)
expectV6PKPayload label pk =
  case asV6PKPayload pk of
    Left err ->
      assertFailure (label ++ " should have a v6 PKPayload: " ++ err) >>
      fail "expected v6 PKPayload"
    Right v6pk -> pure v6pk

loadDeterministicEd25519Signer :: IO (SomePKPayload, Ed25519.SecretKey)
loadDeterministicEd25519Signer = do
  let seed = B.pack [1 .. 32]
  secretKey <-
    case CE.eitherCryptoError (Ed25519.secretKey seed) of
      Left err ->
        assertFailure ("failed to initialize deterministic Ed25519 secret key: " ++ show err) >>
        fail "expected deterministic Ed25519 secret key"
      Right sk -> pure sk
  let publicKeyBytes = BA.convert (Ed25519.toPublic secretKey) :: B.ByteString
      signer =
        PKPayload
          V4
          0
          0
          EdDSA
          (EdDSAPubKey Ed25519 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 publicKeyBytes)))))
  pure (signer, secretKey)

loadDeterministicEd25519SignerV6 :: IO (SomePKPayload, Ed25519.SecretKey)
loadDeterministicEd25519SignerV6 = do
  let seed = B.pack [1 .. 32]
  secretKey <-
    case CE.eitherCryptoError (Ed25519.secretKey seed) of
      Left err ->
        assertFailure ("failed to initialize deterministic Ed25519 secret key: " ++ show err) >>
        fail "expected deterministic Ed25519 secret key"
      Right sk -> pure sk
  let publicKeyBytes = BA.convert (Ed25519.toPublic secretKey) :: B.ByteString
      signer =
        PKPayload
          V6
          0
          0
          EdDSA
          (EdDSAPubKey Ed25519 (NativeEPoint (EPoint (os2ip publicKeyBytes))))
  pure (signer, secretKey)

loadDeterministicEd448Signer :: IO (SomePKPayload, Ed448.SecretKey)
loadDeterministicEd448Signer = do
  let seed = B.pack [1 .. 57]
  secretKey <-
    case CE.eitherCryptoError (Ed448.secretKey seed) of
      Left err ->
        assertFailure ("failed to initialize deterministic Ed448 secret key: " ++ show err) >>
        fail "expected deterministic Ed448 secret key"
      Right sk -> pure sk
  let publicKeyBytes = BA.convert (Ed448.toPublic secretKey) :: B.ByteString
      signer =
        PKPayload
          V4
          0
          0
          EdDSA
          (EdDSAPubKey Ed448 (PrefixedNativeEPoint (EPoint (os2ip (B.cons 0x40 publicKeyBytes)))))
  pure (signer, secretKey)

loadDeterministicEd448SignerV6 :: IO (SomePKPayload, Ed448.SecretKey)
loadDeterministicEd448SignerV6 = do
  let seed = B.pack [1 .. 57]
  secretKey <-
    case CE.eitherCryptoError (Ed448.secretKey seed) of
      Left err ->
        assertFailure ("failed to initialize deterministic Ed448 secret key: " ++ show err) >>
        fail "expected deterministic Ed448 secret key"
      Right sk -> pure sk
  let publicKeyBytes = BA.convert (Ed448.toPublic secretKey) :: B.ByteString
      signer =
        PKPayload
          V6
          0
          0
          EdDSA
          (EdDSAPubKey Ed448 (NativeEPoint (EPoint (os2ip publicKeyBytes))))
  pure (signer, secretKey)

testSEIPDv2ForV4KeyArmor :: Assertion
testSEIPDv2ForV4KeyArmor = do
  armors <- loadArmor "seipdv2-for-v4-key.pgp.aa"
  armor <-
    case armors of
      [a] -> pure a
      _ ->
        assertFailure "seipdv2-for-v4-key fixture should contain one armored payload" >>
        fail "expected one armored payload"
  payload <-
    case armor of
      Armor ArmorMessage _ p -> pure p
      Armor atype _ _ ->
        assertFailure
          ("seipdv2-for-v4-key fixture should decode as a message block, got " ++
           show atype) >>
        fail "expected message block"
      _ ->
        assertFailure "seipdv2-for-v4-key fixture should decode as an armored payload" >>
        fail "expected armored payload"
  let packets = parsePkts (BL.fromStrict (BLC8.toStrict payload))
  case packets of
    [PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _)), SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize _ _)] -> do
      let ridHex = map toUpper (BLC8.unpack (B16L.encode rid))
      if ridHex `elem` ["C8263FC6D676044B6E973959C2F2C2CAE30DE908", "04C8263FC6D676044B6E973959C2F2C2CAE30DE908"]
        then pure ()
        else
          assertFailure
            ("seipdv2-for-v4-key fixture should target the expected recipient fingerprint, got " ++
             ridHex)
      assertEqual
        "seipdv2-for-v4-key fixture should use ECDH PKESKv6"
        ECDH
        pka
      assertEqual
        "seipdv2-for-v4-key fixture should use AES-256"
        AES256
        sa
      assertEqual
        "seipdv2-for-v4-key fixture should use OCB"
        OCB
        aa
      assertEqual
        "seipdv2-for-v4-key fixture should use 4KiB chunks"
        6
        chunkSize
    _ ->
      assertFailure
        ("seipdv2-for-v4-key fixture should contain [PKESKPkt (PKESK6 ...), SymEncIntegrityProtectedDataPkt (SEIPD2 ...)], got: " ++
         show packets)

loadSEIPDv2FixtureWithV4Secret :: FilePath -> IO ([Pkt], [Pkt], BL.ByteString)
loadSEIPDv2FixtureWithV4Secret fixture = do
  messageArmor <- loadFirstArmor fixture
  encryptedSecretArmor <- loadFirstArmor "v4-encrypted-secret.pgp.aa"
  passphrase <- readPKIPassphrase
  pure
    ( parsePkts (armorPayload messageArmor)
    , parsePkts (armorPayload encryptedSecretArmor)
    , passphrase
    )

forceVersionedRecipientIdentifier :: Pkt -> Pkt
forceVersionedRecipientIdentifier pkt =
  case pkt of
    PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka esk))
      | BL.length rid == 20 ->
          PKESKPkt
            (PKESKPayloadV6Packet (PKESKPayloadV6 (BL.cons 0x04 rid) pka esk))
      | BL.length rid == 32 ->
          PKESKPkt
            (PKESKPayloadV6Packet (PKESKPayloadV6 (BL.cons 0x06 rid) pka esk))
      | otherwise -> pkt
    _ -> pkt

selectRecipientKeyInfoByRawRecipientId :: Pkt -> [PKESKRecipientKey] -> Maybe PKESKRecipientKey
selectRecipientKeyInfoByRawRecipientId (PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _))) keyInfos =
  listToMaybe
    [ keyInfo
    | keyInfo <- keyInfos
    , supportsPKESKAlgorithm pka keyInfo
    , matchesRawRecipientFingerprint rid keyInfo
    ]
selectRecipientKeyInfoByRawRecipientId _ keyInfos = listToMaybe keyInfos

matchesRawRecipientFingerprint :: BL.ByteString -> PKESKRecipientKey -> Bool
matchesRawRecipientFingerprint rid keyInfo =
  case pkeskRecipientPKPayload keyInfo of
    Nothing -> False
    Just pkp ->
      BL.toStrict rid == BL.toStrict (unFingerprint (fingerprint pkp))

testSEIPDv2TwoRecipientsArmor :: Assertion
testSEIPDv2TwoRecipientsArmor =
  testSEIPDv2RecipientFixtureArmor "seipdv2-two-recipients.pgp.aa" 2

testSEIPDv2ThreeRecipientsArmor :: Assertion
testSEIPDv2ThreeRecipientsArmor =
  testSEIPDv2RecipientFixtureArmor "seipdv2-three-recipients.pgp.aa" 3

testSEIPDv2RecipientFixtureArmor :: FilePath -> Int -> Assertion
testSEIPDv2RecipientFixtureArmor file expectedRecipients = do
  armors <- loadArmor file
  armor <-
    case armors of
      [a] -> pure a
      _ ->
        assertFailure (file ++ " fixture should contain one armored payload") >>
        fail "expected one armored payload"
  payload <-
    case armor of
      Armor ArmorMessage _ p -> pure p
      Armor atype _ _ ->
        assertFailure
          (file ++ " fixture should decode as a message block, got " ++ show atype) >>
        fail "expected message block"
      _ ->
        assertFailure (file ++ " fixture should decode as an armored payload") >>
        fail "expected armored payload"
  let packets = parsePkts (BL.fromStrict (BLC8.toStrict payload))
      pkesks = [(rid, pka) | PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 rid pka _)) <- packets]
      x25519Esks = [BL.toStrict esk | PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ X25519 esk)) <- packets]
      x448Esks = [BL.toStrict esk | PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 _ X448 esk)) <- packets]
      seipdv2Packets =
        [(sa, aa, chunkSize) | SymEncIntegrityProtectedDataPkt (SEIPD2 sa aa chunkSize _ _) <- packets]
  assertEqual
    (file ++ " fixture should contain expected number of PKESKv6 packets")
    expectedRecipients
    (length pkesks)
  if all (\(_, pka) -> pka == ECDH || pka == X25519) pkesks
    then pure ()
    else
      assertFailure
        (file ++ " fixture should use only ECDH/X25519 PKESKv6 packets, got " ++
         show (map snd pkesks))
  if any ((== ECDH) . snd) pkesks
    then pure ()
    else
      assertFailure (file ++ " fixture should include an ECDH recipient for the v4 key")
  mapM_ (assertX25519EskShape file) x25519Esks
  mapM_ (assertX448EskShape file) x448Esks
  if all
       (\(rid, _) ->
          let l = BL.length rid
           in l == 20 || l == 21 || l == 32 || l == 33)
       pkesks
    then pure ()
    else
      assertFailure
        (file ++ " fixture should use 20/21-byte or 32/33-byte recipient identifiers")
  case seipdv2Packets of
    [(sa, aa, chunkSize)] -> do
      assertEqual (file ++ " fixture should use AES-256") AES256 sa
      assertEqual (file ++ " fixture should use OCB") OCB aa
      assertEqual (file ++ " fixture should use 4KiB chunks") 6 chunkSize
    _ ->
      assertFailure
        (file ++ " fixture should contain one SymEncIntegrityProtectedDataV2 packet")

assertX25519EskShape :: FilePath -> B.ByteString -> Assertion
assertX25519EskShape file x25519Esk
  | B.length x25519Esk < 33 =
      assertFailure
        (file ++ " X25519 PKESKv6 ESK should contain 32-octet ephemeral and wrapped-len")
  | otherwise = do
      let wrappedLen = fromIntegral (B.index x25519Esk 32) :: Int
          wrapped = B.drop 33 x25519Esk
      assertEqual
        (file ++ " X25519 PKESKv6 wrapped length octet")
        wrappedLen
        (B.length wrapped)

assertX448EskShape :: FilePath -> B.ByteString -> Assertion
assertX448EskShape file x448Esk
  | B.length x448Esk < 57 =
      assertFailure
        (file ++ " X448 PKESKv6 ESK should contain 56-octet ephemeral and wrapped-len")
  | otherwise = do
      let wrappedLen = fromIntegral (B.index x448Esk 56) :: Int
          wrapped = B.drop 57 x448Esk
      assertEqual
        (file ++ " X448 PKESKv6 wrapped length octet")
        wrappedLen
        (B.length wrapped)

prependUnusableLatestPKESK :: [Pkt] -> [Pkt]
prependUnusableLatestPKESK packets =
  let bogusRid = BL.pack (0x06 : replicate 32 0x99)
      bogusPKESK = PKESKPkt (PKESKPayloadV6Packet (PKESKPayloadV6 bogusRid RSA "bogus-esk"))
      (eskPrefix, encryptedSuffix) = span isPrecedingESK packets
   in eskPrefix ++ [bogusPKESK] ++ encryptedSuffix

reorderPrecedingPKESKs :: [Pkt] -> [Pkt]
reorderPrecedingPKESKs packets =
  let (eskPrefix, encryptedSuffix) = span isPrecedingESK packets
      reorderedPKESKs = reverse [pkt | pkt@(PKESKPkt _) <- eskPrefix]
   in refillPKESKSlots eskPrefix reorderedPKESKs ++ encryptedSuffix
  where
    refillPKESKSlots [] _ = []
    refillPKESKSlots (pkt:rest) pkesks =
      case pkt of
        PKESKPkt {} ->
          case pkesks of
            [] -> pkt : refillPKESKSlots rest []
            (replacement:remaining) ->
              replacement : refillPKESKSlots rest remaining
        _ -> pkt : refillPKESKSlots rest pkesks

isPrecedingESK :: Pkt -> Bool
isPrecedingESK (PKESKPkt _) = True
isPrecedingESK (SKESKPkt _) = True
isPrecedingESK _ = False

messageVerificationFixtures :: [(String, FilePath, FilePath, [Fingerprint])]
messageVerificationFixtures =
  [ ( "uncompressed-ops-dsa"
    , "pubring.gpg"
    , "uncompressed-ops-dsa.gpg"
    , [fp "1EB2 0B2F 5A5C C3BE AFD6  E5CB 7732 CF98 8A63 EA86"])
  , ( "uncompressed-ops-dsa-sha384"
    , "pubring.gpg"
    , "uncompressed-ops-dsa-sha384.txt.gpg"
    , [fp "1EB2 0B2F 5A5C C3BE AFD6  E5CB 7732 CF98 8A63 EA86"])
  , ( "uncompressed-ops-rsa"
    , "pubring.gpg"
    , "uncompressed-ops-rsa.gpg"
    , [fp "CB79 3345 9F59 C70D F1C3  FBEE DEDC 3ECF 689A F56D"])
  , ( "compressedsig"
    , "pubring.gpg"
    , "compressedsig.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "compressedsig-zlib"
    , "pubring.gpg"
    , "compressedsig-zlib.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "compressedsig-bzip2"
    , "pubring.gpg"
    , "compressedsig-bzip2.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  ]

certificateVerificationFixtures :: [(String, FilePath, FilePath, [Fingerprint])]
certificateVerificationFixtures =
  [ ( "userid"
    , "pubring.gpg"
    , "minimized.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "subkey"
    , "pubring.gpg"
    , "subkey.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "primary key binding"
    , "signing-subkey.gpg"
    , "primary-binding.gpg"
    , [fp "ED1B D216 F70E 5D5F 4444  48F9 B830 F2C4 83A9 9AE5"])
  , ( "attribute"
    , "pubring.gpg"
    , "uat.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "primary key revocation"
    , "pubring.gpg"
    , "prikey-rev.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "subkey revocation"
    , "pubring.gpg"
    , "subkey-rev.gpg"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "6F87040E"
    , "pubring.gpg"
    , "6F87040E.pubkey"
    , [ fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"
      , fp "CB79 3345 9F59 C70D F1C3  FBEE DEDC 3ECF 689A F56D"
      , fp "AF95 E4D7 BAC5 21EE 9740  BED7 5E9F 1523 4132 62DC"
      ])
  , ( "6F87040E-cr"
    , "pubring.gpg"
    , "6F87040E-cr.pubkey"
    , [ fp "AF95 E4D7 BAC5 21EE 9740  BED7 5E9F 1523 4132 62DC"
      , fp "AF95 E4D7 BAC5 21EE 9740  BED7 5E9F 1523 4132 62DC"
      , fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"
      , fp "CB79 3345 9F59 C70D F1C3  FBEE DEDC 3ECF 689A F56D"
      , fp "AF95 E4D7 BAC5 21EE 9740  BED7 5E9F 1523 4132 62DC"
      ])
  , ( "simple RSA secret key"
    , "pubring.gpg"
    , "simple.seckey"
    , [fp "421F 28FE AAD2 22F8 56C8  FFD5 D4D5 4EA1 6F87 040E"])
  , ( "simple ECDSA public key"
    , "ecdsa-key-without-ecdh.pubkey"
    , "ecdsa-key-without-ecdh.pubkey"
    , [fp "174C CF12 C571 6D0E 527F  B50E F770 8BAD D606 3224"])
  ]

verificationFixtureGroup ::
     String
  -> (PublicKeyring -> BL.ByteString -> [Either String Verification])
  -> [(String, FilePath, FilePath, [Fingerprint])]
  -> TestTree
verificationFixtureGroup groupName verifier fixtures =
  testGroup
    groupName
    [ testCase name (assertMessageVerification verifier keyringFile messageFile issuers)
    | (name, keyringFile, messageFile, issuers) <- fixtures
    ]

loadV6UnencryptedSecretKeyFixtureForProperty :: IO (Either String (SomePKPayload, SKAddendum, SKey))
loadV6UnencryptedSecretKeyFixtureForProperty = do
  armored <- readFixtureLazy "v6-secret.pgp.aa"
  pure $ do
    armors <- first ("failed to decode v6 secret fixture: " ++) (AA.decodeLazy armored)
    armor <-
      case armors of
        (a:_) -> Right a
        [] -> Left "v6-secret.pgp.aa should contain one armored payload"
    let packets = parsePkts (armorPayload armor)
    (pkp, ska) <-
      case packets of
        (SecretKeyPkt pkpayload skaddendum:_) -> Right (pkpayload, skaddendum)
        _ -> Left "v6-secret.pgp.aa should begin with a secret key packet"
    skey <-
      case ska of
        SUUnencrypted x _ -> Right x
        _ -> Left "v6-secret.pgp.aa should contain unencrypted secret key material"
    Right (pkp, ska, skey)

loadV4EncryptedSecretKeyFixtureForProperty ::
     IO (Either String (SomePKPayload, SKAddendum, SKey, BL.ByteString))
loadV4EncryptedSecretKeyFixtureForProperty = do
  passphrase <- readPKIPassphrase
  packets <-
    DC.runConduitRes $
    CB.sourceFile "tests/data/aes256-sha512.seckey" DC..| conduitGet get DC..|
    CL.consume
  pure $ do
    (pkp, ska) <-
      case packets of
        (SecretKeyPkt pkpayload skaddendum:_) -> Right (pkpayload, skaddendum)
        _ -> Left "aes256-sha512.seckey should begin with a secret key packet"
    skey <-
      case decryptPrivateKey (pkp, ska) passphrase of
        Right (SUUnencrypted x _) -> Right x
        Right other ->
          Left ("unexpected decrypted key shape for v4 fixture: " ++ show other)
        Left err ->
          Left ("failed to decrypt v4 fixture secret key: " ++ err)
    Right (pkp, ska, skey, passphrase)

reverseIf :: Bool -> [a] -> [a]
reverseIf shouldReverse xs
  | shouldReverse = reverse xs
  | otherwise = xs

cgp :: DC.ConduitT B.ByteString Pkt (ResourceT IO) ()
cgp = conduitGet (get :: Get Pkt)

fp :: Text -> Fingerprint
fp = either error id . parseFingerprint

doPkeyAndSkeyMatch :: PKey -> SKey -> Assertion
doPkeyAndSkeyMatch (RSAPubKey (RSA_PublicKey rpub)) (RSAPrivateKey (RSA_PrivateKey rpriv)) =
  assertEqual
    "RSA private key matches RSA public key"
    rpub
    (RSA.private_pub rpriv)
doPkeyAndSkeyMatch (ECDSAPubKey (ECDSA_PublicKey ecpub)) (ECDSAPrivateKey (ECDSA_PrivateKey ecpriv)) =
  assertEqual
    "ECDSA private key curve matches ECDSA public key curve"
    (ECDSA.public_curve ecpub)
    (ECDSA.private_curve ecpriv)
doPkeyAndSkeyMatch _ _ = assertFailure "matching unimplemented"

collectSecretKeyInfos :: [Pkt] -> BL.ByteString -> IO [PKESKRecipientKey]
collectSecretKeyInfos pkts passphrase = do
  let keyInfoResults = map toKeyInfo pkts
      keyInfos = catMaybes [mKeyInfo | Right mKeyInfo <- keyInfoResults]
      unlockErrors = [err | Left err <- keyInfoResults]
  unless (null unlockErrors) $
    assertFailure
      ("one or more secret key packets failed to unlock (partial failures are surfaced to\
       \ prevent silent key-context gaps):\n" ++
       unlines unlockErrors)
  pure keyInfos
  where
    toKeyInfo (SecretKeyPkt pkp ska) = decryptToRecipientKey "SecretKeyPkt" pkp ska
    toKeyInfo (SecretSubkeyPkt pkp ska) = decryptToRecipientKey "SecretSubkeyPkt" pkp ska
    toKeyInfo _ = Right Nothing

    decryptToRecipientKey contextLabel pkp ska =
      case ska of
        SUUnencrypted skey _ ->
          Right
            (Just
               (PKESKRecipientKey
                  { pkeskRecipientPKPayload = Just pkp
                  , pkeskRecipientSKey = skey
                  }))
        _ ->
          case decryptPrivateKey (pkp, ska) passphrase of
            Right (SUUnencrypted skey _) ->
              Right
                (Just
                  (PKESKRecipientKey
                      { pkeskRecipientPKPayload = Just pkp
                      , pkeskRecipientSKey = skey
                      }))
            Right decryptedSKA ->
              Left
                (contextLabel ++
                 " " ++
                 show (fingerprint pkp) ++
                 ": decryptPrivateKey returned unexpected protection: " ++
                 show decryptedSKA)
            Left err ->
              Left
                (contextLabel ++
                 " " ++
                 show (fingerprint pkp) ++
                 ": decryptPrivateKey failed: " ++ err)

signSubkeyRevocationWithRSAAt ::
     SomePKPayload
  -> SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> IO SignaturePayload
signSubkeyRevocationWithRSAAt signer subkey signingKey creationTime = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signSubkeyRevocationWithRSA signer subkey hashed unhashed signingKey of
    Left err ->
      assertFailure ("failed to sign subkey revocation: " ++ renderSignError err) >>
      fail "expected subkey revocation signature"
    Right sigPayload -> pure sigPayload

signSubkeyBindingWithRSAAt ::
     SomePKPayload
  -> SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> IO SignaturePayload
signSubkeyBindingWithRSAAt signer subkey signingKey creationTime = do
  signSubkeyBindingWithRSAExtrasAt signer subkey signingKey creationTime []

signDirectKeyWithRSAExtrasAt ::
     SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> [SigSubPacket]
  -> IO SignaturePayload
signDirectKeyWithRSAExtrasAt signer signingKey creationTime hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signDirectKeyWithRSA SignatureDirectlyOnAKey signer (hashedExtras ++ hashed) unhashed signingKey of
    Left err ->
      assertFailure ("failed to sign direct key self-signature: " ++ renderSignError err) >>
      fail "expected direct key self-signature"
    Right sigPayload -> pure sigPayload

signSubkeyBindingWithRSAExtrasAt ::
     SomePKPayload
  -> SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> [SigSubPacket]
  -> IO SignaturePayload
signSubkeyBindingWithRSAExtrasAt signer subkey signingKey creationTime hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  let bindingPayload =
        payloadForSig
          SubkeyBindingSig
          emptyPSC
            { lastPrimaryKey = PublicKeyPkt signer
            , lastSubkey = PublicSubkeyPkt subkey
            }
  case signDataWithRSA SubkeyBindingSig signingKey (hashedExtras ++ hashed) unhashed bindingPayload of
    Left err ->
      assertFailure ("failed to sign subkey binding: " ++ renderSignError err) >>
      fail "expected subkey binding signature"
    Right sigPayload -> pure sigPayload

verifyTimelinePackets :: PublicKeyring -> BL.ByteString -> SignaturePayload -> [Either VerificationError Verification]
verifyTimelinePackets keyring payload sigPayload =
  verifyMessagePackets
    defaultVerificationOptions
      { verificationPolicy = VerifyStrict
      , verificationMode = VerificationBatch
      }
    keyring
    [LiteralDataPkt BinaryData BL.empty 0 payload, SignaturePkt sigPayload]

assertSingleSignerFingerprint ::
     String
  -> Fingerprint
  -> [Either VerificationError Verification]
  -> Assertion
assertSingleSignerFingerprint label expected results =
  case results of
    [Right verification] ->
      assertEqual label expected (fingerprint (_verificationSigner verification))
    other ->
      assertFailure
        (label ++
         ", expected one successful verification result, got " ++
         show (length other) ++
         " result(s)")

assertSingleFailureContainsTimeline ::
     String
  -> String
  -> [Either VerificationError Verification]
  -> Assertion
assertSingleFailureContainsTimeline label needle results =
  case results of
    [Left err] ->
      assertBool
        label
        (needle `isInfixOf` renderVerificationError err)
    other ->
      assertFailure
        (label ++
         ", expected one verification failure result, got " ++
         show (length other) ++
         " result(s)")

signCertificationWithEd25519At ::
     SomePKPayload
  -> SomePKPayload
  -> Ed25519.SecretKey
  -> UserId
  -> ThirtyTwoBitTimeStamp
  -> [SigSubPacket]
  -> IO SignaturePayload
signCertificationWithEd25519At certifiedSigner certifierSigner signingKey uid creationTime hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt certifierSigner creationTime
  let state =
        emptyPSC
          { lastPrimaryKey = PublicKeyPkt certifiedSigner
          , lastUIDorUAt = UserIdPkt (let UserId uidText = uid in uidText)
          }
  case signDataWithEd25519
         GenericCert
         signingKey
         (hashedExtras ++ hashed)
         unhashed
         (payloadForSig GenericCert state) of
    Left err ->
      assertFailure ("failed to sign Ed25519 certification: " ++ renderSignError err) >>
      fail "expected Ed25519 certification signature"
    Right sigPayload -> pure sigPayload

signCertificationRevocationWithEd25519At ::
     SomePKPayload
  -> SomePKPayload
  -> Ed25519.SecretKey
  -> UserId
  -> ThirtyTwoBitTimeStamp
  -> [SigSubPacket]
  -> IO SignaturePayload
signCertificationRevocationWithEd25519At certifiedSigner certifierSigner signingKey uid creationTime hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt certifierSigner creationTime
  let state =
        emptyPSC
          { lastPrimaryKey = PublicKeyPkt certifiedSigner
          , lastUIDorUAt = UserIdPkt (let UserId uidText = uid in uidText)
          }
  case signDataWithEd25519
         CertRevocationSig
         signingKey
         (hashedExtras ++ hashed)
         unhashed
         (payloadForSig CertRevocationSig state) of
    Left err ->
      assertFailure ("failed to sign Ed25519 certification revocation: " ++ renderSignError err) >>
      fail "expected Ed25519 certification revocation signature"
    Right sigPayload -> pure sigPayload

signKeyRevocationWithReasonAt ::
     SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> RevocationCode
  -> IO SignaturePayload
signKeyRevocationWithReasonAt signer signingKey creationTime reasonCode =
  signKeyRevocationWithReasonAndExtrasAt
    signer
    signingKey
    creationTime
    reasonCode
    []

signKeyRevocationWithReasonAndExtrasAt ::
     SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> RevocationCode
  -> [SigSubPacket]
  -> IO SignaturePayload
signKeyRevocationWithReasonAndExtrasAt signer signingKey creationTime reasonCode hashedExtras = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signKeyRevocationWithRSA
         signer
         (SigSubPacket False (ReasonForRevocation reasonCode "") : hashedExtras ++ hashed)
         unhashed
         signingKey of
    Left err ->
      assertFailure ("failed to sign key revocation: " ++ renderSignError err) >>
      fail "expected key revocation signature"
    Right sigPayload -> pure sigPayload

signBinaryMessageWithRSAAt ::
     SomePKPayload
  -> RSA.PrivateKey
  -> ThirtyTwoBitTimeStamp
  -> BL.ByteString
  -> IO SignaturePayload
signBinaryMessageWithRSAAt signer signingKey creationTime payload = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signDataWithRSA BinarySig signingKey hashed unhashed payload of
    Left err ->
      assertFailure ("failed to sign RSA message payload: " ++ renderSignError err) >>
      fail "expected RSA message signature"
    Right sigPayload -> pure sigPayload

signBinaryMessageWithEd25519At ::
     SomePKPayload
  -> Ed25519.SecretKey
  -> ThirtyTwoBitTimeStamp
  -> BL.ByteString
  -> IO SignaturePayload
signBinaryMessageWithEd25519At signer signingKey creationTime payload = do
  (hashed, unhashed) <- messageIssuerSubpacketsAt signer creationTime
  case signDataWithEd25519 BinarySig signingKey hashed unhashed payload of
    Left err ->
      assertFailure ("failed to sign Ed25519 message payload: " ++ renderSignError err) >>
      fail "expected Ed25519 message signature"
    Right sigPayload -> pure sigPayload